create-zudo-doc 0.2.5 → 0.2.7
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/features/claude-resources.js +4 -1
- package/dist/scaffold.js +25 -6
- package/dist/settings-gen.js +7 -2
- package/package.json +1 -1
- package/templates/base/pages/lib/_search-widget-script.ts +45 -1
- package/templates/base/pages/lib/_search-widget.tsx +9 -1
- package/templates/base/pages/sitemap.xml.tsx +8 -0
- package/templates/base/plugins/connect-adapter.mjs +6 -10
- package/templates/base/plugins/search-index-plugin.mjs +2 -2
- package/templates/base/scripts/gen-z-index.mjs +157 -0
- package/templates/base/src/components/sidebar-toggle.tsx +8 -3
- package/templates/base/src/components/sidebar-tree.tsx +2 -2
- package/templates/base/src/components/site-tree-nav.tsx +1 -1
- package/templates/base/src/config/z-index-tokens.ts +127 -0
- package/templates/base/src/styles/global.css +38 -7
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +32 -4
- package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -1
- package/templates/features/i18n/files/pages/[locale]/index.tsx +10 -4
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +10 -1
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +9 -2
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +1 -1
- package/templates/features/tauri/files/src/components/find-bar.tsx +1 -1
- 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
|
-
//
|
|
4
|
+
// No shared file injection needed — this 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
|
@@ -277,7 +277,7 @@ function generatePackageJson(choices) {
|
|
|
277
277
|
// pages render on first request instead of on every file-change tick
|
|
278
278
|
// (Takazudo/zudo-front-builder#1029); `ZFB_DEV_EAGER=1` restores eager
|
|
279
279
|
// mode. Dev-server-only change, no build/config migration needed.
|
|
280
|
-
// next.41
|
|
280
|
+
// next.41 adds a URL-space fallback in resolve_links —
|
|
281
281
|
// dir-style hrefs written from non-index pages now resolve against the
|
|
282
282
|
// page's URL directory when the file-space lookup misses
|
|
283
283
|
// (Takazudo/zudo-front-builder#1030) — and the data-file skip warning
|
|
@@ -285,16 +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
|
-
|
|
289
|
-
|
|
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",
|
|
290
300
|
// zfb-adapter-cloudflare — required for any route with `prerender = false`.
|
|
291
301
|
// Pinned in lockstep with @takazudo/zfb.
|
|
292
|
-
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.
|
|
302
|
+
"@takazudo/zfb-adapter-cloudflare": "0.1.0-next.47",
|
|
293
303
|
// @takazudo/zudo-doc — published from this monorepo via
|
|
294
304
|
// .github/workflows/publish-zudo-doc.yml. The pin here is bumped in
|
|
295
305
|
// lockstep by scripts/release-create-zudo-doc.sh whenever zudo-doc's
|
|
296
306
|
// version moves, so a fresh scaffold pulls the version we just published.
|
|
297
|
-
"@takazudo/zudo-doc": "^0.2.
|
|
307
|
+
"@takazudo/zudo-doc": "^0.2.7",
|
|
298
308
|
// zod — used by the generated zfb.config.ts. zfb-config-gen emits
|
|
299
309
|
// `import { z } from "zod"` for the content-collection schema +
|
|
300
310
|
// `z.toJSONSchema(...)` conversion. Without this dep, the consumer
|
|
@@ -306,6 +316,7 @@ function generatePackageJson(choices) {
|
|
|
306
316
|
// ^10.29.1 floor satisfies @takazudo/zdtp's preact peer range so the app
|
|
307
317
|
// and zdtp resolve a single preact instance — a lower floor can split into
|
|
308
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.
|
|
309
320
|
preact: "^10.29.1",
|
|
310
321
|
// preact-render-to-string — zfb's emitted entry.mjs imports
|
|
311
322
|
// `renderToString` from this package as `__zfb_renderToString` to
|
|
@@ -348,7 +359,7 @@ function generatePackageJson(choices) {
|
|
|
348
359
|
// @takazudo/zudo-doc/integrations/doc-history which in turn imports
|
|
349
360
|
// @takazudo/zudo-doc-history-server/git-history. Without this dep the
|
|
350
361
|
// plugin host fails at init with ERR_MODULE_NOT_FOUND — W8A (#1739).
|
|
351
|
-
deps["@takazudo/zudo-doc-history-server"] = "^0.2.
|
|
362
|
+
deps["@takazudo/zudo-doc-history-server"] = "^0.2.7";
|
|
352
363
|
// W7A (#1736): doc-history-plugin.mjs spawns `tsx -e <inline-script>` to
|
|
353
364
|
// run the v2 runtime in a TS-aware Node subprocess; without tsx the
|
|
354
365
|
// plugin's preBuild step exits with ENOENT before zfb finishes config
|
|
@@ -362,6 +373,8 @@ function generatePackageJson(choices) {
|
|
|
362
373
|
devDeps["tsx"] = "^4.21.0";
|
|
363
374
|
}
|
|
364
375
|
if (choices.features.includes("designTokenPanel")) {
|
|
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.
|
|
365
378
|
deps["@takazudo/zdtp"] = "0.2.0-next.2";
|
|
366
379
|
}
|
|
367
380
|
if (choices.features.includes("tagGovernance")) {
|
|
@@ -386,6 +399,12 @@ function generatePackageJson(choices) {
|
|
|
386
399
|
// the pages/lib call sites, so emitting the script would fail on a fresh
|
|
387
400
|
// scaffold. Revisit once the template stubs carry typed props.
|
|
388
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",
|
|
389
408
|
};
|
|
390
409
|
if (choices.features.includes("tagGovernance")) {
|
|
391
410
|
scripts["tags:audit"] = "tsx scripts/tags-audit.ts";
|
package/dist/settings-gen.js
CHANGED
|
@@ -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
|
@@ -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, "&")
|
|
@@ -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
|
-
|
|
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
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
121
|
+
className="absolute border-l border-dashed border-muted z-local-1"
|
|
122
122
|
style={{
|
|
123
123
|
left: connectorLeft(depth),
|
|
124
124
|
top: 0,
|
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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);
|
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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-
|
|
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-
|
|
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"
|