create-zudo-doc 0.2.20 → 0.2.22

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 (74) hide show
  1. package/dist/features/image-enlarge.d.ts +10 -2
  2. package/dist/features/image-enlarge.js +10 -2
  3. package/dist/scaffold.js +22 -22
  4. package/dist/zfb-config-gen.d.ts +16 -10
  5. package/dist/zfb-config-gen.js +34 -239
  6. package/package.json +1 -1
  7. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  8. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  9. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  10. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  11. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  12. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  13. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  15. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  16. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  17. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  18. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  19. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  20. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  21. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  22. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  23. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  24. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  25. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  26. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  27. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  28. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  29. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  30. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  31. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -47
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  33. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  34. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  35. package/templates/base/pages/lib/locale-merge.ts +32 -145
  36. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  37. package/templates/base/pages/robots.txt.tsx +2 -26
  38. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  39. package/templates/base/src/components/doc-history.tsx +9 -8
  40. package/templates/base/src/components/image-enlarge.tsx +11 -8
  41. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  42. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  43. package/templates/base/src/components/site-tree-nav.tsx +6 -220
  44. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  45. package/templates/base/src/config/i18n.ts +9 -0
  46. package/templates/base/src/config/z-index-tokens.ts +5 -4
  47. package/templates/base/src/styles/global.css +5 -579
  48. package/templates/base/src/utils/base.ts +1 -1
  49. package/templates/base/src/utils/docs.ts +47 -16
  50. package/templates/base/src/utils/github.ts +12 -9
  51. package/templates/base/src/utils/nav-scope.ts +13 -42
  52. package/templates/base/src/utils/sidebar.ts +18 -86
  53. package/templates/base/src/utils/slug.ts +10 -53
  54. package/templates/base/src/utils/smart-break.tsx +12 -120
  55. package/templates/base/src/utils/tags.ts +25 -68
  56. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  57. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  58. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  59. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  60. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  61. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  62. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  63. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  64. package/templates/features/tauri/files/src-tauri/capabilities/default.json +1 -4
  65. package/templates/features/tauri/files/src-tauri/tauri.conf.json +1 -1
  66. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +1 -1
  67. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  68. package/templates/base/plugins/connect-adapter.mjs +0 -169
  69. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  70. package/templates/base/scripts/gen-z-index.mjs +0 -157
  71. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  72. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  73. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  74. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,85 +1,33 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // Shared renderer for the documentation-versions pages (#2010).
4
- //
5
- // Collapses the per-locale page pair:
6
- // pages/docs/versions.tsx + pages/[locale]/docs/versions.tsx
7
- // into one locale-parameterized renderer. The page files stay thin shells
8
- // that own only their paths() param shapes; URL prefixes and the
9
- // default-vs-locale href shapes live here.
3
+ // Thin stub versions-page moved to the package (epic #2344, S8).
4
+ // Calls `createVersionsPageView(deps)` from
5
+ // @takazudo/zudo-doc/versions-page with host singletons injected,
6
+ // then re-exports the resulting component so all existing call sites continue
7
+ // to work unchanged.
10
8
 
9
+ import { createVersionsPageView } from "@takazudo/zudo-doc/versions-page";
11
10
  import { settings } from "@/config/settings";
12
- import { defaultLocale, t, type Locale } from "@/config/i18n";
11
+ import { defaultLocale, t } from "@/config/i18n";
13
12
  import { withBase } from "@/utils/base";
14
- import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
15
- import { VersionsPageContent } from "@takazudo/zudo-doc/nav-indexing";
16
- import type { VersionPageEntry, VersionsPageLabels } from "@takazudo/zudo-doc/nav-indexing";
17
- import type { JSX } from "preact";
18
13
  import { FooterWithDefaults } from "./_footer-with-defaults";
19
14
  import { HeaderWithDefaults } from "./_header-with-defaults";
20
15
  import { HeadWithDefaults } from "./_head-with-defaults";
21
16
  import { composeMetaTitle } from "./_compose-meta-title";
22
17
  import { BodyEndIslands } from "./_body-end-islands";
23
18
 
24
- /** Versions index page for one locale. Lists the latest version and any past
25
- * versions configured in settings.versions. */
26
- export function VersionsPageView({ locale }: { locale: string }): JSX.Element {
27
- const isDefault = locale === defaultLocale;
28
- const prefix = isDefault ? "" : `/${locale}`;
29
- const pageTitle = t("version.page.title", locale);
19
+ export { type VersionsPageViewProps } from "@takazudo/zudo-doc/versions-page";
30
20
 
31
- const labels: VersionsPageLabels = {
32
- pageTitle,
33
- latestTitle: t("version.page.latest.title", locale),
34
- latestDescription: t("version.page.latest.description", locale),
35
- latestLink: t("version.page.latest.link", locale),
36
- pastTitle: t("version.page.past.title", locale),
37
- pastDescription: t("version.page.past.description", locale),
38
- unmaintained: t("version.page.unmaintained", locale),
39
- unreleased: t("version.page.unreleased", locale),
40
- versionCol: t("version.switcher.label", locale),
41
- statusCol: t("version.page.status", locale),
42
- docsCol: t("version.page.docs", locale),
43
- };
44
-
45
- // Latest docs href — points to the default docs entry point
46
- const latestHref = withBase(`${prefix}/docs/getting-started`);
47
-
48
- // Past version entries from settings
49
- const versions: VersionPageEntry[] = settings.versions
50
- ? settings.versions.map((v) => ({
51
- slug: v.slug,
52
- label: v.label ?? v.slug,
53
- // Version prefix comes BEFORE the locale — the only routed shape is
54
- // pages/v/[version]/{locale}/docs/...; /{locale}/v/... has no route.
55
- docsHref: withBase(`/v/${v.slug}${prefix}/docs/getting-started/`),
56
- banner: v.banner as "unmaintained" | "unreleased" | undefined,
57
- }))
58
- : [];
59
-
60
- return (
61
- <DocLayoutWithDefaults
62
- title={composeMetaTitle(pageTitle)}
63
- head={<HeadWithDefaults title={pageTitle} />}
64
- lang={locale}
65
- noindex={settings.noindex}
66
- hideSidebar={true}
67
- hideToc={true}
68
- // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
69
- // Sidebar island — its marker never hydrates for published-package
70
- // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
71
- // hidden on this page anyway (zudolab/zudo-doc#2057).
72
- sidebarOverride={<></>}
73
- headerOverride={<HeaderWithDefaults lang={locale as Locale} currentPath={withBase(`${prefix}/docs/versions`)} />}
74
- footerOverride={<FooterWithDefaults lang={locale} />}
75
- bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
76
- enableClientRouter={settings.dynamicPageTransition}
77
- >
78
- <VersionsPageContent
79
- latestHref={latestHref}
80
- versions={versions}
81
- labels={labels}
82
- />
83
- </DocLayoutWithDefaults>
84
- );
85
- }
21
+ export const VersionsPageView = createVersionsPageView({
22
+ settings,
23
+ defaultLocale,
24
+ t,
25
+ withBase,
26
+ composeMetaTitle,
27
+ components: {
28
+ HeadWithDefaults,
29
+ HeaderWithDefaults,
30
+ FooterWithDefaults,
31
+ BodyEndIslands,
32
+ },
33
+ });
@@ -1,169 +0,0 @@
1
- // @ts-check
2
- // Adapter from Connect-style middleware (`(req, res, next) => void`) to
3
- // the request-response shape zfb's `devMiddleware` lifecycle hook
4
- // expects (`(req: ZfbDevMiddlewareRequest) => Promise<ZfbDevMiddlewareResponse | undefined>`).
5
- //
6
- // zfb's plugin host runs in a separate Node subprocess and only sees a
7
- // JSON envelope of the request — `{ method, url, headers, body? }` —
8
- // not real Node IPC. The adapter mocks just enough of `IncomingMessage`
9
- // and `ServerResponse` for the v2 integration middlewares (which were
10
- // written against Node's `http` types) to think they are talking to a
11
- // regular HTTP server, then captures the response status / headers /
12
- // body and returns them in the shape the host expects.
13
- //
14
- // The adapter is shared by every plugin module under this directory so
15
- // any future Connect-style middleware can be wired into zfb's
16
- // devMiddleware hook without rewriting it. Lives at the host repo —
17
- // the v2 integration package keeps its Connect-style API surface so
18
- // non-zfb embedders (Astro, plain Vite, a unit test) continue to work.
19
-
20
- /** @import { ZfbDevMiddlewareRequest, ZfbDevMiddlewareResponse } from "@takazudo/zfb/plugins" */
21
-
22
- import { Buffer } from "node:buffer";
23
-
24
- /**
25
- * Connect-style middleware: `(req, res, next) => void`.
26
- * The req/res parameters are typed as `any` here because this adapter
27
- * intentionally passes a plain-object shim that structurally satisfies the
28
- * subset of `IncomingMessage` / `ServerResponse` that the v2 integration
29
- * middlewares actually access — not the full Node.js types.
30
- * @typedef {(req: any, res: any, next: (err?: unknown) => void) => void} ConnectMiddleware
31
- */
32
-
33
- /**
34
- * Convert a Connect-style middleware to a zfb devMiddleware handler.
35
- * The returned async function takes a `ZfbDevMiddlewareRequest` and
36
- * returns either `undefined` (passthrough — zfb falls through to its
37
- * built-in routes) or a `ZfbDevMiddlewareResponse` envelope.
38
- *
39
- * Behaviour:
40
- *
41
- * - `next()` from the middleware → resolves with `undefined`
42
- * (passthrough).
43
- * - `res.end(body)` → resolves with `{ status, headers, body }`.
44
- * `status` defaults to 200 if the middleware didn't set one,
45
- * mirroring Node's `ServerResponse` default.
46
- * - `next(err)` or a thrown error → rejects so the host surfaces a
47
- * 500 with the error message the same way it does for any other
48
- * plugin throw.
49
- * - Binary bodies (Buffer / Uint8Array) → encoded as base64 and
50
- * flagged `bodyEncoding: "base64"` so the JSON envelope round-trip
51
- * stays loss-less.
52
- *
53
- * @param {ConnectMiddleware} middleware
54
- * @returns {(zfbReq: ZfbDevMiddlewareRequest) => Promise<ZfbDevMiddlewareResponse | undefined>}
55
- */
56
- export function connectToZfbHandler(middleware) {
57
- return (zfbReq) => {
58
- return new Promise((resolveResponse, rejectResponse) => {
59
- // Build a minimal `IncomingMessage` shim. Only the fields the v2
60
- // integration middlewares actually read are populated — `method`,
61
- // `url`, and `headers`. Body parsing is not used by any of the
62
- // three middlewares (they're all GET routes), so we leave the
63
- // stream surface unimplemented.
64
- const req = {
65
- method: zfbReq.method,
66
- url: zfbReq.url,
67
- headers: zfbReq.headers ?? {},
68
- };
69
-
70
- // Build a `ServerResponse` shim that captures status, headers,
71
- // and body. We expose the API surface the v2 middlewares touch
72
- // today (`statusCode`, `setHeader`, `getHeader`, `end`) — extend
73
- // here if a future middleware needs more.
74
- let statusCode = 200;
75
- /** @type {Record<string, string>} */
76
- const headers = {};
77
- let settled = false;
78
-
79
- /**
80
- * @param {string | Buffer | Uint8Array | null | undefined} body
81
- * @returns {void}
82
- */
83
- const finish = (body) => {
84
- if (settled) return;
85
- settled = true;
86
- // Lower-case header names so the host's response shape matches
87
- // axum's expectation (`Record<string, string>` of arbitrary
88
- // case). Last-wins on collision; with `setHeader` callers this
89
- // shouldn't happen.
90
- /** @type {Record<string, string>} */
91
- const normalisedHeaders = {};
92
- for (const [k, v] of Object.entries(headers)) {
93
- normalisedHeaders[k.toLowerCase()] = String(v);
94
- }
95
-
96
- if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
97
- resolveResponse({
98
- status: statusCode,
99
- headers: normalisedHeaders,
100
- body: Buffer.from(body).toString("base64"),
101
- bodyEncoding: "base64",
102
- });
103
- return;
104
- }
105
- resolveResponse({
106
- status: statusCode,
107
- headers: normalisedHeaders,
108
- body: body == null ? "" : String(body),
109
- bodyEncoding: "utf8",
110
- });
111
- };
112
-
113
- const res = {
114
- get statusCode() {
115
- return statusCode;
116
- },
117
- /** @param {number} v */
118
- set statusCode(v) {
119
- statusCode = v;
120
- },
121
- /**
122
- * @param {string} name
123
- * @param {string | number | readonly string[]} value
124
- */
125
- setHeader(name, 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);
129
- },
130
- /** @param {string} name */
131
- getHeader(name) {
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()];
135
- },
136
- get headersSent() {
137
- return settled;
138
- },
139
- /** @param {string | Buffer | Uint8Array | null | undefined} [body] */
140
- end(body) {
141
- finish(body);
142
- },
143
- };
144
-
145
- /** @param {unknown} [err] */
146
- const next = (err) => {
147
- if (settled) return;
148
- if (err) {
149
- settled = true;
150
- rejectResponse(err instanceof Error ? err : new Error(String(err)));
151
- return;
152
- }
153
- // Connect's `next()` with no error means "I did not handle
154
- // this request". Resolve with `undefined` so zfb's host
155
- // surfaces a passthrough.
156
- settled = true;
157
- resolveResponse(undefined);
158
- };
159
-
160
- try {
161
- middleware(req, res, next);
162
- } catch (err) {
163
- if (settled) return;
164
- settled = true;
165
- rejectResponse(err instanceof Error ? err : new Error(String(err)));
166
- }
167
- });
168
- };
169
- }
@@ -1,66 +0,0 @@
1
- // @ts-check
2
- // zfb plugin module: search-index.
3
- //
4
- // Wires two lifecycle hooks for the search-index integration:
5
- //
6
- // postBuild — invokes `emitSearchIndex` to write `dist/search-index.json`.
7
- // There is no settings gate — the index is always emitted,
8
- // matching the legacy Astro behaviour.
9
- //
10
- // devMiddleware — rebuilds the in-memory search index from disk on every
11
- // request so authoring edits surface without a dev-server
12
- // restart. Registered at `/search-index.json`.
13
- //
14
- // `options` carries `{ docsDir, locales, base }` from the matching entry
15
- // in `zfb.config.ts`.
16
- //
17
- // Inline functions are not supported by zfb's plugin runtime; see the
18
- // sibling `doc-history-plugin.mjs` for the rationale.
19
-
20
- /** @import { ZfbBuildHookContext, ZfbDevMiddlewareContext, ZfbPlugin } from "@takazudo/zfb/plugins" */
21
- /** @import { SearchIndexBuildOptions, SearchIndexConfig } from "@takazudo/zudo-doc/integrations/search-index" */
22
-
23
- import { emitSearchIndex, createSearchIndexDevMiddleware } from "@takazudo/zudo-doc/integrations/search-index";
24
- import { connectToZfbHandler } from "./connect-adapter.mjs";
25
-
26
- /** @type {ZfbPlugin} */
27
- export default {
28
- name: "search-index",
29
-
30
- /** @param {ZfbBuildHookContext} ctx */
31
- async postBuild(ctx) {
32
- await emitSearchIndex(/** @type {SearchIndexBuildOptions} */ (/** @type {unknown} */ ({
33
- ...ctx.options,
34
- outDir: ctx.outDir,
35
- logger: ctx.logger,
36
- })));
37
- },
38
-
39
- /** @param {ZfbDevMiddlewareContext} ctx */
40
- devMiddleware(ctx) {
41
- const middleware = createSearchIndexDevMiddleware(
42
- /** @type {SearchIndexConfig} */ (/** @type {unknown} */ (ctx.options)),
43
- );
44
- // zfb's `register(path, handler)` matches against the FULL request
45
- // URL (no base-stripping). For a non-root base (e.g. "/my-docs/"),
46
- // requests arrive as `/my-docs/search-index.json`, so we register
47
- // the full base-prefixed route. For base="/", the prefix is empty
48
- // and the route is `/search-index.json` as expected. The v2
49
- // middleware itself is base-tolerant (matches via
50
- // `endsWith("/search-index.json")`), so it does not need a
51
- // separate base-stripping pass.
52
- const basePrefix = stripTrailingSlash(
53
- typeof ctx.options["base"] === "string" ? ctx.options["base"] : "",
54
- );
55
- ctx.register(`${basePrefix}/search-index.json`, connectToZfbHandler(middleware));
56
- },
57
- };
58
-
59
- /**
60
- * @param {string} s
61
- * @returns {string}
62
- */
63
- function stripTrailingSlash(s) {
64
- if (typeof s !== "string" || s.length === 0) return "";
65
- return s.endsWith("/") ? s.slice(0, -1) : s;
66
- }
@@ -1,157 +0,0 @@
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());