@opendocsdev/cli 0.2.6 → 0.2.7
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/package.json +1 -1
- package/src/engine/src/components/CodeGroup.astro +0 -170
- package/src/engine/src/components/Tabs.astro +0 -74
- package/src/engine/src/components/react/CodeGroup.tsx +68 -0
- package/src/engine/src/components/react/Tab.tsx +25 -0
- package/src/engine/src/components/react/Tabs.tsx +71 -0
- package/src/engine/src/components/react/index.ts +3 -0
- package/src/engine/src/layouts/DocsLayout.astro +3 -0
- package/src/engine/src/scripts/interactive-components.ts +328 -0
- package/src/engine/src/styles/global.css +3 -3
package/package.json
CHANGED
|
@@ -59,173 +59,3 @@ const showTabs = labels.length > 1;
|
|
|
59
59
|
<slot />
|
|
60
60
|
</div>
|
|
61
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>
|
|
@@ -59,77 +59,3 @@ const showTabs = tabs.length > 1;
|
|
|
59
59
|
<slot />
|
|
60
60
|
</div>
|
|
61
61
|
</content-tabs>
|
|
62
|
-
|
|
63
|
-
<script>
|
|
64
|
-
class ContentTabsElement extends HTMLElement {
|
|
65
|
-
private panels: HTMLElement[] = [];
|
|
66
|
-
|
|
67
|
-
connectedCallback() {
|
|
68
|
-
const panelsContainer = this.querySelector(".tabs-panels");
|
|
69
|
-
if (!panelsContainer) return;
|
|
70
|
-
|
|
71
|
-
this.panels = Array.from(
|
|
72
|
-
panelsContainer.querySelectorAll(":scope > .tab-panel")
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
if (this.panels.length === 0) return;
|
|
76
|
-
|
|
77
|
-
// Initialize panels with data-active attribute
|
|
78
|
-
this.initPanels();
|
|
79
|
-
|
|
80
|
-
// If only one panel, just mark it active
|
|
81
|
-
if (this.panels.length === 1) {
|
|
82
|
-
this.panels[0].setAttribute("data-active", "true");
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Add click handlers to server-rendered buttons
|
|
87
|
-
this.attachClickHandlers();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
private initPanels() {
|
|
91
|
-
this.panels.forEach((panel, index) => {
|
|
92
|
-
panel.setAttribute("role", "tabpanel");
|
|
93
|
-
panel.setAttribute("data-active", String(index === 0));
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private attachClickHandlers() {
|
|
98
|
-
const buttons = this.querySelectorAll<HTMLButtonElement>(
|
|
99
|
-
".tabs-nav button"
|
|
100
|
-
);
|
|
101
|
-
buttons.forEach((btn, index) => {
|
|
102
|
-
btn.addEventListener("click", () => this.setActiveTab(index));
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private setActiveTab(index: number) {
|
|
107
|
-
// Update buttons
|
|
108
|
-
const buttons = this.querySelectorAll<HTMLButtonElement>(
|
|
109
|
-
".tabs-nav button"
|
|
110
|
-
);
|
|
111
|
-
buttons.forEach((btn, i) => {
|
|
112
|
-
const isActive = i === index;
|
|
113
|
-
btn.className = this.getTabClass(isActive);
|
|
114
|
-
btn.setAttribute("aria-selected", String(isActive));
|
|
115
|
-
btn.tabIndex = isActive ? 0 : -1;
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Update panels
|
|
119
|
-
this.panels.forEach((panel, i) => {
|
|
120
|
-
panel.setAttribute("data-active", String(i === index));
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
private getTabClass(isActive: boolean): string {
|
|
125
|
-
const base =
|
|
126
|
-
"px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px";
|
|
127
|
-
const active = "border-[var(--color-primary)] text-[var(--color-primary)]";
|
|
128
|
-
const inactive =
|
|
129
|
-
"border-transparent text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border)]";
|
|
130
|
-
return `${base} ${isActive ? active : inactive}`;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
customElements.define("content-tabs", ContentTabsElement);
|
|
135
|
-
</script>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
interface CodeGroupProps {
|
|
5
|
+
"data-labels"?: string;
|
|
6
|
+
"data-sync-key"?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* React version of CodeGroup for use in snippets.
|
|
13
|
+
* Renders a <div> with the same class/structure as CodeGroup.astro.
|
|
14
|
+
* Uses <div> instead of <code-group> custom element because React 18 SSR
|
|
15
|
+
* doesn't convert className to class for custom elements.
|
|
16
|
+
* The global interactive-components.ts script handles interactivity via event delegation.
|
|
17
|
+
*/
|
|
18
|
+
export function CodeGroup({
|
|
19
|
+
"data-labels": labelsJson,
|
|
20
|
+
"data-sync-key": syncKey,
|
|
21
|
+
className,
|
|
22
|
+
children,
|
|
23
|
+
}: CodeGroupProps) {
|
|
24
|
+
const labels: string[] = labelsJson ? JSON.parse(labelsJson) : [];
|
|
25
|
+
const showTabs = labels.length > 1;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
className={cn(
|
|
30
|
+
"code-group not-prose my-4 rounded-xl border border-[var(--color-border)] overflow-hidden bg-[var(--color-surface-raised)] block",
|
|
31
|
+
className
|
|
32
|
+
)}
|
|
33
|
+
data-sync-key={syncKey}
|
|
34
|
+
data-labels={labelsJson}
|
|
35
|
+
>
|
|
36
|
+
{showTabs && (
|
|
37
|
+
<nav
|
|
38
|
+
className="tabs flex gap-1 px-3 sm:px-4 pt-3 pb-0 border-b border-[var(--color-border)] overflow-x-auto"
|
|
39
|
+
role="tablist"
|
|
40
|
+
aria-label="Code examples"
|
|
41
|
+
>
|
|
42
|
+
{labels.map((label, i) => (
|
|
43
|
+
<button
|
|
44
|
+
key={label}
|
|
45
|
+
type="button"
|
|
46
|
+
role="tab"
|
|
47
|
+
className={cn(
|
|
48
|
+
"px-2 sm:px-3 py-2 text-xs sm:text-sm font-medium transition-colors -mb-px whitespace-nowrap flex-shrink-0",
|
|
49
|
+
i === 0
|
|
50
|
+
? "text-[var(--color-foreground)] border-b-2 border-[var(--color-primary)]"
|
|
51
|
+
: "text-[var(--color-muted)] hover:text-[var(--color-foreground)]"
|
|
52
|
+
)}
|
|
53
|
+
aria-selected={i === 0 ? true : false}
|
|
54
|
+
tabIndex={i === 0 ? 0 : -1}
|
|
55
|
+
data-index={i}
|
|
56
|
+
data-label={label}
|
|
57
|
+
>
|
|
58
|
+
{label}
|
|
59
|
+
</button>
|
|
60
|
+
))}
|
|
61
|
+
</nav>
|
|
62
|
+
)}
|
|
63
|
+
<div className="panels">{children}</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default CodeGroup;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cn } from "../../lib/utils";
|
|
2
|
+
|
|
3
|
+
interface TabProps {
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Tab({ label, icon, className, children }: TabProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className={cn(
|
|
14
|
+
"tab-panel prose prose-sm dark:prose-invert max-w-none",
|
|
15
|
+
className
|
|
16
|
+
)}
|
|
17
|
+
data-label={label}
|
|
18
|
+
data-icon={icon}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default Tab;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
interface TabInfo {
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface TabsProps {
|
|
10
|
+
"data-tabs"?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* React version of Tabs for use in snippets.
|
|
17
|
+
* Renders a <div> with the same class/structure as Tabs.astro.
|
|
18
|
+
* Uses <div> instead of <content-tabs> custom element because React 18 SSR
|
|
19
|
+
* doesn't convert className to class for custom elements.
|
|
20
|
+
* The global interactive-components.ts script handles interactivity via event delegation.
|
|
21
|
+
*/
|
|
22
|
+
export function Tabs({
|
|
23
|
+
"data-tabs": tabsJson,
|
|
24
|
+
className,
|
|
25
|
+
children,
|
|
26
|
+
}: TabsProps) {
|
|
27
|
+
const tabs: TabInfo[] = tabsJson ? JSON.parse(tabsJson) : [];
|
|
28
|
+
const showTabs = tabs.length > 1;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={cn("tabs-container my-6 block", className)}
|
|
33
|
+
data-tabs={tabsJson}
|
|
34
|
+
>
|
|
35
|
+
{showTabs && (
|
|
36
|
+
<nav
|
|
37
|
+
className="tabs-nav flex border-b border-[var(--color-border)]"
|
|
38
|
+
role="tablist"
|
|
39
|
+
aria-label="Content tabs"
|
|
40
|
+
>
|
|
41
|
+
{tabs.map((tab, i) => (
|
|
42
|
+
<button
|
|
43
|
+
key={tab.label}
|
|
44
|
+
type="button"
|
|
45
|
+
role="tab"
|
|
46
|
+
className={cn(
|
|
47
|
+
"px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px",
|
|
48
|
+
i === 0
|
|
49
|
+
? "border-[var(--color-primary)] text-[var(--color-primary)]"
|
|
50
|
+
: "border-transparent text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border)]"
|
|
51
|
+
)}
|
|
52
|
+
aria-selected={i === 0 ? true : false}
|
|
53
|
+
tabIndex={i === 0 ? 0 : -1}
|
|
54
|
+
data-index={i}
|
|
55
|
+
>
|
|
56
|
+
{tab.icon && (
|
|
57
|
+
<span className="mr-2" aria-hidden="true">
|
|
58
|
+
{tab.icon}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
{tab.label}
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</nav>
|
|
65
|
+
)}
|
|
66
|
+
<div className="tabs-panels pt-4">{children}</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default Tabs;
|
|
@@ -281,6 +281,9 @@ const { previous: previousPage, next: nextPage } = getPageNavigation(navigation,
|
|
|
281
281
|
<!-- Mobile sidebar toggle script -->
|
|
282
282
|
<script src="../scripts/mobile-sidebar.ts"></script>
|
|
283
283
|
|
|
284
|
+
<!-- Interactive components (CodeGroup, Tabs) - global so both Astro and snippet-rendered instances work -->
|
|
285
|
+
<script src="../scripts/interactive-components.ts"></script>
|
|
286
|
+
|
|
284
287
|
<!-- Search trigger for mobile header -->
|
|
285
288
|
<script>
|
|
286
289
|
function initSearchTriggers() {
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Web Component definitions for CodeGroup and Tabs.
|
|
3
|
+
* Also includes event-delegation handlers for React-rendered instances
|
|
4
|
+
* (React 18 SSR doesn't convert className→class for custom elements,
|
|
5
|
+
* so React versions render <div> instead).
|
|
6
|
+
*
|
|
7
|
+
* Loaded in the layout so both Astro and React-rendered instances work.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// Shared helpers
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
const CODE_TAB_BASE =
|
|
15
|
+
"px-2 sm:px-3 py-2 text-xs sm:text-sm font-medium transition-colors -mb-px whitespace-nowrap flex-shrink-0";
|
|
16
|
+
const CODE_TAB_ACTIVE =
|
|
17
|
+
"text-[var(--color-foreground)] border-b-2 border-[var(--color-primary)]";
|
|
18
|
+
const CODE_TAB_INACTIVE =
|
|
19
|
+
"text-[var(--color-muted)] hover:text-[var(--color-foreground)]";
|
|
20
|
+
|
|
21
|
+
const TABS_TAB_BASE =
|
|
22
|
+
"px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px";
|
|
23
|
+
const TABS_TAB_ACTIVE =
|
|
24
|
+
"border-[var(--color-primary)] text-[var(--color-primary)]";
|
|
25
|
+
const TABS_TAB_INACTIVE =
|
|
26
|
+
"border-transparent text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border)]";
|
|
27
|
+
|
|
28
|
+
function codeTabClass(active: boolean) {
|
|
29
|
+
return `${CODE_TAB_BASE} ${active ? CODE_TAB_ACTIVE : CODE_TAB_INACTIVE}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function tabsTabClass(active: boolean) {
|
|
33
|
+
return `${TABS_TAB_BASE} ${active ? TABS_TAB_ACTIVE : TABS_TAB_INACTIVE}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findPanels(container: Element): HTMLElement[] {
|
|
37
|
+
const pc = container.querySelector(".panels");
|
|
38
|
+
if (!pc) return [];
|
|
39
|
+
let panels = Array.from(pc.querySelectorAll<HTMLElement>(":scope > pre"));
|
|
40
|
+
if (panels.length === 0) {
|
|
41
|
+
panels = Array.from(pc.querySelectorAll<HTMLElement>("pre"));
|
|
42
|
+
}
|
|
43
|
+
return panels;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setCodeGroupActive(
|
|
47
|
+
cg: Element,
|
|
48
|
+
panels: HTMLElement[],
|
|
49
|
+
index: number
|
|
50
|
+
) {
|
|
51
|
+
cg.querySelectorAll<HTMLButtonElement>(".tabs button").forEach((btn, i) => {
|
|
52
|
+
const active = i === index;
|
|
53
|
+
btn.className = codeTabClass(active);
|
|
54
|
+
btn.setAttribute("aria-selected", String(active));
|
|
55
|
+
btn.tabIndex = active ? 0 : -1;
|
|
56
|
+
});
|
|
57
|
+
panels.forEach((p, i) =>
|
|
58
|
+
p.setAttribute("data-active", String(i === index))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setTabsActive(ct: Element, panels: HTMLElement[], index: number) {
|
|
63
|
+
ct.querySelectorAll<HTMLButtonElement>(".tabs-nav button").forEach(
|
|
64
|
+
(btn, i) => {
|
|
65
|
+
const active = i === index;
|
|
66
|
+
btn.className = tabsTabClass(active);
|
|
67
|
+
btn.setAttribute("aria-selected", String(active));
|
|
68
|
+
btn.tabIndex = active ? 0 : -1;
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
panels.forEach((p, i) =>
|
|
72
|
+
p.setAttribute("data-active", String(i === index))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// CodeGroup Web Component (for Astro-rendered <code-group>)
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
interface CodeGroupSyncEvent extends CustomEvent {
|
|
81
|
+
detail: { syncKey: string; label: string };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!customElements.get("code-group")) {
|
|
85
|
+
class CodeGroupElement extends HTMLElement {
|
|
86
|
+
private syncKey = "";
|
|
87
|
+
private labels: string[] = [];
|
|
88
|
+
private panels: HTMLElement[] = [];
|
|
89
|
+
private boundSync: (e: Event) => void;
|
|
90
|
+
private boundStorage: (e: StorageEvent) => void;
|
|
91
|
+
|
|
92
|
+
constructor() {
|
|
93
|
+
super();
|
|
94
|
+
this.boundSync = this.handleSync.bind(this);
|
|
95
|
+
this.boundStorage = this.handleStorage.bind(this);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
connectedCallback() {
|
|
99
|
+
this.labels = this.dataset.labels
|
|
100
|
+
? JSON.parse(this.dataset.labels)
|
|
101
|
+
: [];
|
|
102
|
+
this.syncKey = this.dataset.syncKey || "";
|
|
103
|
+
this.panels = findPanels(this);
|
|
104
|
+
if (this.panels.length === 0) return;
|
|
105
|
+
|
|
106
|
+
this.panels.forEach((p, i) => {
|
|
107
|
+
p.setAttribute("role", "tabpanel");
|
|
108
|
+
p.setAttribute("data-active", String(i === 0));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (this.panels.length === 1) return;
|
|
112
|
+
|
|
113
|
+
this.querySelectorAll<HTMLButtonElement>(".tabs button").forEach(
|
|
114
|
+
(btn) => {
|
|
115
|
+
btn.addEventListener("click", () => {
|
|
116
|
+
const label = btn.dataset.label;
|
|
117
|
+
if (label) {
|
|
118
|
+
this.activateLabel(label);
|
|
119
|
+
this.broadcast(label);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
this.restore();
|
|
126
|
+
window.addEventListener("codegroup-sync", this.boundSync);
|
|
127
|
+
window.addEventListener("storage", this.boundStorage);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
disconnectedCallback() {
|
|
131
|
+
window.removeEventListener("codegroup-sync", this.boundSync);
|
|
132
|
+
window.removeEventListener("storage", this.boundStorage);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private activateLabel(label: string) {
|
|
136
|
+
const i = this.labels.indexOf(label);
|
|
137
|
+
if (i >= 0) setCodeGroupActive(this, this.panels, i);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private broadcast(label: string) {
|
|
141
|
+
if (!this.syncKey) return;
|
|
142
|
+
try {
|
|
143
|
+
localStorage.setItem(`codegroup:${this.syncKey}`, label);
|
|
144
|
+
} catch {}
|
|
145
|
+
window.dispatchEvent(
|
|
146
|
+
new CustomEvent("codegroup-sync", {
|
|
147
|
+
detail: { syncKey: this.syncKey, label },
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private restore() {
|
|
153
|
+
if (!this.syncKey) return;
|
|
154
|
+
try {
|
|
155
|
+
const saved = localStorage.getItem(`codegroup:${this.syncKey}`);
|
|
156
|
+
if (saved && this.labels.includes(saved)) this.activateLabel(saved);
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private handleSync(e: Event) {
|
|
161
|
+
const { syncKey, label } = (e as CodeGroupSyncEvent).detail;
|
|
162
|
+
if (syncKey === this.syncKey && this.labels.includes(label))
|
|
163
|
+
this.activateLabel(label);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private handleStorage(e: StorageEvent) {
|
|
167
|
+
if (!e.key?.startsWith("codegroup:")) return;
|
|
168
|
+
const sk = e.key.replace("codegroup:", "");
|
|
169
|
+
if (sk === this.syncKey && e.newValue) this.activateLabel(e.newValue);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
customElements.define("code-group", CodeGroupElement);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============================================
|
|
177
|
+
// Tabs Web Component (for Astro-rendered <content-tabs>)
|
|
178
|
+
// ============================================
|
|
179
|
+
|
|
180
|
+
if (!customElements.get("content-tabs")) {
|
|
181
|
+
class ContentTabsElement extends HTMLElement {
|
|
182
|
+
private panels: HTMLElement[] = [];
|
|
183
|
+
|
|
184
|
+
connectedCallback() {
|
|
185
|
+
const pc = this.querySelector(".tabs-panels");
|
|
186
|
+
if (!pc) return;
|
|
187
|
+
this.panels = Array.from(
|
|
188
|
+
pc.querySelectorAll<HTMLElement>(":scope > .tab-panel")
|
|
189
|
+
);
|
|
190
|
+
if (this.panels.length === 0) return;
|
|
191
|
+
|
|
192
|
+
this.panels.forEach((p, i) => {
|
|
193
|
+
p.setAttribute("role", "tabpanel");
|
|
194
|
+
p.setAttribute("data-active", String(i === 0));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (this.panels.length === 1) return;
|
|
198
|
+
|
|
199
|
+
this.querySelectorAll<HTMLButtonElement>(".tabs-nav button").forEach(
|
|
200
|
+
(btn, index) => {
|
|
201
|
+
btn.addEventListener("click", () =>
|
|
202
|
+
setTabsActive(this, this.panels, index)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
customElements.define("content-tabs", ContentTabsElement);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================
|
|
213
|
+
// Event delegation for React-rendered instances
|
|
214
|
+
// (div.code-group and div.tabs-container)
|
|
215
|
+
// ============================================
|
|
216
|
+
|
|
217
|
+
function initDivCodeGroups() {
|
|
218
|
+
document
|
|
219
|
+
.querySelectorAll<HTMLElement>(".code-group:not(code-group)")
|
|
220
|
+
.forEach((cg) => {
|
|
221
|
+
if (cg.dataset.cgInit) return;
|
|
222
|
+
cg.dataset.cgInit = "1";
|
|
223
|
+
|
|
224
|
+
const labels: string[] = cg.dataset.labels
|
|
225
|
+
? JSON.parse(cg.dataset.labels)
|
|
226
|
+
: [];
|
|
227
|
+
const syncKey = cg.dataset.syncKey || "";
|
|
228
|
+
const panels = findPanels(cg);
|
|
229
|
+
if (panels.length === 0) return;
|
|
230
|
+
|
|
231
|
+
panels.forEach((p, i) => {
|
|
232
|
+
p.setAttribute("role", "tabpanel");
|
|
233
|
+
p.setAttribute("data-active", String(i === 0));
|
|
234
|
+
});
|
|
235
|
+
if (panels.length === 1) return;
|
|
236
|
+
|
|
237
|
+
cg.querySelectorAll<HTMLButtonElement>(".tabs button").forEach((btn) => {
|
|
238
|
+
btn.addEventListener("click", () => {
|
|
239
|
+
const label = btn.dataset.label;
|
|
240
|
+
if (!label) return;
|
|
241
|
+
const idx = labels.indexOf(label);
|
|
242
|
+
if (idx < 0) return;
|
|
243
|
+
setCodeGroupActive(cg, panels, idx);
|
|
244
|
+
if (syncKey) {
|
|
245
|
+
try {
|
|
246
|
+
localStorage.setItem(`codegroup:${syncKey}`, label);
|
|
247
|
+
} catch {}
|
|
248
|
+
window.dispatchEvent(
|
|
249
|
+
new CustomEvent("codegroup-sync", {
|
|
250
|
+
detail: { syncKey, label },
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Restore from localStorage
|
|
258
|
+
if (syncKey) {
|
|
259
|
+
try {
|
|
260
|
+
const saved = localStorage.getItem(`codegroup:${syncKey}`);
|
|
261
|
+
if (saved && labels.includes(saved)) {
|
|
262
|
+
const idx = labels.indexOf(saved);
|
|
263
|
+
if (idx >= 0) setCodeGroupActive(cg, panels, idx);
|
|
264
|
+
}
|
|
265
|
+
} catch {}
|
|
266
|
+
|
|
267
|
+
// Cross-instance sync
|
|
268
|
+
window.addEventListener("codegroup-sync", (e: Event) => {
|
|
269
|
+
const { syncKey: sk, label } = (e as CodeGroupSyncEvent).detail;
|
|
270
|
+
if (sk === syncKey && labels.includes(label)) {
|
|
271
|
+
const idx = labels.indexOf(label);
|
|
272
|
+
if (idx >= 0) setCodeGroupActive(cg, panels, idx);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
window.addEventListener("storage", (e: StorageEvent) => {
|
|
277
|
+
if (e.key === `codegroup:${syncKey}` && e.newValue) {
|
|
278
|
+
const idx = labels.indexOf(e.newValue);
|
|
279
|
+
if (idx >= 0) setCodeGroupActive(cg, panels, idx);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function initDivTabs() {
|
|
287
|
+
document
|
|
288
|
+
.querySelectorAll<HTMLElement>(".tabs-container:not(content-tabs)")
|
|
289
|
+
.forEach((ct) => {
|
|
290
|
+
if (ct.dataset.tabsInit) return;
|
|
291
|
+
ct.dataset.tabsInit = "1";
|
|
292
|
+
|
|
293
|
+
const pc = ct.querySelector(".tabs-panels");
|
|
294
|
+
if (!pc) return;
|
|
295
|
+
const panels = Array.from(
|
|
296
|
+
pc.querySelectorAll<HTMLElement>(":scope > .tab-panel")
|
|
297
|
+
);
|
|
298
|
+
if (panels.length === 0) return;
|
|
299
|
+
|
|
300
|
+
panels.forEach((p, i) => {
|
|
301
|
+
p.setAttribute("role", "tabpanel");
|
|
302
|
+
p.setAttribute("data-active", String(i === 0));
|
|
303
|
+
});
|
|
304
|
+
if (panels.length === 1) return;
|
|
305
|
+
|
|
306
|
+
ct.querySelectorAll<HTMLButtonElement>(".tabs-nav button").forEach(
|
|
307
|
+
(btn, index) => {
|
|
308
|
+
btn.addEventListener("click", () =>
|
|
309
|
+
setTabsActive(ct, panels, index)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function initAll() {
|
|
317
|
+
initDivCodeGroups();
|
|
318
|
+
initDivTabs();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (document.readyState === "loading") {
|
|
322
|
+
document.addEventListener("DOMContentLoaded", initAll);
|
|
323
|
+
} else {
|
|
324
|
+
initAll();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Re-initialize after Astro view transitions
|
|
328
|
+
document.addEventListener("astro:after-swap", initAll);
|
|
@@ -415,17 +415,17 @@
|
|
|
415
415
|
/* Handle both direct children and nested pre elements (MDX may wrap them) */
|
|
416
416
|
.code-group .panels > pre:not(:first-child),
|
|
417
417
|
.code-group .panels pre:not(:first-of-type) {
|
|
418
|
-
|
|
418
|
+
display: none; /* Hide non-first panels by default (prevents flash before JS) */
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
.code-group .panels > pre[data-active="true"],
|
|
422
422
|
.code-group .panels pre[data-active="true"] {
|
|
423
|
-
|
|
423
|
+
display: block;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
426
|
.code-group .panels > pre[data-active="false"],
|
|
427
427
|
.code-group .panels pre[data-active="false"] {
|
|
428
|
-
|
|
428
|
+
display: none;
|
|
429
429
|
}
|
|
430
430
|
|
|
431
431
|
/* Tabs component styles - visibility for slotted tab panels */
|