@takazudo/zudo-doc 5.7.0 → 5.9.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 (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +17 -0
  3. package/dist/config.d.ts +18 -0
  4. package/dist/config.js +1 -0
  5. package/dist/eject/index.js +9 -0
  6. package/dist/i18n-defaults/index.js +3 -0
  7. package/dist/plugins/codex-resources.d.ts +4 -0
  8. package/dist/plugins/codex-resources.js +31 -0
  9. package/dist/plugins/internal/claude-resources/generate.js +20 -291
  10. package/dist/plugins/internal/codex-resources/agents-md.d.ts +6 -0
  11. package/dist/plugins/internal/codex-resources/agents-md.js +70 -0
  12. package/dist/plugins/internal/codex-resources/agents.d.ts +7 -0
  13. package/dist/plugins/internal/codex-resources/agents.js +104 -0
  14. package/dist/plugins/internal/codex-resources/config.d.ts +6 -0
  15. package/dist/plugins/internal/codex-resources/config.js +111 -0
  16. package/dist/plugins/internal/codex-resources/generate.d.ts +20 -0
  17. package/dist/plugins/internal/codex-resources/generate.js +34 -0
  18. package/dist/plugins/internal/codex-resources/hooks.d.ts +6 -0
  19. package/dist/plugins/internal/codex-resources/hooks.js +174 -0
  20. package/dist/plugins/internal/codex-resources/index.d.ts +24 -0
  21. package/dist/plugins/internal/codex-resources/index.js +21 -0
  22. package/dist/plugins/internal/codex-resources/overview.d.ts +10 -0
  23. package/dist/plugins/internal/codex-resources/overview.js +33 -0
  24. package/dist/plugins/internal/codex-resources/rules.d.ts +6 -0
  25. package/dist/plugins/internal/codex-resources/rules.js +255 -0
  26. package/dist/plugins/internal/codex-resources/skills.d.ts +2 -0
  27. package/dist/plugins/internal/codex-resources/skills.js +84 -0
  28. package/dist/plugins/internal/codex-resources/utils.d.ts +14 -0
  29. package/dist/plugins/internal/codex-resources/utils.js +80 -0
  30. package/dist/plugins/internal/resource-docs-shared/fs.d.ts +3 -0
  31. package/dist/plugins/internal/resource-docs-shared/fs.js +19 -0
  32. package/dist/plugins/internal/resource-docs-shared/index.d.ts +7 -0
  33. package/dist/plugins/internal/resource-docs-shared/index.js +44 -0
  34. package/dist/plugins/internal/resource-docs-shared/links.d.ts +22 -0
  35. package/dist/plugins/internal/resource-docs-shared/links.js +44 -0
  36. package/dist/plugins/internal/resource-docs-shared/markdown-structure.d.ts +9 -0
  37. package/dist/plugins/internal/resource-docs-shared/markdown-structure.js +22 -0
  38. package/dist/plugins/internal/resource-docs-shared/mdx.d.ts +23 -0
  39. package/dist/plugins/internal/resource-docs-shared/mdx.js +63 -0
  40. package/dist/plugins/internal/resource-docs-shared/skills.d.ts +37 -0
  41. package/dist/plugins/internal/resource-docs-shared/skills.js +318 -0
  42. package/dist/plugins/internal/resource-docs-shared/walk.d.ts +20 -0
  43. package/dist/plugins/internal/resource-docs-shared/walk.js +41 -0
  44. package/dist/preset.d.ts +8 -0
  45. package/dist/preset.js +11 -0
  46. package/dist/safelist.css +1 -1
  47. package/dist/settings.d.ts +5 -0
  48. package/dist/transitions/nested-island-props-refresh.js +9 -3
  49. package/eject/header/gen-nav-overflow-script.mjs +771 -0
  50. package/eject/header/header.tsx +2 -0
  51. package/eject/header/nav-overflow-script.ts +11 -8
  52. package/package.json +9 -4
  53. /package/dist/plugins/internal/{claude-resources → resource-docs-shared}/escape-for-mdx.d.ts +0 -0
  54. /package/dist/plugins/internal/{claude-resources → resource-docs-shared}/escape-for-mdx.js +0 -0
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gen-nav-overflow-script.mjs
3
+ //
4
+ // Generates the git-committed `src/header/nav-overflow-generated-script.ts`
5
+ // build artifact (zudolab/zudo-doc#3534, epic #3533).
6
+ //
7
+ // WHY this exists (the load-bearing reason — recoverable only from the
8
+ // issue): the previous `src/header/nav-overflow-script.ts` built
9
+ // NAV_OVERFLOW_SCRIPT as a template literal evaluated at MODULE-EVALUATION
10
+ // TIME, embedding `pathMatchesNavPath`/`computeActiveNavPath` via
11
+ // `Function.prototype.toString()` on the LIVE imported bindings, plus a third
12
+ // live `CURRENT_PATH_SCRIPT_PRELUDE` interpolation added in 5.6.0
13
+ // (zudolab/zudo-doc#3502). Because the string was recomputed by executing
14
+ // code whose emit shape depends on the CONSUMER's own bundler, two
15
+ // renderings of the same logical script hashed differently (measured: zfb
16
+ // SSR 12392 bytes vs a consumer Vite build 12216 bytes) — so a consumer
17
+ // publishing a CSP inline-script hash could never reconcile it against this
18
+ // package's own build.
19
+ //
20
+ // This script freezes the whole embedding ONCE, at zudo-doc's own package
21
+ // build time, into a plain string literal — so nothing downstream ever
22
+ // reflects on a live function again, and the emitted bytes become
23
+ // consumer-bundler-independent.
24
+ //
25
+ // HOW (package build): reads `src/current-path/index.ts`,
26
+ // `src/header/nav-active.ts`, `src/header/nav-class-tokens.ts`, and
27
+ // `src/transitions/page-events.ts`
28
+ // SOURCE, strips TypeScript types deterministically via esbuild's
29
+ // `transformSync()` (same rationale as gen-search-widget-script.mjs — see
30
+ // that file's header comment for the full `ts.transpileModule()` vs esbuild
31
+ // history, zudolab/zudo-doc#3422 / #3430 — identical here: esbuild is only a
32
+ // package dependency (it is also used by the shipped ejected-header generator),
33
+ // and the effective floor tracks the root `pnpm.overrides` range), executes
34
+ // each transpiled CommonJS module in an isolated sandbox (empty
35
+ // `module`/`exports`; all four source files are import-free), then reads
36
+ // the REAL runtime values off each sandbox's `exports`:
37
+ // - `exports.CURRENT_PATH_SCRIPT_PRELUDE` — read as a VALUE, never
38
+ // reconstructed (it is itself a pre-built string, not a function).
39
+ // - `exports.pathMatchesNavPath.toString()` /
40
+ // `exports.computeActiveNavPath.toString()` — the exact same
41
+ // Function.prototype.toString() mechanism the old code used, just run
42
+ // once here instead of on every module evaluation downstream.
43
+ // `computeActiveNavPath` closes over `pathMatchesNavPath`, so both are
44
+ // extracted together (nav-active.ts:70-73 documents the closure).
45
+ // - `exports.NAV_TOP_ACTIVE` / … — the twelve class-token arrays from
46
+ // nav-class-tokens.ts, read as real array values. The three splice
47
+ // formatters (`clsArgs`/`clsLiteral`/`clsAppend`) that used to live in
48
+ // nav-overflow-script.ts move into this generator (below) since they now
49
+ // run once at generation time instead of at every module evaluation.
50
+ // - `exports.AFTER_NAVIGATE_EVENT` — the real event-name string, never
51
+ // hardcoded here.
52
+ //
53
+ // **Duplicates the gen-search-widget-script.mjs scaffolding on purpose — does
54
+ // NOT extract a shared lib.** Factoring the two generators together would
55
+ // require touching the search-widget generator, whose output is CSP-hash-pinned
56
+ // in two places (its own drift-guard test and any downstream consumer's
57
+ // published hash); that byte-preserving-refactor risk isn't worth coupling to
58
+ // this change. A shared lib is a possible follow-up, not this one
59
+ // (zudolab/zudo-doc#3533 epic body).
60
+ //
61
+ // Composes the full IIFE script string (the template logic moved out of
62
+ // nav-overflow-script.ts) and writes it, write-if-changed, to
63
+ // `src/header/nav-overflow-generated-script.ts` with a GENERATED banner.
64
+ //
65
+ // Like `search-widget-script/generated-script.ts`, this generated file IS
66
+ // tracked in git — a deliberate departure from the gitignored-build-artifact
67
+ // convention used by `routes-src/` / `virtual-modules.d.ts`. Its whole value
68
+ // is a frozen, reviewable snapshot that a plain `git diff` can catch drifting
69
+ // from its four source files. It stays internal like the source files it
70
+ // reads — NOT added to the package `exports` map or `files[]`.
71
+ //
72
+ // EJECTED HEADER MODE (zudolab/zudo-doc#3541): copy-eject-sources.mjs ships
73
+ // this same generator beside the ejected header files. In that location it
74
+ // reads project-owned nav-active.ts / nav-class-tokens.ts locally, while the
75
+ // current-path and page-event inputs come from the installed package's dist/
76
+ // modules. It resolves esbuild from that package's dependency graph, so a
77
+ // consumer runs the understandable, self-contained command printed by eject:
78
+ // `node ./src/components/zudo-doc/header/gen-nav-overflow-script.mjs`.
79
+ //
80
+ // `buildNavOverflowScript()` is exported so both this script's CLI entry
81
+ // point AND the vitest drift-guard test
82
+ // (`src/header/__tests__/nav-overflow-script.test.ts`) can call it: the test
83
+ // re-runs the REAL extraction (fresh transpile of the current source files)
84
+ // and asserts it byte-matches the frozen `NAV_OVERFLOW_SCRIPT` shipped in
85
+ // `nav-overflow-generated-script.ts` — catching the case where one of the
86
+ // four source files changed but the generated file was never regenerated.
87
+ //
88
+ // Runs BEFORE tsup (build / prepare / predev — see package.json) so the
89
+ // first compile always has `nav-overflow-generated-script.ts` on disk to
90
+ // import from `nav-overflow-script.ts`, AND is hooked into the tsup `--watch`
91
+ // `onSuccess` chain (tsup.config.ts) BEFORE `copy-eject-sources.mjs` (running
92
+ // it after would copy the previous literal into `eject/` on the first watch
93
+ // cycle); write-if-changed keeps that loop-free (an unchanged regeneration
94
+ // does not re-trigger tsup's watcher).
95
+ //
96
+ // The `pnpm check:nav-overflow-drift` guard (b4push step + pr-checks CI step,
97
+ // mirroring `check:search-widget-drift`) landed as sub-issue
98
+ // zudolab/zudo-doc#3535 — see scripts/check-nav-overflow-script-drift.sh.
99
+
100
+ import { readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
101
+ import { createRequire } from "node:module";
102
+ import { resolve, dirname } from "node:path";
103
+ import { fileURLToPath } from "node:url";
104
+
105
+ const __dirname = dirname(fileURLToPath(import.meta.url));
106
+
107
+ /** Find the installed package from an ejected header without assuming npm's
108
+ * node_modules layout. The symlinked package root is enough: createRequire()
109
+ * below resolves esbuild from the package's own dependency graph under npm,
110
+ * pnpm, and yarn installs. */
111
+ function findInstalledPackageRoot(startDirs) {
112
+ for (const startDir of startDirs) {
113
+ let dir = resolve(startDir);
114
+ while (true) {
115
+ const candidate = resolve(dir, "node_modules/@takazudo/zudo-doc");
116
+ if (existsSync(resolve(candidate, "package.json"))) return realpathSync(candidate);
117
+ const parent = dirname(dir);
118
+ if (parent === dir) break;
119
+ dir = parent;
120
+ }
121
+ }
122
+ throw new Error(
123
+ "[gen-nav-overflow-script] could not find node_modules/@takazudo/zudo-doc. " +
124
+ "Run this command from an installed zudo-doc project after `pnpm install`.",
125
+ );
126
+ }
127
+
128
+ /** Resolve the generator's four inputs and output in package-build or ejected mode. */
129
+ export function resolveGenerationContext() {
130
+ const packageRootCandidate = resolve(__dirname, "..");
131
+ const packageHeaderDir = resolve(packageRootCandidate, "src/header");
132
+ const isPackageGenerator = existsSync(resolve(packageHeaderDir, "nav-active.ts"));
133
+
134
+ if (isPackageGenerator) {
135
+ return {
136
+ kind: "package",
137
+ packageRoot: packageRootCandidate,
138
+ currentPathSource: resolve(packageRootCandidate, "src/current-path/index.ts"),
139
+ navActiveSource: resolve(packageHeaderDir, "nav-active.ts"),
140
+ navClassTokensSource: resolve(packageHeaderDir, "nav-class-tokens.ts"),
141
+ pageEventsSource: resolve(packageRootCandidate, "src/transitions/page-events.ts"),
142
+ outputPath: resolve(packageHeaderDir, "nav-overflow-generated-script.ts"),
143
+ };
144
+ }
145
+
146
+ // In an ejected payload this script sits beside the two project-owned
147
+ // customization inputs. The current-path prelude and page-event vocabulary
148
+ // intentionally stay package-owned and come from the installed compiled
149
+ // modules, so an ejected project does not fork framework lifecycle inputs.
150
+ if (
151
+ !existsSync(resolve(__dirname, "nav-active.ts")) ||
152
+ !existsSync(resolve(__dirname, "nav-class-tokens.ts"))
153
+ ) {
154
+ throw new Error(
155
+ "[gen-nav-overflow-script] expected nav-active.ts and nav-class-tokens.ts " +
156
+ `beside the ejected generator at ${__dirname}`,
157
+ );
158
+ }
159
+ const packageRoot = findInstalledPackageRoot([process.cwd(), __dirname]);
160
+ return {
161
+ kind: "ejected",
162
+ packageRoot,
163
+ currentPathSource: resolve(packageRoot, "dist/current-path/index.js"),
164
+ navActiveSource: resolve(__dirname, "nav-active.ts"),
165
+ navClassTokensSource: resolve(__dirname, "nav-class-tokens.ts"),
166
+ pageEventsSource: resolve(packageRoot, "dist/transitions/page-events.js"),
167
+ outputPath: resolve(__dirname, "nav-overflow-generated-script.ts"),
168
+ };
169
+ }
170
+
171
+ function loadTransformSync(packageRoot) {
172
+ try {
173
+ const requireFromPackage = createRequire(resolve(packageRoot, "package.json"));
174
+ return requireFromPackage("esbuild").transformSync;
175
+ } catch (err) {
176
+ throw new Error(
177
+ "[gen-nav-overflow-script] could not load the esbuild dependency shipped by " +
178
+ `@takazudo/zudo-doc: ${err.message}`,
179
+ );
180
+ }
181
+ }
182
+
183
+ // Explicit, stable esbuild options — identical rationale to
184
+ // gen-search-widget-script.mjs's TRANSFORM_OPTIONS (see that file): `format:
185
+ // "cjs"` so each source's `export`s become a CommonJS `module.exports` object
186
+ // we can read off the sandboxed `module` param, `target: "es2020"` so
187
+ // `var`-style function bodies pass through unchanged (no downlevel helpers),
188
+ // `platform: "neutral"` so esbuild injects no Node/browser global shims (all
189
+ // four source files are import-free — nothing to shim). Every minify knob is
190
+ // explicitly false: the acceptance criterion is a human-readable, byte-stable
191
+ // emit across runs, never a minified one.
192
+ const TRANSFORM_OPTIONS = {
193
+ loader: "ts",
194
+ format: "cjs",
195
+ target: "es2020",
196
+ platform: "neutral",
197
+ sourcemap: false,
198
+ minify: false,
199
+ minifyWhitespace: false,
200
+ minifyIdentifiers: false,
201
+ minifySyntax: false,
202
+ };
203
+
204
+ /** A source string carrying no CommonJS/ESM module scaffolding that could
205
+ * survive the transpile into the embedded browser script. Matches only the
206
+ * syntactic shapes (`require(...)`, `import(...)`, `module.exports`,
207
+ * `exports.x`), NOT the bare words — the extracted function text includes
208
+ * body comments, and a comment merely mentioning "import"/"export" must not
209
+ * fail the build. */
210
+ function assertNoModuleScaffolding(label, text) {
211
+ if (/\brequire\s*\(|\bimport\s*\(|\bmodule\.exports\b|\bexports\s*\./.test(text)) {
212
+ throw new Error(
213
+ `[gen-nav-overflow-script] ${label} leaked module scaffolding into the embedded script:\n${text}`,
214
+ );
215
+ }
216
+ }
217
+
218
+ /** Transpile a TS source file to CommonJS JS, type-stripped, via esbuild's `transformSync`. */
219
+ function transpile(sourcePath, transformSync) {
220
+ const source = readFileSync(sourcePath, "utf8");
221
+ let result;
222
+ try {
223
+ result = transformSync(source, {
224
+ ...TRANSFORM_OPTIONS,
225
+ loader: sourcePath.endsWith(".ts") ? "ts" : "js",
226
+ });
227
+ } catch (err) {
228
+ const messages = (err.errors ?? []).map((e) => e.text).join("; ") || err.message;
229
+ throw new Error(
230
+ `[gen-nav-overflow-script] failed to transpile ${sourcePath}: ${messages}`,
231
+ );
232
+ }
233
+ // Warnings are fatal on purpose: this file's output is embedded verbatim
234
+ // into a shipped browser script, so anything esbuild flags must be resolved
235
+ // in the source rather than silently carried through.
236
+ if (result.warnings.length > 0) {
237
+ const messages = result.warnings.map((w) => w.text).join("; ");
238
+ throw new Error(
239
+ `[gen-nav-overflow-script] esbuild reported warning(s) while transpiling ${sourcePath} (treated as fatal): ${messages}`,
240
+ );
241
+ }
242
+ return result.code;
243
+ }
244
+
245
+ /** Execute transpiled CommonJS source in an isolated sandbox and return its exports. */
246
+ function executeCommonJs(code, label) {
247
+ const moduleObj = { exports: {} };
248
+ const fn = new Function("module", "exports", code);
249
+ try {
250
+ fn(moduleObj, moduleObj.exports);
251
+ } catch (err) {
252
+ throw new Error(
253
+ `[gen-nav-overflow-script] failed to execute transpiled ${label}: ${err.message}`,
254
+ );
255
+ }
256
+ return moduleObj.exports;
257
+ }
258
+
259
+ /** Extract the real CURRENT_PATH_SCRIPT_PRELUDE value from current-path/index.ts. */
260
+ function extractCurrentPathPrelude(context, transformSync) {
261
+ const outputText = transpile(context.currentPathSource, transformSync);
262
+ const exportsObj = executeCommonJs(outputText, "current-path/index.ts");
263
+ const value = exportsObj.CURRENT_PATH_SCRIPT_PRELUDE;
264
+ if (typeof value !== "string" || !value) {
265
+ throw new Error(
266
+ "[gen-nav-overflow-script] CURRENT_PATH_SCRIPT_PRELUDE missing or not a string in current-path/index.ts",
267
+ );
268
+ }
269
+ assertNoModuleScaffolding("CURRENT_PATH_SCRIPT_PRELUDE", value);
270
+ return value;
271
+ }
272
+
273
+ /** Extract the real, unit-tested pathMatchesNavPath/computeActiveNavPath source text from nav-active.ts. */
274
+ function extractNavActiveFunctions(context, transformSync) {
275
+ const outputText = transpile(context.navActiveSource, transformSync);
276
+ const exportsObj = executeCommonJs(outputText, "nav-active.ts");
277
+ const { pathMatchesNavPath, computeActiveNavPath } = exportsObj;
278
+ if (typeof pathMatchesNavPath !== "function" || typeof computeActiveNavPath !== "function") {
279
+ throw new Error(
280
+ "[gen-nav-overflow-script] nav-active.ts did not export pathMatchesNavPath/computeActiveNavPath functions",
281
+ );
282
+ }
283
+ const pathMatchesNavPathSrc = pathMatchesNavPath.toString();
284
+ const computeActiveNavPathSrc = computeActiveNavPath.toString();
285
+ assertNoModuleScaffolding("pathMatchesNavPath", pathMatchesNavPathSrc);
286
+ assertNoModuleScaffolding("computeActiveNavPath", computeActiveNavPathSrc);
287
+ return { pathMatchesNavPathSrc, computeActiveNavPathSrc };
288
+ }
289
+
290
+ // The twelve class-token arrays nav-overflow-script.ts used to import
291
+ // directly from nav-class-tokens.ts (see that module's header comment for
292
+ // the SSR ↔ runtime lockstep rationale, zudolab/zudo-doc#3023).
293
+ const NAV_CLASS_TOKEN_NAMES = [
294
+ "NAV_TOP_ACTIVE",
295
+ "NAV_TOP_INACTIVE",
296
+ "NAV_CHEVRON_ACTIVE",
297
+ "NAV_CHEVRON_INACTIVE",
298
+ "NAV_CHILD_ACTIVE",
299
+ "NAV_CHILD_INACTIVE",
300
+ "NAV_MENU_PARENT",
301
+ "NAV_MENU_PARENT_ACTIVE_SUFFIX",
302
+ "NAV_MENU_PLAIN",
303
+ "NAV_MENU_PLAIN_ACTIVE_SUFFIX",
304
+ "NAV_MENU_CHILD_ACTIVE",
305
+ "NAV_MENU_CHILD_INACTIVE",
306
+ ];
307
+
308
+ /** Extract the twelve real class-token arrays from nav-class-tokens.ts. */
309
+ function extractNavClassTokens(context, transformSync) {
310
+ const outputText = transpile(context.navClassTokensSource, transformSync);
311
+ const exportsObj = executeCommonJs(outputText, "nav-class-tokens.ts");
312
+ const tokens = {};
313
+ for (const name of NAV_CLASS_TOKEN_NAMES) {
314
+ const value = exportsObj[name];
315
+ if (!Array.isArray(value) || !value.every((token) => typeof token === "string")) {
316
+ throw new Error(
317
+ `[gen-nav-overflow-script] nav-class-tokens.ts did not export ${name} as a string array`,
318
+ );
319
+ }
320
+ tokens[name] = value;
321
+ }
322
+ // Exhaustiveness: a token array exported by nav-class-tokens.ts (and thus
323
+ // available to header.tsx's SSR markup) but missing from
324
+ // NAV_CLASS_TOKEN_NAMES would be silently absent from the frozen script —
325
+ // the exact SSR ↔ runtime class drift the tokens module exists to prevent,
326
+ // and one no drift guard can see (the generated bytes legitimately don't
327
+ // change). Fail loudly instead.
328
+ const unconsumed = Object.keys(exportsObj).filter(
329
+ (name) => name.startsWith("NAV_") && !NAV_CLASS_TOKEN_NAMES.includes(name),
330
+ );
331
+ if (unconsumed.length > 0) {
332
+ throw new Error(
333
+ `[gen-nav-overflow-script] nav-class-tokens.ts exports token array(s) not embedded in the script: ${unconsumed.join(", ")}. Add them to NAV_CLASS_TOKEN_NAMES and splice them into the template in buildNavOverflowScript().`,
334
+ );
335
+ }
336
+ return tokens;
337
+ }
338
+
339
+ /** Extract the real AFTER_NAVIGATE_EVENT value from transitions/page-events.ts — never hardcoded. */
340
+ function extractAfterNavigateEvent(context, transformSync) {
341
+ const outputText = transpile(context.pageEventsSource, transformSync);
342
+ const exportsObj = executeCommonJs(outputText, "page-events.ts");
343
+ const value = exportsObj.AFTER_NAVIGATE_EVENT;
344
+ if (typeof value !== "string" || !value) {
345
+ throw new Error(
346
+ "[gen-nav-overflow-script] AFTER_NAVIGATE_EVENT missing or not a string in transitions/page-events.ts",
347
+ );
348
+ }
349
+ return value;
350
+ }
351
+
352
+ // The class lists spliced into the script below are the SSR ↔ runtime
353
+ // lockstep: they must match the strings header.tsx renders (nav-class-tokens.ts
354
+ // header comment, zudolab/zudo-doc#3023). Moved here from nav-overflow-script.ts
355
+ // (zudolab/zudo-doc#3534) — they now run once at generation time rather than on
356
+ // every module evaluation.
357
+
358
+ // -> `"bg-fg", "text-bg"` — argument list for a classList.add/remove(...) call.
359
+ const clsArgs = (tokens) => tokens.map((token) => JSON.stringify(token)).join(", ");
360
+
361
+ // -> `"bg-fg text-bg"` — a single class-string literal for `className = ...`.
362
+ const clsLiteral = (tokens) => JSON.stringify(tokens.join(" "));
363
+
364
+ // -> `" font-bold text-accent"` — leading-space append for `className += ...`.
365
+ const clsAppend = (tokens) => JSON.stringify(" " + tokens.join(" "));
366
+
367
+ /**
368
+ * Composes the full desktop-nav overflow controller IIFE script,
369
+ * string-for-string identical in structure to the old template-literal build
370
+ * in nav-overflow-script.ts, but with the four previously-live interpolations
371
+ * replaced by frozen values extracted above.
372
+ */
373
+ export function buildNavOverflowScript(context = resolveGenerationContext()) {
374
+ const transformSync = loadTransformSync(context.packageRoot);
375
+ const currentPathPrelude = extractCurrentPathPrelude(context, transformSync);
376
+ const { pathMatchesNavPathSrc, computeActiveNavPathSrc } = extractNavActiveFunctions(
377
+ context,
378
+ transformSync,
379
+ );
380
+ const {
381
+ NAV_TOP_ACTIVE,
382
+ NAV_TOP_INACTIVE,
383
+ NAV_CHEVRON_ACTIVE,
384
+ NAV_CHEVRON_INACTIVE,
385
+ NAV_CHILD_ACTIVE,
386
+ NAV_CHILD_INACTIVE,
387
+ NAV_MENU_PARENT,
388
+ NAV_MENU_PARENT_ACTIVE_SUFFIX,
389
+ NAV_MENU_PLAIN,
390
+ NAV_MENU_PLAIN_ACTIVE_SUFFIX,
391
+ NAV_MENU_CHILD_ACTIVE,
392
+ NAV_MENU_CHILD_INACTIVE,
393
+ } = extractNavClassTokens(context, transformSync);
394
+ const afterNavigateEventLiteral = JSON.stringify(
395
+ extractAfterNavigateEvent(context, transformSync),
396
+ );
397
+
398
+ return /* javascript */ `(function () {
399
+ var cleanupNavOverflow = null;
400
+
401
+ function trimSlashes(p) {
402
+ while (p.length > 1 && p.charAt(p.length - 1) === "/") p = p.slice(0, -1);
403
+ return p || "/";
404
+ }
405
+
406
+ function navPathname(a) {
407
+ try { return trimSlashes(new URL(a.href, location.href).pathname); }
408
+ catch (e) { return ""; }
409
+ }
410
+
411
+ // Explicit current-route override, embedded from current-path/index.ts so
412
+ // this script cannot drift from the three other read sites
413
+ // (zudolab/zudo-doc#3398, #3408).
414
+ ${currentPathPrelude}
415
+
416
+ // Shared matching core (zudolab/zudo-doc#3398): embedded verbatim from
417
+ // nav-active.ts so this script's longest-match walk cannot drift from the
418
+ // SSR header's own computeActiveNavPath call (header.tsx). computeActiveNavPath
419
+ // closes over pathMatchesNavPath, so both are embedded together.
420
+ var pathMatchesNavPath = ${pathMatchesNavPathSrc};
421
+ var computeActiveNavPath = ${computeActiveNavPathSrc};
422
+
423
+ // Recompute which header nav item is "active" from the CURRENT URL and
424
+ // repaint the highlight. SSR sets the active item on first paint, but the
425
+ // header is persisted across same-locale client-router swaps
426
+ // (data-zfb-transition-persist), so without this the highlight would stay
427
+ // frozen on the page where the header was first rendered. Mirrors the
428
+ // sidebar island's client-side approach (match the current path against
429
+ // each entry's href) and the SSR longest-match + dropdown-parent rules.
430
+ // URL-based: hrefs and the current path both carry the base + locale
431
+ // prefix, so they compare directly without stripping.
432
+ function applyActiveNav() {
433
+ var nav = document.querySelector("[data-header-nav]");
434
+ if (!nav) return;
435
+ var topItems = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
436
+ if (topItems.length === 0) return;
437
+
438
+ var cur = trimSlashes(readCurrentPath(CURRENT_PATH_DATASET_KEY));
439
+
440
+ // Build NavItemLike-shaped entries from the live DOM so the shared
441
+ // computeActiveNavPath can do the deepest-match walk — the same call
442
+ // shape the SSR header uses (matches computeActiveNavPath). A dropdown
443
+ // missing its own top-level anchor is skipped entirely (path "" would
444
+ // otherwise match every current path — pathMatchesNavPath treats "" as
445
+ // the root "/"), mirroring the parentLink guard used below for the same
446
+ // malformed-markup case.
447
+ var navItems = [];
448
+ topItems.forEach(function (it) {
449
+ var isDropdown = it.hasAttribute("data-nav-item-dropdown");
450
+ var topA = isDropdown ? it.querySelector(":scope > a") : it;
451
+ if (!topA) return;
452
+ var children = [];
453
+ if (isDropdown) {
454
+ it.querySelectorAll(":scope > div a").forEach(function (c) {
455
+ children.push({ path: navPathname(c) });
456
+ });
457
+ }
458
+ navItems.push({ path: navPathname(topA), children: children });
459
+ });
460
+
461
+ var activePath = computeActiveNavPath(navItems, cur) || "";
462
+
463
+ function setTopActive(a, active) {
464
+ if (!a) return;
465
+ if (active) {
466
+ a.classList.add(${clsArgs(NAV_TOP_ACTIVE)});
467
+ a.classList.remove(${clsArgs(NAV_TOP_INACTIVE)});
468
+ a.setAttribute("aria-current", "page");
469
+ } else {
470
+ a.classList.remove(${clsArgs(NAV_TOP_ACTIVE)});
471
+ a.classList.add(${clsArgs(NAV_TOP_INACTIVE)});
472
+ a.removeAttribute("aria-current");
473
+ }
474
+ }
475
+
476
+ topItems.forEach(function (it) {
477
+ var isDropdown = it.hasAttribute("data-nav-item-dropdown");
478
+ var topA = isDropdown ? it.querySelector(":scope > a") : it;
479
+ var topActive = false;
480
+
481
+ if (isDropdown) {
482
+ var parentMatch = !!topA && navPathname(topA) === activePath && activePath !== "";
483
+ var anyChild = false;
484
+ it.querySelectorAll(":scope > div a").forEach(function (c) {
485
+ var childActive = navPathname(c) === activePath && activePath !== "";
486
+ if (childActive) {
487
+ anyChild = true;
488
+ c.setAttribute("data-active", "");
489
+ c.classList.add(${clsArgs(NAV_CHILD_ACTIVE)});
490
+ c.classList.remove(${clsArgs(NAV_CHILD_INACTIVE)});
491
+ } else {
492
+ c.removeAttribute("data-active");
493
+ c.classList.remove(${clsArgs(NAV_CHILD_ACTIVE)});
494
+ c.classList.add(${clsArgs(NAV_CHILD_INACTIVE)});
495
+ }
496
+ });
497
+ topActive = parentMatch || anyChild;
498
+ var svg = topA ? topA.querySelector("svg") : null;
499
+ if (svg) {
500
+ if (topActive) { svg.classList.add(${clsArgs(NAV_CHEVRON_ACTIVE)}); svg.classList.remove(${clsArgs(NAV_CHEVRON_INACTIVE)}); }
501
+ else { svg.classList.add(${clsArgs(NAV_CHEVRON_INACTIVE)}); svg.classList.remove(${clsArgs(NAV_CHEVRON_ACTIVE)}); }
502
+ }
503
+ } else {
504
+ topActive = activePath !== "" && navPathname(topA) === activePath;
505
+ }
506
+
507
+ setTopActive(topA, topActive);
508
+ });
509
+ }
510
+
511
+ function initNavOverflow() {
512
+ if (cleanupNavOverflow) cleanupNavOverflow();
513
+
514
+ // Repaint the active highlight for the current URL before measuring /
515
+ // cloning, so the overflow "···" menu mirrors the correct active state.
516
+ applyActiveNav();
517
+
518
+ var nav = document.querySelector("[data-header-nav]");
519
+ var moreContainer = document.querySelector("[data-nav-more]");
520
+ var moreMenu = document.querySelector("[data-nav-more-menu]");
521
+ var moreToggle = document.querySelector("[data-nav-more-toggle]");
522
+ if (!nav || !moreContainer || !moreMenu || !moreToggle) return;
523
+
524
+ var items = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
525
+ if (items.length === 0) return;
526
+
527
+ var controller = new AbortController();
528
+
529
+ function update() {
530
+ items.forEach(function (el) { el.style.display = ""; });
531
+ moreContainer.style.display = "";
532
+ moreMenu.innerHTML = "";
533
+ moreMenu.classList.add("hidden");
534
+ moreToggle.setAttribute("aria-expanded", "false");
535
+
536
+ var itemWidths = items.map(function (el) { return el.offsetWidth; });
537
+ var moreWidth = moreContainer.offsetWidth;
538
+ var navGap = parseFloat(getComputedStyle(nav).columnGap) || 0;
539
+ var available = nav.clientWidth;
540
+
541
+ if (available <= 0) {
542
+ moreContainer.style.display = "none";
543
+ return;
544
+ }
545
+
546
+ var total = 0;
547
+ for (var i = 0; i < itemWidths.length; i++) {
548
+ total += itemWidths[i] + (i > 0 ? navGap : 0);
549
+ }
550
+
551
+ if (total <= available) {
552
+ moreContainer.style.display = "none";
553
+ return;
554
+ }
555
+
556
+ var used = 0;
557
+ var cutoffIndex = 0;
558
+
559
+ for (var i2 = 0; i2 < items.length; i2++) {
560
+ var w = itemWidths[i2] + (i2 > 0 ? navGap : 0);
561
+ if (used + w > available - moreWidth - navGap) break;
562
+ used += w;
563
+ cutoffIndex = i2 + 1;
564
+ }
565
+
566
+ for (var i3 = cutoffIndex; i3 < items.length; i3++) {
567
+ items[i3].style.display = "none";
568
+ }
569
+
570
+ for (var i4 = cutoffIndex; i4 < items.length; i4++) {
571
+ var el = items[i4];
572
+ var isDropdown = el.hasAttribute("data-nav-item-dropdown");
573
+
574
+ if (isDropdown) {
575
+ var parentLink = el.querySelector(":scope > a");
576
+ var childLinks = el.querySelectorAll(":scope > div a");
577
+ if (parentLink) {
578
+ var li = document.createElement("li");
579
+ var a = document.createElement("a");
580
+ a.href = parentLink.href;
581
+ var parentText = parentLink.textContent ? parentLink.textContent.trim().replace(/\\s+/g, " ") : "";
582
+ a.textContent = parentText;
583
+ a.className = ${clsLiteral(NAV_MENU_PARENT)};
584
+ if (parentLink.getAttribute("aria-current") === "page") {
585
+ a.className += ${clsAppend(NAV_MENU_PARENT_ACTIVE_SUFFIX)};
586
+ }
587
+ li.appendChild(a);
588
+ moreMenu.appendChild(li);
589
+ }
590
+ childLinks.forEach(function (child) {
591
+ var li = document.createElement("li");
592
+ var a = document.createElement("a");
593
+ a.href = child.href;
594
+ a.textContent = child.textContent ? child.textContent.trim() : "";
595
+ var isChildActive = child.hasAttribute("data-active");
596
+ a.className = isChildActive
597
+ ? ${clsLiteral(NAV_MENU_CHILD_ACTIVE)}
598
+ : ${clsLiteral(NAV_MENU_CHILD_INACTIVE)};
599
+ li.appendChild(a);
600
+ moreMenu.appendChild(li);
601
+ });
602
+ } else {
603
+ var anchor = el;
604
+ var li2 = document.createElement("li");
605
+ var a2 = document.createElement("a");
606
+ a2.href = anchor.href;
607
+ a2.textContent = anchor.textContent ? anchor.textContent.trim() : "";
608
+ a2.className = ${clsLiteral(NAV_MENU_PLAIN)};
609
+ if (anchor.getAttribute("aria-current") === "page") {
610
+ a2.className += ${clsAppend(NAV_MENU_PLAIN_ACTIVE_SUFFIX)};
611
+ }
612
+ li2.appendChild(a2);
613
+ moreMenu.appendChild(li2);
614
+ }
615
+ }
616
+ }
617
+
618
+ moreToggle.addEventListener("click", function () {
619
+ var isOpen = !moreMenu.classList.contains("hidden");
620
+ moreMenu.classList.toggle("hidden", isOpen);
621
+ moreToggle.setAttribute("aria-expanded", String(!isOpen));
622
+ }, { signal: controller.signal });
623
+
624
+ document.addEventListener("click", function (e) {
625
+ if (!moreContainer.contains(e.target)) {
626
+ moreMenu.classList.add("hidden");
627
+ moreToggle.setAttribute("aria-expanded", "false");
628
+ }
629
+ }, { signal: controller.signal });
630
+
631
+ document.addEventListener("keydown", function (e) {
632
+ if (e.key !== "Escape") return;
633
+ if (!moreMenu.classList.contains("hidden")) {
634
+ moreMenu.classList.add("hidden");
635
+ moreToggle.setAttribute("aria-expanded", "false");
636
+ moreToggle.focus();
637
+ return;
638
+ }
639
+ var active = document.activeElement;
640
+ var dropdown = active && active.closest ? active.closest("[data-nav-item-dropdown]") : null;
641
+ if (dropdown && active && active.blur) {
642
+ active.blur();
643
+ }
644
+ }, { signal: controller.signal });
645
+
646
+ var dropdowns = nav.querySelectorAll("[data-nav-item-dropdown]");
647
+ dropdowns.forEach(function (dd) {
648
+ var trigger = dd.querySelector(":scope > a");
649
+ if (!trigger) return;
650
+ function setExpanded(v) {
651
+ trigger.setAttribute("aria-expanded", String(v));
652
+ }
653
+ dd.addEventListener("mouseenter", function () { setExpanded(true); }, { signal: controller.signal });
654
+ dd.addEventListener("mouseleave", function () { setExpanded(false); }, { signal: controller.signal });
655
+ dd.addEventListener("focusin", function () { setExpanded(true); }, { signal: controller.signal });
656
+ dd.addEventListener("focusout", function (e) {
657
+ if (!dd.contains(e.relatedTarget)) {
658
+ setExpanded(false);
659
+ }
660
+ }, { signal: controller.signal });
661
+ });
662
+
663
+ var ro = new ResizeObserver(update);
664
+ ro.observe(nav);
665
+ controller.signal.addEventListener("abort", function () { ro.disconnect(); });
666
+
667
+ document.fonts.ready.then(update);
668
+
669
+ update();
670
+
671
+ cleanupNavOverflow = function () { controller.abort(); };
672
+ }
673
+
674
+ initNavOverflow();
675
+ document.addEventListener(${afterNavigateEventLiteral}, initNavOverflow);
676
+ })();`;
677
+ }
678
+
679
+ // ── CLI entry: write-if-changed ─────────────────────────────────────────────
680
+ // Realpath both sides: Node resolves the ESM entry's `import.meta.url` to its
681
+ // REAL path (default --preserve-symlinks=false) while `process.argv[1]` keeps
682
+ // whatever spelling the invoker typed, so under a symlinked checkout a plain
683
+ // `resolve()` comparison silently mismatches and the CLI becomes an exit-0
684
+ // no-op — leaving nav-overflow-generated-script.ts missing (hard tsup failure
685
+ // later) or, worse, stale.
686
+ const isMainModule = (() => {
687
+ if (!process.argv[1]) return false;
688
+ try {
689
+ // Realpath BOTH sides: under `--preserve-symlinks-main` (sometimes set
690
+ // via NODE_OPTIONS in pnpm/monorepo setups) `import.meta.url` keeps the
691
+ // symlinked spelling, so a one-sided realpath re-creates the silent
692
+ // exit-0 no-op this block exists to prevent.
693
+ return (
694
+ realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url))
695
+ );
696
+ } catch {
697
+ return false;
698
+ }
699
+ })();
700
+
701
+ function buildGeneratedModule(script, context) {
702
+ const banner = context.kind === "package"
703
+ ? `// GENERATED FILE — do not edit by hand.
704
+ // Produced by scripts/gen-nav-overflow-script.mjs (zudolab/zudo-doc#3534,
705
+ // epic #3533) from src/current-path/index.ts (CURRENT_PATH_SCRIPT_PRELUDE),
706
+ // src/header/nav-active.ts (pathMatchesNavPath/computeActiveNavPath,
707
+ // type-stripped), src/header/nav-class-tokens.ts (the twelve class-token
708
+ // arrays), and src/transitions/page-events.ts (AFTER_NAVIGATE_EVENT).
709
+ // Re-run \`pnpm --filter @takazudo/zudo-doc gen:nav-overflow-script\` (or any
710
+ // build/dev entry point, which already runs it) to regenerate after editing
711
+ // any of those source files.
712
+ //
713
+ // This file is committed to git (mirrors search-widget-script/generated-script.ts,
714
+ // zudolab/zudo-doc#3421 / #3431) — a deliberate departure from this repo's
715
+ // usual gitignored-generated-file convention (routes-src/, virtual-modules.d.ts).
716
+ // Regenerate AND commit the result after editing any of the four source files.
717
+ `
718
+ : `// GENERATED FILE — do not edit by hand.
719
+ // Produced by the ejected header's gen-nav-overflow-script.mjs from the local
720
+ // nav-active.ts and nav-class-tokens.ts customization inputs plus the installed
721
+ // @takazudo/zudo-doc current-path and page-event inputs.
722
+ // Re-run \`node ./src/components/zudo-doc/header/gen-nav-overflow-script.mjs\`
723
+ // after editing either local input, then commit this file with your customization.
724
+ // The script value remains frozen so its CSP bytes do not depend on the
725
+ // consumer bundler.
726
+ `;
727
+
728
+ return `${banner}
729
+ /** Returns the frozen desktop-nav overflow controller IIFE script. NOTE: the
730
+ * vitest drift guard imports buildNavOverflowScript from
731
+ * scripts/gen-nav-overflow-script.mjs (a fresh re-generation) — NEVER from
732
+ * this module: comparing NAV_OVERFLOW_SCRIPT below against this same file's
733
+ * function would be a vacuous self-comparison. */
734
+ export function buildNavOverflowScript(): string {
735
+ return ${JSON.stringify(script)};
736
+ }
737
+
738
+ /** Client-side script string for the desktop header nav overflow controller.
739
+ * See the module header of this generator for the embedding contract; see
740
+ * current-path/index.ts / header/nav-active.ts / header/nav-class-tokens.ts /
741
+ * transitions/page-events.ts for the frozen sources. */
742
+ export const NAV_OVERFLOW_SCRIPT: string = buildNavOverflowScript();
743
+ `;
744
+ }
745
+
746
+ /** Regenerate the committed package literal or the local ejected literal. */
747
+ export function generateNavOverflowScript(context = resolveGenerationContext()) {
748
+ const script = buildNavOverflowScript(context);
749
+ const output = buildGeneratedModule(script, context);
750
+
751
+ const existing = existsSync(context.outputPath)
752
+ ? readFileSync(context.outputPath, "utf8")
753
+ : null;
754
+
755
+ if (existing === output) {
756
+ process.stdout.write(
757
+ `[gen-nav-overflow-script] ${context.outputPath} unchanged, skip write\n`,
758
+ );
759
+ } else {
760
+ writeFileSync(context.outputPath, output, "utf8");
761
+ process.stdout.write(
762
+ `[gen-nav-overflow-script] ${context.outputPath} written\n`,
763
+ );
764
+ }
765
+
766
+ return { output, outputPath: context.outputPath, changed: existing !== output };
767
+ }
768
+
769
+ if (isMainModule) {
770
+ generateNavOverflowScript();
771
+ }