create-zudo-doc 0.2.0 → 0.2.1
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/api.js +4 -1
- package/dist/cli.js +4 -6
- package/dist/preset.js +11 -0
- package/dist/prompts.js +2 -6
- package/dist/scaffold.js +15 -9
- package/dist/settings-gen.js +7 -7
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +25 -0
- package/dist/zfb-config-gen.js +11 -50
- package/package.json +1 -1
- package/templates/base/pages/_data.ts +10 -23
- package/templates/base/pages/docs/[[...slug]].tsx +27 -168
- package/templates/base/pages/lib/_doc-content-header.tsx +24 -4
- package/templates/base/pages/lib/_doc-history-area.tsx +21 -5
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +22 -2
- package/templates/base/pages/lib/_doc-page-renderer.tsx +192 -0
- package/templates/base/pages/lib/_doc-page-shell.tsx +3 -2
- package/templates/base/pages/lib/_doc-route-entries.ts +188 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +7 -2
- package/templates/base/pages/lib/_footer-with-defaults.tsx +38 -27
- package/templates/base/pages/lib/_head-with-defaults.tsx +7 -10
- package/templates/base/pages/lib/_header-with-defaults.tsx +51 -89
- package/templates/base/pages/lib/_inline-version-switcher.tsx +5 -4
- package/templates/base/pages/lib/_nav-data-prep.ts +137 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +10 -6
- package/templates/base/pages/lib/_search-widget-script.ts +32 -9
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +15 -60
- package/templates/base/pages/lib/locale-merge.ts +1 -1
- package/templates/base/pages/lib/route-enumerators.ts +11 -7
- package/templates/base/plugins/connect-adapter.mjs +30 -1
- package/templates/base/plugins/copy-public-plugin.mjs +10 -2
- package/templates/base/plugins/search-index-plugin.mjs +20 -8
- package/templates/base/src/components/sidebar-toggle.tsx +1 -1
- package/templates/base/src/components/sidebar-tree.tsx +10 -4
- package/templates/base/src/config/color-schemes.ts +4 -0
- package/templates/base/src/config/docs-schema.ts +94 -0
- package/templates/base/src/config/i18n.ts +10 -3
- package/templates/base/src/styles/global.css +14 -0
- package/templates/base/src/types/docs-entry.ts +8 -26
- package/templates/base/src/utils/base.ts +5 -3
- package/templates/base/src/utils/docs.ts +144 -169
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +20 -110
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +62 -38
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +34 -8
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +27 -45
- package/templates/features/docHistory/files/src/components/doc-history.tsx +28 -8
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +6 -74
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +6 -77
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +7 -69
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +6 -76
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +201 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[[...slug]].tsx +41 -179
- package/templates/features/i18n/files/pages/[locale]/index.tsx +5 -5
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +33 -21
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +5 -59
- package/templates/features/versioning/files/pages/docs/versions.tsx +8 -66
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +79 -0
- package/templates/features/versioning/files/pages/v/[version]/[locale]/docs/[[...slug]].tsx +46 -191
- package/templates/features/versioning/files/pages/v/[version]/docs/[[...slug]].tsx +31 -173
- package/templates/base/src/components/content/heading-h3.tsx +0 -20
- package/templates/base/src/components/theme-toggle.tsx +0 -107
- package/templates/base/src/hooks/use-active-heading.ts +0 -133
- package/templates/base/src/plugins/docs-source-map.ts +0 -103
- package/templates/base/src/plugins/hast-utils.ts +0 -10
- package/templates/base/src/plugins/rehype-code-title.ts +0 -50
- package/templates/base/src/plugins/rehype-heading-links.ts +0 -53
- package/templates/base/src/plugins/rehype-mermaid.ts +0 -41
- package/templates/base/src/plugins/url-utils.ts +0 -4
- package/templates/base/src/utils/dedent.ts +0 -24
- package/templates/features/docHistory/files/src/utils/doc-history.ts +0 -180
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +0 -198
|
@@ -19,30 +19,20 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Version banner: if version.banner is set ("unmaintained" | "unreleased"),
|
|
21
21
|
// the DocLayoutWithDefaults version-banner prop drives the banner display.
|
|
22
|
+
//
|
|
23
|
+
// Enumeration + per-entry derived data are built by the shared, memoized
|
|
24
|
+
// buildDocRouteEntries (#2010); rendering by the shared renderDocPage. This
|
|
25
|
+
// file owns only the route's nav source, its versioned URL closure, and the
|
|
26
|
+
// param/prop shapes.
|
|
22
27
|
|
|
23
28
|
import { settings } from "@/config/settings";
|
|
24
29
|
import type { VersionConfig } from "@/config/settings";
|
|
25
|
-
import {
|
|
26
|
-
import { docsUrl, versionedDocsUrl, absoluteUrl } from "@/utils/base";
|
|
27
|
-
import {
|
|
28
|
-
buildNavTree,
|
|
29
|
-
buildBreadcrumbs,
|
|
30
|
-
collectAutoIndexNodes,
|
|
31
|
-
type NavNode,
|
|
32
|
-
} from "@/utils/docs";
|
|
33
|
-
import { getNavSectionForSlug, getNavSubtree } from "@/utils/nav-scope";
|
|
34
|
-
import { toRouteSlug, toSlugParams } from "@/utils/slug";
|
|
35
|
-
// Locale-aware MDX components factory — see `pages/_mdx-components.ts`.
|
|
36
|
-
import { createMdxComponents } from "../../../_mdx-components";
|
|
30
|
+
import { versionedDocsUrl } from "@/utils/base";
|
|
37
31
|
import type { JSX } from "preact";
|
|
38
32
|
import { resolveNavSource } from "../../../lib/_nav-source-docs";
|
|
39
|
-
import {
|
|
40
|
-
import
|
|
41
|
-
import {
|
|
42
|
-
import { buildInlineVersionSwitcher } from "../../../lib/_inline-version-switcher";
|
|
43
|
-
import { DocContentHeader } from "../../../lib/_doc-content-header";
|
|
44
|
-
import { DocPageShell } from "../../../lib/_doc-page-shell";
|
|
45
|
-
import { resolveDocPrevNext, flattenSubtree, rewriteNavHref, remapNavChildHrefs } from "../../../lib/_doc-route-paths";
|
|
33
|
+
import type { DocPageEntryProps, DocPageAutoIndexProps } from "../../../lib/doc-page-props";
|
|
34
|
+
import { buildDocRouteEntries } from "../../../lib/_doc-route-entries";
|
|
35
|
+
import { renderDocPage } from "../../../lib/_doc-page-renderer";
|
|
46
36
|
|
|
47
37
|
export const frontmatter = { title: "Docs" };
|
|
48
38
|
|
|
@@ -50,8 +40,6 @@ export const frontmatter = { title: "Docs" };
|
|
|
50
40
|
// Types
|
|
51
41
|
// ---------------------------------------------------------------------------
|
|
52
42
|
|
|
53
|
-
// DocPageEntry, AutoIndexNode imported from pages/lib/doc-page-props.ts
|
|
54
|
-
|
|
55
43
|
/** Route-specific extra fields — present on both branches of the union. */
|
|
56
44
|
interface VersionedDocPageExtra {
|
|
57
45
|
/** The version config for the active version. */
|
|
@@ -89,73 +77,31 @@ export function paths(): Array<{
|
|
|
89
77
|
for (const version of settings.versions) {
|
|
90
78
|
// Identity-stable nav source for this version (EN base, draft-filtered,
|
|
91
79
|
// unlisted retained). Reused across the route's per-page paths()
|
|
92
|
-
// invocations so buildNavTree's identity fast-path
|
|
93
|
-
// pages/lib/_nav-source-docs.ts
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const tree = buildNavTree(navDocs, "en", categoryMeta);
|
|
80
|
+
// invocations so buildNavTree's identity fast-path and the
|
|
81
|
+
// buildDocRouteEntries memo apply — see pages/lib/_nav-source-docs.ts
|
|
82
|
+
// (#1902). Versioned docs always use EN locale for the nav tree.
|
|
83
|
+
const source = resolveNavSource("en", version.slug);
|
|
97
84
|
|
|
98
85
|
// URL closure for THIS version — every versioned href (prev/next,
|
|
99
86
|
// breadcrumb crumbs, auto-index cards) is produced by this single
|
|
100
87
|
// function bound to the version slug. Because it is built per-version
|
|
101
|
-
// inside this loop, a latest-page pagination override
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// closure (#1916).
|
|
88
|
+
// inside this loop, a latest-page pagination override is rewritten
|
|
89
|
+
// through the VERSIONED closure for this route only — it can never bleed
|
|
90
|
+
// into the latest route, which has no such closure (#1916).
|
|
105
91
|
const urlFor = (s: string): string => versionedDocsUrl(s, version.slug);
|
|
106
92
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const slug = entry.data.slug ?? toRouteSlug(entry.slug);
|
|
114
|
-
const navSection = getNavSectionForSlug(slug);
|
|
115
|
-
const subtree = getNavSubtree(tree, navSection);
|
|
116
|
-
|
|
117
|
-
// Prev/next + pagination overrides against THIS version's own `tree`,
|
|
118
|
-
// then hrefs rewritten to the versioned URL form via urlFor.
|
|
119
|
-
const { prev: prevNode, next: nextNode } = resolveDocPrevNext(
|
|
120
|
-
tree,
|
|
121
|
-
flattenSubtree(subtree),
|
|
122
|
-
slug,
|
|
123
|
-
entry.data,
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
result.push({
|
|
127
|
-
params: { version: version.slug, slug: toSlugParams(slug) },
|
|
128
|
-
props: {
|
|
129
|
-
kind: "entry",
|
|
130
|
-
entry,
|
|
131
|
-
version,
|
|
132
|
-
// #1916 #1: breadcrumb crumbs remapped to the versioned URL space.
|
|
133
|
-
breadcrumbs: buildBreadcrumbs(tree, slug, "en", urlFor),
|
|
134
|
-
prev: rewriteNavHref(prevNode, urlFor),
|
|
135
|
-
next: rewriteNavHref(nextNode, urlFor),
|
|
136
|
-
headings: extractHeadings(entry.body ?? ""),
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Auto-generated index pages for categories without index.mdx
|
|
142
|
-
for (const node of collectAutoIndexNodes(tree)) {
|
|
93
|
+
for (const item of buildDocRouteEntries({
|
|
94
|
+
source,
|
|
95
|
+
locale: "en",
|
|
96
|
+
routeSig: `v-docs;${version.slug}`,
|
|
97
|
+
urlFor,
|
|
98
|
+
})) {
|
|
143
99
|
result.push({
|
|
144
|
-
params: { version: version.slug, slug:
|
|
145
|
-
props:
|
|
146
|
-
kind
|
|
147
|
-
|
|
148
|
-
...
|
|
149
|
-
// #1916 #2: child-card hrefs ALWAYS resolve to the versioned URL.
|
|
150
|
-
children: remapNavChildHrefs(node.children, urlFor) as NavNode[],
|
|
151
|
-
} as AutoIndexNode,
|
|
152
|
-
version,
|
|
153
|
-
// #1916 #1: breadcrumb crumbs remapped to the versioned URL space.
|
|
154
|
-
breadcrumbs: buildBreadcrumbs(tree, node.slug, "en", urlFor),
|
|
155
|
-
prev: null,
|
|
156
|
-
next: null,
|
|
157
|
-
headings: [],
|
|
158
|
-
},
|
|
100
|
+
params: { version: version.slug, slug: item.slugParams },
|
|
101
|
+
props:
|
|
102
|
+
item.props.kind === "entry"
|
|
103
|
+
? { ...item.props, version }
|
|
104
|
+
: { ...item.props, version },
|
|
159
105
|
});
|
|
160
106
|
}
|
|
161
107
|
}
|
|
@@ -170,96 +116,8 @@ export function paths(): Array<{
|
|
|
170
116
|
type PageArgs = DocPageProps & { params: { version: string; slug: string[] } };
|
|
171
117
|
|
|
172
118
|
export default function VersionedDocsPage(props: PageArgs): JSX.Element {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
? props.autoIndex.slug
|
|
178
|
-
: (props.entry.data.slug ?? toRouteSlug(props.entry.slug));
|
|
179
|
-
|
|
180
|
-
const title = props.kind === "autoIndex" ? props.autoIndex.label : props.entry.data.title;
|
|
181
|
-
const description = props.kind === "autoIndex" ? props.autoIndex.description : props.entry.data.description;
|
|
182
|
-
|
|
183
|
-
// Locale-aware components bag — creates nav wrappers bound to the active
|
|
184
|
-
// locale so CategoryNav/CategoryTreeNav/SiteTreeNav query the right collection.
|
|
185
|
-
const components = createMdxComponents(locale);
|
|
186
|
-
|
|
187
|
-
// #1916 #2: child cards already carry versioned hrefs from paths(); just
|
|
188
|
-
// filter to renderable nodes here.
|
|
189
|
-
const autoIndexChildren = props.kind === "autoIndex"
|
|
190
|
-
? props.autoIndex.children.filter((c: NavNode) => c.hasPage || c.children.length > 0)
|
|
191
|
-
: [];
|
|
192
|
-
|
|
193
|
-
// Version banner: drives the `<VersionBanner>` element inside
|
|
194
|
-
// DocLayoutWithDefaults when `version.banner` is "unmaintained" or
|
|
195
|
-
// "unreleased". The banner links out to the latest version of the
|
|
196
|
-
// current page (slug-preserving — strips the /v/{version}/ prefix).
|
|
197
|
-
const versionBannerType = version.banner ? version.banner : undefined;
|
|
198
|
-
const versionBannerLatestUrl = versionBannerType
|
|
199
|
-
? docsUrl(slug, locale)
|
|
200
|
-
: undefined;
|
|
201
|
-
const versionBannerLabels = versionBannerType
|
|
202
|
-
? {
|
|
203
|
-
message:
|
|
204
|
-
versionBannerType === "unmaintained"
|
|
205
|
-
? t("version.banner.unmaintained", locale)
|
|
206
|
-
: t("version.banner.unreleased", locale),
|
|
207
|
-
latestLink: t("version.banner.latestLink", locale),
|
|
208
|
-
}
|
|
209
|
-
: undefined;
|
|
210
|
-
|
|
211
|
-
// Canonical URL — versioned pages use the versioned URL as canonical.
|
|
212
|
-
const currentPath = versionedDocsUrl(slug, version.slug, locale);
|
|
213
|
-
const canonical = absoluteUrl(currentPath);
|
|
214
|
-
|
|
215
|
-
// Persist key: locale + nav-section so the sidebar DOM node is reused
|
|
216
|
-
// across same-locale + same-section navigations only. No sanitizer needed —
|
|
217
|
-
// both lang (BCP-47 locale string) and navSection (filesystem-derived
|
|
218
|
-
// kebab-case slug) come from controlled, trusted sources.
|
|
219
|
-
const navSection = getNavSectionForSlug(slug);
|
|
220
|
-
const hideSidebar = props.kind === "entry" ? props.entry.data.hide_sidebar : undefined;
|
|
221
|
-
const sidebarPersistKey = hideSidebar
|
|
222
|
-
? undefined
|
|
223
|
-
: `sidebar-${locale}-${navSection ?? "default"}`;
|
|
224
|
-
|
|
225
|
-
return (
|
|
226
|
-
<DocPageShell
|
|
227
|
-
kind={props.kind}
|
|
228
|
-
locale={locale}
|
|
229
|
-
slug={slug}
|
|
230
|
-
title={title}
|
|
231
|
-
description={description}
|
|
232
|
-
canonical={canonical}
|
|
233
|
-
breadcrumbs={breadcrumbs}
|
|
234
|
-
prev={prev}
|
|
235
|
-
next={next}
|
|
236
|
-
headings={headings}
|
|
237
|
-
navSection={navSection}
|
|
238
|
-
sidebarPersistKey={sidebarPersistKey}
|
|
239
|
-
hideSidebar={hideSidebar}
|
|
240
|
-
hideToc={props.kind === "entry" ? props.entry.data.hide_toc : undefined}
|
|
241
|
-
currentPath={currentPath}
|
|
242
|
-
currentVersion={version.slug}
|
|
243
|
-
versionSwitcher={buildInlineVersionSwitcher(slug, locale, version.slug)}
|
|
244
|
-
versionBanner={versionBannerType}
|
|
245
|
-
versionBannerLatestUrl={versionBannerLatestUrl}
|
|
246
|
-
versionBannerLabels={versionBannerLabels}
|
|
247
|
-
autoIndexLabel={props.kind === "autoIndex" ? props.autoIndex.label : undefined}
|
|
248
|
-
autoIndexChildren={autoIndexChildren}
|
|
249
|
-
metainfoSlot={
|
|
250
|
-
props.kind === "autoIndex" ? <DocMetainfoArea slug={slug} locale={locale} /> : null
|
|
251
|
-
}
|
|
252
|
-
contentHeaderSlot={
|
|
253
|
-
props.kind === "entry" ? (
|
|
254
|
-
<DocContentHeader entry={props.entry} slug={slug} locale={locale} />
|
|
255
|
-
) : undefined
|
|
256
|
-
}
|
|
257
|
-
contentSlot={
|
|
258
|
-
props.kind === "entry" ? <props.entry.Content components={components} /> : undefined
|
|
259
|
-
}
|
|
260
|
-
// #1916 #5: doc-history hidden on versioned pages until versioned
|
|
261
|
-
// history is supported.
|
|
262
|
-
docHistorySlot={null}
|
|
263
|
-
/>
|
|
264
|
-
);
|
|
119
|
+
return renderDocPage(props, {
|
|
120
|
+
locale: "en",
|
|
121
|
+
version: props.version,
|
|
122
|
+
});
|
|
265
123
|
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { CSSProperties } from 'react';
|
|
2
|
-
|
|
3
|
-
type Props = React.ComponentPropsWithoutRef<'h3'>;
|
|
4
|
-
|
|
5
|
-
export function HeadingH3({ id, children, className, ...rest }: Props) {
|
|
6
|
-
return (
|
|
7
|
-
<h3
|
|
8
|
-
id={id}
|
|
9
|
-
className={`text-body font-bold leading-snug pt-vsp-xs border-t-[2px] border-transparent${className ? ` ${className}` : ''}`}
|
|
10
|
-
style={
|
|
11
|
-
{
|
|
12
|
-
borderImage: 'linear-gradient(to right, var(--color-muted), transparent) 1',
|
|
13
|
-
} as CSSProperties
|
|
14
|
-
}
|
|
15
|
-
{...rest}
|
|
16
|
-
>
|
|
17
|
-
{children}
|
|
18
|
-
</h3>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
// Use preact hook entrypoints directly — the "react" → "preact/compat" alias
|
|
4
|
-
// lets us consume React-typed components in this Preact app (configured
|
|
5
|
-
// project-wide). Same pattern as packages/zudo-doc/src/theme/theme-toggle.tsx.
|
|
6
|
-
import { useState, useEffect } from "preact/hooks";
|
|
7
|
-
|
|
8
|
-
const STORAGE_KEY = "zudo-doc-theme";
|
|
9
|
-
|
|
10
|
-
function SunIcon() {
|
|
11
|
-
return (
|
|
12
|
-
<svg
|
|
13
|
-
aria-hidden="true"
|
|
14
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
-
width="20"
|
|
16
|
-
height="20"
|
|
17
|
-
viewBox="0 0 24 24"
|
|
18
|
-
fill="none"
|
|
19
|
-
stroke="currentColor"
|
|
20
|
-
strokeWidth="2"
|
|
21
|
-
strokeLinecap="round"
|
|
22
|
-
strokeLinejoin="round"
|
|
23
|
-
>
|
|
24
|
-
<circle cx="12" cy="12" r="5" />
|
|
25
|
-
<line x1="12" y1="1" x2="12" y2="3" />
|
|
26
|
-
<line x1="12" y1="21" x2="12" y2="23" />
|
|
27
|
-
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
28
|
-
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
29
|
-
<line x1="1" y1="12" x2="3" y2="12" />
|
|
30
|
-
<line x1="21" y1="12" x2="23" y2="12" />
|
|
31
|
-
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
32
|
-
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
33
|
-
</svg>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function MoonIcon() {
|
|
38
|
-
return (
|
|
39
|
-
<svg
|
|
40
|
-
aria-hidden="true"
|
|
41
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
42
|
-
width="20"
|
|
43
|
-
height="20"
|
|
44
|
-
viewBox="0 0 24 24"
|
|
45
|
-
fill="none"
|
|
46
|
-
stroke="currentColor"
|
|
47
|
-
strokeWidth="2"
|
|
48
|
-
strokeLinecap="round"
|
|
49
|
-
strokeLinejoin="round"
|
|
50
|
-
>
|
|
51
|
-
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
52
|
-
</svg>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface ThemeToggleProps {
|
|
57
|
-
defaultMode?: "light" | "dark";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export default function ThemeToggle({ defaultMode = "dark" }: ThemeToggleProps) {
|
|
61
|
-
// Initial state must match server render to avoid hydration mismatch.
|
|
62
|
-
// Actual theme is synced from DOM in useEffect below.
|
|
63
|
-
const [mode, setMode] = useState<"light" | "dark">(defaultMode);
|
|
64
|
-
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
const actual =
|
|
67
|
-
(document.documentElement.getAttribute("data-theme") as
|
|
68
|
-
| "light"
|
|
69
|
-
| "dark") || defaultMode;
|
|
70
|
-
if (actual !== mode) {
|
|
71
|
-
setMode(actual);
|
|
72
|
-
}
|
|
73
|
-
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
74
|
-
|
|
75
|
-
function toggle() {
|
|
76
|
-
const next = mode === "dark" ? "light" : "dark";
|
|
77
|
-
setMode(next);
|
|
78
|
-
document.documentElement.setAttribute("data-theme", next);
|
|
79
|
-
document.documentElement.style.colorScheme = next;
|
|
80
|
-
localStorage.setItem(STORAGE_KEY, next);
|
|
81
|
-
// Clear both v1 and v2 tweak state so the new scheme's palette takes effect.
|
|
82
|
-
localStorage.removeItem("zudo-doc-tweak-state");
|
|
83
|
-
localStorage.removeItem("zudo-doc-tweak-state-v2");
|
|
84
|
-
window.dispatchEvent(new CustomEvent("color-scheme-changed"));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const nextMode = mode === "dark" ? "light" : "dark";
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<button
|
|
91
|
-
onClick={toggle}
|
|
92
|
-
aria-label={`Switch to ${nextMode} mode`}
|
|
93
|
-
className="text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
|
94
|
-
>
|
|
95
|
-
{mode === "dark" ? <SunIcon /> : <MoonIcon />}
|
|
96
|
-
</button>
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
// Pin the island marker name to "ThemeToggle" regardless of esbuild's
|
|
100
|
-
// identifier deduplication. Both this host component and the v2 package's
|
|
101
|
-
// ThemeToggleInner share the plain name "ThemeToggle"; when both land in the
|
|
102
|
-
// same SSR bundle esbuild renames one to "ThemeToggle2", making
|
|
103
|
-
// captureComponentName() emit "ThemeToggle2" — a name that has no entry in
|
|
104
|
-
// the island manifest. Setting displayName explicitly ensures Island() reads
|
|
105
|
-
// the attribute-level name (displayName is preferred over .name) and emits
|
|
106
|
-
// the correct data-zfb-island="ThemeToggle" marker. zudolab/zudo-doc#1446.
|
|
107
|
-
ThemeToggle.displayName = "ThemeToggle";
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "preact/compat";
|
|
2
|
-
import type { Heading } from "@/types/heading";
|
|
3
|
-
|
|
4
|
-
const SCROLL_MARGIN_TOP = 80;
|
|
5
|
-
const DEBOUNCE_MS = 200;
|
|
6
|
-
|
|
7
|
-
function getActiveHeadingId(
|
|
8
|
-
headingIds: string[],
|
|
9
|
-
elementMap: Map<string, HTMLElement>,
|
|
10
|
-
): string | null {
|
|
11
|
-
if (headingIds.length === 0) return null;
|
|
12
|
-
|
|
13
|
-
const viewportHeight = window.innerHeight;
|
|
14
|
-
|
|
15
|
-
let firstVisibleIndex = -1;
|
|
16
|
-
for (let i = 0; i < headingIds.length; i++) {
|
|
17
|
-
const el = elementMap.get(headingIds[i]);
|
|
18
|
-
if (!el) continue;
|
|
19
|
-
const { top } = el.getBoundingClientRect();
|
|
20
|
-
if (top >= SCROLL_MARGIN_TOP) {
|
|
21
|
-
firstVisibleIndex = i;
|
|
22
|
-
break;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (firstVisibleIndex === -1) {
|
|
27
|
-
return headingIds[headingIds.length - 1];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (firstVisibleIndex === 0) {
|
|
31
|
-
const el = elementMap.get(headingIds[0]);
|
|
32
|
-
if (el) {
|
|
33
|
-
const { top } = el.getBoundingClientRect();
|
|
34
|
-
if (top < viewportHeight / 2) {
|
|
35
|
-
return headingIds[0];
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const el = elementMap.get(headingIds[firstVisibleIndex]);
|
|
42
|
-
if (el) {
|
|
43
|
-
const { top } = el.getBoundingClientRect();
|
|
44
|
-
if (top < viewportHeight / 2) {
|
|
45
|
-
return headingIds[firstVisibleIndex];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return headingIds[firstVisibleIndex - 1];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface UseActiveHeadingResult {
|
|
53
|
-
activeId: string | null;
|
|
54
|
-
activate: (id: string) => void;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function useActiveHeading(
|
|
58
|
-
headings: Heading[],
|
|
59
|
-
): UseActiveHeadingResult {
|
|
60
|
-
const [activeId, setActiveId] = useState<string | null>(null);
|
|
61
|
-
const headingIdsRef = useRef<string[]>([]);
|
|
62
|
-
const elementMapRef = useRef<Map<string, HTMLElement>>(new Map());
|
|
63
|
-
const suppressedRef = useRef(false);
|
|
64
|
-
|
|
65
|
-
const activate = useCallback((id: string) => {
|
|
66
|
-
setActiveId(id);
|
|
67
|
-
suppressedRef.current = true;
|
|
68
|
-
// Safety timeout: unsuppress if no scroll event fires (target already in view)
|
|
69
|
-
setTimeout(() => {
|
|
70
|
-
suppressedRef.current = false;
|
|
71
|
-
}, 2000);
|
|
72
|
-
}, []);
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
const ids = headings.map((h) => h.slug);
|
|
76
|
-
headingIdsRef.current = ids;
|
|
77
|
-
|
|
78
|
-
const map = new Map<string, HTMLElement>();
|
|
79
|
-
for (const id of ids) {
|
|
80
|
-
const el = document.getElementById(id);
|
|
81
|
-
if (el) map.set(id, el);
|
|
82
|
-
}
|
|
83
|
-
elementMapRef.current = map;
|
|
84
|
-
|
|
85
|
-
let timerId: ReturnType<typeof setTimeout> | null = null;
|
|
86
|
-
|
|
87
|
-
function update() {
|
|
88
|
-
timerId = null;
|
|
89
|
-
setActiveId(
|
|
90
|
-
getActiveHeadingId(headingIdsRef.current, elementMapRef.current),
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let fallbackTimerId: ReturnType<typeof setTimeout> | null = null;
|
|
95
|
-
|
|
96
|
-
function onScroll() {
|
|
97
|
-
if (suppressedRef.current) {
|
|
98
|
-
// Fallback for browsers without scrollend (Safari < 18)
|
|
99
|
-
if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
|
|
100
|
-
fallbackTimerId = setTimeout(onScrollEnd, 1500);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (timerId !== null) clearTimeout(timerId);
|
|
104
|
-
timerId = setTimeout(update, DEBOUNCE_MS);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function onScrollEnd() {
|
|
108
|
-
suppressedRef.current = false;
|
|
109
|
-
if (fallbackTimerId !== null) {
|
|
110
|
-
clearTimeout(fallbackTimerId);
|
|
111
|
-
fallbackTimerId = null;
|
|
112
|
-
}
|
|
113
|
-
if (timerId !== null) clearTimeout(timerId);
|
|
114
|
-
update();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
update();
|
|
118
|
-
|
|
119
|
-
window.addEventListener("scroll", onScroll, { passive: true });
|
|
120
|
-
window.addEventListener("resize", onScroll, { passive: true });
|
|
121
|
-
window.addEventListener("scrollend", onScrollEnd, { passive: true });
|
|
122
|
-
|
|
123
|
-
return () => {
|
|
124
|
-
window.removeEventListener("scroll", onScroll);
|
|
125
|
-
window.removeEventListener("resize", onScroll);
|
|
126
|
-
window.removeEventListener("scrollend", onScrollEnd);
|
|
127
|
-
if (timerId !== null) clearTimeout(timerId);
|
|
128
|
-
if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
|
|
129
|
-
};
|
|
130
|
-
}, [headings]);
|
|
131
|
-
|
|
132
|
-
return { activeId, activate };
|
|
133
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { readdirSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export interface DocsSourceMapOptions {
|
|
5
|
-
/** Absolute root directory of the project */
|
|
6
|
-
rootDir: string;
|
|
7
|
-
/** Main docs directory relative to rootDir (e.g., "src/content/docs") */
|
|
8
|
-
docsDir: string;
|
|
9
|
-
/** Locale configurations: { ja: { dir: "src/content/docs-ja" } } */
|
|
10
|
-
locales: Record<string, { dir: string }>;
|
|
11
|
-
/** Version configurations */
|
|
12
|
-
versions: Array<{ slug: string; docsDir: string }> | false;
|
|
13
|
-
/** Base URL path (e.g., "/" or "/pj/my-docs/") */
|
|
14
|
-
base: string;
|
|
15
|
-
/** Whether to append trailing slash to URLs */
|
|
16
|
-
trailingSlash: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Build a map of absolute file paths to URL paths.
|
|
21
|
-
* Scans all configured docs directories for .md/.mdx files.
|
|
22
|
-
*/
|
|
23
|
-
export function buildDocsSourceMap(
|
|
24
|
-
options: DocsSourceMapOptions,
|
|
25
|
-
): Map<string, string> {
|
|
26
|
-
const map = new Map<string, string>();
|
|
27
|
-
const { rootDir, docsDir, locales, versions, base, trailingSlash } = options;
|
|
28
|
-
|
|
29
|
-
const normalizedBase = base.replace(/\/+$/, "");
|
|
30
|
-
|
|
31
|
-
function applyTS(url: string): string {
|
|
32
|
-
if (trailingSlash) {
|
|
33
|
-
if (url.endsWith("/")) return url;
|
|
34
|
-
return url + "/";
|
|
35
|
-
}
|
|
36
|
-
// Strip trailing slashes when trailingSlash is false (except root "/")
|
|
37
|
-
if (url !== "/" && url.endsWith("/")) {
|
|
38
|
-
return url.replace(/\/+$/, "");
|
|
39
|
-
}
|
|
40
|
-
return url;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function withBase(path: string): string {
|
|
44
|
-
const raw =
|
|
45
|
-
normalizedBase === ""
|
|
46
|
-
? path
|
|
47
|
-
: `${normalizedBase}${path.startsWith("/") ? path : `/${path}`}`;
|
|
48
|
-
return applyTS(raw);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function scanDir(dir: string, urlPrefix: string): void {
|
|
52
|
-
const absDir = resolve(rootDir, dir);
|
|
53
|
-
let files: string[];
|
|
54
|
-
try {
|
|
55
|
-
files = readdirSync(absDir, { recursive: true })
|
|
56
|
-
.map((f) => String(f))
|
|
57
|
-
.filter((f) => /\.(md|mdx)$/.test(f));
|
|
58
|
-
} catch {
|
|
59
|
-
// Directory doesn't exist — skip silently
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
for (const file of files) {
|
|
63
|
-
const absFile = resolve(absDir, file);
|
|
64
|
-
// Convert file path to slug: strip extension, strip /index suffix
|
|
65
|
-
const slug = file
|
|
66
|
-
.replace(/\.(md|mdx)$/, "")
|
|
67
|
-
.replace(/(^|\/)index$/, "$1") // Strip index (root or nested)
|
|
68
|
-
.replace(/(^|\\)index$/, "$1") // Windows
|
|
69
|
-
.replace(/\\/g, "/") // Windows path sep
|
|
70
|
-
.replace(/\/$/, ""); // Trailing slash from index strip
|
|
71
|
-
const url = withBase(`${urlPrefix}/${slug}`);
|
|
72
|
-
map.set(absFile, url);
|
|
73
|
-
|
|
74
|
-
// Also register with alternative extension for cross-referencing
|
|
75
|
-
// e.g., if file is foo.mdx, also register foo.md → same URL
|
|
76
|
-
// Only set if not already registered (avoid shadowing a real file)
|
|
77
|
-
const altExt = file.endsWith(".mdx")
|
|
78
|
-
? file.replace(/\.mdx$/, ".md")
|
|
79
|
-
: file.replace(/\.md$/, ".mdx");
|
|
80
|
-
const altFile = resolve(absDir, altExt);
|
|
81
|
-
if (!map.has(altFile)) {
|
|
82
|
-
map.set(altFile, url);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Main docs (default locale)
|
|
88
|
-
scanDir(docsDir, "/docs");
|
|
89
|
-
|
|
90
|
-
// Locale docs
|
|
91
|
-
for (const [code, config] of Object.entries(locales)) {
|
|
92
|
-
scanDir(config.dir, `/${code}/docs`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Versioned docs
|
|
96
|
-
if (versions) {
|
|
97
|
-
for (const version of versions) {
|
|
98
|
-
scanDir(version.docsDir, `/v/${version.slug}/docs`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return map;
|
|
103
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { Element, ElementContent, Text } from "hast";
|
|
2
|
-
|
|
3
|
-
/** Recursively extract plain text from a HAST node tree. */
|
|
4
|
-
export function extractText(node: Element | ElementContent | Text): string {
|
|
5
|
-
if (node.type === "text") return node.value;
|
|
6
|
-
if (node.type === "element") {
|
|
7
|
-
return node.children.map((c) => extractText(c)).join("");
|
|
8
|
-
}
|
|
9
|
-
return "";
|
|
10
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import type { Root, Element } from "hast";
|
|
2
|
-
import { visit, SKIP } from "unist-util-visit";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Rehype plugin that extracts title="..." from code block meta strings
|
|
6
|
-
* and wraps the <pre> in a container with a title header.
|
|
7
|
-
*
|
|
8
|
-
* Usage in MDX:
|
|
9
|
-
* ```js title="config.js"
|
|
10
|
-
* const x = 1;
|
|
11
|
-
* ```
|
|
12
|
-
*/
|
|
13
|
-
export function rehypeCodeTitle() {
|
|
14
|
-
return (tree: Root) => {
|
|
15
|
-
visit(tree, "element", (node: Element, index, parent) => {
|
|
16
|
-
if (node.tagName !== "pre" || !parent || index === undefined) return;
|
|
17
|
-
|
|
18
|
-
const codeEl = node.children.find(
|
|
19
|
-
(child): child is Element =>
|
|
20
|
-
child.type === "element" && child.tagName === "code",
|
|
21
|
-
);
|
|
22
|
-
if (!codeEl) return;
|
|
23
|
-
|
|
24
|
-
const meta =
|
|
25
|
-
String(codeEl.properties?.meta ?? "") ||
|
|
26
|
-
String((codeEl.data as Record<string, unknown> | undefined)?.meta ?? "");
|
|
27
|
-
const titleMatch = meta.match(/title="([^"]+)"/);
|
|
28
|
-
if (!titleMatch) return;
|
|
29
|
-
|
|
30
|
-
const title = titleMatch[1];
|
|
31
|
-
|
|
32
|
-
const titleEl: Element = {
|
|
33
|
-
type: "element",
|
|
34
|
-
tagName: "div",
|
|
35
|
-
properties: { className: ["code-block-title"] },
|
|
36
|
-
children: [{ type: "text", value: title }],
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const wrapper: Element = {
|
|
40
|
-
type: "element",
|
|
41
|
-
tagName: "div",
|
|
42
|
-
properties: { className: ["code-block-container"] },
|
|
43
|
-
children: [titleEl, { ...node }],
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
(parent as Element).children[index] = wrapper;
|
|
47
|
-
return SKIP;
|
|
48
|
-
});
|
|
49
|
-
};
|
|
50
|
-
}
|