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
package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts
CHANGED
|
@@ -64,7 +64,9 @@ function parseFrontmatter(content: string) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function escapeTitle(s: string): string {
|
|
67
|
-
|
|
67
|
+
// Backslashes must be escaped first — the value is embedded in
|
|
68
|
+
// double-quoted YAML frontmatter where `\d` or `C:\path` is invalid.
|
|
69
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
function listFiles(dir: string): string[] {
|
|
@@ -93,6 +95,35 @@ generated: true
|
|
|
93
95
|
fs.writeFileSync(path.join(outputDir, "index.mdx"), mdx);
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Writes an unlisted sub-page MDX file (flat file with a custom nested slug).
|
|
100
|
+
* Used for skill references, scripts, and assets.
|
|
101
|
+
*/
|
|
102
|
+
function writeUnlistedSubPage(
|
|
103
|
+
outputPath: string,
|
|
104
|
+
title: string,
|
|
105
|
+
slug: string,
|
|
106
|
+
body: string,
|
|
107
|
+
) {
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
outputPath,
|
|
110
|
+
`---\ntitle: "${escapeTitle(title)}"\nslug: "${slug}"\nunlisted: true\ngenerated: true\n---\n\n${body}\n`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Guards that the given name/slug is not the reserved "index" value.
|
|
116
|
+
* Throws with a contextual message if it is.
|
|
117
|
+
*/
|
|
118
|
+
function assertNotIndexReserved(
|
|
119
|
+
nameOrSlug: string,
|
|
120
|
+
errorMessage: string,
|
|
121
|
+
) {
|
|
122
|
+
if (nameOrSlug === "index") {
|
|
123
|
+
throw new Error(errorMessage);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
96
127
|
// ---------------------------------------------------------------------------
|
|
97
128
|
// CLAUDE.md discovery
|
|
98
129
|
// ---------------------------------------------------------------------------
|
|
@@ -176,11 +207,10 @@ function generateClaudemdDocs(
|
|
|
176
207
|
|
|
177
208
|
const emittedSlugs = new Map<string, string>();
|
|
178
209
|
items.forEach((item, index) => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
210
|
+
assertNotIndexReserved(
|
|
211
|
+
item.slug,
|
|
212
|
+
`claude-resources: "${item.relPath}" maps to the reserved slug "index", which is used for the category metadata file. Rename the directory to resolve the conflict.`,
|
|
213
|
+
);
|
|
184
214
|
const previous = emittedSlugs.get(item.slug);
|
|
185
215
|
if (previous !== undefined) {
|
|
186
216
|
throw new Error(
|
|
@@ -232,11 +262,10 @@ function generateCommandsDocs(config: ClaudeResourcesConfig): CommandItem[] {
|
|
|
232
262
|
if (!parsed) continue;
|
|
233
263
|
|
|
234
264
|
const name = file.replace(/\.md$/, "");
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
265
|
+
assertNotIndexReserved(
|
|
266
|
+
name,
|
|
267
|
+
`claude-resources: ".claude/commands/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the command file to resolve the conflict.`,
|
|
268
|
+
);
|
|
240
269
|
const description = (parsed.data.description as string) || "";
|
|
241
270
|
|
|
242
271
|
items.push({ name, description });
|
|
@@ -357,11 +386,10 @@ function generateSkillsDocs(config: ClaudeResourcesConfig): SkillItem[] {
|
|
|
357
386
|
const items: SkillItem[] = [];
|
|
358
387
|
|
|
359
388
|
for (const dir of dirs) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
389
|
+
assertNotIndexReserved(
|
|
390
|
+
dir,
|
|
391
|
+
`claude-resources: skill directory ".claude/skills/index/" uses the reserved name "index", which is used for the category metadata file. Rename the skill directory to resolve the conflict.`,
|
|
392
|
+
);
|
|
365
393
|
const content = fs.readFileSync(
|
|
366
394
|
path.join(skillsDir, dir, "SKILL.md"),
|
|
367
395
|
"utf8",
|
|
@@ -445,46 +473,43 @@ ${body}`;
|
|
|
445
473
|
const skillSlugBase = `claude-skills/${dir}`;
|
|
446
474
|
|
|
447
475
|
for (const ref of references) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
---
|
|
455
|
-
|
|
456
|
-
${escapeForMdx(ref.content.trim())}
|
|
457
|
-
`;
|
|
458
|
-
fs.writeFileSync(path.join(outputDir, `${dir}--ref-${ref.name}.mdx`), refMdx);
|
|
476
|
+
writeUnlistedSubPage(
|
|
477
|
+
path.join(outputDir, `${dir}--ref-${ref.name}.mdx`),
|
|
478
|
+
ref.title,
|
|
479
|
+
`${skillSlugBase}/ref-${ref.name}`,
|
|
480
|
+
escapeForMdx(ref.content.trim()),
|
|
481
|
+
);
|
|
459
482
|
}
|
|
460
483
|
|
|
461
484
|
for (const f of scriptFiles.filter((s) => s.endsWith(".md"))) {
|
|
462
485
|
const slug = f.replace(/\.md$/, "");
|
|
463
|
-
const subSlug = `${skillSlugBase}/script-${slug}`;
|
|
464
486
|
const raw = fs.readFileSync(
|
|
465
487
|
path.join(skillsDir, dir, "scripts", f),
|
|
466
488
|
"utf8",
|
|
467
489
|
);
|
|
468
490
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
469
491
|
const title = h1Match ? h1Match[1] : slug;
|
|
470
|
-
|
|
492
|
+
writeUnlistedSubPage(
|
|
471
493
|
path.join(outputDir, `${dir}--script-${slug}.mdx`),
|
|
472
|
-
|
|
494
|
+
title,
|
|
495
|
+
`${skillSlugBase}/script-${slug}`,
|
|
496
|
+
escapeForMdx(raw.trim()),
|
|
473
497
|
);
|
|
474
498
|
}
|
|
475
499
|
|
|
476
500
|
for (const f of assetFiles.filter((a) => a.endsWith(".md"))) {
|
|
477
501
|
const slug = f.replace(/\.md$/, "");
|
|
478
|
-
const subSlug = `${skillSlugBase}/asset-${slug}`;
|
|
479
502
|
const raw = fs.readFileSync(
|
|
480
503
|
path.join(skillsDir, dir, "assets", f),
|
|
481
504
|
"utf8",
|
|
482
505
|
);
|
|
483
506
|
const h1Match = raw.match(/^#\s+(.+)$/m);
|
|
484
507
|
const title = h1Match ? h1Match[1] : slug;
|
|
485
|
-
|
|
508
|
+
writeUnlistedSubPage(
|
|
486
509
|
path.join(outputDir, `${dir}--asset-${slug}.mdx`),
|
|
487
|
-
|
|
510
|
+
title,
|
|
511
|
+
`${skillSlugBase}/asset-${slug}`,
|
|
512
|
+
escapeForMdx(raw.trim()),
|
|
488
513
|
);
|
|
489
514
|
}
|
|
490
515
|
}
|
|
@@ -522,11 +547,10 @@ function generateAgentsDocs(config: ClaudeResourcesConfig): AgentItem[] {
|
|
|
522
547
|
const description = (parsed.data.description as string) || "";
|
|
523
548
|
const model = (parsed.data.model as string) || "";
|
|
524
549
|
const fileSlug = file.replace(/\.md$/, "");
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
550
|
+
assertNotIndexReserved(
|
|
551
|
+
fileSlug,
|
|
552
|
+
`claude-resources: ".claude/agents/index.md" uses the reserved name "index", which is used for the category metadata file. Rename the agent file to resolve the conflict.`,
|
|
553
|
+
);
|
|
530
554
|
|
|
531
555
|
items.push({ name, file: fileSlug, description, model });
|
|
532
556
|
|
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Type notes:
|
|
8
8
|
* - zdtp's `ColorScheme` requires a `shikiTheme: string` field that is not
|
|
9
|
-
* present in
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* present in this project's local `ColorScheme` type or data (zdtp uses it
|
|
10
|
+
* only for the panel's client-side code-block preview). Rather than an unsafe
|
|
11
|
+
* `as unknown as Record<string, ZdtpColorScheme>` double-cast, every local
|
|
12
|
+
* scheme map is run through `toZdtpColorSchemes()` below, which supplies
|
|
13
|
+
* `DEFAULT_SHIKI_THEME` as the fallback so the result satisfies zdtp's
|
|
14
|
+
* required-field shape with an ordinary type-checked assignment.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import type {
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
SIZE_TOKENS,
|
|
29
30
|
} from "./design-tokens-manifest";
|
|
30
31
|
import { colorSchemes } from "./color-schemes";
|
|
32
|
+
import type { ColorScheme as LocalColorScheme } from "./color-schemes";
|
|
31
33
|
import { SEMANTIC_DEFAULTS, SEMANTIC_CSS_NAMES } from "./color-scheme-utils";
|
|
32
34
|
import { settings } from "./settings";
|
|
33
35
|
import { DESIGN_TOKEN_SCHEMA } from "@takazudo/zudo-doc/theme";
|
|
@@ -49,6 +51,30 @@ const BASE_DEFAULTS = {
|
|
|
49
51
|
*/
|
|
50
52
|
const DEFAULT_SHIKI_THEME = "github-dark";
|
|
51
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Normalize this project's local `ColorScheme` records into zdtp's
|
|
56
|
+
* `ColorScheme` shape. zdtp's type requires `shikiTheme: string`; the local
|
|
57
|
+
* scheme type makes it optional. This helper fills `DEFAULT_SHIKI_THEME` only
|
|
58
|
+
* when a scheme doesn't declare its own, so the result is assignable to
|
|
59
|
+
* `Record<string, ZdtpColorScheme>` via an ordinary type-checked assignment —
|
|
60
|
+
* replacing the previous `as unknown as` double-cast that bypassed every field
|
|
61
|
+
* check. Tracked upstream at Takazudo/zudo-design-token-panel#342 (shikiTheme
|
|
62
|
+
* should be optional in zdtp's `ColorScheme` type); drop this helper once that
|
|
63
|
+
* lands.
|
|
64
|
+
*/
|
|
65
|
+
function toZdtpColorSchemes(
|
|
66
|
+
schemes: Record<string, LocalColorScheme>,
|
|
67
|
+
): Record<string, ZdtpColorScheme> {
|
|
68
|
+
const normalized: Record<string, ZdtpColorScheme> = {};
|
|
69
|
+
for (const [name, scheme] of Object.entries(schemes)) {
|
|
70
|
+
normalized[name] = {
|
|
71
|
+
...scheme,
|
|
72
|
+
shikiTheme: scheme.shikiTheme ?? DEFAULT_SHIKI_THEME,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
77
|
+
|
|
52
78
|
/**
|
|
53
79
|
* Initial palette taken from the configured active scheme.
|
|
54
80
|
*/
|
|
@@ -178,9 +204,9 @@ const COLOR_EXTRAS: ColorClusterExtras = {
|
|
|
178
204
|
},
|
|
179
205
|
baseDefaults: BASE_DEFAULTS,
|
|
180
206
|
defaultShikiTheme: DEFAULT_SHIKI_THEME,
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
colorSchemes: colorSchemes
|
|
207
|
+
// toZdtpColorSchemes fills the fallback only for schemes without their own
|
|
208
|
+
// shikiTheme, so this is a type-checked assignment rather than an unsafe cast.
|
|
209
|
+
colorSchemes: toZdtpColorSchemes(colorSchemes),
|
|
184
210
|
panelSettings: {
|
|
185
211
|
colorScheme: settings.colorScheme,
|
|
186
212
|
// colorMode: strip off respectPrefersColorScheme (not in zdtp's shape).
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
// zfb plugin module: doc-history.
|
|
2
3
|
//
|
|
3
4
|
// Wires three lifecycle hooks for the doc-history integration:
|
|
4
5
|
//
|
|
5
6
|
// preBuild — emits `.zfb/doc-history-meta.json` (per-page git metadata
|
|
6
|
-
// consumed at bundle time).
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// directly. Honours `SKIP_DOC_HISTORY=1` via `env: process.env`.
|
|
7
|
+
// consumed at bundle time). Calls `runDocHistoryMetaStep`
|
|
8
|
+
// directly; honours `SKIP_DOC_HISTORY=1` via the runner's
|
|
9
|
+
// own env check.
|
|
10
10
|
//
|
|
11
11
|
// postBuild — invokes `runDocHistoryPostBuild` to write
|
|
12
12
|
// `<outDir>/doc-history/<slug>.json` files. Skipped by default
|
|
@@ -20,69 +20,45 @@
|
|
|
20
20
|
// Inline functions are not supported by zfb's plugin runtime — see
|
|
21
21
|
// `@takazudo/zfb/plugins` source comment. Plugins must be authored as
|
|
22
22
|
// standalone modules and referenced from `zfb.config.ts` by `name`.
|
|
23
|
-
//
|
|
24
|
-
// The legacy `scripts/zfb-{pre,post}build.mjs` npm-script glue and
|
|
25
|
-
// `scripts/dev-sidecar.mjs` stay in place during the merge window;
|
|
26
|
-
// T6 retires them once all lifecycle epics land.
|
|
27
23
|
|
|
28
|
-
import {
|
|
29
|
-
import { fileURLToPath } from "node:url";
|
|
30
|
-
import { dirname, resolve } from "node:path";
|
|
24
|
+
/** @import { ZfbBuildHookContext, ZfbDevMiddlewareContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
|
|
31
25
|
|
|
26
|
+
import { runDocHistoryMetaStep } from "@takazudo/zudo-doc/integrations/doc-history";
|
|
32
27
|
import { runDocHistoryPostBuild } from "@takazudo/zudo-doc/integrations/doc-history";
|
|
33
28
|
import { createDocHistoryDevMiddleware } from "@takazudo/zudo-doc/integrations/doc-history";
|
|
34
29
|
import { connectToZfbHandler } from "./connect-adapter.mjs";
|
|
35
30
|
|
|
36
|
-
|
|
37
|
-
// tsx is a workspace dep; resolving the binary explicitly avoids PATH
|
|
38
|
-
// dependency — the plugin host is spawned by zfb without the user's shell profile.
|
|
39
|
-
const TSX_BIN = resolve(HERE, "..", "node_modules", ".bin", "tsx");
|
|
40
|
-
|
|
31
|
+
/** @type {ZfbPlugin} */
|
|
41
32
|
export default {
|
|
42
33
|
name: "doc-history",
|
|
43
34
|
|
|
35
|
+
/** @param {ZfbBuildHookContext} ctx */
|
|
44
36
|
async preBuild(ctx) {
|
|
45
37
|
const { docsDir, locales } = ctx.options;
|
|
46
|
-
|
|
47
|
-
// (`runDocHistoryMetaStep`) honours SKIP_DOC_HISTORY=1 internally;
|
|
48
|
-
// passing `env: process.env` propagates the flag to the child process.
|
|
49
|
-
const optsJson = JSON.stringify({
|
|
38
|
+
await runDocHistoryMetaStep({
|
|
50
39
|
projectRoot: ctx.projectRoot,
|
|
51
40
|
docsDir: typeof docsDir === "string" ? docsDir : "src/content/docs",
|
|
52
|
-
locales:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const { runDocHistoryMetaStep } = await import("@takazudo/zudo-doc/integrations/doc-history");
|
|
57
|
-
const opts = ${optsJson};
|
|
58
|
-
await runDocHistoryMetaStep(opts);
|
|
59
|
-
})().catch((err) => {
|
|
60
|
-
process.stderr.write(err && err.stack ? err.stack : String(err));
|
|
61
|
-
process.exit(1);
|
|
62
|
-
});
|
|
63
|
-
`;
|
|
64
|
-
const result = spawnSync(TSX_BIN, ["-e", script], {
|
|
65
|
-
cwd: ctx.projectRoot,
|
|
66
|
-
stdio: "inherit",
|
|
67
|
-
env: process.env,
|
|
41
|
+
locales:
|
|
42
|
+
locales != null
|
|
43
|
+
? /** @type {Record<string,{dir:string}>} */ (locales)
|
|
44
|
+
: undefined,
|
|
68
45
|
});
|
|
69
|
-
if (result.status !== 0) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
`doc-history-meta preBuild failed (exit ${result.status})`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
46
|
},
|
|
75
47
|
|
|
48
|
+
/** @param {ZfbBuildHookContext} ctx */
|
|
76
49
|
async postBuild(ctx) {
|
|
77
|
-
const { docsDir, locales } = ctx.options;
|
|
78
50
|
await runDocHistoryPostBuild(
|
|
79
|
-
{
|
|
51
|
+
/** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
|
|
80
52
|
{ outDir: ctx.outDir, logger: ctx.logger },
|
|
81
53
|
);
|
|
82
54
|
},
|
|
83
55
|
|
|
56
|
+
/** @param {ZfbDevMiddlewareContext} ctx */
|
|
84
57
|
devMiddleware(ctx) {
|
|
85
|
-
const middleware = createDocHistoryDevMiddleware(
|
|
58
|
+
const middleware = createDocHistoryDevMiddleware(
|
|
59
|
+
/** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
|
|
60
|
+
ctx.logger,
|
|
61
|
+
);
|
|
86
62
|
// zfb's `register(path, handler)` matches against the FULL request
|
|
87
63
|
// URL (no base-stripping). For a non-root base (e.g. "/my-docs/"),
|
|
88
64
|
// requests arrive as `/my-docs/doc-history/foo.json`, so we register
|
|
@@ -90,11 +66,17 @@ export default {
|
|
|
90
66
|
// and the route is `/doc-history` as expected. The v2 middleware
|
|
91
67
|
// itself is base-tolerant (matches via `url.includes("/doc-history/")`)
|
|
92
68
|
// and slices from `/doc-history/` onward when proxying upstream.
|
|
93
|
-
const basePrefix = stripTrailingSlash(
|
|
69
|
+
const basePrefix = stripTrailingSlash(
|
|
70
|
+
typeof ctx.options["base"] === "string" ? ctx.options["base"] : "",
|
|
71
|
+
);
|
|
94
72
|
ctx.register(`${basePrefix}/doc-history`, connectToZfbHandler(middleware));
|
|
95
73
|
},
|
|
96
74
|
};
|
|
97
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} s
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
98
80
|
function stripTrailingSlash(s) {
|
|
99
81
|
if (typeof s !== "string" || s.length === 0) return "";
|
|
100
82
|
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo, useRef } from "preact/compat";
|
|
2
|
-
import {
|
|
2
|
+
import type { Change } from "diff";
|
|
3
3
|
import type { DocHistoryData, DocHistoryEntry } from "@/types/doc-history";
|
|
4
4
|
import { SmartBreak } from "@/utils/smart-break";
|
|
5
5
|
|
|
@@ -104,8 +104,10 @@ interface DiffRow {
|
|
|
104
104
|
type: "context" | "removed" | "added" | "changed";
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
type DiffChanges = Change[];
|
|
108
|
+
|
|
107
109
|
function buildSideBySideRows(
|
|
108
|
-
changes:
|
|
110
|
+
changes: DiffChanges,
|
|
109
111
|
): DiffRow[] {
|
|
110
112
|
const rows: DiffRow[] = [];
|
|
111
113
|
let leftNum = 0;
|
|
@@ -177,11 +179,24 @@ function DiffViewer({
|
|
|
177
179
|
onBack: () => void;
|
|
178
180
|
showBackButton: boolean;
|
|
179
181
|
}) {
|
|
180
|
-
const changes =
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
const [changes, setChanges] = useState<DiffChanges | null>(null);
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
let cancelled = false;
|
|
186
|
+
// Lazy-load diff — only needed after History → Compare. This keeps the
|
|
187
|
+
// module out of the eager islands bundle.
|
|
188
|
+
import("diff").then(({ diffLines }) => {
|
|
189
|
+
if (!cancelled) {
|
|
190
|
+
setChanges(diffLines(selection.older.content, selection.newer.content));
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return () => { cancelled = true; };
|
|
194
|
+
}, [selection.older.content, selection.newer.content]);
|
|
195
|
+
|
|
196
|
+
const rows = useMemo(
|
|
197
|
+
() => (changes ? buildSideBySideRows(changes) : []),
|
|
198
|
+
[changes],
|
|
183
199
|
);
|
|
184
|
-
const rows = useMemo(() => buildSideBySideRows(changes), [changes]);
|
|
185
200
|
|
|
186
201
|
return (
|
|
187
202
|
<div className="flex flex-col h-full">
|
|
@@ -207,8 +222,13 @@ function DiffViewer({
|
|
|
207
222
|
</div>
|
|
208
223
|
</div>
|
|
209
224
|
|
|
210
|
-
{/* Side-by-side diff */}
|
|
211
|
-
|
|
225
|
+
{/* Side-by-side diff — shows a loading state while the diff module lazy-loads */}
|
|
226
|
+
{!changes && (
|
|
227
|
+
<div className="flex-1 flex items-center justify-center py-vsp-xl">
|
|
228
|
+
<p className="text-small text-muted">Loading diff…</p>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
<div className={`flex-1 overflow-auto${!changes ? " hidden" : ""}`}>
|
|
212
232
|
<table className="w-full border-collapse" style={{ tableLayout: "fixed" }}>
|
|
213
233
|
<colgroup>
|
|
214
234
|
<col style={{ width: "2.5rem" }} />
|
|
@@ -11,29 +11,14 @@
|
|
|
11
11
|
// params: { locale: string; tag: string }
|
|
12
12
|
// props: { tagInfo: TagInfo }
|
|
13
13
|
//
|
|
14
|
+
// Tag collection + rendering are shared with the default-locale route via
|
|
15
|
+
// pages/lib/_tag-pages.tsx (#2010) — this file owns only the param shape.
|
|
14
16
|
// Fallback strategy: see pages/lib/locale-merge.ts for full details.
|
|
15
|
-
// Locale docs take priority; base docs fill in missing slugs.
|
|
16
|
-
// Build tag map; emit one route per (locale, tag) pair.
|
|
17
17
|
|
|
18
|
-
import { mergeLocaleDocs } from "../../../lib/locale-merge";
|
|
19
|
-
import { loadDocs } from "../../../_data";
|
|
20
|
-
import { collectTags } from "@/utils/tags";
|
|
21
|
-
import type { TagInfo } from "@/utils/tags";
|
|
22
|
-
import { toRouteSlug } from "@/utils/slug";
|
|
23
|
-
import { t } from "@/config/i18n";
|
|
24
|
-
import { withBase, docsUrl } from "@/utils/base";
|
|
25
18
|
import { settings } from "@/config/settings";
|
|
26
|
-
import {
|
|
27
|
-
import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
|
|
28
|
-
import type { BreadcrumbItem } from "@takazudo/zudo-doc/breadcrumb";
|
|
29
|
-
import { DocCardGrid } from "@takazudo/zudo-doc/nav-indexing";
|
|
19
|
+
import type { TagInfo } from "@/utils/tags";
|
|
30
20
|
import type { JSX } from "preact";
|
|
31
|
-
import {
|
|
32
|
-
import { HeaderWithDefaults } from "../../../lib/_header-with-defaults";
|
|
33
|
-
import { HeadWithDefaults } from "../../../lib/_head-with-defaults";
|
|
34
|
-
import { composeMetaTitle } from "../../../lib/_compose-meta-title";
|
|
35
|
-
import { DocHistoryArea } from "../../../lib/_doc-history-area";
|
|
36
|
-
import { BodyEndIslands } from "../../../lib/_body-end-islands";
|
|
21
|
+
import { collectTagMapForLocale, TagDetailPageView } from "../../../lib/_tag-pages";
|
|
37
22
|
|
|
38
23
|
export const frontmatter = { title: "Tag" };
|
|
39
24
|
|
|
@@ -48,18 +33,7 @@ export function paths(): Array<{
|
|
|
48
33
|
}> = [];
|
|
49
34
|
|
|
50
35
|
for (const locale of Object.keys(settings.locales)) {
|
|
51
|
-
const
|
|
52
|
-
baseDocs: loadDocs("docs").filter((d) => !d.data.draft),
|
|
53
|
-
localeDocs: loadDocs(`docs-${locale}`).filter((d) => !d.data.draft),
|
|
54
|
-
applyDefaultLocaleOnlyFilter: true,
|
|
55
|
-
});
|
|
56
|
-
// category_no_page index files build no route — drop them AFTER the merge
|
|
57
|
-
// so a locale override carrying the flag first wins the merge (suppressing
|
|
58
|
-
// the base doc); pre-merge filtering would drop it from localeSlugSet and
|
|
59
|
-
// the unflagged base doc would resurface as a card linking to a locale
|
|
60
|
-
// route the docs route never builds.
|
|
61
|
-
const docs = mergedDocs.filter((d) => !d.data.category_no_page);
|
|
62
|
-
const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
|
|
36
|
+
const tagMap = collectTagMapForLocale(locale);
|
|
63
37
|
|
|
64
38
|
for (const [tag, tagInfo] of tagMap.entries()) {
|
|
65
39
|
result.push({
|
|
@@ -81,47 +55,5 @@ export default function LocaleDocTagPage({
|
|
|
81
55
|
params,
|
|
82
56
|
tagInfo,
|
|
83
57
|
}: PageProps): JSX.Element {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const countText =
|
|
87
|
-
tagInfo.count === 1
|
|
88
|
-
? t("doc.pageCountSingle", locale).replace("{count}", String(tagInfo.count))
|
|
89
|
-
: t("doc.pageCount", locale).replace("{count}", String(tagInfo.count));
|
|
90
|
-
|
|
91
|
-
const pageTitle = `${t("doc.taggedWith", locale)}: ${tag}`;
|
|
92
|
-
|
|
93
|
-
const breadcrumbItems: BreadcrumbItem[] = [
|
|
94
|
-
{ label: "Docs" },
|
|
95
|
-
{
|
|
96
|
-
label: t("doc.allTags", locale),
|
|
97
|
-
href: withBase(`/${locale}/docs/tags`),
|
|
98
|
-
},
|
|
99
|
-
{ label: tag },
|
|
100
|
-
];
|
|
101
|
-
|
|
102
|
-
const cardItems = tagInfo.docs.map((doc) => ({
|
|
103
|
-
href: docsUrl(doc.slug, locale),
|
|
104
|
-
title: doc.title,
|
|
105
|
-
description: doc.description,
|
|
106
|
-
}));
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<DocLayoutWithDefaults
|
|
110
|
-
title={composeMetaTitle(pageTitle)}
|
|
111
|
-
head={<HeadWithDefaults title={pageTitle} />}
|
|
112
|
-
lang={locale}
|
|
113
|
-
noindex={settings.noindex}
|
|
114
|
-
hideSidebar={true}
|
|
115
|
-
hideToc={true}
|
|
116
|
-
headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`/${locale}/docs/tags/${tag}`)} />}
|
|
117
|
-
breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
|
|
118
|
-
footerOverride={<FooterWithDefaults lang={locale} />}
|
|
119
|
-
bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
|
|
120
|
-
>
|
|
121
|
-
<h1 class="text-heading font-bold mb-vsp-xs">{pageTitle}</h1>
|
|
122
|
-
<p class="text-muted mb-vsp-lg">{countText}</p>
|
|
123
|
-
<DocCardGrid ariaLabel={pageTitle} items={cardItems} />
|
|
124
|
-
<DocHistoryArea slug={`tags/${tag}`} locale={locale} />
|
|
125
|
-
</DocLayoutWithDefaults>
|
|
126
|
-
);
|
|
58
|
+
return <TagDetailPageView locale={params.locale} tag={params.tag} tagInfo={tagInfo} />;
|
|
127
59
|
}
|
|
@@ -3,36 +3,21 @@
|
|
|
3
3
|
// Page module for the locale-prefixed "All Tags" index route.
|
|
4
4
|
//
|
|
5
5
|
// Non-default-locale "All Tags" index page. paths() emits one route per
|
|
6
|
-
// locale defined in settings.locales (
|
|
7
|
-
// handled by pages/docs/tags/index.tsx). The component recomputes the tag
|
|
6
|
+
// locale defined in settings.locales (the default locale has no prefix — it
|
|
7
|
+
// is handled by pages/docs/tags/index.tsx). The component recomputes the tag
|
|
8
8
|
// map at render time using a locale-doc + base-doc fallback strategy
|
|
9
9
|
// (see pages/lib/locale-merge.ts for the merge logic).
|
|
10
10
|
//
|
|
11
|
-
//
|
|
11
|
+
// Tag collection + rendering are shared with the default-locale route via
|
|
12
|
+
// pages/lib/_tag-pages.tsx (#2010).
|
|
12
13
|
//
|
|
13
14
|
// paths() contract (zfb ADR-004 — synchronous):
|
|
14
15
|
// params: { locale: string }
|
|
15
16
|
// props: (none — tag map computed at render time)
|
|
16
17
|
|
|
17
|
-
import { mergeLocaleDocs } from "../../../lib/locale-merge";
|
|
18
|
-
import { loadDocs } from "../../../_data";
|
|
19
|
-
import { collectTags } from "@/utils/tags";
|
|
20
|
-
import { toRouteSlug } from "@/utils/slug";
|
|
21
|
-
import { t } from "@/config/i18n";
|
|
22
|
-
import { withBase } from "@/utils/base";
|
|
23
18
|
import { settings } from "@/config/settings";
|
|
24
|
-
import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
|
|
25
|
-
import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
|
|
26
|
-
import type { BreadcrumbItem } from "@takazudo/zudo-doc/breadcrumb";
|
|
27
|
-
import { TagNav } from "@takazudo/zudo-doc/nav-indexing";
|
|
28
|
-
import type { TagItem, TagNavLabels } from "@takazudo/zudo-doc/nav-indexing";
|
|
29
19
|
import type { JSX } from "preact";
|
|
30
|
-
import {
|
|
31
|
-
import { HeaderWithDefaults } from "../../../lib/_header-with-defaults";
|
|
32
|
-
import { HeadWithDefaults } from "../../../lib/_head-with-defaults";
|
|
33
|
-
import { composeMetaTitle } from "../../../lib/_compose-meta-title";
|
|
34
|
-
import { DocHistoryArea } from "../../../lib/_doc-history-area";
|
|
35
|
-
import { BodyEndIslands } from "../../../lib/_body-end-islands";
|
|
20
|
+
import { TagsIndexPageView } from "../../../lib/_tag-pages";
|
|
36
21
|
|
|
37
22
|
export const frontmatter = { title: "All Tags" };
|
|
38
23
|
|
|
@@ -50,61 +35,5 @@ interface PageProps {
|
|
|
50
35
|
export default function LocaleTagsIndexPage({
|
|
51
36
|
params,
|
|
52
37
|
}: PageProps): JSX.Element {
|
|
53
|
-
|
|
54
|
-
const pageTitle = t("doc.allTags", locale);
|
|
55
|
-
|
|
56
|
-
const { docs: mergedDocs } = mergeLocaleDocs({
|
|
57
|
-
baseDocs: loadDocs("docs").filter((d) => !d.data.draft),
|
|
58
|
-
localeDocs: loadDocs(`docs-${locale}`).filter((d) => !d.data.draft),
|
|
59
|
-
applyDefaultLocaleOnlyFilter: true,
|
|
60
|
-
});
|
|
61
|
-
// category_no_page index files build no route — drop them AFTER the merge
|
|
62
|
-
// so a locale override carrying the flag first wins the merge (suppressing
|
|
63
|
-
// the base doc); pre-merge filtering would drop it from localeSlugSet and
|
|
64
|
-
// the unflagged base doc would resurface as a card linking to a locale
|
|
65
|
-
// route the docs route never builds.
|
|
66
|
-
const docs = mergedDocs.filter((d) => !d.data.category_no_page);
|
|
67
|
-
const tagMap = collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
|
|
68
|
-
|
|
69
|
-
const labels: TagNavLabels = {
|
|
70
|
-
tags: t("doc.tags", locale),
|
|
71
|
-
taggedWith: t("doc.taggedWith", locale),
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Sort alphabetically using the page locale — matches documented tag-nav sort order.
|
|
75
|
-
const tags: TagItem[] = [...tagMap.values()]
|
|
76
|
-
.sort((a, b) => a.tag.localeCompare(b.tag, locale))
|
|
77
|
-
.map((info) => ({
|
|
78
|
-
tag: info.tag,
|
|
79
|
-
count: info.count,
|
|
80
|
-
href: withBase(`/${locale}/docs/tags/${info.tag}`),
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
const breadcrumbItems: BreadcrumbItem[] = [
|
|
84
|
-
{ label: "Docs" },
|
|
85
|
-
{ label: pageTitle },
|
|
86
|
-
];
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<DocLayoutWithDefaults
|
|
90
|
-
title={composeMetaTitle(pageTitle)}
|
|
91
|
-
head={<HeadWithDefaults title={pageTitle} />}
|
|
92
|
-
lang={locale}
|
|
93
|
-
noindex={settings.noindex}
|
|
94
|
-
hideSidebar={true}
|
|
95
|
-
hideToc={true}
|
|
96
|
-
headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`/${locale}/docs/tags`)} />}
|
|
97
|
-
breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
|
|
98
|
-
footerOverride={<FooterWithDefaults lang={locale} />}
|
|
99
|
-
bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
|
|
100
|
-
>
|
|
101
|
-
<h1 class="text-heading font-bold mb-vsp-lg">{pageTitle}</h1>
|
|
102
|
-
{!settings.docTags || tags.length === 0 ? (
|
|
103
|
-
<p class="text-muted">{t("doc.noTags", locale)}</p>
|
|
104
|
-
) : (
|
|
105
|
-
<TagNav variant="all" tags={tags} labels={labels} />
|
|
106
|
-
)}
|
|
107
|
-
<DocHistoryArea slug="tags" locale={locale} />
|
|
108
|
-
</DocLayoutWithDefaults>
|
|
109
|
-
);
|
|
38
|
+
return <TagsIndexPageView locale={params.locale} />;
|
|
110
39
|
}
|