@takazudo/zudo-doc 5.5.0 → 5.5.2

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 CHANGED
@@ -4,6 +4,27 @@ All notable changes to `@takazudo/zudo-doc` are documented in this file.
4
4
 
5
5
  The format is based on Keep a Changelog, and release notes are generated from the changelog MDX pages.
6
6
 
7
+ ## [5.5.2] - 2026-08-16
8
+
9
+ ### Bug Fixes
10
+
11
+ - Preserve the sidebar scroll position across SPA navigation and island remounts. (7e8130f4b, 63629e73e, 7fadc3308)
12
+ - Suppress GFM task-list markers in rendered documentation. (4d9dbf941)
13
+
14
+ ### Other Changes
15
+
16
+ - Added regression coverage for stale sidebar snapshots and sidebar timing. (5632de06f, 360857d79)
17
+
18
+ ## [5.5.1] - 2026-08-15
19
+
20
+ ### Bug Fixes
21
+
22
+ - Bumped the `@takazudo/zfb` family (`zfb`, `zfb-runtime`, `zfb-md-wasm`, `zfb-adapter-cloudflare`) from 2.5.0 to 2.5.1 across the root, `@takazudo/zudo-doc`, and the `create-zudo-doc` scaffold pin. This is a bugfix-only patch that settles the autolink-literal regressions introduced when 2.5.0 flipped `gfm.autolinkLiteral` on by default: a link is now unwrapped only when it genuinely is an autolink literal (the shape is matched on the visible text rather than the URL alone, case-folded), nested autolinks inside a link label or author-written MDX JSX are dropped rather than emitted as an `<a>` inside an `<a>`, and footnote bodies are spared. `[https://example.com](https://example.com)` therefore no longer produces invalid nested-anchor HTML — [Takazudo/zudo-front-builder#2388](https://github.com/Takazudo/zudo-front-builder/issues/2388), reported from this project during the 5.5.0 release, is fixed upstream. The same release also makes CJK-friendly emphasis, hard breaks, math constructs, and `markdown.gfm` itself apply consistently at the secondary parse sites (transcluded files, re-parsed directive bodies, and the HTML path), so those constructs no longer behave differently depending on where in a document they appear. (68aa94c5)
23
+
24
+ ### Other Changes
25
+
26
+ - Raised the `@takazudo/zudo-doc-history-server` peer floor in `@takazudo/zudo-doc` from `^5.4.0` to `^5.5.0`, the routine post-publish catch-up now that 5.5.0 is live on npm. The floor trails the in-flight release by one version permanently and by design. (68aa94c5)
27
+
7
28
  ## [5.5.0] - 2026-08-14
8
29
 
9
30
  ### Features
package/dist/content.css CHANGED
@@ -164,6 +164,19 @@
164
164
  color: var(--color-muted);
165
165
  }
166
166
 
167
+ /* GFM task-list inputs are emitted as disabled direct children of a tight
168
+ * item, or as children of the item's paragraph in a loose list. The
169
+ * component-rendered ancestor `ul` carries an inline disc marker, so reset
170
+ * only those generated task rows on the `li` itself. Keep the direct-child
171
+ * shape and disabled constraint narrow so authored/nested checkboxes remain
172
+ * ordinary list content. */
173
+ .zd-content :where(
174
+ li:has(> input[type="checkbox"][disabled]),
175
+ li:has(> p > input[type="checkbox"][disabled])
176
+ ) {
177
+ list-style-type: none;
178
+ }
179
+
167
180
  .zd-content :where(li > ul, li > ol) {
168
181
  margin-top: var(--spacing-vsp-xs);
169
182
  margin-bottom: 0;
@@ -9,6 +9,8 @@ import { smartBreakToHtml } from "../smart-break/index.js";
9
9
  import { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT } from "../transitions/index.js";
10
10
  import { filterTree } from "../sidebar-filter/index.js";
11
11
  import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
12
+ import { ensureSidebarScrollPreserve } from "./sidebar-scroll-preserve.js";
13
+ ensureSidebarScrollPreserve();
12
14
  function ToggleChevron({ isExpanded, className }) {
13
15
  return /* @__PURE__ */ jsx(
14
16
  ChevronRight,
@@ -58,35 +60,6 @@ function useActiveSlug(nodes, initial) {
58
60
  }, [nodes]);
59
61
  return slug;
60
62
  }
61
- function useSidebarScrollPreserve() {
62
- useEffect(() => {
63
- let savedScrollTop = 0;
64
- let restoreTimer;
65
- const onBefore = () => {
66
- if (restoreTimer !== void 0) {
67
- clearTimeout(restoreTimer);
68
- restoreTimer = void 0;
69
- }
70
- const aside = document.querySelector("#desktop-sidebar");
71
- if (aside) savedScrollTop = aside.scrollTop;
72
- };
73
- const onAfter = () => {
74
- const aside = document.querySelector("#desktop-sidebar");
75
- if (!aside) return;
76
- restoreTimer = setTimeout(() => {
77
- restoreTimer = void 0;
78
- aside.scrollTop = savedScrollTop;
79
- }, 50);
80
- };
81
- document.addEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
82
- document.addEventListener(AFTER_NAVIGATE_EVENT, onAfter);
83
- return () => {
84
- document.removeEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
85
- document.removeEventListener(AFTER_NAVIGATE_EVENT, onAfter);
86
- if (restoreTimer !== void 0) clearTimeout(restoreTimer);
87
- };
88
- }, []);
89
- }
90
63
  function RootMenuItemEntry({ item }) {
91
64
  const [expanded, setExpanded] = useState(false);
92
65
  const hasChildren = item.children && item.children.length > 0;
@@ -141,7 +114,6 @@ function SidebarFooter({ links, themeDefaultMode }) {
141
114
  }
142
115
  function SidebarTree({ nodes, currentSlug, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }) {
143
116
  const activeSlug = useActiveSlug(nodes, currentSlug);
144
- useSidebarScrollPreserve();
145
117
  const [query, setQuery] = useState("");
146
118
  const [showingRootMenu, setShowingRootMenu] = useState(false);
147
119
  const filterRef = useRef(null);
@@ -0,0 +1,21 @@
1
+ interface SidebarScrollPreserveOptions {
2
+ document: Document;
3
+ requestAnimationFrame: (callback: FrameRequestCallback) => number;
4
+ cancelAnimationFrame: (handle: number) => void;
5
+ }
6
+ /**
7
+ * Install the scroll-preservation state machine on a document.
8
+ *
9
+ * This low-level entrypoint owns an explicit cleanup for focused tests and
10
+ * non-singleton hosts. The sidebar's browser module uses the durable,
11
+ * duplicate-safe `ensureSidebarScrollPreserve` wrapper below instead.
12
+ */
13
+ export declare function installSidebarScrollPreserve({ document, requestAnimationFrame, cancelAnimationFrame, }: SidebarScrollPreserveOptions): () => void;
14
+ /**
15
+ * Install exactly one module-lifetime controller for a browser document.
16
+ * Repeated calls from component renders or duplicate boot paths are no-ops.
17
+ */
18
+ export declare function ensureSidebarScrollPreserve(options?: SidebarScrollPreserveOptions): void;
19
+ /** Test/HMR teardown for a controller installed through the singleton wrapper. */
20
+ export declare function disposeSidebarScrollPreserve(document: Document): void;
21
+ export {};
@@ -0,0 +1,63 @@
1
+ import { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT } from "../transitions/index.js";
2
+ const installedControllers = /* @__PURE__ */ new WeakMap();
3
+ function installSidebarScrollPreserve({
4
+ document: document2,
5
+ requestAnimationFrame,
6
+ cancelAnimationFrame
7
+ }) {
8
+ let snapshot;
9
+ let restoreFrame;
10
+ const cancelPendingRestore = () => {
11
+ if (restoreFrame === void 0) return;
12
+ cancelAnimationFrame(restoreFrame);
13
+ restoreFrame = void 0;
14
+ };
15
+ const onBefore = () => {
16
+ cancelPendingRestore();
17
+ const element = document2.querySelector("#desktop-sidebar");
18
+ snapshot = element ? { element, scrollTop: element.scrollTop } : void 0;
19
+ };
20
+ const onAfter = () => {
21
+ if (!snapshot) return;
22
+ const saved = snapshot;
23
+ snapshot = void 0;
24
+ restoreFrame = requestAnimationFrame(() => {
25
+ restoreFrame = void 0;
26
+ const current = document2.querySelector("#desktop-sidebar");
27
+ if (current === saved.element) current.scrollTop = saved.scrollTop;
28
+ });
29
+ };
30
+ document2.addEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
31
+ document2.addEventListener(AFTER_NAVIGATE_EVENT, onAfter);
32
+ return () => {
33
+ document2.removeEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
34
+ document2.removeEventListener(AFTER_NAVIGATE_EVENT, onAfter);
35
+ cancelPendingRestore();
36
+ snapshot = void 0;
37
+ };
38
+ }
39
+ function ensureSidebarScrollPreserve(options) {
40
+ const resolved = options ?? resolveBrowserOptions();
41
+ if (!resolved || installedControllers.has(resolved.document)) return;
42
+ installedControllers.set(
43
+ resolved.document,
44
+ installSidebarScrollPreserve(resolved)
45
+ );
46
+ }
47
+ function resolveBrowserOptions() {
48
+ if (typeof document === "undefined" || typeof window === "undefined") return void 0;
49
+ return {
50
+ document,
51
+ requestAnimationFrame: window.requestAnimationFrame.bind(window),
52
+ cancelAnimationFrame: window.cancelAnimationFrame.bind(window)
53
+ };
54
+ }
55
+ function disposeSidebarScrollPreserve(document2) {
56
+ installedControllers.get(document2)?.();
57
+ installedControllers.delete(document2);
58
+ }
59
+ export {
60
+ disposeSidebarScrollPreserve,
61
+ ensureSidebarScrollPreserve,
62
+ installSidebarScrollPreserve
63
+ };
@@ -18,6 +18,13 @@ import { smartBreakToHtml } from "../smart-break/index.js";
18
18
  import { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT } from "../transitions/index.js";
19
19
  import { filterTree } from "../sidebar-filter/index.js";
20
20
  import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
21
+ import { ensureSidebarScrollPreserve } from "./sidebar-scroll-preserve.js";
22
+
23
+ // The persisted aside can transiently tear down and re-mount its SidebarTree
24
+ // effect during a body swap. Keep navigation snapshot ownership at browser
25
+ // module/document lifetime so that island lifecycle cannot discard it between
26
+ // before-preparation and after-swap. SSR evaluation is a safe no-op.
27
+ ensureSidebarScrollPreserve();
21
28
 
22
29
  function ToggleChevron({ isExpanded, className }: { isExpanded: boolean; className?: string }) {
23
30
  return (
@@ -90,42 +97,6 @@ function useActiveSlug(nodes: SidebarNavNode[], initial?: string): string | unde
90
97
  return slug;
91
98
  }
92
99
 
93
- /**
94
- * Preserve `#desktop-sidebar` scrollTop across SPA navigations.
95
- */
96
- function useSidebarScrollPreserve() {
97
- useEffect(() => {
98
- let savedScrollTop = 0;
99
- let restoreTimer: ReturnType<typeof setTimeout> | undefined;
100
-
101
- const onBefore = () => {
102
- if (restoreTimer !== undefined) {
103
- clearTimeout(restoreTimer);
104
- restoreTimer = undefined;
105
- }
106
- const aside = document.querySelector<HTMLElement>("#desktop-sidebar");
107
- if (aside) savedScrollTop = aside.scrollTop;
108
- };
109
-
110
- const onAfter = () => {
111
- const aside = document.querySelector<HTMLElement>("#desktop-sidebar");
112
- if (!aside) return;
113
- restoreTimer = setTimeout(() => {
114
- restoreTimer = undefined;
115
- aside.scrollTop = savedScrollTop;
116
- }, 50);
117
- };
118
-
119
- document.addEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
120
- document.addEventListener(AFTER_NAVIGATE_EVENT, onAfter);
121
- return () => {
122
- document.removeEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
123
- document.removeEventListener(AFTER_NAVIGATE_EVENT, onAfter);
124
- if (restoreTimer !== undefined) clearTimeout(restoreTimer);
125
- };
126
- }, []);
127
- }
128
-
129
100
  function RootMenuItemEntry({ item }: { item: SidebarRootMenuItem }) {
130
101
  const [expanded, setExpanded] = useState(false);
131
102
  const hasChildren = item.children && item.children.length > 0;
@@ -202,7 +173,6 @@ function SidebarFooter({ links, themeDefaultMode }: { links?: SidebarLocaleLink[
202
173
 
203
174
  export function SidebarTree({ nodes, currentSlug, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }: SidebarTreeProps) {
204
175
  const activeSlug = useActiveSlug(nodes, currentSlug);
205
- useSidebarScrollPreserve();
206
176
  const [query, setQuery] = useState("");
207
177
  const [showingRootMenu, setShowingRootMenu] = useState(false);
208
178
  const filterRef = useRef<HTMLInputElement>(null);
@@ -0,0 +1,88 @@
1
+ import { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT } from "../transitions/index.js";
2
+
3
+ interface SidebarScrollPreserveOptions {
4
+ document: Document;
5
+ requestAnimationFrame: (callback: FrameRequestCallback) => number;
6
+ cancelAnimationFrame: (handle: number) => void;
7
+ }
8
+
9
+ const installedControllers = new WeakMap<Document, () => void>();
10
+
11
+ /**
12
+ * Install the scroll-preservation state machine on a document.
13
+ *
14
+ * This low-level entrypoint owns an explicit cleanup for focused tests and
15
+ * non-singleton hosts. The sidebar's browser module uses the durable,
16
+ * duplicate-safe `ensureSidebarScrollPreserve` wrapper below instead.
17
+ */
18
+ export function installSidebarScrollPreserve({
19
+ document,
20
+ requestAnimationFrame,
21
+ cancelAnimationFrame,
22
+ }: SidebarScrollPreserveOptions): () => void {
23
+ let snapshot: { element: HTMLElement; scrollTop: number } | undefined;
24
+ let restoreFrame: number | undefined;
25
+
26
+ const cancelPendingRestore = () => {
27
+ if (restoreFrame === undefined) return;
28
+ cancelAnimationFrame(restoreFrame);
29
+ restoreFrame = undefined;
30
+ };
31
+
32
+ const onBefore = () => {
33
+ cancelPendingRestore();
34
+ const element = document.querySelector<HTMLElement>("#desktop-sidebar");
35
+ snapshot = element ? { element, scrollTop: element.scrollTop } : undefined;
36
+ };
37
+
38
+ const onAfter = () => {
39
+ if (!snapshot) return;
40
+ const saved = snapshot;
41
+ snapshot = undefined;
42
+ restoreFrame = requestAnimationFrame(() => {
43
+ restoreFrame = undefined;
44
+ const current = document.querySelector<HTMLElement>("#desktop-sidebar");
45
+ if (current === saved.element) current.scrollTop = saved.scrollTop;
46
+ });
47
+ };
48
+
49
+ document.addEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
50
+ document.addEventListener(AFTER_NAVIGATE_EVENT, onAfter);
51
+
52
+ return () => {
53
+ document.removeEventListener(BEFORE_NAVIGATE_EVENT, onBefore);
54
+ document.removeEventListener(AFTER_NAVIGATE_EVENT, onAfter);
55
+ cancelPendingRestore();
56
+ snapshot = undefined;
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Install exactly one module-lifetime controller for a browser document.
62
+ * Repeated calls from component renders or duplicate boot paths are no-ops.
63
+ */
64
+ export function ensureSidebarScrollPreserve(
65
+ options?: SidebarScrollPreserveOptions,
66
+ ): void {
67
+ const resolved = options ?? resolveBrowserOptions();
68
+ if (!resolved || installedControllers.has(resolved.document)) return;
69
+ installedControllers.set(
70
+ resolved.document,
71
+ installSidebarScrollPreserve(resolved),
72
+ );
73
+ }
74
+
75
+ function resolveBrowserOptions(): SidebarScrollPreserveOptions | undefined {
76
+ if (typeof document === "undefined" || typeof window === "undefined") return undefined;
77
+ return {
78
+ document,
79
+ requestAnimationFrame: window.requestAnimationFrame.bind(window),
80
+ cancelAnimationFrame: window.cancelAnimationFrame.bind(window),
81
+ };
82
+ }
83
+
84
+ /** Test/HMR teardown for a controller installed through the singleton wrapper. */
85
+ export function disposeSidebarScrollPreserve(document: Document): void {
86
+ installedControllers.get(document)?.();
87
+ installedControllers.delete(document);
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "5.5.0",
3
+ "version": "5.5.2",
4
4
  "type": "module",
5
5
  "description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
6
6
  "license": "MIT",
@@ -610,10 +610,10 @@
610
610
  ],
611
611
  "peerDependencies": {
612
612
  "@takazudo/zdtp": "^0.4.10",
613
- "@takazudo/zfb": "^2.5.0",
614
- "@takazudo/zfb-md-wasm": "^2.5.0",
615
- "@takazudo/zfb-runtime": "^2.5.0",
616
- "@takazudo/zudo-doc-history-server": "^5.4.0",
613
+ "@takazudo/zfb": "^2.5.1",
614
+ "@takazudo/zfb-md-wasm": "^2.5.1",
615
+ "@takazudo/zfb-runtime": "^2.5.1",
616
+ "@takazudo/zudo-doc-history-server": "^5.5.0",
617
617
  "diff": "^8.0.0",
618
618
  "katex": "^0.16.0",
619
619
  "preact": "^10.29.1",
@@ -647,9 +647,9 @@
647
647
  "tsx": "^4.21.0"
648
648
  },
649
649
  "devDependencies": {
650
- "@takazudo/zfb": "2.5.0",
651
- "@takazudo/zfb-md-wasm": "2.5.0",
652
- "@takazudo/zfb-runtime": "2.5.0",
650
+ "@takazudo/zfb": "2.5.1",
651
+ "@takazudo/zfb-md-wasm": "2.5.1",
652
+ "@takazudo/zfb-runtime": "2.5.1",
653
653
  "@types/fs-extra": "^11.0.4",
654
654
  "@types/minimist": "^1.2.5",
655
655
  "@types/node": "^25.3.5",
@@ -663,7 +663,7 @@
663
663
  "typescript": "^5.0.0",
664
664
  "vitest": "^4.1.0",
665
665
  "zod": "^4.3.6",
666
- "@takazudo/zudo-doc-history-server": "5.5.0"
666
+ "@takazudo/zudo-doc-history-server": "5.5.2"
667
667
  },
668
668
  "scripts": {
669
669
  "build": "tsup && tsc -p tsconfig.build.json",