@takazudo/zudo-doc 5.8.0 → 5.10.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 (104) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +1 -1
  3. package/dist/chrome/derive.d.ts +1 -1
  4. package/dist/chrome/derive.js +24 -2
  5. package/dist/config-assertions/index.d.ts +30 -0
  6. package/dist/config-assertions/index.js +41 -1
  7. package/dist/config.d.ts +18 -0
  8. package/dist/config.js +1 -0
  9. package/dist/doc-content-header/index.js +6 -0
  10. package/dist/doc-metainfo-area/index.js +1 -14
  11. package/dist/doc-page-renderer/index.d.ts +3 -3
  12. package/dist/doc-page-renderer/index.js +1 -1
  13. package/dist/doc-page-shell/index.d.ts +6 -0
  14. package/dist/doc-pager/index.d.ts +6 -0
  15. package/dist/doc-pager/index.js +5 -2
  16. package/dist/docs-schema/index.d.ts +11 -0
  17. package/dist/docs-schema/index.js +8 -1
  18. package/dist/format-date/index.d.ts +12 -0
  19. package/dist/format-date/index.js +64 -0
  20. package/dist/frontmatter-preview-defaults/index.js +6 -1
  21. package/dist/header/header.js +9 -1
  22. package/dist/home-page/index.js +3 -1
  23. package/dist/html-preview-wrapper/highlight-runtime.d.ts +7 -7
  24. package/dist/html-preview-wrapper/highlight-runtime.js +1 -1
  25. package/dist/i18n-defaults/index.js +3 -0
  26. package/dist/integrations/changelog/emit.js +19 -2
  27. package/dist/mdx-components/index.d.ts +4 -1
  28. package/dist/mdx-components/index.js +3 -1
  29. package/dist/nav-indexing/index.d.ts +2 -0
  30. package/dist/nav-indexing/index.js +2 -0
  31. package/dist/nav-indexing/note-tray-index.d.ts +29 -0
  32. package/dist/nav-indexing/note-tray-index.js +120 -0
  33. package/dist/note-tray-index/index.d.ts +45 -0
  34. package/dist/note-tray-index/index.js +52 -0
  35. package/dist/note-tray-model/index.d.ts +31 -0
  36. package/dist/note-tray-model/index.js +74 -0
  37. package/dist/plugins/codex-resources.d.ts +4 -0
  38. package/dist/plugins/codex-resources.js +31 -0
  39. package/dist/plugins/internal/claude-resources/generate.js +20 -291
  40. package/dist/plugins/internal/codex-resources/agents-md.d.ts +6 -0
  41. package/dist/plugins/internal/codex-resources/agents-md.js +70 -0
  42. package/dist/plugins/internal/codex-resources/agents.d.ts +7 -0
  43. package/dist/plugins/internal/codex-resources/agents.js +104 -0
  44. package/dist/plugins/internal/codex-resources/config.d.ts +6 -0
  45. package/dist/plugins/internal/codex-resources/config.js +111 -0
  46. package/dist/plugins/internal/codex-resources/generate.d.ts +20 -0
  47. package/dist/plugins/internal/codex-resources/generate.js +34 -0
  48. package/dist/plugins/internal/codex-resources/hooks.d.ts +6 -0
  49. package/dist/plugins/internal/codex-resources/hooks.js +174 -0
  50. package/dist/plugins/internal/codex-resources/index.d.ts +24 -0
  51. package/dist/plugins/internal/codex-resources/index.js +21 -0
  52. package/dist/plugins/internal/codex-resources/overview.d.ts +10 -0
  53. package/dist/plugins/internal/codex-resources/overview.js +33 -0
  54. package/dist/plugins/internal/codex-resources/rules.d.ts +6 -0
  55. package/dist/plugins/internal/codex-resources/rules.js +255 -0
  56. package/dist/plugins/internal/codex-resources/skills.d.ts +2 -0
  57. package/dist/plugins/internal/codex-resources/skills.js +84 -0
  58. package/dist/plugins/internal/codex-resources/utils.d.ts +14 -0
  59. package/dist/plugins/internal/codex-resources/utils.js +80 -0
  60. package/dist/plugins/internal/resource-docs-shared/fs.d.ts +3 -0
  61. package/dist/plugins/internal/resource-docs-shared/fs.js +19 -0
  62. package/dist/plugins/internal/resource-docs-shared/index.d.ts +7 -0
  63. package/dist/plugins/internal/resource-docs-shared/index.js +44 -0
  64. package/dist/plugins/internal/resource-docs-shared/links.d.ts +22 -0
  65. package/dist/plugins/internal/resource-docs-shared/links.js +44 -0
  66. package/dist/plugins/internal/resource-docs-shared/markdown-structure.d.ts +9 -0
  67. package/dist/plugins/internal/resource-docs-shared/markdown-structure.js +22 -0
  68. package/dist/plugins/internal/resource-docs-shared/mdx.d.ts +23 -0
  69. package/dist/plugins/internal/resource-docs-shared/mdx.js +63 -0
  70. package/dist/plugins/internal/resource-docs-shared/skills.d.ts +37 -0
  71. package/dist/plugins/internal/resource-docs-shared/skills.js +318 -0
  72. package/dist/plugins/internal/resource-docs-shared/walk.d.ts +20 -0
  73. package/dist/plugins/internal/resource-docs-shared/walk.js +41 -0
  74. package/dist/preset.d.ts +19 -0
  75. package/dist/preset.js +16 -1
  76. package/dist/safelist.css +1 -1
  77. package/dist/settings.d.ts +5 -0
  78. package/dist/sidebar/types.d.ts +6 -0
  79. package/dist/sidebar-tree/build-tree.js +8 -0
  80. package/dist/sidebar-tree/types.d.ts +12 -0
  81. package/dist/sidebar-tree-island/index.js +175 -1
  82. package/dist/sidebar-utils/index.d.ts +6 -0
  83. package/dist/site-schema/doc-route-entries.js +2 -0
  84. package/dist/site-schema/index.d.ts +1 -0
  85. package/dist/site-schema/index.js +3 -1
  86. package/dist/site-schema/nav-tree.js +17 -2
  87. package/dist/site-schema/note-tray-validate.d.ts +7 -0
  88. package/dist/site-schema/note-tray-validate.js +94 -0
  89. package/dist/site-schema/types.d.ts +11 -0
  90. package/dist/site-tree-nav-island/index.d.ts +5 -1
  91. package/dist/site-tree-nav-island/index.js +119 -12
  92. package/dist/theme-packs/drift/pack.css +6 -1
  93. package/dist/theme-packs/hearth/pack.css +5 -0
  94. package/dist/theme-packs/sakura/pack.css +6 -0
  95. package/dist/theme-packs/scandi/pack.css +7 -7
  96. package/dist/theme-packs/timberline/pack.css +5 -0
  97. package/eject/doc-pager/index.tsx +9 -0
  98. package/eject/header/header.tsx +26 -1
  99. package/eject/sidebar/types.ts +6 -0
  100. package/eject/sidebar-tree-island/index.tsx +238 -6
  101. package/eject/site-tree-nav-island/index.tsx +166 -16
  102. package/package.json +25 -8
  103. /package/dist/plugins/internal/{claude-resources → resource-docs-shared}/escape-for-mdx.d.ts +0 -0
  104. /package/dist/plugins/internal/{claude-resources → resource-docs-shared}/escape-for-mdx.js +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ 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.10.0] - 2026-08-22
8
+
9
+ ### Features
10
+
11
+ - Added first-class multi-package changelogs, including nested package navigation, per-package release pages, bilingual guidance, and the `create-zudo-doc --changelog-packages` option. (`91f179c8`, `302eac4f`)
12
+ - Upgraded the zfb package family to 2.9.0 and moved HtmlPreview to the public highlight-only WASM runtime, reducing the emitted WASM payload by about 55%. (`52291ec5`, `2911a651`)
13
+
14
+ ### Bug Fixes
15
+
16
+ - Hardened nested changelog navigation and added diagnostics when an emitter targets a multi-package landing directory instead of a package release directory. (`302eac4f`, `c4266e4a`)
17
+ - Normalized non-pill sidebar current rows in Drift, Hearth, Sakura, Scandi, and Timberline while preserving each pack's nested active treatment. (`7c4e0444`)
18
+
19
+ ### Other Changes
20
+
21
+ - Added bilingual documentation, publish-contract coverage, and positive browser detectors for the highlight-only runtime. (`b97ef30c`, `92269d49`)
22
+ - Added package-test hook headroom for concurrent CI load without changing subprocess deadlines. (`4d1417a9`)
23
+
24
+ ## [5.9.0] - 2026-08-20
25
+
26
+ ### Features
27
+
28
+ - Added Codex Resources generation for `AGENTS.md`, Codex configuration, custom agents, hooks, rules, and skills, with dedicated parsing and rendering for each resource type. (`666c12059`)
29
+ - Added the public Codex Resources plugin and wired it through package settings, presets, `create-zudo-doc`, and generated project configuration. (`752a3fe5a`, `a7fa47906`, `5831cd73e`)
30
+ - Added Codex Resources showcase data, navigation, localization, development watching, and complete English and Japanese documentation. (`a4dca1e40`, `1a2e6d5bc`, `f1396e7a1`)
31
+
32
+ ### Bug Fixes
33
+
34
+ - Added type validation and deterministic fallbacks for malformed skill frontmatter values. (`710a1d225`)
35
+ - Made generated Codex Resources frontmatter formatter-stable while preserving YAML string semantics and existing Claude Resources output.
36
+
37
+ ### Other Changes
38
+
39
+ - Extracted shared resource-documentation helpers while preserving existing Claude Resources output byte-for-byte. (`c9165d5cb`)
40
+ - Expanded generator, scaffold, build, and responsive navigation regression coverage for Codex Resources. (`142c11aa4`)
41
+
7
42
  ## [5.8.0] - 2026-08-20
8
43
 
9
44
  ### Features
package/README.md CHANGED
@@ -59,7 +59,7 @@ const breadcrumbs = buildBreadcrumbs(tree, "guides/color", homeHref);
59
59
 
60
60
  ## Optional peer dependency: `@takazudo/zfb-md-wasm`
61
61
 
62
- `./html-preview-wrapper`'s `<HighlightedCode>` lazily imports the package root and calls `highlightCode()` for client-side semantic syntax highlighting. `@takazudo/zfb-md-wasm` is declared as an **optional peerDependency** — install the same prerelease version as the rest of your zfb packages if you use that subpath:
62
+ `./html-preview-wrapper`'s `<HighlightedCode>` lazily imports the public `@takazudo/zfb-md-wasm/highlight` subpath and calls `highlightCode()` for client-side semantic syntax highlighting. `@takazudo/zfb-md-wasm` is declared as an **optional peerDependency** — install the same version as the rest of your zfb packages if you use that subpath:
63
63
 
64
64
  ```sh
65
65
  pnpm add @takazudo/zfb-md-wasm
@@ -152,7 +152,7 @@ export declare function deriveInlineVersionSwitcher(ctx: ChromeContext, getUnava
152
152
  * happens one layer up in `createMdxComponents()`, after the wrapper choice.
153
153
  */
154
154
  export declare function deriveMdxComponents(ctx: ChromeContext): {
155
- createMdxComponentsBound: (lang?: string, currentVersion?: string) => Record<string, unknown>;
155
+ createMdxComponentsBound: (lang?: string, currentVersion?: string, currentSlug?: string) => Record<string, unknown>;
156
156
  SiteTreeNavWrapper: FactoryComponent;
157
157
  };
158
158
  export type { Settings };
@@ -14,6 +14,7 @@ import { DEFAULT_THEME_PACK_SLUG } from "../theme-pack-switcher/theme-pack-sync.
14
14
  import { SearchWidget } from "../search-widget/index.js";
15
15
  import { createMdxComponents } from "../mdx-components/index.js";
16
16
  import { createCategoryNavWrapper } from "../category-nav/index.js";
17
+ import { createNoteTrayIndexWrapper } from "../note-tray-index/index.js";
17
18
  import { createCategoryTreeNavWrapper } from "../category-tree-nav/index.js";
18
19
  import { createSiteTreeNavWrapper } from "../site-tree-nav/index.js";
19
20
  import { Details } from "../details/index.js";
@@ -271,6 +272,25 @@ function deriveMdxComponents(ctx) {
271
272
  getCategoryOrder: ctx.getCategoryOrder,
272
273
  versionedDocsUrl: ctx.versionedDocsUrl
273
274
  });
275
+ const NoteTrayIndexWrapper = createNoteTrayIndexWrapper({
276
+ defaultLocale: ctx.defaultLocale,
277
+ docTags: ctx.settings.docTags,
278
+ resolveNavSource: ctx.resolveNavSource,
279
+ buildNavTree: ((docs, locale, categoryMeta) => ctx.buildNavTree(
280
+ docs,
281
+ locale,
282
+ categoryMeta,
283
+ (slug, loc) => ctx.docsUrl(slug, loc)
284
+ )),
285
+ findNode: ctx.findNode,
286
+ toRouteSlug: ctx.toRouteSlug,
287
+ resolveTag: (raw) => ctx.resolveTagBound(raw).canonical,
288
+ tagHref: (tag, locale) => ctx.withBase(
289
+ locale === ctx.defaultLocale ? `/docs/tags/${encodeURIComponent(tag)}` : `/${locale}/docs/tags/${encodeURIComponent(tag)}`
290
+ ),
291
+ t: (key, locale) => ctx.t(key, locale),
292
+ versionedDocsUrl: ctx.versionedDocsUrl
293
+ });
274
294
  function HtmlPreviewBound(props) {
275
295
  return HtmlPreviewWrapper({
276
296
  globalConfig: ctx.settings.htmlPreview ?? null,
@@ -286,15 +306,17 @@ function deriveMdxComponents(ctx) {
286
306
  PresetGenerator: (_props) => null
287
307
  };
288
308
  const mdxExtras = { ...mdxExtrasDefault, ...ctx.hostBindings.mdxExtras ?? {} };
289
- function createMdxComponentsBound(lang = ctx.defaultLocale, currentVersion) {
309
+ function createMdxComponentsBound(lang = ctx.defaultLocale, currentVersion, currentSlug = "") {
290
310
  return createMdxComponents({
291
311
  settings: ctx.settings,
292
312
  locale: lang,
293
313
  currentVersion,
314
+ currentSlug,
294
315
  navData: {
295
316
  CategoryNav: CategoryNavWrapper,
296
317
  CategoryTreeNav: CategoryTreeNavWrapper,
297
- SiteTreeNav: SiteTreeNavWrapper
318
+ SiteTreeNav: SiteTreeNavWrapper,
319
+ NoteTrayIndex: NoteTrayIndexWrapper
298
320
  },
299
321
  // Package-owned content components wired here so an INJECTED docs route
300
322
  // renders MDX using these tags without the "MDX requires '<X>' to be
@@ -1,4 +1,17 @@
1
1
  import type { FaviconConfig } from "../settings.js";
2
+ /** Structural subset of a header dropdown read by the category-match guard. */
3
+ export interface AmbiguousDropdownCategoryMatchItem {
4
+ label?: string;
5
+ categoryMatch?: string;
6
+ children?: Array<{
7
+ label?: string;
8
+ categoryMatch?: string;
9
+ }>;
10
+ }
11
+ /** Logger surface accepted by the non-throwing header-nav diagnostic. */
12
+ export interface ConfigAssertionLogger {
13
+ warn(message: string): void;
14
+ }
2
15
  /** Structural subset of `Settings` {@link assertNoEmptyStringFaviconOrLogo} reads. */
3
16
  export interface EmptyStringFaviconOrLogoSubject {
4
17
  logo?: string | false;
@@ -29,3 +42,20 @@ export interface EmptyStringFaviconOrLogoSubject {
29
42
  * object is not an empty string).
30
43
  */
31
44
  export declare function assertNoEmptyStringFaviconOrLogo(settings: EmptyStringFaviconOrLogoSubject): void;
45
+ /**
46
+ * Warn about `categoryMatch` values that cannot express the intended
47
+ * dropdown grouping.
48
+ *
49
+ * A category matcher is compared with the first slug segment by the nav-scope
50
+ * resolver, so a value containing `/` never matches. Likewise, duplicate
51
+ * child values make every matching child category-active. Children grouped
52
+ * under one top-level directory should omit `categoryMatch` and rely on the
53
+ * deepest matching child path; Learn-style children should use distinct
54
+ * top-level values.
55
+ *
56
+ * This is intentionally a diagnostic rather than a config assertion: malformed
57
+ * navigation should remain buildable, and each offending value is reported at
58
+ * most once per parent dropdown. The `"!"` matcher is the intentional default
59
+ * bucket and is excluded from both checks.
60
+ */
61
+ export declare function warnAmbiguousDropdownCategoryMatch(headerNav: readonly AmbiguousDropdownCategoryMatchItem[] | undefined, logger?: ConfigAssertionLogger): void;
@@ -19,6 +19,46 @@ function assertNoEmptyStringFaviconOrLogo(settings) {
19
19
  }
20
20
  }
21
21
  }
22
+ function warnAmbiguousDropdownCategoryMatch(headerNav, logger = console) {
23
+ if (!headerNav) return;
24
+ for (const item of headerNav) {
25
+ const children = item.children;
26
+ if (!children || children.length === 0) continue;
27
+ const childCounts = /* @__PURE__ */ new Map();
28
+ for (const child of children) {
29
+ const value = child.categoryMatch;
30
+ if (value == null || value === "!") continue;
31
+ childCounts.set(value, (childCounts.get(value) ?? 0) + 1);
32
+ }
33
+ const offendingValues = [];
34
+ const addOffendingValue = (value) => {
35
+ if (value == null || value === "!" || offendingValues.includes(value)) return;
36
+ offendingValues.push(value);
37
+ };
38
+ if (item.categoryMatch?.includes("/")) {
39
+ addOffendingValue(item.categoryMatch);
40
+ }
41
+ for (const child of children) {
42
+ if (child.categoryMatch?.includes("/")) {
43
+ addOffendingValue(child.categoryMatch);
44
+ }
45
+ }
46
+ for (const [value, count] of childCounts) {
47
+ if (count >= 2) addOffendingValue(value);
48
+ }
49
+ for (const value of offendingValues) {
50
+ const reasons = [
51
+ ...value.includes("/") ? ["contains `/` and never matches a top-level slug"] : [],
52
+ ...(childCounts.get(value) ?? 0) >= 2 ? ["is shared by multiple children"] : []
53
+ ].join("; ");
54
+ const parentLabel = item.label ?? "(unnamed)";
55
+ logger.warn(
56
+ `zudo-doc: header dropdown "${parentLabel}" has categoryMatch "${value}" that ${reasons}. Children grouped under one top-level directory should omit categoryMatch (active state follows the deepest matching child path); Learn-style children should use distinct top-level values.`
57
+ );
58
+ }
59
+ }
60
+ }
22
61
  export {
23
- assertNoEmptyStringFaviconOrLogo
62
+ assertNoEmptyStringFaviconOrLogo,
63
+ warnAmbiguousDropdownCategoryMatch
24
64
  };
package/dist/config.d.ts CHANGED
@@ -72,6 +72,12 @@ type ClaudeResourcesConfig = {
72
72
  projectRoot?: string;
73
73
  scanRoot?: string;
74
74
  } | false;
75
+ /** The `settings.codexResources` block (or `false` when disabled). */
76
+ type CodexResourcesConfig = {
77
+ codexDir: string;
78
+ projectRoot?: string;
79
+ scanRoot?: string;
80
+ } | false;
75
81
  export declare const DEFAULT_SETTINGS: Settings;
76
82
  export interface ZudoDocConfig {
77
83
  /**
@@ -372,6 +378,18 @@ export interface ZudoDocConfig {
372
378
  * @default false
373
379
  */
374
380
  claudeResources?: ClaudeResourcesConfig;
381
+ /**
382
+ * Codex-resources ingestion config, or `false` to disable.
383
+ * `codexDir` is the `.codex/` directory, resolved against `projectRoot`.
384
+ * `projectRoot` anchors relative paths and the output base. `scanRoot` is
385
+ * the repo-wide discovery root for the `AGENTS.md` / `AGENTS.override.md`
386
+ * walk (including its relPath base for titles/slugs) and the repo-level
387
+ * `.agents/skills/` root; it defaults to `projectRoot`. This lets a doc site
388
+ * in a repo subdirectory scan repo-wide instructions and team skills while
389
+ * writing into its own collection (#2558).
390
+ * @default false
391
+ */
392
+ codexResources?: CodexResourcesConfig;
375
393
  /**
376
394
  * Route prefixes served only in the default locale (never locale-prefixed).
377
395
  * @default []
package/dist/config.js CHANGED
@@ -70,6 +70,7 @@ const DEFAULT_SETTINGS = {
70
70
  htmlPreview: void 0,
71
71
  versions: false,
72
72
  claudeResources: false,
73
+ codexResources: false,
73
74
  defaultLocaleOnlyPrefixes: [],
74
75
  footer: false,
75
76
  headerNav: [],
@@ -3,6 +3,7 @@ import { FrontmatterPreview } from "../metainfo/index.js";
3
3
  import { createDocMetainfoArea } from "../doc-metainfo-area/index.js";
4
4
  import { createDocTagsArea } from "../doc-tags-area/index.js";
5
5
  import { assertChromeContext } from "../chrome/assert-chrome-context.js";
6
+ import { formatDate } from "../format-date/index.js";
6
7
  function createDocContentHeader(ctx) {
7
8
  assertChromeContext(ctx, "createDocContentHeader");
8
9
  const t = ctx.t;
@@ -27,6 +28,11 @@ function createDocContentHeader(ctx) {
27
28
  isFallback,
28
29
  version
29
30
  }),
31
+ (entry.data.date || entry.data.updated) && /* @__PURE__ */ jsxs("p", { class: "text-caption text-muted mb-vsp-md", "data-doc-date": true, children: [
32
+ entry.data.date ? formatDate(entry.data.date, locale) : null,
33
+ entry.data.date && entry.data.updated ? " \xB7 " : null,
34
+ entry.data.updated ? `${t("doc.updated", locale)} ${formatDate(entry.data.updated, locale)}` : null
35
+ ] }),
30
36
  !version && /* @__PURE__ */ jsx(DocMetainfoArea, { slug, locale, isFallback }),
31
37
  !version && /* @__PURE__ */ jsx(DocTagsArea, { slug, locale, tags: entry.data.tags }),
32
38
  isFallback && !entry.data.generated && /* @__PURE__ */ jsx(
@@ -2,20 +2,7 @@ import { jsx } from "preact/jsx-runtime";
2
2
  import { DocMetainfo } from "../metainfo/index.js";
3
3
  import { toHistorySlug } from "../slug/index.js";
4
4
  import { assertChromeContext } from "../chrome/assert-chrome-context.js";
5
- const LOCALE_TO_BCP47 = {
6
- en: "en-US",
7
- ja: "ja-JP",
8
- de: "de-DE"
9
- };
10
- function formatDate(isoDate, locale) {
11
- const d = new Date(isoDate);
12
- if (isNaN(d.getTime())) return isoDate;
13
- return d.toLocaleDateString(LOCALE_TO_BCP47[locale] ?? "en-US", {
14
- year: "numeric",
15
- month: "short",
16
- day: "numeric"
17
- });
18
- }
5
+ import { formatDate } from "../format-date/index.js";
19
6
  function createDocMetainfoArea(ctx) {
20
7
  assertChromeContext(ctx, "createDocMetainfoArea");
21
8
  const settings = ctx.settings;
@@ -48,10 +48,10 @@ export interface DocPageRendererDeps {
48
48
  * Build locale-aware MDX components bag.
49
49
  * Host passes `createMdxComponents` (from `pages/_mdx-components.ts`).
50
50
  * `currentVersion` (#3218) threads the active `/v/{version}` slug into the
51
- * nav-card wrappers (CategoryNav/CategoryTreeNav/SiteTreeNav) so their hrefs
52
- * resolve within the version undefined on latest/unversioned pages.
51
+ * nav wrappers so their data and hrefs resolve within the version. The
52
+ * current slug lets NoteTrayIndex infer its containing tray.
53
53
  */
54
- createMdxComponents: (locale: string, currentVersion?: string) => Record<string, unknown>;
54
+ createMdxComponents: (locale: string, currentVersion: string | undefined, currentSlug: string) => Record<string, unknown>;
55
55
  /** Translate a UI string key for a locale. */
56
56
  t: (key: string, locale: string) => string;
57
57
  /**
@@ -35,7 +35,7 @@ function createRenderDocPage(ctx) {
35
35
  const slug = props.kind === "autoIndex" ? props.autoIndex.slug : props.entry.data.slug ?? toRouteSlug(props.entry.slug);
36
36
  const title = props.kind === "autoIndex" ? props.autoIndex.label : props.entry.data.title;
37
37
  const description = props.kind === "autoIndex" ? props.autoIndex.description : props.entry.data.description;
38
- const components = createMdxComponents(locale, version?.slug);
38
+ const components = createMdxComponents(locale, version?.slug, slug);
39
39
  const autoIndexChildren = props.kind === "autoIndex" ? version ? props.autoIndex.children.filter((c) => c.hasPage || c.children.length > 0) : props.autoIndex.children.filter((c) => c.hasPage || c.children.length > 0).map((c) => ({
40
40
  ...c,
41
41
  href: c.href ?? docsUrl(c.slug, locale)
@@ -22,6 +22,12 @@ export interface DocPageNavNode {
22
22
  href?: string;
23
23
  hasPage: boolean;
24
24
  children: DocPageNavNode[];
25
+ shape?: "note-tray";
26
+ noteTrayDated?: boolean;
27
+ noteTraySidebar?: "index" | "year" | "month";
28
+ date?: string;
29
+ updated?: string;
30
+ rank?: number;
25
31
  }
26
32
  /** Slots and parameters that vary between the 4 doc routes. */
27
33
  export interface DocPageShellProps {
@@ -6,6 +6,12 @@ import type { Settings } from "../settings.js";
6
6
  interface PagerNode {
7
7
  href?: string;
8
8
  label: string;
9
+ shape?: "note-tray";
10
+ noteTrayDated?: boolean;
11
+ noteTraySidebar?: "index" | "year" | "month";
12
+ date?: string;
13
+ updated?: string;
14
+ rank?: number;
9
15
  }
10
16
  export interface DocPagerProps {
11
17
  /** Previous page node (null = no previous page → renders placeholder). */
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "preact/jsx-runtime";
2
2
  import { ChevronLeft, ChevronRight } from "../icons/index.js";
3
3
  import { assertChromeContext } from "../chrome/assert-chrome-context.js";
4
+ import { formatDate } from "../format-date/index.js";
4
5
  function createDocPager(ctx) {
5
6
  assertChromeContext(ctx, "createDocPager");
6
7
  const t = ctx.t;
@@ -16,7 +17,8 @@ function createDocPager(ctx) {
16
17
  /* @__PURE__ */ jsx(ChevronLeft, { className: "h-[1.125rem] w-[1.125rem]" }),
17
18
  /* @__PURE__ */ jsx("span", { class: "no-underline", children: t("nav.previous", locale) })
18
19
  ] }),
19
- /* @__PURE__ */ jsx("p", { class: "text-small font-semibold underline group-hover:text-accent", children: prev.label })
20
+ /* @__PURE__ */ jsx("p", { class: "text-small font-semibold underline group-hover:text-accent", children: prev.label }),
21
+ prev.date && /* @__PURE__ */ jsx("p", { class: "text-caption text-muted", children: formatDate(prev.date, locale) })
20
22
  ]
21
23
  }
22
24
  ) : /* @__PURE__ */ jsx("div", {}),
@@ -30,7 +32,8 @@ function createDocPager(ctx) {
30
32
  /* @__PURE__ */ jsx("span", { class: "no-underline", children: t("nav.next", locale) }),
31
33
  /* @__PURE__ */ jsx(ChevronRight, { className: "h-[1.125rem] w-[1.125rem]" })
32
34
  ] }),
33
- /* @__PURE__ */ jsx("p", { class: "text-small font-semibold underline group-hover:text-accent", children: next.label })
35
+ /* @__PURE__ */ jsx("p", { class: "text-small font-semibold underline group-hover:text-accent", children: next.label }),
36
+ next.date && /* @__PURE__ */ jsx("p", { class: "text-caption text-muted", children: formatDate(next.date, locale) })
34
37
  ]
35
38
  }
36
39
  ) : /* @__PURE__ */ jsx("div", {})
@@ -58,6 +58,17 @@ export declare function buildDocsSchema(opts?: BuildDocsSchemaOptions): z.ZodObj
58
58
  asc: "asc";
59
59
  desc: "desc";
60
60
  }>>;
61
+ category_shape: z.ZodOptional<z.ZodEnum<{
62
+ "note-tray": "note-tray";
63
+ }>>;
64
+ note_tray_dated: z.ZodOptional<z.ZodBoolean>;
65
+ note_tray_sidebar: z.ZodOptional<z.ZodEnum<{
66
+ index: "index";
67
+ year: "year";
68
+ month: "month";
69
+ }>>;
70
+ date: z.ZodOptional<z.ZodString>;
71
+ updated: z.ZodOptional<z.ZodString>;
61
72
  }, z.core.$loose>;
62
73
  /**
63
74
  * TypeScript type inferred from the default docs frontmatter zod schema.
@@ -35,7 +35,14 @@ function buildDocsSchema(opts) {
35
35
  // `category_sort_order` sets the child sort direction. Frontmatter wins
36
36
  // over the sidecar.
37
37
  category_no_page: z.boolean().optional(),
38
- category_sort_order: z.enum(["asc", "desc"]).optional()
38
+ category_sort_order: z.enum(["asc", "desc"]).optional(),
39
+ category_shape: z.enum(["note-tray"]).optional(),
40
+ note_tray_dated: z.boolean().optional(),
41
+ note_tray_sidebar: z.enum(["index", "year", "month"]).optional(),
42
+ // Quote these values in YAML (for example, date: "2026-08-22") so
43
+ // parsers do not coerce them into Date objects before schema validation.
44
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
45
+ updated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
39
46
  }).passthrough();
40
47
  }
41
48
  export {
@@ -0,0 +1,12 @@
1
+ export interface IsoDateParts {
2
+ year: number;
3
+ month: number;
4
+ day: number;
5
+ }
6
+ export declare function parseIsoDate(iso: string): IsoDateParts | undefined;
7
+ /** Format an ISO date for display while retaining the established locale map. */
8
+ export declare function formatDate(iso: string, locale: string): string;
9
+ /** Format a year/month label with the year first in every locale. */
10
+ export declare function formatYearMonth(iso: string, locale: string): string;
11
+ /** Return the stable numeric month/day portion of a calendar-valid ISO date. */
12
+ export declare function formatMonthDay(iso: string): string;
@@ -0,0 +1,64 @@
1
+ const LOCALE_TO_BCP47 = {
2
+ en: "en-US",
3
+ ja: "ja-JP",
4
+ de: "de-DE"
5
+ };
6
+ function parseIsoDate(iso) {
7
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
8
+ if (!match) return void 0;
9
+ const year = Number(match[1]);
10
+ const month = Number(match[2]);
11
+ const day = Number(match[3]);
12
+ const date = /* @__PURE__ */ new Date(0);
13
+ date.setUTCHours(0, 0, 0, 0);
14
+ date.setUTCFullYear(year, month - 1, day);
15
+ if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
16
+ return void 0;
17
+ }
18
+ return { year, month, day };
19
+ }
20
+ function toUtcDate(iso) {
21
+ const parts = parseIsoDate(iso);
22
+ if (parts) {
23
+ const date = /* @__PURE__ */ new Date(0);
24
+ date.setUTCHours(0, 0, 0, 0);
25
+ date.setUTCFullYear(parts.year, parts.month - 1, parts.day);
26
+ return date;
27
+ }
28
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(iso)) return void 0;
29
+ const timestamp = new Date(iso);
30
+ return Number.isNaN(timestamp.getTime()) ? void 0 : timestamp;
31
+ }
32
+ function formatDate(iso, locale) {
33
+ const date = toUtcDate(iso);
34
+ if (!date) return iso;
35
+ return new Intl.DateTimeFormat(LOCALE_TO_BCP47[locale] ?? "en-US", {
36
+ year: "numeric",
37
+ month: "short",
38
+ day: "numeric",
39
+ timeZone: "UTC"
40
+ }).format(date);
41
+ }
42
+ function formatYearMonth(iso, locale) {
43
+ const date = toUtcDate(iso.length === 7 ? `${iso}-01` : iso);
44
+ if (!date) return iso;
45
+ const year = new Intl.DateTimeFormat(LOCALE_TO_BCP47[locale] ?? "en-US", {
46
+ year: "numeric",
47
+ timeZone: "UTC"
48
+ }).format(date);
49
+ const month = new Intl.DateTimeFormat(LOCALE_TO_BCP47[locale] ?? "en-US", {
50
+ month: "long",
51
+ timeZone: "UTC"
52
+ }).format(date);
53
+ return locale === "ja" ? `${year}${month}` : `${year} ${month}`;
54
+ }
55
+ function formatMonthDay(iso) {
56
+ const parts = parseIsoDate(iso);
57
+ return parts ? `${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}` : iso;
58
+ }
59
+ export {
60
+ formatDate,
61
+ formatMonthDay,
62
+ formatYearMonth,
63
+ parseIsoDate
64
+ };
@@ -18,7 +18,12 @@ const defaultFrontmatterPreviewIgnoreKeys = [
18
18
  "slug",
19
19
  "generated",
20
20
  "category_no_page",
21
- "category_sort_order"
21
+ "category_sort_order",
22
+ "category_shape",
23
+ "note_tray_dated",
24
+ "note_tray_sidebar",
25
+ "date",
26
+ "updated"
22
27
  ];
23
28
  export {
24
29
  defaultFrontmatterPreviewIgnoreKeys
@@ -44,7 +44,8 @@ function Header(props) {
44
44
  } = props;
45
45
  const isNonDefaultLocale = lang != null && lang !== i18n.defaultLocale;
46
46
  const pathWithoutBase = urlHelpers.stripBase(currentPath);
47
- const matchPath = pathForMatch(pathWithoutBase, lang, i18n.defaultLocale);
47
+ const pathWithoutVersion = stripCurrentVersionPrefix(pathWithoutBase, currentVersion);
48
+ const matchPath = pathForMatch(pathWithoutVersion, lang, i18n.defaultLocale);
48
49
  const activeNavPath = computeActiveNavPath(headerNav, matchPath);
49
50
  const rightItemDispatch = createRightItemDispatch(headerRightComponents);
50
51
  return /* @__PURE__ */ jsxs(
@@ -249,6 +250,13 @@ function renderNavItem(item, activeNavPath, activeCategory, lang, currentVersion
249
250
  }
250
251
  );
251
252
  }
253
+ function stripCurrentVersionPrefix(path, currentVersion) {
254
+ if (currentVersion == null || currentVersion === "") return path;
255
+ const prefix = `/v/${currentVersion}`;
256
+ if (path === prefix) return "/";
257
+ if (path.startsWith(`${prefix}/`)) return path.slice(prefix.length);
258
+ return path;
259
+ }
252
260
  function TriggerButton({
253
261
  index,
254
262
  id,
@@ -132,7 +132,9 @@ function createHomePageView(ctx) {
132
132
  tree,
133
133
  categoryOrder,
134
134
  categoryIgnore: ["inbox", "develop"],
135
- initiallyCollapsedCategorySlugs
135
+ initiallyCollapsedCategorySlugs,
136
+ locale,
137
+ updatedLabel: t("doc.updated", locale)
136
138
  }
137
139
  )
138
140
  }),
@@ -1,5 +1,5 @@
1
- import type { HighlightCodeOptions, HighlightCodeResult } from "@takazudo/zfb-md-wasm";
2
- type HighlightModule = Pick<typeof import("@takazudo/zfb-md-wasm"), "highlightCode">;
1
+ import type { HighlightCodeOptions, HighlightCodeResult } from "@takazudo/zfb-md-wasm/highlight";
2
+ type HighlightModule = Pick<typeof import("@takazudo/zfb-md-wasm/highlight"), "highlightCode">;
3
3
  export type HighlightModuleImporter = () => Promise<HighlightModule>;
4
4
  export interface HtmlPreviewHighlightRuntime {
5
5
  highlightCode(code: string, options: HighlightCodeOptions): Promise<HighlightCodeResult>;
@@ -7,11 +7,11 @@ export interface HtmlPreviewHighlightRuntime {
7
7
  /**
8
8
  * Build the HTML Preview's lazy zfb highlighting adapter.
9
9
  *
10
- * The cached value is only the public package-root module import. A rejected
11
- * import is evicted so a later source-panel mount can retry a transient chunk
12
- * load. Calls to `highlightCode` are deliberately not cached: the upstream
13
- * package owns WASM initialization and trap recovery, while the component owns
14
- * request cancellation and stale-result protection.
10
+ * The cached value is only the public `./highlight` subpath module import. A
11
+ * rejected import is evicted so a later source-panel mount can retry a transient
12
+ * chunk load. Calls to `highlightCode` are deliberately not cached: the
13
+ * upstream package owns WASM initialization and trap recovery, while the
14
+ * component owns request cancellation and stale-result protection.
15
15
  */
16
16
  export declare function createHighlightRuntime(importModule: HighlightModuleImporter): HtmlPreviewHighlightRuntime;
17
17
  /**
@@ -20,7 +20,7 @@ function createHighlightRuntime(importModule) {
20
20
  };
21
21
  }
22
22
  const defaultRuntime = createHighlightRuntime(
23
- () => import("@takazudo/zfb-md-wasm")
23
+ () => import("@takazudo/zfb-md-wasm/highlight")
24
24
  );
25
25
  function getUsableHighlightHtml(result) {
26
26
  if (result.html == null || result.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
@@ -6,6 +6,7 @@ const defaultTranslations = {
6
6
  "nav.components": "Components",
7
7
  "nav.reference": "Reference",
8
8
  "nav.claude": "Claude",
9
+ "nav.codex": "Codex",
9
10
  "nav.changelog": "Changelog",
10
11
  "nav.develop": "Develop",
11
12
  "nav.previous": "Previous",
@@ -65,6 +66,7 @@ const defaultTranslations = {
65
66
  "nav.components": "\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8",
66
67
  "nav.reference": "\u30EA\u30D5\u30A1\u30EC\u30F3\u30B9",
67
68
  "nav.claude": "Claude",
69
+ "nav.codex": "Codex",
68
70
  "nav.changelog": "\u5909\u66F4\u5C65\u6B74",
69
71
  "nav.develop": "\u958B\u767A",
70
72
  "nav.previous": "\u524D\u3078",
@@ -124,6 +126,7 @@ const defaultTranslations = {
124
126
  "nav.components": "Komponenten",
125
127
  "nav.reference": "Referenz",
126
128
  "nav.claude": "Claude",
129
+ "nav.codex": "Codex",
127
130
  "nav.changelog": "Changelog",
128
131
  "nav.develop": "Entwicklung",
129
132
  "nav.previous": "Zur\xFCck",
@@ -1,5 +1,5 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname, resolve } from "node:path";
1
+ import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
3
  import { generateChangelogMarkdown } from "./generate.js";
4
4
  import { loadChangelogEntries } from "./load.js";
5
5
  function emitChangelogs(options) {
@@ -8,6 +8,16 @@ function emitChangelogs(options) {
8
8
  const sourceDir = resolve(options.projectRoot, config.sourceDir);
9
9
  const outputFile = resolve(options.projectRoot, config.outputFile);
10
10
  const entries = loadChangelogEntries({ sourceDir });
11
+ if (entries.length === 0) {
12
+ const nestedMdxDirs = findNestedMdxDirs(sourceDir);
13
+ if (nestedMdxDirs.length > 0) {
14
+ options.logger?.warn?.(
15
+ `Changelog sourceDir "${config.sourceDir}" (${sourceDir}) yielded 0 releases but contains .mdx files in sub-directories: ${nestedMdxDirs.join(
16
+ ", "
17
+ )}. In a multi-changelog layout, each changelogs[] entry must point at a per-package directory (for example "${config.sourceDir}/<name>") whose per-release files are non-index .mdx files; the loader intentionally skips index.mdx.`
18
+ );
19
+ }
20
+ }
11
21
  const markdown = generateChangelogMarkdown(entries, {
12
22
  title: config.title,
13
23
  packageName: config.packageName
@@ -19,6 +29,13 @@ function emitChangelogs(options) {
19
29
  }
20
30
  return { written };
21
31
  }
32
+ function findNestedMdxDirs(sourceDir) {
33
+ return readdirSync(sourceDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter(
34
+ (entry) => readdirSync(join(sourceDir, entry.name), { withFileTypes: true }).some(
35
+ (nestedEntry) => nestedEntry.isFile() && nestedEntry.name.endsWith(".mdx")
36
+ )
37
+ ).map((entry) => entry.name).sort();
38
+ }
22
39
  export {
23
40
  emitChangelogs
24
41
  };