@takazudo/zudo-doc 2.2.2 → 2.3.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.
@@ -32,6 +32,7 @@ export declare function createChrome<S extends Settings = Settings>(context: Rou
32
32
  }) => import("preact").JSX.Element;
33
33
  SiteTreeNavWrapper: import("../factory-context/index.js").FactoryComponent;
34
34
  BodyEndIslands: (props: import("../doc-body-end-islands/index.js").BodyEndIslandsProps) => import("preact").JSX.Element;
35
+ HomePageView: (props: import("../home-page/index.js").HomePageViewProps) => import("preact").JSX.Element;
35
36
  };
36
37
  /** The wired page-chrome surface returned by {@link createChrome}. */
37
38
  export type Chrome = ReturnType<typeof createChrome>;
@@ -5,6 +5,7 @@ import { createSidebarWithDefaults } from "../sidebar-with-defaults/index.js";
5
5
  import { createRenderDocPage } from "../doc-page-renderer/index.js";
6
6
  import { createVersionsPageView } from "../versions-page/index.js";
7
7
  import { createTagPages } from "../tag-pages/index.js";
8
+ import { createHomePageView } from "../home-page/index.js";
8
9
  import {
9
10
  deriveComposeMetaTitle,
10
11
  deriveBodyEndIslands,
@@ -23,6 +24,7 @@ function createChrome(context, hostBindings = {}) {
23
24
  const { collectTagMapForLocale, TagDetailPageView, TagsIndexPageView } = createTagPages(ctx);
24
25
  const { SiteTreeNavWrapper } = deriveMdxComponents(ctx);
25
26
  const BodyEndIslands = deriveBodyEndIslands(ctx);
27
+ const HomePageView = createHomePageView(ctx);
26
28
  return {
27
29
  composeMetaTitle,
28
30
  HeadWithDefaults,
@@ -35,7 +37,8 @@ function createChrome(context, hostBindings = {}) {
35
37
  TagDetailPageView,
36
38
  TagsIndexPageView,
37
39
  SiteTreeNavWrapper,
38
- BodyEndIslands
40
+ BodyEndIslands,
41
+ HomePageView
39
42
  };
40
43
  }
41
44
  export {
@@ -73,5 +73,9 @@ interface DocContentHeaderProps {
73
73
  * are HOST-bound slots (`ctx.hostBindings.buildFrontmatterPreviewEntries` /
74
74
  * `…frontmatterRenderers`, defaulting to `() => []` / `{}`). The nested
75
75
  * `DocMetainfoArea` / `DocTagsArea` are rebuilt from the same context.
76
+ *
77
+ * `ctx.hostBindings.docContentHeaderExtras` (default: absent) renders after
78
+ * the `<h1>` and before `DocMetainfoArea` — see the `ChromeHostBindings`
79
+ * interface in `./factory-context`.
76
80
  */
77
81
  export declare function createDocContentHeader<S extends Settings = Settings>(ctx: ChromeContext<S>): (props: DocContentHeaderProps) => JSX.Element;
@@ -8,6 +8,7 @@ function createDocContentHeader(ctx) {
8
8
  const t = ctx.t;
9
9
  const buildFrontmatterPreviewEntries = ctx.hostBindings.buildFrontmatterPreviewEntries ?? (() => []);
10
10
  const frontmatterRenderers = ctx.hostBindings.frontmatterRenderers ?? {};
11
+ const docContentHeaderExtras = ctx.hostBindings.docContentHeaderExtras;
11
12
  const DocMetainfoArea = createDocMetainfoArea(ctx);
12
13
  const DocTagsArea = createDocTagsArea(ctx);
13
14
  function DocContentHeader({
@@ -19,6 +20,13 @@ function createDocContentHeader(ctx) {
19
20
  }) {
20
21
  return /* @__PURE__ */ jsxs(Fragment, { children: [
21
22
  /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-xs", children: entry.data.title }),
23
+ docContentHeaderExtras?.({
24
+ entry,
25
+ slug,
26
+ locale,
27
+ isFallback,
28
+ version
29
+ }),
22
30
  !version && /* @__PURE__ */ jsx(DocMetainfoArea, { slug, locale, isFallback }),
23
31
  !version && /* @__PURE__ */ jsx(DocTagsArea, { slug, locale, tags: entry.data.tags }),
24
32
  isFallback && !entry.data.generated && /* @__PURE__ */ jsx(
@@ -224,6 +224,32 @@ export interface ChromeHostBindings {
224
224
  /** MDX content-component overrides (Details / HtmlPreview / Island /
225
225
  * PresetGenerator). Default: package SSR impls + a `PresetGenerator` stub. */
226
226
  mdxExtras?: Record<string, FactoryComponent>;
227
+ /**
228
+ * Extra content rendered inside the doc content header, between the `<h1>`
229
+ * and the metainfo block. A RENDERER (not a component) so it is naturally
230
+ * keyed on the page entry/frontmatter rather than re-deriving them from
231
+ * props. Default: absent → renders nothing (byte-identical output to the
232
+ * pre-seam header). Called for `kind === "entry"` doc pages on all 4 doc
233
+ * routes, INCLUDING versioned pages — it receives `version` and decides for
234
+ * itself whether/how to render on a versioned page.
235
+ */
236
+ docContentHeaderExtras?: (args: {
237
+ entry: DocPageEntry;
238
+ slug: string;
239
+ locale: string;
240
+ isFallback?: boolean;
241
+ version?: string;
242
+ }) => unknown;
243
+ /**
244
+ * Extra content rendered in the home hero. A RENDERER (not a component).
245
+ * Default: absent → renders nothing. The `/` home route is never injected
246
+ * by the routes plugin (zfb rejects `/`), so this binding fires on injected
247
+ * `/[locale]` homes and on any host that threads it through `createChrome`.
248
+ * A `HomePageView` `extras` prop (added in a later task) takes precedence.
249
+ */
250
+ homeExtras?: (args: {
251
+ locale: string;
252
+ }) => unknown;
227
253
  }
228
254
  /**
229
255
  * The unified chrome context — the superset that fully wires the page chrome:
@@ -0,0 +1,51 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+ import type { ComponentChildren, JSX } from "preact";
4
+ import type { DocNavNode } from "../doc-page-props/index.js";
5
+ import type { ChromeContext } from "../factory-context/index.js";
6
+ import type { Settings } from "../settings.js";
7
+ export { prepareHomeData } from "./prepare-home-data.js";
8
+ export type { PrepareHomeDataOptions, HomeData } from "./prepare-home-data.js";
9
+ /** Props for the `HomePageView` component built by {@link createHomePageView}. */
10
+ export interface HomePageViewProps {
11
+ /** Active locale — drives hero copy, link locale-prefixing, and the
12
+ * `hostBindings.homeExtras({ locale })` call when `extras` is absent. */
13
+ locale: string;
14
+ /**
15
+ * Extra content rendered after the links row, inside the hero text column.
16
+ * A VALUE (already-rendered children) — the host-PAGE path. Takes
17
+ * precedence over `ctx.hostBindings.homeExtras` when both are present.
18
+ *
19
+ * This value-vs-renderer asymmetry (prop = value, `hostBindings.homeExtras`
20
+ * = renderer) is INTENTIONAL, not an oversight to "unify": a host page
21
+ * already has its JSX in hand (a value), while the injected/bindings path
22
+ * only has a locale string at render time and must derive its own content
23
+ * from it (a renderer). Resolved as `extras ?? hostBindings.homeExtras?.({
24
+ * locale })`.
25
+ */
26
+ extras?: ComponentChildren;
27
+ /** Prepared nav tree for the `SiteTreeNav` grid (already grouped via
28
+ * `groupSatelliteNodes` by the caller — the two routes group their own
29
+ * locale-specific tree before handing it here). */
30
+ tree: DocNavNode[];
31
+ /** Ordered category prefixes, passed through to `SiteTreeNav`. */
32
+ categoryOrder: string[];
33
+ /** Unique tag count for the current locale — gates the "all tags" section
34
+ * together with `settings.docTags`. */
35
+ tagCount: number;
36
+ }
37
+ /**
38
+ * Create a `HomePageView` component from the unified {@link ChromeContext}
39
+ * (epic Collapse Wiring Shells #2420-style factory shape, adopted for the
40
+ * home page in #2502).
41
+ *
42
+ * Reads `settings`/`t`/`withBase`/`defaultLocale` off the context, derives
43
+ * `composeMetaTitle` + Head/Header/Footer/BodyEndIslands chrome from the SAME
44
+ * context (so the injected package-routes path and a host-bound context both
45
+ * render byte-identically), and computes the locale-URL prefix (`""` for the
46
+ * default locale, `/{locale}` otherwise) shared by the overview link, the
47
+ * header `currentPath`, and the "all tags" link — reproducing the exact
48
+ * per-route path construction `routes/index.tsx` / `routes/locale-index.tsx`
49
+ * did inline before this extraction.
50
+ */
51
+ export declare function createHomePageView<S extends Settings = Settings>(ctx: ChromeContext<S>): (props: HomePageViewProps) => JSX.Element;
@@ -0,0 +1,124 @@
1
+ import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
2
+ import { Island } from "@takazudo/zfb";
3
+ import { DocLayoutWithDefaults } from "../doclayout/index.js";
4
+ import { SiteTreeNav } from "../site-tree-nav-island/index.js";
5
+ import { createHeadWithDefaults } from "../head-with-defaults/index.js";
6
+ import { createHeaderWithDefaults } from "../header-with-defaults/index.js";
7
+ import { createFooterWithDefaults } from "../footer-with-defaults/index.js";
8
+ import { deriveComposeMetaTitle, deriveBodyEndIslands } from "../chrome/derive.js";
9
+ import { assertChromeContext } from "../chrome/assert-chrome-context.js";
10
+ import { prepareHomeData } from "./prepare-home-data.js";
11
+ function createHomePageView(ctx) {
12
+ assertChromeContext(ctx, "createHomePageView");
13
+ const settings = ctx.settings;
14
+ const t = ctx.t;
15
+ const withBase = ctx.withBase;
16
+ const defaultLocale = ctx.defaultLocale;
17
+ const composeMetaTitle = deriveComposeMetaTitle(ctx);
18
+ const HeadWithDefaults = createHeadWithDefaults(ctx);
19
+ const HeaderWithDefaults = createHeaderWithDefaults(ctx);
20
+ const FooterWithDefaults = createFooterWithDefaults(ctx);
21
+ const BodyEndIslands = deriveBodyEndIslands(ctx);
22
+ const homeExtras = ctx.hostBindings.homeExtras;
23
+ function HomePageView({
24
+ locale,
25
+ extras,
26
+ tree,
27
+ categoryOrder,
28
+ tagCount
29
+ }) {
30
+ const prefix = locale === defaultLocale ? "" : `/${locale}`;
31
+ const ctaNav = settings.headerNav[0] ?? null;
32
+ const overview = ctaNav ? withBase(`${prefix}${ctaNav.path}`) : null;
33
+ const logoUrl = withBase("/img/logo.svg");
34
+ const resolvedExtras = extras ?? homeExtras?.({ locale });
35
+ return /* @__PURE__ */ jsxs(
36
+ DocLayoutWithDefaults,
37
+ {
38
+ title: composeMetaTitle(settings.siteName),
39
+ head: /* @__PURE__ */ jsx(HeadWithDefaults, { title: settings.siteName }),
40
+ lang: locale,
41
+ noindex: settings.noindex,
42
+ hideSidebar: true,
43
+ hideToc: true,
44
+ sidebarOverride: /* @__PURE__ */ jsx(Fragment, {}),
45
+ headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: withBase(`${prefix}/`) }),
46
+ footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }),
47
+ bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
48
+ enableClientRouter: settings.dynamicPageTransition,
49
+ children: [
50
+ /* @__PURE__ */ jsx("div", { class: "flex justify-center mb-vsp-xl", children: /* @__PURE__ */ jsxs("div", { class: "flex flex-col items-center text-center gap-hsp-md lg:flex-row lg:text-left lg:gap-hsp-xl", children: [
51
+ /* @__PURE__ */ jsx(
52
+ "div",
53
+ {
54
+ class: "w-[320px] max-w-full aspect-[1200/630] bg-fg shrink-0",
55
+ style: {
56
+ WebkitMask: `url(${logoUrl}) center/contain no-repeat`,
57
+ mask: `url(${logoUrl}) center/contain no-repeat`
58
+ },
59
+ "aria-hidden": "true"
60
+ }
61
+ ),
62
+ /* @__PURE__ */ jsxs("div", { children: [
63
+ /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs", children: settings.siteName }),
64
+ /* @__PURE__ */ jsx("p", { class: "text-muted text-small mb-vsp-sm", children: settings.siteDescription }),
65
+ /* @__PURE__ */ jsxs("div", { class: "flex items-center justify-center lg:justify-start gap-hsp-md text-small", children: [
66
+ overview && /* @__PURE__ */ jsxs(Fragment, { children: [
67
+ /* @__PURE__ */ jsx("a", { href: overview, class: "text-fg underline hover:text-accent", children: t("nav.overview", locale) }),
68
+ /* @__PURE__ */ jsx("span", { class: "text-muted", children: "/" })
69
+ ] }),
70
+ settings.githubUrl && /* @__PURE__ */ jsxs(
71
+ "a",
72
+ {
73
+ href: settings.githubUrl,
74
+ class: "inline-flex items-center gap-[0.3em] text-fg underline hover:text-accent",
75
+ target: "_blank",
76
+ rel: "noopener noreferrer",
77
+ children: [
78
+ /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", class: "w-[1em] h-[1em] shrink-0", children: /* @__PURE__ */ jsx(
79
+ "path",
80
+ {
81
+ fill: "currentColor",
82
+ d: "M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
83
+ }
84
+ ) }),
85
+ "GitHub"
86
+ ]
87
+ }
88
+ )
89
+ ] }),
90
+ resolvedExtras
91
+ ] })
92
+ ] }) }),
93
+ Island({
94
+ when: "idle",
95
+ children: /* @__PURE__ */ jsx(
96
+ SiteTreeNav,
97
+ {
98
+ tree,
99
+ categoryOrder,
100
+ categoryIgnore: ["inbox", "develop"]
101
+ }
102
+ )
103
+ }),
104
+ settings.docTags && tagCount > 0 && /* @__PURE__ */ jsxs("section", { class: "mt-vsp-xl", children: [
105
+ /* @__PURE__ */ jsx("h2", { class: "text-title font-bold mb-vsp-md", children: t("doc.allTags", locale) }),
106
+ /* @__PURE__ */ jsx(
107
+ "a",
108
+ {
109
+ href: withBase(`${prefix}/docs/tags`),
110
+ class: "text-accent underline hover:text-accent-hover",
111
+ children: t("doc.allTags", locale)
112
+ }
113
+ )
114
+ ] })
115
+ ]
116
+ }
117
+ );
118
+ }
119
+ return HomePageView;
120
+ }
121
+ export {
122
+ createHomePageView,
123
+ prepareHomeData
124
+ };
@@ -0,0 +1,46 @@
1
+ import type { RouteContext } from "../factory-context/index.js";
2
+ import type { NavSourceOptions } from "../nav-source-docs/index.js";
3
+ import type { DocNavNode } from "../doc-page-props/index.js";
4
+ export interface PrepareHomeDataOptions {
5
+ /**
6
+ * Override nav-source filtering. Default: `{}` for the default locale;
7
+ * `{ applyDefaultLocaleOnlyFilter: true, keepUnlisted: true }` otherwise.
8
+ */
9
+ navSourceOptions?: NavSourceOptions;
10
+ /**
11
+ * Override the category-meta directory. When set, category meta is loaded via
12
+ * `loadCategoryMeta(categoryMetaDir)` on BOTH branches — replacing the default
13
+ * locale's merged `resolveNavSource` categoryMeta and the non-default locale's
14
+ * `loadCategoryMeta(getLocaleConfig(locale).dir)`. When unset, the default
15
+ * locale uses the categoryMeta `resolveNavSource` returns and non-default
16
+ * locales use `loadCategoryMeta(cfg.dir)` — locale-dir-ONLY.
17
+ */
18
+ categoryMetaDir?: string;
19
+ }
20
+ export interface HomeData {
21
+ /** GROUPED tree — the post-`groupSatelliteNodes` output (spreads onto
22
+ * `HomePageViewProps.tree`). */
23
+ tree: DocNavNode[];
24
+ categoryOrder: string[];
25
+ tagCount: number;
26
+ }
27
+ /**
28
+ * Prepare the home-page nav tree / category order / tag count for `locale`.
29
+ *
30
+ * Branches on `locale === ctx.defaultLocale`:
31
+ *
32
+ * - Default locale: `resolveNavSource(locale, undefined, {})` and its
33
+ * returned `categoryMeta` (the merged base+locale meta) — unless
34
+ * `options.categoryMetaDir` is set, in which case that dir is loaded via
35
+ * `loadCategoryMeta` instead.
36
+ * - Non-default locale: requires `ctx.getLocaleConfig(locale)` — throws if
37
+ * the locale isn't configured, absorbing the duplicated route/page guard —
38
+ * then resolves nav source with the locale-home filter and reads category
39
+ * meta from the locale dir ONLY, `loadCategoryMeta(cfg.dir)`. This is
40
+ * deliberately NOT the merged base+locale categoryMeta `resolveNavSource`
41
+ * returns: locale home pages historically never merged in base meta (unlike
42
+ * the locale doc route), and that exact behavior is preserved here — do not
43
+ * "fix" it to use the merged meta, that would silently change sidebar-grid
44
+ * labels/positions.
45
+ */
46
+ export declare function prepareHomeData(ctx: RouteContext, locale: string, options?: PrepareHomeDataOptions): HomeData;
@@ -0,0 +1,37 @@
1
+ import { loadCategoryMeta } from "../sidebar-tree/index.js";
2
+ const LOCALE_NAV_SOURCE_OPTIONS = {
3
+ applyDefaultLocaleOnlyFilter: true,
4
+ keepUnlisted: true
5
+ };
6
+ function prepareHomeData(ctx, locale, options) {
7
+ let navDocs;
8
+ let categoryMeta;
9
+ if (locale === ctx.defaultLocale) {
10
+ const resolved = ctx.resolveNavSource(locale, void 0, options?.navSourceOptions ?? {});
11
+ navDocs = resolved.navDocs;
12
+ categoryMeta = options?.categoryMetaDir ? loadCategoryMeta(options.categoryMetaDir) : resolved.categoryMeta;
13
+ } else {
14
+ const cfg = ctx.getLocaleConfig(locale);
15
+ if (!cfg) {
16
+ throw new Error(`prepareHomeData: locale "${locale}" is not configured in settings.locales`);
17
+ }
18
+ const resolved = ctx.resolveNavSource(
19
+ locale,
20
+ void 0,
21
+ options?.navSourceOptions ?? LOCALE_NAV_SOURCE_OPTIONS
22
+ );
23
+ navDocs = resolved.navDocs;
24
+ categoryMeta = loadCategoryMeta(options?.categoryMetaDir ?? cfg.dir);
25
+ }
26
+ const tree = ctx.buildNavTree(navDocs, locale, categoryMeta, (slug, loc) => ctx.docsUrl(slug, loc));
27
+ const categoryOrder = ctx.getCategoryOrder();
28
+ const grouped = ctx.groupSatelliteNodes(tree, categoryOrder);
29
+ const tagCount = ctx.collectTags(
30
+ navDocs.filter((d) => !d.data.category_no_page),
31
+ (id, data) => data.slug ?? ctx.toRouteSlug(id)
32
+ ).size;
33
+ return { tree: grouped, categoryOrder, tagCount };
34
+ }
35
+ export {
36
+ prepareHomeData
37
+ };
@@ -2,12 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import {
4
4
  collectContentFiles,
5
- getFileCommitsMetaAsync
5
+ getAllFilesFirstLastMetaAsync
6
6
  } from "@takazudo/zudo-doc-history-server/git-history";
7
- import {
8
- makeSemaphore,
9
- defaultGitConcurrency
10
- } from "@takazudo/zudo-doc-history-server/concurrency";
11
7
  const META_OUT_RELATIVE_DIR = ".zfb";
12
8
  const META_OUT_FILENAME = "doc-history-meta.json";
13
9
  function deriveSourceExt(filePath) {
@@ -34,8 +30,6 @@ async function runDocHistoryMetaStep(options) {
34
30
  dirEntries.push([code, path.resolve(projectRoot, locale.dir)]);
35
31
  }
36
32
  }
37
- const concurrency = defaultGitConcurrency();
38
- const acquire = makeSemaphore(concurrency);
39
33
  const jobs = [];
40
34
  for (const [localeKey, contentDir] of dirEntries) {
41
35
  const files = collectContentFiles(contentDir);
@@ -44,36 +38,20 @@ async function runDocHistoryMetaStep(options) {
44
38
  jobs.push({ composedSlug, filePath });
45
39
  }
46
40
  }
47
- const results = new Array(jobs.length).fill(null);
48
- await Promise.all(
49
- jobs.map(async ({ filePath }, i) => {
50
- const release = await acquire();
51
- try {
52
- const allCommits = await getFileCommitsMetaAsync(filePath);
53
- if (allCommits.length === 0) return;
54
- const newestInfo = allCommits[0];
55
- const oldestInfo = allCommits[allCommits.length - 1] ?? newestInfo;
56
- results[i] = {
57
- newest: { author: newestInfo.author, date: newestInfo.date },
58
- oldest: { author: oldestInfo.author, date: oldestInfo.date }
59
- };
60
- } finally {
61
- release();
62
- }
63
- })
64
- );
41
+ const contentDirs = dirEntries.map(([, dir]) => dir);
42
+ const firstLast = await getAllFilesFirstLastMetaAsync(contentDirs);
65
43
  const meta = {};
66
- for (let i = 0; i < jobs.length; i++) {
67
- const result = results[i];
68
- if (result == null) continue;
69
- meta[jobs[i].composedSlug] = {
44
+ for (const { composedSlug, filePath } of jobs) {
45
+ const entry = firstLast.get(filePath);
46
+ if (entry == null) continue;
47
+ meta[composedSlug] = {
70
48
  // Author comes from the FIRST (oldest) commit.
71
- author: result.oldest.author,
49
+ author: entry.oldest.author,
72
50
  // createdDate = oldest commit; updatedDate = newest commit.
73
- createdDate: result.oldest.date,
74
- updatedDate: result.newest.date,
51
+ createdDate: entry.oldest.date,
52
+ updatedDate: entry.newest.date,
75
53
  // Source extension for the view-source URL (".md" walkers accepted).
76
- ext: deriveSourceExt(jobs[i].filePath)
54
+ ext: deriveSourceExt(filePath)
77
55
  };
78
56
  }
79
57
  fs.mkdirSync(zfbDir, { recursive: true });
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { existsSync, cpSync, rmSync, mkdirSync } from "node:fs";
2
+ import { existsSync, statSync, cpSync, rmSync, mkdirSync } from "node:fs";
3
3
  import { dirname, basename, join } from "node:path";
4
4
  import { definePlugin } from "@takazudo/zfb/plugins";
5
5
  function deriveRoutes(settings) {
@@ -57,6 +57,33 @@ const plugin = definePlugin({
57
57
  tagVocabulary,
58
58
  colorSchemes
59
59
  })};
60
+ `
61
+ );
62
+ if (typeof settings.chromeBindingsModule === "string" && settings.chromeBindingsModule.trim() === "") {
63
+ throw new Error(
64
+ 'zudo-doc: settings.chromeBindingsModule is set to an empty string \u2014 set it to a project-root-relative module path (e.g. "./src/chrome-bindings.tsx") or remove the setting.'
65
+ );
66
+ }
67
+ const chromeBindingsModule = typeof settings.chromeBindingsModule === "string" ? settings.chromeBindingsModule : void 0;
68
+ let chromeBindingsAbsPath;
69
+ if (chromeBindingsModule) {
70
+ const resolved = join(ctx.projectRoot, chromeBindingsModule).split("\\").join("/");
71
+ if (!existsSync(resolved)) {
72
+ throw new Error(
73
+ `zudo-doc: settings.chromeBindingsModule is set to "${chromeBindingsModule}", which resolves to "${resolved}" \u2014 that file does not exist. Create it (must export a named \`chromeBindings: ChromeHostBindings\`) or remove the setting.`
74
+ );
75
+ }
76
+ if (!statSync(resolved).isFile()) {
77
+ throw new Error(
78
+ `zudo-doc: settings.chromeBindingsModule is set to "${chromeBindingsModule}", which resolves to "${resolved}" \u2014 that path is a directory, not a module file. Point it at a module file (e.g. "./src/chrome-bindings.tsx") or remove the setting.`
79
+ );
80
+ }
81
+ chromeBindingsAbsPath = resolved;
82
+ }
83
+ ctx.addVirtualModule(
84
+ "virtual:zudo-doc-chrome-bindings",
85
+ () => chromeBindingsAbsPath ? `export { chromeBindings } from ${JSON.stringify(chromeBindingsAbsPath)};
86
+ ` : `export const chromeBindings = {};
60
87
  `
61
88
  );
62
89
  const require2 = createRequire(import.meta.url);
package/dist/preset.d.ts CHANGED
@@ -83,6 +83,15 @@ export interface PresetSettings {
83
83
  * succeeds). See `docs/adr/route-injection-seam.md`. (#2404)
84
84
  */
85
85
  packageOwnedRoutes?: boolean;
86
+ /**
87
+ * Project-root-relative path to a host module exporting a named
88
+ * `chromeBindings: ChromeHostBindings` (from `@takazudo/zudo-doc/factory-context`).
89
+ * Only consumed when `packageOwnedRoutes` is on — see `settings.ts` and
90
+ * `docs/adr/route-injection-seam.md` ("Host-callables channel") for the
91
+ * full contract. Omit to keep the injected chrome shim's bindings at their
92
+ * package-default stubs (byte-identical to today).
93
+ */
94
+ chromeBindingsModule?: string;
86
95
  /** Gate for the `/docs/tags` + `/docs/tags/[tag]` injected routes. */
87
96
  docTags?: boolean;
88
97
  /** Gate for the SSR `/api/ai-chat` injected route (`prerender: false`). */
@@ -8,6 +8,6 @@ export declare const composeMetaTitle: (title: string) => string, HeadWithDefaul
8
8
  }) => import("preact").JSX.Element, TagsIndexPageView: (props: {
9
9
  locale: string;
10
10
  children?: import("preact").ComponentChildren;
11
- }) => import("preact").JSX.Element, SiteTreeNavWrapper: import("../factory-context/index.js").FactoryComponent, BodyEndIslands: (props: import("../doc-body-end-islands/index.js").BodyEndIslandsProps) => import("preact").JSX.Element;
11
+ }) => import("preact").JSX.Element, SiteTreeNavWrapper: import("../factory-context/index.js").FactoryComponent, BodyEndIslands: (props: import("../doc-body-end-islands/index.js").BodyEndIslandsProps) => import("preact").JSX.Element, HomePageView: (props: import("../home-page/index.js").HomePageViewProps) => import("preact").JSX.Element;
12
12
  export { buildDocRouteEntries } from "./_context.js";
13
13
  export type { DocNavNode };
@@ -1,8 +1,10 @@
1
1
  import { routeCtx } from "./_context.js";
2
2
  import { createChrome } from "../chrome/index.js";
3
3
  import { DocHistory } from "../doc-history/index.js";
4
+ import { chromeBindings } from "virtual:zudo-doc-chrome-bindings";
4
5
  const chrome = createChrome(routeCtx, {
5
- DocHistory
6
+ DocHistory,
7
+ ...chromeBindings
6
8
  });
7
9
  const {
8
10
  composeMetaTitle,
@@ -16,7 +18,8 @@ const {
16
18
  TagDetailPageView,
17
19
  TagsIndexPageView,
18
20
  SiteTreeNavWrapper,
19
- BodyEndIslands
21
+ BodyEndIslands,
22
+ HomePageView
20
23
  } = chrome;
21
24
  import { buildDocRouteEntries } from "./_context.js";
22
25
  export {
@@ -24,6 +27,7 @@ export {
24
27
  FooterWithDefaults,
25
28
  HeadWithDefaults,
26
29
  HeaderWithDefaults,
30
+ HomePageView,
27
31
  SidebarWithDefaults,
28
32
  SiteTreeNavWrapper,
29
33
  TagDetailPageView,
@@ -1,115 +1,12 @@
1
- import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
2
- import { Island } from "@takazudo/zfb";
3
- import { DocLayoutWithDefaults } from "../doclayout/index.js";
4
- import { SiteTreeNav } from "../site-tree-nav-island/index.js";
5
- import {
6
- settings,
7
- defaultLocale,
8
- t,
9
- withBase,
10
- resolveNavSource,
11
- buildNavTree,
12
- groupSatelliteNodes,
13
- getCategoryOrder,
14
- collectTags,
15
- docsUrl,
16
- toRouteSlug
17
- } from "./_context.js";
18
- import {
19
- HeadWithDefaults,
20
- HeaderWithDefaults,
21
- FooterWithDefaults,
22
- BodyEndIslands,
23
- composeMetaTitle
24
- } from "./_chrome.js";
1
+ import { jsx } from "preact/jsx-runtime";
2
+ import { defaultLocale, routeCtx } from "./_context.js";
3
+ import { prepareHomeData } from "../home-page/prepare-home-data.js";
4
+ import { HomePageView } from "./_chrome.js";
25
5
  const frontmatter = { title: "Home" };
26
6
  function IndexPage() {
27
7
  const locale = defaultLocale;
28
- const { navDocs, categoryMeta } = resolveNavSource(locale, void 0);
29
- const tree = buildNavTree(
30
- navDocs,
31
- locale,
32
- categoryMeta,
33
- (slug, loc) => docsUrl(slug, loc)
34
- );
35
- const categoryOrder = getCategoryOrder();
36
- const groupedTree = groupSatelliteNodes(tree, categoryOrder);
37
- const tagCount = collectTags(
38
- navDocs.filter((d) => !d.data.category_no_page),
39
- (id, data) => data.slug ?? toRouteSlug(id)
40
- ).size;
41
- const ctaNav = settings.headerNav[0] ?? null;
42
- const overview = ctaNav ? withBase(ctaNav.path) : null;
43
- const logoUrl = withBase("/img/logo.svg");
44
- return /* @__PURE__ */ jsxs(
45
- DocLayoutWithDefaults,
46
- {
47
- title: composeMetaTitle(settings.siteName),
48
- head: /* @__PURE__ */ jsx(HeadWithDefaults, { title: settings.siteName }),
49
- lang: locale,
50
- noindex: settings.noindex,
51
- hideSidebar: true,
52
- hideToc: true,
53
- sidebarOverride: /* @__PURE__ */ jsx(Fragment, {}),
54
- headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: withBase("/") }),
55
- footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }),
56
- bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
57
- enableClientRouter: settings.dynamicPageTransition,
58
- children: [
59
- /* @__PURE__ */ jsx("div", { class: "flex justify-center mb-vsp-xl", children: /* @__PURE__ */ jsxs("div", { class: "flex flex-col items-center text-center gap-hsp-md lg:flex-row lg:text-left lg:gap-hsp-xl", children: [
60
- /* @__PURE__ */ jsx(
61
- "div",
62
- {
63
- class: "w-[320px] max-w-full aspect-[1200/630] bg-fg shrink-0",
64
- style: {
65
- WebkitMask: `url(${logoUrl}) center/contain no-repeat`,
66
- mask: `url(${logoUrl}) center/contain no-repeat`
67
- },
68
- "aria-hidden": "true"
69
- }
70
- ),
71
- /* @__PURE__ */ jsxs("div", { children: [
72
- /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs", children: settings.siteName }),
73
- /* @__PURE__ */ jsx("p", { class: "text-muted text-small mb-vsp-sm", children: settings.siteDescription }),
74
- /* @__PURE__ */ jsxs("div", { class: "flex items-center justify-center lg:justify-start gap-hsp-md text-small", children: [
75
- overview && /* @__PURE__ */ jsxs(Fragment, { children: [
76
- /* @__PURE__ */ jsx("a", { href: overview, class: "text-fg underline hover:text-accent", children: t("nav.overview", locale) }),
77
- /* @__PURE__ */ jsx("span", { class: "text-muted", children: "/" })
78
- ] }),
79
- settings.githubUrl && /* @__PURE__ */ jsxs(Fragment, { children: [
80
- /* @__PURE__ */ jsx(
81
- "a",
82
- {
83
- href: settings.githubUrl,
84
- class: "inline-flex items-center gap-[0.3em] text-fg underline hover:text-accent",
85
- target: "_blank",
86
- rel: "noopener noreferrer",
87
- children: "GitHub"
88
- }
89
- ),
90
- /* @__PURE__ */ jsx("span", { class: "text-muted", children: "/" })
91
- ] })
92
- ] })
93
- ] })
94
- ] }) }),
95
- Island({
96
- when: "idle",
97
- children: /* @__PURE__ */ jsx(
98
- SiteTreeNav,
99
- {
100
- tree: groupedTree,
101
- categoryOrder,
102
- categoryIgnore: ["inbox", "develop"]
103
- }
104
- )
105
- }),
106
- settings.docTags && tagCount > 0 && /* @__PURE__ */ jsxs("section", { class: "mt-vsp-xl", children: [
107
- /* @__PURE__ */ jsx("h2", { class: "text-title font-bold mb-vsp-md", children: t("doc.allTags", locale) }),
108
- /* @__PURE__ */ jsx("a", { href: withBase("/docs/tags"), class: "text-accent underline hover:text-accent-hover", children: t("doc.allTags", locale) })
109
- ] })
110
- ]
111
- }
112
- );
8
+ const { tree, categoryOrder, tagCount } = prepareHomeData(routeCtx, locale);
9
+ return /* @__PURE__ */ jsx(HomePageView, { locale, tree, categoryOrder, tagCount });
113
10
  }
114
11
  export {
115
12
  IndexPage as default,