@takazudo/zudo-doc 6.2.0 → 7.0.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 CHANGED
@@ -8,6 +8,20 @@ The format is based on Keep a Changelog, and release notes are generated from th
8
8
 
9
9
  No unreleased changes.
10
10
 
11
+ ## [7.0.0] - 2026-10-15
12
+
13
+ ### Breaking Changes
14
+
15
+ - The supported Node.js runtime moves to the current production LTS line, Node 24 (`>=24 <25`). Node 22 is no longer supported. (98c94df73)
16
+ - Raises the optional KaTeX peer floor to `^0.18.2 || ^0.19.0` for the GHSA-238p-pmpm-9mq7 patch; 0.16.x is no longer supported. Updates the internal KaTeX color selector while preserving server-only rendering, error fallback and math-disabled builds without KaTeX. Math-enabled hosts must install a supported version and import its local CSS. (9ee3950c9)
17
+ - Requires the published zfb 4.5.2 family: the `@takazudo/zfb`, `zfb-runtime` and `zfb-md-wasm` peer floors move from `^4.5.0` to `^4.5.2`. This picks up live `public/assets/` development serving, explicit and compact one-line MDX table fixes, and a `zfb dev` ready banner that waits until routes are published. Upgrade the zfb family (and `zfb-adapter-cloudflare` when used) together. (e5b7de457, d5e0d7078)
18
+ - The `@takazudo/zudo-doc-history-server` peer floor moves from `^6.0.0` to `^7.0.0` with this major release.
19
+
20
+ ### Bug Fixes
21
+
22
+ - Keep the header stationary during dynamic page transitions, including when destination markup replaces it, and keep the desktop sidebar stationary within the same navigation category and locale/version/tree context. Fresh destination content and existing sidebar state handling are preserved; changing articles and table-of-contents content still animate. Sidebar entry and exit also cover `hide_sidebar` pages. Loading feedback now covers the content area without dimming the header or desktop sidebar, stays out of the page animation, and clears on the content swap or navigation cancellation. (ae7152ad5, 6abeadf5f, 479309f1f)
23
+ - Ordinary documentation prose and navigation labels now wrap at narrow widths instead of overflowing horizontally (`overflow-wrap: anywhere` on content). Preformatted code keeps its local scrolling. (648e38da8)
24
+
11
25
  ## [6.2.0] - 2026-10-14
12
26
 
13
27
  ### Features
package/README.md CHANGED
@@ -109,7 +109,7 @@ If your project renders **semi-trusted or user-submitted** HTML in a preview, ov
109
109
 
110
110
  **6.0.0** is the coordinated major release of `@takazudo/zudo-doc`,
111
111
  `@takazudo/zudo-doc-history-server` and `create-zudo-doc`. The current workspace
112
- targets the published **zfb 4.5.0** family (peer floor **^4.5.0**).
112
+ targets the published **zfb 4.5.2** family (peer floor **^4.5.2**).
113
113
  [Upstream #4097](https://github.com/Takazudo/zudo-front-builder/issues/4097) is
114
114
  resolved; its strict published 4.3.1 consumer control passed 3/3 with no skips or
115
115
  retries in the [source-bound verification](../../docs/findings/zfb-4.3.1/verification.md).
@@ -160,3 +160,18 @@ zfb CSS generation. Do not use physical package `dist/*.css` import paths.
160
160
  This package is published to npm as `@takazudo/zudo-doc` (since `0.2.0`, `latest` tracks the current line). Inside this repo the host site consumes it as a workspace package through its compiled `dist/` — `pnpm dev` at the repo root runs two watchers in parallel, `tsup --watch` for the JS and `tsc -p tsconfig.build.json --watch` for the `.d.ts`, so edits under `src/` rebuild both halves automatically; for a one-off rebuild use `pnpm --filter @takazudo/zudo-doc build`.
161
161
 
162
162
  zfb itself comes from npm (versions pinned in the root `package.json`). To develop against a local zfb checkout, use the temporary `pnpm.overrides` link escape hatch documented in root `CONTRIBUTING.md` — do not commit the override.
163
+
164
+ ### Optional math peer (unreleased compatibility floor)
165
+
166
+ Math-enabled consumers install `katex@^0.19.0` (supported peer range:
167
+ `^0.18.2 || ^0.19.0`) and import `katex/dist/katex.min.css` before
168
+ `@takazudo/zudo-doc/features.css` in their CSS entry. CSS and fonts are bundled
169
+ locally. The consumer matrix covers 0.18.2, 0.18.11 and 0.19.0. The old 0.16
170
+ branch is excluded because GHSA-238p-pmpm-9mq7 is patched from 0.18.2.
171
+ KaTeX 0.18's `.katex-base` selector replaces `.base` in the package color rule.
172
+
173
+ `MathBlock` remains server-rendered, with inline/display wrappers, safe defaults
174
+ and visible malformed-input fallback. A math-disabled consumer needs neither
175
+ KaTeX nor its stylesheet; module evaluation succeeds without it, and explicit
176
+ `MathBlock` rendering reports a useful missing-peer error. This source change
177
+ raises the compatibility floor and is not yet published to npm.
package/dist/compiled.css CHANGED
@@ -626,6 +626,7 @@ body {
626
626
  color: var(--color-fg);
627
627
  font-size: var(--text-body);
628
628
  line-height: var(--leading-relaxed);
629
+ overflow-wrap: anywhere;
629
630
  }
630
631
 
631
632
  @layer zd-flow {
@@ -1045,6 +1046,7 @@ a.text-accent.underline {
1045
1046
 
1046
1047
  .page-loading-overlay {
1047
1048
  position: fixed;
1049
+ view-transition-name: zfb-loading;
1048
1050
  inset: 0;
1049
1051
  z-index: var(--z-index-modal, 100);
1050
1052
  display: flex;
@@ -1053,11 +1055,12 @@ a.text-accent.underline {
1053
1055
  background: var(--color-page-loading-overlay, color-mix(in oklch, var(--color-overlay, #000) 60%, transparent));
1054
1056
  opacity: 0;
1055
1057
  pointer-events: none;
1056
- transition: opacity .15s ease-out;
1058
+ transition: none;
1057
1059
  }
1058
1060
 
1059
1061
  .page-loading-overlay[data-visible] {
1060
1062
  opacity: 1;
1063
+ transition: opacity .15s ease-out;
1061
1064
  }
1062
1065
 
1063
1066
  a[data-zd-nav-pending], button[data-zd-nav-pending] {
@@ -1100,6 +1103,14 @@ a[data-zd-nav-pending], button[data-zd-nav-pending] {
1100
1103
  }
1101
1104
  }
1102
1105
 
1106
+ ::view-transition-group(zfb-loading) {
1107
+ animation: none;
1108
+ }
1109
+
1110
+ ::view-transition-old(zfb-loading), ::view-transition-new(zfb-loading) {
1111
+ display: none;
1112
+ }
1113
+
1103
1114
  header[data-header] {
1104
1115
  background-color: var(--color-surface, var(--color-bg));
1105
1116
  z-index: var(--z-index-toolbar, 20);
@@ -1294,7 +1305,7 @@ pre.hi-root .line .highlighted-word {
1294
1305
  white-space: nowrap;
1295
1306
  }
1296
1307
 
1297
- .katex .base {
1308
+ .katex .katex-base {
1298
1309
  color: var(--zd-fg);
1299
1310
  }
1300
1311
 
@@ -1403,11 +1414,11 @@ pre.hi-root .line .highlighted-word {
1403
1414
  }
1404
1415
  }
1405
1416
 
1406
- [data-zfb-transition-persist^="header-"] {
1417
+ body > header {
1407
1418
  view-transition-name: zfb-header;
1408
1419
  }
1409
1420
 
1410
- [data-zfb-transition-persist^="sidebar-"] {
1421
+ #desktop-sidebar:not(.sr-only) {
1411
1422
  view-transition-name: zfb-sidebar;
1412
1423
  }
1413
1424
 
@@ -1415,26 +1426,34 @@ pre.hi-root .line .highlighted-word {
1415
1426
  view-transition-name: zfb-footer;
1416
1427
  }
1417
1428
 
1418
- [data-zfb-transition-persist="desktop-sidebar-toggle"] {
1429
+ .zd-desktop-sidebar-toggle {
1419
1430
  view-transition-name: zfb-sidebar-toggle;
1420
1431
  }
1421
1432
 
1422
- [data-zfb-transition-persist="desktop-toc-toggle"] {
1433
+ .zd-desktop-toc-toggle {
1423
1434
  view-transition-name: zfb-toc-toggle;
1424
1435
  }
1425
1436
 
1426
- ::view-transition-old(zfb-header), ::view-transition-new(zfb-header), ::view-transition-group(zfb-header), ::view-transition-old(zfb-sidebar), ::view-transition-new(zfb-sidebar), ::view-transition-group(zfb-sidebar), ::view-transition-old(zfb-footer), ::view-transition-new(zfb-footer), ::view-transition-group(zfb-footer), ::view-transition-old(zfb-sidebar-toggle), ::view-transition-new(zfb-sidebar-toggle), ::view-transition-group(zfb-sidebar-toggle), ::view-transition-old(zfb-toc-toggle), ::view-transition-new(zfb-toc-toggle), ::view-transition-group(zfb-toc-toggle) {
1437
+ ::view-transition-old(zfb-header), ::view-transition-new(zfb-header), ::view-transition-group(zfb-header), ::view-transition-group(zfb-sidebar), ::view-transition-old(zfb-footer), ::view-transition-new(zfb-footer), ::view-transition-group(zfb-footer), ::view-transition-old(zfb-sidebar-toggle), ::view-transition-new(zfb-sidebar-toggle), ::view-transition-group(zfb-sidebar-toggle), ::view-transition-old(zfb-toc-toggle), ::view-transition-new(zfb-toc-toggle), ::view-transition-group(zfb-toc-toggle) {
1427
1438
  animation: none;
1428
1439
  }
1429
1440
 
1430
- ::view-transition-old(zfb-header):only-child, ::view-transition-old(zfb-sidebar):only-child, ::view-transition-old(zfb-footer):only-child, ::view-transition-old(zfb-sidebar-toggle):only-child, ::view-transition-old(zfb-toc-toggle):only-child {
1441
+ ::view-transition-group(zfb-header) {
1442
+ z-index: var(--z-index-toolbar, 20);
1443
+ }
1444
+
1445
+ ::view-transition-old(zfb-sidebar), ::view-transition-old(zfb-footer):only-child, ::view-transition-old(zfb-sidebar-toggle):only-child, ::view-transition-old(zfb-toc-toggle):only-child {
1431
1446
  animation: var(--default-transition-duration) ease-in both contentFadeOut;
1432
1447
  }
1433
1448
 
1434
- ::view-transition-new(zfb-header):only-child, ::view-transition-new(zfb-sidebar):only-child, ::view-transition-new(zfb-footer):only-child, ::view-transition-new(zfb-sidebar-toggle):only-child, ::view-transition-new(zfb-toc-toggle):only-child {
1449
+ ::view-transition-new(zfb-sidebar), ::view-transition-new(zfb-footer):only-child, ::view-transition-new(zfb-sidebar-toggle):only-child, ::view-transition-new(zfb-toc-toggle):only-child {
1435
1450
  animation: var(--zd-transition-slower) ease-out both contentFadeIn;
1436
1451
  }
1437
1452
 
1453
+ :root[data-zd-sidebar-transition="stationary"]::view-transition-old(zfb-sidebar), :root[data-zd-sidebar-transition="stationary"]::view-transition-new(zfb-sidebar) {
1454
+ animation: none;
1455
+ }
1456
+
1438
1457
  ::view-transition-old(root) {
1439
1458
  animation: var(--default-transition-duration) ease-in both contentFadeOut;
1440
1459
  }
@@ -1444,7 +1463,7 @@ pre.hi-root .line .highlighted-word {
1444
1463
  }
1445
1464
 
1446
1465
  @media (prefers-reduced-motion: reduce) {
1447
- ::view-transition-old(root), ::view-transition-new(root), ::view-transition-group(root), ::view-transition-old(zfb-header):only-child, ::view-transition-old(zfb-sidebar):only-child, ::view-transition-old(zfb-footer):only-child, ::view-transition-old(zfb-sidebar-toggle):only-child, ::view-transition-old(zfb-toc-toggle):only-child, ::view-transition-new(zfb-header):only-child, ::view-transition-new(zfb-sidebar):only-child, ::view-transition-new(zfb-footer):only-child, ::view-transition-new(zfb-sidebar-toggle):only-child, ::view-transition-new(zfb-toc-toggle):only-child {
1466
+ ::view-transition-old(root), ::view-transition-new(root), ::view-transition-group(root), ::view-transition-old(zfb-sidebar), ::view-transition-old(zfb-footer):only-child, ::view-transition-old(zfb-sidebar-toggle):only-child, ::view-transition-old(zfb-toc-toggle):only-child, ::view-transition-new(zfb-sidebar), ::view-transition-new(zfb-footer):only-child, ::view-transition-new(zfb-sidebar-toggle):only-child, ::view-transition-new(zfb-toc-toggle):only-child {
1448
1467
  animation: none;
1449
1468
  }
1450
1469
  }
package/dist/content.css CHANGED
@@ -52,6 +52,9 @@
52
52
  color: var(--color-fg);
53
53
  font-size: var(--text-body);
54
54
  line-height: var(--leading-relaxed);
55
+ /* Let ordinary prose and navigation labels reflow at narrow widths (#4585).
56
+ * Preformatted code keeps its white-space contract and local scrolling. */
57
+ overflow-wrap: anywhere;
55
58
  }
56
59
 
57
60
  /* ── Flow spacing (vertical rhythm) ── */
@@ -1,5 +1,6 @@
1
1
  import { jsx, jsxs } from "@takazudo/zfb/zudo-react/jsx-runtime";
2
2
  import { ClientRouter } from "@takazudo/zfb-runtime";
3
+ import { buildChromeTransitionBootstrap } from "../transitions/chrome-transition-bootstrap.js";
3
4
  const DESKTOP_SIDEBAR_ID = "desktop-sidebar";
4
5
  function DocLayout(props) {
5
6
  const {
@@ -70,6 +71,7 @@ function DocLayout(props) {
70
71
  );
71
72
  }
72
73
  if (head !== void 0) headChildren.push(head);
74
+ if (enableClientRouter !== false) headChildren.push(/* @__PURE__ */ jsx("script", { rawHtml: buildChromeTransitionBootstrap() }));
73
75
  return /* @__PURE__ */ jsxs("html", { ...htmlAttrs, children: [
74
76
  /* @__PURE__ */ jsx("head", { children: headChildren }),
75
77
  /* @__PURE__ */ jsxs("body", { class: "min-h-screen antialiased", children: [
package/dist/features.css CHANGED
@@ -283,7 +283,7 @@ pre.hi-root .line .highlighted-word {
283
283
  }
284
284
 
285
285
  /* Make KaTeX respect our color scheme */
286
- .katex .base {
286
+ .katex .katex-base {
287
287
  color: var(--zd-fg);
288
288
  }
289
289
 
@@ -457,56 +457,22 @@ html[data-sidebar-hidden] .zd-doc-content-band[data-zd-wide] {
457
457
  }
458
458
  }
459
459
 
460
- /* Root cross-fade for same-document View Transitions (Strategy B).
461
- *
462
- * @view-transition { navigation: auto; } (Strategy A, cross-document) is
463
- * deleted — Strategy B uses <ClientRouter /> which drives same-document
464
- * transitions via document.startViewTransition on each navigation.
465
- *
466
- * Chrome extraction via view-transition-name (Strategy B+, zudolab/zudo-doc#1558):
467
- * <header>, <aside id="desktop-sidebar">, <footer>, the desktop-sidebar-toggle
468
- * button, and (since #3254) the desktop-toc-toggle button each carry a
469
- * data-zfb-transition-persist attribute whose value is keyed by locale and
470
- * nav-section. The five attribute selectors below assign a stable
471
- * view-transition-name based on the attribute value prefix, extracting those
472
- * elements from the root snapshot into their own named layers:
473
- * - header-{lang} → zfb-header
474
- * - sidebar-{locale}-… → zfb-sidebar
475
- * - footer-{lang} → zfb-footer
476
- * - desktop-sidebar-toggle → zfb-sidebar-toggle
477
- * - desktop-toc-toggle → zfb-toc-toggle
478
- *
479
- * Once extracted, the root cross-fade animates only non-chrome content (main,
480
- * article, TOC, etc.). The ::view-transition-{old,new,group}(<name>) rules
481
- * disable animation for all five chrome layers. The group pseudo must be
482
- * neutralised too — even when old/new are static the group container can still
483
- * produce a geometry-morph animation when snapshot size/position differs.
484
- *
485
- * Entry/exit cross-fade (zudolab/zudo-doc#2072): animation: none is only
486
- * correct when the chrome element exists on BOTH pages of a navigation.
487
- * When it exists on one side only (docs page with sidebar → top page
488
- * without), the named group holds a single snapshot — frozen at full
489
- * opacity for the whole transition, then dropped abruptly at finish. The
490
- * :only-child rules below detect that one-sided case and cross-fade the
491
- * lone snapshot in sync with the root content fade (spec-standard
492
- * entry/exit pattern; :only-child outranks the animation: none rules via
493
- * the extra pseudo-class specificity). */
494
-
495
- /* Chrome extraction — assign view-transition-name from data-zfb-transition-persist */
496
- [data-zfb-transition-persist^="header-"] { view-transition-name: zfb-header; }
497
- [data-zfb-transition-persist^="sidebar-"] { view-transition-name: zfb-sidebar; }
460
+ /* Visual region identity is independent of persistence/remount policy. The
461
+ * layout's header slot is a direct body header, including consumer overrides.
462
+ * A sidebar keeps its name even when incoming island props require replacement.
463
+ * Never name an ancestor of these regions: it would move their whole layer.
464
+ */
465
+ body > header { view-transition-name: zfb-header; }
466
+ #desktop-sidebar:not(.sr-only) { view-transition-name: zfb-sidebar; }
498
467
  [data-zfb-transition-persist^="footer-"] { view-transition-name: zfb-footer; }
499
- [data-zfb-transition-persist="desktop-sidebar-toggle"] { view-transition-name: zfb-sidebar-toggle; }
500
- [data-zfb-transition-persist="desktop-toc-toggle"] { view-transition-name: zfb-toc-toggle; }
468
+ .zd-desktop-sidebar-toggle { view-transition-name: zfb-sidebar-toggle; }
469
+ .zd-desktop-toc-toggle { view-transition-name: zfb-toc-toggle; }
501
470
 
502
- /* Disable animation for all four chrome layers (old, new, and group), plus
503
- * the desktop-toc-toggle button (#3254 — mirrors desktop-sidebar-toggle so
504
- * it doesn't animate on SPA swaps). */
471
+ /* Header snapshots and every chrome group stay at their authored geometry.
472
+ * Footer and toggle pairs keep their established stationary policy. */
505
473
  ::view-transition-old(zfb-header),
506
474
  ::view-transition-new(zfb-header),
507
475
  ::view-transition-group(zfb-header),
508
- ::view-transition-old(zfb-sidebar),
509
- ::view-transition-new(zfb-sidebar),
510
476
  ::view-transition-group(zfb-sidebar),
511
477
  ::view-transition-old(zfb-footer),
512
478
  ::view-transition-new(zfb-footer),
@@ -518,24 +484,35 @@ html[data-sidebar-hidden] .zd-doc-content-band[data-zd-wide] {
518
484
  ::view-transition-new(zfb-toc-toggle),
519
485
  ::view-transition-group(zfb-toc-toggle) { animation: none; }
520
486
 
521
- /* Entry/exit: chrome element present on one side only (#2072).
522
- * Exit — lone old snapshot fades out with the content cross-fade. */
523
- ::view-transition-old(zfb-header):only-child,
524
- ::view-transition-old(zfb-sidebar):only-child,
487
+ /* A translating incoming sidebar/root must not paint over the header's lower
488
+ * edge. Named snapshot groups have their own stacking order above the live DOM. */
489
+ ::view-transition-group(zfb-header) { z-index: var(--z-index-toolbar, 20); }
490
+
491
+ /* A category/context change or a one-sided sidebar uses the content animation.
492
+ * The header remains stationary even when replaced or present on only one side.
493
+ * Other chrome retains its established entry/exit behavior. */
494
+ ::view-transition-old(zfb-sidebar),
525
495
  ::view-transition-old(zfb-footer):only-child,
526
496
  ::view-transition-old(zfb-sidebar-toggle):only-child,
527
497
  ::view-transition-old(zfb-toc-toggle):only-child {
528
498
  animation: var(--default-transition-duration) ease-in both contentFadeOut;
529
499
  }
530
500
  /* Entry — lone new snapshot fades in with the content cross-fade. */
531
- ::view-transition-new(zfb-header):only-child,
532
- ::view-transition-new(zfb-sidebar):only-child,
501
+ ::view-transition-new(zfb-sidebar),
533
502
  ::view-transition-new(zfb-footer):only-child,
534
503
  ::view-transition-new(zfb-sidebar-toggle):only-child,
535
504
  ::view-transition-new(zfb-toc-toggle):only-child {
536
505
  animation: var(--zd-transition-slower) ease-out both contentFadeIn;
537
506
  }
538
507
 
508
+ /* Resolved navSection AND the existing navigation context must agree. The
509
+ * context already isolates locale, version and custom-tree topology; article,
510
+ * focused subtree and DOM identity are deliberately irrelevant here. */
511
+ :root[data-zd-sidebar-transition="stationary"]::view-transition-old(zfb-sidebar),
512
+ :root[data-zd-sidebar-transition="stationary"]::view-transition-new(zfb-sidebar) {
513
+ animation: none;
514
+ }
515
+
539
516
  /* Root cross-fade rules — animate only the non-chrome snapshot */
540
517
  ::view-transition-old(root) {
541
518
  animation: var(--default-transition-duration) ease-in both contentFadeOut;
@@ -559,13 +536,11 @@ html[data-sidebar-hidden] .zd-doc-content-band[data-zd-wide] {
559
536
  ::view-transition-old(root),
560
537
  ::view-transition-new(root),
561
538
  ::view-transition-group(root),
562
- ::view-transition-old(zfb-header):only-child,
563
- ::view-transition-old(zfb-sidebar):only-child,
539
+ ::view-transition-old(zfb-sidebar),
564
540
  ::view-transition-old(zfb-footer):only-child,
565
541
  ::view-transition-old(zfb-sidebar-toggle):only-child,
566
542
  ::view-transition-old(zfb-toc-toggle):only-child,
567
- ::view-transition-new(zfb-header):only-child,
568
- ::view-transition-new(zfb-sidebar):only-child,
543
+ ::view-transition-new(zfb-sidebar),
569
544
  ::view-transition-new(zfb-footer):only-child,
570
545
  ::view-transition-new(zfb-sidebar-toggle):only-child,
571
546
  ::view-transition-new(zfb-toc-toggle):only-child {
@@ -15,13 +15,13 @@ export interface PageLoadingOverlayProps {
15
15
  */
16
16
  export declare function buildPageLoadingOverlayBootstrap(overlayId: string): string;
17
17
  /**
18
- * Full-page loading overlay shown during view-transition navigations.
18
+ * Content loading overlay shown while a dynamic navigation is pending.
19
19
  *
20
20
  * Mount this once per layout (typically inside `DocLayoutWithDefaults`'s
21
21
  * `bodyEnd` slot, alongside the existing body-end providers). It is
22
22
  * server-rendered and self-wires its visibility — no hydration needed.
23
23
  *
24
- * CSS lives in the host project's `src/styles/global.css` (`.page-loading-overlay`,
24
+ * CSS ships in `@takazudo/zudo-doc/page-loading.css` (`.page-loading-overlay`,
25
25
  * `.page-loading-spinner`, `[data-zd-nav-pending]` rules) rather than in an
26
26
  * inline `<style>` block here — a `<style>` inside `<body>` violates HTML5
27
27
  * element-permitted-content and fails html-validate (same fix applied to the
@@ -1,7 +1,8 @@
1
1
  import { Fragment, jsx, jsxs } from "@takazudo/zfb/zudo-react/jsx-runtime";
2
2
  import {
3
3
  AFTER_NAVIGATE_EVENT,
4
- BEFORE_NAVIGATE_EVENT
4
+ BEFORE_NAVIGATE_EVENT,
5
+ NAVIGATION_ABORTED_EVENT
5
6
  } from "../transitions/page-events.js";
6
7
  const PAGE_LOADING_OVERLAY_ID = "page-loading-overlay";
7
8
  function buildPageLoadingOverlayBootstrap(overlayId) {
@@ -10,7 +11,7 @@ function buildPageLoadingOverlayBootstrap(overlayId) {
10
11
  const after = JSON.stringify(AFTER_NAVIGATE_EVENT);
11
12
  return `(function(){
12
13
  var id=${id};
13
- function show(){var el=document.getElementById(id);if(!el)return;el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
14
+ function show(){var el=document.getElementById(id);if(!el)return;var content=document.querySelector(".zd-sidebar-content-wrapper");var header=document.querySelector("body > header");el.style.left=Math.max(0,content?content.getBoundingClientRect().left:0)+"px";el.style.top=Math.max(0,header?header.getBoundingClientRect().bottom:0)+"px";el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
14
15
  function hide(){var el=document.getElementById(id);if(!el)return;el.removeAttribute("data-visible");el.setAttribute("aria-hidden","true");}
15
16
  function setPending(ev){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});var src=ev&&ev.sourceElement;if(src&&src instanceof Element)src.setAttribute("data-zd-nav-pending","");}
16
17
  function clearPending(){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});}
@@ -18,6 +19,7 @@ document.addEventListener(${before},show);
18
19
  document.addEventListener(${after},hide);
19
20
  document.addEventListener(${before},setPending);
20
21
  document.addEventListener(${after},clearPending);
22
+ document.addEventListener(${JSON.stringify(NAVIGATION_ABORTED_EVENT)},function(){hide();clearPending();});
21
23
  })();`;
22
24
  }
23
25
  function PageLoadingOverlay({
@@ -31,8 +31,9 @@
31
31
 
32
32
  .page-loading-overlay {
33
33
  position: fixed;
34
+ view-transition-name: zfb-loading;
34
35
  inset: 0;
35
- /* Full-screen blocking load overlay — modal tier.
36
+ /* Pending-content overlay — the bootstrap excludes the layout's chrome.
36
37
  --z-index-modal fallback (100) ensures the overlay stacks on top even for
37
38
  bare consumers that have not defined the token. */
38
39
  z-index: var(--z-index-modal, 100);
@@ -45,11 +46,13 @@
45
46
  );
46
47
  opacity: 0;
47
48
  pointer-events: none;
48
- transition: opacity 150ms ease-out;
49
+ /* Hidden loading feedback must disappear synchronously. */
50
+ transition: none;
49
51
  }
50
52
 
51
53
  .page-loading-overlay[data-visible] {
52
54
  opacity: 1;
55
+ transition: opacity 150ms ease-out;
53
56
  }
54
57
 
55
58
  a[data-zd-nav-pending],
@@ -90,3 +93,11 @@ button[data-zd-nav-pending] {
90
93
  border-bottom-color: var(--color-page-loading-spinner, #fff);
91
94
  }
92
95
  }
96
+
97
+ /* Pending feedback belongs to the fetch, not the page cross-fade. Extract it
98
+ * from the root snapshot, then suppress its snapshot layer. This also avoids
99
+ * an unscoped after-preparation event from an older fetch hiding newer feedback.
100
+ */
101
+ ::view-transition-group(zfb-loading) { animation: none; }
102
+ ::view-transition-old(zfb-loading),
103
+ ::view-transition-new(zfb-loading) { display: none; }
@@ -0,0 +1,2 @@
1
+ /** Visual snapshot policy only; persistence and sidebar state keep their owners. */
2
+ export declare function buildChromeTransitionBootstrap(): string;
@@ -0,0 +1,23 @@
1
+ import { BEFORE_SWAP_EVENT } from "./page-events.js";
2
+ function buildChromeTransitionBootstrap() {
3
+ return `(function(){
4
+ if(document.__zdChromeTransitionPolicy)return;
5
+ document.__zdChromeTransitionPolicy=true;
6
+ function identity(doc){
7
+ var sidebar=doc.getElementById("desktop-sidebar");
8
+ var context=sidebar&&sidebar.querySelector("[data-sidebar-context]");
9
+ var band=doc.querySelector("[data-zd-nav-section]");
10
+ return {section:band&&band.getAttribute("data-zd-nav-section"),context:band&&!band.hasAttribute("data-zd-nosidebar")&&context&&context.getAttribute("data-sidebar-context")};
11
+ }
12
+ document.addEventListener(${JSON.stringify(BEFORE_SWAP_EVENT)},function(ev){
13
+ var incoming=ev.newDocument;
14
+ if(!incoming||incoming.nodeType!==9)return;
15
+ var from=identity(document),to=identity(incoming);
16
+ var stationary=!!from.section&&!!from.context&&from.section===to.section&&from.context===to.context;
17
+ incoming.documentElement.setAttribute("data-zd-sidebar-transition",stationary?"stationary":"changing");
18
+ });
19
+ })();`;
20
+ }
21
+ export {
22
+ buildChromeTransitionBootstrap
23
+ };
@@ -7,6 +7,10 @@
7
7
  * the v2 vocabulary.
8
8
  */
9
9
  export declare const BEFORE_NAVIGATE_EVENT = "zfb:before-preparation";
10
+ /** Fetch/preparation finished, before the native old snapshot is captured. */
11
+ export declare const AFTER_PREPARATION_EVENT = "zfb:after-preparation";
12
+ /** A navigation was abandoned before committing the destination DOM. */
13
+ export declare const NAVIGATION_ABORTED_EVENT = "zfb:navigation-aborted";
10
14
  /**
11
15
  * Event name fired after a SPA body swap. Today resolves to
12
16
  * `"zfb:after-swap"`. The initial page load uses `zfb:page-load` instead;
@@ -1,4 +1,6 @@
1
1
  const BEFORE_NAVIGATE_EVENT = "zfb:before-preparation";
2
+ const AFTER_PREPARATION_EVENT = "zfb:after-preparation";
3
+ const NAVIGATION_ABORTED_EVENT = "zfb:navigation-aborted";
2
4
  const AFTER_NAVIGATE_EVENT = "zfb:after-swap";
3
5
  const BEFORE_SWAP_EVENT = "zfb:before-swap";
4
6
  function onBeforeNavigate(handler) {
@@ -15,8 +17,10 @@ function onAfterNavigate(handler) {
15
17
  }
16
18
  export {
17
19
  AFTER_NAVIGATE_EVENT,
20
+ AFTER_PREPARATION_EVENT,
18
21
  BEFORE_NAVIGATE_EVENT,
19
22
  BEFORE_SWAP_EVENT,
23
+ NAVIGATION_ABORTED_EVENT,
20
24
  onAfterNavigate,
21
25
  onBeforeNavigate
22
26
  };
@@ -1,5 +1,5 @@
1
1
  /** @jsxRuntime automatic */
2
- // Server-rendered, zero-hydration full-page loading overlay.
2
+ // Server-rendered, zero-hydration content loading overlay.
3
3
  //
4
4
  // Renders the overlay and its bootstrap into the document:
5
5
  //
@@ -15,12 +15,13 @@
15
15
  // `zfb:after-swap` (AFTER_NAVIGATE_EVENT); see
16
16
  // `transitions/page-events.ts` for rationale.
17
17
  //
18
- // Its CSS stays in the host's global stylesheet, and the component is not
18
+ // Its package CSS is imported by the host, and the component is not
19
19
  // hydrated: the static script owns its navigation listeners.
20
20
 
21
21
  import {
22
22
  AFTER_NAVIGATE_EVENT,
23
23
  BEFORE_NAVIGATE_EVENT,
24
+ NAVIGATION_ABORTED_EVENT,
24
25
  } from "../transitions/page-events.js";
25
26
 
26
27
  /** Default `id` for the overlay element. Stable so test rigs can target it. */
@@ -52,7 +53,7 @@ export function buildPageLoadingOverlayBootstrap(overlayId: string): string {
52
53
  const after = JSON.stringify(AFTER_NAVIGATE_EVENT);
53
54
  return `(function(){
54
55
  var id=${id};
55
- function show(){var el=document.getElementById(id);if(!el)return;el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
56
+ function show(){var el=document.getElementById(id);if(!el)return;var content=document.querySelector(".zd-sidebar-content-wrapper");var header=document.querySelector("body > header");el.style.left=Math.max(0,content?content.getBoundingClientRect().left:0)+"px";el.style.top=Math.max(0,header?header.getBoundingClientRect().bottom:0)+"px";el.setAttribute("data-visible","");el.setAttribute("aria-hidden","false");}
56
57
  function hide(){var el=document.getElementById(id);if(!el)return;el.removeAttribute("data-visible");el.setAttribute("aria-hidden","true");}
57
58
  function setPending(ev){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});var src=ev&&ev.sourceElement;if(src&&src instanceof Element)src.setAttribute("data-zd-nav-pending","");}
58
59
  function clearPending(){document.querySelectorAll("[data-zd-nav-pending]").forEach(function(el){el.removeAttribute("data-zd-nav-pending");});}
@@ -60,17 +61,18 @@ document.addEventListener(${before},show);
60
61
  document.addEventListener(${after},hide);
61
62
  document.addEventListener(${before},setPending);
62
63
  document.addEventListener(${after},clearPending);
64
+ document.addEventListener(${JSON.stringify(NAVIGATION_ABORTED_EVENT)},function(){hide();clearPending();});
63
65
  })();`;
64
66
  }
65
67
 
66
68
  /**
67
- * Full-page loading overlay shown during view-transition navigations.
69
+ * Content loading overlay shown while a dynamic navigation is pending.
68
70
  *
69
71
  * Mount this once per layout (typically inside `DocLayoutWithDefaults`'s
70
72
  * `bodyEnd` slot, alongside the existing body-end providers). It is
71
73
  * server-rendered and self-wires its visibility — no hydration needed.
72
74
  *
73
- * CSS lives in the host project's `src/styles/global.css` (`.page-loading-overlay`,
75
+ * CSS ships in `@takazudo/zudo-doc/page-loading.css` (`.page-loading-overlay`,
74
76
  * `.page-loading-spinner`, `[data-zd-nav-pending]` rules) rather than in an
75
77
  * inline `<style>` block here — a `<style>` inside `<body>` violates HTML5
76
78
  * element-permitted-content and fails html-validate (same fix applied to the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "6.2.0",
3
+ "version": "7.0.0",
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",
@@ -14,6 +14,9 @@
14
14
  "bugs": {
15
15
  "url": "https://github.com/zudolab/zudo-doc/issues"
16
16
  },
17
+ "engines": {
18
+ "node": ">=24 <25"
19
+ },
17
20
  "keywords": [
18
21
  "zudo-doc",
19
22
  "documentation",
@@ -733,12 +736,12 @@
733
736
  "peerDependencies": {
734
737
  "@modelcontextprotocol/sdk": "1.32.1",
735
738
  "@takazudo/zdtp": "^0.5.2 || ^0.6.0 || ^0.7.0 || ^0.8.0",
736
- "@takazudo/zfb": "^4.5.0",
737
- "@takazudo/zfb-md-wasm": "^4.5.0",
738
- "@takazudo/zfb-runtime": "^4.5.0",
739
- "@takazudo/zudo-doc-history-server": "^6.0.0",
739
+ "@takazudo/zfb": "^4.5.2",
740
+ "@takazudo/zfb-md-wasm": "^4.5.2",
741
+ "@takazudo/zfb-runtime": "^4.5.2",
742
+ "@takazudo/zudo-doc-history-server": "^7.0.0",
740
743
  "diff": "^8.0.0",
741
- "katex": "^0.16.0",
744
+ "katex": "^0.18.2 || ^0.19.0",
742
745
  "zod": "^4.3.6"
743
746
  },
744
747
  "peerDependenciesMeta": {
@@ -776,9 +779,9 @@
776
779
  "devDependencies": {
777
780
  "@modelcontextprotocol/sdk": "1.32.1",
778
781
  "@takazudo/mdx-formatter": "1.3.0",
779
- "@takazudo/zfb": "4.5.0",
780
- "@takazudo/zfb-md-wasm": "4.5.0",
781
- "@takazudo/zfb-runtime": "4.5.0",
782
+ "@takazudo/zfb": "4.5.2",
783
+ "@takazudo/zfb-md-wasm": "4.5.2",
784
+ "@takazudo/zfb-runtime": "4.5.2",
782
785
  "@types/fs-extra": "^11.0.4",
783
786
  "@types/minimist": "^1.2.5",
784
787
  "@types/node": "^25.3.5",
@@ -788,7 +791,7 @@
788
791
  "typescript": "^5.0.0",
789
792
  "vitest": "^4.1.0",
790
793
  "zod": "^4.3.6",
791
- "@takazudo/zudo-doc-history-server": "6.2.0"
794
+ "@takazudo/zudo-doc-history-server": "7.0.0"
792
795
  },
793
796
  "scripts": {
794
797
  "gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",