@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.
- package/dist/__tests__/design-tokens.test.d.ts +1 -0
- package/dist/__tests__/design-tokens.test.js +107 -0
- package/dist/islands/CopyButton.js +2 -6
- package/dist/islands/LanguageToggle.d.ts +16 -0
- package/dist/islands/LanguageToggle.js +88 -0
- package/dist/islands/SearchPalette.js +57 -8
- package/dist/islands/ThemeToggle.js +1 -1
- package/dist/scripts/code-block-enhancer.js +6 -3
- package/dist/themes/notdiamond-dark.json +168 -0
- package/dist/themes/notdiamond-light.json +168 -0
- package/dist/ui/command.js +2 -2
- package/dist/ui/dialog.js +2 -2
- package/dist/utils/shiki.d.ts +1 -1
- package/dist/utils/shiki.js +5 -3
- package/package.json +5 -3
- package/src/components/ApiAuth.astro +31 -4
- package/src/components/ApiEndpoint.astro +67 -44
- package/src/components/ApiNavigation.astro +8 -11
- package/src/components/ApiParameters.astro +113 -162
- package/src/components/ApiResponse.astro +1 -1
- package/src/components/Callout.astro +59 -18
- package/src/components/Card.astro +4 -4
- package/src/components/CodeBlock.astro +7 -7
- package/src/components/CodeBlockGroup.astro +3 -3
- package/src/components/CodeExample.astro +183 -0
- package/src/components/EditLink.astro +53 -0
- package/src/components/Footer.astro +87 -25
- package/src/components/Header.astro +63 -7
- package/src/components/Sidebar.astro +43 -11
- package/src/components/TableOfContents.astro +5 -5
- package/src/components/Tabs.astro +51 -20
- package/src/islands/CopyButton.tsx +36 -34
- package/src/islands/LanguageToggle.tsx +214 -0
- package/src/islands/SearchPalette.tsx +121 -39
- package/src/islands/ThemeToggle.tsx +45 -48
- package/src/layouts/ApiReferencePage.astro +67 -56
- package/src/layouts/DocPage.astro +32 -27
- package/src/layouts/LandingPage.astro +348 -27
- package/src/scripts/code-block-enhancer.ts +8 -3
- package/src/styles/global.css +388 -59
- package/src/themes/notdiamond-dark.json +168 -0
- package/src/themes/notdiamond-light.json +168 -0
- package/src/ui/command.tsx +1 -2
- package/src/ui/dialog.tsx +8 -5
- 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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
17
|
-
<div
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
<
|
|
23
|
-
{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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="
|
|
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-
|
|
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
|
|
61
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
33
|
-
heading.depth === 3 ? "pl-4" : "pl-
|
|
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-
|
|
69
|
+
link.classList.add("text-text", "font-medium");
|
|
70
70
|
link.classList.remove("text-text-muted");
|
|
71
71
|
} else {
|
|
72
|
-
link.classList.remove("text-
|
|
72
|
+
link.classList.remove("text-text", "font-medium");
|
|
73
73
|
link.classList.add("text-text-muted");
|
|
74
74
|
}
|
|
75
75
|
});
|