@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
|
@@ -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;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Scandi — zudo-doc theme pack (ADR docs/adr/theme-packs.md)
|
|
3
3
|
|
|
4
4
|
Hygge minimalism: an oat/ecru room with sage and clay accents,
|
|
5
|
-
pill-shaped nav actives, an even 8px radius discipline, and a
|
|
5
|
+
pill-shaped nested nav actives, an even 8px radius discipline, and a
|
|
6
6
|
linen-subtle weave on the page body (two cheap repeating
|
|
7
7
|
gradients at ~2% ink — text backdrops stay effectively solid).
|
|
8
8
|
Light = a Scandinavian interior at midday; dark = hygge night —
|
|
@@ -180,12 +180,12 @@ html[data-theme-pack="scandi"] aside[data-zd-mobile-sidebar] a:hover:not([data-n
|
|
|
180
180
|
background: color-mix(in srgb, var(--zd-accent) 10%, transparent);
|
|
181
181
|
color: var(--zd-accent-hover);
|
|
182
182
|
}
|
|
183
|
-
/*
|
|
184
|
-
data-nav-active
|
|
185
|
-
|
|
186
|
-
html[data-theme-pack="scandi"] #desktop-sidebar a[aria-current="page"],
|
|
187
|
-
html[data-theme-pack="scandi"] aside[data-zd-mobile-sidebar] a[aria-current="page"] {
|
|
188
|
-
border-radius:
|
|
183
|
+
/* Non-pill current rows keep the structural full-width band square in
|
|
184
|
+
both sidebar surfaces. The data-nav-active rule below owns the flat
|
|
185
|
+
nested pill. */
|
|
186
|
+
html[data-theme-pack="scandi"] #desktop-sidebar a[aria-current="page"]:not([data-nav-active]),
|
|
187
|
+
html[data-theme-pack="scandi"] aside[data-zd-mobile-sidebar] a[aria-current="page"]:not([data-nav-active]) {
|
|
188
|
+
border-radius: 0;
|
|
189
189
|
}
|
|
190
190
|
/* active leaf item: THE signature — a full pill in deep sage
|
|
191
191
|
(candle amber at night), oat text, flat as painted wood. Fill
|
|
@@ -243,6 +243,11 @@ html[data-theme-pack="timberline"] #desktop-sidebar a {
|
|
|
243
243
|
background-color 0.15s ease,
|
|
244
244
|
color 0.15s ease;
|
|
245
245
|
}
|
|
246
|
+
/* Keep the structural current-row plank square; data-nav-active is the
|
|
247
|
+
explicit hook for Timberline's nested chamfer. */
|
|
248
|
+
html[data-theme-pack="timberline"] #desktop-sidebar a[aria-current="page"]:not([data-nav-active]) {
|
|
249
|
+
border-radius: 0;
|
|
250
|
+
}
|
|
246
251
|
html[data-theme-pack="timberline"] #desktop-sidebar a:hover:not([data-nav-active]):not([aria-current="page"]) {
|
|
247
252
|
color: var(--zd-accent);
|
|
248
253
|
text-decoration: none;
|
|
@@ -12,11 +12,18 @@ import { ChevronLeft, ChevronRight } from "../icons/index.js";
|
|
|
12
12
|
import type { ChromeContext } from "../factory-context/index.js";
|
|
13
13
|
import type { Settings } from "../settings.js";
|
|
14
14
|
import { assertChromeContext } from "../chrome/assert-chrome-context.js";
|
|
15
|
+
import { formatDate } from "../format-date/index.js";
|
|
15
16
|
|
|
16
17
|
// NavNode is a superset; we only need the fields the pager uses.
|
|
17
18
|
interface PagerNode {
|
|
18
19
|
href?: string;
|
|
19
20
|
label: string;
|
|
21
|
+
shape?: "note-tray";
|
|
22
|
+
noteTrayDated?: boolean;
|
|
23
|
+
noteTraySidebar?: "index" | "year" | "month";
|
|
24
|
+
date?: string;
|
|
25
|
+
updated?: string;
|
|
26
|
+
rank?: number;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export interface DocPagerProps {
|
|
@@ -63,6 +70,7 @@ export function createDocPager<S extends Settings = Settings>(
|
|
|
63
70
|
<p class="text-small font-semibold underline group-hover:text-accent">
|
|
64
71
|
{prev.label}
|
|
65
72
|
</p>
|
|
73
|
+
{prev.date && <p class="text-caption text-muted">{formatDate(prev.date, locale)}</p>}
|
|
66
74
|
</a>
|
|
67
75
|
) : (
|
|
68
76
|
<div />
|
|
@@ -79,6 +87,7 @@ export function createDocPager<S extends Settings = Settings>(
|
|
|
79
87
|
<p class="text-small font-semibold underline group-hover:text-accent">
|
|
80
88
|
{next.label}
|
|
81
89
|
</p>
|
|
90
|
+
{next.date && <p class="text-caption text-muted">{formatDate(next.date, locale)}</p>}
|
|
82
91
|
</a>
|
|
83
92
|
) : (
|
|
84
93
|
<div />
|
package/eject/header/header.tsx
CHANGED
|
@@ -282,7 +282,14 @@ export function Header(props: HeaderProps): JSX.Element {
|
|
|
282
282
|
|
|
283
283
|
const isNonDefaultLocale = lang != null && lang !== i18n.defaultLocale;
|
|
284
284
|
const pathWithoutBase = urlHelpers.stripBase(currentPath);
|
|
285
|
-
|
|
285
|
+
// `navHref` adds the active version prefix to rendered links, but the
|
|
286
|
+
// configured header paths are intentionally unversioned. Remove the
|
|
287
|
+
// version segment before locale normalization so SSR active state agrees
|
|
288
|
+
// with those configured paths on archived pages too. Keep this local to
|
|
289
|
+
// the renderer: `nav-active.ts` is embedded verbatim in the frozen
|
|
290
|
+
// overflow script and must remain unchanged.
|
|
291
|
+
const pathWithoutVersion = stripCurrentVersionPrefix(pathWithoutBase, currentVersion);
|
|
292
|
+
const matchPath = pathForMatch(pathWithoutVersion, lang, i18n.defaultLocale);
|
|
286
293
|
|
|
287
294
|
const activeNavPath = computeActiveNavPath(headerNav, matchPath);
|
|
288
295
|
const rightItemDispatch = createRightItemDispatch(headerRightComponents);
|
|
@@ -542,6 +549,24 @@ function renderNavItem(
|
|
|
542
549
|
);
|
|
543
550
|
}
|
|
544
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Remove the active `/v/{version}` route prefix from a pathname.
|
|
554
|
+
*
|
|
555
|
+
* Version slugs are a path segment, so `/v/1.0` must not strip a path such as
|
|
556
|
+
* `/v/1.01/...`. The equality branch also keeps the route root normalized to
|
|
557
|
+
* `/` instead of returning an empty string. This helper deliberately lives in
|
|
558
|
+
* `header.tsx`; `nav-active.ts` is copied into the CSP-hash-pinned overflow
|
|
559
|
+
* script and is not part of the SSR-only version normalization seam.
|
|
560
|
+
*/
|
|
561
|
+
function stripCurrentVersionPrefix(path: string, currentVersion?: string): string {
|
|
562
|
+
if (currentVersion == null || currentVersion === "") return path;
|
|
563
|
+
|
|
564
|
+
const prefix = `/v/${currentVersion}`;
|
|
565
|
+
if (path === prefix) return "/";
|
|
566
|
+
if (path.startsWith(`${prefix}/`)) return path.slice(prefix.length);
|
|
567
|
+
return path;
|
|
568
|
+
}
|
|
569
|
+
|
|
545
570
|
type RightItemContext = Omit<HeaderRightComponentProps, "item" | "index">;
|
|
546
571
|
|
|
547
572
|
/**
|
package/eject/sidebar/types.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface SidebarNavNode {
|
|
|
35
35
|
children: SidebarNavNode[];
|
|
36
36
|
sortOrder?: "asc" | "desc";
|
|
37
37
|
collapsed?: boolean;
|
|
38
|
+
shape?: "note-tray";
|
|
39
|
+
noteTrayDated?: boolean;
|
|
40
|
+
noteTraySidebar?: "index" | "year" | "month";
|
|
41
|
+
date?: string;
|
|
42
|
+
updated?: string;
|
|
43
|
+
rank?: number;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/**
|
|
@@ -20,6 +20,14 @@ import { filterTree } from "../sidebar-filter/index.js";
|
|
|
20
20
|
import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
|
|
21
21
|
import { CURRENT_PATH_DATASET_KEY, readCurrentPath } from "../current-path/index.js";
|
|
22
22
|
import { ensureSidebarScrollPreserve } from "./sidebar-scroll-preserve.js";
|
|
23
|
+
import {
|
|
24
|
+
formatYearMonthLabel,
|
|
25
|
+
getNoteTrayItems,
|
|
26
|
+
groupItems,
|
|
27
|
+
rankWidth,
|
|
28
|
+
type NoteTrayGroup,
|
|
29
|
+
} from "../note-tray-model/index.js";
|
|
30
|
+
import { formatMonthDay } from "../format-date/index.js";
|
|
23
31
|
|
|
24
32
|
// The persisted aside can transiently tear down and re-mount its SidebarTree
|
|
25
33
|
// effect during a body swap. Keep navigation snapshot ownership at browser
|
|
@@ -36,6 +44,7 @@ function ToggleChevron({ isExpanded, className }: { isExpanded: boolean; classNa
|
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
const STORAGE_KEY = "zd-sidebar-open";
|
|
47
|
+
const GROUPED_TRAY_ITEM_DEPTH = 3;
|
|
39
48
|
|
|
40
49
|
function padLeft(depth: number, forCategory: boolean): string {
|
|
41
50
|
if (depth === 0) return `calc(${BASE_PAD} + ${forCategory ? "0.15rem" : "0rem"})`;
|
|
@@ -235,6 +244,12 @@ export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, ba
|
|
|
235
244
|
[localeLinks, themeDefaultMode],
|
|
236
245
|
);
|
|
237
246
|
|
|
247
|
+
const noteTrayRoot = nodes.length === 1 && nodes[0]?.shape === "note-tray" ? nodes[0] : undefined;
|
|
248
|
+
const filteredNoteTrayRoot = noteTrayRoot
|
|
249
|
+
? filteredNodes.find((node) => node.slug === noteTrayRoot.slug)
|
|
250
|
+
: undefined;
|
|
251
|
+
const locale = localeLinks?.find((link) => link.active)?.code ?? "en";
|
|
252
|
+
|
|
238
253
|
// Root menu view: show headerNav items as a simple list (Docusaurus-style)
|
|
239
254
|
if (showingRootMenu && rootMenuItems) {
|
|
240
255
|
return (
|
|
@@ -293,18 +308,238 @@ export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, ba
|
|
|
293
308
|
/>
|
|
294
309
|
</div>
|
|
295
310
|
</div>
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
311
|
+
{noteTrayRoot ? (
|
|
312
|
+
filteredNoteTrayRoot && (
|
|
313
|
+
<TrayList
|
|
314
|
+
tray={filteredNoteTrayRoot}
|
|
315
|
+
itemCount={getNoteTrayItems(noteTrayRoot).length}
|
|
316
|
+
currentSlug={activeSlug}
|
|
317
|
+
forceOpen={!!query}
|
|
318
|
+
locale={locale}
|
|
319
|
+
/>
|
|
320
|
+
)
|
|
321
|
+
) : (
|
|
322
|
+
<NodeList
|
|
323
|
+
nodes={filteredNodes}
|
|
324
|
+
currentSlug={activeSlug}
|
|
325
|
+
depth={0}
|
|
326
|
+
forceOpen={!!query}
|
|
327
|
+
/>
|
|
328
|
+
)}
|
|
302
329
|
{footer}
|
|
303
330
|
</nav>
|
|
304
331
|
);
|
|
305
332
|
}
|
|
306
333
|
SidebarTree.displayName = "SidebarTree";
|
|
307
334
|
|
|
335
|
+
function TrayList({
|
|
336
|
+
tray,
|
|
337
|
+
itemCount,
|
|
338
|
+
currentSlug,
|
|
339
|
+
forceOpen,
|
|
340
|
+
locale,
|
|
341
|
+
}: {
|
|
342
|
+
tray: SidebarNavNode;
|
|
343
|
+
itemCount: number;
|
|
344
|
+
currentSlug?: string;
|
|
345
|
+
forceOpen: boolean;
|
|
346
|
+
locale: string;
|
|
347
|
+
}) {
|
|
348
|
+
const items = getNoteTrayItems(tray);
|
|
349
|
+
const sidebarStyle = tray.noteTraySidebar ?? "index";
|
|
350
|
+
const width = rankWidth(itemCount);
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<>
|
|
354
|
+
<LeafNode node={tray} currentSlug={currentSlug} depth={0} isLast={items.length === 0} />
|
|
355
|
+
{sidebarStyle === "index" ? (
|
|
356
|
+
items.map((item, index) => (
|
|
357
|
+
<TrayItem
|
|
358
|
+
key={item.slug}
|
|
359
|
+
item={item}
|
|
360
|
+
currentSlug={currentSlug}
|
|
361
|
+
rankDigits={width}
|
|
362
|
+
isLast={index === items.length - 1}
|
|
363
|
+
/>
|
|
364
|
+
))
|
|
365
|
+
) : (
|
|
366
|
+
groupItems(items, sidebarStyle, tray.sortOrder ?? "asc").map((group, index, groups) => (
|
|
367
|
+
<TrayGroupNode
|
|
368
|
+
key={group.key}
|
|
369
|
+
traySlug={tray.slug}
|
|
370
|
+
group={group}
|
|
371
|
+
grouping={sidebarStyle}
|
|
372
|
+
locale={locale}
|
|
373
|
+
currentSlug={currentSlug}
|
|
374
|
+
forceOpen={forceOpen}
|
|
375
|
+
isLast={index === groups.length - 1}
|
|
376
|
+
/>
|
|
377
|
+
))
|
|
378
|
+
)}
|
|
379
|
+
</>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function TrayItem({
|
|
384
|
+
item,
|
|
385
|
+
currentSlug,
|
|
386
|
+
rankDigits,
|
|
387
|
+
isLast,
|
|
388
|
+
showDate = false,
|
|
389
|
+
depth = 1,
|
|
390
|
+
}: {
|
|
391
|
+
item: SidebarNavNode;
|
|
392
|
+
currentSlug?: string;
|
|
393
|
+
rankDigits?: number;
|
|
394
|
+
isLast: boolean;
|
|
395
|
+
showDate?: boolean;
|
|
396
|
+
depth?: number;
|
|
397
|
+
}) {
|
|
398
|
+
if (!item.href) return null;
|
|
399
|
+
const isActive = item.slug === currentSlug;
|
|
400
|
+
const labelHtml = smartBreakToHtml(item.label);
|
|
401
|
+
const shortDate = item.date ? formatMonthDay(item.date) : undefined;
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<div className={isLast ? "pb-vsp-md" : ""}>
|
|
405
|
+
<div className="relative">
|
|
406
|
+
<ConnectorLines depth={depth} isLast={isLast} topPad="var(--spacing-vsp-2xs)" />
|
|
407
|
+
<a
|
|
408
|
+
href={item.href}
|
|
409
|
+
aria-current={isActive ? "page" : undefined}
|
|
410
|
+
data-nav-active={isActive ? "" : undefined}
|
|
411
|
+
className={`flex items-start gap-hsp-xs py-vsp-2xs pr-[4px] text-small break-words ${
|
|
412
|
+
isActive
|
|
413
|
+
? "bg-fg font-medium text-bg"
|
|
414
|
+
: "text-muted hover:text-accent hover:underline focus:underline focus:text-accent"
|
|
415
|
+
}`}
|
|
416
|
+
style={{ paddingLeft: padLeft(depth, false) }}
|
|
417
|
+
>
|
|
418
|
+
{rankDigits !== undefined && (
|
|
419
|
+
<span className={`shrink-0 tabular-nums${isActive ? "" : " text-muted"}`}>
|
|
420
|
+
{item.rank === undefined ? "" : String(item.rank).padStart(rankDigits, "0")}
|
|
421
|
+
</span>
|
|
422
|
+
)}
|
|
423
|
+
<span className="min-w-0 flex-1" dangerouslySetInnerHTML={{ __html: labelHtml }} />
|
|
424
|
+
{showDate && shortDate && (
|
|
425
|
+
<span className={`shrink-0 tabular-nums${isActive ? "" : " text-muted"}`}>
|
|
426
|
+
{shortDate}
|
|
427
|
+
</span>
|
|
428
|
+
)}
|
|
429
|
+
</a>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function noteTrayGroupStorageKey(traySlug: string, groupKey: string): string {
|
|
436
|
+
return `${traySlug}#${groupKey}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function TrayGroupNode({
|
|
440
|
+
traySlug,
|
|
441
|
+
group,
|
|
442
|
+
grouping,
|
|
443
|
+
locale,
|
|
444
|
+
currentSlug,
|
|
445
|
+
forceOpen,
|
|
446
|
+
isLast,
|
|
447
|
+
}: {
|
|
448
|
+
traySlug: string;
|
|
449
|
+
group: NoteTrayGroup<SidebarNavNode>;
|
|
450
|
+
grouping: "year" | "month";
|
|
451
|
+
locale: string;
|
|
452
|
+
currentSlug?: string;
|
|
453
|
+
forceOpen: boolean;
|
|
454
|
+
isLast: boolean;
|
|
455
|
+
}) {
|
|
456
|
+
const containsCurrent = group.items.some((item) => item.slug === currentSlug);
|
|
457
|
+
const [open, setOpen] = useState(containsCurrent);
|
|
458
|
+
const storageKey = noteTrayGroupStorageKey(traySlug, group.key);
|
|
459
|
+
const label = grouping === "year" ? group.key : formatYearMonthLabel(group.key, locale);
|
|
460
|
+
|
|
461
|
+
useEffect(() => {
|
|
462
|
+
const stored = getOpenSet();
|
|
463
|
+
if (stored.has(storageKey) && !open) setOpen(true);
|
|
464
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
465
|
+
|
|
466
|
+
useEffect(() => {
|
|
467
|
+
if (containsCurrent && !open) {
|
|
468
|
+
setOpen(true);
|
|
469
|
+
const stored = getOpenSet();
|
|
470
|
+
stored.add(storageKey);
|
|
471
|
+
saveOpenSet(stored);
|
|
472
|
+
}
|
|
473
|
+
}, [containsCurrent]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
474
|
+
|
|
475
|
+
useEffect(() => {
|
|
476
|
+
if (open) {
|
|
477
|
+
const stored = getOpenSet();
|
|
478
|
+
if (!stored.has(storageKey)) {
|
|
479
|
+
stored.add(storageKey);
|
|
480
|
+
saveOpenSet(stored);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}, [open, storageKey]);
|
|
484
|
+
|
|
485
|
+
const toggle = useCallback(() => {
|
|
486
|
+
setOpen((previous) => {
|
|
487
|
+
const next = !previous;
|
|
488
|
+
const stored = getOpenSet();
|
|
489
|
+
if (next) stored.add(storageKey);
|
|
490
|
+
else stored.delete(storageKey);
|
|
491
|
+
saveOpenSet(stored);
|
|
492
|
+
return next;
|
|
493
|
+
});
|
|
494
|
+
}, [storageKey]);
|
|
495
|
+
|
|
496
|
+
const isExpanded = forceOpen || open;
|
|
497
|
+
|
|
498
|
+
return (
|
|
499
|
+
<div className={!isLast && isExpanded ? "relative" : ""}>
|
|
500
|
+
{!isLast && isExpanded && (
|
|
501
|
+
<div
|
|
502
|
+
className="absolute border-l border-solid border-muted z-local-1"
|
|
503
|
+
style={{ left: connectorLeft(1), top: 0, bottom: 0 }}
|
|
504
|
+
/>
|
|
505
|
+
)}
|
|
506
|
+
<div className="relative">
|
|
507
|
+
<ConnectorLines depth={1} isLast={isLast} topPad="var(--spacing-vsp-xs)" />
|
|
508
|
+
<button
|
|
509
|
+
type="button"
|
|
510
|
+
onClick={toggle}
|
|
511
|
+
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"
|
|
512
|
+
style={{ paddingLeft: padLeft(1, true) }}
|
|
513
|
+
aria-expanded={isExpanded}
|
|
514
|
+
aria-label={isExpanded ? `Collapse ${label}` : `Expand ${label}`}
|
|
515
|
+
data-zd-sidebar-open-key={storageKey}
|
|
516
|
+
>
|
|
517
|
+
<span className="aspect-square flex items-center justify-center w-[1.5rem] shrink-0 border border-muted">
|
|
518
|
+
<ToggleChevron isExpanded={isExpanded} className="text-muted" />
|
|
519
|
+
</span>
|
|
520
|
+
<span>{label}</span>
|
|
521
|
+
</button>
|
|
522
|
+
</div>
|
|
523
|
+
{isExpanded && (
|
|
524
|
+
<div>
|
|
525
|
+
{group.items.map((item, index) => (
|
|
526
|
+
<TrayItem
|
|
527
|
+
key={item.slug}
|
|
528
|
+
item={item}
|
|
529
|
+
currentSlug={currentSlug}
|
|
530
|
+
isLast={index === group.items.length - 1}
|
|
531
|
+
showDate
|
|
532
|
+
// Leave one visual indentation step between the group branch
|
|
533
|
+
// and its dated child branch so the hierarchy reads clearly.
|
|
534
|
+
depth={GROUPED_TRAY_ITEM_DEPTH}
|
|
535
|
+
/>
|
|
536
|
+
))}
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
308
543
|
// NodeList is memo-wrapped so that when only the filter query changes but
|
|
309
544
|
// a subtree's nodes/currentSlug/depth/forceOpen are unchanged, Preact can
|
|
310
545
|
// skip re-rendering the whole subtree.
|
|
@@ -6,8 +6,21 @@
|
|
|
6
6
|
// lets us consume React-typed components in this Preact app.
|
|
7
7
|
import { useState } from "preact/hooks";
|
|
8
8
|
import type { SidebarNavNode } from "../sidebar/types.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
INDENT,
|
|
11
|
+
connectorLeft,
|
|
12
|
+
ConnectorLines,
|
|
13
|
+
CategoryLinkIcon,
|
|
14
|
+
} from "../tree-nav-shared/index.js";
|
|
10
15
|
import { ChevronRight } from "../icons/index.js";
|
|
16
|
+
import {
|
|
17
|
+
formatDate,
|
|
18
|
+
formatYearMonthLabel,
|
|
19
|
+
getNoteTrayItems,
|
|
20
|
+
groupItems,
|
|
21
|
+
rankWidth,
|
|
22
|
+
} from "../note-tray-model/index.js";
|
|
23
|
+
import { formatMonthDay } from "../format-date/index.js";
|
|
11
24
|
import { initialCategoryOpenState, toggleCategoryOpenState } from "./state.js";
|
|
12
25
|
|
|
13
26
|
// site-tree-nav uses wider padding than the narrow sidebar
|
|
@@ -42,6 +55,10 @@ export interface SiteTreeNavProps {
|
|
|
42
55
|
categoryIgnore?: string[];
|
|
43
56
|
/** Root-category slugs that should start collapsed. */
|
|
44
57
|
initiallyCollapsedCategorySlugs?: string[];
|
|
58
|
+
/** Locale used by dated note-tray rows. */
|
|
59
|
+
locale?: string;
|
|
60
|
+
/** Localized label shown before an item's updated date. */
|
|
61
|
+
updatedLabel?: string;
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
export function SiteTreeNav({
|
|
@@ -50,6 +67,8 @@ export function SiteTreeNav({
|
|
|
50
67
|
categoryOrder,
|
|
51
68
|
categoryIgnore,
|
|
52
69
|
initiallyCollapsedCategorySlugs,
|
|
70
|
+
locale = "en",
|
|
71
|
+
updatedLabel = "Updated",
|
|
53
72
|
}: SiteTreeNavProps) {
|
|
54
73
|
let processedTree = tree;
|
|
55
74
|
if (categoryIgnore) {
|
|
@@ -69,20 +88,27 @@ export function SiteTreeNav({
|
|
|
69
88
|
gridTemplateColumns: "repeat(auto-fill, minmax(min(18rem, 100%), 1fr))",
|
|
70
89
|
}}
|
|
71
90
|
>
|
|
72
|
-
{processedTree.map((node) =>
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
91
|
+
{processedTree.map((node) => {
|
|
92
|
+
if (node.shape === "note-tray" && getNoteTrayItems(node).length === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
return (
|
|
96
|
+
<div key={node.slug} className="min-w-0 border border-muted pl-hsp-sm py-vsp-2xs">
|
|
97
|
+
{node.children.length > 0 ? (
|
|
98
|
+
<CategoryNode
|
|
99
|
+
node={node}
|
|
100
|
+
depth={0}
|
|
101
|
+
isLast={true}
|
|
102
|
+
initiallyCollapsed={initiallyCollapsed.has(node.slug)}
|
|
103
|
+
locale={locale}
|
|
104
|
+
updatedLabel={updatedLabel}
|
|
105
|
+
/>
|
|
106
|
+
) : (
|
|
107
|
+
<LeafNode node={node} depth={0} isLast={true} />
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
})}
|
|
86
112
|
</nav>
|
|
87
113
|
);
|
|
88
114
|
}
|
|
@@ -118,11 +144,15 @@ function CategoryNode({
|
|
|
118
144
|
depth,
|
|
119
145
|
isLast,
|
|
120
146
|
initiallyCollapsed = false,
|
|
147
|
+
locale = "en",
|
|
148
|
+
updatedLabel = "Updated",
|
|
121
149
|
}: {
|
|
122
150
|
node: SidebarNavNode;
|
|
123
151
|
depth: number;
|
|
124
152
|
isLast: boolean;
|
|
125
153
|
initiallyCollapsed?: boolean;
|
|
154
|
+
locale?: string;
|
|
155
|
+
updatedLabel?: string;
|
|
126
156
|
}) {
|
|
127
157
|
const [open, setOpen] = useState(() => initialCategoryOpenState(initiallyCollapsed));
|
|
128
158
|
const toggle = () => setOpen(toggleCategoryOpenState);
|
|
@@ -185,13 +215,133 @@ function CategoryNode({
|
|
|
185
215
|
</div>
|
|
186
216
|
{open && (
|
|
187
217
|
<div>
|
|
188
|
-
|
|
218
|
+
{node.shape === "note-tray" && depth === 0 ? (
|
|
219
|
+
<NoteTrayNodeList node={node} locale={locale} updatedLabel={updatedLabel} />
|
|
220
|
+
) : (
|
|
221
|
+
<NodeList nodes={node.children} depth={depth + 1} />
|
|
222
|
+
)}
|
|
189
223
|
</div>
|
|
190
224
|
)}
|
|
191
225
|
</div>
|
|
192
226
|
);
|
|
193
227
|
}
|
|
194
228
|
|
|
229
|
+
function NoteTrayNodeList({
|
|
230
|
+
node,
|
|
231
|
+
locale,
|
|
232
|
+
updatedLabel,
|
|
233
|
+
}: {
|
|
234
|
+
node: SidebarNavNode;
|
|
235
|
+
locale: string;
|
|
236
|
+
updatedLabel: string;
|
|
237
|
+
}) {
|
|
238
|
+
const items = getNoteTrayItems(node);
|
|
239
|
+
const width = rankWidth(items);
|
|
240
|
+
const showDate = node.noteTrayDated === true;
|
|
241
|
+
const grouping =
|
|
242
|
+
showDate && node.noteTraySidebar !== "index"
|
|
243
|
+
? node.noteTraySidebar
|
|
244
|
+
: undefined;
|
|
245
|
+
|
|
246
|
+
if (grouping === "year" || grouping === "month") {
|
|
247
|
+
return (
|
|
248
|
+
<div className="pl-hsp-md">
|
|
249
|
+
{groupItems(items, grouping, node.sortOrder ?? "asc").map((group) => (
|
|
250
|
+
<div key={group.key} data-note-tray-group={group.key}>
|
|
251
|
+
<div className="pt-vsp-sm pb-vsp-2xs text-micro tracking-wide uppercase text-muted">
|
|
252
|
+
{grouping === "year"
|
|
253
|
+
? group.key
|
|
254
|
+
: formatYearMonthLabel(group.key, locale)}
|
|
255
|
+
</div>
|
|
256
|
+
{group.items.map((item) => (
|
|
257
|
+
<NoteTrayRow
|
|
258
|
+
key={item.slug}
|
|
259
|
+
item={item}
|
|
260
|
+
locale={locale}
|
|
261
|
+
updatedLabel={updatedLabel}
|
|
262
|
+
rankWidth={width}
|
|
263
|
+
showDate={showDate}
|
|
264
|
+
groupedDate={true}
|
|
265
|
+
/>
|
|
266
|
+
))}
|
|
267
|
+
</div>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<div className="pl-hsp-md">
|
|
275
|
+
{items.map((item) => (
|
|
276
|
+
<NoteTrayRow
|
|
277
|
+
key={item.slug}
|
|
278
|
+
item={item}
|
|
279
|
+
locale={locale}
|
|
280
|
+
updatedLabel={updatedLabel}
|
|
281
|
+
rankWidth={width}
|
|
282
|
+
showDate={showDate}
|
|
283
|
+
groupedDate={false}
|
|
284
|
+
/>
|
|
285
|
+
))}
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function NoteTrayRow({
|
|
291
|
+
item,
|
|
292
|
+
locale,
|
|
293
|
+
updatedLabel,
|
|
294
|
+
rankWidth: width,
|
|
295
|
+
showDate,
|
|
296
|
+
groupedDate,
|
|
297
|
+
}: {
|
|
298
|
+
item: SidebarNavNode;
|
|
299
|
+
locale: string;
|
|
300
|
+
updatedLabel: string;
|
|
301
|
+
rankWidth: number;
|
|
302
|
+
showDate: boolean;
|
|
303
|
+
groupedDate: boolean;
|
|
304
|
+
}) {
|
|
305
|
+
if (!item.href) return null;
|
|
306
|
+
const dateLabel = showDate && item.date
|
|
307
|
+
? groupedDate
|
|
308
|
+
? formatMonthDay(item.date)
|
|
309
|
+
: formatDate(item.date, locale)
|
|
310
|
+
: undefined;
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<a
|
|
314
|
+
href={item.href}
|
|
315
|
+
data-note-tray-row
|
|
316
|
+
className="flex items-start gap-hsp-sm py-vsp-2xs text-small text-fg hover:text-accent hover:underline focus:underline"
|
|
317
|
+
>
|
|
318
|
+
{dateLabel ? (
|
|
319
|
+
<time
|
|
320
|
+
dateTime={item.date}
|
|
321
|
+
className="shrink-0 font-mono tabular-nums text-caption text-muted"
|
|
322
|
+
>
|
|
323
|
+
{dateLabel}
|
|
324
|
+
</time>
|
|
325
|
+
) : (
|
|
326
|
+
<span
|
|
327
|
+
className="shrink-0 font-mono tabular-nums text-caption text-muted"
|
|
328
|
+
style={{ width: `${width}ch` }}
|
|
329
|
+
>
|
|
330
|
+
{item.rank === undefined ? "" : String(item.rank).padStart(width, "0")}
|
|
331
|
+
</span>
|
|
332
|
+
)}
|
|
333
|
+
<span className="min-w-0">
|
|
334
|
+
<span>{item.label}</span>
|
|
335
|
+
{item.updated && (
|
|
336
|
+
<span className="block text-micro text-muted">
|
|
337
|
+
{updatedLabel} {formatDate(item.updated, locale)}
|
|
338
|
+
</span>
|
|
339
|
+
)}
|
|
340
|
+
</span>
|
|
341
|
+
</a>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
195
345
|
function LeafNode({
|
|
196
346
|
node,
|
|
197
347
|
depth,
|