docs-site: implement tabs

This commit is contained in:
Glen Huang
2025-10-16 21:25:43 +08:00
parent f9fe1b8913
commit b899f95cf6
9 changed files with 214 additions and 13 deletions

View File

@@ -3,6 +3,9 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<script>
document.documentElement.classList.add("js");
</script>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">

View File

@@ -1,5 +1,6 @@
@import url("./shiki.css"); @import url("./shiki.css");
@import url("./admonition.css"); @import url("./admonition.css");
@import url("./tabs.css");
code { code {
font-family: font-family:

View File

@@ -0,0 +1,50 @@
.md-tabs-bar {
display: none;
gap: 7px;
align-items: flex-end;
}
.md-tabs-tab {
padding: 8px 0;
}
.md-tabs-container {
margin: 20px 0;
}
.js {
.md-tabs-bar {
display: flex;
}
.md-tabs-container {
margin: 0;
> .md-tabs-tab {
display: none;
}
}
.md-tabs {
margin: 20px 0;
}
.md-tabs-tab {
background: #d7dadf;
padding: 8px 18px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
cursor: pointer;
&.is-active {
background: #eff1f5;
.md-tabs.is-singleton & {
padding: 8px 16px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
flex: 1;
border-bottom: 1px solid #d8dbe1;
}
}
}
.md-tabs-content {
display: none;
margin: 0 var(--pageMargin);
&.is-active {
display: block;
}
}
}

View File

@@ -17,6 +17,7 @@ import rehypeTocSlug from "./rehype-toc-slug";
import transformerLineNumbers from "./shiki-transformer-line-numbers"; import transformerLineNumbers from "./shiki-transformer-line-numbers";
import remarkParse from "./remark-parse"; import remarkParse from "./remark-parse";
import remarkAdmonition from "./remark-admonition"; import remarkAdmonition from "./remark-admonition";
import remarkTabs from "./remark-tabs";
import rehypeWrapHeadings from "./rehype-wrap-headings"; import rehypeWrapHeadings from "./rehype-wrap-headings";
import remarkLinkMigration from "./link-migration"; import remarkLinkMigration from "./link-migration";
@@ -44,6 +45,7 @@ export default function ({
.use(remarkGfm) .use(remarkGfm)
.use(remarkDirective) .use(remarkDirective)
.use(remarkAdmonition) .use(remarkAdmonition)
.use(remarkTabs)
.use(remarkRehype) .use(remarkRehype)
.use(rehypeTocSlug, { .use(rehypeTocSlug, {
tocMaxDepth, tocMaxDepth,

View File

@@ -0,0 +1,93 @@
import { visit } from "unist-util-visit";
import type { Paragraph, Root, Text } from "mdast";
export default function remarkTabs() {
return (tree: Root) => {
visit(tree, (node) => {
if (node.type != "containerDirective" || node.name != "tabs") {
return;
}
const data = (node.data ||= {});
data.hName = "div";
data.hProperties = {
className: "md-tabs",
};
let tabIndex = 0;
let tabTitles: string[] = [];
for (const [i, child] of node.children.entries()) {
if (child.type != "containerDirective" || child.name != "tab") {
continue;
}
let tabTitle: string;
if (child.children?.[0].data?.directiveLabel) {
const p = child.children.shift() as Paragraph;
tabTitle = (p.children[0] as Text).value;
} else {
tabTitle = "(empty)";
}
tabTitles.push(tabTitle);
node.children[i] = {
type: "containerDirective",
name: "",
data: {
hName: "div",
hProperties: {
className: "md-tabs-container",
},
},
children: [
{
type: "paragraph",
data: {
hName: "div",
hProperties: {
className: `md-tabs-tab ${tabIndex == 0 ? "is-active" : ""}`,
},
},
children: [{ type: "text", value: tabTitle }],
},
{
type: "containerDirective",
name: "",
data: {
hName: "div",
hProperties: {
className: `md-tabs-content ${tabIndex == 0 ? "is-active" : ""}`,
},
},
children: child.children,
},
],
};
tabIndex++;
}
if (tabTitles.length === 1) {
data.hProperties.className += " is-singleton";
}
// Add tab bar for when js is enabled
node.children = [
{
type: "paragraph",
data: {
hName: "div",
hProperties: {
className: "md-tabs-bar",
},
},
children: tabTitles.map((tabTitle, tabIndex) => ({
type: "text",
data: {
hName: "div",
hProperties: {
className: `md-tabs-tab ${tabIndex == 0 ? "is-active" : ""}`,
},
},
value: tabTitle,
})),
},
...node.children,
];
});
};
}

View File

@@ -111,7 +111,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0 var(--pagePadding); padding: 0 var(--pageMargin);
color: var(--fgInvertedColor); color: var(--fgInvertedColor);
background: var(--bgInvertedColor); background: var(--bgInvertedColor);
} }

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import "$lib/markdown/main.css"; import "$lib/markdown/main.css";
import { visit, type Heading as ArticleHeading } from "$lib/docs"; import { visit, type Heading as ArticleHeading } from "$lib/docs";
import { onMount } from "svelte";
const { data } = $props(); const { data } = $props();
type Heading = ArticleHeading & { type Heading = ArticleHeading & {
@@ -39,6 +40,36 @@
} }
}); });
onMount(() => {
const onClick = (ev: MouseEvent) => {
const targetTabEl = (ev.target as HTMLElement).closest(".md-tabs-tab");
if (!targetTabEl || targetTabEl.classList.contains(".is-active")) {
return;
}
const tabsEl = targetTabEl.closest(".md-tabs")!;
const tabEls = tabsEl.querySelectorAll(".md-tabs-tab")!;
const tabIndex = Array.from(tabEls).indexOf(targetTabEl);
if (tabIndex == -1) {
return;
}
const tabContentEls = tabsEl.querySelectorAll(".md-tabs-content");
const tabContentEl = tabContentEls[tabIndex];
if (!tabContentEl) {
return;
}
tabEls.forEach((tabEl) => tabEl.classList.remove("is-active"));
targetTabEl.classList.add("is-active");
tabContentEls.forEach((tabContentEl) =>
tabContentEl.classList.remove("is-active"),
);
tabContentEl.classList.add("is-active");
};
document.addEventListener("click", onClick);
return () => {
document.removeEventListener("click", onClick);
};
});
function normalizeHeadings(headings: ArticleHeading[]): Heading[] { function normalizeHeadings(headings: ArticleHeading[]): Heading[] {
return headings.map((heading) => ({ return headings.map((heading) => ({
...heading, ...heading,
@@ -232,7 +263,7 @@
:global { :global {
& :is(h1, h2, h3, h4, h5, h6) { & :is(h1, h2, h3, h4, h5, h6) {
margin-left: calc(-1 * var(--pagePadding)); margin-left: calc(-1 * var(--pageMargin));
display: flex; display: flex;
align-items: center; align-items: center;
&.is-scrolledPast { &.is-scrolledPast {

View File

@@ -10,7 +10,9 @@ See the complete [list](../guides/inventory/autoincludes.md) of auto-loaded file
## Create a machine ## Create a machine
=== "clan.nix (declarative)" ::::tabs
:::tab[clan.nix (declarative)]
```nix {3-4} ```nix {3-4}
{ {
@@ -28,7 +30,9 @@ See the complete [list](../guides/inventory/autoincludes.md) of auto-loaded file
} }
``` ```
=== "CLI (imperative)" :::
:::tab[CLI (imperative)]
```sh ```sh
clan machines create jon clan machines create jon
@@ -36,6 +40,23 @@ clan machines create jon
The imperative command might create a machine folder in `machines/jon` The imperative command might create a machine folder in `machines/jon`
And might persist information in `inventory.json` And might persist information in `inventory.json`
:::
::::
::::tabs
:::tab[file name test]
```nix
{
inventory.machines = {
# Define a machine
jon = { };
};
}
```
::::
### Configuring a machine ### Configuring a machine

View File

@@ -1,7 +1,7 @@
@import "@fontsource-variable/geist"; @import "@fontsource-variable/geist";
:root { :root {
--pagePadding: 15px; --pageMargin: 15px;
--globalBarHeight: 60px; --globalBarHeight: 60px;
--fgColor: #000; --fgColor: #000;
--fgInvertedColor: #fff; --fgInvertedColor: #fff;