create-zudo-doc 0.2.6 → 0.2.8

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 (25) hide show
  1. package/dist/features/claude-resources.js +4 -1
  2. package/dist/scaffold.js +25 -10
  3. package/dist/settings-gen.js +7 -2
  4. package/package.json +1 -1
  5. package/templates/base/pages/lib/_search-widget-script.ts +45 -1
  6. package/templates/base/pages/lib/_search-widget.tsx +9 -1
  7. package/templates/base/pages/sitemap.xml.tsx +8 -0
  8. package/templates/base/plugins/connect-adapter.mjs +6 -10
  9. package/templates/base/plugins/search-index-plugin.mjs +2 -2
  10. package/templates/base/scripts/gen-z-index.mjs +157 -0
  11. package/templates/base/src/components/sidebar-toggle.tsx +8 -3
  12. package/templates/base/src/components/sidebar-tree.tsx +2 -2
  13. package/templates/base/src/components/site-tree-nav.tsx +1 -1
  14. package/templates/base/src/config/color-schemes.ts +5 -3
  15. package/templates/base/src/config/z-index-tokens.ts +127 -0
  16. package/templates/base/src/styles/global.css +38 -7
  17. package/templates/base/zfb-shim.d.ts +12 -1
  18. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +32 -4
  19. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -1
  20. package/templates/features/i18n/files/pages/[locale]/index.tsx +10 -4
  21. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +10 -1
  22. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +9 -2
  23. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
  24. package/templates/features/tauri/files/src/components/find-bar.tsx +1 -1
  25. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +1 -1
@@ -1,6 +1,9 @@
1
1
  export const claudeResourcesFeature = () => ({
2
2
  name: "claudeResources",
3
3
  injections: [
4
- // Plugin entry is handled by zfb-config-gen.ts no shared file injection needed
4
+ // No shared file injection neededthis feature's touch points are:
5
+ // - plugin entry: zfb-config-gen.ts (claudeResources conditional import/plugin)
6
+ // - settings: settings-gen.ts (claudeResources object + defaultLocaleOnlyPrefixes array)
7
+ // - devDep: scaffold.ts (tsx devDep — same subprocess runner as docHistory)
5
8
  ],
6
9
  });
package/dist/scaffold.js CHANGED
@@ -285,20 +285,26 @@ function generatePackageJson(choices) {
285
285
  // include/exclude globs (#1032). No consumer-facing breaking change.
286
286
  // next.42/next.43: release-tooling + formatter-glob fixes only (npm-publish
287
287
  // idempotency, gitignored-artifact excludes). No consumer-facing change.
288
- // next.44 (current pin): embed-as-library enhancements only — ServerBuilder
289
- // ::with_page_cache for live content, an opt-in ExternalInvalidationHook to
290
- // narrow extraWatchPaths rebuilds, and TS-config-loader path canonicalization
291
- // (Takazudo/zudo-front-builder#1036–#1043). No consumer-facing / CLI change.
292
- "@takazudo/zfb": "0.1.0-next.44",
293
- "@takazudo/zfb-runtime": "0.1.0-next.44",
288
+ // next.44: embed-as-library enhancements only — ServerBuilder::with_page_cache
289
+ // for live content, an opt-in ExternalInvalidationHook to narrow
290
+ // extraWatchPaths rebuilds, and TS-config-loader path canonicalization
291
+ // (Takazudo/zudo-front-builder#1036–#1043). next.45: docs-only. next.46:
292
+ // opt-in dev boot-lazy mode (#1057) + client-router timer lifecycle fixes —
293
+ // dev-server-only. next.47 (current pin): dual light/dark syntect themes
294
+ // (themeLight/themeDark on CodeHighlightConfig, --shiki-light/--shiki-dark,
295
+ // #1067) plus stricter build-start validation that rejects unknown theme
296
+ // names — additive; a fresh scaffold sets no explicit codeHighlight.theme so
297
+ // the default still applies. No consumer-facing / CLI breaking change.
298
+ "@takazudo/zfb": "0.1.0-next.47",
299
+ "@takazudo/zfb-runtime": "0.1.0-next.47",
294
300
  // zfb-adapter-cloudflare — required for any route with `prerender = false`.
295
301
  // Pinned in lockstep with @takazudo/zfb.
296
- "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.44",
302
+ "@takazudo/zfb-adapter-cloudflare": "0.1.0-next.47",
297
303
  // @takazudo/zudo-doc — published from this monorepo via
298
304
  // .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
299
305
  // lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
300
306
  // version moves, so a fresh scaffold pulls the version we just published.
301
- "@takazudo/zudo-doc": "^0.2.6",
307
+ "@takazudo/zudo-doc": "^0.2.8",
302
308
  // zod — used by the generated zfb.config.ts. zfb-config-gen emits
303
309
  // `import { z } from "zod"` for the content-collection schema +
304
310
  // `z.toJSONSchema(...)` conversion. Without this dep, the consumer
@@ -310,6 +316,7 @@ function generatePackageJson(choices) {
310
316
  // ^10.29.1 floor satisfies @takazudo/zdtp's preact peer range so the app
311
317
  // and zdtp resolve a single preact instance — a lower floor can split into
312
318
  // two copies and crash hook-using SSR islands with "undefined reading __H".
319
+ // See the designTokenPanel dep block below (~line 443) for the coupling.
313
320
  preact: "^10.29.1",
314
321
  // preact-render-to-string — zfb's emitted entry.mjs imports
315
322
  // `renderToString` from this package as `__zfb_renderToString` to
@@ -352,7 +359,7 @@ function generatePackageJson(choices) {
352
359
  // @takazudo/zudo-doc/integrations/doc-history which in turn imports
353
360
  // @takazudo/zudo-doc-history-server/git-history. Without this dep the
354
361
  // plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
355
- deps["@takazudo/zudo-doc-history-server"] = "^0.2.6";
362
+ deps["@takazudo/zudo-doc-history-server"] = "^0.2.8";
356
363
  // W7A (#1736): doc-history-plugin.mjs spawns `tsx -e <inline-script>` to
357
364
  // run the v2 runtime in a TS-aware Node subprocess; without tsx the
358
365
  // plugin's preBuild step exits with ENOENT before zfb finishes config
@@ -366,7 +373,9 @@ function generatePackageJson(choices) {
366
373
  devDeps["tsx"] = "^4.21.0";
367
374
  }
368
375
  if (choices.features.includes("designTokenPanel")) {
369
- deps["@takazudo/zdtp"] = "0.2.0-next.2";
376
+ // @takazudo/zdtp requires preact >= 10.29.1 — see the preact floor comment
377
+ // above (~line 382) for why the floor is set there and the coupling this creates.
378
+ deps["@takazudo/zdtp"] = "0.2.3";
370
379
  }
371
380
  if (choices.features.includes("tagGovernance")) {
372
381
  // gray-matter is already in `deps` unconditionally (base template uses it),
@@ -390,6 +399,12 @@ function generatePackageJson(choices) {
390
399
  // the pages/lib call sites, so emitting the script would fail on a fresh
391
400
  // scaffold. Revisit once the template stubs carry typed props.
392
401
  "check:html": "html-validate \"dist/**/*.html\"",
402
+ // Z-index token codegen (#2148): regenerate the GENERATED:Z_INDEX @theme
403
+ // block in src/styles/global.css from src/config/z-index-tokens.ts, and a
404
+ // drift check for pre-push/CI. Both ship in base — the z-index token system
405
+ // is part of every scaffold.
406
+ "gen:z-index": "node scripts/gen-z-index.mjs",
407
+ "check:z-index": "node scripts/gen-z-index.mjs --check",
393
408
  };
394
409
  if (choices.features.includes("tagGovernance")) {
395
410
  scripts["tags:audit"] = "tsx scripts/tags-audit.ts";
@@ -8,10 +8,10 @@ export function generateSettingsFile(choices) {
8
8
  lines.push(` HeaderRightItem,`);
9
9
  lines.push(` ColorModeConfig,`);
10
10
  lines.push(` HtmlPreviewConfig,`);
11
- lines.push(` FrontmatterPreviewConfig,`);
12
11
  lines.push(` LocaleConfig,`);
13
12
  lines.push(` VersionConfig,`);
14
13
  lines.push(` FooterConfig,`);
14
+ lines.push(` FrontmatterPreviewConfig,`);
15
15
  lines.push(` BodyFootUtilAreaConfig,`);
16
16
  lines.push(` TagPlacement,`);
17
17
  lines.push(` TagGovernanceMode,`);
@@ -23,10 +23,10 @@ export function generateSettingsFile(choices) {
23
23
  lines.push(` HeaderRightItem,`);
24
24
  lines.push(` ColorModeConfig,`);
25
25
  lines.push(` HtmlPreviewConfig,`);
26
- lines.push(` FrontmatterPreviewConfig,`);
27
26
  lines.push(` LocaleConfig,`);
28
27
  lines.push(` VersionConfig,`);
29
28
  lines.push(` FooterConfig,`);
29
+ lines.push(` FrontmatterPreviewConfig,`);
30
30
  lines.push(` BodyFootUtilAreaConfig,`);
31
31
  lines.push(` TagPlacement,`);
32
32
  lines.push(` TagGovernanceMode,`);
@@ -121,6 +121,8 @@ export function generateSettingsFile(choices) {
121
121
  lines.push(` tagGovernance: "off" as TagGovernanceMode,`);
122
122
  lines.push(` tagVocabulary: false as boolean,`);
123
123
  }
124
+ // Default false — fresh scaffolds typically don't need live frontmatter preview;
125
+ // users opt in once they're ready to wire up the preview panel.
124
126
  lines.push(` frontmatterPreview: false as FrontmatterPreviewConfig | false,`);
125
127
  if (choices.features.includes("llmsTxt")) {
126
128
  lines.push(` llmsTxt: true,`);
@@ -279,6 +281,9 @@ export function generateSettingsFile(choices) {
279
281
  }
280
282
  lines.push(` { type: "component", component: "github-link" },`);
281
283
  lines.push(` { type: "component", component: "theme-toggle" },`);
284
+ if (choices.features.includes("search")) {
285
+ lines.push(` { type: "component", component: "search" },`);
286
+ }
282
287
  if (choices.features.includes("i18n")) {
283
288
  lines.push(` { type: "component", component: "language-switcher" },`);
284
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-zudo-doc",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Create a new zudo-doc documentation site",
5
5
  "license": "MIT",
6
6
  "author": "Takeshi Takatsudo",
@@ -24,6 +24,19 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
24
24
 
25
25
  var PAGE_SIZE = 10;
26
26
 
27
+ // Allowlist-based href sanitizer: only relative paths and http(s) URLs are
28
+ // permitted. Anything else (e.g. javascript:, data:) falls back to "#" so a
29
+ // malicious entry in search-index.json cannot turn a result link into a
30
+ // script-injection vector.
31
+ function safeHref(url) {
32
+ if (!url) return "#";
33
+ var s = String(url);
34
+ if (s.startsWith("/") || s.startsWith("http://") || s.startsWith("https://")) {
35
+ return s;
36
+ }
37
+ return "#";
38
+ }
39
+
27
40
  function escapeHtml(text) {
28
41
  return String(text)
29
42
  .replace(/&/g, "&amp;")
@@ -122,6 +135,10 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
122
135
  this._shortcut = "";
123
136
  this._resultCountTemplate = "";
124
137
  this._keydownHandler = null;
138
+ // Delegated click handler on the results container: closing the dialog
139
+ // when a result link is activated (epic #2148). Held so disconnectedCallback
140
+ // can detach it on body swap.
141
+ this._resultsClickHandler = null;
125
142
  this._observer = null;
126
143
  this._sentinel = null;
127
144
  this._isLoadingBatch = false;
@@ -178,6 +195,24 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
178
195
  this._input.addEventListener("input", function() { self.handleInput(); });
179
196
  }
180
197
 
198
+ // Close-on-result-click (epic #2148): result links are created dynamically
199
+ // in renderResult(), so use one delegated listener on the results container
200
+ // instead of per-link handlers. We do NOT preventDefault — the link's own
201
+ // navigation (zfb Strategy-B SPA swap or a plain load) must still proceed;
202
+ // we only close the <dialog> so it does not linger over the swapped page.
203
+ // closeDialog() runs synchronously before navigation; the dialog's close
204
+ // restores documentElement overflow via the existing "close" listener.
205
+ if (this._results) {
206
+ this._resultsClickHandler = function(e) {
207
+ var t = e.target;
208
+ while (t && t !== self._results) {
209
+ if (t.tagName === "A") { self.closeDialog(); return; }
210
+ t = t.parentNode;
211
+ }
212
+ };
213
+ this._results.addEventListener("click", this._resultsClickHandler);
214
+ }
215
+
181
216
  // Global keyboard shortcut (⌘K / Ctrl+K to open)
182
217
  this._keydownHandler = function(e) {
183
218
  if ((e.metaKey || e.ctrlKey) && e.key === "k") {
@@ -192,6 +227,11 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
192
227
  // body swap when this element is NOT persisted via
193
228
  // data-zfb-transition-persist (zudolab/zudo-doc#1523).
194
229
  this._afterNavHandler = function() {
230
+ // Backstop for the original bug (epic #2148): if the dialog is somehow
231
+ // still open after an SPA body swap (e.g. a nav path that bypassed the
232
+ // result-click handler), close it so it does not linger / flash over the
233
+ // newly-swapped page. Safe no-op when already closed.
234
+ if (self._dialog && self._dialog.open) self.closeDialog();
195
235
  var kbdEl2 = self.querySelector("[data-kbd-shortcut]");
196
236
  if (kbdEl2) kbdEl2.textContent = self._shortcut;
197
237
  };
@@ -207,6 +247,10 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
207
247
  document.removeEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
208
248
  this._afterNavHandler = null;
209
249
  }
250
+ if (this._resultsClickHandler && this._results) {
251
+ this._results.removeEventListener("click", this._resultsClickHandler);
252
+ this._resultsClickHandler = null;
253
+ }
210
254
  this.teardownSentinel();
211
255
  }
212
256
 
@@ -401,7 +445,7 @@ export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
401
445
  var article = document.createElement("article");
402
446
  article.className = "-mx-hsp-lg border-b border-muted";
403
447
  var link = document.createElement("a");
404
- link.href = entry.url || "#";
448
+ link.href = safeHref(entry.url);
405
449
  link.className =
406
450
  "group block px-hsp-lg py-vsp-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent";
407
451
  var title = document.createElement("span");
@@ -88,9 +88,17 @@ export function SearchWidget(props: SearchWidgetProps): JSX.Element {
88
88
  static HTML for no-JS users and crawlers. The browser treats
89
89
  it as a closed <dialog> until the custom element calls
90
90
  showModal(). */}
91
+ {/* z-modal / backdrop:z-modal-backdrop are defense-in-depth for the
92
+ SPA-swap window (zfb Strategy-B `zfb:after-swap`): a native
93
+ showModal() dialog normally sits in the top layer, but if it is
94
+ still open while the page body is swapped it can lose top-layer
95
+ promotion and fall back to z-index:auto, flashing behind the
96
+ header/sidebar. The explicit modal-tier z-index keeps it above all
97
+ chrome during that window. Intentionally redundant in the normal
98
+ (top-layer) case — do not remove as "redundant" (epic #2148). */}
91
99
  <dialog
92
100
  data-search-dialog
93
- class="m-0 h-full w-full max-w-none border-none bg-transparent p-0 backdrop:bg-overlay/60 sm:mx-auto sm:my-[10vh] sm:h-auto sm:max-h-[80vh] sm:max-w-[52rem] sm:rounded-lg"
101
+ class="z-modal m-0 h-full w-full max-w-none border-none bg-transparent p-0 backdrop:z-modal-backdrop backdrop:bg-overlay/60 sm:mx-auto sm:my-[10vh] sm:h-auto sm:max-h-[80vh] sm:max-w-[52rem] sm:rounded-lg"
94
102
  >
95
103
  <div class="flex h-full flex-col overflow-hidden bg-surface sm:rounded-lg sm:border sm:border-muted">
96
104
  {/* ── Dialog header (input row) ─────────────────────────── */}
@@ -30,6 +30,14 @@ function escapeXml(str: string): string {
30
30
  }
31
31
 
32
32
  export default function Sitemap(): string {
33
+ // When sitemap is disabled, return an empty urlset so the route still
34
+ // resolves (returns XML, not 404) but contains no URLs.
35
+ if (!settings.sitemap) {
36
+ return `<?xml version="1.0" encoding="UTF-8"?>
37
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
38
+ </urlset>`;
39
+ }
40
+
33
41
  const routeMap = enumerateAllRoutes();
34
42
  const siteUrlBase = (settings.siteUrl ?? "").replace(/\/$/, "");
35
43
 
@@ -123,19 +123,15 @@ export function connectToZfbHandler(middleware) {
123
123
  * @param {string | number | readonly string[]} value
124
124
  */
125
125
  setHeader(name, value) {
126
- headers[name] = String(value);
126
+ // Store under lowercased key to avoid duplicate-case collisions
127
+ // when finish() merges headers — last-wins is then unambiguous.
128
+ headers[name.toLowerCase()] = String(value);
127
129
  },
128
130
  /** @param {string} name */
129
131
  getHeader(name) {
130
- // Header lookup is case-insensitive in Node's real
131
- // ServerResponse mirror that so middlewares that probe an
132
- // existing header before overwriting it (`if
133
- // (!res.getHeader("Content-Type"))`) keep working.
134
- const lower = name.toLowerCase();
135
- for (const [k, v] of Object.entries(headers)) {
136
- if (k.toLowerCase() === lower) return v;
137
- }
138
- return undefined;
132
+ // Keys are always stored lowercased (see setHeader), so a direct
133
+ // lowercased lookup is sufficient and avoids an O(n) scan.
134
+ return headers[name.toLowerCase()];
139
135
  },
140
136
  get headersSent() {
141
137
  return settled;
@@ -28,8 +28,8 @@ export default {
28
28
  name: "search-index",
29
29
 
30
30
  /** @param {ZfbBuildHookContext} ctx */
31
- postBuild(ctx) {
32
- emitSearchIndex(/** @type {SearchIndexBuildOptions} */ (/** @type {unknown} */ ({
31
+ async postBuild(ctx) {
32
+ await emitSearchIndex(/** @type {SearchIndexBuildOptions} */ (/** @type {unknown} */ ({
33
33
  ...ctx.options,
34
34
  outDir: ctx.outDir,
35
35
  logger: ctx.logger,
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gen-z-index.mjs
3
+ //
4
+ // Codegen: rewrite the GENERATED:Z_INDEX marker block inside
5
+ // src/styles/global.css from the single source of truth in
6
+ // src/config/z-index-tokens.ts.
7
+ //
8
+ // The block is a Tailwind v4 `@theme { --z-index-<name>: <value>; }` for every
9
+ // tier, so Tailwind generates `z-<name>` utilities (e.g. `--z-index-toolbar: 20`
10
+ // → `.z-toolbar { z-index: 20 }`) and raw CSS can reference the same var via
11
+ // `z-index: var(--z-index-<name>)`.
12
+ //
13
+ // Pure Node (fs only — NO npm deps). Idempotent: running twice produces no diff.
14
+ //
15
+ // Usage:
16
+ // node scripts/gen-z-index.mjs # rewrite the block in global.css
17
+ // node scripts/gen-z-index.mjs --check # verify committed block is up to date
18
+ // # (exit 1 on drift, no write)
19
+ //
20
+ // MAINTENANCE: edit src/config/z-index-tokens.ts (the source of truth), then run
21
+ // `pnpm gen:z-index` and commit the regenerated global.css. Never hand-edit the
22
+ // block between the BEGIN/END markers.
23
+
24
+ import { readFileSync, writeFileSync } from "node:fs";
25
+ import { resolve, dirname } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+ const ROOT = resolve(__dirname, "..");
30
+
31
+ const TOKENS_PATH = resolve(ROOT, "src/config/z-index-tokens.ts");
32
+ const CSS_PATH = resolve(ROOT, "src/styles/global.css");
33
+
34
+ const BEGIN_MARKER = "GENERATED:Z_INDEX_BEGIN";
35
+ const END_MARKER = "GENERATED:Z_INDEX_END";
36
+
37
+ /**
38
+ * Parse the Z_INDEX_TIERS array out of z-index-tokens.ts WITHOUT importing it
39
+ * (this script is a dependency-free .mjs and cannot resolve TypeScript). Reads
40
+ * each `{ name: "...", value: <n>, ... }` object literal. Throws on a malformed
41
+ * source so drift between the parser and the file surfaces loudly.
42
+ */
43
+ function parseTiers(src) {
44
+ const arrayMatch = src.match(
45
+ /export const Z_INDEX_TIERS[^=]*=\s*\[([\s\S]*?)\];/,
46
+ );
47
+ if (!arrayMatch) {
48
+ throw new Error(
49
+ `Could not locate "export const Z_INDEX_TIERS = [ ... ]" in ${TOKENS_PATH}`,
50
+ );
51
+ }
52
+ const body = arrayMatch[1];
53
+ const tiers = [];
54
+ // Each tier is a `{ ... }` object literal; iterate top-level braces.
55
+ const objectRe = /\{([\s\S]*?)\}/g;
56
+ let m;
57
+ while ((m = objectRe.exec(body)) !== null) {
58
+ const obj = m[1];
59
+ const nameMatch = obj.match(/name:\s*"([^"]+)"/);
60
+ const valueMatch = obj.match(/value:\s*(-?\d+)/);
61
+ if (!nameMatch || !valueMatch) {
62
+ throw new Error(
63
+ `Malformed tier object in Z_INDEX_TIERS (missing name/value): ${obj.trim()}`,
64
+ );
65
+ }
66
+ tiers.push({ name: nameMatch[1], value: Number(valueMatch[1]) });
67
+ }
68
+ if (tiers.length === 0) {
69
+ throw new Error(`Z_INDEX_TIERS in ${TOKENS_PATH} parsed to an empty list`);
70
+ }
71
+ return tiers;
72
+ }
73
+
74
+ /**
75
+ * Build the full generated block (markers included). Two leading spaces of
76
+ * indentation match the surrounding `@theme` style in global.css.
77
+ */
78
+ function buildBlock(tiers) {
79
+ const lines = [];
80
+ lines.push(` /* ${BEGIN_MARKER}`);
81
+ lines.push(
82
+ ` * GENERATED:Z_INDEX — do not hand-edit; run pnpm gen:z-index.`,
83
+ );
84
+ lines.push(
85
+ ` * Source of truth: src/config/z-index-tokens.ts. Tailwind v4 reads the`,
86
+ );
87
+ lines.push(
88
+ ` * --z-index-<name> theme key and generates a z-<name> utility. */`,
89
+ );
90
+ lines.push(` @theme {`);
91
+ for (const tier of tiers) {
92
+ lines.push(` --z-index-${tier.name}: ${tier.value};`);
93
+ }
94
+ lines.push(` }`);
95
+ lines.push(` /* ${END_MARKER} */`);
96
+ return lines.join("\n");
97
+ }
98
+
99
+ /**
100
+ * Replace the existing BEGIN…END block in `css` with `block`. Throws if the
101
+ * markers are missing (the block must be seeded once by hand — see global.css).
102
+ */
103
+ function replaceBlock(css, block) {
104
+ const beginIdx = css.indexOf(BEGIN_MARKER);
105
+ const endIdx = css.indexOf(END_MARKER);
106
+ if (beginIdx === -1 || endIdx === -1) {
107
+ throw new Error(
108
+ `Could not find ${BEGIN_MARKER} … ${END_MARKER} markers in ${CSS_PATH}.\n` +
109
+ `Seed the marker block once by hand, then re-run the generator.`,
110
+ );
111
+ }
112
+ // Expand to the full comment line that opens the block (" /* GENERATED:...")
113
+ // and to the end of the closing "*/ " line so the whole region is replaced.
114
+ const lineStart = css.lastIndexOf("\n", beginIdx) + 1;
115
+ const afterEnd = css.indexOf("\n", endIdx);
116
+ const lineEnd = afterEnd === -1 ? css.length : afterEnd;
117
+ return css.slice(0, lineStart) + block + css.slice(lineEnd);
118
+ }
119
+
120
+ function main() {
121
+ const check = process.argv.includes("--check");
122
+
123
+ const tokensSrc = readFileSync(TOKENS_PATH, "utf8");
124
+ const css = readFileSync(CSS_PATH, "utf8");
125
+
126
+ const tiers = parseTiers(tokensSrc);
127
+ const block = buildBlock(tiers);
128
+ const next = replaceBlock(css, block);
129
+
130
+ if (check) {
131
+ if (next !== css) {
132
+ console.error(
133
+ "z-index codegen drift detected: src/styles/global.css is out of date.",
134
+ );
135
+ console.error("Run `pnpm gen:z-index` and commit the result.");
136
+ return 1;
137
+ }
138
+ console.log(
139
+ `OK — z-index @theme block is up to date (${tiers.length} tiers).`,
140
+ );
141
+ return 0;
142
+ }
143
+
144
+ if (next === css) {
145
+ console.log(
146
+ `z-index @theme block already up to date (${tiers.length} tiers); no change.`,
147
+ );
148
+ return 0;
149
+ }
150
+ writeFileSync(CSS_PATH, next);
151
+ console.log(
152
+ `Wrote z-index @theme block to src/styles/global.css (${tiers.length} tiers).`,
153
+ );
154
+ return 0;
155
+ }
156
+
157
+ process.exit(main());
@@ -124,9 +124,14 @@ export default function SidebarToggle({
124
124
  {/* Backdrop overlay - mobile only.
125
125
  Rendered unconditionally; CSS `hidden` toggles visibility so
126
126
  the SSR DOM tree matches the hydrated tree (no subtree
127
- mount/unmount across the hydration boundary). */}
127
+ mount/unmount across the hydration boundary).
128
+ `z-modal-backdrop` (50) intentionally sits ABOVE the header
129
+ (`z-toolbar`, 20): the open mobile drawer is a modal surface that
130
+ dims the whole viewport, header included. Closing is via tapping the
131
+ backdrop (onClick below), so the header hamburger being dimmed under
132
+ it is fine. */}
128
133
  <div
129
- className={clsx("fixed inset-0 z-30 bg-overlay/30 lg:hidden", !open && "hidden")}
134
+ className={clsx("fixed inset-0 z-modal-backdrop bg-overlay/30 lg:hidden", !open && "hidden")}
130
135
  aria-hidden={!open}
131
136
  onClick={() => setOpen(false)}
132
137
  />
@@ -141,7 +146,7 @@ export default function SidebarToggle({
141
146
  <aside
142
147
  inert={!open}
143
148
  className={`
144
- fixed top-[3.5rem] left-0 z-40 h-[calc(100vh-3.5rem)] w-[16rem] flex flex-col
149
+ fixed top-[3.5rem] left-0 z-modal h-[calc(100vh-3.5rem)] w-[16rem] flex flex-col
145
150
  border-r border-muted bg-bg transition-transform duration-200
146
151
  lg:hidden
147
152
  ${open ? "translate-x-0" : "-translate-x-full"}
@@ -288,7 +288,7 @@ export default function SidebarTree({ nodes, currentSlug, rootMenuItems, backToM
288
288
  type="text"
289
289
  placeholder={filterPlaceholder}
290
290
  value={query}
291
- onChange={(e) => setQuery(e.currentTarget.value)}
291
+ onInput={(e) => setQuery(e.currentTarget.value)}
292
292
  className="bg-transparent text-small outline-none w-full text-fg placeholder:text-muted"
293
293
  />
294
294
  </div>
@@ -419,7 +419,7 @@ function CategoryNode({
419
419
  <div className={`${depth === 0 ? "border-t border-muted" : ""} ${depth >= 1 && !isLast ? "relative" : ""}`}>
420
420
  {depth >= 1 && !isLast && isExpanded && (
421
421
  <div
422
- className="absolute border-l border-solid border-muted z-10"
422
+ className="absolute border-l border-solid border-muted z-local-1"
423
423
  style={{
424
424
  left: connectorLeft(depth),
425
425
  top: 0,
@@ -118,7 +118,7 @@ function CategoryNode({
118
118
  <div className={`${depth >= 1 && !isLast ? "relative" : ""}`}>
119
119
  {depth >= 1 && !isLast && open && (
120
120
  <div
121
- className="absolute border-l border-dashed border-muted z-10"
121
+ className="absolute border-l border-dashed border-muted z-local-1"
122
122
  style={{
123
123
  left: connectorLeft(depth),
124
124
  top: 0,
@@ -11,9 +11,11 @@ export interface ColorScheme {
11
11
  string, string, string, string, string, string, string, string,
12
12
  string, string, string, string, string, string, string, string,
13
13
  ];
14
- /** Optional Shiki theme for the zdtp panel's client-side code-block preview.
15
- * Falls back to the panel config's DEFAULT_SHIKI_THEME when omitted.
16
- * Static highlighting (syntect via zfb's Rust pipeline) is unaffected. */
14
+ /** Optional, vestigial. Carried only in the zdtp panel's color-scheme
15
+ * config envelope (falls back to DEFAULT_SHIKI_THEME when omitted), but has
16
+ * no visible effect: zdtp's Shiki integration is a no-op stub, and page code
17
+ * highlighting is done by syntect (dual-theme, configured via `codeHighlight`
18
+ * in zfb.config.ts), not Shiki. */
17
19
  shikiTheme?: string;
18
20
  /** Optional semantic overrides — when omitted, defaults are used:
19
21
  * surface=p0, muted=p8, accent=p5, accentHover=p14
@@ -0,0 +1,127 @@
1
+ // z-index design tokens — single source of truth.
2
+ //
3
+ // This file is the ONE place z-index tiers are defined. The CSS `@theme` block
4
+ // in `src/styles/global.css` is GENERATED from this list by
5
+ // `scripts/gen-z-index.mjs` (run `pnpm gen:z-index`); never hand-edit the
6
+ // generated block. `pnpm check:z-index` re-runs the generator into a buffer and
7
+ // fails on drift, so the committed CSS can never silently diverge from this list.
8
+ //
9
+ // Strategy (from zudolab/zudo-css-wisdom z-index-strategy): semantic single-
10
+ // namespace tokens — names describe ROLES, never magnitudes. One flat ordered
11
+ // list. Values are deliberately gapped but otherwise arbitrary: renaming and
12
+ // reordering is cheap, which is the whole point. Tailwind v4 reads the
13
+ // `--z-index-<name>` theme key and generates a `z-<name>` utility, so e.g.
14
+ // `@theme { --z-index-toolbar: 20 }` produces `.z-toolbar { z-index: 20 }`.
15
+ //
16
+ // `kind` distinguishes:
17
+ // - "global": a tier on the single global stacking scale (overlay chrome etc.)
18
+ // - "local": anonymous reusable helpers for promoting a child WITHIN a parent
19
+ // stacking context (`isolation: isolate` / `position: relative`),
20
+ // not a position on the global scale.
21
+ //
22
+ // Rationale for the two zudo-doc-specific additions (NOT in the generic
23
+ // overlay-centric strategy scale):
24
+ // - `sidebar` — persistent layout chrome (desktop sidebar, TOC, sidebar-toggle
25
+ // handle, resizer handle). The strategy's scale has no persistent-sidebar/TOC
26
+ // tier. `toolbar` (the sticky header) is deliberately placed ABOVE `sidebar`
27
+ // to preserve the existing header-wins ordering; they do not overlap
28
+ // spatially, but the historical relationship is kept explicit.
29
+ // - `drag` — transient drag affordance (sidebar-resizer ghost line). Replaces
30
+ // the old `z-9999` anti-pattern with a named top-of-steady-UI tier.
31
+ //
32
+ // `popover`, `toast`, and `tooltip` are reserved canonical tiers from the
33
+ // strategy's scale — kept for completeness (and so downstream `create-zudo-doc`
34
+ // users inherit the full scale) even though zudo-doc does not use them yet.
35
+
36
+ export type ZIndexKind = "global" | "local";
37
+
38
+ export interface ZIndexTier {
39
+ name: string;
40
+ value: number;
41
+ purpose: string;
42
+ kind: ZIndexKind;
43
+ }
44
+
45
+ export const Z_INDEX_TIERS: ZIndexTier[] = [
46
+ {
47
+ name: "content",
48
+ value: 0,
49
+ purpose: "default in-flow content (implicit baseline)",
50
+ kind: "global",
51
+ },
52
+ {
53
+ name: "local-1",
54
+ value: 1,
55
+ purpose: "child promotion inside an isolated parent stacking context",
56
+ kind: "local",
57
+ },
58
+ {
59
+ name: "local-2",
60
+ value: 2,
61
+ purpose: "child promotion inside an isolated parent stacking context",
62
+ kind: "local",
63
+ },
64
+ {
65
+ name: "local-3",
66
+ value: 3,
67
+ purpose: "child promotion inside an isolated parent stacking context",
68
+ kind: "local",
69
+ },
70
+ {
71
+ name: "sidebar",
72
+ value: 10,
73
+ purpose:
74
+ "persistent layout chrome: desktop sidebar, TOC, sidebar-toggle handle, resizer handle",
75
+ kind: "global",
76
+ },
77
+ {
78
+ name: "toolbar",
79
+ value: 20,
80
+ purpose: "sticky top header (sits above sidebar chrome)",
81
+ kind: "global",
82
+ },
83
+ {
84
+ name: "dropdown",
85
+ value: 30,
86
+ purpose: "header menus, version/language switchers",
87
+ kind: "global",
88
+ },
89
+ {
90
+ name: "popover",
91
+ value: 40,
92
+ purpose: "reserved — inline popovers (canonical scale; not yet used)",
93
+ kind: "global",
94
+ },
95
+ {
96
+ name: "modal-backdrop",
97
+ value: 50,
98
+ purpose: "mobile drawer backdrop, <dialog> ::backdrop",
99
+ kind: "global",
100
+ },
101
+ {
102
+ name: "modal",
103
+ value: 60,
104
+ purpose: "mobile sidebar drawer panel, search <dialog>",
105
+ kind: "global",
106
+ },
107
+ {
108
+ name: "toast",
109
+ value: 70,
110
+ purpose: "reserved — transient notifications (canonical scale; not yet used)",
111
+ kind: "global",
112
+ },
113
+ {
114
+ name: "tooltip",
115
+ value: 80,
116
+ purpose:
117
+ "reserved — highest steady UI layer, below only the transient drag tier (canonical scale; not yet used)",
118
+ kind: "global",
119
+ },
120
+ {
121
+ name: "drag",
122
+ value: 90,
123
+ purpose:
124
+ "transient drag affordance: sidebar-resizer ghost line (replaces the z-9999 anti-pattern)",
125
+ kind: "global",
126
+ },
127
+ ];
@@ -216,6 +216,37 @@
216
216
  --breakpoint-xl: 1280px;
217
217
  }
218
218
 
219
+ /* ========================================
220
+ * Z-index tiers — semantic, single-namespace scale.
221
+ *
222
+ * The @theme block below is GENERATED from src/config/z-index-tokens.ts
223
+ * (the single source of truth) by `pnpm gen:z-index`. Tailwind v4 reads the
224
+ * --z-index-<name> theme key and generates a `z-<name>` utility, so e.g.
225
+ * --z-index-toolbar: 20 produces `.z-toolbar { z-index: 20 }`; raw CSS can
226
+ * also reference `z-index: var(--z-index-<name>)`. `pnpm check:z-index` fails
227
+ * on drift. Do NOT hand-edit between the BEGIN/END markers.
228
+ * ======================================== */
229
+ /* GENERATED:Z_INDEX_BEGIN
230
+ * GENERATED:Z_INDEX — do not hand-edit; run pnpm gen:z-index.
231
+ * Source of truth: src/config/z-index-tokens.ts. Tailwind v4 reads the
232
+ * --z-index-<name> theme key and generates a z-<name> utility. */
233
+ @theme {
234
+ --z-index-content: 0;
235
+ --z-index-local-1: 1;
236
+ --z-index-local-2: 2;
237
+ --z-index-local-3: 3;
238
+ --z-index-sidebar: 10;
239
+ --z-index-toolbar: 20;
240
+ --z-index-dropdown: 30;
241
+ --z-index-popover: 40;
242
+ --z-index-modal-backdrop: 50;
243
+ --z-index-modal: 60;
244
+ --z-index-toast: 70;
245
+ --z-index-tooltip: 80;
246
+ --z-index-drag: 90;
247
+ }
248
+ /* GENERATED:Z_INDEX_END */
249
+
219
250
  :root {
220
251
  /* Default responsive range; sidebar-resizer.ts allows 192–448px for explicit resizing */
221
252
  --zd-sidebar-w: clamp(14rem, 20vw, 22rem);
@@ -696,7 +727,7 @@ pre[class^="syntect-"].word-wrap code {
696
727
  gap: var(--spacing-hsp-2xs);
697
728
  opacity: 0;
698
729
  transition: opacity 0.15s;
699
- z-index: 1;
730
+ z-index: var(--z-index-local-1);
700
731
  }
701
732
 
702
733
  .code-block-wrapper:hover .code-buttons,
@@ -1152,7 +1183,7 @@ pre[class^="syntect-"] .line .highlighted-word {
1152
1183
  border: none;
1153
1184
  background: transparent;
1154
1185
  cursor: pointer;
1155
- z-index: 1;
1186
+ z-index: var(--z-index-local-1);
1156
1187
  transition: opacity var(--default-transition-duration);
1157
1188
  }
1158
1189
 
@@ -1161,12 +1192,12 @@ pre[class^="syntect-"] .line .highlighted-word {
1161
1192
  position: absolute;
1162
1193
  inset: 0;
1163
1194
  background: color-mix(in oklch, var(--color-image-overlay-bg) 80%, transparent);
1164
- z-index: 0;
1195
+ z-index: var(--z-index-local-1);
1165
1196
  }
1166
1197
 
1167
1198
  .zd-enlarge-btn > svg {
1168
1199
  position: relative;
1169
- z-index: 1;
1200
+ z-index: var(--z-index-local-2);
1170
1201
  width: var(--spacing-icon-sm);
1171
1202
  height: var(--spacing-icon-sm);
1172
1203
  color: var(--color-image-overlay-fg);
@@ -1201,7 +1232,7 @@ dialog.zd-enlarge-dialog::backdrop {
1201
1232
  border: none;
1202
1233
  background: transparent;
1203
1234
  cursor: pointer;
1204
- z-index: 1;
1235
+ z-index: var(--z-index-local-1);
1205
1236
  transition: opacity var(--default-transition-duration);
1206
1237
  }
1207
1238
 
@@ -1214,12 +1245,12 @@ dialog.zd-enlarge-dialog::backdrop {
1214
1245
  position: absolute;
1215
1246
  inset: 0;
1216
1247
  background: color-mix(in oklch, var(--color-image-overlay-bg) 80%, transparent);
1217
- z-index: 0;
1248
+ z-index: var(--z-index-local-1);
1218
1249
  }
1219
1250
 
1220
1251
  .zd-enlarge-dialog-close > svg {
1221
1252
  position: relative;
1222
- z-index: 1;
1253
+ z-index: var(--z-index-local-2);
1223
1254
  width: var(--spacing-icon-lg);
1224
1255
  height: var(--spacing-icon-lg);
1225
1256
  color: var(--color-image-overlay-fg);
@@ -109,12 +109,23 @@ declare module "zfb/config" {
109
109
  base?: string;
110
110
  /**
111
111
  * Configures the syntect-based syntax highlighter shipped with zfb.
112
- * Mirrors `code_highlight` in crates/zfb/src/config.rs (Takazudo/zudo-front-builder#188 / sub #194; landed in commit 339e30f).
112
+ * Mirrors `CodeHighlightConfig` / `code_highlight` in crates/zfb/src/config.rs
113
+ * (single-theme: Takazudo/zudo-front-builder#188 / sub #194, commit 339e30f;
114
+ * dual-theme themeLight/themeDark added in the follow-up shipped in
115
+ * zfb 0.1.0-next.45+).
113
116
  * When omitted, the engine falls back to the hardcoded default theme `base16-ocean.dark`.
117
+ *
118
+ * Single-theme mode: set `theme` — tokens get inline `style="color:#hex"`.
119
+ * Dual-theme mode: set BOTH `themeLight` and `themeDark` (mutually exclusive
120
+ * with `theme`) — tokens get `--shiki-light`/`--shiki-dark` CSS custom
121
+ * properties and the `<pre>` gains `class="syntect-dual"` + `--shiki-*-bg`.
122
+ * All names are SYNTECT theme names, not Shiki names.
114
123
  */
115
124
  codeHighlight?: {
116
125
  theme?: string;
117
126
  themesDir?: string;
127
+ themeLight?: string;
128
+ themeDark?: string;
118
129
  };
119
130
  /**
120
131
  * Markdown link resolver (port of `remarkResolveMarkdownLinks`).
@@ -35,6 +35,22 @@ export default {
35
35
  /** @param {ZfbBuildHookContext} ctx */
36
36
  async preBuild(ctx) {
37
37
  const { docsDir, locales } = ctx.options;
38
+ // Validate each locale entry before passing downstream so a misconfigured
39
+ // locales map surfaces a clear error instead of a confusing runtime crash.
40
+ if (locales != null) {
41
+ for (const [key, entry] of Object.entries(/** @type {Record<string, unknown>} */ (locales))) {
42
+ if (
43
+ entry == null ||
44
+ typeof entry !== "object" ||
45
+ typeof /** @type {any} */ (entry).dir !== "string" ||
46
+ /** @type {any} */ (entry).dir.length === 0
47
+ ) {
48
+ throw new Error(
49
+ `[doc-history] invalid locales entry "${key}": expected { dir: string }`,
50
+ );
51
+ }
52
+ }
53
+ }
38
54
  await runDocHistoryMetaStep({
39
55
  projectRoot: ctx.projectRoot,
40
56
  docsDir: typeof docsDir === "string" ? docsDir : "src/content/docs",
@@ -47,10 +63,22 @@ export default {
47
63
 
48
64
  /** @param {ZfbBuildHookContext} ctx */
49
65
  async postBuild(ctx) {
50
- await runDocHistoryPostBuild(
51
- /** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
52
- { outDir: ctx.outDir, logger: ctx.logger },
53
- );
66
+ try {
67
+ await runDocHistoryPostBuild(
68
+ /** @type {import("@takazudo/zudo-doc/integrations/doc-history").DocHistoryOptions} */ (/** @type {unknown} */ (ctx.options)),
69
+ { outDir: ctx.outDir, logger: ctx.logger },
70
+ );
71
+ } catch (err) {
72
+ // postBuild dropdown JSON is redundant: the parallel build-history CI job
73
+ // is the deployed source of truth. Downgrade failures to a warning so a
74
+ // transient git/CLI error does not red the build-site job.
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ if (ctx.logger?.warn) {
77
+ ctx.logger.warn(`[doc-history] postBuild failed (non-fatal): ${msg}`);
78
+ } else {
79
+ console.warn(`[doc-history] postBuild failed (non-fatal): ${msg}`);
80
+ }
81
+ }
54
82
  },
55
83
 
56
84
  /** @param {ZfbDevMiddlewareContext} ctx */
@@ -559,10 +559,17 @@ export function DocHistory({ slug, locale, basePath = "/" }: DocHistoryProps) {
559
559
  )}
560
560
 
561
561
  {/* Full-screen dialog — renders in top layer, above all stacking contexts */}
562
+ {/* z-modal / backdrop:z-modal-backdrop are defense-in-depth for the
563
+ SPA-swap window: clicking a history entry link swaps the page body
564
+ while this dialog is still open, and a native showModal() dialog can
565
+ momentarily lose top-layer promotion and fall back to z-index:auto,
566
+ flashing behind the header/sidebar. The explicit modal-tier z-index
567
+ keeps it above all chrome during that window. Intentionally redundant
568
+ in the normal (top-layer) case — do not remove as "redundant". */}
562
569
  <dialog
563
570
  ref={dialogRef}
564
571
  aria-label="Document revision history"
565
- className="doc-history-panel fixed inset-0 m-0 h-full w-full max-h-full max-w-full bg-bg border-none p-0 backdrop:bg-bg/30"
572
+ className="doc-history-panel z-modal fixed inset-0 m-0 h-full w-full max-h-full max-w-full bg-bg border-none p-0 backdrop:z-modal-backdrop backdrop:bg-bg/30"
566
573
  style={{ color: "var(--color-fg)" }}
567
574
  >
568
575
  {/* Panel header */}
@@ -67,6 +67,15 @@ interface PageArgs {
67
67
  export default function LocaleIndexPage({ params }: PageArgs): JSX.Element {
68
68
  const locale = params.locale;
69
69
 
70
+ // Guard: paths() only emits routes for locales defined in settings.locales,
71
+ // but the component can still be exercised with an unconfigured locale value
72
+ // (e.g. during testing or if the router dispatches an unexpected param).
73
+ // Fail loudly rather than silently serving EN content under a bogus prefix.
74
+ const cfg = getLocaleConfig(locale);
75
+ if (!cfg) {
76
+ throw new Error(`LocaleIndexPage: locale "${locale}" is not configured in settings.locales`);
77
+ }
78
+
70
79
  // Identity-stable, locale-first merge with EN fallback (shared `navDocs`
71
80
  // instance). categoryMeta is intentionally locale-dir-only here — this page
72
81
  // historically did NOT merge in base meta (unlike the locale doc route), so
@@ -75,10 +84,7 @@ export default function LocaleIndexPage({ params }: PageArgs): JSX.Element {
75
84
  applyDefaultLocaleOnlyFilter: true,
76
85
  keepUnlisted: true,
77
86
  });
78
- const localeConfig = getLocaleConfig(locale);
79
- const categoryMeta = localeConfig
80
- ? loadCategoryMeta(localeConfig.dir)
81
- : loadCategoryMeta(settings.docsDir);
87
+ const categoryMeta = loadCategoryMeta(cfg.dir);
82
88
 
83
89
  const tree = buildNavTree(navDocs, locale as Locale, categoryMeta);
84
90
  const categoryOrder = getCategoryOrder();
@@ -18,8 +18,17 @@ interface ImageData {
18
18
  // agree on class string and inline style — otherwise the dist HTML and the
19
19
  // post-hydration DOM disagree on size / position and the first interaction
20
20
  // flashes. Sourcing both from the same constants closes the drift gap.
21
+ //
22
+ // z-modal / backdrop:z-modal-backdrop are defense-in-depth for the SPA-swap
23
+ // window: if this dialog is still open while the page body is swapped, a native
24
+ // showModal() dialog can lose top-layer promotion and fall back to z-index:auto,
25
+ // flashing behind the header/sidebar. `backdrop:z-modal-backdrop` targets the
26
+ // native `::backdrop` (present for every showModal() dialog even with no backdrop
27
+ // tint). The explicit modal-tier z-index keeps it above all chrome during that
28
+ // window. Intentionally redundant in the normal (top-layer) case — do not remove
29
+ // as "redundant".
21
30
  const DIALOG_CLASS =
22
- "zd-enlarge-dialog mx-auto max-h-[90vh] max-w-[90vw] overflow-hidden border border-muted bg-surface p-0";
31
+ "zd-enlarge-dialog z-modal mx-auto max-h-[90vh] max-w-[90vw] overflow-hidden border border-muted bg-surface p-0 backdrop:z-modal-backdrop";
23
32
  // Center the modal with `inset: 0; margin: auto` rather than a transform.
24
33
  // A `transform` on the dialog would establish a containing block for its
25
34
  // `position: fixed` descendants, which would trap the `.zd-enlarge-dialog-close`
@@ -32,8 +32,8 @@ export default {
32
32
  name: "llms-txt",
33
33
 
34
34
  /** @param {ZfbBuildHookContext} ctx */
35
- postBuild(ctx) {
36
- emitLlmsTxt(/** @type {LlmsTxtEmitOptions} */ (/** @type {unknown} */ ({
35
+ async postBuild(ctx) {
36
+ await emitLlmsTxt(/** @type {LlmsTxtEmitOptions} */ (/** @type {unknown} */ ({
37
37
  ...ctx.options,
38
38
  outDir: ctx.outDir,
39
39
  // siteUrl is normalised to undefined when falsy because the runner
@@ -70,6 +70,13 @@ export default {
70
70
  if (locale && typeof locale === "object" && typeof locale.code === "string") {
71
71
  ctx.register(`${basePrefix}/${locale.code}/llms.txt`, handler);
72
72
  ctx.register(`${basePrefix}/${locale.code}/llms-full.txt`, handler);
73
+ } else {
74
+ const repr = JSON.stringify(locale);
75
+ if (ctx.logger?.warn) {
76
+ ctx.logger.warn(`[llms-txt] skipping malformed locale entry (expected { code: string }): ${repr}`);
77
+ } else {
78
+ console.warn(`[llms-txt] skipping malformed locale entry (expected { code: string }): ${repr}`);
79
+ }
73
80
  }
74
81
  }
75
82
  }
@@ -101,7 +101,7 @@ export default function DesktopSidebarToggle() {
101
101
  <button
102
102
  type="button"
103
103
  onClick={() => setVisible((v) => !v)}
104
- className="zd-desktop-sidebar-toggle hidden lg:flex fixed bottom-vsp-xl z-40 items-center justify-center w-[1.5rem] h-[3rem] bg-surface border border-muted border-l-0 rounded-r-DEFAULT text-muted cursor-pointer transition-[left,color] duration-200 ease-in-out hover:text-fg"
104
+ className="zd-desktop-sidebar-toggle hidden lg:flex fixed bottom-vsp-xl z-sidebar items-center justify-center w-[1.5rem] h-[3rem] bg-surface border border-muted border-l-0 rounded-r-DEFAULT text-muted cursor-pointer transition-[left,color] duration-200 ease-in-out hover:text-fg"
105
105
  aria-label={visible ? 'Hide sidebar' : 'Show sidebar'}
106
106
  aria-pressed={visible}
107
107
  data-zfb-transition-persist="desktop-sidebar-toggle"
@@ -70,7 +70,7 @@ export function FindBar({ visible, onClose, findInPage, containerSelector }: Fin
70
70
  if (!visible) return null;
71
71
 
72
72
  return (
73
- <div className="fixed top-[3.5rem] right-0 z-50 flex items-center gap-hsp-sm py-hsp-xs px-hsp-md bg-surface border-b border-l border-muted rounded-bl-lg shadow-md">
73
+ <div className="fixed top-[3.5rem] right-0 z-dropdown flex items-center gap-hsp-sm py-hsp-xs px-hsp-md bg-surface border-b border-l border-muted rounded-bl-lg shadow-md">
74
74
  <input
75
75
  ref={inputRef}
76
76
  className="w-48 py-[4px] px-hsp-sm rounded text-small bg-bg border border-muted text-fg outline-none focus:border-accent"
@@ -2,6 +2,6 @@
2
2
  "identifier": "default",
3
3
  "description": "Default capabilities for the main window",
4
4
  "windows": ["main"],
5
- "remote": { "urls": ["http://localhost:*/*"] },
5
+ "remote": { "urls": ["http://localhost:*/**"] },
6
6
  "permissions": ["core:default"]
7
7
  }