@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.
- package/CHANGELOG.md +31 -0
- package/README.md +1 -1
- package/dist/chrome/derive.d.ts +1 -1
- package/dist/chrome/derive.js +25 -2
- package/dist/config-assertions/index.d.ts +30 -0
- package/dist/config-assertions/index.js +41 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +1 -0
- package/dist/doc-content-header/index.js +6 -0
- package/dist/doc-metainfo-area/index.js +1 -14
- package/dist/doc-page-renderer/index.d.ts +3 -3
- package/dist/doc-page-renderer/index.js +1 -1
- package/dist/doc-page-shell/index.d.ts +6 -0
- package/dist/doc-pager/index.d.ts +6 -0
- package/dist/doc-pager/index.js +5 -2
- package/dist/docs-schema/index.d.ts +11 -0
- package/dist/docs-schema/index.js +11 -1
- package/dist/format-date/index.d.ts +12 -0
- package/dist/format-date/index.js +64 -0
- package/dist/frontmatter-preview-defaults/index.js +6 -1
- package/dist/header/header.js +9 -1
- package/dist/home-page/index.js +5 -2
- package/dist/html-preview-wrapper/highlight-runtime.d.ts +7 -7
- package/dist/html-preview-wrapper/highlight-runtime.js +1 -1
- package/dist/integrations/changelog/emit.js +19 -2
- package/dist/mdx-components/index.d.ts +4 -1
- package/dist/mdx-components/index.js +3 -1
- package/dist/nav-indexing/index.d.ts +2 -0
- package/dist/nav-indexing/index.js +2 -0
- package/dist/nav-indexing/note-tray-index.d.ts +29 -0
- package/dist/nav-indexing/note-tray-index.js +120 -0
- package/dist/note-tray-index/index.d.ts +45 -0
- package/dist/note-tray-index/index.js +52 -0
- package/dist/note-tray-model/index.d.ts +31 -0
- package/dist/note-tray-model/index.js +74 -0
- package/dist/preset.d.ts +11 -0
- package/dist/preset.js +5 -1
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +2 -0
- package/dist/sidebar/types.d.ts +6 -0
- package/dist/sidebar-tree/build-tree.js +8 -0
- package/dist/sidebar-tree/types.d.ts +12 -0
- package/dist/sidebar-tree-island/index.js +176 -1
- package/dist/sidebar-utils/index.d.ts +6 -0
- package/dist/site-schema/doc-route-entries.js +2 -0
- package/dist/site-schema/index.d.ts +1 -0
- package/dist/site-schema/index.js +3 -1
- package/dist/site-schema/nav-tree.js +17 -2
- package/dist/site-schema/note-tray-validate.d.ts +7 -0
- package/dist/site-schema/note-tray-validate.js +94 -0
- package/dist/site-schema/types.d.ts +11 -0
- package/dist/site-tree-nav/index.d.ts +7 -0
- package/dist/site-tree-nav/index.js +3 -2
- package/dist/site-tree-nav-island/index.d.ts +5 -1
- package/dist/site-tree-nav-island/index.js +119 -12
- package/dist/theme-packs/drift/pack.css +6 -1
- package/dist/theme-packs/hearth/pack.css +5 -0
- package/dist/theme-packs/sakura/pack.css +6 -0
- package/dist/theme-packs/scandi/pack.css +7 -7
- package/dist/theme-packs/timberline/pack.css +5 -0
- package/eject/doc-pager/index.tsx +9 -0
- package/eject/header/header.tsx +26 -1
- package/eject/sidebar/types.ts +6 -0
- package/eject/sidebar-tree-island/index.tsx +241 -6
- package/eject/site-tree-nav-island/index.tsx +166 -16
- 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
|
|
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 {
|
|
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) =>
|
|
53
|
-
|
|
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
|
-
|
|
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;
|