@takazudo/zudo-doc 5.13.1 → 5.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/bin/run-parallel.mjs +272 -0
  3. package/bin/tags-suggest-runner.ts +7 -3
  4. package/dist/asset-components/asset-card.d.ts +10 -0
  5. package/dist/asset-components/asset-card.js +91 -0
  6. package/dist/asset-components/asset-code.d.ts +10 -0
  7. package/dist/asset-components/asset-code.js +93 -0
  8. package/dist/asset-components/index.d.ts +3 -0
  9. package/dist/asset-components/index.js +12 -0
  10. package/dist/asset-components/shared.d.ts +25 -0
  11. package/dist/asset-components/shared.js +89 -0
  12. package/dist/asset-index-page/index.d.ts +21 -0
  13. package/dist/asset-index-page/index.js +175 -0
  14. package/dist/asset-index-page/script.d.ts +2 -0
  15. package/dist/asset-index-page/script.js +4 -0
  16. package/dist/asset-page/index.d.ts +67 -0
  17. package/dist/asset-page/index.js +244 -0
  18. package/dist/asset-page/script.d.ts +2 -0
  19. package/dist/asset-page/script.js +4 -0
  20. package/dist/asset-path/index.d.ts +53 -0
  21. package/dist/asset-path/index.js +149 -0
  22. package/dist/chrome/derive.js +45 -9
  23. package/dist/chrome/index.d.ts +2 -0
  24. package/dist/chrome/index.js +7 -1
  25. package/dist/chrome-bindings.d.ts +4 -0
  26. package/dist/client-router/raw-link-attrs.d.ts +8 -0
  27. package/dist/client-router/raw-link-attrs.js +6 -0
  28. package/dist/compiled.css +135 -0
  29. package/dist/config.d.ts +13 -2
  30. package/dist/config.js +36 -1
  31. package/dist/content/content-link.d.ts +12 -0
  32. package/dist/content/content-link.js +43 -2
  33. package/dist/content/index.d.ts +1 -1
  34. package/dist/content/index.js +5 -1
  35. package/dist/doc-body-end-islands/index.d.ts +2 -0
  36. package/dist/doc-body-end-islands/index.js +3 -2
  37. package/dist/doclayout/doc-layout-with-defaults.d.ts +1 -1
  38. package/dist/doclayout/doc-layout-with-defaults.js +1 -1
  39. package/dist/eject/index.js +8 -0
  40. package/dist/factory-context/index.d.ts +3 -1
  41. package/dist/features.css +124 -3
  42. package/dist/frontmatter/index.d.ts +56 -0
  43. package/dist/frontmatter/index.js +61 -0
  44. package/dist/header/header.js +11 -1
  45. package/dist/header/nav-class-tokens.d.ts +2 -0
  46. package/dist/header/nav-class-tokens.js +8 -0
  47. package/dist/header/nav-overflow-generated-script.js +47 -2
  48. package/dist/header-with-defaults/index.d.ts +2 -0
  49. package/dist/header-with-defaults/index.js +3 -2
  50. package/dist/i18n-defaults/index.d.ts +3 -2
  51. package/dist/i18n-defaults/index.js +44 -0
  52. package/dist/icons/index.d.ts +9 -0
  53. package/dist/icons/index.js +246 -0
  54. package/dist/md-utils/index.js +1 -1
  55. package/dist/mdx-components/index.d.ts +4 -1
  56. package/dist/mdx-components/index.js +80 -7
  57. package/dist/plugins/internal/asset-viewer/build.d.ts +25 -0
  58. package/dist/plugins/internal/asset-viewer/build.js +118 -0
  59. package/dist/plugins/internal/asset-viewer/git-meta.d.ts +7 -0
  60. package/dist/plugins/internal/asset-viewer/git-meta.js +40 -0
  61. package/dist/plugins/internal/asset-viewer/highlight.d.ts +34 -0
  62. package/dist/plugins/internal/asset-viewer/highlight.js +181 -0
  63. package/dist/plugins/internal/asset-viewer/link-graph.d.ts +34 -0
  64. package/dist/plugins/internal/asset-viewer/link-graph.js +193 -0
  65. package/dist/plugins/internal/asset-viewer/probe.d.ts +13 -0
  66. package/dist/plugins/internal/asset-viewer/probe.js +322 -0
  67. package/dist/plugins/internal/asset-viewer/scan.d.ts +2 -0
  68. package/dist/plugins/internal/asset-viewer/scan.js +77 -0
  69. package/dist/plugins/internal/asset-viewer/sidecar.d.ts +6 -0
  70. package/dist/plugins/internal/asset-viewer/sidecar.js +34 -0
  71. package/dist/plugins/internal/asset-viewer/types.d.ts +22 -0
  72. package/dist/plugins/internal/asset-viewer/types.js +0 -0
  73. package/dist/plugins/internal/codex-resources/skills.js +1 -1
  74. package/dist/plugins/internal/llms-txt/load.d.ts +1 -1
  75. package/dist/plugins/internal/resource-docs-shared/mdx.d.ts +1 -2
  76. package/dist/plugins/internal/resource-docs-shared/mdx.js +1 -1
  77. package/dist/plugins/routes.js +146 -11
  78. package/dist/preset.d.ts +10 -0
  79. package/dist/preset.js +20 -3
  80. package/dist/route-context/index.js +18 -2
  81. package/dist/route-context-payload/index.d.ts +6 -2
  82. package/dist/route-context-payload/index.js +2 -1
  83. package/dist/route-context-payload/types.d.ts +37 -0
  84. package/dist/routes/_chrome.d.ts +1 -1
  85. package/dist/routes/_chrome.js +5 -1
  86. package/dist/routes/_context.d.ts +2 -0
  87. package/dist/routes/_context.js +2 -0
  88. package/dist/routes/files-path.d.ts +22 -0
  89. package/dist/routes/files-path.js +31 -0
  90. package/dist/safelist.css +1 -1
  91. package/dist/settings.d.ts +10 -0
  92. package/dist/sidebar-tree-island/index.js +2 -2
  93. package/dist/site-tree-nav-island/index.js +3 -3
  94. package/dist/tags-audit.js +1 -1
  95. package/eject/asset-components/asset-card.tsx +106 -0
  96. package/eject/asset-components/asset-code.tsx +127 -0
  97. package/eject/asset-components/index.ts +7 -0
  98. package/eject/asset-components/shared.tsx +113 -0
  99. package/eject/header/gen-nav-overflow-script.mjs +53 -4
  100. package/eject/header/header.tsx +11 -1
  101. package/eject/header/nav-class-tokens.ts +12 -0
  102. package/eject/header/nav-overflow-generated-script.ts +1 -1
  103. package/eject/sidebar-tree-island/index.tsx +3 -3
  104. package/eject/site-tree-nav-island/index.tsx +4 -4
  105. package/package.json +34 -14
  106. package/routes-src/_chrome.tsx +2 -0
  107. package/routes-src/_context.ts +2 -0
  108. package/routes-src/_virtual.d.ts +6 -0
  109. package/routes-src/files-path.tsx +43 -0
  110. package/virtual-modules.d.ts +6 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ All notable changes to `@takazudo/zudo-doc` are documented in this file.
4
4
 
5
5
  The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
6
6
 
7
+ ## [5.14.0] - 2026-08-31
8
+
9
+ ### Features
10
+
11
+ - Added the configurable Asset Viewer for files under `public/assets/`, with type-specific viewer pages, raw/download links, linked-from context, and `assetViewerDir`, `assetViewerRoutePrefix`, and `assetViewerExclude` settings. (`45dc50cdb`, `19b8f0599`, `e6e77f790`)
12
+ - Added asset authoring entry points for markdown links, the `Asset` and `AssetCode` components, and image caption links, with URL-safe viewer paths and code excerpts. (`2b74f1da2`, `6f22ebb97`, `b883bff41`)
13
+ - Added the opt-in Asset Index at `/files/`, with nested native disclosures, file-kind icons, viewer links, file metadata, expansion persistence, and the `assetViewerIndex` setting. (`debdd3cf8`, `bbf10483b`, `177028439`)
14
+ - Added the package-owned `run-parallel` command for concurrent development tasks, with signal forwarding, descendant cleanup, and real failure-code propagation, replacing `npm-run-all2`. (`9402ff157`, `3ebfa6e9c`, `dab5d7e07`)
15
+ - Added public `@takazudo/zudo-doc/frontmatter` parse and stringify helpers backed by YAML 1.2 parsing and YAML 1.1-safe serialization, removing the `gray-matter` and `js-yaml` 3 dependency chain. (`c07422672`, `49cfb1895`, `bd8ad5be9`)
16
+
17
+ ### Bug Fixes
18
+
19
+ - Preserved the active navigation state when its header item moves into the overflow menu, while keeping exactly one visible `aria-current="page"`. (`81582a042`)
20
+ - Restored deliberate right gutters in tree navigation rows. (`fa6a2074d`)
21
+ - Retained package-default Asset Index translations when projects provide partial locale overrides. (`0b1958562`)
22
+ - Kept Asset Index disclosure semantics native and aligned leaf-file icons with the first control column. (`295b3d8d0`, `35356dad4`)
23
+
24
+ ### Other Changes
25
+
26
+ - Updated the zfb peer family to 2.14.0, `@takazudo/zdtp` to 0.4.14, and the doc-history-server peer floor to 5.13.1. (`dbe8553c7`, `038870c94`, `95a82f15f`)
27
+
7
28
  ## [5.13.1] - 2026-08-28
8
29
 
9
30
  ### Other Changes
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ // packages/zudo-doc/bin/run-parallel.mjs
3
+ //
4
+ // Runs several package.json scripts in parallel. Replaces the `run-p <task>...`
5
+ // form this project used from npm-run-all2, which was removed because it was the
6
+ // sole source of four security advisories (shell-quote quadratic-complexity DoS;
7
+ // brace-expansion@2 DoS x3) that nothing else in the tree pulled.
8
+ //
9
+ // Only the literal-name form is supported -- `run-parallel a b c`. npm-run-all2's
10
+ // globs (`dev:*`), flags, and `{@}` placeholders are deliberately NOT implemented;
11
+ // every call site here and in generated projects uses the plain form.
12
+ //
13
+ // Behaviour was checked against npm-run-all2@7.0.2 rather than assumed. It matches
14
+ // on the points that matter, and deviates on two, on purpose:
15
+ //
16
+ // MATCHED
17
+ // - A task exiting NON-ZERO aborts every sibling. A task exiting ZERO does not
18
+ // (that is run-p's `--race`, off by default).
19
+ // - A task killed by a signal reports 128 + signum, per the POSIX convention
20
+ // Node documents for exit codes -- non-zero, so it aborts too.
21
+ // - Only the FIRST failure is reported; siblings killed by the resulting
22
+ // teardown stay quiet.
23
+ // - The failure line matches run-p's wording, because packages/zudo-doc/CLAUDE.md
24
+ // (#3129) quotes it verbatim as the signature of the accepted cascade:
25
+ // ERROR: "dev:dts" exited with 1.
26
+ //
27
+ // DELIBERATELY DIFFERENT
28
+ // - Exit code: run-p ALWAYS exits 1 on failure. Its bin/common/bootstrap.js ends
29
+ // with `.then(() => process.exit(0), () => process.exit(1))`, discarding the
30
+ // code its own error object carries -- verified by running run-p 7.0.2 against
31
+ // a task exiting 2: it printed `exited with 2` and returned 1. This script
32
+ // propagates the real code instead, so a signal kill stays distinguishable
33
+ // (137 for SIGKILL) rather than flattening to 1. Both are non-zero, so the
34
+ // #3129 cascade behaves identically either way.
35
+ // - Trailing args: run-p silently swallowed them, which is why a separate
36
+ // `dev:network` script exists (#2940). Silently ignoring a flag the user
37
+ // clearly meant is exactly the "quiet lie" #3129 argues against, so this
38
+ // script fails loudly and names the remedy instead.
39
+ //
40
+ // The teardown cascade is the POINT, not a bug: root `pnpm dev` nests one of these
41
+ // inside another, so a fatal `dev:dts` exit takes down the whole dev session loudly
42
+ // rather than leaving a dead watcher emitting stale output. Do NOT add a
43
+ // --continue-on-error equivalent; #3129 rejects it explicitly (frozen .d.ts files
44
+ // typecheck cleanly against stale types).
45
+ //
46
+ // Two implementation choices that look incidental and are not:
47
+ //
48
+ // 1. Children are spawned WITHOUT `detached`, exactly as run-p did, so they stay
49
+ // in this process's group. That keeps terminal job control working and lets a
50
+ // child read the inherited TTY stdin: `zfb dev` is Vite-based and binds stdin
51
+ // for its keyboard shortcuts. A child in its own group is not the terminal's
52
+ // foreground group, so that same read raises SIGTTIN and stops it.
53
+ // 2. Teardown walks the full descendant tree rather than signalling just the
54
+ // direct child. `pnpm run x` sits between us and the real watcher and does not
55
+ // reliably forward SIGTERM, so signalling only the child strands it. Orphaned
56
+ // watchers are not hypothetical here -- they accumulate until `inotify_init`
57
+ // fails with EMFILE (see the #3129 section).
58
+
59
+ import { spawn, execFileSync } from "node:child_process";
60
+ import { readFileSync, readdirSync } from "node:fs";
61
+ import os from "node:os";
62
+ import path from "node:path";
63
+
64
+ // Platform-reported numbers rather than a hand-copied table, so the 128+signum
65
+ // arithmetic cannot drift from the host's actual signal set.
66
+ const SIGNAL_NUMBERS = os.constants.signals;
67
+
68
+ function usage(message) {
69
+ process.stderr.write(`ERROR: ${message}\n`);
70
+ process.stderr.write(
71
+ "Usage: run-parallel <script-name>...\n" +
72
+ "Only literal script names are supported: no globs, no flags, and no\n" +
73
+ "forwarded arguments. To pass flags to one script, run it directly:\n" +
74
+ " <npm|pnpm|yarn|bun> run <script> -- <flags>\n",
75
+ );
76
+ process.exit(1);
77
+ }
78
+
79
+ const tasks = process.argv.slice(2);
80
+
81
+ if (tasks.length === 0) {
82
+ usage("no scripts given.");
83
+ }
84
+ for (const task of tasks) {
85
+ // Reject anything resembling an npm-run-all2 feature that was not ported, so a
86
+ // stale `run-p --continue-on-error`, or a `pnpm dev -- --host 0.0.0.0` that used
87
+ // to be swallowed, fails loudly instead of being taken for a script name.
88
+ if (task.startsWith("-")) {
89
+ usage(`flags are not supported, got ${JSON.stringify(task)}.`);
90
+ }
91
+ if (task.includes("*")) {
92
+ usage(`glob patterns are not supported, got ${JSON.stringify(task)}.`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve the package-manager command used to run a script, mirroring how
98
+ * npm-run-all2 did it: prefer `npm_execpath` (set by every package manager while
99
+ * running a script), and run it through the current Node binary when it points at
100
+ * a JS file, since a `.cjs` shim is not directly executable everywhere.
101
+ */
102
+ function resolveRunner() {
103
+ const execpath = process.env.npm_execpath;
104
+ if (execpath) {
105
+ if (/\.(c|m)?js$/.test(path.extname(execpath))) {
106
+ return { command: process.execPath, prefix: [execpath, "run"] };
107
+ }
108
+ return { command: execpath, prefix: ["run"] };
109
+ }
110
+ // Fallback covers all four package managers create-zudo-doc can scaffold.
111
+ const agent = process.env.npm_config_user_agent ?? "";
112
+ const name =
113
+ ["pnpm", "yarn", "bun"].find((pm) => agent.startsWith(pm)) ?? "npm";
114
+ return { command: name, prefix: ["run"] };
115
+ }
116
+
117
+ /**
118
+ * Collect a process and all of its descendants, parents before children, so a
119
+ * caller can signal the whole tree. Reads /proc directly on Linux (no subprocess,
120
+ * and this runs on a teardown path where spawning is least welcome) and falls back
121
+ * to `ps` elsewhere, notably macOS.
122
+ */
123
+ function collectTree(rootPid) {
124
+ const childrenByParent = new Map();
125
+ const record = (pid, ppid) => {
126
+ if (!Number.isInteger(pid) || !Number.isInteger(ppid)) return;
127
+ const siblings = childrenByParent.get(ppid);
128
+ if (siblings) siblings.push(pid);
129
+ else childrenByParent.set(ppid, [pid]);
130
+ };
131
+
132
+ try {
133
+ if (process.platform === "linux") {
134
+ for (const entry of readdirSync("/proc")) {
135
+ if (!/^\d+$/.test(entry)) continue;
136
+ let stat;
137
+ try {
138
+ stat = readFileSync(`/proc/${entry}/stat`, "utf8");
139
+ } catch {
140
+ continue; // the process exited between readdir and read
141
+ }
142
+ // The comm field is parenthesised and may itself contain spaces or
143
+ // parentheses, so split after the LAST ')' rather than on whitespace.
144
+ const tail = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
145
+ record(Number(entry), Number(tail[1]));
146
+ }
147
+ } else {
148
+ const out = execFileSync("ps", ["-Ao", "pid=,ppid="], {
149
+ encoding: "utf8",
150
+ });
151
+ for (const line of out.split("\n")) {
152
+ const [pid, ppid] = line.trim().split(/\s+/);
153
+ record(Number(pid), Number(ppid));
154
+ }
155
+ }
156
+ } catch {
157
+ // Enumeration failed; fall through and signal just the root below.
158
+ }
159
+
160
+ const ordered = [];
161
+ const walk = (pid) => {
162
+ ordered.push(pid);
163
+ for (const child of childrenByParent.get(pid) ?? []) walk(child);
164
+ };
165
+ walk(rootPid);
166
+ return ordered;
167
+ }
168
+
169
+ const runner = resolveRunner();
170
+
171
+ /** @type {Map<string, import("node:child_process").ChildProcess>} */
172
+ const running = new Map();
173
+ let firstFailure = null;
174
+ let tearingDown = false;
175
+ let signalCount = 0;
176
+
177
+ function signalTree(child, signal) {
178
+ if (child.exitCode !== null || child.signalCode !== null) return;
179
+ // `pid` is undefined when the spawn itself failed. There is no tree to walk,
180
+ // and passing it through would print a bogus "could not signal pid undefined"
181
+ // line (or run `taskkill /pid undefined`) on an already-failing teardown.
182
+ if (child.pid === undefined) return;
183
+
184
+ if (process.platform === "win32") {
185
+ // Windows has no process groups to signal; taskkill /T walks the tree.
186
+ try {
187
+ execFileSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
188
+ stdio: "ignore",
189
+ });
190
+ } catch {
191
+ // Already gone, or not ours to kill. Nothing actionable on teardown.
192
+ }
193
+ return;
194
+ }
195
+
196
+ // collectTree yields parents before children; reversing signals the deepest
197
+ // descendants first, so an intermediate `pnpm run x` is not left briefly
198
+ // holding a still-live grandchild it does not forward signals to.
199
+ for (const pid of collectTree(child.pid).reverse()) {
200
+ try {
201
+ process.kill(pid, signal);
202
+ } catch (error) {
203
+ // ESRCH just means it already exited, which is the common case in a tree
204
+ // that is collapsing anyway. Anything else is worth a line, but must never
205
+ // mask the original failure by throwing here.
206
+ if (error.code !== "ESRCH") {
207
+ process.stderr.write(
208
+ `run-parallel: could not signal pid ${pid}: ${error.code ?? error.message}\n`,
209
+ );
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ function tearDown(signal = "SIGTERM") {
216
+ if (tearingDown) return;
217
+ tearingDown = true;
218
+ for (const child of running.values()) signalTree(child, signal);
219
+ }
220
+
221
+ for (const task of tasks) {
222
+ const child = spawn(runner.command, [...runner.prefix, task], {
223
+ stdio: "inherit",
224
+ // No `detached` -- see the header note. Children share this process group so
225
+ // terminal job control works and they can read the inherited TTY stdin.
226
+ });
227
+ running.set(task, child);
228
+
229
+ child.on("error", (error) => {
230
+ running.delete(task);
231
+ if (!firstFailure) {
232
+ firstFailure = { task, code: 1 };
233
+ process.stderr.write(
234
+ `ERROR: "${task}" failed to start: ${error.message}\n`,
235
+ );
236
+ }
237
+ tearDown();
238
+ if (running.size === 0) process.exitCode = firstFailure.code;
239
+ });
240
+
241
+ child.on("close", (code, signal) => {
242
+ running.delete(task);
243
+ const exitCode =
244
+ code === null ? 128 + (SIGNAL_NUMBERS[signal] ?? 0) : code;
245
+
246
+ if (exitCode !== 0 && !firstFailure) {
247
+ firstFailure = { task, code: exitCode };
248
+ process.stderr.write(`ERROR: "${task}" exited with ${exitCode}.\n`);
249
+ tearDown();
250
+ }
251
+
252
+ if (running.size === 0) {
253
+ process.exitCode = firstFailure ? firstFailure.code : 0;
254
+ }
255
+ });
256
+ }
257
+
258
+ // A terminal-generated Ctrl+C already reached the children directly, since they
259
+ // share this process group -- but a signal aimed at this pid alone did not, and
260
+ // without forwarding it that case would leave every child running. Re-signalling
261
+ // a child that is already dying is harmless (ESRCH is ignored above).
262
+ for (const name of ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"]) {
263
+ process.on(name, () => {
264
+ signalCount += 1;
265
+ // Escalate if a second signal arrives: something is refusing to shut down,
266
+ // and hanging here would strand exactly the watchers this script exists to
267
+ // reap. `tearingDown` is reset so the second pass is not short-circuited.
268
+ const forwarded = signalCount > 1 ? "SIGKILL" : name;
269
+ tearingDown = false;
270
+ tearDown(forwarded);
271
+ });
272
+ }
@@ -13,7 +13,11 @@ import { appendFile, readFile, writeFile } from "node:fs/promises";
13
13
  import { existsSync } from "node:fs";
14
14
  import { isAbsolute, relative, resolve } from "node:path";
15
15
  import { parseArgs } from "node:util";
16
- import matter from "gray-matter";
16
+ import {
17
+ matter,
18
+ stringify as stringifyFrontmatter,
19
+ type ParsedFrontmatter,
20
+ } from "@takazudo/zudo-doc/frontmatter";
17
21
 
18
22
  import type { TagVocabularyEntry } from "@takazudo/zudo-doc/settings";
19
23
  import {
@@ -266,11 +270,11 @@ function asStringArray(v: unknown): string[] {
266
270
 
267
271
  async function writeFrontmatterTags(
268
272
  filePath: string,
269
- parsed: matter.GrayMatterFile<string>,
273
+ parsed: ParsedFrontmatter,
270
274
  nextTags: string[],
271
275
  ): Promise<void> {
272
276
  const data = { ...parsed.data, tags: nextTags };
273
- const rebuilt = matter.stringify(parsed.content, data);
277
+ const rebuilt = stringifyFrontmatter(parsed.content, data);
274
278
  await writeFile(filePath, rebuilt, "utf-8");
275
279
  }
276
280
 
@@ -0,0 +1,10 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { JSX } from "preact";
4
+ import { type AssetComponentContext } from "./shared.js";
5
+ export interface AssetCardProps {
6
+ src: string;
7
+ description?: string;
8
+ title?: string;
9
+ }
10
+ export declare function createAssetCard(context: AssetComponentContext): ({ src, description, title, }: AssetCardProps) => JSX.Element | null;
@@ -0,0 +1,91 @@
1
+ import { jsx, jsxs } from "preact/jsx-runtime";
2
+ import { assetRawHref, assetViewerHref } from "../asset-path/index.js";
3
+ import {
4
+ AssetFileIcon,
5
+ assetComponentText,
6
+ formatAssetBytes,
7
+ formatAssetLanguage,
8
+ MissingAssetWarning,
9
+ resolveAssetEntry
10
+ } from "./shared.js";
11
+ function createAssetCard(context) {
12
+ return function AssetCard({
13
+ src,
14
+ description,
15
+ title
16
+ }) {
17
+ if (context.assetManifest === null) return null;
18
+ const resolved = resolveAssetEntry(src, context);
19
+ if (!resolved) {
20
+ return /* @__PURE__ */ jsx(MissingAssetWarning, { children: "Asset not found in the asset manifest" });
21
+ }
22
+ const { path, entry } = resolved;
23
+ const viewerHref = assetViewerHref({
24
+ base: context.base,
25
+ routePrefix: context.routePrefix,
26
+ path
27
+ });
28
+ const rawHref = assetRawHref({ base: context.base, dir: context.dir, path });
29
+ const details = [
30
+ formatAssetLanguage(entry.language),
31
+ formatAssetBytes(entry.bytes),
32
+ entry.lines === void 0 ? void 0 : assetComponentText(
33
+ context,
34
+ "asset.lines",
35
+ "{count} lines",
36
+ { count: entry.lines }
37
+ )
38
+ ].filter((value) => value !== void 0);
39
+ const finalDescription = description ?? entry.description;
40
+ return /* @__PURE__ */ jsx("article", { className: "rounded-lg border border-muted bg-surface px-hsp-lg py-vsp-sm", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-x-hsp-md", children: [
41
+ /* @__PURE__ */ jsx("span", { className: "flex h-icon-lg w-icon-lg shrink-0 items-center justify-center text-muted", children: /* @__PURE__ */ jsx(AssetFileIcon, { className: "h-icon-lg w-icon-lg" }) }),
42
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
43
+ /* @__PURE__ */ jsxs("div", { className: "font-mono text-small text-fg", children: [
44
+ entry.dir ? /* @__PURE__ */ jsxs("span", { className: "text-muted", children: [
45
+ entry.dir,
46
+ "/"
47
+ ] }) : null,
48
+ /* @__PURE__ */ jsx("strong", { children: title ?? entry.name })
49
+ ] }),
50
+ /* @__PURE__ */ jsx("div", { className: "mt-vsp-3xs text-caption text-muted", children: details.join(" \xB7 ") }),
51
+ finalDescription ? /* @__PURE__ */ jsx("p", { className: "mt-vsp-xs text-small text-muted", children: finalDescription }) : null,
52
+ /* @__PURE__ */ jsxs("div", { className: "mt-vsp-xs flex gap-x-hsp-lg text-caption", children: [
53
+ /* @__PURE__ */ jsxs(
54
+ "a",
55
+ {
56
+ className: "text-accent hover:underline focus-visible:underline",
57
+ href: viewerHref,
58
+ children: [
59
+ assetComponentText(
60
+ context,
61
+ "asset.viewFullFile",
62
+ "View full file"
63
+ ),
64
+ " \u2192"
65
+ ]
66
+ }
67
+ ),
68
+ /* @__PURE__ */ jsxs(
69
+ "a",
70
+ {
71
+ className: "text-accent hover:underline focus-visible:underline",
72
+ href: rawHref,
73
+ download: true,
74
+ children: [
75
+ assetComponentText(
76
+ context,
77
+ "asset.download",
78
+ "Download"
79
+ ),
80
+ " \u2192"
81
+ ]
82
+ }
83
+ )
84
+ ] })
85
+ ] })
86
+ ] }) });
87
+ };
88
+ }
89
+ export {
90
+ createAssetCard
91
+ };
@@ -0,0 +1,10 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { JSX } from "preact";
4
+ import { type AssetComponentContext } from "./shared.js";
5
+ export interface AssetCodeProps {
6
+ src: string;
7
+ lines?: string;
8
+ title?: string;
9
+ }
10
+ export declare function createAssetCode(context: AssetComponentContext): ({ src, lines, title, }: AssetCodeProps) => JSX.Element | null;
@@ -0,0 +1,93 @@
1
+ import { jsx, jsxs } from "preact/jsx-runtime";
2
+ import { assetViewerHref } from "../asset-path/index.js";
3
+ import {
4
+ AssetFileIcon,
5
+ assetComponentText,
6
+ MissingAssetWarning,
7
+ resolveAssetEntry
8
+ } from "./shared.js";
9
+ function parseRequestedLines(value) {
10
+ if (value === void 0) return null;
11
+ const match = /^(\d+)(?:-(\d*))?$/.exec(value);
12
+ if (!match) return null;
13
+ const start = Number(match[1]);
14
+ if (!Number.isSafeInteger(start) || start < 1) return null;
15
+ if (match[2] === void 0) return { start, end: start };
16
+ if (match[2] === "") return { start };
17
+ const end = Number(match[2]);
18
+ if (!Number.isSafeInteger(end) || end < 1) return null;
19
+ return { start, end };
20
+ }
21
+ function createAssetCode(context) {
22
+ return function AssetCode({
23
+ src,
24
+ lines,
25
+ title
26
+ }) {
27
+ if (context.assetManifest === null) return null;
28
+ const resolved = resolveAssetEntry(src, context);
29
+ const requested = parseRequestedLines(lines);
30
+ if (!resolved || !requested) {
31
+ return /* @__PURE__ */ jsx(MissingAssetWarning, { children: "Excerpt not built \u2014 check the `lines` attribute" });
32
+ }
33
+ const requestedEnd = requested.end ?? resolved.entry.lines;
34
+ if (requestedEnd === void 0) {
35
+ return /* @__PURE__ */ jsx(MissingAssetWarning, { children: "Excerpt not built \u2014 check the `lines` attribute" });
36
+ }
37
+ const key = `${resolved.path}#${requested.start}-${requestedEnd}`;
38
+ const excerpt = context.assetManifest.excerpts[key];
39
+ if (!excerpt) {
40
+ return /* @__PURE__ */ jsx(MissingAssetWarning, { children: "Excerpt not built \u2014 check the `lines` attribute" });
41
+ }
42
+ const viewerHref = assetViewerHref({
43
+ base: context.base,
44
+ routePrefix: context.routePrefix,
45
+ path: resolved.path,
46
+ fragment: excerpt.viewerLineAvailable ? `L${excerpt.startLine}` : void 0
47
+ });
48
+ const shown = Math.max(0, excerpt.endLine - excerpt.startLine + 1);
49
+ const rangeLabel = requested.end === void 0 ? `lines ${requested.start}\u2013end` : requested.start === requested.end ? `line ${requested.start}` : `lines ${requested.start}\u2013${requested.end}`;
50
+ return /* @__PURE__ */ jsxs("section", { className: "overflow-hidden rounded-lg border border-muted bg-surface", children: [
51
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between gap-x-hsp-md border-b border-muted px-hsp-lg py-vsp-2xs text-caption", children: [
52
+ /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-x-hsp-xs font-mono text-fg", children: [
53
+ /* @__PURE__ */ jsx(AssetFileIcon, { className: "h-icon-sm w-icon-sm shrink-0" }),
54
+ /* @__PURE__ */ jsx("span", { children: title ?? resolved.path })
55
+ ] }),
56
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 text-muted", children: rangeLabel })
57
+ ] }),
58
+ /* @__PURE__ */ jsx(
59
+ "div",
60
+ {
61
+ className: "overflow-x-auto bg-code-bg text-code-fg text-caption",
62
+ dangerouslySetInnerHTML: { __html: excerpt.html }
63
+ }
64
+ ),
65
+ /* @__PURE__ */ jsxs("footer", { className: "flex flex-wrap items-center justify-between gap-x-hsp-md gap-y-vsp-3xs border-t border-muted px-hsp-lg py-vsp-2xs text-caption text-muted", children: [
66
+ /* @__PURE__ */ jsx("span", { children: assetComponentText(
67
+ context,
68
+ "asset.showingLines",
69
+ "Showing {shown} of {total} lines",
70
+ { shown, total: excerpt.totalLines }
71
+ ) }),
72
+ /* @__PURE__ */ jsxs(
73
+ "a",
74
+ {
75
+ className: "text-accent hover:underline focus-visible:underline",
76
+ href: viewerHref,
77
+ children: [
78
+ assetComponentText(
79
+ context,
80
+ "asset.viewFullFile",
81
+ "View full file"
82
+ ),
83
+ " \u2192"
84
+ ]
85
+ }
86
+ )
87
+ ] })
88
+ ] });
89
+ };
90
+ }
91
+ export {
92
+ createAssetCode
93
+ };
@@ -0,0 +1,3 @@
1
+ export { createAssetCard, type AssetCardProps } from "./asset-card.js";
2
+ export { createAssetCode, type AssetCodeProps } from "./asset-code.js";
3
+ export { AssetFileIcon, formatAssetBytes, type AssetComponentContext, } from "./shared.js";
@@ -0,0 +1,12 @@
1
+ import { createAssetCard } from "./asset-card.js";
2
+ import { createAssetCode } from "./asset-code.js";
3
+ import {
4
+ AssetFileIcon,
5
+ formatAssetBytes
6
+ } from "./shared.js";
7
+ export {
8
+ AssetFileIcon,
9
+ createAssetCard,
10
+ createAssetCode,
11
+ formatAssetBytes
12
+ };
@@ -0,0 +1,25 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { JSX } from "preact";
4
+ import type { AssetIndexEntry, AssetManifest } from "../route-context-payload/types.js";
5
+ export interface AssetComponentContext {
6
+ base: string;
7
+ assetManifest: AssetManifest | null;
8
+ routePrefix: string;
9
+ dir: string;
10
+ /** Locale-bound package translator. Omitted callers retain English fallbacks. */
11
+ t?: (key: string) => string;
12
+ }
13
+ export declare function assetComponentText(context: AssetComponentContext, key: string, fallback: string, values?: Record<string, string | number>): string;
14
+ export declare function resolveAssetEntry(src: string, context: AssetComponentContext): {
15
+ path: string;
16
+ entry: AssetIndexEntry;
17
+ } | null;
18
+ export declare function formatAssetBytes(bytes: number): string;
19
+ export declare function formatAssetLanguage(language: string | undefined): string;
20
+ export declare function AssetFileIcon({ className, }: {
21
+ className?: string;
22
+ }): JSX.Element;
23
+ export declare function MissingAssetWarning({ children, }: {
24
+ children: string;
25
+ }): JSX.Element;
@@ -0,0 +1,89 @@
1
+ import { jsx, jsxs } from "preact/jsx-runtime";
2
+ import { decodeAuthoredHref } from "../asset-path/index.js";
3
+ function assetComponentText(context, key, fallback, values = {}) {
4
+ let text = context.t?.(key) ?? fallback;
5
+ for (const [name, value] of Object.entries(values)) {
6
+ text = text.replace(`{${name}}`, String(value));
7
+ }
8
+ return text;
9
+ }
10
+ function resolveAssetEntry(src, context) {
11
+ if (context.assetManifest === null) return null;
12
+ const decoded = decodeAuthoredHref(src, {
13
+ base: context.base,
14
+ dir: context.dir
15
+ });
16
+ if (!decoded) return null;
17
+ const entry = context.assetManifest.entries.find(
18
+ (candidate) => candidate.path === decoded.path
19
+ );
20
+ return entry ? { path: decoded.path, entry } : null;
21
+ }
22
+ function formatAssetBytes(bytes) {
23
+ if (!Number.isFinite(bytes) || bytes < 1e3) {
24
+ return `${Math.max(0, Math.trunc(bytes))} B`;
25
+ }
26
+ const units = ["KB", "MB", "GB"];
27
+ let value = bytes / 1e3;
28
+ let unit = units[0];
29
+ for (let index = 1; value >= 1e3 && index < units.length; index += 1) {
30
+ value /= 1e3;
31
+ unit = units[index];
32
+ }
33
+ return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${unit}`;
34
+ }
35
+ function formatAssetLanguage(language) {
36
+ if (!language) return "File";
37
+ const known = {
38
+ javascript: "JavaScript",
39
+ typescript: "TypeScript",
40
+ jsx: "JSX",
41
+ tsx: "TSX",
42
+ json: "JSON",
43
+ html: "HTML",
44
+ css: "CSS",
45
+ markdown: "Markdown"
46
+ };
47
+ return known[language.toLowerCase()] ?? language;
48
+ }
49
+ function AssetFileIcon({
50
+ className
51
+ }) {
52
+ return /* @__PURE__ */ jsxs(
53
+ "svg",
54
+ {
55
+ className,
56
+ viewBox: "0 0 24 24",
57
+ fill: "none",
58
+ stroke: "currentColor",
59
+ "stroke-width": "2",
60
+ "stroke-linecap": "round",
61
+ "stroke-linejoin": "round",
62
+ "aria-hidden": "true",
63
+ children: [
64
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
65
+ /* @__PURE__ */ jsx("path", { d: "M14 2v6h6" })
66
+ ]
67
+ }
68
+ );
69
+ }
70
+ function MissingAssetWarning({
71
+ children
72
+ }) {
73
+ return /* @__PURE__ */ jsx(
74
+ "div",
75
+ {
76
+ className: "rounded border border-warning/30 bg-warning/5 px-hsp-lg py-vsp-xs text-small text-warning",
77
+ role: "status",
78
+ children
79
+ }
80
+ );
81
+ }
82
+ export {
83
+ AssetFileIcon,
84
+ MissingAssetWarning,
85
+ assetComponentText,
86
+ formatAssetBytes,
87
+ formatAssetLanguage,
88
+ resolveAssetEntry
89
+ };