@specglass/theme-default 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/__tests__/design-tokens.test.d.ts +1 -0
  2. package/dist/__tests__/design-tokens.test.js +107 -0
  3. package/dist/islands/CopyButton.js +2 -6
  4. package/dist/islands/LanguageToggle.d.ts +16 -0
  5. package/dist/islands/LanguageToggle.js +88 -0
  6. package/dist/islands/SearchPalette.js +57 -8
  7. package/dist/islands/ThemeToggle.js +1 -1
  8. package/dist/scripts/code-block-enhancer.js +6 -3
  9. package/dist/themes/notdiamond-dark.json +168 -0
  10. package/dist/themes/notdiamond-light.json +168 -0
  11. package/dist/ui/command.js +2 -2
  12. package/dist/ui/dialog.js +2 -2
  13. package/dist/utils/shiki.d.ts +1 -1
  14. package/dist/utils/shiki.js +5 -3
  15. package/package.json +5 -3
  16. package/src/components/ApiAuth.astro +31 -4
  17. package/src/components/ApiEndpoint.astro +67 -44
  18. package/src/components/ApiNavigation.astro +8 -11
  19. package/src/components/ApiParameters.astro +113 -162
  20. package/src/components/ApiResponse.astro +1 -1
  21. package/src/components/Callout.astro +59 -18
  22. package/src/components/Card.astro +4 -4
  23. package/src/components/CodeBlock.astro +7 -7
  24. package/src/components/CodeBlockGroup.astro +3 -3
  25. package/src/components/CodeExample.astro +183 -0
  26. package/src/components/EditLink.astro +53 -0
  27. package/src/components/Footer.astro +87 -25
  28. package/src/components/Header.astro +63 -7
  29. package/src/components/Sidebar.astro +43 -11
  30. package/src/components/TableOfContents.astro +5 -5
  31. package/src/components/Tabs.astro +51 -20
  32. package/src/islands/CopyButton.tsx +36 -34
  33. package/src/islands/LanguageToggle.tsx +214 -0
  34. package/src/islands/SearchPalette.tsx +121 -39
  35. package/src/islands/ThemeToggle.tsx +45 -48
  36. package/src/layouts/ApiReferencePage.astro +67 -56
  37. package/src/layouts/DocPage.astro +32 -27
  38. package/src/layouts/LandingPage.astro +348 -27
  39. package/src/scripts/code-block-enhancer.ts +8 -3
  40. package/src/styles/global.css +388 -59
  41. package/src/themes/notdiamond-dark.json +168 -0
  42. package/src/themes/notdiamond-light.json +168 -0
  43. package/src/ui/command.tsx +1 -2
  44. package/src/ui/dialog.tsx +8 -5
  45. package/src/utils/shiki.ts +5 -3
@@ -0,0 +1,183 @@
1
+ ---
2
+ /**
3
+ * CodeExample.astro — Renders code from an actual source file at build time.
4
+ *
5
+ * This component reads a file from the project, optionally extracts a line
6
+ * range, and renders it as a syntax-highlighted code block. This ensures
7
+ * code examples in docs never go stale — they're always pulled from the
8
+ * real source.
9
+ *
10
+ * @example
11
+ * <CodeExample src="../../examples/quickstart.ts" lines="5-20" />
12
+ * <CodeExample src="./utils.ts" lang="typescript" title="Utility Functions" />
13
+ */
14
+
15
+ import { highlight } from "../utils/shiki.js";
16
+ import { CopyButton } from "../islands/CopyButton";
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+
20
+ export interface Props {
21
+ /** Path to the source file (relative to the content file or absolute) */
22
+ src: string;
23
+ /** Optional line range, e.g. "5-20" */
24
+ lines?: string;
25
+ /** Language override (auto-detected from extension if not provided) */
26
+ lang?: string;
27
+ /** Optional filename label for the header */
28
+ title?: string;
29
+ /** Highlight specific lines, e.g. "1,3-5" */
30
+ highlight?: string;
31
+ /** Show line numbers */
32
+ showLineNumbers?: boolean;
33
+ }
34
+
35
+ const {
36
+ src,
37
+ lines,
38
+ lang: langProp,
39
+ title: titleProp,
40
+ highlight: highlightProp,
41
+ showLineNumbers = false,
42
+ } = Astro.props;
43
+
44
+ // Resolve path: if relative, resolve from the current file's directory
45
+ const currentFile = import.meta.url;
46
+ const currentDir = path.dirname(
47
+ currentFile.startsWith("file://") ? new URL(currentFile).pathname : currentFile,
48
+ );
49
+ const resolvedPath = path.resolve(currentDir, src);
50
+
51
+ // Read source file
52
+ let fileContent: string;
53
+ try {
54
+ fileContent = fs.readFileSync(resolvedPath, "utf-8");
55
+ } catch (e) {
56
+ throw new Error(
57
+ `[CodeExample] Could not read source file: ${resolvedPath}\n` +
58
+ ` Referenced from: ${currentFile}\n` +
59
+ ` src="${src}"`,
60
+ );
61
+ }
62
+
63
+ // Extract line range if specified
64
+ let codeContent = fileContent;
65
+ let startLine = 1;
66
+
67
+ if (lines) {
68
+ const [start, end] = lines.split("-").map(Number);
69
+ const allLines = fileContent.split("\n");
70
+
71
+ if (start < 1 || end > allLines.length || start > end) {
72
+ throw new Error(
73
+ `[CodeExample] Invalid line range "${lines}" — file has ${allLines.length} lines.\n` +
74
+ ` src="${src}"`,
75
+ );
76
+ }
77
+
78
+ codeContent = allLines.slice(start - 1, end).join("\n");
79
+ startLine = start;
80
+ }
81
+
82
+ // Auto-detect language from file extension
83
+ const ext = path.extname(resolvedPath).slice(1);
84
+ const EXTENSION_MAP: Record<string, string> = {
85
+ ts: "typescript",
86
+ tsx: "tsx",
87
+ js: "javascript",
88
+ jsx: "jsx",
89
+ py: "python",
90
+ rb: "ruby",
91
+ go: "go",
92
+ rs: "rust",
93
+ yml: "yaml",
94
+ yaml: "yaml",
95
+ json: "json",
96
+ md: "markdown",
97
+ mdx: "markdown",
98
+ css: "css",
99
+ html: "html",
100
+ sh: "bash",
101
+ bash: "bash",
102
+ sql: "sql",
103
+ };
104
+ const lang = langProp ?? EXTENSION_MAP[ext] ?? ext ?? "text";
105
+ const title = titleProp ?? path.basename(resolvedPath);
106
+
107
+ // Parse highlight ranges
108
+ import { parseHighlightRange } from "../utils/parse-highlight-range.js";
109
+ const markLines = highlightProp ? parseHighlightRange(highlightProp) : undefined;
110
+
111
+ // Syntax highlight
112
+ const highlightedHtml = await highlight(codeContent.trimEnd(), lang, {
113
+ highlightLines: markLines,
114
+ });
115
+
116
+ // Build CSS classes
117
+ const classes = ["code-block", "code-block--source-linked"];
118
+ if (showLineNumbers) classes.push("code-block--line-numbers");
119
+ ---
120
+
121
+ <div class={classes.join(" ")} data-source={src} data-code={codeContent.trimEnd()}>
122
+ <div class="code-block-header">
123
+ <span class="code-block-title">{title}</span>
124
+ {lines && <span class="code-block-lines">L{lines}</span>}
125
+ <a
126
+ href={`#source:${src}`}
127
+ class="code-block-source-link"
128
+ title="View full source"
129
+ aria-label={`View source: ${title}`}
130
+ >
131
+ <svg
132
+ xmlns="http://www.w3.org/2000/svg"
133
+ viewBox="0 0 16 16"
134
+ fill="currentColor"
135
+ class="w-3.5 h-3.5"
136
+ >
137
+ <path
138
+ fill-rule="evenodd"
139
+ d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z"
140
+ clip-rule="evenodd"></path>
141
+ </svg>
142
+ </a>
143
+ </div>
144
+ <div class="code-block-body">
145
+ <Fragment set:html={highlightedHtml} />
146
+ <CopyButton code={codeContent.trimEnd()} client:idle />
147
+ </div>
148
+ </div>
149
+
150
+ <style>
151
+ .code-block--source-linked {
152
+ margin: 1.5rem 0;
153
+ border: 1px solid var(--code-block-border, oklch(0.92 0.004 286.32));
154
+ border-radius: 0.5rem;
155
+ overflow: hidden;
156
+ }
157
+
158
+ .code-block-lines {
159
+ margin-left: 0.5rem;
160
+ color: var(--code-block-title-color, oklch(0.552 0.016 285.938));
161
+ opacity: 0.6;
162
+ font-size: 0.75rem;
163
+ }
164
+
165
+ .code-block-source-link {
166
+ margin-left: auto;
167
+ color: var(--code-block-title-color, oklch(0.552 0.016 285.938));
168
+ opacity: 0.4;
169
+ transition: opacity 150ms ease;
170
+ }
171
+
172
+ .code-block-source-link:hover {
173
+ opacity: 1;
174
+ }
175
+ </style>
176
+
177
+ <style is:global>
178
+ .dark .code-block--source-linked {
179
+ --code-block-border: oklch(0.274 0.006 286.033);
180
+ --code-block-header-bg: oklch(0.21 0.006 285.885);
181
+ --code-block-title-color: oklch(0.705 0.015 286.067);
182
+ }
183
+ </style>
@@ -0,0 +1,53 @@
1
+ ---
2
+ /**
3
+ * EditLink.astro — "Edit on GitHub" link shown at the bottom of doc pages.
4
+ *
5
+ * Only renders when `theme.editUrl` is configured in specglass.config.ts.
6
+ * Appends the current page's source file path to the base edit URL.
7
+ */
8
+ interface Props {
9
+ /** Slug of the current page, used to construct the file path */
10
+ currentSlug: string;
11
+ }
12
+
13
+ import { config } from "virtual:specglass/config";
14
+
15
+ const { currentSlug } = Astro.props;
16
+ const editUrl = config.theme?.editUrl;
17
+
18
+ // Construct the full edit URL: base + slug + .mdx
19
+ const fullEditUrl = editUrl ? `${editUrl.replace(/\/$/, "")}/${currentSlug}.mdx` : null;
20
+ ---
21
+
22
+ {
23
+ fullEditUrl && (
24
+ <a
25
+ href={fullEditUrl}
26
+ target="_blank"
27
+ rel="noopener noreferrer"
28
+ class="inline-flex items-center gap-1.5 text-sm text-text-muted hover:text-text transition-colors mt-8 pt-4 border-t border-border/30"
29
+ >
30
+ <svg
31
+ xmlns="http://www.w3.org/2000/svg"
32
+ viewBox="0 0 20 20"
33
+ fill="currentColor"
34
+ class="w-4 h-4"
35
+ aria-hidden="true"
36
+ >
37
+ <path d="m5.433 13.917 1.262-3.155A4 4 0 0 1 7.58 9.42l6.92-6.918a2.121 2.121 0 0 1 3 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 0 1-.65-.65Z" />
38
+ <path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0 0 10 3H4.75A2.75 2.75 0 0 0 2 5.75v9.5A2.75 2.75 0 0 0 4.75 18h9.5A2.75 2.75 0 0 0 17 15.25V10a.75.75 0 0 0-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5Z" />
39
+ </svg>
40
+ Edit this page on GitHub
41
+ <svg
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ viewBox="0 0 16 16"
44
+ fill="currentColor"
45
+ class="w-3 h-3 opacity-50"
46
+ aria-hidden="true"
47
+ >
48
+ <path d="M6.22 8.72a.75.75 0 0 1 0-1.06l1.25-1.25H3.5a.75.75 0 0 1 0-1.5h3.97L6.22 3.66a.75.75 0 0 1 1.06-1.06l2.5 2.5a.75.75 0 0 1 0 1.06l-2.5 2.5a.75.75 0 0 1-1.06 0Z" />
49
+ <path d="M3.5 9.75a.75.75 0 0 1 0-1.5h3.97L6.22 7.06a.75.75 0 0 1 1.06-1.06l2.5 2.5a.75.75 0 0 1 0 1.06l-2.5 2.5a.75.75 0 0 1-1.06-1.06l1.25-1.25H3.5Z" />
50
+ </svg>
51
+ </a>
52
+ )
53
+ }
@@ -2,40 +2,102 @@
2
2
  /**
3
3
  * Footer component for the specglass theme.
4
4
  *
5
- * Renders a simple footer with copyright year and optional text
6
- * from theme configuration. Can be overridden by placing a custom
7
- * Footer.astro at src/components/overrides/Footer.astro.
5
+ * Renders a multi-column footer with:
6
+ * - Column 1: Site title/logo + tagline
7
+ * - Column 2: Navigation links (headerLinks from config)
8
+ * - Column 3: Social/external links (socialLinks from theme config)
9
+ * - Bottom row: Copyright text
10
+ *
11
+ * Can be overridden by placing a custom Footer.astro at
12
+ * src/components/overrides/Footer.astro.
8
13
  */
9
14
  import { config } from "virtual:specglass/config";
10
15
 
11
16
  const currentYear = new Date().getFullYear();
12
17
  const footerText = config.theme?.footer;
13
18
  const headerLinks = config.theme?.headerLinks;
19
+ const socialLinks = config.theme?.socialLinks;
20
+ const siteTitle = config.site?.title ?? "Documentation";
21
+ const siteDescription = config.site?.description;
22
+ const hasNav = headerLinks && headerLinks.length > 0;
23
+ const hasSocial = socialLinks && socialLinks.length > 0;
14
24
  ---
15
25
 
16
- <footer class="border-t border-border bg-surface py-8 px-6" aria-label="Site footer">
17
- <div
18
- class="max-w-[var(--width-content-max)] mx-auto flex flex-col items-center gap-4 text-sm text-text-muted"
19
- >
26
+ <footer class="border-t border-border/40 py-8 px-6" aria-label="Site footer">
27
+ <div class="max-w-(--width-content-max) mx-auto">
28
+ {/* Multi-column layout falls back to single column when no nav/social */}
20
29
  {
21
- headerLinks && headerLinks.length > 0 && (
22
- <nav aria-label="Footer navigation" class="flex gap-6">
23
- {headerLinks.map((link) => (
24
- <a
25
- href={link.href}
26
- class="hover:text-text transition-colors"
27
- {...(link.href.startsWith("http")
28
- ? { target: "_blank", rel: "noopener noreferrer" }
29
- : {})}
30
- >
31
- {link.label}
32
- </a>
33
- ))}
34
- </nav>
35
- )
30
+ hasNav || hasSocial ? (
31
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
32
+ {/* Column 1: Branding */}
33
+ <div>
34
+ <div class="flex items-center gap-2 mb-2">
35
+ {config.theme?.logo && (
36
+ <img src={config.theme.logo} alt="" class="h-5 w-5" aria-hidden="true" />
37
+ )}
38
+ <span class="font-semibold text-text">{siteTitle}</span>
39
+ </div>
40
+ {siteDescription && (
41
+ <p class="text-sm text-text-muted m-0 leading-relaxed">{siteDescription}</p>
42
+ )}
43
+ </div>
44
+
45
+ {/* Column 2: Navigation */}
46
+ {hasNav && (
47
+ <div>
48
+ <h4 class="text-[0.6875rem] font-medium uppercase tracking-wider text-text-muted mb-3">
49
+ Navigation
50
+ </h4>
51
+ <nav aria-label="Footer navigation" class="flex flex-col gap-2">
52
+ {headerLinks.map((link) => (
53
+ <a
54
+ href={link.href}
55
+ class="text-[0.8125rem] text-text-muted hover:text-text transition-colors no-underline focus-visible:outline-2 focus-visible:outline-text rounded"
56
+ {...(link.href.startsWith("http")
57
+ ? { target: "_blank", rel: "noopener noreferrer" }
58
+ : {})}
59
+ >
60
+ {link.label}
61
+ </a>
62
+ ))}
63
+ </nav>
64
+ </div>
65
+ )}
66
+
67
+ {/* Column 3: Social Links */}
68
+ {hasSocial && (
69
+ <div>
70
+ <h4 class="text-[0.6875rem] font-medium uppercase tracking-wider text-text-muted mb-3">
71
+ Connect
72
+ </h4>
73
+ <nav aria-label="Social links" class="flex flex-col gap-2">
74
+ {socialLinks.map((link) => (
75
+ <a
76
+ href={link.href}
77
+ class="text-[0.8125rem] text-text-muted hover:text-text transition-colors no-underline focus-visible:outline-2 focus-visible:outline-text rounded"
78
+ target="_blank"
79
+ rel="noopener noreferrer"
80
+ >
81
+ {link.label}
82
+ </a>
83
+ ))}
84
+ </nav>
85
+ </div>
86
+ )}
87
+ </div>
88
+ ) : null
36
89
  }
37
- <p class="m-0">
38
- {footerText ? footerText : ${currentYear} Built with specglass`}
39
- </p>
90
+
91
+ {/* Bottom row: copyright */}
92
+ <div
93
+ class:list={[
94
+ "text-[0.8125rem] text-text-muted text-center",
95
+ (hasNav || hasSocial) && "border-t border-border/40 pt-6",
96
+ ]}
97
+ >
98
+ <p class="m-0">
99
+ {footerText ? footerText : `© ${currentYear} Built with specglass`}
100
+ </p>
101
+ </div>
40
102
  </div>
41
103
  </footer>
@@ -3,13 +3,15 @@
3
3
  * Header — Site-wide header with title/logo and navigation links.
4
4
  *
5
5
  * Reads from virtual:specglass/config for site title and theme config.
6
- * Includes a mobile menu toggle button and a slot for right-side items
6
+ * Includes a mobile menu toggle button and a slot for right-side items.
7
+ * Features scroll-aware styling: subtle primary border glow on scroll.
7
8
  */
8
9
  import { config } from "virtual:specglass/config";
9
10
  ---
10
11
 
11
12
  <header
12
- class="fixed top-0 left-0 right-0 z-20 h-(--height-header) border-b border-border bg-surface/95 backdrop-blur-sm"
13
+ id="site-header"
14
+ class="fixed top-0 left-0 right-0 z-20 h-(--height-header) border-b border-border/40 bg-surface-0/80 backdrop-blur-xl transition-[border-color,box-shadow] duration-200"
13
15
  data-pagefind-ignore
14
16
  >
15
17
  <div class="flex items-center h-full px-4 md:px-6">
@@ -17,7 +19,7 @@ import { config } from "virtual:specglass/config";
17
19
  <button
18
20
  id="mobile-menu-toggle"
19
21
  type="button"
20
- class="inline-flex items-center justify-center p-2 rounded-md text-text-muted hover:text-text hover:bg-hover-bg focus:outline-none focus-visible:ring-2 focus-visible:ring-primary md:hidden mr-2"
22
+ class="inline-flex items-center justify-center w-8 h-8 rounded-lg text-text-muted hover:text-text hover:bg-surface-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary md:hidden mr-2"
21
23
  aria-expanded="false"
22
24
  aria-controls="sidebar-mobile"
23
25
  aria-label="Toggle navigation menu"
@@ -40,7 +42,7 @@ import { config } from "virtual:specglass/config";
40
42
  <!-- Site title / logo -->
41
43
  <a
42
44
  href="/"
43
- class="flex items-center gap-2 font-semibold text-lg text-text hover:text-primary transition-colors no-underline"
45
+ class="flex items-center gap-2 font-semibold text-[0.9375rem] text-text hover:opacity-70 transition-opacity no-underline"
44
46
  >
45
47
  {
46
48
  config.theme?.logo && (
@@ -56,11 +58,11 @@ import { config } from "virtual:specglass/config";
56
58
  <!-- Header navigation links from config -->
57
59
  {
58
60
  config.theme?.headerLinks && config.theme.headerLinks.length > 0 && (
59
- <nav aria-label="Header navigation" class="hidden md:flex items-center gap-4 mr-4">
61
+ <nav aria-label="Header navigation" class="hidden md:flex items-center gap-1 mr-3">
60
62
  {config.theme.headerLinks.map((link) => (
61
63
  <a
62
64
  href={link.href}
63
- class="text-sm text-text-muted hover:text-text transition-colors focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none rounded px-1"
65
+ class="text-[0.8125rem] text-text-muted hover:text-text transition-colors focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none rounded-md px-2.5 py-1.5 hover:bg-surface-2"
64
66
  {...(link.href.startsWith("http")
65
67
  ? { target: "_blank", rel: "noopener noreferrer" }
66
68
  : {})}
@@ -73,8 +75,62 @@ import { config } from "virtual:specglass/config";
73
75
  }
74
76
 
75
77
  <!-- Right-side header actions (theme toggle, search, etc.) -->
76
- <div id="header-actions" class="flex items-center gap-2">
78
+ <div id="header-actions" class="flex items-center gap-1">
77
79
  <slot />
78
80
  </div>
79
81
  </div>
80
82
  </header>
83
+
84
+ <script>
85
+ function initHeaderScroll() {
86
+ const header = document.getElementById("site-header");
87
+ if (!header) return;
88
+
89
+ let ticking = false;
90
+
91
+ function updateHeader() {
92
+ if (window.scrollY > 0) {
93
+ header!.setAttribute("data-scrolled", "");
94
+ } else {
95
+ header!.removeAttribute("data-scrolled");
96
+ }
97
+ ticking = false;
98
+ }
99
+
100
+ function onScroll() {
101
+ if (!ticking) {
102
+ requestAnimationFrame(updateHeader);
103
+ ticking = true;
104
+ }
105
+ }
106
+
107
+ window.addEventListener("scroll", onScroll, { passive: true });
108
+
109
+ // Set initial state
110
+ updateHeader();
111
+
112
+ // Cleanup on page transition to prevent listener leak
113
+ document.addEventListener(
114
+ "astro:before-swap",
115
+ () => {
116
+ window.removeEventListener("scroll", onScroll);
117
+ },
118
+ { once: true },
119
+ );
120
+ }
121
+
122
+ initHeaderScroll();
123
+ document.addEventListener("astro:after-swap", initHeaderScroll);
124
+ </script>
125
+
126
+ <style is:global>
127
+ #site-header[data-scrolled] {
128
+ border-bottom-color: var(--color-border);
129
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
130
+ }
131
+
132
+ .dark #site-header[data-scrolled] {
133
+ border-bottom-color: var(--color-border);
134
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
135
+ }
136
+ </style>
@@ -17,7 +17,7 @@ export interface Props {
17
17
  const { items, currentSlug, depth = 0 } = Astro.props;
18
18
  ---
19
19
 
20
- <ul class:list={["list-none p-0 m-0", depth > 0 && "ml-3 border-l border-border pl-3"]}>
20
+ <ul class:list={["list-none p-0 m-0", depth > 0 && "ml-3 pl-3"]}>
21
21
  {
22
22
  items
23
23
  .filter((item) => !item.hidden)
@@ -27,14 +27,14 @@ const { items, currentSlug, depth = 0 } = Astro.props;
27
27
  const isCollapsed = item.collapsed && !isAncestor;
28
28
 
29
29
  return (
30
- <li class="my-0.5">
30
+ <li class:list={[item.type === "section" && depth === 0 ? "mt-5 first:mt-0" : ""]}>
31
31
  {item.type === "external-link" && item.href ? (
32
32
  /* External link */
33
33
  <a
34
34
  href={item.href}
35
35
  target="_blank"
36
36
  rel="noopener noreferrer"
37
- class="flex items-center gap-1.5 px-2 py-1.5 text-sm text-text-muted hover:text-text rounded-md hover:bg-hover-bg transition-colors no-underline"
37
+ class="flex items-center gap-1.5 px-2 py-[3px] text-[0.8125rem] leading-relaxed text-text-muted hover:text-text transition-colors no-underline focus-visible:outline-2 focus-visible:outline-text"
38
38
  >
39
39
  <span>{item.title}</span>
40
40
  <svg
@@ -57,17 +57,36 @@ const { items, currentSlug, depth = 0 } = Astro.props;
57
57
  <details open={!isCollapsed}>
58
58
  <summary
59
59
  class:list={[
60
- "flex items-center gap-1 px-2 py-1.5 text-sm font-medium cursor-pointer select-none rounded-md hover:bg-hover-bg transition-colors list-none",
61
- isAncestor ? "text-primary" : "text-text",
60
+ "flex items-center gap-1 px-2 py-1 text-[0.8125rem] cursor-pointer select-none transition-colors duration-150 list-none focus-visible:outline-2 focus-visible:outline-text",
61
+ depth === 0
62
+ ? "font-medium text-[0.6875rem] uppercase tracking-wider text-text-muted mb-1"
63
+ : isAncestor
64
+ ? "font-medium text-text"
65
+ : "font-medium text-text-muted",
62
66
  ]}
63
67
  >
64
68
  {item.icon && <span class="mr-1">{item.icon}</span>}
65
69
  <span>{item.title}</span>
70
+ {item.badge && (
71
+ <span
72
+ class:list={[
73
+ "sg-sidebar-badge text-[0.625rem] font-medium px-1.5 py-0.5 rounded-full tracking-wider whitespace-nowrap ml-1.5",
74
+ item.badge.toLowerCase() === "new" && "bg-green-500/10 text-green-400",
75
+ item.badge.toLowerCase() === "beta" && "bg-blue-500/10 text-blue-400",
76
+ item.badge.toLowerCase() === "deprecated" &&
77
+ "bg-orange-500/10 text-orange-400",
78
+ !["new", "beta", "deprecated"].includes(item.badge.toLowerCase()) &&
79
+ "bg-surface-2 text-text-muted",
80
+ ]}
81
+ >
82
+ {item.badge}
83
+ </span>
84
+ )}
66
85
  <svg
67
- class="h-3.5 w-3.5 ml-auto opacity-40 transition-transform duration-200 details-chevron"
86
+ class="h-3 w-3 ml-auto opacity-30 transition-transform duration-150 details-chevron"
68
87
  fill="none"
69
88
  viewBox="0 0 24 24"
70
- stroke-width="2"
89
+ stroke-width="2.5"
71
90
  stroke="currentColor"
72
91
  aria-hidden="true"
73
92
  >
@@ -87,15 +106,28 @@ const { items, currentSlug, depth = 0 } = Astro.props;
87
106
  <a
88
107
  href={`/${item.slug}`}
89
108
  class:list={[
90
- "flex items-center gap-1.5 px-2 py-1.5 text-sm rounded-md transition-colors no-underline",
91
- isActive
92
- ? "bg-primary/10 text-primary font-medium"
93
- : "text-text-muted hover:text-text hover:bg-hover-bg",
109
+ "flex items-center gap-1.5 px-2 py-[3px] text-[0.8125rem] leading-relaxed rounded-md transition-colors duration-150 no-underline focus-visible:outline-2 focus-visible:outline-text",
110
+ isActive ? "text-text font-medium" : "text-text-muted hover:text-text",
94
111
  ]}
95
112
  aria-current={isActive ? "page" : undefined}
96
113
  >
97
114
  {item.icon && <span class="mr-1">{item.icon}</span>}
98
115
  <span>{item.title}</span>
116
+ {item.badge && (
117
+ <span
118
+ class:list={[
119
+ "sg-sidebar-badge ml-auto text-[0.625rem] font-medium px-1.5 py-0.5 rounded-full tracking-wider whitespace-nowrap",
120
+ item.badge.toLowerCase() === "new" && "bg-green-500/10 text-green-400",
121
+ item.badge.toLowerCase() === "beta" && "bg-blue-500/10 text-blue-400",
122
+ item.badge.toLowerCase() === "deprecated" &&
123
+ "bg-orange-500/10 text-orange-400",
124
+ !["new", "beta", "deprecated"].includes(item.badge.toLowerCase()) &&
125
+ "bg-surface-2 text-text-muted",
126
+ ]}
127
+ >
128
+ {item.badge}
129
+ </span>
130
+ )}
99
131
  </a>
100
132
  )}
101
133
  </li>
@@ -20,7 +20,7 @@ const tocHeadings = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
20
20
  {
21
21
  tocHeadings.length > 0 && (
22
22
  <nav aria-label="Table of contents">
23
- <h2 class="text-xs font-semibold uppercase tracking-wider text-text-muted mb-3">
23
+ <h2 class="text-[0.6875rem] font-medium uppercase tracking-wider text-text-muted mb-3">
24
24
  On this page
25
25
  </h2>
26
26
  <ul class="list-none p-0 m-0 space-y-0.5" id="toc-list">
@@ -29,8 +29,8 @@ const tocHeadings = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
29
29
  <a
30
30
  href={`#${heading.slug}`}
31
31
  class:list={[
32
- "block text-sm py-1 no-underline transition-colors duration-150",
33
- heading.depth === 3 ? "pl-4" : "pl-0",
32
+ "block text-[0.8125rem] py-1 no-underline transition-colors duration-150",
33
+ heading.depth === 3 ? "pl-4" : "pl-2",
34
34
  "text-text-muted hover:text-text",
35
35
  ]}
36
36
  data-toc-link
@@ -66,10 +66,10 @@ const tocHeadings = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
66
66
  tocLinks.forEach((link) => {
67
67
  const linkId = (link as HTMLElement).dataset.tocId;
68
68
  if (linkId === id) {
69
- link.classList.add("text-primary", "font-medium");
69
+ link.classList.add("text-text", "font-medium");
70
70
  link.classList.remove("text-text-muted");
71
71
  } else {
72
- link.classList.remove("text-primary", "font-medium");
72
+ link.classList.remove("text-text", "font-medium");
73
73
  link.classList.add("text-text-muted");
74
74
  }
75
75
  });