@takazudo/zudo-doc 5.24.1 → 5.25.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 (55) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/asset-components/asset-card.js +2 -2
  3. package/dist/asset-components/asset-code.js +1 -1
  4. package/dist/asset-index-page/body.d.ts +80 -0
  5. package/dist/asset-index-page/body.js +116 -0
  6. package/dist/asset-index-page/index.d.ts +3 -9
  7. package/dist/asset-index-page/index.js +18 -149
  8. package/dist/asset-index-page/tree.d.ts +24 -0
  9. package/dist/asset-index-page/tree.js +43 -0
  10. package/dist/asset-page/body.d.ts +98 -0
  11. package/dist/asset-page/body.js +107 -0
  12. package/dist/asset-page/components.d.ts +94 -0
  13. package/dist/asset-page/components.js +175 -0
  14. package/dist/asset-page/index.d.ts +3 -78
  15. package/dist/asset-page/index.js +30 -240
  16. package/dist/asset-page/shared.d.ts +15 -0
  17. package/dist/asset-page/shared.js +28 -0
  18. package/dist/breadcrumb/breadcrumb.js +1 -1
  19. package/dist/compiled.css +15 -40
  20. package/dist/config/component-tokens.js +3 -1
  21. package/dist/config.d.ts +10 -0
  22. package/dist/config.js +1 -0
  23. package/dist/doc-history/index.js +6 -1
  24. package/dist/doc-pager/index.js +2 -2
  25. package/dist/footer/footer.js +3 -3
  26. package/dist/header-with-defaults/index.d.ts +20 -0
  27. package/dist/header-with-defaults/index.js +9 -2
  28. package/dist/home-page/index.js +48 -21
  29. package/dist/i18n-defaults/index.js +3 -0
  30. package/dist/math-block/index.js +11 -1
  31. package/dist/mdx-components/index.js +1 -1
  32. package/dist/nav-indexing/category-nav.js +1 -1
  33. package/dist/nav-indexing/category-tree-nav.js +1 -1
  34. package/dist/nav-indexing/doc-card-grid.js +2 -2
  35. package/dist/nav-indexing/docs-sitemap.js +2 -2
  36. package/dist/nav-indexing/nav-card-grid.d.ts +3 -2
  37. package/dist/nav-indexing/nav-card-grid.js +3 -3
  38. package/dist/nav-indexing/note-tray-index-parts/card-list.js +1 -1
  39. package/dist/nav-indexing/note-tray-index-parts/index-list.js +1 -1
  40. package/dist/nav-indexing/note-tray-index-parts/timeline.js +1 -1
  41. package/dist/nav-indexing/site-tree-nav-demo.js +2 -2
  42. package/dist/nav-indexing/versions-page-content.js +2 -2
  43. package/dist/plugins/routes.js +2 -13
  44. package/dist/safelist.css +1 -1
  45. package/dist/settings.d.ts +2 -0
  46. package/dist/site-tree-nav-island/index.js +3 -3
  47. package/dist/theme-packs/foundry/pack.css +4 -9
  48. package/eject/asset-components/asset-card.tsx +2 -2
  49. package/eject/asset-components/asset-code.tsx +1 -1
  50. package/eject/breadcrumb/breadcrumb.tsx +1 -1
  51. package/eject/doc-history/index.tsx +10 -2
  52. package/eject/doc-pager/index.tsx +2 -2
  53. package/eject/footer/footer.tsx +4 -4
  54. package/eject/site-tree-nav-island/index.tsx +4 -4
  55. package/package.json +37 -9
package/CHANGELOG.md CHANGED
@@ -8,6 +8,31 @@ The format is based on Keep a Changelog, and release notes are generated from th
8
8
 
9
9
  No unreleased changes.
10
10
 
11
+ ## [5.25.0] - 2026-09-16
12
+
13
+ ### Features
14
+
15
+ - Added a `siteTreeNavSecondary` setting: top-level categories named here move out of the home page's card grid into a compact secondary link row, and the "See all tags" link was restyled to match it. There is no CLI or prompt surface yet — set it by hand in your config after scaffolding. (42bd642c5)
16
+ - Added an optional `sidebarNodes` prop to `HeaderWithDefaultsProps`, letting a host override the mobile-drawer sidebar tree per render (as a fixed array or a callback with a lazy `buildDefault()`) without replacing the whole `Header` chrome component. (01a7d088d)
17
+ - Added zfb-free asset-viewer subpaths — `./asset-page/{body,components,script}`, `./asset-index-page`, and `./asset-index-page/{body,tree,script}` — so `AssetPageBody` / `AssetIndexPageBody` and their `resolveAsset*Labels(t, locale)` helpers can be imported and rendered without a `ChromeContext` or any `@takazudo/zfb*` package. `./asset-page` and `./asset-index-page` remain the zfb-bound factories; neither body provides `DocLayout`, so the caller owns the outer page shell. (558aa58cf)
18
+ - Applied the sitewide link-color rule across package chrome, so link styling is now consistent everywhere instead of varying per component. Prose links stay accent + underline; every other link sits at `text-fg` (or `text-muted` in tertiary chrome) and turns accent on hover and focus, with static underlines removed outside prose and tertiary inline rows. This covers the footer, breadcrumb, and DocPager (ffef68952), nav-indexing lists, note-tray parts, and site-tree-nav focus (4ed4e2f7d), nav-indexing card grids (224232644), and the asset controls and image-caption viewer link (87ee6c1d5). Expect visible link-color and underline changes if you relied on the previous per-component styling.
19
+
20
+ ### Bug Fixes
21
+
22
+ - The asset page no longer reuses a singleton prepaint-script VNode across SSR passes, which could let one render's prepaint state leak into another. (9bc892343)
23
+ - The `foundry` theme pack keeps its footer copyright underline, which the link-color rule work had removed. (dcec0daeb)
24
+
25
+ ### Other Changes
26
+
27
+ - Removed the `.zudo-doc/routes-src` project-local staging step for package-owned route injection. It was a workaround for zfb not resolving `virtual:*` imports from route files under `node_modules`; zfb resolves those imports directly now, so the published `routes-src/` file is injected without a staging copy. If your project has a leftover `<project>/.zudo-doc/` directory, it is now inert and can be deleted. (c56b24342)
28
+ - The `@takazudo/zfb`, `@takazudo/zfb-runtime`, and `@takazudo/zfb-md-wasm` peer dependency floors are now `^2.18.0` (e4d4b64b3). That release reworks package-injected routes and plugin virtual modules — the islands scanner resolves plugin virtual modules and is seeded with package-route entrypoints, dev CSS content globs are seeded from injected routes, and injected-route entrypoints are staged as exact files that SSR virtual modules may import. It changes no API, export, or config default, so existing projects need no migration beyond having zfb 2.18.0 installed.
29
+
30
+ ## [5.24.2] - 2026-09-15
31
+
32
+ ### Bug Fixes
33
+
34
+ - `katex` and `diff` are optional peers again in practice: a site with `math` and `docHistory` off now builds without either installed. Both are loaded through a rejection-handled dynamic import instead of a static or unguarded import that failed with `Could not resolve "katex"` / `Could not resolve "diff"` under `packageOwnedRoutes`. Rendering `` without `katex` installed throws a clear error telling you to install it and set `math: true`. (72f36d027)
35
+
11
36
  ## [5.24.1] - 2026-09-15
12
37
 
13
38
  ### Bug Fixes
@@ -55,7 +55,7 @@ function createAssetCard(context) {
55
55
  /* @__PURE__ */ jsxs(
56
56
  "a",
57
57
  {
58
- className: "text-accent hover:underline focus-visible:underline",
58
+ className: "text-fg hover:text-accent focus-visible:text-accent hover:underline focus-visible:underline",
59
59
  href: viewerHref,
60
60
  children: [
61
61
  assetComponentText(
@@ -70,7 +70,7 @@ function createAssetCard(context) {
70
70
  /* @__PURE__ */ jsxs(
71
71
  "a",
72
72
  {
73
- className: "text-accent hover:underline focus-visible:underline",
73
+ className: "text-fg hover:text-accent focus-visible:text-accent hover:underline focus-visible:underline",
74
74
  href: rawHref,
75
75
  download: true,
76
76
  children: [
@@ -74,7 +74,7 @@ function createAssetCode(context) {
74
74
  /* @__PURE__ */ jsxs(
75
75
  "a",
76
76
  {
77
- className: "text-accent hover:underline focus-visible:underline",
77
+ className: "text-fg hover:text-accent focus-visible:text-accent hover:underline focus-visible:underline",
78
78
  href: viewerHref,
79
79
  children: [
80
80
  assetComponentText(
@@ -0,0 +1,80 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { ComponentType, VNode } from "preact";
4
+ import type { AssetIndexEntry } from "../route-context-payload/types.js";
5
+ import { type AssetTreeNode } from "./tree.js";
6
+ /**
7
+ * Localized strings `AssetIndexPageBody` needs to render, resolved ahead of
8
+ * time (typically via `resolveAssetIndexPageLabels`) so the body needs no
9
+ * `t()`/`ctx` of its own.
10
+ *
11
+ * `fileCount`, `fileCountSingle`, `folderCount`, `folderCountSingle`, and
12
+ * `lines` are raw translation templates and still carry the literal
13
+ * `"{count}"` placeholder — they are NOT pre-expanded. The body substitutes
14
+ * the placeholder itself (via `countLabel` for the two counts, and per-file
15
+ * for `lines`) because the substituted count depends on the specific node/
16
+ * asset being rendered, not on locale alone. Every other field below is an
17
+ * already-resolved, ready-to-render string.
18
+ */
19
+ export interface AssetIndexPageLabels {
20
+ /** Page/crumb title (e.g. "Assets") — doubles as the eyebrow badge and `<h1>` text. */
21
+ crumb: string;
22
+ /** Secondary eyebrow badge, e.g. "Index". */
23
+ indexBadge: string;
24
+ /** Description shown under the title. */
25
+ indexDescription: string;
26
+ /** Raw `asset.fileCount` template — see the interface doc comment above. */
27
+ fileCount: string;
28
+ /** Raw `asset.fileCountSingle` template. */
29
+ fileCountSingle: string;
30
+ /** Raw `asset.folderCount` template. */
31
+ folderCount: string;
32
+ /** Raw `asset.folderCountSingle` template. */
33
+ folderCountSingle: string;
34
+ expandAll: string;
35
+ collapseAll: string;
36
+ indexEmpty: string;
37
+ /** Raw `asset.lines` template. */
38
+ lines: string;
39
+ }
40
+ /**
41
+ * Builds `AssetIndexPageLabels` from a `t(key, locale)` translator, bound to
42
+ * the given `locale` — NOT the default locale, so a localized page gets its
43
+ * own strings even when `t` itself falls back internally.
44
+ */
45
+ export declare function resolveAssetIndexPageLabels(t: (key: string, locale?: string) => string, locale: string): AssetIndexPageLabels;
46
+ export declare function iconFor(asset: AssetIndexEntry): ComponentType<{
47
+ className?: string;
48
+ }>;
49
+ export declare function AssetTree({ node, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel, root }: {
50
+ node: AssetTreeNode;
51
+ base: string;
52
+ routePrefix: string;
53
+ locale?: string;
54
+ fileCountLabel: string;
55
+ fileCountSingleLabel: string;
56
+ linesLabel: string;
57
+ root?: boolean;
58
+ }): VNode;
59
+ export interface AssetIndexPageBodyProps {
60
+ /** Every asset entry in the index. */
61
+ entries: AssetIndexEntry[];
62
+ /** Localized strings — see `resolveAssetIndexPageLabels()`. */
63
+ labels: AssetIndexPageLabels;
64
+ /** Resolved `settings.base`, forwarded to file link hrefs. */
65
+ base: string;
66
+ /** Resolved asset-viewer route prefix, forwarded to file link hrefs. */
67
+ routePrefix: string;
68
+ /** Asset-viewer source directory (e.g. `assets`), rendered verbatim as `public/${dir}/`. */
69
+ dir: string;
70
+ /** Locale segment (undefined for the default locale), forwarded to file link hrefs. */
71
+ locale?: string;
72
+ }
73
+ /**
74
+ * Renders the asset index page body — header, file/folder counts, toolbar,
75
+ * the nested tree (or empty state), and the inline bootstrap script —
76
+ * exactly what `createAssetIndexPageView` placed inline as
77
+ * `DocLayoutWithDefaults`'s children before the #4223 split. Plain props
78
+ * only, no `ChromeContext`.
79
+ */
80
+ export declare function AssetIndexPageBody({ entries, labels, base, routePrefix, dir, locale }: AssetIndexPageBodyProps): VNode;
@@ -0,0 +1,116 @@
1
+ import { jsx, jsxs } from "preact/jsx-runtime";
2
+ import { formatAssetBytes } from "../asset-components/index.js";
3
+ import { assetViewerHref } from "../asset-path/index.js";
4
+ import { facetLabel, kindLabel } from "../asset-page/shared.js";
5
+ import {
6
+ ChevronRight,
7
+ FileArchive,
8
+ FileCode,
9
+ FileGeneric,
10
+ FileImage,
11
+ FilePdf,
12
+ FileText,
13
+ FileVideo,
14
+ Folder,
15
+ FolderOpen
16
+ } from "../icons/index.js";
17
+ import { ASSET_INDEX_PAGE_SCRIPT } from "./script.js";
18
+ import { basename, buildAssetTree, countLabel, folderCount } from "./tree.js";
19
+ const ARCHIVE_EXTENSIONS = /* @__PURE__ */ new Set(["zip", "tar", "gz", "tgz", "7z", "rar"]);
20
+ function resolveAssetIndexPageLabels(t, locale) {
21
+ return {
22
+ crumb: t("asset.crumb", locale),
23
+ indexBadge: t("asset.indexBadge", locale),
24
+ indexDescription: t("asset.indexDescription", locale),
25
+ fileCount: t("asset.fileCount", locale),
26
+ fileCountSingle: t("asset.fileCountSingle", locale),
27
+ folderCount: t("asset.folderCount", locale),
28
+ folderCountSingle: t("asset.folderCountSingle", locale),
29
+ expandAll: t("asset.expandAll", locale),
30
+ collapseAll: t("asset.collapseAll", locale),
31
+ indexEmpty: t("asset.indexEmpty", locale),
32
+ lines: t("asset.lines", locale)
33
+ };
34
+ }
35
+ function iconFor(asset) {
36
+ const extension = basename(asset.path).split(".").at(-1)?.toLowerCase();
37
+ if (asset.kind === "other" && extension && ARCHIVE_EXTENSIONS.has(extension)) return FileArchive;
38
+ const icons = {
39
+ code: FileCode,
40
+ text: FileText,
41
+ image: FileImage,
42
+ video: FileVideo,
43
+ pdf: FilePdf,
44
+ other: FileGeneric
45
+ };
46
+ return icons[asset.kind];
47
+ }
48
+ function AssetTree({ node, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel, root = false }) {
49
+ return /* @__PURE__ */ jsxs("ul", { "data-zd-asset-tree": root ? true : void 0, children: [
50
+ node.dirs.map((dir) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("details", { open: true, children: [
51
+ /* @__PURE__ */ jsxs("summary", { class: "flex cursor-pointer items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small hover:bg-accent/10 focus-visible:bg-accent/10", children: [
52
+ /* @__PURE__ */ jsx(ChevronRight, { className: "h-icon-xs w-icon-xs shrink-0 text-muted transition-transform" }),
53
+ /* @__PURE__ */ jsx(Folder, { className: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
54
+ /* @__PURE__ */ jsx(FolderOpen, { className: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
55
+ /* @__PURE__ */ jsxs("span", { class: "min-w-0 truncate font-mono text-fg", children: [
56
+ dir.name,
57
+ "/"
58
+ ] }),
59
+ /* @__PURE__ */ jsxs("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block", children: [
60
+ countLabel(dir.fileCount, fileCountLabel, fileCountSingleLabel),
61
+ " \xB7 ",
62
+ formatAssetBytes(dir.bytes)
63
+ ] })
64
+ ] }),
65
+ /* @__PURE__ */ jsx(AssetTree, { node: dir, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel })
66
+ ] }) })),
67
+ node.files.map((asset) => {
68
+ const Icon = iconFor(asset);
69
+ const facet = facetLabel(asset, asset.lines !== void 0 ? linesLabel.replace("{count}", String(asset.lines)) : void 0);
70
+ const meta = [kindLabel(asset), facet, formatAssetBytes(asset.bytes)].filter(Boolean).join(" \xB7 ");
71
+ return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: assetViewerHref({ base, routePrefix, path: asset.path, locale }), title: asset.name, class: "flex min-w-0 items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small text-fg hover:bg-accent/10 hover:text-accent hover:underline focus-visible:bg-accent/10 focus-visible:text-accent focus-visible:underline", children: [
72
+ /* @__PURE__ */ jsx(Icon, { className: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
73
+ /* @__PURE__ */ jsx("span", { class: "min-w-0 truncate font-mono", children: basename(asset.path) }),
74
+ /* @__PURE__ */ jsx("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block", children: meta })
75
+ ] }) });
76
+ })
77
+ ] });
78
+ }
79
+ function AssetIndexPageBody({ entries, labels, base, routePrefix, dir, locale }) {
80
+ const tree = buildAssetTree(entries);
81
+ const folders = folderCount(tree);
82
+ return /* @__PURE__ */ jsxs("div", { "data-zd-asset-index-page": true, children: [
83
+ /* @__PURE__ */ jsxs("header", { children: [
84
+ /* @__PURE__ */ jsxs("div", { class: "mb-vsp-xs flex flex-wrap items-center gap-hsp-xs text-micro tracking-wide uppercase", children: [
85
+ /* @__PURE__ */ jsx("span", { class: "rounded-full border border-muted px-hsp-sm py-vsp-3xs text-fg", children: labels.crumb }),
86
+ /* @__PURE__ */ jsx("span", { class: "rounded-full border border-muted px-hsp-sm py-vsp-3xs text-muted", children: labels.indexBadge })
87
+ ] }),
88
+ /* @__PURE__ */ jsx("h1", { class: "mb-vsp-xs border-b border-fg pb-vsp-xs font-mono text-heading font-bold leading-tight break-words", children: labels.crumb }),
89
+ /* @__PURE__ */ jsxs("div", { "data-doc-metainfo": true, class: "mb-vsp-md flex flex-wrap items-center gap-x-hsp-md gap-y-vsp-2xs text-caption text-fg", children: [
90
+ /* @__PURE__ */ jsx("span", { children: countLabel(tree.fileCount, labels.fileCount, labels.fileCountSingle) }),
91
+ /* @__PURE__ */ jsx("span", { children: countLabel(folders, labels.folderCount, labels.folderCountSingle) }),
92
+ /* @__PURE__ */ jsx("span", { children: formatAssetBytes(tree.bytes) })
93
+ ] }),
94
+ /* @__PURE__ */ jsx("p", { class: "mb-vsp-lg text-title text-muted", children: labels.indexDescription })
95
+ ] }),
96
+ /* @__PURE__ */ jsxs("div", { class: "mb-vsp-sm flex flex-wrap items-center justify-between gap-hsp-sm text-caption", children: [
97
+ /* @__PURE__ */ jsxs("span", { class: "font-mono text-muted", children: [
98
+ "public/",
99
+ dir,
100
+ "/"
101
+ ] }),
102
+ /* @__PURE__ */ jsxs("span", { class: "flex gap-hsp-sm", children: [
103
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: true, "data-zd-asset-index-action": "expand", class: "text-fg hover:text-accent focus-visible:text-accent", children: labels.expandAll }),
104
+ /* @__PURE__ */ jsx("button", { type: "button", disabled: true, "data-zd-asset-index-action": "collapse", class: "text-fg hover:text-accent focus-visible:text-accent", children: labels.collapseAll })
105
+ ] })
106
+ ] }),
107
+ entries.length > 0 ? /* @__PURE__ */ jsx(AssetTree, { root: true, node: tree, base, routePrefix, locale, fileCountLabel: labels.fileCount, fileCountSingleLabel: labels.fileCountSingle, linesLabel: labels.lines }) : /* @__PURE__ */ jsx("p", { class: "text-small text-muted", "data-zd-asset-index-empty": true, children: labels.indexEmpty }),
108
+ /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: ASSET_INDEX_PAGE_SCRIPT } })
109
+ ] });
110
+ }
111
+ export {
112
+ AssetIndexPageBody,
113
+ AssetTree,
114
+ iconFor,
115
+ resolveAssetIndexPageLabels
116
+ };
@@ -5,18 +5,12 @@ import type { ChromeContext } from "../factory-context/index.js";
5
5
  import type { AssetIndexEntry } from "../route-context-payload/types.js";
6
6
  import type { Settings } from "../settings.js";
7
7
  export { ASSET_INDEX_PAGE_SCRIPT } from "./script.js";
8
- export interface AssetTreeNode {
9
- name: string;
10
- dirs: AssetTreeNode[];
11
- files: AssetIndexEntry[];
12
- fileCount: number;
13
- bytes: number;
14
- }
8
+ export { buildAssetTree } from "./tree.js";
9
+ export type { AssetTreeNode } from "./tree.js";
10
+ export { AssetIndexPageBody, AssetTree, iconFor, resolveAssetIndexPageLabels, type AssetIndexPageBodyProps, type AssetIndexPageLabels, } from "./body.js";
15
11
  export interface AssetIndexPageViewProps {
16
12
  entries: AssetIndexEntry[];
17
13
  locale?: string;
18
14
  }
19
- /** Build a sorted directory tree and aggregate every directory's subtree totals. */
20
- export declare function buildAssetTree(entries: AssetIndexEntry[]): AssetTreeNode;
21
15
  /** Build the package-owned wide asset index from a chrome context. */
22
16
  export declare function createAssetIndexPageView<S extends Settings = Settings>(ctx: ChromeContext<S>): (props: AssetIndexPageViewProps) => JSX.Element;
@@ -1,122 +1,19 @@
1
- import { jsx, jsxs } from "preact/jsx-runtime";
2
- import { assetViewerHref } from "../asset-path/index.js";
3
- import { formatAssetBytes } from "../asset-components/index.js";
1
+ import { jsx } from "preact/jsx-runtime";
4
2
  import { assertChromeContext } from "../chrome/assert-chrome-context.js";
5
3
  import { deriveBodyEndIslands, deriveComposeMetaTitle } from "../chrome/derive.js";
6
4
  import { derivePrimaryChromeSlots } from "../chrome/primary-slots.js";
7
5
  import { DocLayoutWithDefaults } from "../doclayout/index.js";
8
6
  import { createHeadWithDefaults } from "../head-with-defaults/index.js";
9
- import {
10
- ChevronRight,
11
- FileArchive,
12
- FileCode,
13
- FileGeneric,
14
- FileImage,
15
- FilePdf,
16
- FileText,
17
- FileVideo,
18
- Folder,
19
- FolderOpen
20
- } from "../icons/index.js";
21
7
  import { resolveThemePackSsrSlug } from "../theme/theme-pack-provider.js";
8
+ import { AssetIndexPageBody, resolveAssetIndexPageLabels } from "./body.js";
22
9
  import { ASSET_INDEX_PAGE_SCRIPT } from "./script.js";
23
- import { ASSET_INDEX_PAGE_SCRIPT as ASSET_INDEX_PAGE_SCRIPT2 } from "./script.js";
24
- const ARCHIVE_EXTENSIONS = /* @__PURE__ */ new Set(["zip", "tar", "gz", "tgz", "7z", "rar"]);
25
- function basename(path) {
26
- return path.split("/").at(-1) ?? path;
27
- }
28
- function freezeTree(node) {
29
- const dirs = [...node.dirs.values()].sort((a, b) => a.name.localeCompare(b.name)).map(freezeTree);
30
- const files = [...node.files].sort((a, b) => basename(a.path).localeCompare(basename(b.path)));
31
- return {
32
- name: node.name,
33
- dirs,
34
- files,
35
- fileCount: files.length + dirs.reduce((total, dir) => total + dir.fileCount, 0),
36
- bytes: files.reduce((total, file) => total + file.bytes, 0) + dirs.reduce((total, dir) => total + dir.bytes, 0)
37
- };
38
- }
39
- function buildAssetTree(entries) {
40
- const root = { name: "", dirs: /* @__PURE__ */ new Map(), files: [] };
41
- for (const entry of entries) {
42
- let node = root;
43
- for (const segment of entry.dir.split("/").filter(Boolean)) {
44
- let child = node.dirs.get(segment);
45
- if (!child) {
46
- child = { name: segment, dirs: /* @__PURE__ */ new Map(), files: [] };
47
- node.dirs.set(segment, child);
48
- }
49
- node = child;
50
- }
51
- node.files.push(entry);
52
- }
53
- return freezeTree(root);
54
- }
55
- function folderCount(node) {
56
- return node.dirs.reduce((total, dir) => total + 1 + folderCount(dir), 0);
57
- }
58
- function formatDuration(seconds) {
59
- const rounded = Math.round(seconds);
60
- return `${Math.floor(rounded / 60)}:${String(rounded % 60).padStart(2, "0")}`;
61
- }
62
- function kindLabel(asset) {
63
- if (asset.language) return asset.language;
64
- return asset.mime.split("/").at(-1)?.toUpperCase() ?? asset.kind.toUpperCase();
65
- }
66
- function facetLabel(asset, linesLabel) {
67
- if (asset.lines !== void 0) return linesLabel.replace("{count}", String(asset.lines));
68
- if (asset.kind === "video" && asset.durationSec !== void 0) return formatDuration(asset.durationSec);
69
- if (asset.width !== void 0 && asset.height !== void 0) return `${asset.width} \xD7 ${asset.height}`;
70
- if (asset.durationSec !== void 0) return formatDuration(asset.durationSec);
71
- return null;
72
- }
73
- function iconFor(asset) {
74
- const extension = basename(asset.path).split(".").at(-1)?.toLowerCase();
75
- if (asset.kind === "other" && extension && ARCHIVE_EXTENSIONS.has(extension)) return FileArchive;
76
- const icons = {
77
- code: FileCode,
78
- text: FileText,
79
- image: FileImage,
80
- video: FileVideo,
81
- pdf: FilePdf,
82
- other: FileGeneric
83
- };
84
- return icons[asset.kind];
85
- }
86
- function countLabel(count, plural, single) {
87
- return (count === 1 ? single : plural).replace("{count}", String(count));
88
- }
89
- function AssetTree({ node, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel, root = false }) {
90
- return /* @__PURE__ */ jsxs("ul", { "data-zd-asset-tree": root ? true : void 0, children: [
91
- node.dirs.map((dir) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("details", { open: true, children: [
92
- /* @__PURE__ */ jsxs("summary", { class: "flex cursor-pointer items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small hover:bg-accent/10 focus-visible:bg-accent/10", children: [
93
- /* @__PURE__ */ jsx(ChevronRight, { className: "h-icon-xs w-icon-xs shrink-0 text-muted transition-transform" }),
94
- /* @__PURE__ */ jsx(Folder, { className: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
95
- /* @__PURE__ */ jsx(FolderOpen, { className: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
96
- /* @__PURE__ */ jsxs("span", { class: "min-w-0 truncate font-mono text-fg", children: [
97
- dir.name,
98
- "/"
99
- ] }),
100
- /* @__PURE__ */ jsxs("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block", children: [
101
- countLabel(dir.fileCount, fileCountLabel, fileCountSingleLabel),
102
- " \xB7 ",
103
- formatAssetBytes(dir.bytes)
104
- ] })
105
- ] }),
106
- /* @__PURE__ */ jsx(AssetTree, { node: dir, base, routePrefix, locale, fileCountLabel, fileCountSingleLabel, linesLabel })
107
- ] }) })),
108
- node.files.map((asset) => {
109
- const Icon = iconFor(asset);
110
- const facet = facetLabel(asset, linesLabel);
111
- const meta = [kindLabel(asset), facet, formatAssetBytes(asset.bytes)].filter(Boolean).join(" \xB7 ");
112
- return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("a", { href: assetViewerHref({ base, routePrefix, path: asset.path, locale }), title: asset.name, class: "flex min-w-0 items-center gap-hsp-xs rounded px-hsp-sm py-vsp-3xs text-small text-fg hover:bg-accent/10 hover:text-accent hover:underline focus-visible:bg-accent/10 focus-visible:text-accent focus-visible:underline", children: [
113
- /* @__PURE__ */ jsx(Icon, { className: "h-icon-sm w-icon-sm shrink-0 text-muted" }),
114
- /* @__PURE__ */ jsx("span", { class: "min-w-0 truncate font-mono", children: basename(asset.path) }),
115
- /* @__PURE__ */ jsx("span", { class: "ml-auto hidden whitespace-nowrap pl-hsp-lg text-caption text-muted sm:block", children: meta })
116
- ] }) });
117
- })
118
- ] });
119
- }
10
+ import { buildAssetTree } from "./tree.js";
11
+ import {
12
+ AssetIndexPageBody as AssetIndexPageBody2,
13
+ AssetTree,
14
+ iconFor,
15
+ resolveAssetIndexPageLabels as resolveAssetIndexPageLabels2
16
+ } from "./body.js";
120
17
  function createAssetIndexPageView(ctx) {
121
18
  assertChromeContext(ctx, "createAssetIndexPageView");
122
19
  const settings = ctx.settings;
@@ -132,45 +29,17 @@ function createAssetIndexPageView(ctx) {
132
29
  const dir = ctx.assetManifest?.dir ?? settings.assetViewerDir;
133
30
  const indexUrl = ctx.withBase(`/${localeSegment ? `${localeSegment}/` : ""}${routePrefix}/`);
134
31
  const homeUrl = ctx.withBase(`/${localeSegment ? `${localeSegment}/` : ""}`);
135
- const tree = buildAssetTree(entries);
136
- const folders = folderCount(tree);
137
- const fileCountLabel = t("asset.fileCount", locale);
138
- const fileCountSingleLabel = t("asset.fileCountSingle", locale);
139
- const folderCountLabel = t("asset.folderCount", locale);
140
- const folderCountSingleLabel = t("asset.folderCountSingle", locale);
141
- const title = t("asset.crumb", locale);
142
- return /* @__PURE__ */ jsx(DocLayoutWithDefaults, { title: composeMetaTitle(title), head: /* @__PURE__ */ jsx(HeadWithDefaults, { title, description: t("asset.indexDescription", locale), canonical: ctx.absoluteUrl(indexUrl) }), lang: locale, dataThemePack, noindex: settings.noindex, hideSidebar: true, hideToc: true, sidebarOverride: false, contentWide: true, breadcrumbOverride: /* @__PURE__ */ jsx(BreadcrumbWithDefaults, { items: [{ label: "", href: homeUrl }, { label: title }] }), headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: indexUrl, hideSidebarToggle: true }), footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }), bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }), enableClientRouter: settings.dynamicPageTransition, children: /* @__PURE__ */ jsxs("div", { "data-zd-asset-index-page": true, children: [
143
- /* @__PURE__ */ jsxs("header", { children: [
144
- /* @__PURE__ */ jsxs("div", { class: "mb-vsp-xs flex flex-wrap items-center gap-hsp-xs text-micro tracking-wide uppercase", children: [
145
- /* @__PURE__ */ jsx("span", { class: "rounded-full border border-muted px-hsp-sm py-vsp-3xs text-fg", children: title }),
146
- /* @__PURE__ */ jsx("span", { class: "rounded-full border border-muted px-hsp-sm py-vsp-3xs text-muted", children: t("asset.indexBadge", locale) })
147
- ] }),
148
- /* @__PURE__ */ jsx("h1", { class: "mb-vsp-xs border-b border-fg pb-vsp-xs font-mono text-heading font-bold leading-tight break-words", children: title }),
149
- /* @__PURE__ */ jsxs("div", { "data-doc-metainfo": true, class: "mb-vsp-md flex flex-wrap items-center gap-x-hsp-md gap-y-vsp-2xs text-caption text-fg", children: [
150
- /* @__PURE__ */ jsx("span", { children: countLabel(tree.fileCount, fileCountLabel, fileCountSingleLabel) }),
151
- /* @__PURE__ */ jsx("span", { children: countLabel(folders, folderCountLabel, folderCountSingleLabel) }),
152
- /* @__PURE__ */ jsx("span", { children: formatAssetBytes(tree.bytes) })
153
- ] }),
154
- /* @__PURE__ */ jsx("p", { class: "mb-vsp-lg text-title text-muted", children: t("asset.indexDescription", locale) })
155
- ] }),
156
- /* @__PURE__ */ jsxs("div", { class: "mb-vsp-sm flex flex-wrap items-center justify-between gap-hsp-sm text-caption", children: [
157
- /* @__PURE__ */ jsxs("span", { class: "font-mono text-muted", children: [
158
- "public/",
159
- dir,
160
- "/"
161
- ] }),
162
- /* @__PURE__ */ jsxs("span", { class: "flex gap-hsp-sm", children: [
163
- /* @__PURE__ */ jsx("button", { type: "button", disabled: true, "data-zd-asset-index-action": "expand", class: "text-fg hover:text-accent focus-visible:text-accent", children: t("asset.expandAll", locale) }),
164
- /* @__PURE__ */ jsx("button", { type: "button", disabled: true, "data-zd-asset-index-action": "collapse", class: "text-fg hover:text-accent focus-visible:text-accent", children: t("asset.collapseAll", locale) })
165
- ] })
166
- ] }),
167
- entries.length > 0 ? /* @__PURE__ */ jsx(AssetTree, { root: true, node: tree, base: settings.base, routePrefix, locale: localeSegment, fileCountLabel, fileCountSingleLabel, linesLabel: t("asset.lines", locale) }) : /* @__PURE__ */ jsx("p", { class: "text-small text-muted", "data-zd-asset-index-empty": true, children: t("asset.indexEmpty", locale) }),
168
- /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: ASSET_INDEX_PAGE_SCRIPT } })
169
- ] }) });
32
+ const labels = resolveAssetIndexPageLabels(t, locale);
33
+ const title = labels.crumb;
34
+ return /* @__PURE__ */ jsx(DocLayoutWithDefaults, { title: composeMetaTitle(title), head: /* @__PURE__ */ jsx(HeadWithDefaults, { title, description: labels.indexDescription, canonical: ctx.absoluteUrl(indexUrl) }), lang: locale, dataThemePack, noindex: settings.noindex, hideSidebar: true, hideToc: true, sidebarOverride: false, contentWide: true, breadcrumbOverride: /* @__PURE__ */ jsx(BreadcrumbWithDefaults, { items: [{ label: "", href: homeUrl }, { label: title }] }), headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: indexUrl, hideSidebarToggle: true }), footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }), bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }), enableClientRouter: settings.dynamicPageTransition, children: /* @__PURE__ */ jsx(AssetIndexPageBody, { entries, labels, base: settings.base, routePrefix, dir, locale: localeSegment }) });
170
35
  };
171
36
  }
172
37
  export {
173
- ASSET_INDEX_PAGE_SCRIPT2 as ASSET_INDEX_PAGE_SCRIPT,
38
+ ASSET_INDEX_PAGE_SCRIPT,
39
+ AssetIndexPageBody2 as AssetIndexPageBody,
40
+ AssetTree,
174
41
  buildAssetTree,
175
- createAssetIndexPageView
42
+ createAssetIndexPageView,
43
+ iconFor,
44
+ resolveAssetIndexPageLabels2 as resolveAssetIndexPageLabels
176
45
  };
@@ -0,0 +1,24 @@
1
+ import type { AssetIndexEntry } from "../route-context-payload/types.js";
2
+ export interface AssetTreeNode {
3
+ name: string;
4
+ dirs: AssetTreeNode[];
5
+ files: AssetIndexEntry[];
6
+ fileCount: number;
7
+ bytes: number;
8
+ }
9
+ interface MutableAssetTreeNode {
10
+ name: string;
11
+ dirs: Map<string, MutableAssetTreeNode>;
12
+ files: AssetIndexEntry[];
13
+ }
14
+ /** Last path segment (basename) of an asset's path, used for sorting and display. */
15
+ export declare function basename(path: string): string;
16
+ /** Freeze a mutable build-time node into the sorted, aggregated `AssetTreeNode` shape. */
17
+ export declare function freezeTree(node: MutableAssetTreeNode): AssetTreeNode;
18
+ /** Build a sorted directory tree and aggregate every directory's subtree totals. */
19
+ export declare function buildAssetTree(entries: AssetIndexEntry[]): AssetTreeNode;
20
+ /** Total number of directories in the subtree rooted at `node`, excluding the root itself. */
21
+ export declare function folderCount(node: AssetTreeNode): number;
22
+ /** Pick the singular/plural label variant for `count` and substitute its `{count}` placeholder. */
23
+ export declare function countLabel(count: number, plural: string, single: string): string;
24
+ export {};
@@ -0,0 +1,43 @@
1
+ function basename(path) {
2
+ return path.split("/").at(-1) ?? path;
3
+ }
4
+ function freezeTree(node) {
5
+ const dirs = [...node.dirs.values()].sort((a, b) => a.name.localeCompare(b.name)).map(freezeTree);
6
+ const files = [...node.files].sort((a, b) => basename(a.path).localeCompare(basename(b.path)));
7
+ return {
8
+ name: node.name,
9
+ dirs,
10
+ files,
11
+ fileCount: files.length + dirs.reduce((total, dir) => total + dir.fileCount, 0),
12
+ bytes: files.reduce((total, file) => total + file.bytes, 0) + dirs.reduce((total, dir) => total + dir.bytes, 0)
13
+ };
14
+ }
15
+ function buildAssetTree(entries) {
16
+ const root = { name: "", dirs: /* @__PURE__ */ new Map(), files: [] };
17
+ for (const entry of entries) {
18
+ let node = root;
19
+ for (const segment of entry.dir.split("/").filter(Boolean)) {
20
+ let child = node.dirs.get(segment);
21
+ if (!child) {
22
+ child = { name: segment, dirs: /* @__PURE__ */ new Map(), files: [] };
23
+ node.dirs.set(segment, child);
24
+ }
25
+ node = child;
26
+ }
27
+ node.files.push(entry);
28
+ }
29
+ return freezeTree(root);
30
+ }
31
+ function folderCount(node) {
32
+ return node.dirs.reduce((total, dir) => total + 1 + folderCount(dir), 0);
33
+ }
34
+ function countLabel(count, plural, single) {
35
+ return (count === 1 ? single : plural).replace("{count}", String(count));
36
+ }
37
+ export {
38
+ basename,
39
+ buildAssetTree,
40
+ countLabel,
41
+ folderCount,
42
+ freezeTree
43
+ };
@@ -0,0 +1,98 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { VNode } from "preact";
4
+ import type { DateFormatPattern } from "../format-date/index.js";
5
+ import type { AssetRecord } from "../plugins/internal/asset-viewer/types.js";
6
+ /**
7
+ * Localized strings `AssetPageBody` needs to render, resolved ahead of time
8
+ * (typically via `resolveAssetPageLabels`) so the body needs no `t()`/`ctx`
9
+ * of its own.
10
+ *
11
+ * `linesTemplate` is the raw `asset.lines` translation and still carries the
12
+ * literal `"{count}"` placeholder — it is NOT pre-expanded. `AssetPageBody`
13
+ * substitutes the placeholder per-asset (mirroring `createAssetPageView`'s
14
+ * prior inline `.replace("{count}", …)` call), because the substituted count
15
+ * depends on the specific `entry` being rendered, not on locale alone. Every
16
+ * other field below is an already-resolved, ready-to-render string.
17
+ */
18
+ export interface AssetPageLabels {
19
+ /** Eyebrow badge text (e.g. "Asset"). */
20
+ badge: string;
21
+ /** "Updated" label — shared by the header metadata line and the details table. */
22
+ updated: string;
23
+ /** Raw `asset.lines` translation template — see the interface doc comment above. */
24
+ linesTemplate: string;
25
+ download: string;
26
+ openRaw: string;
27
+ copy: string;
28
+ wrap: string;
29
+ truncated: string;
30
+ noPreview: string;
31
+ linkedFrom: string;
32
+ backTo: string;
33
+ detailsHeading: string;
34
+ detailsType: string;
35
+ detailsSize: string;
36
+ detailsPath: string;
37
+ detailsDimensions: string;
38
+ fit: string;
39
+ actualSize: string;
40
+ checker: string;
41
+ dark: string;
42
+ enlarge: string;
43
+ detailsCollapse: string;
44
+ detailsExpand: string;
45
+ /** "View source on GitHub" label for the body-foot utility area. */
46
+ viewSource: string;
47
+ }
48
+ /**
49
+ * Builds `AssetPageLabels` from a `t(key, locale)` translator, bound to the
50
+ * given `locale` — NOT the default locale, so a localized page gets its own
51
+ * strings even when `t` itself falls back internally.
52
+ */
53
+ export declare function resolveAssetPageLabels(t: (key: string, locale?: string) => string, locale: string): AssetPageLabels;
54
+ /**
55
+ * Head-level pre-paint markup: restores a persisted collapsed details-rail
56
+ * preference to `<html data-asset-details-hidden>` before first paint, so a
57
+ * hard reload of a collapsed page never flashes the expanded rail (#3941
58
+ * D3). Render this ahead of the rest of the page `head` — see
59
+ * `createAssetPageView`'s `head` prop on `DocLayoutWithDefaults`.
60
+ *
61
+ * A function, not a shared constant VNode: Preact's diffing mutates
62
+ * bookkeeping fields onto the vnode objects it renders, so one instance
63
+ * reused across many independent SSR passes (e.g. every asset page in a
64
+ * `zfb build` run, all sharing this module) is a latent hazard. Each call
65
+ * returns a fresh vnode.
66
+ */
67
+ export declare function renderAssetDetailsPrepaintScript(): VNode;
68
+ export interface AssetPageBodyProps {
69
+ /** The asset record being rendered. */
70
+ entry: AssetRecord;
71
+ /** Active locale — used for date formatting; all labels are pre-resolved. */
72
+ locale: string;
73
+ /** Resolved raw-asset URL (download href / stage src), e.g. from `assetRawHref()`. */
74
+ rawUrl: string;
75
+ /** Localized strings — see `resolveAssetPageLabels()`. */
76
+ labels: AssetPageLabels;
77
+ /** Resolved date pattern for the "Updated" date, e.g. `deriveDateFormats(ctx)(locale).full`. */
78
+ fullPattern?: DateFormatPattern;
79
+ /** The "← Back to …" link target. Omit to render no back link. */
80
+ backLink?: AssetRecord["linkedFrom"][number];
81
+ /**
82
+ * Resolved GitHub "view source" URL (via `buildGitHubSourceUrl`), or
83
+ * `null`/`undefined` when `githubUrl` isn't configured. Only rendered when
84
+ * `showViewSource` is true; `BodyFootUtilArea` itself hides the link when
85
+ * this is falsy.
86
+ */
87
+ viewSourceUrl?: string | null;
88
+ /** Whether the body-foot "view source" utility area is enabled (`settings.bodyFootUtilArea`). */
89
+ showViewSource: boolean;
90
+ }
91
+ /**
92
+ * Renders the asset viewer page body — back link, header, actions, the
93
+ * stage/details/linked media grid, bottom actions, the body-foot view-source
94
+ * link, and the inline bootstrap script — exactly what `createAssetPageView`
95
+ * placed inline as `DocLayoutWithDefaults`'s children before the #4221
96
+ * split. Plain props only, no `ChromeContext`.
97
+ */
98
+ export declare function AssetPageBody({ entry: asset, locale, rawUrl, labels, fullPattern, backLink, viewSourceUrl, showViewSource, }: AssetPageBodyProps): VNode;