@takazudo/zudo-doc 5.9.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 (61) hide show
  1. package/CHANGELOG.md +17 -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/doc-content-header/index.js +6 -0
  8. package/dist/doc-metainfo-area/index.js +1 -14
  9. package/dist/doc-page-renderer/index.d.ts +3 -3
  10. package/dist/doc-page-renderer/index.js +1 -1
  11. package/dist/doc-page-shell/index.d.ts +6 -0
  12. package/dist/doc-pager/index.d.ts +6 -0
  13. package/dist/doc-pager/index.js +5 -2
  14. package/dist/docs-schema/index.d.ts +11 -0
  15. package/dist/docs-schema/index.js +8 -1
  16. package/dist/format-date/index.d.ts +12 -0
  17. package/dist/format-date/index.js +64 -0
  18. package/dist/frontmatter-preview-defaults/index.js +6 -1
  19. package/dist/header/header.js +9 -1
  20. package/dist/home-page/index.js +3 -1
  21. package/dist/html-preview-wrapper/highlight-runtime.d.ts +7 -7
  22. package/dist/html-preview-wrapper/highlight-runtime.js +1 -1
  23. package/dist/integrations/changelog/emit.js +19 -2
  24. package/dist/mdx-components/index.d.ts +4 -1
  25. package/dist/mdx-components/index.js +3 -1
  26. package/dist/nav-indexing/index.d.ts +2 -0
  27. package/dist/nav-indexing/index.js +2 -0
  28. package/dist/nav-indexing/note-tray-index.d.ts +29 -0
  29. package/dist/nav-indexing/note-tray-index.js +120 -0
  30. package/dist/note-tray-index/index.d.ts +45 -0
  31. package/dist/note-tray-index/index.js +52 -0
  32. package/dist/note-tray-model/index.d.ts +31 -0
  33. package/dist/note-tray-model/index.js +74 -0
  34. package/dist/preset.d.ts +11 -0
  35. package/dist/preset.js +5 -1
  36. package/dist/safelist.css +1 -1
  37. package/dist/sidebar/types.d.ts +6 -0
  38. package/dist/sidebar-tree/build-tree.js +8 -0
  39. package/dist/sidebar-tree/types.d.ts +12 -0
  40. package/dist/sidebar-tree-island/index.js +175 -1
  41. package/dist/sidebar-utils/index.d.ts +6 -0
  42. package/dist/site-schema/doc-route-entries.js +2 -0
  43. package/dist/site-schema/index.d.ts +1 -0
  44. package/dist/site-schema/index.js +3 -1
  45. package/dist/site-schema/nav-tree.js +17 -2
  46. package/dist/site-schema/note-tray-validate.d.ts +7 -0
  47. package/dist/site-schema/note-tray-validate.js +94 -0
  48. package/dist/site-schema/types.d.ts +11 -0
  49. package/dist/site-tree-nav-island/index.d.ts +5 -1
  50. package/dist/site-tree-nav-island/index.js +119 -12
  51. package/dist/theme-packs/drift/pack.css +6 -1
  52. package/dist/theme-packs/hearth/pack.css +5 -0
  53. package/dist/theme-packs/sakura/pack.css +6 -0
  54. package/dist/theme-packs/scandi/pack.css +7 -7
  55. package/dist/theme-packs/timberline/pack.css +5 -0
  56. package/eject/doc-pager/index.tsx +9 -0
  57. package/eject/header/header.tsx +26 -1
  58. package/eject/sidebar/types.ts +6 -0
  59. package/eject/sidebar-tree-island/index.tsx +238 -6
  60. package/eject/site-tree-nav-island/index.tsx +166 -16
  61. 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(
@@ -147,6 +154,9 @@ function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMen
147
154
  () => localeLinks || themeDefaultMode ? /* @__PURE__ */ jsx(SidebarFooter, { links: localeLinks, themeDefaultMode }) : null,
148
155
  [localeLinks, themeDefaultMode]
149
156
  );
157
+ const noteTrayRoot = nodes.length === 1 && nodes[0]?.shape === "note-tray" ? nodes[0] : void 0;
158
+ const filteredNoteTrayRoot = noteTrayRoot ? filteredNodes.find((node) => node.slug === noteTrayRoot.slug) : void 0;
159
+ const locale = localeLinks?.find((link) => link.active)?.code ?? "en";
150
160
  if (showingRootMenu && rootMenuItems) {
151
161
  return /* @__PURE__ */ jsxs("nav", { children: [
152
162
  /* @__PURE__ */ jsxs(
@@ -199,7 +209,16 @@ function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMen
199
209
  }
200
210
  )
201
211
  ] }) }),
202
- /* @__PURE__ */ jsx(
212
+ noteTrayRoot ? filteredNoteTrayRoot && /* @__PURE__ */ jsx(
213
+ TrayList,
214
+ {
215
+ tray: filteredNoteTrayRoot,
216
+ itemCount: getNoteTrayItems(noteTrayRoot).length,
217
+ currentSlug: activeSlug,
218
+ forceOpen: !!query,
219
+ locale
220
+ }
221
+ ) : /* @__PURE__ */ jsx(
203
222
  NodeList,
204
223
  {
205
224
  nodes: filteredNodes,
@@ -212,6 +231,161 @@ function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMen
212
231
  ] });
213
232
  }
214
233
  SidebarTree.displayName = "SidebarTree";
234
+ function TrayList({
235
+ tray,
236
+ itemCount,
237
+ currentSlug,
238
+ forceOpen,
239
+ locale
240
+ }) {
241
+ const items = getNoteTrayItems(tray);
242
+ const sidebarStyle = tray.noteTraySidebar ?? "index";
243
+ const width = rankWidth(itemCount);
244
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
245
+ /* @__PURE__ */ jsx(LeafNode, { node: tray, currentSlug, depth: 0, isLast: items.length === 0 }),
246
+ sidebarStyle === "index" ? items.map((item, index) => /* @__PURE__ */ jsx(
247
+ TrayItem,
248
+ {
249
+ item,
250
+ currentSlug,
251
+ rankDigits: width,
252
+ isLast: index === items.length - 1
253
+ },
254
+ item.slug
255
+ )) : groupItems(items, sidebarStyle, tray.sortOrder ?? "asc").map((group, index, groups) => /* @__PURE__ */ jsx(
256
+ TrayGroupNode,
257
+ {
258
+ traySlug: tray.slug,
259
+ group,
260
+ grouping: sidebarStyle,
261
+ locale,
262
+ currentSlug,
263
+ forceOpen,
264
+ isLast: index === groups.length - 1
265
+ },
266
+ group.key
267
+ ))
268
+ ] });
269
+ }
270
+ function TrayItem({
271
+ item,
272
+ currentSlug,
273
+ rankDigits,
274
+ isLast,
275
+ showDate = false,
276
+ depth = 1
277
+ }) {
278
+ if (!item.href) return null;
279
+ const isActive = item.slug === currentSlug;
280
+ const labelHtml = smartBreakToHtml(item.label);
281
+ const shortDate = item.date ? formatMonthDay(item.date) : void 0;
282
+ return /* @__PURE__ */ jsx("div", { className: isLast ? "pb-vsp-md" : "", children: /* @__PURE__ */ jsxs("div", { className: "relative", children: [
283
+ /* @__PURE__ */ jsx(ConnectorLines, { depth, isLast, topPad: "var(--spacing-vsp-2xs)" }),
284
+ /* @__PURE__ */ jsxs(
285
+ "a",
286
+ {
287
+ href: item.href,
288
+ "aria-current": isActive ? "page" : void 0,
289
+ "data-nav-active": isActive ? "" : void 0,
290
+ 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"}`,
291
+ style: { paddingLeft: padLeft(depth, false) },
292
+ children: [
293
+ 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") }),
294
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1", dangerouslySetInnerHTML: { __html: labelHtml } }),
295
+ showDate && shortDate && /* @__PURE__ */ jsx("span", { className: `shrink-0 tabular-nums${isActive ? "" : " text-muted"}`, children: shortDate })
296
+ ]
297
+ }
298
+ )
299
+ ] }) });
300
+ }
301
+ function noteTrayGroupStorageKey(traySlug, groupKey) {
302
+ return `${traySlug}#${groupKey}`;
303
+ }
304
+ function TrayGroupNode({
305
+ traySlug,
306
+ group,
307
+ grouping,
308
+ locale,
309
+ currentSlug,
310
+ forceOpen,
311
+ isLast
312
+ }) {
313
+ const containsCurrent = group.items.some((item) => item.slug === currentSlug);
314
+ const [open, setOpen] = useState(containsCurrent);
315
+ const storageKey = noteTrayGroupStorageKey(traySlug, group.key);
316
+ const label = grouping === "year" ? group.key : formatYearMonthLabel(group.key, locale);
317
+ useEffect(() => {
318
+ const stored = getOpenSet();
319
+ if (stored.has(storageKey) && !open) setOpen(true);
320
+ }, []);
321
+ useEffect(() => {
322
+ if (containsCurrent && !open) {
323
+ setOpen(true);
324
+ const stored = getOpenSet();
325
+ stored.add(storageKey);
326
+ saveOpenSet(stored);
327
+ }
328
+ }, [containsCurrent]);
329
+ useEffect(() => {
330
+ if (open) {
331
+ const stored = getOpenSet();
332
+ if (!stored.has(storageKey)) {
333
+ stored.add(storageKey);
334
+ saveOpenSet(stored);
335
+ }
336
+ }
337
+ }, [open, storageKey]);
338
+ const toggle = useCallback(() => {
339
+ setOpen((previous) => {
340
+ const next = !previous;
341
+ const stored = getOpenSet();
342
+ if (next) stored.add(storageKey);
343
+ else stored.delete(storageKey);
344
+ saveOpenSet(stored);
345
+ return next;
346
+ });
347
+ }, [storageKey]);
348
+ const isExpanded = forceOpen || open;
349
+ return /* @__PURE__ */ jsxs("div", { className: !isLast && isExpanded ? "relative" : "", children: [
350
+ !isLast && isExpanded && /* @__PURE__ */ jsx(
351
+ "div",
352
+ {
353
+ className: "absolute border-l border-solid border-muted z-local-1",
354
+ style: { left: connectorLeft(1), top: 0, bottom: 0 }
355
+ }
356
+ ),
357
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
358
+ /* @__PURE__ */ jsx(ConnectorLines, { depth: 1, isLast, topPad: "var(--spacing-vsp-xs)" }),
359
+ /* @__PURE__ */ jsxs(
360
+ "button",
361
+ {
362
+ type: "button",
363
+ onClick: toggle,
364
+ 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",
365
+ style: { paddingLeft: padLeft(1, true) },
366
+ "aria-expanded": isExpanded,
367
+ "aria-label": isExpanded ? `Collapse ${label}` : `Expand ${label}`,
368
+ "data-zd-sidebar-open-key": storageKey,
369
+ children: [
370
+ /* @__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" }) }),
371
+ /* @__PURE__ */ jsx("span", { children: label })
372
+ ]
373
+ }
374
+ )
375
+ ] }),
376
+ isExpanded && /* @__PURE__ */ jsx("div", { children: group.items.map((item, index) => /* @__PURE__ */ jsx(
377
+ TrayItem,
378
+ {
379
+ item,
380
+ currentSlug,
381
+ isLast: index === group.items.length - 1,
382
+ showDate: true,
383
+ depth: 2
384
+ },
385
+ item.slug
386
+ )) })
387
+ ] });
388
+ }
215
389
  const NodeList = memo(function NodeList2({
216
390
  nodes,
217
391
  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[];
@@ -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;
@@ -250,6 +250,12 @@ html[data-theme-pack="sakura"] #desktop-sidebar a,
250
250
  html[data-theme-pack="sakura"] aside[data-zd-mobile-sidebar] a {
251
251
  border-radius: 9999px;
252
252
  }
253
+ /* Non-pill current rows stay square in both sidebar surfaces; the
254
+ data-nav-active rule below owns Sakura's nested petal shape. */
255
+ html[data-theme-pack="sakura"] #desktop-sidebar a[aria-current="page"]:not([data-nav-active]),
256
+ html[data-theme-pack="sakura"] aside[data-zd-mobile-sidebar] a[aria-current="page"]:not([data-nav-active]) {
257
+ border-radius: 0;
258
+ }
253
259
  html[data-theme-pack="sakura"] #desktop-sidebar a:not([data-nav-active]):hover,
254
260
  html[data-theme-pack="sakura"] aside[data-zd-mobile-sidebar] a:not([data-nav-active]):hover {
255
261
  text-decoration: none;