@takazudo/zudo-doc 5.7.0 → 5.8.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
@@ -4,6 +4,23 @@ 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.8.0] - 2026-08-20
8
+
9
+ ### Features
10
+
11
+ - Added strict anchor validation to the showcase and generated-project link checkers, including source-first validation for scaffolded sites. (`b50bd9aac`, `59127e7fe`)
12
+ - Added a published-version guard that prevents release scaffolds from pinning first-party packages that are not yet available from npm. (`4bdc37132`, `8c234c08e`)
13
+ - Shipped a complete nav-overflow regeneration path with ejected headers, preserving stable CSP bytes while allowing local nav behavior and class-token customization. (`1b3d501c3`, `721ff37d3`)
14
+
15
+ ### Bug Fixes
16
+
17
+ - Made nested island props preservation explicit with `data-zd-props-preserve`, preventing opted-out islands from receiving props updates or remount flags during client navigation. (`e75cff084`)
18
+
19
+ ### Other Changes
20
+
21
+ - Strengthened `docTags` regression coverage so an unselected feature cannot emit either a `true` or `false` config field. (`37dec787c`)
22
+ - Updated `@takazudo/zdtp` to 0.4.12 across the package, showcase, and generated scaffold.
23
+
7
24
  ## [5.7.0] - 2026-08-20
8
25
 
9
26
  ### Features
package/README.md CHANGED
@@ -67,6 +67,23 @@ pnpm add @takazudo/zfb-md-wasm
67
67
 
68
68
  Projects scaffolded by `create-zudo-doc` already include it. If you never render `<HtmlPreview>` / `<HighlightedCode>`, you can omit it.
69
69
 
70
+ ## Ejected header customization
71
+
72
+ `zudo-doc eject header` copies a complete frozen-script regeneration path into
73
+ `src/components/zudo-doc/header`. After changing the ejected `nav-active.ts` or
74
+ `nav-class-tokens.ts`, regenerate the local client controller and commit the
75
+ result:
76
+
77
+ ```sh
78
+ node ./src/components/zudo-doc/header/gen-nav-overflow-script.mjs
79
+ ```
80
+
81
+ The command uses the two local customization inputs plus the installed
82
+ package's current-path and page-event inputs. Its `esbuild` transformer is
83
+ provided by `@takazudo/zudo-doc`; no additional project dependency is needed.
84
+ The resulting `nav-overflow-generated-script.ts` stays frozen for stable CSP
85
+ hashes across consumer bundlers.
86
+
70
87
  ## ⚠️ HTML preview iframe sandbox — trust assumption
71
88
 
72
89
  `<HtmlPreview>` / `<HtmlPreviewWrapper>` render their preview inside an `<iframe srcdoc>` whose `sandbox` attribute **defaults** to:
@@ -340,6 +340,15 @@ ${status} from @takazudo/zudo-doc@${installedVersion}
340
340
  `
341
341
  )
342
342
  );
343
+ if (component === "header") {
344
+ console.log(
345
+ pc.bold("Header frozen-script regeneration:") + `
346
+ After editing ${localDir}/nav-active.ts or nav-class-tokens.ts, run:
347
+ node ./${localDir}/gen-nav-overflow-script.mjs
348
+ Commit ${localDir}/nav-overflow-generated-script.ts with your edits.
349
+ `
350
+ );
351
+ }
343
352
  }
344
353
  export {
345
354
  EJECTABLE,
package/dist/safelist.css CHANGED
@@ -1,2 +1,2 @@
1
1
  /* generated by gen-safelist.mjs — do not edit by hand */
2
- @source inline("-domtweaker-enabled -elpath-enabled -link -mb-px -ml-hsp-sm -noscript -open -state -translate-x-full 2xl:w-[24px] [&::-webkit-details-marker]:hidden [&_a]:text-accent [&_a]:underline [&_nav]:mb-0 [data-admonition] [data-kbd-shortcut] [data-switcher-launcher] [doc-history-meta] [doc-history] [doc-layout] [llms-txt] [zudo-doc] a a2 abbr about above absent absolute accent accent- accent:accent- access across activated active actual actually added admonition admonition- admonition-body admonition-title admonition/callout after after-breadcrumb after-content after-navigate after-sidebar after-title against agent agents ai-chat ai-chat-md ai-chat-trigger alert align-top all allow-same-origin allow-scripts allowed alone already already-executed already-multiline already-picked also always an anchor and and/or animate-pulse animate-spin announce ansehen antialiased any anywhere anzeigen app appear application/json application/xml applied applies apply applying approach are area arg argument aria-atomic aria-busy aria-controls aria-current aria-disabled aria-expanded aria-haspopup aria-hidden aria-label aria-labelledby aria-live aria-orientation aria-pressed aria-selected aria-valuemax aria-valuemin aria-valuenow arm arms arrows article as asc aside aspect-[1200/630] aspect-square asset- assets assistant async at at-rule attach attribute attributes auf authored auto auto-logo-mask autogenerated availability available avoid await away b back backdrop:bg-bg/30 backdrop:bg-bg/80 backdrop:bg-overlay/60 backdrop:z-modal-backdrop background background-color backtick backticks baked banner bare base base- base64 base:base- based batch be bearbeiten because becomes been before below best best-effort between bg bg-[#fff] bg-accent bg-bg bg-chat-assistant-bg bg-chat-user-bg bg-code-bg bg-fg bg-info/10 bg-info/5 bg-muted bg-overlay/30 bg-surface bg-surface/50 bg-transparent bg-warning/10 bg-warning/5 bi bigint bin binaries bind binding blank blanks block blockquote blocks blur body body-end-components body-end-scripts bold boolean bootstrap border border-accent border-b border-b-2 border-b-[5px] border-bg/30 border-collapse border-danger border-dashed border-fg border-image border-info/30 border-l border-l-0 border-l-[3px] border-left-width border-muted border-none border-r border-r-0 border-radius border-solid border-t border-t-[2px] border-t-[3px] border-transparent border-warning/30 border-width border-y both bottom-hsp-lg bottom-vsp-xl boundaries box box-border br brackets brand breadcrumb:end breadcrumb:start break-words brief brown browser browser-tab browsers browses btn budget bug build builder built built-in bundler but button buttons by bypassed byte-identical bytes cache cached call callable called caller calls can cancellation candidate cannot canonical canvas caption captured captures card card-grid carry cases cat-nav- catch category caught caution center center/contain ch chains change changed changelog changelogs changes check child children choose chrome chrome-font ci circle cite class class-less class-mode claude claude-agents claude-commands claude-md claude-skills cleaned cleanly clear clearing click client client-router client-side clip clobber clobbering close closed closes closing closure code code-block-sr-announce code-group code-group-panel col col-resize colgroup collapses collapsible collision color color-scheme color-scheme-changed color-scheme-provider color-tweak colorization colors column comma command commands commas comment commercial commercial-font-denylist commit compare component component:github-link component:language-switcher component:search component:theme-toggle component:version-switcher composes composition compute computed concrete config configuration configurations configure configured confuse connect const construction consumer consumes contain container containers containing contains content content-admonition content-layer content-link content-type content-wrapper:end content-wrapper:start contents context contract control controller controls converts cookie-blocking copied copy core corners correct correctly corrupt could count covered covers crashes created cross-component crumb- cs css css-presence ctx cur current current-path/index.ts current-route currently cursor cursor-not-allowed cursor-pointer custom cycle danger dark data data-active data-admonition data-auto-logo data-base data-close-search data-current-locale data-default-locale data-doc-description data-doc-metainfo data-doc-pager data-doc-unavailable-versions data-find-active data-find-match data-footer data-group-id data-header data-header-logo data-header-nav data-header-right data-kbd-shortcut data-language-switcher data-loading-index data-mermaid-enlarge-ready data-mermaid-rendered data-mermaid-src data-nav-active data-nav-item data-nav-item-dropdown data-nav-more data-nav-more-menu data-nav-more-toggle data-no-results data-open-search data-pan-active data-processed data-props data-result-count-template data-search-count data-search-count-narrow data-search-dialog data-search-input data-search-placeholder data-search-results data-search-unavailable data-sidebar-hidden data-sidebar-resizer data-site-nav data-switcher-card data-switcher-launcher data-tab-btn data-tab-default data-tab-label data-tab-value data-tabs data-taglist-group data-testid data-theme data-theme-pack data-theme-pack-switcher data-theme/style data-toc-hidden data-trailing-slash data-unavailable-label data-variant data-version-banner data-version-latest data-version-menu data-version-rewire data-version-slug data-version-switcher data-version-toggle data-version-trigger-label data-zd-mobile-sidebar data-zd-mobile-toc data-zd-nosidebar data-zd-theme-pack-css data-zd-theme-pack-css-loading data-zd-theme-pack-loading data-zd-toc data-zd-wide data-zfb-island data-zfb-island-remount data-zfb-transition-persist dd decimal declaration declare declares decoration decoration-muted deepest-match default default-transition-duration defaults deferral deferred del delegated delete deliberately delimiter dependency depends depth der desc description design design-token design-token-panel design-token-trigger desktop desktop-sidebar desktop-sidebar-toggle desktop-sidebar-toggle-island desktop-toc-toggle destroys destructive detach detached details determine deterministic dev develop dfn diagram diagrams dialog did die dieser diff diff-line-added diff-line-content diff-line-empty diff-line-num diff-line-removed diff-row different dir directly directories directory disabled disabled:opacity-50 disabled:pointer-events-none disc display display:none dist distance distinct div dl do doc doc-card- doc-content-band doc-history doc-history-generate doc-history-panel doc-history-trigger doc-page doc-pager doc-prose doc-title docblock docs docs- docs-v- document document-level documentation documented does dog double-registration draft drag drawer drift drifts drop dropdown dropdown-parent dropdowns dt duplicate duration-150 duration-200 during dynamically e e2e each earlier early ease-in-out edge editing einer either eject ejectable ejectables ejected el element elements els else em embedded emit emitting empty empty/undefined en enable enabled end enhanced enhancement enlarged entire entirely entities entries entry entrypoint equal error escape escaped even eventually every everything-enabled exactly exceeds excerpt excludes exclusively existing exists exit expected explicit export extends extra f factories failed fall fallback fallbacks falling falls false family fast favicon feature fg field fields fieldset figcaption figure file fill fills finally find find-match find-match-active fire fires first first-paint fix fixed fixed-width fixtures flag flash flat flex flex-1 flex-col flex-wrap flip flipping flips flow flush-left focus focus-visible:border-accent focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 focus-visible:text-accent focus-visible:underline focus:border-accent focus:outline-none focus:text-accent focus:underline font font-bold font-face-parity font-family font-file-missing font-medium font-mono font-sans font-scale font-semibold font-size font-weight font-weight-bold font-weight-medium font-weight-normal font-weight-semibold font/woff2 fonts footer footer- for form format former found four-link fox frame free freeze fresh from frontmatter frontmatter-preview frozen fs-extra full fully function further g gains gap-[0.3em] gap-[clamp(1.5rem,3vw,4rem)] gap-hsp-2xs gap-hsp-lg gap-hsp-md gap-hsp-sm gap-hsp-xl gap-hsp-xs gap-vsp-2xs gap-vsp-3xs gap-vsp-lg gap-vsp-md gap-vsp-xs gap-x-hsp-2xs gap-x-hsp-lg gap-x-hsp-md gap-x-hsp-sm gap-x-hsp-xs gap-y-vsp-2xs gap-y-vsp-lg gap-y-vsp-md gap-y-vsp-xs gaps gate geladen2026 generate generated generation genuine geometry get getting-started github github-dark github-link give go got grab gradient granular graph gray-matter grid grid-cols-1 grid-cols-2 group group-focus-visible:text-accent group-focus-visible:underline group-focus-within:block group-hover:bg-fg group-hover:block group-hover:text-accent group-hover:text-bg group-hover:underline group-open:rotate-90 guard h-[0.5rem] h-[0.625rem] h-[0.875rem] h-[1.125rem] h-[1.25rem] h-[1.575rem] h-[10rem] h-[14px] h-[1em] h-[1lh] h-[2.5rem] h-[2rem] h-[3.5rem] h-[3rem] h-[90vh] h-[calc(100%-3rem)] h-[calc(100vh-3.5rem)] h-dvh h-full h-icon-lg h-icon-md h-icon-sm h-icon-xs h1 h1s h2 h22013h4 h2s h3 h4 h5 h6 half hand-copied hand-editable handle handled handler handlers happens hard-loaded hardcoded has hash-link have head head-links head-scripts header header- header-call:end header-call:start header-right heading heading-h2 heading-h3 heading-h4 heading-rule headings height here hex hidden hide hierarchical highlight history home horizontal host hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)] hover:bg-accent-hover hover:bg-accent/10 hover:bg-danger/10 hover:bg-surface hover:border-accent hover:border-accent-hover hover:border-fg hover:text-accent hover:text-accent-hover hover:text-fg hover:underline hr href hrefs hsp hsp-2xl hsp-2xs hsp-lg hsp-md hsp-sm hsp-xl hsp-xs html i i18n/theme. i2 i3 i4 ico icon icon-lg icon-md icon-sm icon-xs identical idle idx if iframe image image-enlarge image-overlay-inset image/avif image/gif image/jpeg image/png image/webp image/x-icon img implementation import important important-allowlist imports in inactive inbox includes including incomplete independently index index2026 indirectly info inherit inherited initial initialised injected inline inline-block inline-flex inner input input-clear ins inserted-after-color-mode inserted-after-color-scheme inserted-after-site-name inserted-first insertion inset-0 inside inside-only install installation installed instance instanceof instead instructions intended intent intercept internal interpolation into invalid invalidated inverse inversion invoke is island island-root issues it italic item item- items items-baseline items-center items-end items-start iteration its itself javascript js jsx jumps just justify-between justify-center justify-end justify-start katex kbd keep keeping keeps kept key keyboard keyboard-shortcut keydown keys keystroke keyword keywords khroma known-token-names kopieren label landing lands language-switcher last:border-b-0 later latest launch layout lazy leading-normal leading-relaxed leading-snug leading-tight leaf- leak leaves leaving left left-0 left:calc legend legitimate length lets letter-spacing lg lg:block lg:border lg:border-fg lg:border-solid lg:flex lg:flex-col lg:flex-row lg:gap-hsp-xl lg:grid-cols-3 lg:grid-cols-[repeat(auto-fit,minmax(12rem,1fr))] lg:h-[90vh] lg:hidden lg:justify-start lg:m-auto lg:max-h-[90vh] lg:max-w-[52.5rem] lg:ml-[var(--zd-sidebar-w)] lg:pt-vsp-2xl lg:px-hsp-2xl lg:py-vsp-2xl lg:text-left lg:w-[90vw] lg:w-[clamp(16rem,25%,22rem)] li li2 library license light light/dark like likely line line-height line/statement linger link link- links list list-disc list-none listener lists literal literally literals live lives llms llms-txt load loaded loading local local-1 local-2 local-3 locale locales log logo longer longest-match look loses lostpointercapture lower luminance m m-0 m-auto m21 m6 machinery main major make malformed malformed-markup malicious manually maps mark markdown marks match matching math math-display math-inline max max-h-[85vh] max-h-[90vh] max-h-full max-h-none max-w-[64rem] max-w-[85%] max-w-[85vw] max-w-[90vw] max-w-[calc(100vw-2rem)] max-w-[clamp(50rem,75vw,90rem)] max-w-full max-w-none max-w-sm max-width may mb-0 mb-vsp-2xs mb-vsp-lg mb-vsp-md mb-vsp-sm mb-vsp-xl mb-vsp-xs md mdx means measured measurement measures measuring mechanism menu mermaid message messages meta meta-knob meta-schema metadata migration min-h-0 min-h-[60vh] min-h-[calc(100vh-3.5rem)] min-h-screen min-w-0 min-w-[10rem] min-w-[3rem] min-w-[8rem] minifier minor mirror mirroring mirrors missing mit ml-auto ml-hsp-2xl ml-hsp-lg ml-hsp-sm ml-hsp-xl mobile mod modal modal-backdrop mode modify module moment monospace more most mount mounted mouseenter mouseleave move mr-hsp-sm ms mt-0 mt-vsp-2xl mt-vsp-2xs mt-vsp-3xs mt-vsp-lg mt-vsp-md mt-vsp-sm mt-vsp-xl must mutates mutation mutations muted mx-auto my-vsp-lg my-vsp-md n name named names native natural nav nav-active nav-card- nav/doc navigating navigation navigations near needed needs neither nested neutral never new newly-swapped next nicht no no-color-scheme no-data-theme-selector no-enlarge no-op no-repeat no-underline noch node node:buffer node:fs node:fs/promises node:module node:path node:url nodes nofollow noindex non-draggable non-empty non-light-dark non-literal non-persisted none noopener noreferrer normal noscript not notable note notes now null number numeric object object-contain observe observer occurred of off offered offsets ofl-required og:description og:image og:image:alt og:image:height og:image:width og:title og:type og:url oklch ol old older omit on once one only onto opacity-60 open open/close option or order original other others otherwise out outgoing outline-none over overflow overflow-auto overflow-hidden overflow-x-auto overflow-y-auto overflow-y:auto override overrides overscroll-contain overwrite own owned p p-0 p-hsp-2xs p-hsp-lg p-hsp-md p-hsp-sm pack pack-scoped package package-default package-injected package-owned packages packs padding page page-loading page-loading-overlay page-loading-spinner page-navigate-end page-title page-wide pages pages/. paint paint-and-read palette pan panel panels paren-balance-aware parent parse parsed parser parses part pass passed passes patch path paths pattern payload payload-budget pb-[50vh] pb-vsp-md pb-vsp-xl pb-vsp-xs pending per per-block per-link permanently persisted persistence pi pick picked picks picocolors pins pipelines pl-[1.25rem] pl-hsp-lg pl-hsp-sm pl-hsp-xl place placeholder placeholder:text-muted plain plural plus png16 png32 pnpm point pointer pointer-events-none pointercancel pointerdown pointermove pointerup polite polygon polyline popover populates port position position:fixed pr-[4px] pr-hsp-lg pr-hsp-md pr-hsp-sm pr-hsp-xl pre pre-lowercased preact preact/compat preact/hooks preact/jsx-runtime preconnect preference prefix preload pres present preserving preview preview-swatch-color previews2026 previously primary print private produce produced produces producing production project project-owned project-root-relative properties property props prose provided proxy pt-[0.15rem] pt-[2px] pt-vsp-3xs pt-vsp-md pt-vsp-sm pt-vsp-xl pt-vsp-xs ptag- public purely puts px px-hsp-2xl px-hsp-2xs px-hsp-lg px-hsp-md px-hsp-sm px-hsp-xl px-hsp-xs py-0 py-[2px] py-[4px] py-[calc(var(--spacing-vsp-xs)+0.15rem)] py-hsp-2xs py-hsp-3xs py-hsp-sm py-hsp-xs py-vsp-2xs py-vsp-3xs py-vsp-lg py-vsp-md py-vsp-sm py-vsp-xl py-vsp-xs q query question quick r radius radius-full radius-lg rail ramp range rather raw re-encode/decode re-exports re-querying re-render re-renders re-run re-running re-runs re-selects re-syncs reach reached reaches read reader reader-facing reading readings reads/rewrites real real-value received receives recorded recovers rect redefine redistribution ref- reference referenced references refetch refreshes regardless regenerate regenerates regex registry reinit reinits rejected rel relative release released reload relying rem remapped remembered remove removed render rendered renderer renderers renders reorder repaint repair repeated repeating replace replaced replacement replaces repopulate report repository required requires reserved resize resize-x resolve resolved resolves responded response restore restores restyle result result-click results results-area retry return returns reveal revision revisions rewire rewrite rewrites right right- right-0 right-hsp-lg ring-2 ring-accent risking ro robots role roles root rotate-180 rotate-90 round round-trip rounded rounded-[0.75rem] rounded-bl-[0.25rem] rounded-bl-[1rem] rounded-bl-lg rounded-br-[0.25rem] rounded-br-[1rem] rounded-full rounded-lg rounded-md rounded-t-[1rem] rounds route router routes routes-src rule run running runs runtime s safe safely safer same same-locale samp sans sans-serif scale scanned scanning scheme scoped scoping score scored script script- script-eval script-evaluation script-injection scripts scroll scrollbar scrolled scrollend scrolling seam search search-index section section- see seed sehen select select-none selection-bg selection-fg selector self self-contained self-hosted self-start semantic semibold semver sentinel separator serialised serialize server server-rendered session set sets setting settles setup shadow shadow-[0_1px_3px_color-mix(in_srgb,var(--color-fg)_8%,transparent)] shadow-lg shadow-md shadowed shape share shared sharing shell ship shipped ships short shortcut should show shown shrink-0 sidebar sidebar- sidebar-toggle-island sidebar-tree-island sidebar-w signal silently similarity simple since single single-line single-object-literal singular site site-search site-tree-nav-island sitemap- sites size skill skills skipped skipping skips slash slot slug slug-dir-parity slugs sm:border sm:border-muted sm:flex-row sm:grid-cols-2 sm:h-auto sm:items-center sm:justify-between sm:max-h-[80vh] sm:max-w-[52rem] sm:mx-auto sm:my-[10vh] sm:rounded-lg small smooth snapping snapshot snapshots so soft soft-nav solid some somehow source sources space-y-vsp-2xs spacing spacing-0 spacing-px span spans spec specifiers specify spelling splitter spread spurious square sr-only src stable stack stale standalone start state state- state:state- statement status stay staying sticky still stock stop stops stored straddles stray strict string strings strip stripe strips stroke-linecap stroke-linejoin stroke-width strong stronger stub-rendered style style-attribute styled styles stylesheet sub subagents subsequent substitute substitution subtracting success successful summary sup supply supported surface surfaces survives svg swap swapped swaps switcher switching synchronous synchronously syntactically syntax t tab tab-item tab-panel tabindex table tablist tabpanel tabs tabs-container tabs-content tabs-nav tag tag- tag-item- tagged tags tags:audit take tbody td temp-element template temporary temporary-element terminal terms test-results tested text text-accent text-bg text-body text-caption text-center text-chat-assistant-text text-chat-user-text text-code-fg text-danger text-decoration text-display text-fg text-fg/60 text-heading text-info text-left text-micro text-muted text-muted/50 text-right text-scale-2xl text-scale-2xs text-scale-lg text-scale-md text-scale-sm text-scale-xl text-scale-xs text-small text-title text-warning text/css text/plain textarea tfoot th than that the thead their them theme theme-color theme-pack theme-pack-changed theme-packs theme-packs/index.json theme-toggle theme/token then there these they this those though three threw through throw throws tighten time tip title to toast toc toggle toggle- toggle-ai-chat toggle-design-token-panel toggles toggling token tokens tolerates too toolbar tooltip top top-0 top-[3.5rem] top-full top-level total touches tr tracked tracking-wide tracking-wider trade-off trailing transition transition-[background,color,border-color] transition-[left,color] transition-[right,color] transition-colors transition-transform translate-x-0 translated translations transparent treats tree tree-child- tree-item- tree-top- trigger trigger:ai-chat trigger:design-token-panel triggers true truncate truncated try ts tsx turn twitter:card twitter:creator twitter:description twitter:image twitter:site twitter:title two type typeface typeof typography u ul umschalten unavailable unbalanced unchanged und undefined under underline underlines understand unit-tested unknown unlike unlisted unmaintained unobserve unreadable unrelated unreleased unresolvable unresolved unset unterminated until unusable unwrapped up up-to-date update uppercase use used useful user uses usual utf-8 utf8 utilities utility v v2 val value value-reader values var variable variant verbatim version version- version-menu version-switcher versions vertical via video viewing viewport viewports virtual:zudo-doc-chrome-bindings virtual:zudo-doc-design-token-panel-config virtual:zudo-doc-route-context visibility visible vocabulary void von vsp vsp-2xl vsp-2xs vsp-3xs vsp-lg vsp-md vsp-sm vsp-xl vsp-xs w w-1/2 w-[0.5rem] w-[0.625rem] w-[0.875rem] w-[1.125rem] w-[1.575rem] w-[1.5rem] w-[1.75rem] w-[12rem] w-[14px] w-[16px] w-[16rem] w-[18px] w-[1em] w-[2.5rem] w-[280px] w-[2rem] w-[320px] w-[360px] w-[90vw] w-[calc(100vw-2rem)] w-[var(--zd-sidebar-w)] w-dvw w-full w-icon-lg w-icon-md w-icon-sm w-icon-xs walk walks want warn warning was watching way wbr wbr- we website weight went were what when where whereas whether which while whitespace-nowrap whitespace-pre whole wide wide-gamut wider-than-scrollbar width will window wins wird wired with without word wordmark working works worktrees would wrap wrapped wrapper wrappers wrapping wraps written wrong wrote wurde x xl:flex xl:hidden y-scrollbar yet yields you your z-dropdown z-local-1 z-modal z-modal-backdrop z-popover z-sidebar z-toolbar zd-content zd-desktop-sidebar-toggle zd-desktop-toc-toggle zd-doc-content-band zd-enlarge-btn zd-enlarge-dialog zd-enlarge-dialog-close zd-enlargeable zd-html-preview-code zd-mermaid-dialog zd-mermaid-enlargeable zd-mermaid-tool-btn zd-mermaid-toolbar zd-mermaid-transform zd-mermaid-viewport zd-sidebar-content-wrapper zd-sidebar-open zd-theme-pack-dialog-title zd-toc-col zdtp zfb zfb:after-swap zfb:before-preparation zfb:before-swap zod zoom zudo-design-token-panel zudo-design-tokens/v3 zudo-doc zudo-doc-code-wrap zudo-doc-design-token-panel-modal zudo-doc-design-tokens zudo-doc-sidebar-visible zudo-doc-sidebar-width zudo-doc-theme zudo-doc-theme-pack zudo-doc-toc-visible zudo-doc-tweak zum");
2
+ @source inline("-domtweaker-enabled -elpath-enabled -link -mb-px -ml-hsp-sm -noscript -open -state -translate-x-full 2xl:w-[24px] [&::-webkit-details-marker]:hidden [&_a]:text-accent [&_a]:underline [&_nav]:mb-0 [data-admonition] [data-kbd-shortcut] [data-switcher-launcher] [doc-history-meta] [doc-history] [doc-layout] [llms-txt] [zudo-doc] a a2 abbr about above absent absolute accent accent- accent:accent- access across activated active actual actually added admonition admonition- admonition-body admonition-title admonition/callout after after-breadcrumb after-content after-navigate after-sidebar after-title against agent agents ai-chat ai-chat-md ai-chat-trigger alert align-top all allow-same-origin allow-scripts allowed alone already already-executed already-multiline already-picked also always an anchor and and/or animate-pulse animate-spin announce ansehen antialiased any anywhere anzeigen app appear application/json application/xml applied applies apply applying approach are area arg argument aria-atomic aria-busy aria-controls aria-current aria-disabled aria-expanded aria-haspopup aria-hidden aria-label aria-labelledby aria-live aria-orientation aria-pressed aria-selected aria-valuemax aria-valuemin aria-valuenow arm arms arrows article as asc aside aspect-[1200/630] aspect-square asset- assets assistant async at at-rule attach attribute attributes auf authored auto auto-logo-mask autogenerated availability available avoid await away b back backdrop:bg-bg/30 backdrop:bg-bg/80 backdrop:bg-overlay/60 backdrop:z-modal-backdrop background background-color backtick backticks baked banner bare base base- base64 base:base- based batch be bearbeiten because becomes been before below best best-effort between bg bg-[#fff] bg-accent bg-bg bg-chat-assistant-bg bg-chat-user-bg bg-code-bg bg-fg bg-info/10 bg-info/5 bg-muted bg-overlay/30 bg-surface bg-surface/50 bg-transparent bg-warning/10 bg-warning/5 bi bigint bin binaries bind binding blank blanks block blockquote blocks blur body body-end-components body-end-scripts bold boolean bootstrap border border-accent border-b border-b-2 border-b-[5px] border-bg/30 border-collapse border-danger border-dashed border-fg border-image border-info/30 border-l border-l-0 border-l-[3px] border-left-width border-muted border-none border-r border-r-0 border-radius border-solid border-t border-t-[2px] border-t-[3px] border-transparent border-warning/30 border-width border-y both bottom-hsp-lg bottom-vsp-xl boundaries box box-border br brackets brand breadcrumb:end breadcrumb:start break-words brief brown browser browser-tab browsers browses btn budget bug build builder built built-in bundler but button buttons by bypassed byte-identical bytes cache cached call callable called caller calls can cancellation candidate cannot canonical canvas caption captured captures card card-grid carry cases cat-nav- catch category caught caution center center/contain ch chains change changed changelog changelogs changes check child children choose chrome chrome-font ci circle cite class class-less class-mode claude claude-agents claude-commands claude-md claude-skills cleaned cleanly clear clearing click client client-router client-side clip clobber clobbering close closed closes closing closure code code-block-sr-announce code-group code-group-panel col col-resize colgroup collapses collapsible collision color color-scheme color-scheme-changed color-scheme-provider color-tweak colorization colors column comma command commands commas comment commercial commercial-font-denylist commit compare component component:github-link component:language-switcher component:search component:theme-toggle component:version-switcher composes composition compute computed concrete config configuration configurations configure configured confuse connect const construction consumer consumes contain container containers containing contains content content-admonition content-layer content-link content-type content-wrapper:end content-wrapper:start contents context contract control controller controls converts cookie-blocking copied copy core corners correct correctly corrupt could count covered covers crashes created cross-component crumb- cs css css-presence ctx cur current current-path/index.ts current-route currently cursor cursor-not-allowed cursor-pointer custom cycle danger dark data data-active data-admonition data-auto-logo data-base data-close-search data-current-locale data-default-locale data-doc-description data-doc-metainfo data-doc-pager data-doc-unavailable-versions data-find-active data-find-match data-footer data-group-id data-header data-header-logo data-header-nav data-header-right data-kbd-shortcut data-language-switcher data-loading-index data-mermaid-enlarge-ready data-mermaid-rendered data-mermaid-src data-nav-active data-nav-item data-nav-item-dropdown data-nav-more data-nav-more-menu data-nav-more-toggle data-no-results data-open-search data-pan-active data-processed data-props data-result-count-template data-search-count data-search-count-narrow data-search-dialog data-search-input data-search-placeholder data-search-results data-search-unavailable data-sidebar-hidden data-sidebar-resizer data-site-nav data-switcher-card data-switcher-launcher data-tab-btn data-tab-default data-tab-label data-tab-value data-tabs data-taglist-group data-testid data-theme data-theme-pack data-theme-pack-switcher data-theme/style data-toc-hidden data-trailing-slash data-unavailable-label data-variant data-version-banner data-version-latest data-version-menu data-version-rewire data-version-slug data-version-switcher data-version-toggle data-version-trigger-label data-zd-mobile-sidebar data-zd-mobile-toc data-zd-nosidebar data-zd-props-preserve data-zd-theme-pack-css data-zd-theme-pack-css-loading data-zd-theme-pack-loading data-zd-toc data-zd-wide data-zfb-island data-zfb-island-remount data-zfb-transition-persist dd decimal declaration declare declares decoration decoration-muted deepest-match default default-transition-duration defaults deferral deferred del delegated delete deliberately delimiter dependency depends depth der desc description design design-token design-token-panel design-token-trigger desktop desktop-sidebar desktop-sidebar-toggle desktop-sidebar-toggle-island desktop-toc-toggle destroys destructive detach detached details determine deterministic dev develop dfn diagram diagrams dialog did die dieser diff diff-line-added diff-line-content diff-line-empty diff-line-num diff-line-removed diff-row different dir directly directories directory disabled disabled:opacity-50 disabled:pointer-events-none disc display display:none dist distance distinct div dl do doc doc-card- doc-content-band doc-history doc-history-generate doc-history-panel doc-history-trigger doc-page doc-pager doc-prose doc-title docblock docs docs- docs-v- document document-level documentation documented does dog double-registration draft drag drawer drift drifts drop dropdown dropdown-parent dropdowns dt duplicate duration-150 duration-200 during dynamically e e2e each earlier early ease-in-out edge editing einer either eject ejectable ejectables ejected el element elements els else em embedded emit emitting empty empty/undefined en enable enabled end enhanced enhancement enlarged entire entirely entities entries entry entrypoint equal error escape escaped even eventually every everything-enabled exactly exceeds excerpt excludes exclusively existing exists exit expected explicit export extends extra f factories failed fall fallback fallbacks falling falls false family fast favicon feature fg field fields fieldset figcaption figure file fill fills finally find find-match find-match-active fire fires first first-paint fix fixed fixed-width fixtures flag flash flat flex flex-1 flex-col flex-wrap flip flipping flips flow flush-left focus focus-visible:border-accent focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 focus-visible:text-accent focus-visible:underline focus:border-accent focus:outline-none focus:text-accent focus:underline font font-bold font-face-parity font-family font-file-missing font-medium font-mono font-sans font-scale font-semibold font-size font-weight font-weight-bold font-weight-medium font-weight-normal font-weight-semibold font/woff2 fonts footer footer- for form format former found four-link fox frame free freeze fresh from frontmatter frontmatter-preview frozen frozen-script fs-extra full fully function further g gains gap-[0.3em] gap-[clamp(1.5rem,3vw,4rem)] gap-hsp-2xs gap-hsp-lg gap-hsp-md gap-hsp-sm gap-hsp-xl gap-hsp-xs gap-vsp-2xs gap-vsp-3xs gap-vsp-lg gap-vsp-md gap-vsp-xs gap-x-hsp-2xs gap-x-hsp-lg gap-x-hsp-md gap-x-hsp-sm gap-x-hsp-xs gap-y-vsp-2xs gap-y-vsp-lg gap-y-vsp-md gap-y-vsp-xs gaps gate geladen2026 generate generated generation genuine geometry get getting-started github github-dark github-link give go got grab gradient granular graph gray-matter grid grid-cols-1 grid-cols-2 group group-focus-visible:text-accent group-focus-visible:underline group-focus-within:block group-hover:bg-fg group-hover:block group-hover:text-accent group-hover:text-bg group-hover:underline group-open:rotate-90 guard h-[0.5rem] h-[0.625rem] h-[0.875rem] h-[1.125rem] h-[1.25rem] h-[1.575rem] h-[10rem] h-[14px] h-[1em] h-[1lh] h-[2.5rem] h-[2rem] h-[3.5rem] h-[3rem] h-[90vh] h-[calc(100%-3rem)] h-[calc(100vh-3.5rem)] h-dvh h-full h-icon-lg h-icon-md h-icon-sm h-icon-xs h1 h1s h2 h22013h4 h2s h3 h4 h5 h6 half hand-copied hand-editable handle handled handler handlers happens hard-loaded hardcoded has hash-link have head head-links head-scripts header header- header-call:end header-call:start header-right heading heading-h2 heading-h3 heading-h4 heading-rule headings height here hex hidden hide hierarchical highlight history home horizontal host hover:bg-[color-mix(in_srgb,var(--color-surface)_80%,var(--color-fg)_20%)] hover:bg-accent-hover hover:bg-accent/10 hover:bg-danger/10 hover:bg-surface hover:border-accent hover:border-accent-hover hover:border-fg hover:text-accent hover:text-accent-hover hover:text-fg hover:underline hr href hrefs hsp hsp-2xl hsp-2xs hsp-lg hsp-md hsp-sm hsp-xl hsp-xs html i i18n/theme. i2 i3 i4 ico icon icon-lg icon-md icon-sm icon-xs identical idle idx if iframe image image-enlarge image-overlay-inset image/avif image/gif image/jpeg image/png image/webp image/x-icon img implementation import important important-allowlist imports in inactive inbox includes including incomplete independently index index2026 indirectly info inherit inherited initial initialised injected inline inline-block inline-flex inner input input-clear ins inserted-after-color-mode inserted-after-color-scheme inserted-after-site-name inserted-first insertion inset-0 inside inside-only install installation installed instance instanceof instead instructions intended intent intercept internal interpolation into invalid invalidated inverse inversion invoke is island island-root issues it italic item item- items items-baseline items-center items-end items-start iteration its itself javascript js jsx jumps just justify-between justify-center justify-end justify-start katex kbd keep keeping keeps kept key keyboard keyboard-shortcut keydown keys keystroke keyword keywords khroma known-token-names kopieren label landing lands language-switcher last:border-b-0 later latest launch layout lazy leading-normal leading-relaxed leading-snug leading-tight leaf- leak leaves leaving left left-0 left:calc legend legitimate length lets letter-spacing lg lg:block lg:border lg:border-fg lg:border-solid lg:flex lg:flex-col lg:flex-row lg:gap-hsp-xl lg:grid-cols-3 lg:grid-cols-[repeat(auto-fit,minmax(12rem,1fr))] lg:h-[90vh] lg:hidden lg:justify-start lg:m-auto lg:max-h-[90vh] lg:max-w-[52.5rem] lg:ml-[var(--zd-sidebar-w)] lg:pt-vsp-2xl lg:px-hsp-2xl lg:py-vsp-2xl lg:text-left lg:w-[90vw] lg:w-[clamp(16rem,25%,22rem)] li li2 library license light light/dark like likely line line-height line/statement linger link link- links list list-disc list-none listener lists literal literally literals live lives llms llms-txt load loaded loading local local-1 local-2 local-3 locale locales log logo longer longest-match look loses lostpointercapture lower luminance m m-0 m-auto m21 m6 machinery main major make malformed malformed-markup malicious manually maps mark markdown marks match matching math math-display math-inline max max-h-[85vh] max-h-[90vh] max-h-full max-h-none max-w-[64rem] max-w-[85%] max-w-[85vw] max-w-[90vw] max-w-[calc(100vw-2rem)] max-w-[clamp(50rem,75vw,90rem)] max-w-full max-w-none max-w-sm max-width may mb-0 mb-vsp-2xs mb-vsp-lg mb-vsp-md mb-vsp-sm mb-vsp-xl mb-vsp-xs md mdx means measured measurement measures measuring mechanism menu mermaid message messages meta meta-knob meta-schema metadata migration min-h-0 min-h-[60vh] min-h-[calc(100vh-3.5rem)] min-h-screen min-w-0 min-w-[10rem] min-w-[3rem] min-w-[8rem] minifier minor mirror mirroring mirrors missing mit ml-auto ml-hsp-2xl ml-hsp-lg ml-hsp-sm ml-hsp-xl mobile mod modal modal-backdrop mode modify module moment monospace more most mount mounted mouseenter mouseleave move mr-hsp-sm ms mt-0 mt-vsp-2xl mt-vsp-2xs mt-vsp-3xs mt-vsp-lg mt-vsp-md mt-vsp-sm mt-vsp-xl must mutates mutation mutations muted mx-auto my-vsp-lg my-vsp-md n name named names native natural nav nav-active nav-card- nav/doc navigating navigation navigations near needed needs neither nested neutral never new newly-swapped next nicht no no-color-scheme no-data-theme-selector no-enlarge no-op no-repeat no-underline noch node node:buffer node:fs node:fs/promises node:module node:path node:url nodes nofollow noindex non-draggable non-empty non-light-dark non-literal non-persisted none noopener noreferrer normal noscript not notable note notes now null number numeric object object-contain observe observer occurred of off offered offsets ofl-required og:description og:image og:image:alt og:image:height og:image:width og:title og:type og:url oklch ol old older omit on once one only onto opacity-60 open open/close option or order original other others otherwise out outgoing outline-none over overflow overflow-auto overflow-hidden overflow-x-auto overflow-y-auto overflow-y:auto override overrides overscroll-contain overwrite own owned p p-0 p-hsp-2xs p-hsp-lg p-hsp-md p-hsp-sm pack pack-scoped package package-default package-injected package-owned packages packs padding page page-loading page-loading-overlay page-loading-spinner page-navigate-end page-title page-wide pages pages/. paint paint-and-read palette pan panel panels paren-balance-aware parent parse parsed parser parses part pass passed passes patch path paths pattern payload payload-budget pb-[50vh] pb-vsp-md pb-vsp-xl pb-vsp-xs pending per per-block per-link permanently persisted persistence pi pick picked picks picocolors pins pipelines pl-[1.25rem] pl-hsp-lg pl-hsp-sm pl-hsp-xl place placeholder placeholder:text-muted plain plural plus png16 png32 pnpm point pointer pointer-events-none pointercancel pointerdown pointermove pointerup polite polygon polyline popover populates port position position:fixed pr-[4px] pr-hsp-lg pr-hsp-md pr-hsp-sm pr-hsp-xl pre pre-lowercased preact preact/compat preact/hooks preact/jsx-runtime preconnect preference prefix preload pres present preserving preview preview-swatch-color previews2026 previously primary print private produce produced produces producing production project project-owned project-root-relative properties property props prose provided proxy pt-[0.15rem] pt-[2px] pt-vsp-3xs pt-vsp-md pt-vsp-sm pt-vsp-xl pt-vsp-xs ptag- public purely puts px px-hsp-2xl px-hsp-2xs px-hsp-lg px-hsp-md px-hsp-sm px-hsp-xl px-hsp-xs py-0 py-[2px] py-[4px] py-[calc(var(--spacing-vsp-xs)+0.15rem)] py-hsp-2xs py-hsp-3xs py-hsp-sm py-hsp-xs py-vsp-2xs py-vsp-3xs py-vsp-lg py-vsp-md py-vsp-sm py-vsp-xl py-vsp-xs q query question quick r radius radius-full radius-lg rail ramp range rather raw re-encode/decode re-exports re-querying re-render re-renders re-run re-running re-runs re-selects re-syncs reach reached reaches read reader reader-facing reading readings reads/rewrites real real-value received receives recorded recovers rect redefine redistribution ref- reference referenced references refetch refreshes regardless regenerate regenerates regex registry reinit reinits rejected rel relative release released reload relying rem remapped remembered remove removed render rendered renderer renderers renders reorder repaint repair repeated repeating replace replaced replacement replaces repopulate report repository required requires reserved resize resize-x resolve resolved resolves responded response restore restores restyle result result-click results results-area retry return returns reveal revision revisions rewire rewrite rewrites right right- right-0 right-hsp-lg ring-2 ring-accent risking ro robots role roles root rotate-180 rotate-90 round round-trip rounded rounded-[0.75rem] rounded-bl-[0.25rem] rounded-bl-[1rem] rounded-bl-lg rounded-br-[0.25rem] rounded-br-[1rem] rounded-full rounded-lg rounded-md rounded-t-[1rem] rounds route router routes routes-src rule run running runs runtime s safe safely safer same same-locale samp sans sans-serif scale scanned scanning scheme scoped scoping score scored script script- script-eval script-evaluation script-injection scripts scroll scrollbar scrolled scrollend scrolling seam search search-index section section- see seed sehen select select-none selection-bg selection-fg selector self self-contained self-hosted self-start semantic semibold semver sentinel separator serialised serialize server server-rendered session set sets setting settles setup shadow shadow-[0_1px_3px_color-mix(in_srgb,var(--color-fg)_8%,transparent)] shadow-lg shadow-md shadowed shape share shared sharing shell ship shipped ships short shortcut should show shown shrink-0 sidebar sidebar- sidebar-toggle-island sidebar-tree-island sidebar-w signal silently similarity simple since single single-line single-object-literal singular site site-search site-tree-nav-island sitemap- sites size skill skills skipped skipping skips slash slot slug slug-dir-parity slugs sm:border sm:border-muted sm:flex-row sm:grid-cols-2 sm:h-auto sm:items-center sm:justify-between sm:max-h-[80vh] sm:max-w-[52rem] sm:mx-auto sm:my-[10vh] sm:rounded-lg small smooth snapping snapshot snapshots so soft soft-nav solid some somehow source sources space-y-vsp-2xs spacing spacing-0 spacing-px span spans spec specifiers specify spelling splitter spread spurious square sr-only src stable stack stale standalone start state state- state:state- statement status stay staying sticky still stock stop stops stored straddles stray strict string strings strip stripe strips stroke-linecap stroke-linejoin stroke-width strong stronger stub-rendered style style-attribute styled styles stylesheet sub subagents subsequent substitute substitution subtracting success successful summary sup supply supported surface surfaces survives svg swap swapped swaps switcher switching synchronous synchronously syntactically syntax t tab tab-item tab-panel tabindex table tablist tabpanel tabs tabs-container tabs-content tabs-nav tag tag- tag-item- tagged tags tags:audit take tbody td temp-element template temporary temporary-element terminal terms test-results tested text text-accent text-bg text-body text-caption text-center text-chat-assistant-text text-chat-user-text text-code-fg text-danger text-decoration text-display text-fg text-fg/60 text-heading text-info text-left text-micro text-muted text-muted/50 text-right text-scale-2xl text-scale-2xs text-scale-lg text-scale-md text-scale-sm text-scale-xl text-scale-xs text-small text-title text-warning text/css text/plain textarea tfoot th than that the thead their them theme theme-color theme-pack theme-pack-changed theme-packs theme-packs/index.json theme-toggle theme/token then there these they this those though three threw through throw throws tighten time tip title to toast toc toggle toggle- toggle-ai-chat toggle-design-token-panel toggles toggling token tokens tolerates too toolbar tooltip top top-0 top-[3.5rem] top-full top-level total touches tr tracked tracking-wide tracking-wider trade-off trailing transition transition-[background,color,border-color] transition-[left,color] transition-[right,color] transition-colors transition-transform translate-x-0 translated translations transparent treats tree tree-child- tree-item- tree-top- trigger trigger:ai-chat trigger:design-token-panel triggers true truncate truncated try ts tsx turn twitter:card twitter:creator twitter:description twitter:image twitter:site twitter:title two type typeface typeof typography u ul umschalten unavailable unbalanced unchanged und undefined under underline underlines understand unit-tested unknown unlike unlisted unmaintained unobserve unreadable unrelated unreleased unresolvable unresolved unset unterminated until unusable unwrapped up up-to-date update uppercase use used useful user uses usual utf-8 utf8 utilities utility v v2 val value value-reader values var variable variant verbatim version version- version-menu version-switcher versions vertical via video viewing viewport viewports virtual:zudo-doc-chrome-bindings virtual:zudo-doc-design-token-panel-config virtual:zudo-doc-route-context visibility visible vocabulary void von vsp vsp-2xl vsp-2xs vsp-3xs vsp-lg vsp-md vsp-sm vsp-xl vsp-xs w w-1/2 w-[0.5rem] w-[0.625rem] w-[0.875rem] w-[1.125rem] w-[1.575rem] w-[1.5rem] w-[1.75rem] w-[12rem] w-[14px] w-[16px] w-[16rem] w-[18px] w-[1em] w-[2.5rem] w-[280px] w-[2rem] w-[320px] w-[360px] w-[90vw] w-[calc(100vw-2rem)] w-[var(--zd-sidebar-w)] w-dvw w-full w-icon-lg w-icon-md w-icon-sm w-icon-xs walk walks want warn warning was watching way wbr wbr- we website weight went were what when where whereas whether which while whitespace-nowrap whitespace-pre whole wide wide-gamut wider-than-scrollbar width will window wins wird wired with without word wordmark working works worktrees would wrap wrapped wrapper wrappers wrapping wraps written wrong wrote wurde x xl:flex xl:hidden y-scrollbar yet yields you your z-dropdown z-local-1 z-modal z-modal-backdrop z-popover z-sidebar z-toolbar zd-content zd-desktop-sidebar-toggle zd-desktop-toc-toggle zd-doc-content-band zd-enlarge-btn zd-enlarge-dialog zd-enlarge-dialog-close zd-enlargeable zd-html-preview-code zd-mermaid-dialog zd-mermaid-enlargeable zd-mermaid-tool-btn zd-mermaid-toolbar zd-mermaid-transform zd-mermaid-viewport zd-sidebar-content-wrapper zd-sidebar-open zd-theme-pack-dialog-title zd-toc-col zdtp zfb zfb:after-swap zfb:before-preparation zfb:before-swap zod zoom zudo-design-token-panel zudo-design-tokens/v3 zudo-doc zudo-doc-code-wrap zudo-doc-design-token-panel-modal zudo-doc-design-tokens zudo-doc-sidebar-visible zudo-doc-sidebar-width zudo-doc-theme zudo-doc-theme-pack zudo-doc-toc-visible zudo-doc-tweak zum");
@@ -2,6 +2,7 @@ import { BEFORE_SWAP_EVENT } from "./page-events.js";
2
2
  const PERSIST_ATTR = "data-zfb-transition-persist";
3
3
  const ISLAND_ATTR = "data-zfb-island";
4
4
  const PROPS_ATTR = "data-props";
5
+ const PROPS_PRESERVE_ATTR = "data-zd-props-preserve";
5
6
  const ISLAND_REMOUNT_ATTR = "data-zfb-island-remount";
6
7
  const DOCUMENT_NODE = 9;
7
8
  const installedControllers = /* @__PURE__ */ new WeakMap();
@@ -37,7 +38,7 @@ function refreshNestedIslandProps(liveDocument, incomingDocument) {
37
38
  if (!incomingRoot) continue;
38
39
  const incomingIslands = collectUniqueOwnedIslands(incomingRoot);
39
40
  if (incomingIslands.size === 0) continue;
40
- for (const [name, liveIsland] of collectUniqueOwnedIslands(liveRoot)) {
41
+ for (const [name, liveIsland] of collectUniqueOwnedIslands(liveRoot, true)) {
41
42
  const incomingIsland = incomingIslands.get(name);
42
43
  if (!incomingIsland) continue;
43
44
  applyProps(liveIsland, incomingIsland);
@@ -56,9 +57,14 @@ function collectRefreshableRoots(doc) {
56
57
  }
57
58
  return roots;
58
59
  }
59
- function collectUniqueOwnedIslands(root) {
60
+ function collectUniqueOwnedIslands(root, excludePreserved = false) {
60
61
  const owned = Array.from(root.querySelectorAll(`[${ISLAND_ATTR}]`)).filter(
61
- (island) => island.closest(`[${PERSIST_ATTR}]`) === root
62
+ (island) => {
63
+ if (island.closest(`[${PERSIST_ATTR}]`) !== root) return false;
64
+ if (!excludePreserved) return true;
65
+ const preserveBoundary = island.closest(`[${PROPS_PRESERVE_ATTR}]`);
66
+ return preserveBoundary === null || !root.contains(preserveBoundary);
67
+ }
62
68
  );
63
69
  return indexUniquely(owned, ISLAND_ATTR);
64
70
  }
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gen-nav-overflow-script.mjs
3
+ //
4
+ // Generates the git-committed `src/header/nav-overflow-generated-script.ts`
5
+ // build artifact (zudolab/zudo-doc#3534, epic #3533).
6
+ //
7
+ // WHY this exists (the load-bearing reason — recoverable only from the
8
+ // issue): the previous `src/header/nav-overflow-script.ts` built
9
+ // NAV_OVERFLOW_SCRIPT as a template literal evaluated at MODULE-EVALUATION
10
+ // TIME, embedding `pathMatchesNavPath`/`computeActiveNavPath` via
11
+ // `Function.prototype.toString()` on the LIVE imported bindings, plus a third
12
+ // live `CURRENT_PATH_SCRIPT_PRELUDE` interpolation added in 5.6.0
13
+ // (zudolab/zudo-doc#3502). Because the string was recomputed by executing
14
+ // code whose emit shape depends on the CONSUMER's own bundler, two
15
+ // renderings of the same logical script hashed differently (measured: zfb
16
+ // SSR 12392 bytes vs a consumer Vite build 12216 bytes) — so a consumer
17
+ // publishing a CSP inline-script hash could never reconcile it against this
18
+ // package's own build.
19
+ //
20
+ // This script freezes the whole embedding ONCE, at zudo-doc's own package
21
+ // build time, into a plain string literal — so nothing downstream ever
22
+ // reflects on a live function again, and the emitted bytes become
23
+ // consumer-bundler-independent.
24
+ //
25
+ // HOW (package build): reads `src/current-path/index.ts`,
26
+ // `src/header/nav-active.ts`, `src/header/nav-class-tokens.ts`, and
27
+ // `src/transitions/page-events.ts`
28
+ // SOURCE, strips TypeScript types deterministically via esbuild's
29
+ // `transformSync()` (same rationale as gen-search-widget-script.mjs — see
30
+ // that file's header comment for the full `ts.transpileModule()` vs esbuild
31
+ // history, zudolab/zudo-doc#3422 / #3430 — identical here: esbuild is only a
32
+ // package dependency (it is also used by the shipped ejected-header generator),
33
+ // and the effective floor tracks the root `pnpm.overrides` range), executes
34
+ // each transpiled CommonJS module in an isolated sandbox (empty
35
+ // `module`/`exports`; all four source files are import-free), then reads
36
+ // the REAL runtime values off each sandbox's `exports`:
37
+ // - `exports.CURRENT_PATH_SCRIPT_PRELUDE` — read as a VALUE, never
38
+ // reconstructed (it is itself a pre-built string, not a function).
39
+ // - `exports.pathMatchesNavPath.toString()` /
40
+ // `exports.computeActiveNavPath.toString()` — the exact same
41
+ // Function.prototype.toString() mechanism the old code used, just run
42
+ // once here instead of on every module evaluation downstream.
43
+ // `computeActiveNavPath` closes over `pathMatchesNavPath`, so both are
44
+ // extracted together (nav-active.ts:70-73 documents the closure).
45
+ // - `exports.NAV_TOP_ACTIVE` / … — the twelve class-token arrays from
46
+ // nav-class-tokens.ts, read as real array values. The three splice
47
+ // formatters (`clsArgs`/`clsLiteral`/`clsAppend`) that used to live in
48
+ // nav-overflow-script.ts move into this generator (below) since they now
49
+ // run once at generation time instead of at every module evaluation.
50
+ // - `exports.AFTER_NAVIGATE_EVENT` — the real event-name string, never
51
+ // hardcoded here.
52
+ //
53
+ // **Duplicates the gen-search-widget-script.mjs scaffolding on purpose — does
54
+ // NOT extract a shared lib.** Factoring the two generators together would
55
+ // require touching the search-widget generator, whose output is CSP-hash-pinned
56
+ // in two places (its own drift-guard test and any downstream consumer's
57
+ // published hash); that byte-preserving-refactor risk isn't worth coupling to
58
+ // this change. A shared lib is a possible follow-up, not this one
59
+ // (zudolab/zudo-doc#3533 epic body).
60
+ //
61
+ // Composes the full IIFE script string (the template logic moved out of
62
+ // nav-overflow-script.ts) and writes it, write-if-changed, to
63
+ // `src/header/nav-overflow-generated-script.ts` with a GENERATED banner.
64
+ //
65
+ // Like `search-widget-script/generated-script.ts`, this generated file IS
66
+ // tracked in git — a deliberate departure from the gitignored-build-artifact
67
+ // convention used by `routes-src/` / `virtual-modules.d.ts`. Its whole value
68
+ // is a frozen, reviewable snapshot that a plain `git diff` can catch drifting
69
+ // from its four source files. It stays internal like the source files it
70
+ // reads — NOT added to the package `exports` map or `files[]`.
71
+ //
72
+ // EJECTED HEADER MODE (zudolab/zudo-doc#3541): copy-eject-sources.mjs ships
73
+ // this same generator beside the ejected header files. In that location it
74
+ // reads project-owned nav-active.ts / nav-class-tokens.ts locally, while the
75
+ // current-path and page-event inputs come from the installed package's dist/
76
+ // modules. It resolves esbuild from that package's dependency graph, so a
77
+ // consumer runs the understandable, self-contained command printed by eject:
78
+ // `node ./src/components/zudo-doc/header/gen-nav-overflow-script.mjs`.
79
+ //
80
+ // `buildNavOverflowScript()` is exported so both this script's CLI entry
81
+ // point AND the vitest drift-guard test
82
+ // (`src/header/__tests__/nav-overflow-script.test.ts`) can call it: the test
83
+ // re-runs the REAL extraction (fresh transpile of the current source files)
84
+ // and asserts it byte-matches the frozen `NAV_OVERFLOW_SCRIPT` shipped in
85
+ // `nav-overflow-generated-script.ts` — catching the case where one of the
86
+ // four source files changed but the generated file was never regenerated.
87
+ //
88
+ // Runs BEFORE tsup (build / prepare / predev — see package.json) so the
89
+ // first compile always has `nav-overflow-generated-script.ts` on disk to
90
+ // import from `nav-overflow-script.ts`, AND is hooked into the tsup `--watch`
91
+ // `onSuccess` chain (tsup.config.ts) BEFORE `copy-eject-sources.mjs` (running
92
+ // it after would copy the previous literal into `eject/` on the first watch
93
+ // cycle); write-if-changed keeps that loop-free (an unchanged regeneration
94
+ // does not re-trigger tsup's watcher).
95
+ //
96
+ // The `pnpm check:nav-overflow-drift` guard (b4push step + pr-checks CI step,
97
+ // mirroring `check:search-widget-drift`) landed as sub-issue
98
+ // zudolab/zudo-doc#3535 — see scripts/check-nav-overflow-script-drift.sh.
99
+
100
+ import { readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
101
+ import { createRequire } from "node:module";
102
+ import { resolve, dirname } from "node:path";
103
+ import { fileURLToPath } from "node:url";
104
+
105
+ const __dirname = dirname(fileURLToPath(import.meta.url));
106
+
107
+ /** Find the installed package from an ejected header without assuming npm's
108
+ * node_modules layout. The symlinked package root is enough: createRequire()
109
+ * below resolves esbuild from the package's own dependency graph under npm,
110
+ * pnpm, and yarn installs. */
111
+ function findInstalledPackageRoot(startDirs) {
112
+ for (const startDir of startDirs) {
113
+ let dir = resolve(startDir);
114
+ while (true) {
115
+ const candidate = resolve(dir, "node_modules/@takazudo/zudo-doc");
116
+ if (existsSync(resolve(candidate, "package.json"))) return realpathSync(candidate);
117
+ const parent = dirname(dir);
118
+ if (parent === dir) break;
119
+ dir = parent;
120
+ }
121
+ }
122
+ throw new Error(
123
+ "[gen-nav-overflow-script] could not find node_modules/@takazudo/zudo-doc. " +
124
+ "Run this command from an installed zudo-doc project after `pnpm install`.",
125
+ );
126
+ }
127
+
128
+ /** Resolve the generator's four inputs and output in package-build or ejected mode. */
129
+ export function resolveGenerationContext() {
130
+ const packageRootCandidate = resolve(__dirname, "..");
131
+ const packageHeaderDir = resolve(packageRootCandidate, "src/header");
132
+ const isPackageGenerator = existsSync(resolve(packageHeaderDir, "nav-active.ts"));
133
+
134
+ if (isPackageGenerator) {
135
+ return {
136
+ kind: "package",
137
+ packageRoot: packageRootCandidate,
138
+ currentPathSource: resolve(packageRootCandidate, "src/current-path/index.ts"),
139
+ navActiveSource: resolve(packageHeaderDir, "nav-active.ts"),
140
+ navClassTokensSource: resolve(packageHeaderDir, "nav-class-tokens.ts"),
141
+ pageEventsSource: resolve(packageRootCandidate, "src/transitions/page-events.ts"),
142
+ outputPath: resolve(packageHeaderDir, "nav-overflow-generated-script.ts"),
143
+ };
144
+ }
145
+
146
+ // In an ejected payload this script sits beside the two project-owned
147
+ // customization inputs. The current-path prelude and page-event vocabulary
148
+ // intentionally stay package-owned and come from the installed compiled
149
+ // modules, so an ejected project does not fork framework lifecycle inputs.
150
+ if (
151
+ !existsSync(resolve(__dirname, "nav-active.ts")) ||
152
+ !existsSync(resolve(__dirname, "nav-class-tokens.ts"))
153
+ ) {
154
+ throw new Error(
155
+ "[gen-nav-overflow-script] expected nav-active.ts and nav-class-tokens.ts " +
156
+ `beside the ejected generator at ${__dirname}`,
157
+ );
158
+ }
159
+ const packageRoot = findInstalledPackageRoot([process.cwd(), __dirname]);
160
+ return {
161
+ kind: "ejected",
162
+ packageRoot,
163
+ currentPathSource: resolve(packageRoot, "dist/current-path/index.js"),
164
+ navActiveSource: resolve(__dirname, "nav-active.ts"),
165
+ navClassTokensSource: resolve(__dirname, "nav-class-tokens.ts"),
166
+ pageEventsSource: resolve(packageRoot, "dist/transitions/page-events.js"),
167
+ outputPath: resolve(__dirname, "nav-overflow-generated-script.ts"),
168
+ };
169
+ }
170
+
171
+ function loadTransformSync(packageRoot) {
172
+ try {
173
+ const requireFromPackage = createRequire(resolve(packageRoot, "package.json"));
174
+ return requireFromPackage("esbuild").transformSync;
175
+ } catch (err) {
176
+ throw new Error(
177
+ "[gen-nav-overflow-script] could not load the esbuild dependency shipped by " +
178
+ `@takazudo/zudo-doc: ${err.message}`,
179
+ );
180
+ }
181
+ }
182
+
183
+ // Explicit, stable esbuild options — identical rationale to
184
+ // gen-search-widget-script.mjs's TRANSFORM_OPTIONS (see that file): `format:
185
+ // "cjs"` so each source's `export`s become a CommonJS `module.exports` object
186
+ // we can read off the sandboxed `module` param, `target: "es2020"` so
187
+ // `var`-style function bodies pass through unchanged (no downlevel helpers),
188
+ // `platform: "neutral"` so esbuild injects no Node/browser global shims (all
189
+ // four source files are import-free — nothing to shim). Every minify knob is
190
+ // explicitly false: the acceptance criterion is a human-readable, byte-stable
191
+ // emit across runs, never a minified one.
192
+ const TRANSFORM_OPTIONS = {
193
+ loader: "ts",
194
+ format: "cjs",
195
+ target: "es2020",
196
+ platform: "neutral",
197
+ sourcemap: false,
198
+ minify: false,
199
+ minifyWhitespace: false,
200
+ minifyIdentifiers: false,
201
+ minifySyntax: false,
202
+ };
203
+
204
+ /** A source string carrying no CommonJS/ESM module scaffolding that could
205
+ * survive the transpile into the embedded browser script. Matches only the
206
+ * syntactic shapes (`require(...)`, `import(...)`, `module.exports`,
207
+ * `exports.x`), NOT the bare words — the extracted function text includes
208
+ * body comments, and a comment merely mentioning "import"/"export" must not
209
+ * fail the build. */
210
+ function assertNoModuleScaffolding(label, text) {
211
+ if (/\brequire\s*\(|\bimport\s*\(|\bmodule\.exports\b|\bexports\s*\./.test(text)) {
212
+ throw new Error(
213
+ `[gen-nav-overflow-script] ${label} leaked module scaffolding into the embedded script:\n${text}`,
214
+ );
215
+ }
216
+ }
217
+
218
+ /** Transpile a TS source file to CommonJS JS, type-stripped, via esbuild's `transformSync`. */
219
+ function transpile(sourcePath, transformSync) {
220
+ const source = readFileSync(sourcePath, "utf8");
221
+ let result;
222
+ try {
223
+ result = transformSync(source, {
224
+ ...TRANSFORM_OPTIONS,
225
+ loader: sourcePath.endsWith(".ts") ? "ts" : "js",
226
+ });
227
+ } catch (err) {
228
+ const messages = (err.errors ?? []).map((e) => e.text).join("; ") || err.message;
229
+ throw new Error(
230
+ `[gen-nav-overflow-script] failed to transpile ${sourcePath}: ${messages}`,
231
+ );
232
+ }
233
+ // Warnings are fatal on purpose: this file's output is embedded verbatim
234
+ // into a shipped browser script, so anything esbuild flags must be resolved
235
+ // in the source rather than silently carried through.
236
+ if (result.warnings.length > 0) {
237
+ const messages = result.warnings.map((w) => w.text).join("; ");
238
+ throw new Error(
239
+ `[gen-nav-overflow-script] esbuild reported warning(s) while transpiling ${sourcePath} (treated as fatal): ${messages}`,
240
+ );
241
+ }
242
+ return result.code;
243
+ }
244
+
245
+ /** Execute transpiled CommonJS source in an isolated sandbox and return its exports. */
246
+ function executeCommonJs(code, label) {
247
+ const moduleObj = { exports: {} };
248
+ const fn = new Function("module", "exports", code);
249
+ try {
250
+ fn(moduleObj, moduleObj.exports);
251
+ } catch (err) {
252
+ throw new Error(
253
+ `[gen-nav-overflow-script] failed to execute transpiled ${label}: ${err.message}`,
254
+ );
255
+ }
256
+ return moduleObj.exports;
257
+ }
258
+
259
+ /** Extract the real CURRENT_PATH_SCRIPT_PRELUDE value from current-path/index.ts. */
260
+ function extractCurrentPathPrelude(context, transformSync) {
261
+ const outputText = transpile(context.currentPathSource, transformSync);
262
+ const exportsObj = executeCommonJs(outputText, "current-path/index.ts");
263
+ const value = exportsObj.CURRENT_PATH_SCRIPT_PRELUDE;
264
+ if (typeof value !== "string" || !value) {
265
+ throw new Error(
266
+ "[gen-nav-overflow-script] CURRENT_PATH_SCRIPT_PRELUDE missing or not a string in current-path/index.ts",
267
+ );
268
+ }
269
+ assertNoModuleScaffolding("CURRENT_PATH_SCRIPT_PRELUDE", value);
270
+ return value;
271
+ }
272
+
273
+ /** Extract the real, unit-tested pathMatchesNavPath/computeActiveNavPath source text from nav-active.ts. */
274
+ function extractNavActiveFunctions(context, transformSync) {
275
+ const outputText = transpile(context.navActiveSource, transformSync);
276
+ const exportsObj = executeCommonJs(outputText, "nav-active.ts");
277
+ const { pathMatchesNavPath, computeActiveNavPath } = exportsObj;
278
+ if (typeof pathMatchesNavPath !== "function" || typeof computeActiveNavPath !== "function") {
279
+ throw new Error(
280
+ "[gen-nav-overflow-script] nav-active.ts did not export pathMatchesNavPath/computeActiveNavPath functions",
281
+ );
282
+ }
283
+ const pathMatchesNavPathSrc = pathMatchesNavPath.toString();
284
+ const computeActiveNavPathSrc = computeActiveNavPath.toString();
285
+ assertNoModuleScaffolding("pathMatchesNavPath", pathMatchesNavPathSrc);
286
+ assertNoModuleScaffolding("computeActiveNavPath", computeActiveNavPathSrc);
287
+ return { pathMatchesNavPathSrc, computeActiveNavPathSrc };
288
+ }
289
+
290
+ // The twelve class-token arrays nav-overflow-script.ts used to import
291
+ // directly from nav-class-tokens.ts (see that module's header comment for
292
+ // the SSR ↔ runtime lockstep rationale, zudolab/zudo-doc#3023).
293
+ const NAV_CLASS_TOKEN_NAMES = [
294
+ "NAV_TOP_ACTIVE",
295
+ "NAV_TOP_INACTIVE",
296
+ "NAV_CHEVRON_ACTIVE",
297
+ "NAV_CHEVRON_INACTIVE",
298
+ "NAV_CHILD_ACTIVE",
299
+ "NAV_CHILD_INACTIVE",
300
+ "NAV_MENU_PARENT",
301
+ "NAV_MENU_PARENT_ACTIVE_SUFFIX",
302
+ "NAV_MENU_PLAIN",
303
+ "NAV_MENU_PLAIN_ACTIVE_SUFFIX",
304
+ "NAV_MENU_CHILD_ACTIVE",
305
+ "NAV_MENU_CHILD_INACTIVE",
306
+ ];
307
+
308
+ /** Extract the twelve real class-token arrays from nav-class-tokens.ts. */
309
+ function extractNavClassTokens(context, transformSync) {
310
+ const outputText = transpile(context.navClassTokensSource, transformSync);
311
+ const exportsObj = executeCommonJs(outputText, "nav-class-tokens.ts");
312
+ const tokens = {};
313
+ for (const name of NAV_CLASS_TOKEN_NAMES) {
314
+ const value = exportsObj[name];
315
+ if (!Array.isArray(value) || !value.every((token) => typeof token === "string")) {
316
+ throw new Error(
317
+ `[gen-nav-overflow-script] nav-class-tokens.ts did not export ${name} as a string array`,
318
+ );
319
+ }
320
+ tokens[name] = value;
321
+ }
322
+ // Exhaustiveness: a token array exported by nav-class-tokens.ts (and thus
323
+ // available to header.tsx's SSR markup) but missing from
324
+ // NAV_CLASS_TOKEN_NAMES would be silently absent from the frozen script —
325
+ // the exact SSR ↔ runtime class drift the tokens module exists to prevent,
326
+ // and one no drift guard can see (the generated bytes legitimately don't
327
+ // change). Fail loudly instead.
328
+ const unconsumed = Object.keys(exportsObj).filter(
329
+ (name) => name.startsWith("NAV_") && !NAV_CLASS_TOKEN_NAMES.includes(name),
330
+ );
331
+ if (unconsumed.length > 0) {
332
+ throw new Error(
333
+ `[gen-nav-overflow-script] nav-class-tokens.ts exports token array(s) not embedded in the script: ${unconsumed.join(", ")}. Add them to NAV_CLASS_TOKEN_NAMES and splice them into the template in buildNavOverflowScript().`,
334
+ );
335
+ }
336
+ return tokens;
337
+ }
338
+
339
+ /** Extract the real AFTER_NAVIGATE_EVENT value from transitions/page-events.ts — never hardcoded. */
340
+ function extractAfterNavigateEvent(context, transformSync) {
341
+ const outputText = transpile(context.pageEventsSource, transformSync);
342
+ const exportsObj = executeCommonJs(outputText, "page-events.ts");
343
+ const value = exportsObj.AFTER_NAVIGATE_EVENT;
344
+ if (typeof value !== "string" || !value) {
345
+ throw new Error(
346
+ "[gen-nav-overflow-script] AFTER_NAVIGATE_EVENT missing or not a string in transitions/page-events.ts",
347
+ );
348
+ }
349
+ return value;
350
+ }
351
+
352
+ // The class lists spliced into the script below are the SSR ↔ runtime
353
+ // lockstep: they must match the strings header.tsx renders (nav-class-tokens.ts
354
+ // header comment, zudolab/zudo-doc#3023). Moved here from nav-overflow-script.ts
355
+ // (zudolab/zudo-doc#3534) — they now run once at generation time rather than on
356
+ // every module evaluation.
357
+
358
+ // -> `"bg-fg", "text-bg"` — argument list for a classList.add/remove(...) call.
359
+ const clsArgs = (tokens) => tokens.map((token) => JSON.stringify(token)).join(", ");
360
+
361
+ // -> `"bg-fg text-bg"` — a single class-string literal for `className = ...`.
362
+ const clsLiteral = (tokens) => JSON.stringify(tokens.join(" "));
363
+
364
+ // -> `" font-bold text-accent"` — leading-space append for `className += ...`.
365
+ const clsAppend = (tokens) => JSON.stringify(" " + tokens.join(" "));
366
+
367
+ /**
368
+ * Composes the full desktop-nav overflow controller IIFE script,
369
+ * string-for-string identical in structure to the old template-literal build
370
+ * in nav-overflow-script.ts, but with the four previously-live interpolations
371
+ * replaced by frozen values extracted above.
372
+ */
373
+ export function buildNavOverflowScript(context = resolveGenerationContext()) {
374
+ const transformSync = loadTransformSync(context.packageRoot);
375
+ const currentPathPrelude = extractCurrentPathPrelude(context, transformSync);
376
+ const { pathMatchesNavPathSrc, computeActiveNavPathSrc } = extractNavActiveFunctions(
377
+ context,
378
+ transformSync,
379
+ );
380
+ const {
381
+ NAV_TOP_ACTIVE,
382
+ NAV_TOP_INACTIVE,
383
+ NAV_CHEVRON_ACTIVE,
384
+ NAV_CHEVRON_INACTIVE,
385
+ NAV_CHILD_ACTIVE,
386
+ NAV_CHILD_INACTIVE,
387
+ NAV_MENU_PARENT,
388
+ NAV_MENU_PARENT_ACTIVE_SUFFIX,
389
+ NAV_MENU_PLAIN,
390
+ NAV_MENU_PLAIN_ACTIVE_SUFFIX,
391
+ NAV_MENU_CHILD_ACTIVE,
392
+ NAV_MENU_CHILD_INACTIVE,
393
+ } = extractNavClassTokens(context, transformSync);
394
+ const afterNavigateEventLiteral = JSON.stringify(
395
+ extractAfterNavigateEvent(context, transformSync),
396
+ );
397
+
398
+ return /* javascript */ `(function () {
399
+ var cleanupNavOverflow = null;
400
+
401
+ function trimSlashes(p) {
402
+ while (p.length > 1 && p.charAt(p.length - 1) === "/") p = p.slice(0, -1);
403
+ return p || "/";
404
+ }
405
+
406
+ function navPathname(a) {
407
+ try { return trimSlashes(new URL(a.href, location.href).pathname); }
408
+ catch (e) { return ""; }
409
+ }
410
+
411
+ // Explicit current-route override, embedded from current-path/index.ts so
412
+ // this script cannot drift from the three other read sites
413
+ // (zudolab/zudo-doc#3398, #3408).
414
+ ${currentPathPrelude}
415
+
416
+ // Shared matching core (zudolab/zudo-doc#3398): embedded verbatim from
417
+ // nav-active.ts so this script's longest-match walk cannot drift from the
418
+ // SSR header's own computeActiveNavPath call (header.tsx). computeActiveNavPath
419
+ // closes over pathMatchesNavPath, so both are embedded together.
420
+ var pathMatchesNavPath = ${pathMatchesNavPathSrc};
421
+ var computeActiveNavPath = ${computeActiveNavPathSrc};
422
+
423
+ // Recompute which header nav item is "active" from the CURRENT URL and
424
+ // repaint the highlight. SSR sets the active item on first paint, but the
425
+ // header is persisted across same-locale client-router swaps
426
+ // (data-zfb-transition-persist), so without this the highlight would stay
427
+ // frozen on the page where the header was first rendered. Mirrors the
428
+ // sidebar island's client-side approach (match the current path against
429
+ // each entry's href) and the SSR longest-match + dropdown-parent rules.
430
+ // URL-based: hrefs and the current path both carry the base + locale
431
+ // prefix, so they compare directly without stripping.
432
+ function applyActiveNav() {
433
+ var nav = document.querySelector("[data-header-nav]");
434
+ if (!nav) return;
435
+ var topItems = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
436
+ if (topItems.length === 0) return;
437
+
438
+ var cur = trimSlashes(readCurrentPath(CURRENT_PATH_DATASET_KEY));
439
+
440
+ // Build NavItemLike-shaped entries from the live DOM so the shared
441
+ // computeActiveNavPath can do the deepest-match walk — the same call
442
+ // shape the SSR header uses (matches computeActiveNavPath). A dropdown
443
+ // missing its own top-level anchor is skipped entirely (path "" would
444
+ // otherwise match every current path — pathMatchesNavPath treats "" as
445
+ // the root "/"), mirroring the parentLink guard used below for the same
446
+ // malformed-markup case.
447
+ var navItems = [];
448
+ topItems.forEach(function (it) {
449
+ var isDropdown = it.hasAttribute("data-nav-item-dropdown");
450
+ var topA = isDropdown ? it.querySelector(":scope > a") : it;
451
+ if (!topA) return;
452
+ var children = [];
453
+ if (isDropdown) {
454
+ it.querySelectorAll(":scope > div a").forEach(function (c) {
455
+ children.push({ path: navPathname(c) });
456
+ });
457
+ }
458
+ navItems.push({ path: navPathname(topA), children: children });
459
+ });
460
+
461
+ var activePath = computeActiveNavPath(navItems, cur) || "";
462
+
463
+ function setTopActive(a, active) {
464
+ if (!a) return;
465
+ if (active) {
466
+ a.classList.add(${clsArgs(NAV_TOP_ACTIVE)});
467
+ a.classList.remove(${clsArgs(NAV_TOP_INACTIVE)});
468
+ a.setAttribute("aria-current", "page");
469
+ } else {
470
+ a.classList.remove(${clsArgs(NAV_TOP_ACTIVE)});
471
+ a.classList.add(${clsArgs(NAV_TOP_INACTIVE)});
472
+ a.removeAttribute("aria-current");
473
+ }
474
+ }
475
+
476
+ topItems.forEach(function (it) {
477
+ var isDropdown = it.hasAttribute("data-nav-item-dropdown");
478
+ var topA = isDropdown ? it.querySelector(":scope > a") : it;
479
+ var topActive = false;
480
+
481
+ if (isDropdown) {
482
+ var parentMatch = !!topA && navPathname(topA) === activePath && activePath !== "";
483
+ var anyChild = false;
484
+ it.querySelectorAll(":scope > div a").forEach(function (c) {
485
+ var childActive = navPathname(c) === activePath && activePath !== "";
486
+ if (childActive) {
487
+ anyChild = true;
488
+ c.setAttribute("data-active", "");
489
+ c.classList.add(${clsArgs(NAV_CHILD_ACTIVE)});
490
+ c.classList.remove(${clsArgs(NAV_CHILD_INACTIVE)});
491
+ } else {
492
+ c.removeAttribute("data-active");
493
+ c.classList.remove(${clsArgs(NAV_CHILD_ACTIVE)});
494
+ c.classList.add(${clsArgs(NAV_CHILD_INACTIVE)});
495
+ }
496
+ });
497
+ topActive = parentMatch || anyChild;
498
+ var svg = topA ? topA.querySelector("svg") : null;
499
+ if (svg) {
500
+ if (topActive) { svg.classList.add(${clsArgs(NAV_CHEVRON_ACTIVE)}); svg.classList.remove(${clsArgs(NAV_CHEVRON_INACTIVE)}); }
501
+ else { svg.classList.add(${clsArgs(NAV_CHEVRON_INACTIVE)}); svg.classList.remove(${clsArgs(NAV_CHEVRON_ACTIVE)}); }
502
+ }
503
+ } else {
504
+ topActive = activePath !== "" && navPathname(topA) === activePath;
505
+ }
506
+
507
+ setTopActive(topA, topActive);
508
+ });
509
+ }
510
+
511
+ function initNavOverflow() {
512
+ if (cleanupNavOverflow) cleanupNavOverflow();
513
+
514
+ // Repaint the active highlight for the current URL before measuring /
515
+ // cloning, so the overflow "···" menu mirrors the correct active state.
516
+ applyActiveNav();
517
+
518
+ var nav = document.querySelector("[data-header-nav]");
519
+ var moreContainer = document.querySelector("[data-nav-more]");
520
+ var moreMenu = document.querySelector("[data-nav-more-menu]");
521
+ var moreToggle = document.querySelector("[data-nav-more-toggle]");
522
+ if (!nav || !moreContainer || !moreMenu || !moreToggle) return;
523
+
524
+ var items = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
525
+ if (items.length === 0) return;
526
+
527
+ var controller = new AbortController();
528
+
529
+ function update() {
530
+ items.forEach(function (el) { el.style.display = ""; });
531
+ moreContainer.style.display = "";
532
+ moreMenu.innerHTML = "";
533
+ moreMenu.classList.add("hidden");
534
+ moreToggle.setAttribute("aria-expanded", "false");
535
+
536
+ var itemWidths = items.map(function (el) { return el.offsetWidth; });
537
+ var moreWidth = moreContainer.offsetWidth;
538
+ var navGap = parseFloat(getComputedStyle(nav).columnGap) || 0;
539
+ var available = nav.clientWidth;
540
+
541
+ if (available <= 0) {
542
+ moreContainer.style.display = "none";
543
+ return;
544
+ }
545
+
546
+ var total = 0;
547
+ for (var i = 0; i < itemWidths.length; i++) {
548
+ total += itemWidths[i] + (i > 0 ? navGap : 0);
549
+ }
550
+
551
+ if (total <= available) {
552
+ moreContainer.style.display = "none";
553
+ return;
554
+ }
555
+
556
+ var used = 0;
557
+ var cutoffIndex = 0;
558
+
559
+ for (var i2 = 0; i2 < items.length; i2++) {
560
+ var w = itemWidths[i2] + (i2 > 0 ? navGap : 0);
561
+ if (used + w > available - moreWidth - navGap) break;
562
+ used += w;
563
+ cutoffIndex = i2 + 1;
564
+ }
565
+
566
+ for (var i3 = cutoffIndex; i3 < items.length; i3++) {
567
+ items[i3].style.display = "none";
568
+ }
569
+
570
+ for (var i4 = cutoffIndex; i4 < items.length; i4++) {
571
+ var el = items[i4];
572
+ var isDropdown = el.hasAttribute("data-nav-item-dropdown");
573
+
574
+ if (isDropdown) {
575
+ var parentLink = el.querySelector(":scope > a");
576
+ var childLinks = el.querySelectorAll(":scope > div a");
577
+ if (parentLink) {
578
+ var li = document.createElement("li");
579
+ var a = document.createElement("a");
580
+ a.href = parentLink.href;
581
+ var parentText = parentLink.textContent ? parentLink.textContent.trim().replace(/\\s+/g, " ") : "";
582
+ a.textContent = parentText;
583
+ a.className = ${clsLiteral(NAV_MENU_PARENT)};
584
+ if (parentLink.getAttribute("aria-current") === "page") {
585
+ a.className += ${clsAppend(NAV_MENU_PARENT_ACTIVE_SUFFIX)};
586
+ }
587
+ li.appendChild(a);
588
+ moreMenu.appendChild(li);
589
+ }
590
+ childLinks.forEach(function (child) {
591
+ var li = document.createElement("li");
592
+ var a = document.createElement("a");
593
+ a.href = child.href;
594
+ a.textContent = child.textContent ? child.textContent.trim() : "";
595
+ var isChildActive = child.hasAttribute("data-active");
596
+ a.className = isChildActive
597
+ ? ${clsLiteral(NAV_MENU_CHILD_ACTIVE)}
598
+ : ${clsLiteral(NAV_MENU_CHILD_INACTIVE)};
599
+ li.appendChild(a);
600
+ moreMenu.appendChild(li);
601
+ });
602
+ } else {
603
+ var anchor = el;
604
+ var li2 = document.createElement("li");
605
+ var a2 = document.createElement("a");
606
+ a2.href = anchor.href;
607
+ a2.textContent = anchor.textContent ? anchor.textContent.trim() : "";
608
+ a2.className = ${clsLiteral(NAV_MENU_PLAIN)};
609
+ if (anchor.getAttribute("aria-current") === "page") {
610
+ a2.className += ${clsAppend(NAV_MENU_PLAIN_ACTIVE_SUFFIX)};
611
+ }
612
+ li2.appendChild(a2);
613
+ moreMenu.appendChild(li2);
614
+ }
615
+ }
616
+ }
617
+
618
+ moreToggle.addEventListener("click", function () {
619
+ var isOpen = !moreMenu.classList.contains("hidden");
620
+ moreMenu.classList.toggle("hidden", isOpen);
621
+ moreToggle.setAttribute("aria-expanded", String(!isOpen));
622
+ }, { signal: controller.signal });
623
+
624
+ document.addEventListener("click", function (e) {
625
+ if (!moreContainer.contains(e.target)) {
626
+ moreMenu.classList.add("hidden");
627
+ moreToggle.setAttribute("aria-expanded", "false");
628
+ }
629
+ }, { signal: controller.signal });
630
+
631
+ document.addEventListener("keydown", function (e) {
632
+ if (e.key !== "Escape") return;
633
+ if (!moreMenu.classList.contains("hidden")) {
634
+ moreMenu.classList.add("hidden");
635
+ moreToggle.setAttribute("aria-expanded", "false");
636
+ moreToggle.focus();
637
+ return;
638
+ }
639
+ var active = document.activeElement;
640
+ var dropdown = active && active.closest ? active.closest("[data-nav-item-dropdown]") : null;
641
+ if (dropdown && active && active.blur) {
642
+ active.blur();
643
+ }
644
+ }, { signal: controller.signal });
645
+
646
+ var dropdowns = nav.querySelectorAll("[data-nav-item-dropdown]");
647
+ dropdowns.forEach(function (dd) {
648
+ var trigger = dd.querySelector(":scope > a");
649
+ if (!trigger) return;
650
+ function setExpanded(v) {
651
+ trigger.setAttribute("aria-expanded", String(v));
652
+ }
653
+ dd.addEventListener("mouseenter", function () { setExpanded(true); }, { signal: controller.signal });
654
+ dd.addEventListener("mouseleave", function () { setExpanded(false); }, { signal: controller.signal });
655
+ dd.addEventListener("focusin", function () { setExpanded(true); }, { signal: controller.signal });
656
+ dd.addEventListener("focusout", function (e) {
657
+ if (!dd.contains(e.relatedTarget)) {
658
+ setExpanded(false);
659
+ }
660
+ }, { signal: controller.signal });
661
+ });
662
+
663
+ var ro = new ResizeObserver(update);
664
+ ro.observe(nav);
665
+ controller.signal.addEventListener("abort", function () { ro.disconnect(); });
666
+
667
+ document.fonts.ready.then(update);
668
+
669
+ update();
670
+
671
+ cleanupNavOverflow = function () { controller.abort(); };
672
+ }
673
+
674
+ initNavOverflow();
675
+ document.addEventListener(${afterNavigateEventLiteral}, initNavOverflow);
676
+ })();`;
677
+ }
678
+
679
+ // ── CLI entry: write-if-changed ─────────────────────────────────────────────
680
+ // Realpath both sides: Node resolves the ESM entry's `import.meta.url` to its
681
+ // REAL path (default --preserve-symlinks=false) while `process.argv[1]` keeps
682
+ // whatever spelling the invoker typed, so under a symlinked checkout a plain
683
+ // `resolve()` comparison silently mismatches and the CLI becomes an exit-0
684
+ // no-op — leaving nav-overflow-generated-script.ts missing (hard tsup failure
685
+ // later) or, worse, stale.
686
+ const isMainModule = (() => {
687
+ if (!process.argv[1]) return false;
688
+ try {
689
+ // Realpath BOTH sides: under `--preserve-symlinks-main` (sometimes set
690
+ // via NODE_OPTIONS in pnpm/monorepo setups) `import.meta.url` keeps the
691
+ // symlinked spelling, so a one-sided realpath re-creates the silent
692
+ // exit-0 no-op this block exists to prevent.
693
+ return (
694
+ realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url))
695
+ );
696
+ } catch {
697
+ return false;
698
+ }
699
+ })();
700
+
701
+ function buildGeneratedModule(script, context) {
702
+ const banner = context.kind === "package"
703
+ ? `// GENERATED FILE — do not edit by hand.
704
+ // Produced by scripts/gen-nav-overflow-script.mjs (zudolab/zudo-doc#3534,
705
+ // epic #3533) from src/current-path/index.ts (CURRENT_PATH_SCRIPT_PRELUDE),
706
+ // src/header/nav-active.ts (pathMatchesNavPath/computeActiveNavPath,
707
+ // type-stripped), src/header/nav-class-tokens.ts (the twelve class-token
708
+ // arrays), and src/transitions/page-events.ts (AFTER_NAVIGATE_EVENT).
709
+ // Re-run \`pnpm --filter @takazudo/zudo-doc gen:nav-overflow-script\` (or any
710
+ // build/dev entry point, which already runs it) to regenerate after editing
711
+ // any of those source files.
712
+ //
713
+ // This file is committed to git (mirrors search-widget-script/generated-script.ts,
714
+ // zudolab/zudo-doc#3421 / #3431) — a deliberate departure from this repo's
715
+ // usual gitignored-generated-file convention (routes-src/, virtual-modules.d.ts).
716
+ // Regenerate AND commit the result after editing any of the four source files.
717
+ `
718
+ : `// GENERATED FILE — do not edit by hand.
719
+ // Produced by the ejected header's gen-nav-overflow-script.mjs from the local
720
+ // nav-active.ts and nav-class-tokens.ts customization inputs plus the installed
721
+ // @takazudo/zudo-doc current-path and page-event inputs.
722
+ // Re-run \`node ./src/components/zudo-doc/header/gen-nav-overflow-script.mjs\`
723
+ // after editing either local input, then commit this file with your customization.
724
+ // The script value remains frozen so its CSP bytes do not depend on the
725
+ // consumer bundler.
726
+ `;
727
+
728
+ return `${banner}
729
+ /** Returns the frozen desktop-nav overflow controller IIFE script. NOTE: the
730
+ * vitest drift guard imports buildNavOverflowScript from
731
+ * scripts/gen-nav-overflow-script.mjs (a fresh re-generation) — NEVER from
732
+ * this module: comparing NAV_OVERFLOW_SCRIPT below against this same file's
733
+ * function would be a vacuous self-comparison. */
734
+ export function buildNavOverflowScript(): string {
735
+ return ${JSON.stringify(script)};
736
+ }
737
+
738
+ /** Client-side script string for the desktop header nav overflow controller.
739
+ * See the module header of this generator for the embedding contract; see
740
+ * current-path/index.ts / header/nav-active.ts / header/nav-class-tokens.ts /
741
+ * transitions/page-events.ts for the frozen sources. */
742
+ export const NAV_OVERFLOW_SCRIPT: string = buildNavOverflowScript();
743
+ `;
744
+ }
745
+
746
+ /** Regenerate the committed package literal or the local ejected literal. */
747
+ export function generateNavOverflowScript(context = resolveGenerationContext()) {
748
+ const script = buildNavOverflowScript(context);
749
+ const output = buildGeneratedModule(script, context);
750
+
751
+ const existing = existsSync(context.outputPath)
752
+ ? readFileSync(context.outputPath, "utf8")
753
+ : null;
754
+
755
+ if (existing === output) {
756
+ process.stdout.write(
757
+ `[gen-nav-overflow-script] ${context.outputPath} unchanged, skip write\n`,
758
+ );
759
+ } else {
760
+ writeFileSync(context.outputPath, output, "utf8");
761
+ process.stdout.write(
762
+ `[gen-nav-overflow-script] ${context.outputPath} written\n`,
763
+ );
764
+ }
765
+
766
+ return { output, outputPath: context.outputPath, changed: existing !== output };
767
+ }
768
+
769
+ if (isMainModule) {
770
+ generateNavOverflowScript();
771
+ }
@@ -319,6 +319,8 @@ export function Header(props: HeaderProps): JSX.Element {
319
319
  // island module): on BEFORE_SWAP_EVENT it copies the incoming
320
320
  // document's data-props onto the live nested islands, and
321
321
  // mountNewIslands re-reads the attribute at mount time (#3530)
322
+ // Host islands can opt out with data-zd-props-preserve on the live
323
+ // island or an ancestor inside this persisted root (#3555).
322
324
  // - Header nav + aria-current: NAV_OVERFLOW_SCRIPT re-runs on
323
325
  // AFTER_NAVIGATE_EVENT (frozen by zudolab/zudo-doc#3534 — the
324
326
  // `addEventListener(${afterNavigateEventLiteral}, initNavOverflow)`
@@ -46,12 +46,15 @@
46
46
  // `src/header/__tests__/nav-overflow-script.test.ts` proves the committed
47
47
  // literal still matches a fresh regeneration.
48
48
  //
49
- // EJECTED COPIES (`zudo-doc eject header`): the generator is NOT shipped, so
50
- // in an ejected tree the re-exported literal is permanently frozen — editing
51
- // the ejected `./nav-class-tokens.ts` or `./nav-active.ts` changes the SSR
52
- // markup (header.tsx imports them live) but NOT this client script, breaking
53
- // the SSR ↔ runtime class lockstep those files exist to guarantee. To change
54
- // the client script in an ejected copy, edit the literal in
55
- // `./nav-overflow-generated-script.ts` directly (it is plain JS in a string)
56
- // and keep it in step with your token edits by hand.
49
+ // EJECTED COPIES (`zudo-doc eject header`): the payload ships
50
+ // `./gen-nav-overflow-script.mjs`. After editing the local `./nav-active.ts`
51
+ // or `./nav-class-tokens.ts`, run:
52
+ //
53
+ // node ./src/components/zudo-doc/header/gen-nav-overflow-script.mjs
54
+ //
55
+ // The generator reads those two local customization inputs and the installed
56
+ // package's current-path/page-event inputs, then rewrites the local
57
+ // `./nav-overflow-generated-script.ts`. Commit that generated file with the
58
+ // edits. Do not edit its string literal by hand: regeneration preserves the
59
+ // frozen, consumer-bundler-independent bytes required for stable CSP hashes.
57
60
  export { NAV_OVERFLOW_SCRIPT } from "./nav-overflow-generated-script.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "5.7.0",
3
+ "version": "5.8.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",
@@ -617,7 +617,7 @@
617
617
  "CHANGELOG.md"
618
618
  ],
619
619
  "peerDependencies": {
620
- "@takazudo/zdtp": "^0.4.11",
620
+ "@takazudo/zdtp": "^0.4.12",
621
621
  "@takazudo/zfb": "^2.7.1",
622
622
  "@takazudo/zfb-md-wasm": "^2.7.1",
623
623
  "@takazudo/zfb-runtime": "^2.7.1",
@@ -646,6 +646,7 @@
646
646
  },
647
647
  "dependencies": {
648
648
  "@inquirer/prompts": "^8.4.2",
649
+ "esbuild": ">=0.28.1",
649
650
  "fs-extra": "^11.3.0",
650
651
  "gray-matter": "^4.0.3",
651
652
  "minimist": "^1.2.8",
@@ -663,7 +664,6 @@
663
664
  "@types/node": "^25.3.5",
664
665
  "@types/pluralize": "^0.0.33",
665
666
  "@types/string-similarity": "^4.0.2",
666
- "esbuild": ">=0.28.1",
667
667
  "happy-dom": "^20.10.6",
668
668
  "npm-run-all2": "^7.0.2",
669
669
  "preact": "^10.29.1",
@@ -672,7 +672,7 @@
672
672
  "typescript": "^5.0.0",
673
673
  "vitest": "^4.1.0",
674
674
  "zod": "^4.3.6",
675
- "@takazudo/zudo-doc-history-server": "5.7.0"
675
+ "@takazudo/zudo-doc-history-server": "5.8.0"
676
676
  },
677
677
  "scripts": {
678
678
  "gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",