@specglass/theme-default 0.0.2
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__/code-tabs.test.d.ts +2 -0
- package/dist/__tests__/code-tabs.test.d.ts.map +1 -0
- package/dist/__tests__/code-tabs.test.js +219 -0
- package/dist/__tests__/code-tabs.test.js.map +1 -0
- package/dist/__tests__/copy-button.test.d.ts +2 -0
- package/dist/__tests__/copy-button.test.d.ts.map +1 -0
- package/dist/__tests__/copy-button.test.js +116 -0
- package/dist/__tests__/copy-button.test.js.map +1 -0
- package/dist/__tests__/search-palette.test.d.ts +2 -0
- package/dist/__tests__/search-palette.test.d.ts.map +1 -0
- package/dist/__tests__/search-palette.test.js +71 -0
- package/dist/__tests__/search-palette.test.js.map +1 -0
- package/dist/__tests__/shiki.test.d.ts +2 -0
- package/dist/__tests__/shiki.test.d.ts.map +1 -0
- package/dist/__tests__/shiki.test.js +37 -0
- package/dist/__tests__/shiki.test.js.map +1 -0
- package/dist/__tests__/theme-css.test.d.ts +2 -0
- package/dist/__tests__/theme-css.test.d.ts.map +1 -0
- package/dist/__tests__/theme-css.test.js +124 -0
- package/dist/__tests__/theme-css.test.js.map +1 -0
- package/dist/__tests__/theme-helpers.test.d.ts +2 -0
- package/dist/__tests__/theme-helpers.test.d.ts.map +1 -0
- package/dist/__tests__/theme-helpers.test.js +81 -0
- package/dist/__tests__/theme-helpers.test.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/islands/CodeTabs.d.ts +21 -0
- package/dist/islands/CodeTabs.d.ts.map +1 -0
- package/dist/islands/CodeTabs.js +125 -0
- package/dist/islands/CodeTabs.js.map +1 -0
- package/dist/islands/CopyButton.d.ts +16 -0
- package/dist/islands/CopyButton.d.ts.map +1 -0
- package/dist/islands/CopyButton.js +54 -0
- package/dist/islands/CopyButton.js.map +1 -0
- package/dist/islands/SearchPalette.d.ts +2 -0
- package/dist/islands/SearchPalette.d.ts.map +1 -0
- package/dist/islands/SearchPalette.js +109 -0
- package/dist/islands/SearchPalette.js.map +1 -0
- package/dist/islands/SearchResults.d.ts +2 -0
- package/dist/islands/SearchResults.d.ts.map +1 -0
- package/dist/islands/SearchResults.js +130 -0
- package/dist/islands/SearchResults.js.map +1 -0
- package/dist/islands/ThemeToggle.d.ts +12 -0
- package/dist/islands/ThemeToggle.d.ts.map +1 -0
- package/dist/islands/ThemeToggle.js +43 -0
- package/dist/islands/ThemeToggle.js.map +1 -0
- package/dist/layouts/DocPage.test.d.ts +2 -0
- package/dist/layouts/DocPage.test.d.ts.map +1 -0
- package/dist/layouts/DocPage.test.js +165 -0
- package/dist/layouts/DocPage.test.js.map +1 -0
- package/dist/lib/utils.d.ts +10 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +13 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/scripts/code-block-enhancer.d.ts +16 -0
- package/dist/scripts/code-block-enhancer.d.ts.map +1 -0
- package/dist/scripts/code-block-enhancer.js +55 -0
- package/dist/scripts/code-block-enhancer.js.map +1 -0
- package/dist/ui/command.d.ts +87 -0
- package/dist/ui/command.d.ts.map +1 -0
- package/dist/ui/command.js +28 -0
- package/dist/ui/command.js.map +1 -0
- package/dist/ui/dialog.d.ts +20 -0
- package/dist/ui/dialog.d.ts.map +1 -0
- package/dist/ui/dialog.js +22 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/utils/parse-highlight-range.d.ts +12 -0
- package/dist/utils/parse-highlight-range.d.ts.map +1 -0
- package/dist/utils/parse-highlight-range.js +40 -0
- package/dist/utils/parse-highlight-range.js.map +1 -0
- package/dist/utils/parse-highlight-range.test.d.ts +2 -0
- package/dist/utils/parse-highlight-range.test.d.ts.map +1 -0
- package/dist/utils/parse-highlight-range.test.js +32 -0
- package/dist/utils/parse-highlight-range.test.js.map +1 -0
- package/dist/utils/schema-renderer.d.ts +38 -0
- package/dist/utils/schema-renderer.d.ts.map +1 -0
- package/dist/utils/schema-renderer.js +115 -0
- package/dist/utils/schema-renderer.js.map +1 -0
- package/dist/utils/schema-renderer.test.d.ts +2 -0
- package/dist/utils/schema-renderer.test.d.ts.map +1 -0
- package/dist/utils/schema-renderer.test.js +219 -0
- package/dist/utils/schema-renderer.test.js.map +1 -0
- package/dist/utils/shiki.d.ts +20 -0
- package/dist/utils/shiki.d.ts.map +1 -0
- package/dist/utils/shiki.js +84 -0
- package/dist/utils/shiki.js.map +1 -0
- package/dist/utils/sidebar-helpers.d.ts +10 -0
- package/dist/utils/sidebar-helpers.d.ts.map +1 -0
- package/dist/utils/sidebar-helpers.js +14 -0
- package/dist/utils/sidebar-helpers.js.map +1 -0
- package/dist/utils/theme-css.d.ts +21 -0
- package/dist/utils/theme-css.d.ts.map +1 -0
- package/dist/utils/theme-css.js +77 -0
- package/dist/utils/theme-css.js.map +1 -0
- package/dist/utils/theme-helpers.d.ts +28 -0
- package/dist/utils/theme-helpers.d.ts.map +1 -0
- package/dist/utils/theme-helpers.js +55 -0
- package/dist/utils/theme-helpers.js.map +1 -0
- package/dist/utils/toc-helpers.d.ts +12 -0
- package/dist/utils/toc-helpers.d.ts.map +1 -0
- package/dist/utils/toc-helpers.js +9 -0
- package/dist/utils/toc-helpers.js.map +1 -0
- package/package.json +68 -0
- package/src/components/ApiAuth.astro +116 -0
- package/src/components/ApiEndpoint.astro +75 -0
- package/src/components/ApiNavigation.astro +110 -0
- package/src/components/ApiParameters.astro +204 -0
- package/src/components/ApiResponse.astro +144 -0
- package/src/components/Callout.astro +54 -0
- package/src/components/Card.astro +46 -0
- package/src/components/CodeBlock.astro +142 -0
- package/src/components/CodeBlockGroup.astro +196 -0
- package/src/components/CodeTabs.astro +53 -0
- package/src/components/Footer.astro +41 -0
- package/src/components/Header.astro +80 -0
- package/src/components/Sidebar.astro +117 -0
- package/src/components/TabItem.astro +24 -0
- package/src/components/TableOfContents.astro +111 -0
- package/src/components/Tabs.astro +185 -0
- package/src/islands/CodeTabs.tsx +212 -0
- package/src/islands/CopyButton.tsx +101 -0
- package/src/islands/SearchPalette.tsx +307 -0
- package/src/islands/SearchResults.tsx +301 -0
- package/src/islands/ThemeToggle.tsx +107 -0
- package/src/layouts/ApiReferencePage.astro +239 -0
- package/src/layouts/DocPage.astro +199 -0
- package/src/layouts/DocPage.test.ts +183 -0
- package/src/layouts/LandingPage.astro +143 -0
- package/src/lib/utils.ts +13 -0
- package/src/styles/global.css +241 -0
- package/src/utils/parse-highlight-range.test.ts +40 -0
- package/src/utils/parse-highlight-range.ts +41 -0
- package/src/utils/schema-renderer.test.ts +269 -0
- package/src/utils/schema-renderer.ts +152 -0
- package/src/utils/shiki.ts +99 -0
- package/src/utils/sidebar-helpers.ts +24 -0
- package/src/utils/theme-css.ts +101 -0
- package/src/utils/theme-helpers.ts +59 -0
- package/src/utils/toc-helpers.ts +11 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* CodeBlock component — renders syntax-highlighted code using Shiki
|
|
4
|
+
* directly (via shared highlighter utility) with a copy-to-clipboard button.
|
|
5
|
+
*
|
|
6
|
+
* Uses the same dual-theme CSS variable approach as fenced code blocks
|
|
7
|
+
* (--shiki-light, --shiki-dark) for seamless dark/light mode switching.
|
|
8
|
+
*
|
|
9
|
+
* Supports lang, title (file name label), and highlight (line highlighting).
|
|
10
|
+
* The CopyButton React island hydrates at idle (not critical for first paint).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <CodeBlock lang="ts" title="config.ts" highlight="1,3-5">
|
|
14
|
+
* import { defineConfig } from 'astro/config';
|
|
15
|
+
* export default defineConfig({});
|
|
16
|
+
* </CodeBlock>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { highlight } from "../utils/shiki.js";
|
|
20
|
+
import { CopyButton } from "../islands/CopyButton";
|
|
21
|
+
import { parseHighlightRange } from "../utils/parse-highlight-range.js";
|
|
22
|
+
|
|
23
|
+
export interface Props {
|
|
24
|
+
lang?: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
highlight?: string;
|
|
27
|
+
code?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { lang = "text", title, highlight: highlightProp, code } = Astro.props;
|
|
31
|
+
|
|
32
|
+
// Resolve code content: prefer `code` prop, fall back to slot text
|
|
33
|
+
const slotContent = await Astro.slots.render("default");
|
|
34
|
+
|
|
35
|
+
// Strip HTML tags, then decode common HTML entities from slot rendering
|
|
36
|
+
function decodeEntities(str: string): string {
|
|
37
|
+
return str
|
|
38
|
+
.replace(/<\/?[^>]+(>|$)/g, "")
|
|
39
|
+
.replace(/&/g, "&")
|
|
40
|
+
.replace(/</g, "<")
|
|
41
|
+
.replace(/>/g, ">")
|
|
42
|
+
.replace(/"/g, '"')
|
|
43
|
+
.replace(/'/g, "'")
|
|
44
|
+
.replace(/'/g, "'")
|
|
45
|
+
.replace(///g, "/");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const codeContent = code ?? (slotContent ? decodeEntities(slotContent) : "");
|
|
49
|
+
|
|
50
|
+
// Parse highlight ranges into line numbers array for Shiki decorations
|
|
51
|
+
const markLines = highlightProp ? parseHighlightRange(highlightProp) : undefined;
|
|
52
|
+
|
|
53
|
+
// Syntax highlight using shared Shiki instance (dual-theme CSS variables)
|
|
54
|
+
const highlightedHtml = await highlight(codeContent, lang, {
|
|
55
|
+
highlightLines: markLines,
|
|
56
|
+
});
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
<div class="code-block">
|
|
60
|
+
{
|
|
61
|
+
title && (
|
|
62
|
+
<div class="code-block-header">
|
|
63
|
+
<span class="code-block-title">{title}</span>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
<div class="code-block-body">
|
|
68
|
+
<Fragment set:html={highlightedHtml} />
|
|
69
|
+
<CopyButton code={codeContent} client:idle />
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<style>
|
|
74
|
+
.code-block {
|
|
75
|
+
margin: 1.5rem 0;
|
|
76
|
+
border: 1px solid var(--code-block-border, #e5e7eb);
|
|
77
|
+
border-radius: 0.5rem;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.code-block-header {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
padding: 0.5rem 1rem;
|
|
85
|
+
background-color: var(--code-block-header-bg, #f3f4f6);
|
|
86
|
+
border-bottom: 1px solid var(--code-block-border, #e5e7eb);
|
|
87
|
+
font-size: 0.8125rem;
|
|
88
|
+
font-family: var(
|
|
89
|
+
--font-mono,
|
|
90
|
+
ui-monospace,
|
|
91
|
+
SFMono-Regular,
|
|
92
|
+
"SF Mono",
|
|
93
|
+
Menlo,
|
|
94
|
+
Consolas,
|
|
95
|
+
monospace
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.code-block-title {
|
|
100
|
+
color: var(--code-block-title-color, #6b7280);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.code-block-body {
|
|
104
|
+
position: relative;
|
|
105
|
+
overflow-x: auto;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Show copy button on hover or when it has focus-within */
|
|
109
|
+
.code-block-body:hover :global(.sg-copy-btn),
|
|
110
|
+
.code-block-body:focus-within :global(.sg-copy-btn) {
|
|
111
|
+
opacity: 1 !important;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.code-block-body :global(.sg-copy-btn:focus-visible) {
|
|
115
|
+
outline: 2px solid var(--color-primary, #3b82f6);
|
|
116
|
+
outline-offset: 2px;
|
|
117
|
+
opacity: 1 !important;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.code-block-body :global(pre) {
|
|
121
|
+
margin: 0;
|
|
122
|
+
border-radius: 0;
|
|
123
|
+
padding: 1rem 1.25rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.code-block-body :global(code) {
|
|
127
|
+
font-size: 0.875rem;
|
|
128
|
+
line-height: 1.7;
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
131
|
+
|
|
132
|
+
<!--
|
|
133
|
+
Dark mode CSS custom property overrides.
|
|
134
|
+
Scoped Astro styles don't support :global(.dark), so use an inline style tag.
|
|
135
|
+
-->
|
|
136
|
+
<style is:global>
|
|
137
|
+
.dark .code-block {
|
|
138
|
+
--code-block-border: oklch(0.303 0.034 264.376);
|
|
139
|
+
--code-block-header-bg: oklch(0.179 0.04 264.376);
|
|
140
|
+
--code-block-title-color: oklch(0.708 0.014 264.364);
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* CodeBlockGroup component — renders multiple CodeBlock children in a
|
|
4
|
+
* tabbed interface for comparing related code files.
|
|
5
|
+
*
|
|
6
|
+
* Each child CodeBlock's `title` prop becomes the tab label.
|
|
7
|
+
* If no title is set, falls back to "Tab N".
|
|
8
|
+
*
|
|
9
|
+
* NOTE: This component uses its own vanilla JS tab logic rather than
|
|
10
|
+
* composing Tabs.astro/TabItem.astro. That's intentional — Astro's slot
|
|
11
|
+
* model doesn't let us dynamically wrap slotted CodeBlock children into
|
|
12
|
+
* TabItem containers at build time. The tab behavior (roles, keyboard nav,
|
|
13
|
+
* aria attributes) is equivalent to Tabs.astro.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* <CodeBlockGroup>
|
|
17
|
+
* <CodeBlock lang="ts" title="server.ts">const app = express();</CodeBlock>
|
|
18
|
+
* <CodeBlock lang="json" title="package.json">{"name": "app"}</CodeBlock>
|
|
19
|
+
* </CodeBlockGroup>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// No imports needed — content is slotted
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<div class="code-block-group" data-code-group>
|
|
26
|
+
<slot />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<style>
|
|
30
|
+
.code-block-group {
|
|
31
|
+
margin: 1.5rem 0;
|
|
32
|
+
border: 1px solid var(--code-block-border, #e5e7eb);
|
|
33
|
+
border-radius: 0.5rem;
|
|
34
|
+
overflow: hidden;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Suppress child CodeBlock outer margins and borders — the group owns the chrome */
|
|
38
|
+
.code-block-group :global(.code-block) {
|
|
39
|
+
margin: 0;
|
|
40
|
+
border: none;
|
|
41
|
+
border-radius: 0;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
47
|
+
document.querySelectorAll<HTMLDivElement>("[data-code-group]").forEach((group) => {
|
|
48
|
+
const codeBlocks = group.querySelectorAll<HTMLDivElement>(":scope > .code-block");
|
|
49
|
+
if (codeBlocks.length <= 1) return; // No tabs needed for single block
|
|
50
|
+
|
|
51
|
+
const groupId = `cg-${Math.random().toString(36).slice(2, 9)}`;
|
|
52
|
+
|
|
53
|
+
// Create tab header
|
|
54
|
+
const tabHeader = document.createElement("div");
|
|
55
|
+
tabHeader.role = "tablist";
|
|
56
|
+
tabHeader.className = "code-group-tabs";
|
|
57
|
+
tabHeader.setAttribute("aria-label", "Code file tabs");
|
|
58
|
+
|
|
59
|
+
// Build tabs from code blocks
|
|
60
|
+
codeBlocks.forEach((block, index) => {
|
|
61
|
+
// Extract title from the header, or use fallback
|
|
62
|
+
const titleEl = block.querySelector(".code-block-title");
|
|
63
|
+
const label = titleEl?.textContent?.trim() || `Tab ${index + 1}`;
|
|
64
|
+
|
|
65
|
+
const tabId = `${groupId}-tab-${index}`;
|
|
66
|
+
const panelId = `${groupId}-panel-${index}`;
|
|
67
|
+
|
|
68
|
+
// Create tab button
|
|
69
|
+
const button = document.createElement("button");
|
|
70
|
+
button.role = "tab";
|
|
71
|
+
button.id = tabId;
|
|
72
|
+
button.setAttribute("aria-controls", panelId);
|
|
73
|
+
button.setAttribute("aria-selected", index === 0 ? "true" : "false");
|
|
74
|
+
button.tabIndex = index === 0 ? 0 : -1;
|
|
75
|
+
button.textContent = label;
|
|
76
|
+
tabHeader.appendChild(button);
|
|
77
|
+
|
|
78
|
+
// Set up panel attributes
|
|
79
|
+
block.id = panelId;
|
|
80
|
+
block.setAttribute("role", "tabpanel");
|
|
81
|
+
block.setAttribute("aria-labelledby", tabId);
|
|
82
|
+
block.tabIndex = 0;
|
|
83
|
+
|
|
84
|
+
// Hide the code-block-header since the tab bar replaces it
|
|
85
|
+
const header = block.querySelector<HTMLDivElement>(".code-block-header");
|
|
86
|
+
if (header) header.style.display = "none";
|
|
87
|
+
|
|
88
|
+
// Show/hide blocks
|
|
89
|
+
if (index !== 0) {
|
|
90
|
+
block.style.display = "none";
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Insert tab header at the top of the group
|
|
95
|
+
group.insertBefore(tabHeader, group.firstChild);
|
|
96
|
+
|
|
97
|
+
// Tab switching logic
|
|
98
|
+
const tabs = tabHeader.querySelectorAll<HTMLButtonElement>('[role="tab"]');
|
|
99
|
+
|
|
100
|
+
function activateTab(index: number) {
|
|
101
|
+
tabs.forEach((tab, i) => {
|
|
102
|
+
const isActive = i === index;
|
|
103
|
+
tab.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
104
|
+
tab.tabIndex = isActive ? 0 : -1;
|
|
105
|
+
});
|
|
106
|
+
codeBlocks.forEach((block, i) => {
|
|
107
|
+
block.style.display = i === index ? "" : "none";
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
tabs.forEach((tab, index) => {
|
|
112
|
+
tab.addEventListener("click", () => {
|
|
113
|
+
activateTab(index);
|
|
114
|
+
tab.focus();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Keyboard navigation
|
|
119
|
+
tabHeader.addEventListener("keydown", (e) => {
|
|
120
|
+
const currentIndex = Array.from(tabs).findIndex((t) => t === document.activeElement);
|
|
121
|
+
if (currentIndex === -1) return;
|
|
122
|
+
|
|
123
|
+
let nextIndex: number | null = null;
|
|
124
|
+
|
|
125
|
+
if (e.key === "ArrowRight") {
|
|
126
|
+
nextIndex = (currentIndex + 1) % tabs.length;
|
|
127
|
+
} else if (e.key === "ArrowLeft") {
|
|
128
|
+
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
129
|
+
} else if (e.key === "Home") {
|
|
130
|
+
nextIndex = 0;
|
|
131
|
+
} else if (e.key === "End") {
|
|
132
|
+
nextIndex = tabs.length - 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (nextIndex !== null) {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
activateTab(nextIndex);
|
|
138
|
+
tabs[nextIndex].focus();
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<!-- All global styles consolidated into one block -->
|
|
146
|
+
<style is:global>
|
|
147
|
+
.dark .code-block-group {
|
|
148
|
+
--code-block-border: oklch(0.303 0.034 264.376);
|
|
149
|
+
--code-block-header-bg: oklch(0.179 0.04 264.376);
|
|
150
|
+
--code-block-title-color: oklch(0.708 0.014 264.364);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.code-group-tabs {
|
|
154
|
+
display: flex;
|
|
155
|
+
border-bottom: 1px solid var(--code-block-border, #e5e7eb);
|
|
156
|
+
background-color: var(--code-block-header-bg, #f3f4f6);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.code-group-tabs [role="tab"] {
|
|
160
|
+
padding: 0.5rem 1rem;
|
|
161
|
+
border: none;
|
|
162
|
+
background: none;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
font-size: 0.8125rem;
|
|
165
|
+
font-weight: 500;
|
|
166
|
+
font-family: var(
|
|
167
|
+
--font-mono,
|
|
168
|
+
ui-monospace,
|
|
169
|
+
SFMono-Regular,
|
|
170
|
+
"SF Mono",
|
|
171
|
+
Menlo,
|
|
172
|
+
Consolas,
|
|
173
|
+
monospace
|
|
174
|
+
);
|
|
175
|
+
color: var(--code-block-title-color, #6b7280);
|
|
176
|
+
border-bottom: 2px solid transparent;
|
|
177
|
+
transition:
|
|
178
|
+
color 0.15s,
|
|
179
|
+
border-color 0.15s;
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.code-group-tabs [role="tab"]:hover {
|
|
184
|
+
color: var(--color-text, #374151);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.code-group-tabs [role="tab"][aria-selected="true"] {
|
|
188
|
+
color: var(--color-text, #111827);
|
|
189
|
+
border-bottom-color: var(--color-primary, #3b82f6);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.code-group-tabs [role="tab"]:focus-visible {
|
|
193
|
+
outline: 2px solid var(--color-primary, #3b82f6);
|
|
194
|
+
outline-offset: -2px;
|
|
195
|
+
}
|
|
196
|
+
</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* CodeTabs wrapper — Astro component that hydrates the CodeTabs React island.
|
|
4
|
+
*
|
|
5
|
+
* Usage in MDX:
|
|
6
|
+
*
|
|
7
|
+
* ```mdx
|
|
8
|
+
* <CodeTabs tabs={[
|
|
9
|
+
* { label: "Python", language: "python", content: "<pre>...</pre>" },
|
|
10
|
+
* { label: "Node.js", language: "nodejs", content: "<pre>...</pre>" },
|
|
11
|
+
* { label: "cURL", language: "curl", content: "<pre>...</pre>" },
|
|
12
|
+
* ]} />
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Or with a custom sync key (for independent tab groups):
|
|
16
|
+
*
|
|
17
|
+
* ```mdx
|
|
18
|
+
* <CodeTabs syncKey="api-style" tabs={[
|
|
19
|
+
* { label: "REST", language: "rest", content: "..." },
|
|
20
|
+
* { label: "GraphQL", language: "graphql", content: "..." },
|
|
21
|
+
* ]} />
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @see CodeTabs.tsx for the React island implementation
|
|
25
|
+
*/
|
|
26
|
+
import { CodeTabs as CodeTabsIsland } from "../islands/CodeTabs";
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
tabs: Array<{
|
|
30
|
+
label: string;
|
|
31
|
+
language: string;
|
|
32
|
+
content: string;
|
|
33
|
+
}>;
|
|
34
|
+
syncKey?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { tabs, syncKey } = Astro.props as Props;
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<CodeTabsIsland client:idle tabs={tabs} syncKey={syncKey} />
|
|
41
|
+
|
|
42
|
+
<style is:global>
|
|
43
|
+
/* Suppress inner code-block chrome when rendered inside CodeTabs */
|
|
44
|
+
.code-tabs-panel .code-block {
|
|
45
|
+
margin: 0;
|
|
46
|
+
border: none;
|
|
47
|
+
border-radius: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.code-tabs-panel .code-block-header {
|
|
51
|
+
display: none;
|
|
52
|
+
}
|
|
53
|
+
</style>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Footer component for the specglass theme.
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
import { config } from "virtual:specglass/config";
|
|
10
|
+
|
|
11
|
+
const currentYear = new Date().getFullYear();
|
|
12
|
+
const footerText = config.theme?.footer;
|
|
13
|
+
const headerLinks = config.theme?.headerLinks;
|
|
14
|
+
---
|
|
15
|
+
|
|
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
|
+
>
|
|
20
|
+
{
|
|
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
|
+
)
|
|
36
|
+
}
|
|
37
|
+
<p class="m-0">
|
|
38
|
+
{footerText ? footerText : `© ${currentYear} Built with specglass`}
|
|
39
|
+
</p>
|
|
40
|
+
</div>
|
|
41
|
+
</footer>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Header — Site-wide header with title/logo and navigation links.
|
|
4
|
+
*
|
|
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
|
|
7
|
+
*/
|
|
8
|
+
import { config } from "virtual:specglass/config";
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<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
|
+
data-pagefind-ignore
|
|
14
|
+
>
|
|
15
|
+
<div class="flex items-center h-full px-4 md:px-6">
|
|
16
|
+
<!-- Mobile menu button -->
|
|
17
|
+
<button
|
|
18
|
+
id="mobile-menu-toggle"
|
|
19
|
+
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"
|
|
21
|
+
aria-expanded="false"
|
|
22
|
+
aria-controls="sidebar-mobile"
|
|
23
|
+
aria-label="Toggle navigation menu"
|
|
24
|
+
>
|
|
25
|
+
<svg
|
|
26
|
+
class="h-5 w-5"
|
|
27
|
+
fill="none"
|
|
28
|
+
viewBox="0 0 24 24"
|
|
29
|
+
stroke-width="2"
|
|
30
|
+
stroke="currentColor"
|
|
31
|
+
aria-hidden="true"
|
|
32
|
+
>
|
|
33
|
+
<path
|
|
34
|
+
stroke-linecap="round"
|
|
35
|
+
stroke-linejoin="round"
|
|
36
|
+
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
|
|
37
|
+
</svg>
|
|
38
|
+
</button>
|
|
39
|
+
|
|
40
|
+
<!-- Site title / logo -->
|
|
41
|
+
<a
|
|
42
|
+
href="/"
|
|
43
|
+
class="flex items-center gap-2 font-semibold text-lg text-text hover:text-primary transition-colors no-underline"
|
|
44
|
+
>
|
|
45
|
+
{
|
|
46
|
+
config.theme?.logo && (
|
|
47
|
+
<img src={config.theme.logo} alt="" class="h-6 w-6" aria-hidden="true" />
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
<span>{config.site?.title ?? "Documentation"}</span>
|
|
51
|
+
</a>
|
|
52
|
+
|
|
53
|
+
<!-- Spacer -->
|
|
54
|
+
<div class="flex-1"></div>
|
|
55
|
+
|
|
56
|
+
<!-- Header navigation links from config -->
|
|
57
|
+
{
|
|
58
|
+
config.theme?.headerLinks && config.theme.headerLinks.length > 0 && (
|
|
59
|
+
<nav aria-label="Header navigation" class="hidden md:flex items-center gap-4 mr-4">
|
|
60
|
+
{config.theme.headerLinks.map((link) => (
|
|
61
|
+
<a
|
|
62
|
+
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"
|
|
64
|
+
{...(link.href.startsWith("http")
|
|
65
|
+
? { target: "_blank", rel: "noopener noreferrer" }
|
|
66
|
+
: {})}
|
|
67
|
+
>
|
|
68
|
+
{link.label}
|
|
69
|
+
</a>
|
|
70
|
+
))}
|
|
71
|
+
</nav>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
<!-- Right-side header actions (theme toggle, search, etc.) -->
|
|
76
|
+
<div id="header-actions" class="flex items-center gap-2">
|
|
77
|
+
<slot />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</header>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Sidebar — Recursive navigation tree for the documentation sidebar.
|
|
4
|
+
*
|
|
5
|
+
* Renders NavItem[] with collapsible sections, active page highlighting,
|
|
6
|
+
* external links, and hidden item filtering.
|
|
7
|
+
*/
|
|
8
|
+
import type { NavItem } from "@specglass/core";
|
|
9
|
+
import { isActiveOrAncestor } from "../utils/sidebar-helpers.js";
|
|
10
|
+
|
|
11
|
+
export interface Props {
|
|
12
|
+
items: NavItem[];
|
|
13
|
+
currentSlug: string;
|
|
14
|
+
depth?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { items, currentSlug, depth = 0 } = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<ul class:list={["list-none p-0 m-0", depth > 0 && "ml-3 border-l border-border pl-3"]}>
|
|
21
|
+
{
|
|
22
|
+
items
|
|
23
|
+
.filter((item) => !item.hidden)
|
|
24
|
+
.map((item) => {
|
|
25
|
+
const isActive = item.type === "page" && item.slug === currentSlug;
|
|
26
|
+
const isAncestor = item.type === "section" && isActiveOrAncestor(item, currentSlug);
|
|
27
|
+
const isCollapsed = item.collapsed && !isAncestor;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<li class="my-0.5">
|
|
31
|
+
{item.type === "external-link" && item.href ? (
|
|
32
|
+
/* External link */
|
|
33
|
+
<a
|
|
34
|
+
href={item.href}
|
|
35
|
+
target="_blank"
|
|
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"
|
|
38
|
+
>
|
|
39
|
+
<span>{item.title}</span>
|
|
40
|
+
<svg
|
|
41
|
+
class="h-3.5 w-3.5 opacity-50"
|
|
42
|
+
fill="none"
|
|
43
|
+
viewBox="0 0 24 24"
|
|
44
|
+
stroke-width="2"
|
|
45
|
+
stroke="currentColor"
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
>
|
|
48
|
+
<path
|
|
49
|
+
stroke-linecap="round"
|
|
50
|
+
stroke-linejoin="round"
|
|
51
|
+
d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
|
52
|
+
/>
|
|
53
|
+
</svg>
|
|
54
|
+
</a>
|
|
55
|
+
) : item.type === "section" ? (
|
|
56
|
+
/* Collapsible section */
|
|
57
|
+
<details open={!isCollapsed}>
|
|
58
|
+
<summary
|
|
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",
|
|
62
|
+
]}
|
|
63
|
+
>
|
|
64
|
+
{item.icon && <span class="mr-1">{item.icon}</span>}
|
|
65
|
+
<span>{item.title}</span>
|
|
66
|
+
<svg
|
|
67
|
+
class="h-3.5 w-3.5 ml-auto opacity-40 transition-transform duration-200 details-chevron"
|
|
68
|
+
fill="none"
|
|
69
|
+
viewBox="0 0 24 24"
|
|
70
|
+
stroke-width="2"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
>
|
|
74
|
+
<path
|
|
75
|
+
stroke-linecap="round"
|
|
76
|
+
stroke-linejoin="round"
|
|
77
|
+
d="M8.25 4.5l7.5 7.5-7.5 7.5"
|
|
78
|
+
/>
|
|
79
|
+
</svg>
|
|
80
|
+
</summary>
|
|
81
|
+
{item.children && (
|
|
82
|
+
<Astro.self items={item.children} currentSlug={currentSlug} depth={depth + 1} />
|
|
83
|
+
)}
|
|
84
|
+
</details>
|
|
85
|
+
) : (
|
|
86
|
+
/* Regular page link */
|
|
87
|
+
<a
|
|
88
|
+
href={`/${item.slug}`}
|
|
89
|
+
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",
|
|
94
|
+
]}
|
|
95
|
+
aria-current={isActive ? "page" : undefined}
|
|
96
|
+
>
|
|
97
|
+
{item.icon && <span class="mr-1">{item.icon}</span>}
|
|
98
|
+
<span>{item.title}</span>
|
|
99
|
+
</a>
|
|
100
|
+
)}
|
|
101
|
+
</li>
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
</ul>
|
|
106
|
+
|
|
107
|
+
<style>
|
|
108
|
+
/* Rotate chevron when details is open */
|
|
109
|
+
details[open] > summary .details-chevron {
|
|
110
|
+
transform: rotate(90deg);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* Remove default marker from summary */
|
|
114
|
+
summary::-webkit-details-marker {
|
|
115
|
+
display: none;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* TabItem component — wraps content for use inside <Tabs>.
|
|
4
|
+
*
|
|
5
|
+
* Each TabItem has a required `label` prop that becomes the tab button text.
|
|
6
|
+
* Content is rendered via <slot />.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <Tabs>
|
|
10
|
+
* <TabItem label="npm">npm install foo</TabItem>
|
|
11
|
+
* <TabItem label="yarn">yarn add foo</TabItem>
|
|
12
|
+
* </Tabs>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface Props {
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { label } = Astro.props;
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class="tab-item" data-tab-label={label}>
|
|
23
|
+
<slot />
|
|
24
|
+
</div>
|