@opendocsdev/cli 0.2.0
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/LICENSE +661 -0
- package/README.md +300 -0
- package/dist/bin/opendocs.js +712 -0
- package/dist/bin/opendocs.js.map +1 -0
- package/dist/templates/api-reference.mdx +308 -0
- package/dist/templates/components.mdx +286 -0
- package/dist/templates/configuration.mdx +190 -0
- package/dist/templates/docs.json +27 -0
- package/dist/templates/introduction.mdx +25 -0
- package/dist/templates/logo.svg +4 -0
- package/dist/templates/quickstart.mdx +59 -0
- package/dist/templates/writing-content.mdx +236 -0
- package/package.json +92 -0
- package/src/engine/astro.config.ts +75 -0
- package/src/engine/src/components/Analytics.astro +57 -0
- package/src/engine/src/components/ApiPlayground.astro +24 -0
- package/src/engine/src/components/Callout.astro +66 -0
- package/src/engine/src/components/Card.astro +75 -0
- package/src/engine/src/components/CardGroup.astro +29 -0
- package/src/engine/src/components/CodeGroup.astro +231 -0
- package/src/engine/src/components/CopyButton.astro +179 -0
- package/src/engine/src/components/Steps.astro +27 -0
- package/src/engine/src/components/Tab.astro +21 -0
- package/src/engine/src/components/TableOfContents.astro +119 -0
- package/src/engine/src/components/Tabs.astro +135 -0
- package/src/engine/src/components/index.ts +107 -0
- package/src/engine/src/components/react/ApiPlayground/AuthSection.tsx +91 -0
- package/src/engine/src/components/react/ApiPlayground/CodeBlock.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CodeSnippets.tsx +66 -0
- package/src/engine/src/components/react/ApiPlayground/CollapsibleSection.tsx +26 -0
- package/src/engine/src/components/react/ApiPlayground/KeyValueEditor.tsx +58 -0
- package/src/engine/src/components/react/ApiPlayground/ResponseDisplay.tsx +109 -0
- package/src/engine/src/components/react/ApiPlayground/Spinner.tsx +6 -0
- package/src/engine/src/components/react/ApiPlayground/constants.ts +16 -0
- package/src/engine/src/components/react/ApiPlayground/generators.test.ts +130 -0
- package/src/engine/src/components/react/ApiPlayground/generators.ts +75 -0
- package/src/engine/src/components/react/ApiPlayground/index.tsx +490 -0
- package/src/engine/src/components/react/ApiPlayground/types.ts +35 -0
- package/src/engine/src/components/react/Callout.tsx +54 -0
- package/src/engine/src/components/react/Card.tsx +48 -0
- package/src/engine/src/components/react/CardGroup.tsx +24 -0
- package/src/engine/src/components/react/FeedbackWidget.tsx +166 -0
- package/src/engine/src/components/react/GitHubLink.tsx +28 -0
- package/src/engine/src/components/react/NavigationCard.tsx +53 -0
- package/src/engine/src/components/react/PageActions.tsx +124 -0
- package/src/engine/src/components/react/PageFooter.tsx +91 -0
- package/src/engine/src/components/react/SearchModal.tsx +358 -0
- package/src/engine/src/components/react/SearchProvider.tsx +37 -0
- package/src/engine/src/components/react/Sidebar.tsx +369 -0
- package/src/engine/src/components/react/SidebarSearchTrigger.tsx +57 -0
- package/src/engine/src/components/react/Steps.tsx +25 -0
- package/src/engine/src/components/react/ThemeToggle.tsx +72 -0
- package/src/engine/src/components/react/index.ts +14 -0
- package/src/engine/src/env.d.ts +10 -0
- package/src/engine/src/layouts/DocsLayout.astro +357 -0
- package/src/engine/src/lib/__tests__/markdown.test.ts +124 -0
- package/src/engine/src/lib/__tests__/mdx-loader.test.ts +205 -0
- package/src/engine/src/lib/config.ts +79 -0
- package/src/engine/src/lib/markdown.ts +54 -0
- package/src/engine/src/lib/mdx-loader.ts +143 -0
- package/src/engine/src/lib/mdx-utils.ts +72 -0
- package/src/engine/src/lib/remark-opendocs.ts +195 -0
- package/src/engine/src/lib/utils.ts +221 -0
- package/src/engine/src/pages/[...slug].astro +115 -0
- package/src/engine/src/pages/index.astro +71 -0
- package/src/engine/src/scripts/mobile-sidebar.ts +56 -0
- package/src/engine/src/scripts/theme-init.ts +25 -0
- package/src/engine/src/styles/global.css +703 -0
- package/src/engine/tailwind.config.mjs +60 -0
- package/src/engine/tsconfig.json +15 -0
- package/src/templates/api-reference.mdx +308 -0
- package/src/templates/components.mdx +286 -0
- package/src/templates/configuration.mdx +190 -0
- package/src/templates/docs.json +27 -0
- package/src/templates/introduction.mdx +25 -0
- package/src/templates/logo.svg +4 -0
- package/src/templates/quickstart.mdx +59 -0
- package/src/templates/writing-content.mdx +236 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* CodeGroup - Tabbed code block component
|
|
4
|
+
* Server-renders tab buttons to eliminate FOUC
|
|
5
|
+
* Uses Web Components for click handlers and localStorage sync
|
|
6
|
+
*/
|
|
7
|
+
interface Props {
|
|
8
|
+
"data-labels"?: string;
|
|
9
|
+
"data-sync-key"?: string;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
"data-labels": labelsJson,
|
|
15
|
+
"data-sync-key": syncKey,
|
|
16
|
+
class: className,
|
|
17
|
+
} = Astro.props;
|
|
18
|
+
|
|
19
|
+
// Parse labels from remark plugin (or empty array for fallback)
|
|
20
|
+
const labels: string[] = labelsJson ? JSON.parse(labelsJson) : [];
|
|
21
|
+
const showTabs = labels.length > 1;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<code-group
|
|
25
|
+
class:list={[
|
|
26
|
+
"code-group not-prose my-4 rounded-xl border border-[var(--color-border)] overflow-hidden bg-[var(--color-surface-raised)] block",
|
|
27
|
+
className,
|
|
28
|
+
]}
|
|
29
|
+
data-sync-key={syncKey}
|
|
30
|
+
data-labels={labelsJson}
|
|
31
|
+
>
|
|
32
|
+
{showTabs && (
|
|
33
|
+
<nav
|
|
34
|
+
class="tabs flex gap-1 px-3 sm:px-4 pt-3 pb-0 border-b border-[var(--color-border)] overflow-x-auto"
|
|
35
|
+
role="tablist"
|
|
36
|
+
aria-label="Code examples"
|
|
37
|
+
>
|
|
38
|
+
{labels.map((label, i) => (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
role="tab"
|
|
42
|
+
class:list={[
|
|
43
|
+
"px-2 sm:px-3 py-2 text-xs sm:text-sm font-medium transition-colors -mb-px whitespace-nowrap flex-shrink-0",
|
|
44
|
+
i === 0
|
|
45
|
+
? "text-[var(--color-foreground)] border-b-2 border-[var(--color-primary)]"
|
|
46
|
+
: "text-[var(--color-muted)] hover:text-[var(--color-foreground)]",
|
|
47
|
+
]}
|
|
48
|
+
aria-selected={i === 0 ? "true" : "false"}
|
|
49
|
+
tabindex={i === 0 ? 0 : -1}
|
|
50
|
+
data-index={i}
|
|
51
|
+
data-label={label}
|
|
52
|
+
>
|
|
53
|
+
{label}
|
|
54
|
+
</button>
|
|
55
|
+
))}
|
|
56
|
+
</nav>
|
|
57
|
+
)}
|
|
58
|
+
<div class="panels">
|
|
59
|
+
<slot />
|
|
60
|
+
</div>
|
|
61
|
+
</code-group>
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
interface CodeGroupSyncEvent extends CustomEvent {
|
|
65
|
+
detail: {
|
|
66
|
+
syncKey: string;
|
|
67
|
+
label: string;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class CodeGroupElement extends HTMLElement {
|
|
72
|
+
private syncKey: string = "";
|
|
73
|
+
private labels: string[] = [];
|
|
74
|
+
private panels: HTMLPreElement[] = [];
|
|
75
|
+
private boundHandleSync: (e: Event) => void;
|
|
76
|
+
private boundHandleStorage: (e: StorageEvent) => void;
|
|
77
|
+
|
|
78
|
+
constructor() {
|
|
79
|
+
super();
|
|
80
|
+
this.boundHandleSync = this.handleSync.bind(this);
|
|
81
|
+
this.boundHandleStorage = this.handleStorage.bind(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
connectedCallback() {
|
|
85
|
+
// Parse labels and sync key
|
|
86
|
+
const labelsJson = this.dataset.labels;
|
|
87
|
+
this.labels = labelsJson ? JSON.parse(labelsJson) : [];
|
|
88
|
+
this.syncKey = this.dataset.syncKey || "";
|
|
89
|
+
|
|
90
|
+
const panelsContainer = this.querySelector(".panels");
|
|
91
|
+
if (!panelsContainer) return;
|
|
92
|
+
|
|
93
|
+
// Find all pre elements (code blocks)
|
|
94
|
+
this.panels = Array.from(panelsContainer.querySelectorAll(":scope > pre"));
|
|
95
|
+
if (this.panels.length === 0) {
|
|
96
|
+
this.panels = Array.from(panelsContainer.querySelectorAll("pre"));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (this.panels.length === 0) return;
|
|
100
|
+
|
|
101
|
+
// Initialize panels with data-active attribute
|
|
102
|
+
this.initPanels();
|
|
103
|
+
|
|
104
|
+
// If only one panel, just mark it active
|
|
105
|
+
if (this.panels.length === 1) {
|
|
106
|
+
this.panels[0].setAttribute("data-active", "true");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Add click handlers to server-rendered buttons
|
|
111
|
+
this.attachClickHandlers();
|
|
112
|
+
|
|
113
|
+
// Restore saved selection from localStorage
|
|
114
|
+
this.restoreFromStorage();
|
|
115
|
+
|
|
116
|
+
// Listen for sync events from other CodeGroups on the same page
|
|
117
|
+
window.addEventListener("codegroup-sync", this.boundHandleSync);
|
|
118
|
+
|
|
119
|
+
// Listen for storage events (cross-tab sync)
|
|
120
|
+
window.addEventListener("storage", this.boundHandleStorage);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
disconnectedCallback() {
|
|
124
|
+
window.removeEventListener("codegroup-sync", this.boundHandleSync);
|
|
125
|
+
window.removeEventListener("storage", this.boundHandleStorage);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private initPanels() {
|
|
129
|
+
this.panels.forEach((panel, index) => {
|
|
130
|
+
panel.setAttribute("role", "tabpanel");
|
|
131
|
+
panel.setAttribute("data-active", String(index === 0));
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private attachClickHandlers() {
|
|
136
|
+
const buttons = this.querySelectorAll<HTMLButtonElement>(".tabs button");
|
|
137
|
+
buttons.forEach((btn) => {
|
|
138
|
+
btn.addEventListener("click", () => {
|
|
139
|
+
const label = btn.dataset.label;
|
|
140
|
+
if (label) {
|
|
141
|
+
this.setActiveByLabel(label);
|
|
142
|
+
this.broadcastChange(label);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private restoreFromStorage() {
|
|
149
|
+
if (!this.syncKey) return;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const saved = localStorage.getItem(`codegroup:${this.syncKey}`);
|
|
153
|
+
if (saved && this.labels.includes(saved)) {
|
|
154
|
+
this.setActiveByLabel(saved);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// localStorage may be unavailable (private browsing, etc.)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private handleSync(e: Event) {
|
|
162
|
+
const event = e as CodeGroupSyncEvent;
|
|
163
|
+
const { syncKey, label } = event.detail;
|
|
164
|
+
if (syncKey === this.syncKey && this.labels.includes(label)) {
|
|
165
|
+
this.setActiveByLabel(label);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private handleStorage(e: StorageEvent) {
|
|
170
|
+
if (!e.key?.startsWith("codegroup:")) return;
|
|
171
|
+
|
|
172
|
+
const storedSyncKey = e.key.replace("codegroup:", "");
|
|
173
|
+
if (storedSyncKey === this.syncKey && e.newValue) {
|
|
174
|
+
this.setActiveByLabel(e.newValue);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private broadcastChange(label: string) {
|
|
179
|
+
if (!this.syncKey) return;
|
|
180
|
+
|
|
181
|
+
// Save to localStorage
|
|
182
|
+
try {
|
|
183
|
+
localStorage.setItem(`codegroup:${this.syncKey}`, label);
|
|
184
|
+
} catch {
|
|
185
|
+
// localStorage may be unavailable
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Notify other CodeGroups on the same page
|
|
189
|
+
window.dispatchEvent(
|
|
190
|
+
new CustomEvent("codegroup-sync", {
|
|
191
|
+
detail: { syncKey: this.syncKey, label },
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private setActiveByLabel(label: string) {
|
|
197
|
+
const index = this.labels.indexOf(label);
|
|
198
|
+
if (index >= 0) {
|
|
199
|
+
this.setActiveTab(index);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private setActiveTab(index: number) {
|
|
204
|
+
// Update buttons
|
|
205
|
+
const buttons = this.querySelectorAll<HTMLButtonElement>(".tabs button");
|
|
206
|
+
buttons.forEach((btn, i) => {
|
|
207
|
+
const isActive = i === index;
|
|
208
|
+
btn.className = this.getTabClass(isActive);
|
|
209
|
+
btn.setAttribute("aria-selected", String(isActive));
|
|
210
|
+
btn.tabIndex = isActive ? 0 : -1;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Update panels
|
|
214
|
+
this.panels.forEach((panel, i) => {
|
|
215
|
+
panel.setAttribute("data-active", String(i === index));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private getTabClass(isActive: boolean): string {
|
|
220
|
+
const base =
|
|
221
|
+
"px-2 sm:px-3 py-2 text-xs sm:text-sm font-medium transition-colors -mb-px whitespace-nowrap flex-shrink-0";
|
|
222
|
+
const active =
|
|
223
|
+
"text-[var(--color-foreground)] border-b-2 border-[var(--color-primary)]";
|
|
224
|
+
const inactive =
|
|
225
|
+
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]";
|
|
226
|
+
return `${base} ${isActive ? active : inactive}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
customElements.define("code-group", CodeGroupElement);
|
|
231
|
+
</script>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
// CopyButton component - enhances code blocks with headers and copy buttons
|
|
3
|
+
// This component should be included once in the layout
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<script>
|
|
7
|
+
function initCodeBlocks() {
|
|
8
|
+
// Find all pre elements that haven't been enhanced yet
|
|
9
|
+
document.querySelectorAll("pre:not([data-enhanced])").forEach((pre) => {
|
|
10
|
+
// Skip if inside a code-group (those handle their own styling)
|
|
11
|
+
if (pre.closest(".code-group")) {
|
|
12
|
+
// Just add copy button to code-group pre elements
|
|
13
|
+
addCopyButtonToCodeGroup(pre as HTMLPreElement);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pre.setAttribute("data-enhanced", "true");
|
|
18
|
+
|
|
19
|
+
// Get title from data-title attribute (set by Shiki transformer from meta string)
|
|
20
|
+
const title = pre.getAttribute("data-title");
|
|
21
|
+
|
|
22
|
+
// Get language from code element
|
|
23
|
+
const codeEl = pre.querySelector("code");
|
|
24
|
+
const langClass = codeEl
|
|
25
|
+
? Array.from(codeEl.classList).find((c) => c.startsWith("language-"))
|
|
26
|
+
: null;
|
|
27
|
+
const language = langClass ? langClass.replace("language-", "") : null;
|
|
28
|
+
|
|
29
|
+
// Create wrapper
|
|
30
|
+
const wrapper = document.createElement("div");
|
|
31
|
+
wrapper.className =
|
|
32
|
+
"code-block-wrapper not-prose my-4 rounded-xl border border-[var(--color-border)] overflow-hidden bg-[var(--color-surface-raised)]";
|
|
33
|
+
|
|
34
|
+
// Determine display label
|
|
35
|
+
const displayLabel = title || language;
|
|
36
|
+
|
|
37
|
+
if (displayLabel) {
|
|
38
|
+
// Create header with filename and copy button
|
|
39
|
+
const header = document.createElement("div");
|
|
40
|
+
header.className =
|
|
41
|
+
"code-block-header flex items-center justify-between px-4 py-2.5 border-b border-[var(--color-border)]";
|
|
42
|
+
|
|
43
|
+
// Left side - icon and title
|
|
44
|
+
const leftSide = document.createElement("div");
|
|
45
|
+
leftSide.className =
|
|
46
|
+
"flex items-center gap-2 text-sm text-[var(--color-muted)]";
|
|
47
|
+
leftSide.innerHTML = `
|
|
48
|
+
<svg class="w-4 h-4 flex-shrink-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
49
|
+
<polyline points="16 18 22 12 16 6"/>
|
|
50
|
+
<polyline points="8 6 2 12 8 18"/>
|
|
51
|
+
</svg>
|
|
52
|
+
<span class="font-medium">${displayLabel}</span>
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
// Right side - copy button
|
|
56
|
+
const copyBtn = createCopyButton();
|
|
57
|
+
copyBtn.classList.remove("opacity-0", "group-hover:opacity-100");
|
|
58
|
+
|
|
59
|
+
header.appendChild(leftSide);
|
|
60
|
+
header.appendChild(copyBtn);
|
|
61
|
+
wrapper.appendChild(header);
|
|
62
|
+
|
|
63
|
+
// Setup copy functionality
|
|
64
|
+
setupCopyHandler(copyBtn, pre as HTMLPreElement);
|
|
65
|
+
} else {
|
|
66
|
+
// No title - add copy button that appears on hover
|
|
67
|
+
wrapper.classList.add("group", "relative");
|
|
68
|
+
const copyBtn = createCopyButton();
|
|
69
|
+
copyBtn.classList.add("absolute", "top-3", "right-3");
|
|
70
|
+
|
|
71
|
+
wrapper.appendChild(copyBtn);
|
|
72
|
+
setupCopyHandler(copyBtn, pre as HTMLPreElement);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Insert wrapper and move pre inside
|
|
76
|
+
pre.parentNode?.insertBefore(wrapper, pre);
|
|
77
|
+
wrapper.appendChild(pre);
|
|
78
|
+
|
|
79
|
+
// Style the pre element inside wrapper
|
|
80
|
+
(pre as HTMLElement).style.margin = "0";
|
|
81
|
+
(pre as HTMLElement).style.borderRadius = "0";
|
|
82
|
+
(pre as HTMLElement).style.border = "none";
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function addCopyButtonToCodeGroup(pre: HTMLPreElement) {
|
|
87
|
+
if (pre.hasAttribute("data-has-copy")) return;
|
|
88
|
+
pre.setAttribute("data-has-copy", "true");
|
|
89
|
+
|
|
90
|
+
const wrapper = document.createElement("div");
|
|
91
|
+
wrapper.className = "relative group";
|
|
92
|
+
|
|
93
|
+
const copyBtn = createCopyButton();
|
|
94
|
+
copyBtn.classList.add("absolute", "top-3", "right-3");
|
|
95
|
+
|
|
96
|
+
pre.parentNode?.insertBefore(wrapper, pre);
|
|
97
|
+
wrapper.appendChild(pre);
|
|
98
|
+
wrapper.appendChild(copyBtn);
|
|
99
|
+
|
|
100
|
+
setupCopyHandler(copyBtn, pre);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createCopyButton(): HTMLButtonElement {
|
|
104
|
+
const button = document.createElement("button");
|
|
105
|
+
button.type = "button";
|
|
106
|
+
button.className =
|
|
107
|
+
"copy-button p-1.5 rounded-md text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-surface-sunken)] transition-colors opacity-0 group-hover:opacity-100";
|
|
108
|
+
button.setAttribute("aria-label", "Copy code");
|
|
109
|
+
button.innerHTML = `
|
|
110
|
+
<svg class="w-4 h-4 copy-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
111
|
+
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
|
|
112
|
+
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
|
|
113
|
+
</svg>
|
|
114
|
+
<svg class="w-4 h-4 check-icon hidden text-[var(--color-success)]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
115
|
+
<path d="M20 6 9 17l-5-5"/>
|
|
116
|
+
</svg>
|
|
117
|
+
`;
|
|
118
|
+
return button;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function setupCopyHandler(button: HTMLButtonElement, pre: HTMLPreElement) {
|
|
122
|
+
button.addEventListener("click", async () => {
|
|
123
|
+
const code = pre.querySelector("code");
|
|
124
|
+
const text = code ? code.textContent : pre.textContent;
|
|
125
|
+
|
|
126
|
+
if (!text) return;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await navigator.clipboard.writeText(text);
|
|
130
|
+
|
|
131
|
+
// Show success state
|
|
132
|
+
const copyIcon = button.querySelector(".copy-icon");
|
|
133
|
+
const checkIcon = button.querySelector(".check-icon");
|
|
134
|
+
|
|
135
|
+
copyIcon?.classList.add("hidden");
|
|
136
|
+
checkIcon?.classList.remove("hidden");
|
|
137
|
+
|
|
138
|
+
// Reset after 2 seconds
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
copyIcon?.classList.remove("hidden");
|
|
141
|
+
checkIcon?.classList.add("hidden");
|
|
142
|
+
}, 2000);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error("Failed to copy code:", err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Initialize on DOMContentLoaded and after page transitions
|
|
150
|
+
if (document.readyState === "loading") {
|
|
151
|
+
document.addEventListener("DOMContentLoaded", initCodeBlocks);
|
|
152
|
+
} else {
|
|
153
|
+
initCodeBlocks();
|
|
154
|
+
}
|
|
155
|
+
document.addEventListener("astro:after-swap", initCodeBlocks);
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<style is:global>
|
|
159
|
+
/* Code block wrapper styles */
|
|
160
|
+
.code-block-wrapper pre {
|
|
161
|
+
margin: 0 !important;
|
|
162
|
+
border-radius: 0 !important;
|
|
163
|
+
border: none !important;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* Ensure copy button is always visible in header */
|
|
167
|
+
.code-block-header .copy-button {
|
|
168
|
+
opacity: 1 !important;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* Code group copy button positioning */
|
|
172
|
+
.code-group .copy-button {
|
|
173
|
+
z-index: 10;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.code-group-panels > .relative {
|
|
177
|
+
position: relative;
|
|
178
|
+
}
|
|
179
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Steps - Numbered step list with connecting line
|
|
4
|
+
* Pure Astro component - no client-side JavaScript needed
|
|
5
|
+
*/
|
|
6
|
+
interface Props {
|
|
7
|
+
title?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { title, class: className } = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class:list={["steps my-8 not-prose", className]}>
|
|
15
|
+
{title && (
|
|
16
|
+
<h4 class="steps-title text-lg font-semibold text-[var(--color-foreground)] mb-4">
|
|
17
|
+
{title}
|
|
18
|
+
</h4>
|
|
19
|
+
)}
|
|
20
|
+
<div class="steps-list relative pl-12">
|
|
21
|
+
<div
|
|
22
|
+
class="steps-line absolute left-4 top-4 bottom-4 w-0.5 bg-[var(--color-border)]"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
/>
|
|
25
|
+
<slot />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Tab - Individual tab panel for use inside <Tabs>
|
|
4
|
+
* Renders as a div with data attributes for the parent Tabs component to read
|
|
5
|
+
*/
|
|
6
|
+
export interface Props {
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { label, icon, class: className } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class:list={["tab-panel prose prose-sm dark:prose-invert max-w-none", className]}
|
|
17
|
+
data-label={label}
|
|
18
|
+
data-icon={icon}
|
|
19
|
+
>
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* TableOfContents - Displays page headings for navigation
|
|
4
|
+
* Highlights active heading based on scroll position
|
|
5
|
+
*/
|
|
6
|
+
interface Heading {
|
|
7
|
+
depth: number;
|
|
8
|
+
slug: string;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
headings: Heading[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { headings } = Astro.props;
|
|
17
|
+
|
|
18
|
+
// Filter to only show h2 and h3 headings for the table of contents
|
|
19
|
+
const tocHeadings = headings.filter((h) => h.depth >= 2 && h.depth <= 3);
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
{tocHeadings.length > 0 && (
|
|
23
|
+
<nav class="toc toc-nav" aria-label="Table of contents">
|
|
24
|
+
<h2 class="text-xs font-semibold uppercase tracking-widest text-[var(--color-muted)] mb-4">
|
|
25
|
+
On This Page
|
|
26
|
+
</h2>
|
|
27
|
+
<ul class="space-y-0.5 text-sm">
|
|
28
|
+
{tocHeadings.map((heading) => (
|
|
29
|
+
<li>
|
|
30
|
+
<a
|
|
31
|
+
href={`#${heading.slug}`}
|
|
32
|
+
class:list={[
|
|
33
|
+
"toc-link block transition-all py-1.5 border-l-[3px] border-transparent rounded-r-sm",
|
|
34
|
+
heading.depth === 2
|
|
35
|
+
? "pl-3 text-[var(--color-foreground)] font-medium"
|
|
36
|
+
: "pl-6 text-[var(--color-muted)] text-[13px]",
|
|
37
|
+
]}
|
|
38
|
+
data-slug={heading.slug}
|
|
39
|
+
data-depth={heading.depth}
|
|
40
|
+
>
|
|
41
|
+
{heading.text}
|
|
42
|
+
</a>
|
|
43
|
+
</li>
|
|
44
|
+
))}
|
|
45
|
+
</ul>
|
|
46
|
+
</nav>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<script>
|
|
50
|
+
// Highlight active heading based on scroll position
|
|
51
|
+
function highlightActiveHeading() {
|
|
52
|
+
const headings = document.querySelectorAll("article h2[id], article h3[id]");
|
|
53
|
+
const tocLinks = document.querySelectorAll(".toc-link");
|
|
54
|
+
|
|
55
|
+
if (headings.length === 0 || tocLinks.length === 0) return;
|
|
56
|
+
|
|
57
|
+
const windowHeight = window.innerHeight;
|
|
58
|
+
|
|
59
|
+
// Find the active heading by checking which one is in view
|
|
60
|
+
let activeSlug: string | null = null;
|
|
61
|
+
|
|
62
|
+
headings.forEach((heading) => {
|
|
63
|
+
const rect = heading.getBoundingClientRect();
|
|
64
|
+
// Consider a heading active if it's in the top third of the viewport
|
|
65
|
+
if (rect.top <= windowHeight / 3) {
|
|
66
|
+
activeSlug = heading.id;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// If no heading is in view yet, activate the first TOC link
|
|
71
|
+
if (!activeSlug && tocLinks.length > 0) {
|
|
72
|
+
const firstLink = tocLinks[0] as HTMLElement;
|
|
73
|
+
activeSlug = firstLink.dataset.slug || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Update active state using CSS class
|
|
77
|
+
tocLinks.forEach((link) => {
|
|
78
|
+
const linkSlug = (link as HTMLElement).dataset.slug;
|
|
79
|
+
|
|
80
|
+
if (linkSlug === activeSlug) {
|
|
81
|
+
link.classList.add("active");
|
|
82
|
+
} else {
|
|
83
|
+
link.classList.remove("active");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function initTocHighlight() {
|
|
89
|
+
// Run on scroll with throttling
|
|
90
|
+
let ticking = false;
|
|
91
|
+
|
|
92
|
+
const scrollHandler = () => {
|
|
93
|
+
if (!ticking) {
|
|
94
|
+
window.requestAnimationFrame(() => {
|
|
95
|
+
highlightActiveHeading();
|
|
96
|
+
ticking = false;
|
|
97
|
+
});
|
|
98
|
+
ticking = true;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
window.addEventListener("scroll", scrollHandler);
|
|
103
|
+
|
|
104
|
+
// Run immediately
|
|
105
|
+
highlightActiveHeading();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Initialize on page load
|
|
109
|
+
if (document.readyState === "loading") {
|
|
110
|
+
document.addEventListener("DOMContentLoaded", initTocHighlight);
|
|
111
|
+
} else {
|
|
112
|
+
initTocHighlight();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Reinitialize after View Transitions
|
|
116
|
+
document.addEventListener("astro:after-swap", () => {
|
|
117
|
+
highlightActiveHeading();
|
|
118
|
+
});
|
|
119
|
+
</script>
|