create-zudo-doc 0.2.9 → 0.2.11

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.
@@ -22,6 +22,85 @@ function setDataAttribute(isVisible: boolean) {
22
22
  }
23
23
  }
24
24
 
25
+ // SPA-navigation guard for the desktop sidebar's hidden-state (#2198).
26
+ //
27
+ // zfb's Strategy-B client router wipes EVERY <html> attribute during the body
28
+ // swap (swapRootAttributes re-adds only NON_OVERRIDABLE_ZFB_ATTRS plus the
29
+ // incoming SSR document's attributes), so the persisted `data-sidebar-hidden`
30
+ // runtime value is lost on every navigation, and the pre-paint inline script
31
+ // does not re-run on SPA hops. Left alone, the freshly-rendered sidebar paints
32
+ // visible and then animates shut when the value is restored — the flash + slide.
33
+ //
34
+ // These listeners MUST live at module scope, NOT in the island's useEffect:
35
+ // zfb-runtime (>= 0.1.0-next.51) calls unmountIslands() BEFORE the swap and
36
+ // before firing AFTER_NAVIGATE_EVENT, so a useEffect-registered listener is torn
37
+ // down (effect cleanup) before the swap and never sees the after-swap event —
38
+ // the restore would never run (#2198 verification). Registering once on
39
+ // `document` at bundle load — the same lifecycle-independent pattern as
40
+ // client-router-bootstrap.tsx — keeps the guard alive across island
41
+ // unmount/remount. SSR-safe: no-ops when `document` is undefined.
42
+ //
43
+ // Strategy: on BEFORE_NAVIGATE_EVENT, record whether the sidebar was hidden and
44
+ // set a transient `data-sidebar-no-transition` marker (global.css zeroes the
45
+ // sidebar/wrapper/band/toggle transitions). The swap then wipes BOTH the marker
46
+ // and data-sidebar-hidden. On AFTER_NAVIGATE_EVENT (swap done → the live <html>
47
+ // is no longer wiped) RE-SET the marker, then re-add data-sidebar-hidden in the
48
+ // same synchronous batch, so the hidden geometry snaps in with transitions
49
+ // suppressed (no slide, no open frame). Remove the marker on a double rAF
50
+ // afterward so a later user toggle still animates.
51
+ let spaNavGuardRegistered = false;
52
+ function registerSidebarSpaNavGuard() {
53
+ if (typeof document === 'undefined' || spaNavGuardRegistered) return;
54
+ spaNavGuardRegistered = true;
55
+
56
+ let wasHidden = false;
57
+ let swapToken = 0;
58
+ // Long enough to outlast the View-Transition swap pipeline on slow
59
+ // machines/CI; short enough that an aborted nav self-heals quickly. Only a
60
+ // safety net for a nav that never reaches `restore` (aborted/superseded).
61
+ const STUCK_MARKER_FALLBACK_MS = 1500;
62
+
63
+ const capture = () => {
64
+ wasHidden = document.documentElement.hasAttribute('data-sidebar-hidden');
65
+ document.documentElement.setAttribute('data-sidebar-no-transition', '');
66
+ const token = ++swapToken;
67
+ window.setTimeout(() => {
68
+ if (token === swapToken) {
69
+ document.documentElement.removeAttribute('data-sidebar-no-transition');
70
+ }
71
+ }, STUCK_MARKER_FALLBACK_MS);
72
+ };
73
+
74
+ const restore = () => {
75
+ // Marker first, then the attribute, in one synchronous batch — so the
76
+ // hidden geometry is restored with transitions off (the swap already wiped
77
+ // the capture-time marker, so re-setting it here on the post-swap <html> is
78
+ // what actually suppresses the animation).
79
+ document.documentElement.setAttribute('data-sidebar-no-transition', '');
80
+ if (wasHidden) {
81
+ document.documentElement.setAttribute('data-sidebar-hidden', '');
82
+ }
83
+ // Drop the marker only after the restored state is committed. A single rAF
84
+ // can coincide with the restore's style recalc and animate it, so defer one
85
+ // extra frame. The token bump no-ops capture's safety-net for this swap.
86
+ const token = ++swapToken;
87
+ requestAnimationFrame(() => {
88
+ requestAnimationFrame(() => {
89
+ if (token === swapToken) {
90
+ document.documentElement.removeAttribute('data-sidebar-no-transition');
91
+ }
92
+ });
93
+ });
94
+ };
95
+
96
+ document.addEventListener(BEFORE_NAVIGATE_EVENT, capture);
97
+ document.addEventListener(AFTER_NAVIGATE_EVENT, restore);
98
+ }
99
+
100
+ // Register at module load (browser-only via the guard) so the listeners exist
101
+ // before the first navigation and survive island unmount/remount.
102
+ registerSidebarSpaNavGuard();
103
+
25
104
  export default function DesktopSidebarToggle() {
26
105
  // Initial state must match server render (always `true`) to avoid a
27
106
  // hydration mismatch when the persisted preference is "hidden". The
@@ -66,36 +145,10 @@ export default function DesktopSidebarToggle() {
66
145
  // eslint-disable-next-line react-hooks/exhaustive-deps
67
146
  }, []);
68
147
 
69
- // Re-apply data-sidebar-hidden to <html> after every SPA nav.
70
- // zfb's swapRootAttributes wipes all non-preserved <html> attributes on
71
- // each navigation (data-sidebar-hidden is not in NON_OVERRIDABLE_ZFB_ATTRS),
72
- // and the pre-paint inline script does not re-run on SPA nav. Since this
73
- // island is persisted (data-zfb-transition-persist), this listener stays
74
- // registered across SPA swaps.
75
- //
76
- // Strategy: capture the attribute presence just before the swap
77
- // (BEFORE_NAVIGATE_EVENT fires before swapRootAttributes runs), then
78
- // restore it once the swap completes (AFTER_NAVIGATE_EVENT). This is
79
- // authoritative regardless of how the attribute was set (toggle click,
80
- // localStorage, or external mutation). (#1551, #1552 B10)
81
- useEffect(() => {
82
- let wasHidden = false;
83
- const capture = () => {
84
- wasHidden = document.documentElement.hasAttribute('data-sidebar-hidden');
85
- };
86
- const restore = () => {
87
- if (wasHidden) {
88
- document.documentElement.setAttribute('data-sidebar-hidden', '');
89
- }
90
- // If not hidden, swapRootAttributes already cleared it — nothing to do.
91
- };
92
- document.addEventListener(BEFORE_NAVIGATE_EVENT, capture);
93
- document.addEventListener(AFTER_NAVIGATE_EVENT, restore);
94
- return () => {
95
- document.removeEventListener(BEFORE_NAVIGATE_EVENT, capture);
96
- document.removeEventListener(AFTER_NAVIGATE_EVENT, restore);
97
- };
98
- }, []);
148
+ // The SPA-navigation flash guard (data-sidebar-hidden preservation +
149
+ // transition suppression) is NOT registered here. It must outlive this
150
+ // island's mount/unmount, so it lives at module scope — see
151
+ // registerSidebarSpaNavGuard() above. (#2198)
99
152
 
100
153
  return (
101
154
  <button