@takazudo/zudo-doc 5.9.0 → 5.11.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 (66) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -1
  3. package/dist/chrome/derive.d.ts +1 -1
  4. package/dist/chrome/derive.js +25 -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 +9 -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 +11 -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 +5 -2
  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/integrations/changelog/emit.js +19 -2
  26. package/dist/mdx-components/index.d.ts +4 -1
  27. package/dist/mdx-components/index.js +3 -1
  28. package/dist/nav-indexing/index.d.ts +2 -0
  29. package/dist/nav-indexing/index.js +2 -0
  30. package/dist/nav-indexing/note-tray-index.d.ts +29 -0
  31. package/dist/nav-indexing/note-tray-index.js +120 -0
  32. package/dist/note-tray-index/index.d.ts +45 -0
  33. package/dist/note-tray-index/index.js +52 -0
  34. package/dist/note-tray-model/index.d.ts +31 -0
  35. package/dist/note-tray-model/index.js +74 -0
  36. package/dist/preset.d.ts +11 -0
  37. package/dist/preset.js +5 -1
  38. package/dist/safelist.css +1 -1
  39. package/dist/settings.d.ts +2 -0
  40. package/dist/sidebar/types.d.ts +6 -0
  41. package/dist/sidebar-tree/build-tree.js +8 -0
  42. package/dist/sidebar-tree/types.d.ts +12 -0
  43. package/dist/sidebar-tree-island/index.js +176 -1
  44. package/dist/sidebar-utils/index.d.ts +6 -0
  45. package/dist/site-schema/doc-route-entries.js +2 -0
  46. package/dist/site-schema/index.d.ts +1 -0
  47. package/dist/site-schema/index.js +3 -1
  48. package/dist/site-schema/nav-tree.js +17 -2
  49. package/dist/site-schema/note-tray-validate.d.ts +7 -0
  50. package/dist/site-schema/note-tray-validate.js +94 -0
  51. package/dist/site-schema/types.d.ts +11 -0
  52. package/dist/site-tree-nav/index.d.ts +7 -0
  53. package/dist/site-tree-nav/index.js +3 -2
  54. package/dist/site-tree-nav-island/index.d.ts +5 -1
  55. package/dist/site-tree-nav-island/index.js +119 -12
  56. package/dist/theme-packs/drift/pack.css +6 -1
  57. package/dist/theme-packs/hearth/pack.css +5 -0
  58. package/dist/theme-packs/sakura/pack.css +6 -0
  59. package/dist/theme-packs/scandi/pack.css +7 -7
  60. package/dist/theme-packs/timberline/pack.css +5 -0
  61. package/eject/doc-pager/index.tsx +9 -0
  62. package/eject/header/header.tsx +26 -1
  63. package/eject/sidebar/types.ts +6 -0
  64. package/eject/sidebar-tree-island/index.tsx +241 -6
  65. package/eject/site-tree-nav-island/index.tsx +166 -16
  66. package/package.json +20 -8
@@ -11,6 +11,13 @@ import { filterTree } from "../sidebar-filter/index.js";
11
11
  import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
12
12
  import { CURRENT_PATH_DATASET_KEY, readCurrentPath } from "../current-path/index.js";
13
13
  import { ensureSidebarScrollPreserve } from "./sidebar-scroll-preserve.js";
14
+ import {
15
+ formatYearMonthLabel,
16
+ getNoteTrayItems,
17
+ groupItems,
18
+ rankWidth
19
+ } from "../note-tray-model/index.js";
20
+ import { formatMonthDay } from "../format-date/index.js";
14
21
  ensureSidebarScrollPreserve();
15
22
  function ToggleChevron({ isExpanded, className }) {
16
23
  return /* @__PURE__ */ jsx(
@@ -21,6 +28,7 @@ function ToggleChevron({ isExpanded, className }) {
21
28
  );
22
29
  }
23
30
  const STORAGE_KEY = "zd-sidebar-open";
31
+ const GROUPED_TRAY_ITEM_DEPTH = 3;
24
32
  function padLeft(depth, forCategory) {
25
33
  if (depth === 0) return `calc(${BASE_PAD} + ${forCategory ? "0.15rem" : "0rem"})`;
26
34
  return `calc(${depth} * ${INDENT} + 1.25rem + 5px)`;
@@ -147,6 +155,9 @@ function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMen
147
155
  () => localeLinks || themeDefaultMode ? /* @__PURE__ */ jsx(SidebarFooter, { links: localeLinks, themeDefaultMode }) : null,
148
156
  [localeLinks, themeDefaultMode]
149
157
  );
158
+ const noteTrayRoot = nodes.length === 1 && nodes[0]?.shape === "note-tray" ? nodes[0] : void 0;
159
+ const filteredNoteTrayRoot = noteTrayRoot ? filteredNodes.find((node) => node.slug === noteTrayRoot.slug) : void 0;
160
+ const locale = localeLinks?.find((link) => link.active)?.code ?? "en";
150
161
  if (showingRootMenu && rootMenuItems) {
151
162
  return /* @__PURE__ */ jsxs("nav", { children: [
152
163
  /* @__PURE__ */ jsxs(
@@ -199,7 +210,16 @@ function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMen
199
210
  }
200
211
  )
201
212
  ] }) }),
202
- /* @__PURE__ */ jsx(
213
+ noteTrayRoot ? filteredNoteTrayRoot && /* @__PURE__ */ jsx(
214
+ TrayList,
215
+ {
216
+ tray: filteredNoteTrayRoot,
217
+ itemCount: getNoteTrayItems(noteTrayRoot).length,
218
+ currentSlug: activeSlug,
219
+ forceOpen: !!query,
220
+ locale
221
+ }
222
+ ) : /* @__PURE__ */ jsx(
203
223
  NodeList,
204
224
  {
205
225
  nodes: filteredNodes,
@@ -212,6 +232,161 @@ function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMen
212
232
  ] });
213
233
  }
214
234
  SidebarTree.displayName = "SidebarTree";
235
+ function TrayList({
236
+ tray,
237
+ itemCount,
238
+ currentSlug,
239
+ forceOpen,
240
+ locale
241
+ }) {
242
+ const items = getNoteTrayItems(tray);
243
+ const sidebarStyle = tray.noteTraySidebar ?? "index";
244
+ const width = rankWidth(itemCount);
245
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
246
+ /* @__PURE__ */ jsx(LeafNode, { node: tray, currentSlug, depth: 0, isLast: items.length === 0 }),
247
+ sidebarStyle === "index" ? items.map((item, index) => /* @__PURE__ */ jsx(
248
+ TrayItem,
249
+ {
250
+ item,
251
+ currentSlug,
252
+ rankDigits: width,
253
+ isLast: index === items.length - 1
254
+ },
255
+ item.slug
256
+ )) : groupItems(items, sidebarStyle, tray.sortOrder ?? "asc").map((group, index, groups) => /* @__PURE__ */ jsx(
257
+ TrayGroupNode,
258
+ {
259
+ traySlug: tray.slug,
260
+ group,
261
+ grouping: sidebarStyle,
262
+ locale,
263
+ currentSlug,
264
+ forceOpen,
265
+ isLast: index === groups.length - 1
266
+ },
267
+ group.key
268
+ ))
269
+ ] });
270
+ }
271
+ function TrayItem({
272
+ item,
273
+ currentSlug,
274
+ rankDigits,
275
+ isLast,
276
+ showDate = false,
277
+ depth = 1
278
+ }) {
279
+ if (!item.href) return null;
280
+ const isActive = item.slug === currentSlug;
281
+ const labelHtml = smartBreakToHtml(item.label);
282
+ const shortDate = item.date ? formatMonthDay(item.date) : void 0;
283
+ return /* @__PURE__ */ jsx("div", { className: isLast ? "pb-vsp-md" : "", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
284
+ /* @__PURE__ */ jsx(ConnectorLines, { depth, isLast, topPad: "var(--spacing-vsp-2xs)" }),
285
+ /* @__PURE__ */ jsxs(
286
+ "a",
287
+ {
288
+ href: item.href,
289
+ "aria-current": isActive ? "page" : void 0,
290
+ "data-nav-active": isActive ? "" : void 0,
291
+ className: `flex items-start gap-hsp-xs py-vsp-2xs pr-[4px] text-small break-words ${isActive ? "bg-fg font-medium text-bg" : "text-muted hover:text-accent hover:underline focus:underline focus:text-accent"}`,
292
+ style: { paddingLeft: padLeft(depth, false) },
293
+ children: [
294
+ rankDigits !== void 0 && /* @__PURE__ */ jsx("span", { className: `shrink-0 tabular-nums${isActive ? "" : " text-muted"}`, children: item.rank === void 0 ? "" : String(item.rank).padStart(rankDigits, "0") }),
295
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", dangerouslySetInnerHTML: { __html: labelHtml } }),
296
+ showDate && shortDate && /* @__PURE__ */ jsx("span", { className: `shrink-0 tabular-nums${isActive ? "" : " text-muted"}`, children: shortDate })
297
+ ]
298
+ }
299
+ )
300
+ ] }) });
301
+ }
302
+ function noteTrayGroupStorageKey(traySlug, groupKey) {
303
+ return `${traySlug}#${groupKey}`;
304
+ }
305
+ function TrayGroupNode({
306
+ traySlug,
307
+ group,
308
+ grouping,
309
+ locale,
310
+ currentSlug,
311
+ forceOpen,
312
+ isLast
313
+ }) {
314
+ const containsCurrent = group.items.some((item) => item.slug === currentSlug);
315
+ const [open, setOpen] = useState(containsCurrent);
316
+ const storageKey = noteTrayGroupStorageKey(traySlug, group.key);
317
+ const label = grouping === "year" ? group.key : formatYearMonthLabel(group.key, locale);
318
+ useEffect(() => {
319
+ const stored = getOpenSet();
320
+ if (stored.has(storageKey) && !open) setOpen(true);
321
+ }, []);
322
+ useEffect(() => {
323
+ if (containsCurrent && !open) {
324
+ setOpen(true);
325
+ const stored = getOpenSet();
326
+ stored.add(storageKey);
327
+ saveOpenSet(stored);
328
+ }
329
+ }, [containsCurrent]);
330
+ useEffect(() => {
331
+ if (open) {
332
+ const stored = getOpenSet();
333
+ if (!stored.has(storageKey)) {
334
+ stored.add(storageKey);
335
+ saveOpenSet(stored);
336
+ }
337
+ }
338
+ }, [open, storageKey]);
339
+ const toggle = useCallback(() => {
340
+ setOpen((previous) => {
341
+ const next = !previous;
342
+ const stored = getOpenSet();
343
+ if (next) stored.add(storageKey);
344
+ else stored.delete(storageKey);
345
+ saveOpenSet(stored);
346
+ return next;
347
+ });
348
+ }, [storageKey]);
349
+ const isExpanded = forceOpen || open;
350
+ return /* @__PURE__ */ jsxs("div", { className: !isLast && isExpanded ? "relative" : "", children: [
351
+ !isLast && isExpanded && /* @__PURE__ */ jsx(
352
+ "div",
353
+ {
354
+ className: "absolute border-l border-solid border-muted z-local-1",
355
+ style: { left: connectorLeft(1), top: 0, bottom: 0 }
356
+ }
357
+ ),
358
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
359
+ /* @__PURE__ */ jsx(ConnectorLines, { depth: 1, isLast, topPad: "var(--spacing-vsp-xs)" }),
360
+ /* @__PURE__ */ jsxs(
361
+ "button",
362
+ {
363
+ type: "button",
364
+ onClick: toggle,
365
+ className: "flex w-full items-center gap-hsp-md py-vsp-xs text-left text-small font-semibold text-fg hover:text-accent hover:underline focus:underline focus:text-accent break-words",
366
+ style: { paddingLeft: padLeft(1, true) },
367
+ "aria-expanded": isExpanded,
368
+ "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`,
369
+ "data-zd-sidebar-open-key": storageKey,
370
+ children: [
371
+ /* @__PURE__ */ jsx("span", { className: "aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted", children: /* @__PURE__ */ jsx(ToggleChevron, { isExpanded, className: "text-muted" }) }),
372
+ /* @__PURE__ */ jsx("span", { children: label })
373
+ ]
374
+ }
375
+ )
376
+ ] }),
377
+ isExpanded && /* @__PURE__ */ jsx("div", { children: group.items.map((item, index) => /* @__PURE__ */ jsx(
378
+ TrayItem,
379
+ {
380
+ item,
381
+ currentSlug,
382
+ isLast: index === group.items.length - 1,
383
+ showDate: true,
384
+ depth: GROUPED_TRAY_ITEM_DEPTH
385
+ },
386
+ item.slug
387
+ )) })
388
+ ] });
389
+ }
215
390
  const NodeList = memo(function NodeList2({
216
391
  nodes,
217
392
  currentSlug,
@@ -40,6 +40,12 @@ export interface SidebarNavNode {
40
40
  children: SidebarNavNode[];
41
41
  sortOrder?: "asc" | "desc";
42
42
  collapsed?: boolean;
43
+ shape?: "note-tray";
44
+ noteTrayDated?: boolean;
45
+ noteTraySidebar?: "index" | "year" | "month";
46
+ date?: string;
47
+ updated?: string;
48
+ rank?: number;
43
49
  }
44
50
  /** Category metadata shape (mirrors host's CategoryMeta). */
45
51
  export interface SidebarCategoryMeta {
@@ -6,6 +6,7 @@ import {
6
6
  remapNavChildHrefs
7
7
  } from "../doc-route-paths/index.js";
8
8
  import { buildBreadcrumbs as buildBreadcrumbsImpl } from "./nav-tree.js";
9
+ import { validateNoteTrays } from "./note-tray-validate.js";
9
10
  let nextFactoryId = 1;
10
11
  function createDocRouteEntries(ctx) {
11
12
  const factoryId = nextFactoryId++;
@@ -28,6 +29,7 @@ function createDocRouteEntries(ctx) {
28
29
  return memoizeDerived([source.docs], `docRouteEntries;${factoryId};${routeSig}`, () => {
29
30
  const { docs, navDocs, categoryMeta, localeSlugSet } = source;
30
31
  const tree = buildNavTree(navDocs, locale, categoryMeta);
32
+ validateNoteTrays(tree, docs);
31
33
  const breadcrumbTree = urlFor ? tree : buildNavTree(docs, locale, categoryMeta);
32
34
  const result = [];
33
35
  for (const entry of docs) {
@@ -15,3 +15,4 @@ export type { NavScopeHeaderNavItem, NavScopeNode } from "../nav-scope/index.js"
15
15
  export { buildSidebarTree } from "../sidebar-tree/build-tree.js";
16
16
  export { extractHeadings } from "../extract-headings/index.js";
17
17
  export { createDocRouteEntries } from "./doc-route-entries.js";
18
+ export { validateNoteTrays } from "./note-tray-validate.js";
@@ -23,6 +23,7 @@ import {
23
23
  import { buildSidebarTree } from "../sidebar-tree/build-tree.js";
24
24
  import { extractHeadings } from "../extract-headings/index.js";
25
25
  import { createDocRouteEntries } from "./doc-route-entries.js";
26
+ import { validateNoteTrays } from "./note-tray-validate.js";
26
27
  export {
27
28
  buildBreadcrumbs,
28
29
  buildNavTree,
@@ -42,5 +43,6 @@ export {
42
43
  remapNavChildHrefs,
43
44
  resolveDocPrevNext,
44
45
  rewriteNavHref,
45
- schemaVersion
46
+ schemaVersion,
47
+ validateNoteTrays
46
48
  };
@@ -12,7 +12,13 @@ function toNavNode(node) {
12
12
  href: node.href,
13
13
  hasPage: node.hasPage,
14
14
  children: node.children.map(toNavNode),
15
- sortOrder: node.sortOrder ?? "asc"
15
+ sortOrder: node.sortOrder ?? "asc",
16
+ shape: node.shape,
17
+ noteTrayDated: node.noteTrayDated,
18
+ noteTraySidebar: node.noteTraySidebar,
19
+ date: node.date,
20
+ updated: node.updated,
21
+ rank: node.rank
16
22
  };
17
23
  }
18
24
  function findRootIndexDoc(docs) {
@@ -35,7 +41,13 @@ function toRootNavNode(doc, locale, buildHref, categoryMeta) {
35
41
  href: noPage ? void 0 : buildHref("", locale),
36
42
  hasPage: noPage !== true,
37
43
  children: [],
38
- sortOrder
44
+ sortOrder,
45
+ shape: doc.data.category_shape,
46
+ noteTrayDated: doc.data.note_tray_dated,
47
+ noteTraySidebar: doc.data.note_tray_sidebar,
48
+ date: doc.data.date,
49
+ updated: doc.data.updated,
50
+ rank: 1
39
51
  };
40
52
  }
41
53
  function buildNavTree(docs, locale, categoryMeta, buildHref) {
@@ -60,6 +72,9 @@ function buildNavTree(docs, locale, categoryMeta, buildHref) {
60
72
  if (posCompare !== 0) return posCompare;
61
73
  return a.slug.localeCompare(b.slug);
62
74
  });
75
+ result.forEach((node, index) => {
76
+ node.rank = index + 1;
77
+ });
63
78
  }
64
79
  return result;
65
80
  }
@@ -0,0 +1,7 @@
1
+ import type { DocEntryLike, DocNavNode } from "./types.js";
2
+ /**
3
+ * Validate note-tray declarations against both the visible nav tree and the
4
+ * effective merged collection. The latter is essential: unlisted children do
5
+ * not appear in `tree`, but still belong to a tray and must obey its contract.
6
+ */
7
+ export declare function validateNoteTrays(tree: readonly DocNavNode[], docs: readonly DocEntryLike[]): void;
@@ -0,0 +1,94 @@
1
+ import { parseIsoDate } from "../format-date/index.js";
2
+ import { toRouteSlug } from "../slug/index.js";
3
+ function entrySlug(entry) {
4
+ return entry.data.slug ?? toRouteSlug(entry.slug);
5
+ }
6
+ function findNode(tree, slug) {
7
+ for (const node of tree) {
8
+ if (node.slug === slug) return node;
9
+ const found = findNode(node.children, slug);
10
+ if (found) return found;
11
+ }
12
+ return void 0;
13
+ }
14
+ function isCategoryIndexEntry(entry) {
15
+ return entry.slug === "index" || entry.slug.endsWith("/index");
16
+ }
17
+ function validateNoteTrays(tree, docs) {
18
+ const violations = [];
19
+ const trays = docs.filter((doc) => doc.data.category_shape === "note-tray");
20
+ for (const tray of trays) {
21
+ const slug = entrySlug(tray);
22
+ const node = findNode(tree, slug);
23
+ if (!slug || slug.includes("/")) {
24
+ violations.push({
25
+ rule: "a",
26
+ slug: slug || "index",
27
+ detail: "category_shape is only allowed on top-level categories"
28
+ });
29
+ }
30
+ if (!isCategoryIndexEntry(tray)) {
31
+ violations.push({
32
+ rule: "a",
33
+ slug,
34
+ detail: "a note tray must be declared by a category index.mdx"
35
+ });
36
+ }
37
+ if (tray.data.category_no_page === true || tray.data.unlisted === true || tray.data.standalone === true || !node || node.hasPage === false) {
38
+ violations.push({
39
+ rule: "e",
40
+ slug,
41
+ detail: "a note-tray index must be a visible routed page"
42
+ });
43
+ }
44
+ if ((tray.data.note_tray_sidebar === "year" || tray.data.note_tray_sidebar === "month") && tray.data.note_tray_dated !== true) {
45
+ violations.push({
46
+ rule: "d",
47
+ slug,
48
+ detail: `${tray.data.note_tray_sidebar} grouping requires note_tray_dated: true`
49
+ });
50
+ }
51
+ const prefix = `${slug}/`;
52
+ const children = docs.filter((doc) => entrySlug(doc).startsWith(prefix));
53
+ for (const child of children) {
54
+ const childSlug = entrySlug(child);
55
+ const relative = childSlug.slice(prefix.length);
56
+ const childNode = findNode(tree, childSlug);
57
+ if (relative.includes("/") || isCategoryIndexEntry(child) || (childNode?.children.length ?? 0) > 0) {
58
+ violations.push({
59
+ rule: "b",
60
+ slug: childSlug,
61
+ detail: `note-tray children must be flat leaf files (tray: ${slug})`
62
+ });
63
+ }
64
+ if (tray.data.note_tray_dated === true && !child.data.date) {
65
+ violations.push({
66
+ rule: "c",
67
+ slug: childSlug,
68
+ detail: `dated note-tray children require date (tray: ${slug})`
69
+ });
70
+ }
71
+ }
72
+ }
73
+ for (const doc of docs) {
74
+ const slug = entrySlug(doc) || "index";
75
+ for (const field of ["date", "updated"]) {
76
+ const value = doc.data[field];
77
+ if (typeof value === "string" && !parseIsoDate(value)) {
78
+ violations.push({
79
+ rule: "f",
80
+ slug,
81
+ detail: `${field} is not a calendar-valid YYYY-MM-DD value: ${value}`
82
+ });
83
+ }
84
+ }
85
+ }
86
+ if (violations.length > 0) {
87
+ const lines = violations.sort((a, b) => a.slug.localeCompare(b.slug) || a.rule.localeCompare(b.rule)).map(({ rule, slug, detail }) => `- [${rule}] ${slug}: ${detail}`);
88
+ throw new Error(`Invalid note-tray configuration:
89
+ ${lines.join("\n")}`);
90
+ }
91
+ }
92
+ export {
93
+ validateNoteTrays
94
+ };
@@ -19,6 +19,12 @@ export interface DocNavNode {
19
19
  children: DocNavNode[];
20
20
  sortOrder?: "asc" | "desc";
21
21
  collapsed?: boolean;
22
+ shape?: "note-tray";
23
+ noteTrayDated?: boolean;
24
+ noteTraySidebar?: "index" | "year" | "month";
25
+ date?: string;
26
+ updated?: string;
27
+ rank?: number;
22
28
  }
23
29
  /** A category node with children but no page of its own — an auto-index. */
24
30
  export interface AutoIndexNode extends DocNavNode {
@@ -42,6 +48,11 @@ export interface DocPageFrontmatter {
42
48
  sidebar_label?: string;
43
49
  category_no_page?: boolean;
44
50
  category_sort_order?: "asc" | "desc";
51
+ category_shape?: "note-tray";
52
+ note_tray_dated?: boolean;
53
+ note_tray_sidebar?: "index" | "year" | "month";
54
+ date?: string;
55
+ updated?: string;
45
56
  pagination_prev?: string | null;
46
57
  pagination_next?: string | null;
47
58
  tags?: string[];
@@ -75,6 +75,13 @@ export interface SiteTreeNavDeps {
75
75
  * loud dev warning is emitted) rather than throwing.
76
76
  */
77
77
  versionedDocsUrl?: (slug: string, versionSlug: string, lang: string) => string;
78
+ /**
79
+ * Top-level category slugs hidden from the SiteTreeNav island. This optional
80
+ * field keeps hand-constructed `SiteTreeNavDeps` compatible with the frozen
81
+ * `./site-tree-nav` public subpath; omitted means nothing is hidden. The
82
+ * package chrome passes `settings.siteTreeNavIgnore`.
83
+ */
84
+ categoryIgnore?: string[];
78
85
  }
79
86
  /**
80
87
  * Create the `SiteTreeNavWrapper` component bound to the host's nav source
@@ -9,7 +9,8 @@ function createSiteTreeNavWrapper(deps) {
9
9
  buildNavTree,
10
10
  groupSatelliteNodes,
11
11
  getCategoryOrder,
12
- versionedDocsUrl
12
+ versionedDocsUrl,
13
+ categoryIgnore
13
14
  } = deps;
14
15
  function SiteTreeNavWrapper({
15
16
  lang = defaultLocale,
@@ -42,7 +43,7 @@ function createSiteTreeNavWrapper(deps) {
42
43
  {
43
44
  tree,
44
45
  categoryOrder,
45
- categoryIgnore: ["inbox", "develop"],
46
+ categoryIgnore,
46
47
  ariaLabel
47
48
  }
48
49
  )
@@ -6,8 +6,12 @@ export interface SiteTreeNavProps {
6
6
  categoryIgnore?: string[];
7
7
  /** Root-category slugs that should start collapsed. */
8
8
  initiallyCollapsedCategorySlugs?: string[];
9
+ /** Locale used by dated note-tray rows. */
10
+ locale?: string;
11
+ /** Localized label shown before an item's updated date. */
12
+ updatedLabel?: string;
9
13
  }
10
- export declare function SiteTreeNav({ tree, ariaLabel, categoryOrder, categoryIgnore, initiallyCollapsedCategorySlugs, }: SiteTreeNavProps): import("preact").JSX.Element;
14
+ export declare function SiteTreeNav({ tree, ariaLabel, categoryOrder, categoryIgnore, initiallyCollapsedCategorySlugs, locale, updatedLabel, }: SiteTreeNavProps): import("preact").JSX.Element;
11
15
  export declare namespace SiteTreeNav {
12
16
  var displayName: string;
13
17
  }
@@ -1,8 +1,21 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
3
3
  import { useState } from "preact/hooks";
4
- import { INDENT, connectorLeft, ConnectorLines, CategoryLinkIcon } from "../tree-nav-shared/index.js";
4
+ import {
5
+ INDENT,
6
+ connectorLeft,
7
+ ConnectorLines,
8
+ CategoryLinkIcon
9
+ } from "../tree-nav-shared/index.js";
5
10
  import { ChevronRight } from "../icons/index.js";
11
+ import {
12
+ formatDate,
13
+ formatYearMonthLabel,
14
+ getNoteTrayItems,
15
+ groupItems,
16
+ rankWidth
17
+ } from "../note-tray-model/index.js";
18
+ import { formatMonthDay } from "../format-date/index.js";
6
19
  import { initialCategoryOpenState, toggleCategoryOpenState } from "./state.js";
7
20
  const SITE_BASE_PAD = "clamp(0.5rem, 0.8vw, 1rem)";
8
21
  function padLeft(depth) {
@@ -29,7 +42,9 @@ function SiteTreeNav({
29
42
  ariaLabel = "Site index",
30
43
  categoryOrder,
31
44
  categoryIgnore,
32
- initiallyCollapsedCategorySlugs
45
+ initiallyCollapsedCategorySlugs,
46
+ locale = "en",
47
+ updatedLabel = "Updated"
33
48
  }) {
34
49
  let processedTree = tree;
35
50
  if (categoryIgnore) {
@@ -49,15 +64,22 @@ function SiteTreeNav({
49
64
  style: {
50
65
  gridTemplateColumns: "repeat(auto-fill, minmax(min(18rem, 100%), 1fr))"
51
66
  },
52
- children: processedTree.map((node) => /* @__PURE__ */ jsx("div", { className: "min-w-0 border border-muted pl-hsp-sm py-vsp-2xs", children: node.children.length > 0 ? /* @__PURE__ */ jsx(
53
- CategoryNode,
54
- {
55
- node,
56
- depth: 0,
57
- isLast: true,
58
- initiallyCollapsed: initiallyCollapsed.has(node.slug)
67
+ children: processedTree.map((node) => {
68
+ if (node.shape === "note-tray" && getNoteTrayItems(node).length === 0) {
69
+ return null;
59
70
  }
60
- ) : /* @__PURE__ */ jsx(LeafNode, { node, depth: 0, isLast: true }) }, node.slug))
71
+ return /* @__PURE__ */ jsx("div", { className: "min-w-0 border border-muted pl-hsp-sm py-vsp-2xs", children: node.children.length > 0 ? /* @__PURE__ */ jsx(
72
+ CategoryNode,
73
+ {
74
+ node,
75
+ depth: 0,
76
+ isLast: true,
77
+ initiallyCollapsed: initiallyCollapsed.has(node.slug),
78
+ locale,
79
+ updatedLabel
80
+ }
81
+ ) : /* @__PURE__ */ jsx(LeafNode, { node, depth: 0, isLast: true }) }, node.slug);
82
+ })
61
83
  }
62
84
  );
63
85
  }
@@ -88,7 +110,9 @@ function CategoryNode({
88
110
  node,
89
111
  depth,
90
112
  isLast,
91
- initiallyCollapsed = false
113
+ initiallyCollapsed = false,
114
+ locale = "en",
115
+ updatedLabel = "Updated"
92
116
  }) {
93
117
  const [open, setOpen] = useState(() => initialCategoryOpenState(initiallyCollapsed));
94
118
  const toggle = () => setOpen(toggleCategoryOpenState);
@@ -155,9 +179,92 @@ function CategoryNode({
155
179
  }
156
180
  )
157
181
  ] }),
158
- open && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(NodeList, { nodes: node.children, depth: depth + 1 }) })
182
+ open && /* @__PURE__ */ jsx("div", { children: node.shape === "note-tray" && depth === 0 ? /* @__PURE__ */ jsx(NoteTrayNodeList, { node, locale, updatedLabel }) : /* @__PURE__ */ jsx(NodeList, { nodes: node.children, depth: depth + 1 }) })
159
183
  ] });
160
184
  }
185
+ function NoteTrayNodeList({
186
+ node,
187
+ locale,
188
+ updatedLabel
189
+ }) {
190
+ const items = getNoteTrayItems(node);
191
+ const width = rankWidth(items);
192
+ const showDate = node.noteTrayDated === true;
193
+ const grouping = showDate && node.noteTraySidebar !== "index" ? node.noteTraySidebar : void 0;
194
+ if (grouping === "year" || grouping === "month") {
195
+ return /* @__PURE__ */ jsx("div", { className: "pl-hsp-md", children: groupItems(items, grouping, node.sortOrder ?? "asc").map((group) => /* @__PURE__ */ jsxs("div", { "data-note-tray-group": group.key, children: [
196
+ /* @__PURE__ */ jsx("div", { className: "pt-vsp-sm pb-vsp-2xs text-micro tracking-wide uppercase text-muted", children: grouping === "year" ? group.key : formatYearMonthLabel(group.key, locale) }),
197
+ group.items.map((item) => /* @__PURE__ */ jsx(
198
+ NoteTrayRow,
199
+ {
200
+ item,
201
+ locale,
202
+ updatedLabel,
203
+ rankWidth: width,
204
+ showDate,
205
+ groupedDate: true
206
+ },
207
+ item.slug
208
+ ))
209
+ ] }, group.key)) });
210
+ }
211
+ return /* @__PURE__ */ jsx("div", { className: "pl-hsp-md", children: items.map((item) => /* @__PURE__ */ jsx(
212
+ NoteTrayRow,
213
+ {
214
+ item,
215
+ locale,
216
+ updatedLabel,
217
+ rankWidth: width,
218
+ showDate,
219
+ groupedDate: false
220
+ },
221
+ item.slug
222
+ )) });
223
+ }
224
+ function NoteTrayRow({
225
+ item,
226
+ locale,
227
+ updatedLabel,
228
+ rankWidth: width,
229
+ showDate,
230
+ groupedDate
231
+ }) {
232
+ if (!item.href) return null;
233
+ const dateLabel = showDate && item.date ? groupedDate ? formatMonthDay(item.date) : formatDate(item.date, locale) : void 0;
234
+ return /* @__PURE__ */ jsxs(
235
+ "a",
236
+ {
237
+ href: item.href,
238
+ "data-note-tray-row": true,
239
+ className: "flex items-start gap-hsp-sm py-vsp-2xs text-small text-fg hover:text-accent hover:underline focus:underline",
240
+ children: [
241
+ dateLabel ? /* @__PURE__ */ jsx(
242
+ "time",
243
+ {
244
+ dateTime: item.date,
245
+ className: "shrink-0 font-mono tabular-nums text-caption text-muted",
246
+ children: dateLabel
247
+ }
248
+ ) : /* @__PURE__ */ jsx(
249
+ "span",
250
+ {
251
+ className: "shrink-0 font-mono tabular-nums text-caption text-muted",
252
+ style: { width: `${width}ch` },
253
+ children: item.rank === void 0 ? "" : String(item.rank).padStart(width, "0")
254
+ }
255
+ ),
256
+ /* @__PURE__ */ jsxs("span", { className: "min-w-0", children: [
257
+ /* @__PURE__ */ jsx("span", { children: item.label }),
258
+ item.updated && /* @__PURE__ */ jsxs("span", { className: "block text-micro text-muted", children: [
259
+ updatedLabel,
260
+ " ",
261
+ formatDate(item.updated, locale)
262
+ ] })
263
+ ] })
264
+ ]
265
+ }
266
+ );
267
+ }
161
268
  function LeafNode({
162
269
  node,
163
270
  depth,
@@ -9,7 +9,7 @@
9
9
  same desaturated blue accent family, never harsh white. Chrome is
10
10
  ringed by glow rather than drawn by border: hairline rings +
11
11
  layered soft shadows on code, cards, and admonitions; fixed
12
- radial light pools drift at opposite page corners; nav/TOC
12
+ radial light pools drift at opposite page corners; nested nav/TOC
13
13
  actives are pill-shaped and glowing. Syntax is a hushed pastel
14
14
  palette around the blue axis.
15
15
 
@@ -283,6 +283,11 @@ html[data-theme-pack="drift"] #desktop-sidebar a {
283
283
  background-color 0.2s,
284
284
  box-shadow 0.25s;
285
285
  }
286
+ /* Non-pill current rows keep the stock full-width band. The explicit
287
+ data-nav-active marker below is reserved for Drift's nested glow pill. */
288
+ html[data-theme-pack="drift"] #desktop-sidebar a[aria-current="page"]:not([data-nav-active]) {
289
+ border-radius: 0;
290
+ }
286
291
  html[data-theme-pack="drift"] #desktop-sidebar a:hover {
287
292
  text-decoration: none;
288
293
  background: color-mix(in oklch, var(--zd-accent) 10%, transparent);
@@ -214,6 +214,11 @@ html[data-theme-pack="hearth"] #desktop-sidebar a {
214
214
  background-color 0.15s ease,
215
215
  color 0.15s ease;
216
216
  }
217
+ /* Keep the structural current-row band square; data-nav-active is the
218
+ explicit hook for Hearth's nested brick cushion. */
219
+ html[data-theme-pack="hearth"] #desktop-sidebar a[aria-current="page"]:not([data-nav-active]) {
220
+ border-radius: 0;
221
+ }
217
222
  html[data-theme-pack="hearth"] #desktop-sidebar a:hover {
218
223
  color: var(--zd-accent);
219
224
  text-decoration: none;