@takazudo/zudo-doc 0.2.22 → 1.0.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.
Files changed (103) hide show
  1. package/bin/zudo-doc.mjs +83 -0
  2. package/dist/eject/index.d.ts +33 -0
  3. package/dist/eject/index.js +229 -0
  4. package/dist/plugins/routes.d.ts +2 -0
  5. package/dist/plugins/routes.js +100 -0
  6. package/dist/preset.d.ts +47 -1
  7. package/dist/preset.js +29 -13
  8. package/dist/routes/404.d.ts +7 -0
  9. package/dist/routes/404.js +47 -0
  10. package/dist/routes/_chrome.d.ts +32 -0
  11. package/dist/routes/_chrome.js +365 -0
  12. package/dist/routes/_context.d.ts +56 -0
  13. package/dist/routes/_context.js +222 -0
  14. package/dist/routes/_docs-helpers.d.ts +43 -0
  15. package/dist/routes/_docs-helpers.js +173 -0
  16. package/dist/routes/_virtual.d.js +0 -0
  17. package/dist/routes/api-ai-chat.d.ts +5 -0
  18. package/dist/routes/api-ai-chat.js +24 -0
  19. package/dist/routes/docs-slug.d.ts +21 -0
  20. package/dist/routes/docs-slug.js +26 -0
  21. package/dist/routes/docs-tags-index.d.ts +7 -0
  22. package/dist/routes/docs-tags-index.js +11 -0
  23. package/dist/routes/docs-tags-tag.d.ts +23 -0
  24. package/dist/routes/docs-tags-tag.js +19 -0
  25. package/dist/routes/docs-versions.d.ts +7 -0
  26. package/dist/routes/docs-versions.js +11 -0
  27. package/dist/routes/index.d.ts +7 -0
  28. package/dist/routes/index.js +117 -0
  29. package/dist/routes/locale-docs-slug.d.ts +27 -0
  30. package/dist/routes/locale-docs-slug.js +40 -0
  31. package/dist/routes/locale-docs-tags-index.d.ts +18 -0
  32. package/dist/routes/locale-docs-tags-index.js +15 -0
  33. package/dist/routes/locale-docs-tags-tag.d.ts +25 -0
  34. package/dist/routes/locale-docs-tags-tag.js +22 -0
  35. package/dist/routes/locale-docs-versions.d.ts +18 -0
  36. package/dist/routes/locale-docs-versions.js +15 -0
  37. package/dist/routes/locale-index.d.ts +24 -0
  38. package/dist/routes/locale-index.js +132 -0
  39. package/dist/routes/robots.txt.d.ts +3 -0
  40. package/dist/routes/robots.txt.js +8 -0
  41. package/dist/routes/sitemap.xml.d.ts +5 -0
  42. package/dist/routes/sitemap.xml.js +31 -0
  43. package/dist/routes/v-docs-slug.d.ts +27 -0
  44. package/dist/routes/v-docs-slug.js +34 -0
  45. package/dist/routes/v-locale-docs-slug.d.ts +31 -0
  46. package/dist/routes/v-locale-docs-slug.js +50 -0
  47. package/dist/safelist.css +1 -1
  48. package/dist/settings.d.ts +15 -0
  49. package/eject/breadcrumb/breadcrumb.tsx +194 -0
  50. package/eject/breadcrumb/find-path.ts +26 -0
  51. package/eject/breadcrumb/index.ts +4 -0
  52. package/eject/breadcrumb/types.ts +32 -0
  53. package/eject/code-group/index.tsx +78 -0
  54. package/eject/content-admonition/index.tsx +50 -0
  55. package/eject/details/details.tsx +34 -0
  56. package/eject/details/index.ts +4 -0
  57. package/eject/doc-pager/index.tsx +91 -0
  58. package/eject/footer/footer.tsx +167 -0
  59. package/eject/footer/index.ts +19 -0
  60. package/eject/footer/types.ts +60 -0
  61. package/eject/header/header.tsx +725 -0
  62. package/eject/header/index.ts +36 -0
  63. package/eject/header/nav-active.ts +122 -0
  64. package/eject/header/nav-overflow-script.ts +302 -0
  65. package/eject/header/right-items.ts +78 -0
  66. package/eject/header/types.ts +69 -0
  67. package/eject/page-loading/index.ts +10 -0
  68. package/eject/page-loading/page-loading-overlay.tsx +103 -0
  69. package/eject/sidebar/index.ts +27 -0
  70. package/eject/sidebar/sidebar.tsx +137 -0
  71. package/eject/sidebar/types.ts +73 -0
  72. package/eject/tab-item/index.ts +7 -0
  73. package/eject/tab-item/tab-item.tsx +70 -0
  74. package/eject/theme-toggle/color-scheme-sync.ts +73 -0
  75. package/eject/theme-toggle/index.tsx +120 -0
  76. package/eject/toc/cx.ts +38 -0
  77. package/eject/toc/index.ts +11 -0
  78. package/eject/toc/mobile-toc.tsx +133 -0
  79. package/eject/toc/toc-title.ts +33 -0
  80. package/eject/toc/toc.tsx +119 -0
  81. package/eject/toc/types.ts +14 -0
  82. package/eject/toc/use-active-heading.ts +162 -0
  83. package/package.json +87 -7
  84. package/routes-src/404.tsx +49 -0
  85. package/routes-src/_chrome.tsx +470 -0
  86. package/routes-src/_context.ts +314 -0
  87. package/routes-src/_docs-helpers.ts +284 -0
  88. package/routes-src/_virtual.d.ts +15 -0
  89. package/routes-src/api-ai-chat.tsx +46 -0
  90. package/routes-src/docs-slug.tsx +43 -0
  91. package/routes-src/docs-tags-index.tsx +14 -0
  92. package/routes-src/docs-tags-tag.tsx +29 -0
  93. package/routes-src/docs-versions.tsx +15 -0
  94. package/routes-src/index.tsx +134 -0
  95. package/routes-src/locale-docs-slug.tsx +69 -0
  96. package/routes-src/locale-docs-tags-index.tsx +23 -0
  97. package/routes-src/locale-docs-tags-tag.tsx +39 -0
  98. package/routes-src/locale-docs-versions.tsx +23 -0
  99. package/routes-src/locale-index.tsx +150 -0
  100. package/routes-src/robots.txt.tsx +13 -0
  101. package/routes-src/sitemap.xml.tsx +48 -0
  102. package/routes-src/v-docs-slug.tsx +64 -0
  103. package/routes-src/v-locale-docs-slug.tsx +85 -0
@@ -0,0 +1,103 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+
4
+ // Server-rendered, zero-hydration full-page loading overlay.
5
+ //
6
+ // Renders three things into the document:
7
+ //
8
+ // 1. A fixed-position overlay `<div>` with a centered spinner. Hidden
9
+ // by default (`opacity: 0; pointer-events: none;`) and made
10
+ // visible by adding `data-visible` to it.
11
+ // 2. A `<style>` block that owns the overlay + spinner CSS plus the
12
+ // `prefers-reduced-motion` fallback. Inlined via
13
+ // `dangerouslySetInnerHTML` (matching the ColorSchemeProvider
14
+ // pattern) so no separate stylesheet has to be wired up.
15
+ // 3. A small `<script>` that toggles the `data-visible` attribute on
16
+ // navigation lifecycle events. The event names come from the
17
+ // `@takazudo/zudo-doc/transitions` module — this component does
18
+ // not reach for the underlying browser event names directly. Under
19
+ // zfb's Strategy B SPA navigation, the v2 vocabulary resolves to
20
+ // `zfb:before-preparation` (BEFORE_NAVIGATE_EVENT) and
21
+ // `zfb:after-swap` (AFTER_NAVIGATE_EVENT); see
22
+ // `transitions/page-events.ts` for rationale.
23
+ //
24
+ // The component is intentionally not hydrated. Hydrating Preact just to
25
+ // attach two listeners would be wasteful given the original Astro file
26
+ // shipped a tiny imperative script — this port keeps that profile.
27
+
28
+ import {
29
+ AFTER_NAVIGATE_EVENT,
30
+ BEFORE_NAVIGATE_EVENT,
31
+ } from "../transitions/page-events.js";
32
+
33
+ /** Default `id` for the overlay element. Stable so test rigs can target it. */
34
+ export const PAGE_LOADING_OVERLAY_ID = "page-loading-overlay";
35
+
36
+ export interface PageLoadingOverlayProps {
37
+ /**
38
+ * Override the DOM `id` used by both the overlay element and the
39
+ * bootstrap script. Useful when multiple overlays could co-exist on
40
+ * the page (e.g. tests). Defaults to `PAGE_LOADING_OVERLAY_ID`.
41
+ */
42
+ id?: string;
43
+ }
44
+
45
+ /**
46
+ * Build the inline bootstrap script body. Exported so test rigs can
47
+ * inspect / snapshot it without rendering the component.
48
+ */
49
+ export function buildPageLoadingOverlayBootstrap(overlayId: string): string {
50
+ // Values are inlined as JSON literals so the script is fully
51
+ // self-contained and matches the `define:vars` shape used elsewhere
52
+ // (see ColorSchemeProvider). Event names come from the transitions
53
+ // module's exported constants — no raw `astro:*` strings live in
54
+ // this file.
55
+ const id = JSON.stringify(overlayId);
56
+ const before = JSON.stringify(BEFORE_NAVIGATE_EVENT);
57
+ const after = JSON.stringify(AFTER_NAVIGATE_EVENT);
58
+ return `(function(){
59
+ var id=${id};
60
+ function show(){var el=document.getElementById(id);if(!el)return;el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
61
+ function hide(){var el=document.getElementById(id);if(!el)return;el.removeAttribute("data-visible");el.setAttribute("aria-hidden","true");}
62
+ function setPending(ev){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});var src=ev&&ev.sourceElement;if(src&&src instanceof Element)src.setAttribute("data-zd-nav-pending","");}
63
+ function clearPending(){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});}
64
+ document.addEventListener(${before},show);
65
+ document.addEventListener(${after},hide);
66
+ document.addEventListener(${before},setPending);
67
+ document.addEventListener(${after},clearPending);
68
+ })();`;
69
+ }
70
+
71
+ /**
72
+ * Full-page loading overlay shown during view-transition navigations.
73
+ *
74
+ * Mount this once per layout (typically inside `DocLayoutWithDefaults`'s
75
+ * `bodyEnd` slot, alongside the existing body-end providers). It is
76
+ * server-rendered and self-wires its visibility — no hydration needed.
77
+ *
78
+ * CSS lives in the host project's `src/styles/global.css` (`.page-loading-overlay`,
79
+ * `.page-loading-spinner`, `[data-zd-nav-pending]` rules) rather than in an
80
+ * inline `<style>` block here — a `<style>` inside `<body>` violates HTML5
81
+ * element-permitted-content and fails html-validate (same fix applied to the
82
+ * version-switcher in zudolab/zudo-doc#1505; regression caught in W2A #1543).
83
+ */
84
+ export default function PageLoadingOverlay({
85
+ id = PAGE_LOADING_OVERLAY_ID,
86
+ }: PageLoadingOverlayProps = {}) {
87
+ return (
88
+ <>
89
+ <div
90
+ id={id}
91
+ class="page-loading-overlay"
92
+ aria-hidden="true"
93
+ >
94
+ <span class="page-loading-spinner" />
95
+ </div>
96
+ <script
97
+ dangerouslySetInnerHTML={{
98
+ __html: buildPageLoadingOverlayBootstrap(id),
99
+ }}
100
+ />
101
+ </>
102
+ );
103
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Public entry for the framework-agnostic sidebar shell.
3
+ *
4
+ * Consumers import from `@takazudo/zudo-doc/sidebar`:
5
+ *
6
+ * import {
7
+ * Sidebar,
8
+ * type SidebarProps,
9
+ * type SidebarNavNode,
10
+ * type SidebarRootMenuItem,
11
+ * type SidebarLocaleLink,
12
+ * type SidebarTreeIslandProps,
13
+ * } from "@takazudo/zudo-doc/sidebar";
14
+ *
15
+ * The shell does NOT include the SidebarTree island itself — the host
16
+ * project's existing Preact island is plugged in via the
17
+ * `treeComponent` prop (or rendered as `children`).
18
+ */
19
+
20
+ export { Sidebar } from "./sidebar.js";
21
+ export type { SidebarProps } from "./sidebar.js";
22
+ export type {
23
+ SidebarLocaleLink,
24
+ SidebarNavNode,
25
+ SidebarRootMenuItem,
26
+ SidebarTreeIslandProps,
27
+ } from "./types.js";
@@ -0,0 +1,137 @@
1
+ "use client";
2
+
3
+ /** @jsxRuntime automatic */
4
+ /** @jsxImportSource preact */
5
+
6
+ // JSX port of src/components/sidebar.
7
+ //
8
+ // The original Astro template was almost entirely data assembly —
9
+ // build root-menu items from `settings.headerNav`, load the locale's
10
+ // docs collection, build the nav tree for the active section, optionally
11
+ // remap hrefs for versioned routes, and finally render the project's
12
+ // `<SidebarTree client:load .../>` Preact island.
13
+ //
14
+ // All of that data-prep depends on host-only helpers (`@/config/i18n`,
15
+ // `@/utils/sidebar`, `@/utils/locale-docs`, `@/utils/base`,
16
+ // `@/config/settings`) that v2 must not import. This shell therefore
17
+ // keeps the data-prep on the host side and only provides:
18
+ //
19
+ // - typed props that match the SidebarTree island's prop shape, so
20
+ // the host's prep code can be typed against v2's interface;
21
+ // - a `treeComponent` prop the host supplies (their existing Preact
22
+ // island) — the shell instantiates it with the typed props and
23
+ // forwards everything;
24
+ // - a `children` fallback for callers that want to render the tree
25
+ // themselves (useful for tests, fixtures, and downstream projects
26
+ // that swap the tree out wholesale).
27
+ //
28
+ // Wave 13 ("smoke-toc duplicate-nav regression follow-on", zudolab/zudo-doc#1355):
29
+ // before this refactor, this module exported a `Sidebar` wrapper that
30
+ // itself called `Island({when:"load", children:<SidebarInner/>})` and
31
+ // returned the wrapped div. The zfb island scanner picked the exported
32
+ // `Sidebar` (the wrapper) as the bundle's hydration target, so on the
33
+ // client `mountIslands` ran `hydrate(<Sidebar/>, dataIslandDiv)` where
34
+ // the rendered vnode itself emitted *another* `<div data-zfb-island=
35
+ // "Sidebar"><SidebarInner/></div>` inside the existing one. Unlike the
36
+ // `Toc` / `MobileToc` case (where the inner re-rendered identical
37
+ // content alongside the SSR'd content, producing a visible duplicate),
38
+ // the duplicate here was silent: the host passes `treeComponent` (a
39
+ // Preact component function) as a prop, and `island.ts`'s
40
+ // `captureSerializableProps` does `JSON.stringify` on the props bag,
41
+ // which silently drops function values. At hydrate time the
42
+ // deserialized props had no `treeComponent`, `SidebarInner` returned
43
+ // `null`, and the appended wrapper-div was empty. Net result: an extra
44
+ // dead-mount data-zfb-island Sidebar div alongside the SSR'd tree, with
45
+ // no observable content — but a wasted hydration slot, and the actual
46
+ // user-facing event-handler / state work was being done entirely by
47
+ // the host's `SidebarTree` island registration.
48
+ //
49
+ // Fix: same shape as Toc / MobileToc — drop the `Island`-wrapping
50
+ // outer, export the bare component (was `SidebarInner`) as `Sidebar`,
51
+ // pin `displayName` explicitly. The `<Island when="load">` wrapper is
52
+ // applied at the call site (`<DocLayoutWithDefaults>`).
53
+
54
+ import type { ComponentChildren, FunctionComponent, VNode } from "preact";
55
+
56
+ import type { SidebarTreeIslandProps } from "./types.js";
57
+
58
+ export interface SidebarProps extends SidebarTreeIslandProps {
59
+ /**
60
+ * The host's SidebarTree component. When present, the shell renders
61
+ * `<TreeComponent {...islandProps} />` with the typed props below.
62
+ * Mutually exclusive with `children`: prefer this form so v2 owns the
63
+ * prop forwarding contract.
64
+ */
65
+ treeComponent?: FunctionComponent<SidebarTreeIslandProps>;
66
+ /**
67
+ * Pre-rendered tree content — falls back to this when
68
+ * `treeComponent` is omitted. Useful for tests and for layouts that
69
+ * compose the tree separately (e.g. inside a host wrapper
70
+ * that needs `client:load` on the actual island element).
71
+ */
72
+ children?: ComponentChildren;
73
+ }
74
+
75
+ /**
76
+ * Sidebar shell — typed wrapper around the SidebarTree island.
77
+ *
78
+ * Two usage shapes (matching the breadcrumb shell's pattern):
79
+ *
80
+ * 1. Pass `treeComponent` plus the data props (`nodes`,
81
+ * `rootMenuItems`, `localeLinks`, …). The shell forwards them all
82
+ * to the supplied component.
83
+ *
84
+ * 2. Pass `children` (e.g. a pre-instantiated `<SidebarTree
85
+ * client:load … />` from an Astro wrapper). The shell renders
86
+ * them as-is — the typed island props are then unused but kept on
87
+ * the type signature so call-sites stay self-documenting.
88
+ *
89
+ * **The caller is responsible for wrapping this in `<Island when="load">`**
90
+ * so the SSG output emits the `data-zfb-island="Sidebar"` hydration
91
+ * marker around the rendered tree. `<DocLayoutWithDefaults>` does this
92
+ * for you; consumers who render `<Sidebar>` outside the default layout
93
+ * (e.g. via the `sidebarOverride` prop or in a custom layout) must
94
+ * apply the wrapper themselves — otherwise no hydration marker is
95
+ * emitted, and the runtime walks past the SSR'd tree without claiming
96
+ * it as an island. Either way the actual user-facing interactivity
97
+ * comes from the `treeComponent`'s own island registration; the
98
+ * `<Island>` wrapper here exists so the shell can host data-only
99
+ * variants in the future without losing the marker contract.
100
+ */
101
+ export function Sidebar(props: SidebarProps): VNode | null {
102
+ const {
103
+ treeComponent: TreeComponent,
104
+ children,
105
+ nodes,
106
+ currentSlug,
107
+ rootMenuItems,
108
+ backToMenuLabel,
109
+ localeLinks,
110
+ themeDefaultMode,
111
+ } = props;
112
+
113
+ if (TreeComponent) {
114
+ return (
115
+ <TreeComponent
116
+ nodes={nodes}
117
+ currentSlug={currentSlug}
118
+ rootMenuItems={rootMenuItems}
119
+ backToMenuLabel={backToMenuLabel}
120
+ localeLinks={localeLinks}
121
+ themeDefaultMode={themeDefaultMode}
122
+ />
123
+ );
124
+ }
125
+
126
+ if (children !== undefined && children !== null) {
127
+ return <>{children}</>;
128
+ }
129
+
130
+ return null;
131
+ }
132
+ // Pin the marker name to "Sidebar" regardless of how Preact's compat
133
+ // layer or esbuild minification renames the function. Island's SSR
134
+ // pass reads `displayName ?? name` to derive the marker string;
135
+ // pinning both means the SSG marker stays `data-zfb-island="Sidebar"`
136
+ // independent of the bundle's symbol-renaming.
137
+ Sidebar.displayName = "Sidebar";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Public types for the framework-agnostic sidebar shell.
3
+ *
4
+ * The original `sidebar` mixed three concerns:
5
+ * 1. Building `rootMenuItems` from `settings.headerNav` (with i18n
6
+ * label resolution and version-aware hrefs).
7
+ * 2. Building the doc tree via `loadLocaleDocs` +
8
+ * `buildSidebarForSection`, then optionally remapping hrefs to
9
+ * versioned paths.
10
+ * 3. Rendering a `<SidebarTree client:load .../>` Preact island.
11
+ *
12
+ * Concerns (1) and (2) depend on host-only helpers that v2 must not
13
+ * reach into. The v2 shell exists to:
14
+ * - publish the prop shape so the host's data prep code can be
15
+ * typed against a v2-owned interface, and
16
+ * - render whatever the host hands in (typically the project's
17
+ * `<SidebarTree client:load .../>` island) inside a typed wrapper.
18
+ *
19
+ * The structural types here mirror `src/utils/docs.ts` (`NavNode`) and
20
+ * `src/types/locale.ts` (`LocaleLink`) so the host can keep using its
21
+ * existing data-prep pipeline without re-mapping fields.
22
+ */
23
+
24
+ /**
25
+ * One node in the navigation tree the SidebarTree island renders.
26
+ * Matches `NavNode` in the host project's `src/utils/docs.ts`.
27
+ */
28
+ export interface SidebarNavNode {
29
+ slug: string;
30
+ label: string;
31
+ description?: string;
32
+ position: number;
33
+ href?: string;
34
+ hasPage: boolean;
35
+ children: SidebarNavNode[];
36
+ sortOrder?: "asc" | "desc";
37
+ collapsed?: boolean;
38
+ }
39
+
40
+ /**
41
+ * Item in the root-level "Docusaurus-style" menu shown above the
42
+ * doc tree on mobile / inside the back-to-menu view.
43
+ */
44
+ export interface SidebarRootMenuItem {
45
+ label: string;
46
+ href: string;
47
+ children?: SidebarRootMenuItem[];
48
+ }
49
+
50
+ /**
51
+ * Locale switcher link rendered inside the mobile sidebar footer.
52
+ * Matches `LocaleLink` in the host project's `src/types/locale.ts`.
53
+ */
54
+ export interface SidebarLocaleLink {
55
+ code: string;
56
+ label: string;
57
+ href: string;
58
+ active: boolean;
59
+ }
60
+
61
+ /**
62
+ * Props the SidebarTree island consumes. The shell forwards these
63
+ * to whichever component the host plugs in via {@link SidebarProps.treeComponent}
64
+ * (or to nothing, when the host renders its own tree as `children`).
65
+ */
66
+ export interface SidebarTreeIslandProps {
67
+ nodes: SidebarNavNode[];
68
+ currentSlug?: string;
69
+ rootMenuItems?: SidebarRootMenuItem[];
70
+ backToMenuLabel?: string;
71
+ localeLinks?: SidebarLocaleLink[];
72
+ themeDefaultMode?: "light" | "dark";
73
+ }
@@ -0,0 +1,7 @@
1
+ // E5 framework primitives — tab-item visual wrapper.
2
+ //
3
+ // Subpath: `@takazudo/zudo-doc/tab-item`. JSX port of
4
+ // the legacy `tab-item` component from the host project.
5
+
6
+ export { TabItem, default } from "./tab-item.js";
7
+ export type { TabItemProps } from "./tab-item.js";
@@ -0,0 +1,70 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+
4
+ // JSX port of the legacy `tab-item` component.
5
+ //
6
+ // Visual wrapper for one tab panel inside a Tabs group. Server-rendered,
7
+ // no hydration. The companion Tabs island reads `data-tab-value` /
8
+ // `data-tab-label` / `data-tab-default` to wire interactivity, so the
9
+ // attribute shape here must mirror the Astro original byte-for-byte.
10
+
11
+ import type { ComponentChildren } from "preact";
12
+
13
+ export interface TabItemProps {
14
+ /** Human-readable label rendered by the parent Tabs trigger row. */
15
+ label: string;
16
+ /**
17
+ * Optional stable value for the tab. Falls back to `label` when
18
+ * omitted, matching the Astro original's `value ?? label`.
19
+ */
20
+ value?: string;
21
+ /**
22
+ * When true, this panel is the initially-active one. Encoded as the
23
+ * empty-string `data-tab-default` attribute so plain attribute
24
+ * selectors (`[data-tab-default]`) still match.
25
+ */
26
+ default?: boolean;
27
+ /** Panel body. */
28
+ children?: ComponentChildren;
29
+ }
30
+
31
+ /**
32
+ * One tab panel.
33
+ *
34
+ * Wave 11 (zudolab/zudo-doc#1355): the default panel is rendered
35
+ * **without** the `hidden` attribute so the SSR HTML already shows the
36
+ * correct active panel before any JS runs. Non-default panels keep
37
+ * `hidden`. This avoids the post-Wave-10 regression where the tabs
38
+ * init script could land after first paint and flash an empty tab
39
+ * group; it also gives the no-JS path a usable default tab.
40
+ *
41
+ * The companion `TabsInit` script remains the source of truth at
42
+ * runtime — it re-derives the active tab from `localStorage` and
43
+ * `[data-tab-default]` and reapplies `hidden` / `aria-selected` so
44
+ * the SSR-rendered defaults survive group sync and View Transitions.
45
+ */
46
+ export function TabItem({
47
+ label,
48
+ value,
49
+ default: isDefault,
50
+ children,
51
+ }: TabItemProps) {
52
+ return (
53
+ <div
54
+ class="tab-panel"
55
+ role="tabpanel"
56
+ data-tab-value={value ?? label}
57
+ data-tab-label={label}
58
+ // `data-tab-default={undefined}` omits the attribute entirely,
59
+ // matching the Astro template's conditional spread.
60
+ data-tab-default={isDefault ? "" : undefined}
61
+ // Render `hidden` only on non-default panels so the SSR markup
62
+ // shows the active panel immediately (no JS-required flash).
63
+ hidden={isDefault ? undefined : true}
64
+ >
65
+ {children}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ export default TabItem;
@@ -0,0 +1,73 @@
1
+ // Shared color-scheme state helpers for ThemeToggle (#2012 E3).
2
+ //
3
+ // Two ThemeToggle instances can be mounted at once (header + mobile
4
+ // sidebar footer). Each instance holds its own `mode` state, so a
5
+ // toggle in one used to leave the other's icon stale — the instances
6
+ // only read the DOM at mount. These helpers centralise the write path
7
+ // (`applyColorScheme`) and give every instance a subscription point
8
+ // (`subscribeColorSchemeChanged`) keyed on the `color-scheme-changed`
9
+ // window event, which `applyColorScheme` dispatches after mutating the
10
+ // DOM. The same event is already consumed by the zdtp design-token
11
+ // panel, so the event name is a cross-package contract — do not rename.
12
+ //
13
+ // This module is intentionally NOT marked "use client": zfb's island
14
+ // scanner registers every exported binding of a "use client" file as an
15
+ // island, and these helpers are plain functions, not components. They
16
+ // run in the browser only (called from the ThemeToggle island and
17
+ // unit tests).
18
+
19
+ export type ColorSchemeMode = "light" | "dark";
20
+
21
+ export const COLOR_SCHEME_CHANGED_EVENT = "color-scheme-changed";
22
+
23
+ const STORAGE_KEY = "zudo-doc-theme";
24
+
25
+ /**
26
+ * Read the active color scheme from `<html data-theme>`. Falls back to
27
+ * `defaultMode` when the attribute is missing or holds an unexpected
28
+ * value (e.g. before the ColorSchemeProvider bootstrap script ran).
29
+ */
30
+ export function readColorSchemeFromDom(
31
+ defaultMode: ColorSchemeMode,
32
+ ): ColorSchemeMode {
33
+ const actual = document.documentElement.getAttribute("data-theme");
34
+ return actual === "light" || actual === "dark" ? actual : defaultMode;
35
+ }
36
+
37
+ /**
38
+ * Apply `next` as the active color scheme: mutate the DOM, persist the
39
+ * preference, and notify every subscriber (including other mounted
40
+ * ThemeToggle instances and the zdtp design-token panel) via the
41
+ * `color-scheme-changed` window event.
42
+ *
43
+ * Tweak-state reconciliation is intentionally NOT done here (#2037). The zdtp
44
+ * panel owns its own storage lifecycle: it persists the unified tweak envelope
45
+ * under `zudo-doc-tweak-state-v3` (auto-migrating the legacy
46
+ * `zudo-doc-tweak-state-v2` / `zudo-doc-tweak-state` keys into it), and its own
47
+ * `color-scheme-changed` listener clears applied inline styles and re-seeds the
48
+ * color slice from the newly active scheme. An earlier version of this function
49
+ * deleted `zudo-doc-tweak-state` + `-v2` on every toggle, which (a) targeted
50
+ * stale keys after zdtp moved to v3 — so it no longer did anything — and
51
+ * (b) when it did fire, wiped the whole envelope including scheme-independent
52
+ * spacing/typography/size tweaks, contradicting the documented carry-over
53
+ * guarantee. So the host no longer touches zdtp's private storage keys.
54
+ *
55
+ * Whether palette tweaks should instead persist per-scheme (so a light/dark
56
+ * round-trip keeps them) is a zdtp design question tracked upstream at
57
+ * Takazudo/zudo-design-token-panel#343. See zudo-doc#2037.
58
+ */
59
+ export function applyColorScheme(next: ColorSchemeMode): void {
60
+ document.documentElement.setAttribute("data-theme", next);
61
+ document.documentElement.style.colorScheme = next;
62
+ localStorage.setItem(STORAGE_KEY, next);
63
+ window.dispatchEvent(new CustomEvent(COLOR_SCHEME_CHANGED_EVENT));
64
+ }
65
+
66
+ /**
67
+ * Subscribe to color-scheme changes. Returns an unsubscribe function
68
+ * (suitable as a `useEffect` cleanup).
69
+ */
70
+ export function subscribeColorSchemeChanged(listener: () => void): () => void {
71
+ window.addEventListener(COLOR_SCHEME_CHANGED_EVENT, listener);
72
+ return () => window.removeEventListener(COLOR_SCHEME_CHANGED_EVENT, listener);
73
+ }
@@ -0,0 +1,120 @@
1
+ "use client";
2
+
3
+ /** @jsxRuntime automatic */
4
+ /** @jsxImportSource preact */
5
+ // BARE (non-island-wrapped) theme toggle — the single ThemeToggle
6
+ // implementation (#2012 E2). Published as the dedicated
7
+ // `@takazudo/zudo-doc/theme-toggle` subpath so hosts can compose it
8
+ // into their own `<Island>` wrappers (or nest it inside another island,
9
+ // e.g. the mobile sidebar footer) without inheriting an extra island
10
+ // layer. The island-wrapped variant for the `./theme` barrel lives in
11
+ // `../theme/theme-toggle.tsx`, which wraps this component.
12
+ //
13
+ // Use the preact hook entrypoints directly — zfb's esbuild step does
14
+ // not alias "react" to "preact/compat", so importing from "react" here
15
+ // would fail to resolve.
16
+ import { useState, useEffect } from "preact/hooks";
17
+ import {
18
+ applyColorScheme,
19
+ readColorSchemeFromDom,
20
+ subscribeColorSchemeChanged,
21
+ type ColorSchemeMode,
22
+ } from "./color-scheme-sync.js";
23
+
24
+ function SunIcon() {
25
+ return (
26
+ <svg
27
+ aria-hidden="true"
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ width="20"
30
+ height="20"
31
+ viewBox="0 0 24 24"
32
+ fill="none"
33
+ stroke="currentColor"
34
+ strokeWidth="2"
35
+ strokeLinecap="round"
36
+ strokeLinejoin="round"
37
+ >
38
+ <circle cx="12" cy="12" r="5" />
39
+ <line x1="12" y1="1" x2="12" y2="3" />
40
+ <line x1="12" y1="21" x2="12" y2="23" />
41
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
42
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
43
+ <line x1="1" y1="12" x2="3" y2="12" />
44
+ <line x1="21" y1="12" x2="23" y2="12" />
45
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
46
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
47
+ </svg>
48
+ );
49
+ }
50
+
51
+ function MoonIcon() {
52
+ return (
53
+ <svg
54
+ aria-hidden="true"
55
+ xmlns="http://www.w3.org/2000/svg"
56
+ width="20"
57
+ height="20"
58
+ viewBox="0 0 24 24"
59
+ fill="none"
60
+ stroke="currentColor"
61
+ strokeWidth="2"
62
+ strokeLinecap="round"
63
+ strokeLinejoin="round"
64
+ >
65
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
66
+ </svg>
67
+ );
68
+ }
69
+
70
+ export interface ThemeToggleProps {
71
+ defaultMode?: ColorSchemeMode;
72
+ }
73
+
74
+ // NAMED export (not default) on purpose: tsup compiles a default export
75
+ // to `export { ThemeToggle as default }`, an alias shape zfb's island
76
+ // scanner does not recognize — the island then never registers and the
77
+ // header toggle ships dead (zero hydration). Named exports compile to
78
+ // `export { ThemeToggle }`, which the scanner handles (same pattern as
79
+ // the package's MobileToc island).
80
+ export function ThemeToggle({
81
+ defaultMode = "dark",
82
+ }: ThemeToggleProps) {
83
+ // Initial state must match server render to avoid hydration mismatch.
84
+ // Actual theme is synced from DOM in useEffect below.
85
+ const [mode, setMode] = useState<ColorSchemeMode>(defaultMode);
86
+
87
+ useEffect(() => {
88
+ const sync = () => setMode(readColorSchemeFromDom(defaultMode));
89
+ sync();
90
+ // Cross-instance sync (#2012 E3): every mounted toggle re-reads the
91
+ // DOM whenever any instance (or the zdtp panel) applies a scheme,
92
+ // so the header toggle and the sidebar-footer toggle never disagree.
93
+ return subscribeColorSchemeChanged(sync);
94
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
95
+
96
+ function toggle() {
97
+ const next = mode === "dark" ? "light" : "dark";
98
+ setMode(next);
99
+ applyColorScheme(next);
100
+ }
101
+
102
+ const nextMode = mode === "dark" ? "light" : "dark";
103
+
104
+ return (
105
+ <button
106
+ onClick={toggle}
107
+ aria-label={`Switch to ${nextMode} mode`}
108
+ className="text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
109
+ >
110
+ {mode === "dark" ? <SunIcon /> : <MoonIcon />}
111
+ </button>
112
+ );
113
+ }
114
+ // Pin the island marker name to "ThemeToggle" regardless of bundler
115
+ // identifier mangling: zfb's Island() derives the SSR marker via
116
+ // `displayName ?? name`, and esbuild may rename the function when
117
+ // another binding shares the name in the same bundle. Setting
118
+ // displayName explicitly keeps the emitted marker aligned with the
119
+ // island-manifest entry. zudolab/zudo-doc#1446.
120
+ ThemeToggle.displayName = "ThemeToggle";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tiny `clsx`-style class name joiner. Accepts strings, falsy values,
3
+ * and arrays/objects of the same — only truthy strings survive.
4
+ *
5
+ * Framework primitives keep their dependency surface minimal so that a
6
+ * downstream consumer of `@takazudo/zudo-doc/toc` does not need an extra
7
+ * runtime dep just for class name composition. The behavior here is the
8
+ * subset the TOC primitives use; see `clsx` for the full feature set.
9
+ */
10
+ type ClassValue =
11
+ | string
12
+ | number
13
+ | bigint
14
+ | boolean
15
+ | null
16
+ | undefined
17
+ | ClassValue[]
18
+ | { [key: string]: unknown };
19
+
20
+ export function cx(...args: ClassValue[]): string {
21
+ const out: string[] = [];
22
+ for (const arg of args) {
23
+ if (!arg) continue;
24
+ if (typeof arg === "string") {
25
+ out.push(arg);
26
+ } else if (typeof arg === "number" || typeof arg === "bigint") {
27
+ out.push(String(arg));
28
+ } else if (Array.isArray(arg)) {
29
+ const inner = cx(...arg);
30
+ if (inner) out.push(inner);
31
+ } else if (typeof arg === "object") {
32
+ for (const key of Object.keys(arg)) {
33
+ if ((arg as Record<string, unknown>)[key]) out.push(key);
34
+ }
35
+ }
36
+ }
37
+ return out.join(" ");
38
+ }
@@ -0,0 +1,11 @@
1
+ export { Toc } from "./toc.js";
2
+ export type { TocProps } from "./toc.js";
3
+ export { MobileToc } from "./mobile-toc.js";
4
+ export type { MobileTocProps } from "./mobile-toc.js";
5
+ export { useActiveHeading, getActiveHeadingId } from "./use-active-heading.js";
6
+ export type { UseActiveHeadingResult } from "./use-active-heading.js";
7
+ export type { HeadingItem } from "./types.js";
8
+ // Exposed so consumers wiring their own scanner-visible Toc/MobileToc shims
9
+ // (the override pattern from zudolab/zudo-doc#2057) can derive the locale TOC
10
+ // label from the page `lang` without hand-mirroring the lang→title map.
11
+ export { getTocTitle } from "./toc-title.js";