@takazudo/zudo-doc 5.5.3 → 5.6.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +39 -0
  3. package/dist/chrome/derive.d.ts +46 -0
  4. package/dist/chrome/derive.js +6 -2
  5. package/dist/config-assertions/index.d.ts +31 -0
  6. package/dist/config-assertions/index.js +24 -0
  7. package/dist/config.d.ts +23 -2
  8. package/dist/config.js +3 -0
  9. package/dist/current-path/index.d.ts +27 -0
  10. package/dist/current-path/index.js +11 -0
  11. package/dist/design-token-panel-bootstrap.d.ts +89 -23
  12. package/dist/design-token-panel-bootstrap.js +19 -6
  13. package/dist/doc-page-props/index.d.ts +5 -67
  14. package/dist/doc-route-entries/index.d.ts +10 -95
  15. package/dist/doc-route-entries/index.js +1 -78
  16. package/dist/doc-route-paths/index.d.ts +1 -1
  17. package/dist/head-with-defaults/index.d.ts +3 -1
  18. package/dist/head-with-defaults/index.js +76 -4
  19. package/dist/header/nav-active.d.ts +28 -0
  20. package/dist/header/nav-active.js +2 -1
  21. package/dist/header/nav-overflow-script.js +30 -17
  22. package/dist/header-with-defaults/index.js +11 -2
  23. package/dist/i18n-version/language-switcher.d.ts +6 -0
  24. package/dist/i18n-version/language-switcher.js +3 -1
  25. package/dist/i18n-version/version-switcher.d.ts +6 -0
  26. package/dist/i18n-version/version-switcher.js +3 -1
  27. package/dist/nav-source-docs/index.d.ts +7 -11
  28. package/dist/plugins/route-pages-candidates.d.ts +19 -0
  29. package/dist/plugins/route-pages-candidates.js +17 -0
  30. package/dist/plugins/routes.d.ts +46 -0
  31. package/dist/plugins/routes.js +72 -18
  32. package/dist/preset.d.ts +12 -1
  33. package/dist/preset.js +2 -0
  34. package/dist/route-context/index.js +2 -2
  35. package/dist/routes/_chrome.d.ts +1 -1
  36. package/dist/routes/_chrome.js +4 -0
  37. package/dist/routes/_context.d.ts +3 -3
  38. package/dist/routes/_design-token-panel-bootstrap.d.ts +18 -0
  39. package/dist/routes/_design-token-panel-bootstrap.js +11 -0
  40. package/dist/routes/_docs-helpers.d.ts +1 -36
  41. package/dist/routes/_docs-helpers.js +0 -138
  42. package/dist/safelist.css +1 -1
  43. package/dist/search-widget-script/generated-script.d.ts +8 -0
  44. package/dist/search-widget-script/generated-script.js +465 -0
  45. package/dist/search-widget-script/index.d.ts +1 -18
  46. package/dist/search-widget-script/index.js +1 -443
  47. package/dist/settings.d.ts +82 -1
  48. package/dist/sidebar-tree/category-meta.d.ts +9 -0
  49. package/dist/sidebar-tree/category-meta.js +21 -12
  50. package/dist/sidebar-tree-island/index.d.ts +8 -1
  51. package/dist/sidebar-tree-island/index.js +16 -14
  52. package/dist/site-schema/doc-route-entries.d.ts +89 -0
  53. package/dist/site-schema/doc-route-entries.js +83 -0
  54. package/dist/site-schema/index.d.ts +17 -0
  55. package/dist/site-schema/index.js +46 -0
  56. package/dist/site-schema/nav-tree.d.ts +28 -0
  57. package/dist/site-schema/nav-tree.js +138 -0
  58. package/dist/site-schema/types.d.ts +97 -0
  59. package/dist/site-schema/types.js +0 -0
  60. package/dist/theme/theme-pack-provider.d.ts +34 -3
  61. package/dist/theme/theme-pack-provider.js +30 -2
  62. package/eject/header/nav-active.ts +13 -1
  63. package/eject/header/nav-overflow-script.ts +30 -17
  64. package/eject/sidebar-tree-island/index.tsx +44 -20
  65. package/package.json +22 -12
  66. package/routes-src/_chrome.tsx +21 -9
  67. package/routes-src/_design-token-panel-bootstrap.tsx +63 -0
  68. package/routes-src/_docs-helpers.ts +18 -225
  69. package/routes-src/_virtual.d.ts +5 -2
  70. package/virtual-modules.d.ts +5 -2
@@ -11,6 +11,9 @@ import {
11
11
  THEME_PACK_STORAGE_KEY,
12
12
  buildPackCssUrl
13
13
  } from "../theme-pack-switcher/theme-pack-sync.js";
14
+ const THEME_PACK_LOADING_ATTR = "data-zd-theme-pack-loading";
15
+ const THEME_PACK_LOAD_WATCHDOG_MS = 2e3;
16
+ const THEME_PACK_LATCH_CSS = `html[${THEME_PACK_LOADING_ATTR}] body{visibility:hidden}`;
14
17
  function themePackVersionMap(registry) {
15
18
  const map = {};
16
19
  for (const entry of registry) {
@@ -33,6 +36,8 @@ function buildThemePackBootstrap(configuredSlug, enabled, base) {
33
36
  const runtimeGlobal = JSON.stringify(THEME_PACK_RUNTIME_GLOBAL);
34
37
  const afterNav = JSON.stringify(AFTER_NAVIGATE_EVENT);
35
38
  const beforeSwap = JSON.stringify(BEFORE_SWAP_EVENT);
39
+ const loadingAttr = JSON.stringify(THEME_PACK_LOADING_ATTR);
40
+ const watchdog = JSON.stringify(THEME_PACK_LOAD_WATCHDOG_MS);
36
41
  return `(function(){
37
42
  var configured=${configured};
38
43
  var packs=${packs};
@@ -40,12 +45,31 @@ var base=${b};
40
45
  var KEY=${key};
41
46
  var ATTR=${attr};
42
47
  var LINK_ATTR=${linkAttr};
48
+ var LOADING_ATTR=${loadingAttr};
49
+ var WATCHDOG=${watchdog};
43
50
  var DEFAULT_SLUG=${defaultSlug};
44
51
  function resolveSlug(){var stored=null;try{stored=localStorage.getItem(KEY);}catch(e){}if(stored&&Object.prototype.hasOwnProperty.call(packs,stored))return stored;var live=document.documentElement.getAttribute(ATTR);if(live&&Object.prototype.hasOwnProperty.call(packs,live))return live;return configured;}
45
52
  function packHref(slug){return base+"theme-packs/"+slug+"/pack.css?v="+packs[slug];}
53
+ function hasPackLink(href){var ls=document.querySelectorAll("link["+LINK_ATTR+"]");for(var i=0;i<ls.length;i++){if(ls[i].getAttribute("href")===href)return true;}return false;}
46
54
  var slug=resolveSlug();
47
- document.documentElement.setAttribute(ATTR,slug);
48
- if(slug!==DEFAULT_SLUG){document.write('<link rel="stylesheet" '+LINK_ATTR+' href="'+packHref(slug)+'">');}
55
+ var root=document.documentElement;
56
+ root.setAttribute(ATTR,slug);
57
+ if(slug!==DEFAULT_SLUG){
58
+ var href=packHref(slug);
59
+ if(!hasPackLink(href)){
60
+ var shouldLatch=document.readyState==="loading";
61
+ if(shouldLatch){root.setAttribute(LOADING_ATTR,"");}
62
+ var release=function(){root.removeAttribute(LOADING_ATTR);};
63
+ var link=document.createElement("link");
64
+ link.setAttribute("rel","stylesheet");
65
+ link.setAttribute(LINK_ATTR,"");
66
+ link.setAttribute("href",href);
67
+ link.onload=release;
68
+ link.onerror=release;
69
+ document.head.appendChild(link);
70
+ if(shouldLatch){setTimeout(release,WATCHDOG);}
71
+ }
72
+ }
49
73
  window[${runtimeGlobal}]={base:base,packs:packs,configured:configured};
50
74
  document.addEventListener(${beforeSwap},function(e){
51
75
  var s=resolveSlug();
@@ -81,11 +105,15 @@ function ThemePackProvider({
81
105
  const configuredVersion = enabled[configuredSlug];
82
106
  const noscriptHref = configuredSlug !== DEFAULT_THEME_PACK_SLUG && configuredVersion !== void 0 ? buildPackCssUrl(base, configuredSlug, configuredVersion) : null;
83
107
  return /* @__PURE__ */ jsxs(Fragment, { children: [
108
+ /* @__PURE__ */ jsx("style", { dangerouslySetInnerHTML: { __html: THEME_PACK_LATCH_CSS } }),
84
109
  /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: bootstrap } }),
85
110
  noscriptHref !== null && /* @__PURE__ */ jsx("noscript", { children: /* @__PURE__ */ jsx("link", { rel: "stylesheet", href: noscriptHref }) })
86
111
  ] });
87
112
  }
88
113
  export {
114
+ THEME_PACK_LATCH_CSS,
115
+ THEME_PACK_LOADING_ATTR,
116
+ THEME_PACK_LOAD_WATCHDOG_MS,
89
117
  buildThemePackBootstrap,
90
118
  ThemePackProvider as default,
91
119
  resolveThemePackSsrSlug,
@@ -53,13 +53,25 @@ export function pathForMatch(
53
53
  /**
54
54
  * Segment-aware prefix test: nav path `/docs/guides` matches `/docs/guides`
55
55
  * and `/docs/guides/...` but NOT `/docs/guideship`.
56
+ *
57
+ * Exported (not just module-private) so `nav-overflow-script.ts` can embed it
58
+ * verbatim via `.toString()` alongside `computeActiveNavPath` — that function
59
+ * closes over this one, so a bare `computeActiveNavPath.toString()` embed
60
+ * would reference an undefined `pathMatchesNavPath` in the browser (see the
61
+ * caution note on `computeActiveNavPath` below). Kept self-contained (no
62
+ * outer references) for the same reason.
56
63
  */
57
- function pathMatchesNavPath(currentPath: string, navPath: string): boolean {
64
+ export function pathMatchesNavPath(currentPath: string, navPath: string): boolean {
58
65
  if (currentPath === navPath) return true;
59
66
  const prefix = navPath.endsWith("/") ? navPath : `${navPath}/`;
60
67
  return currentPath.startsWith(prefix);
61
68
  }
62
69
 
70
+ /**
71
+ * CAUTION (zudolab/zudo-doc#3398): this closes over `pathMatchesNavPath`
72
+ * above. `nav-overflow-script.ts` embeds both via `.toString()` (never this
73
+ * one alone) so the generated browser script is self-contained.
74
+ */
63
75
  export function computeActiveNavPath(
64
76
  navItems: readonly NavItemLike[],
65
77
  pathForMatchValue: string,
@@ -31,6 +31,8 @@
31
31
  // script can be reviewed in isolation.
32
32
 
33
33
  import { AFTER_NAVIGATE_EVENT } from "../transitions/page-events.js";
34
+ import { CURRENT_PATH_SCRIPT_PRELUDE } from "../current-path/index.js";
35
+ import { computeActiveNavPath, pathMatchesNavPath } from "./nav-active.js";
34
36
  import {
35
37
  NAV_CHEVRON_ACTIVE,
36
38
  NAV_CHEVRON_INACTIVE,
@@ -78,20 +80,26 @@ export const NAV_OVERFLOW_SCRIPT = `(function () {
78
80
  catch (e) { return ""; }
79
81
  }
80
82
 
81
- function isUnderPath(cur, p) {
82
- if (!p) return false;
83
- if (cur === p) return true;
84
- return p !== "/" && cur.indexOf(p + "/") === 0;
85
- }
83
+ // Explicit current-route override, embedded from current-path/index.ts so
84
+ // this script cannot drift from the three other read sites
85
+ // (zudolab/zudo-doc#3398, #3408).
86
+ ${CURRENT_PATH_SCRIPT_PRELUDE}
87
+
88
+ // Shared matching core (zudolab/zudo-doc#3398): embedded verbatim from
89
+ // nav-active.ts so this script's longest-match walk cannot drift from the
90
+ // SSR header's own computeActiveNavPath call (header.tsx). computeActiveNavPath
91
+ // closes over pathMatchesNavPath, so both are embedded together.
92
+ var pathMatchesNavPath = ${pathMatchesNavPath.toString()};
93
+ var computeActiveNavPath = ${computeActiveNavPath.toString()};
86
94
 
87
95
  // Recompute which header nav item is "active" from the CURRENT URL and
88
96
  // repaint the highlight. SSR sets the active item on first paint, but the
89
97
  // header is persisted across same-locale client-router swaps
90
98
  // (data-zfb-transition-persist), so without this the highlight would stay
91
99
  // frozen on the page where the header was first rendered. Mirrors the
92
- // sidebar island's client-side approach (match location.pathname against
100
+ // sidebar island's client-side approach (match the current path against
93
101
  // each entry's href) and the SSR longest-match + dropdown-parent rules.
94
- // URL-based: hrefs and location.pathname both carry the base + locale
102
+ // URL-based: hrefs and the current path both carry the base + locale
95
103
  // prefix, so they compare directly without stripping.
96
104
  function applyActiveNav() {
97
105
  var nav = document.querySelector("[data-header-nav]");
@@ -99,26 +107,31 @@ export const NAV_OVERFLOW_SCRIPT = `(function () {
99
107
  var topItems = Array.from(nav.querySelectorAll(":scope > [data-nav-item]"));
100
108
  if (topItems.length === 0) return;
101
109
 
102
- var cur = trimSlashes(location.pathname);
110
+ var cur = trimSlashes(readCurrentPath(CURRENT_PATH_DATASET_KEY));
103
111
 
104
- // Deepest (longest) nav path the current URL lives under, across both
105
- // top-level and dropdown-child paths — matches computeActiveNavPath.
106
- var activePath = "";
112
+ // Build NavItemLike-shaped entries from the live DOM so the shared
113
+ // computeActiveNavPath can do the deepest-match walk — the same call
114
+ // shape the SSR header uses (matches computeActiveNavPath). A dropdown
115
+ // missing its own top-level anchor is skipped entirely (path "" would
116
+ // otherwise match every current path — pathMatchesNavPath treats "" as
117
+ // the root "/"), mirroring the parentLink guard used below for the same
118
+ // malformed-markup case.
119
+ var navItems = [];
107
120
  topItems.forEach(function (it) {
108
121
  var isDropdown = it.hasAttribute("data-nav-item-dropdown");
109
122
  var topA = isDropdown ? it.querySelector(":scope > a") : it;
110
- if (topA) {
111
- var tp = navPathname(topA);
112
- if (isUnderPath(cur, tp) && tp.length > activePath.length) activePath = tp;
113
- }
123
+ if (!topA) return;
124
+ var children = [];
114
125
  if (isDropdown) {
115
126
  it.querySelectorAll(":scope > div a").forEach(function (c) {
116
- var cp = navPathname(c);
117
- if (isUnderPath(cur, cp) && cp.length > activePath.length) activePath = cp;
127
+ children.push({ path: navPathname(c) });
118
128
  });
119
129
  }
130
+ navItems.push({ path: navPathname(topA), children: children });
120
131
  });
121
132
 
133
+ var activePath = computeActiveNavPath(navItems, cur) || "";
134
+
122
135
  function setTopActive(a, active) {
123
136
  if (!a) return;
124
137
  if (active) {
@@ -18,6 +18,7 @@ import { smartBreakToHtml } from "../smart-break/index.js";
18
18
  import { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT } from "../transitions/index.js";
19
19
  import { filterTree } from "../sidebar-filter/index.js";
20
20
  import { findActiveSlug, normalizePath } from "../sidebar-active-slug/index.js";
21
+ import { CURRENT_PATH_DATASET_KEY, readCurrentPath } from "../current-path/index.js";
21
22
  import { ensureSidebarScrollPreserve } from "./sidebar-scroll-preserve.js";
22
23
 
23
24
  // The persisted aside can transiently tear down and re-mount its SidebarTree
@@ -61,38 +62,54 @@ function saveOpenSet(set: Set<string>) {
61
62
  }
62
63
 
63
64
  /**
64
- * Derive the active slug from the current document URL. Used as a hydration-
65
- * time fallback when the parent island does not forward `currentSlug` through
66
- * its prop boundary, and at every View Transition to keep the highlight in
67
- * sync.
65
+ * Derive the active slug from an explicit current-route input. Resolution
66
+ * order is owned by {@link readCurrentPath}: the `pathname` argument, then the
67
+ * `data-zd-current-path` override, then `window.location.pathname`. Used as a
68
+ * hydration-time fallback when the parent island does not forward
69
+ * `currentSlug` through its prop boundary, and at every View Transition to
70
+ * keep the highlight in sync.
71
+ *
72
+ * Returns `undefined` both when no pathname is resolvable AND when the
73
+ * resolved pathname matches no route (e.g. the literal "srcdoc") — callers
74
+ * must treat `undefined` as "no update," preserving whatever active slug is
75
+ * already set rather than clearing it.
68
76
  */
69
- function deriveActiveSlugFromUrl(nodes: SidebarNavNode[]): string | undefined {
70
- if (typeof window === "undefined") return undefined;
71
- const pathname = normalizePath(window.location.pathname);
72
- return findActiveSlug(nodes, pathname);
77
+ function deriveActiveSlug(nodes: SidebarNavNode[], pathname?: string): string | undefined {
78
+ // An empty-string `pathname` must fall through to the live sources instead
79
+ // of silently disabling derivation — SidebarWithDefaults defaults its own
80
+ // currentPath to "", so "" is the natural absent shape.
81
+ const resolved = readCurrentPath(CURRENT_PATH_DATASET_KEY, pathname);
82
+ if (!resolved) return undefined;
83
+ return findActiveSlug(nodes, normalizePath(resolved));
73
84
  }
74
85
 
75
86
  /**
76
87
  * Track the current active slug, updating on View Transition navigations.
77
88
  *
78
89
  * The initial-state initialiser prefers the SSR-supplied `initial` prop, but
79
- * falls back to deriving the slug from `window.location.pathname` when the
80
- * prop is missing.
90
+ * falls back to `deriveActiveSlug` (explicit `currentPath` input, then the
91
+ * override/location fallbacks) when the prop is missing.
81
92
  */
82
- function useActiveSlug(nodes: SidebarNavNode[], initial?: string): string | undefined {
93
+ function useActiveSlug(nodes: SidebarNavNode[], initial?: string, currentPath?: string): string | undefined {
83
94
  const [slug, setSlug] = useState<string | undefined>(() =>
84
- initial !== undefined ? initial : deriveActiveSlugFromUrl(nodes),
95
+ initial !== undefined ? initial : deriveActiveSlug(nodes, currentPath),
85
96
  );
86
97
 
87
98
  useEffect(() => {
88
- const update = () => {
89
- const found = deriveActiveSlugFromUrl(nodes);
99
+ const update = (pathname?: string) => {
100
+ const found = deriveActiveSlug(nodes, pathname);
90
101
  if (found !== undefined) setSlug(found);
91
102
  };
92
- update();
93
- document.addEventListener(AFTER_NAVIGATE_EVENT, update);
94
- return () => document.removeEventListener(AFTER_NAVIGATE_EVENT, update);
95
- }, [nodes]);
103
+ // The static `currentPath` prop is an SSR-serialized value for the page the
104
+ // island first hydrated on — valid at hydration time only. Post-navigation
105
+ // updates must re-read the LIVE sources (dataset override, then
106
+ // location.pathname); recomputing from the frozen prop would pin the
107
+ // highlight to the first-loaded page across client-router navigations.
108
+ const onNavigate = () => update();
109
+ update(currentPath);
110
+ document.addEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
111
+ return () => document.removeEventListener(AFTER_NAVIGATE_EVENT, onNavigate);
112
+ }, [nodes, currentPath]);
96
113
 
97
114
  return slug;
98
115
  }
@@ -143,6 +160,13 @@ function RootMenuItemEntry({ item }: { item: SidebarRootMenuItem }) {
143
160
  export interface SidebarTreeProps {
144
161
  nodes: SidebarNavNode[];
145
162
  currentSlug?: string;
163
+ /**
164
+ * Explicit current-route override, checked before the
165
+ * `data-zd-current-path` dataset override and `window.location.pathname`
166
+ * when deriving the active slug on hydration and at every View Transition.
167
+ * See `deriveActiveSlug` (zudolab/zudo-doc#3398).
168
+ */
169
+ currentPath?: string;
146
170
  rootMenuItems?: SidebarRootMenuItem[];
147
171
  backToMenuLabel?: string;
148
172
  localeLinks?: SidebarLocaleLink[];
@@ -171,8 +195,8 @@ function SidebarFooter({ links, themeDefaultMode }: { links?: SidebarLocaleLink[
171
195
  );
172
196
  }
173
197
 
174
- export function SidebarTree({ nodes, currentSlug, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }: SidebarTreeProps) {
175
- const activeSlug = useActiveSlug(nodes, currentSlug);
198
+ export function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }: SidebarTreeProps) {
199
+ const activeSlug = useActiveSlug(nodes, currentSlug, currentPath);
176
200
  const [query, setQuery] = useState("");
177
201
  const [showingRootMenu, setShowingRootMenu] = useState(false);
178
202
  const filterRef = useRef<HTMLInputElement>(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "5.5.3",
3
+ "version": "5.6.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",
@@ -239,6 +239,10 @@
239
239
  "types": "./dist/catalog.d.ts",
240
240
  "default": "./dist/catalog.js"
241
241
  },
242
+ "./site-schema": {
243
+ "types": "./dist/site-schema/index.d.ts",
244
+ "default": "./dist/site-schema/index.js"
245
+ },
242
246
  "./theme.css": "./dist/theme.css",
243
247
  "./safelist.css": "./dist/safelist.css",
244
248
  "./content.css": "./dist/content.css",
@@ -553,6 +557,10 @@
553
557
  "types": "./dist/sidebar-active-slug/index.d.ts",
554
558
  "default": "./dist/sidebar-active-slug/index.js"
555
559
  },
560
+ "./current-path": {
561
+ "types": "./dist/current-path/index.d.ts",
562
+ "default": "./dist/current-path/index.js"
563
+ },
556
564
  "./i18n-defaults": {
557
565
  "types": "./dist/i18n-defaults/index.d.ts",
558
566
  "default": "./dist/i18n-defaults/index.js"
@@ -609,11 +617,11 @@
609
617
  "CHANGELOG.md"
610
618
  ],
611
619
  "peerDependencies": {
612
- "@takazudo/zdtp": "^0.4.10",
613
- "@takazudo/zfb": "^2.5.2",
614
- "@takazudo/zfb-md-wasm": "^2.5.2",
615
- "@takazudo/zfb-runtime": "^2.5.2",
616
- "@takazudo/zudo-doc-history-server": "^5.5.2",
620
+ "@takazudo/zdtp": "^0.4.11",
621
+ "@takazudo/zfb": "^2.7.1",
622
+ "@takazudo/zfb-md-wasm": "^2.7.1",
623
+ "@takazudo/zfb-runtime": "^2.7.1",
624
+ "@takazudo/zudo-doc-history-server": "^5.5.3",
617
625
  "diff": "^8.0.0",
618
626
  "katex": "^0.16.0",
619
627
  "preact": "^10.29.1",
@@ -647,14 +655,15 @@
647
655
  "tsx": "^4.21.0"
648
656
  },
649
657
  "devDependencies": {
650
- "@takazudo/zfb": "2.5.2",
651
- "@takazudo/zfb-md-wasm": "2.5.2",
652
- "@takazudo/zfb-runtime": "2.5.2",
658
+ "@takazudo/zfb": "2.7.1",
659
+ "@takazudo/zfb-md-wasm": "2.7.1",
660
+ "@takazudo/zfb-runtime": "2.7.1",
653
661
  "@types/fs-extra": "^11.0.4",
654
662
  "@types/minimist": "^1.2.5",
655
663
  "@types/node": "^25.3.5",
656
664
  "@types/pluralize": "^0.0.33",
657
665
  "@types/string-similarity": "^4.0.2",
666
+ "esbuild": ">=0.28.1",
658
667
  "happy-dom": "^20.10.6",
659
668
  "npm-run-all2": "^7.0.2",
660
669
  "preact": "^10.29.1",
@@ -663,11 +672,12 @@
663
672
  "typescript": "^5.0.0",
664
673
  "vitest": "^4.1.0",
665
674
  "zod": "^4.3.6",
666
- "@takazudo/zudo-doc-history-server": "5.5.3"
675
+ "@takazudo/zudo-doc-history-server": "5.6.0"
667
676
  },
668
677
  "scripts": {
669
- "build": "tsup && tsc -p tsconfig.build.json",
670
- "predev": "node ../../scripts/ensure-workspace-build.mjs",
678
+ "gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",
679
+ "build": "node scripts/gen-search-widget-script.mjs && tsup && tsc -p tsconfig.build.json",
680
+ "predev": "node ../../scripts/ensure-workspace-build.mjs && node scripts/gen-search-widget-script.mjs",
671
681
  "// dev": "Do NOT add `--continue-on-error` here. It stops a fatal watcher exit from cascading, but the dead watcher then fails silently — a dead dev:dts leaves dist/*.d.ts frozen while the JS keeps updating, which typechecks cleanly against stale types instead of failing loudly like #3113's absent declarations did. See the #3129 section in CLAUDE.md.",
672
682
  "dev": "run-p dev:js dev:dts",
673
683
  "dev:js": "tsup --watch",
@@ -13,7 +13,7 @@
13
13
  import { routeCtx } from "./_context.js";
14
14
  import { createChrome } from "@takazudo/zudo-doc/chrome";
15
15
  import { DocHistory } from "@takazudo/zudo-doc/doc-history";
16
- import type { DocNavNode } from "./_docs-helpers.js";
16
+ import type { DocNavNode } from "@takazudo/zudo-doc/site-schema";
17
17
  // Imported via the BARE published subpath rather than a relative
18
18
  // `../chrome-bindings.js`. This file is copied into consumer projects by the
19
19
  // routes-src mechanism (`scripts/copy-routes-src.mjs`), which rewrites relative
@@ -27,6 +27,10 @@ import { defineChromeBindings } from "@takazudo/zudo-doc/chrome-bindings";
27
27
  // ("Host-callables channel — chromeBindingsModule"). Not present on disk; the
28
28
  // package ships ambient typings for it (`routes/_virtual.d.ts`).
29
29
  import { chromeBindings } from "virtual:zudo-doc-chrome-bindings";
30
+ // Routes-only configured design-token-panel island (#3396) — the ONLY importer
31
+ // of `virtual:zudo-doc-design-token-panel-config` in the package. Static, for
32
+ // the same island-scanner reason as `DocHistory` above.
33
+ import { ConfiguredDesignTokenPanelBootstrap } from "./_design-token-panel-bootstrap.js";
30
34
 
31
35
  // Island-scanner contract (load-bearing): the injected doc routes reach the real
32
36
  // DocHistory client island ONLY through this static import → `createChrome`
@@ -57,15 +61,23 @@ import { chromeBindings } from "virtual:zudo-doc-chrome-bindings";
57
61
  // the way the static `DocHistory` import above is (the virtual re-export sits
58
62
  // outside zfb's static-import scanner reachability graph) — see the ADR.
59
63
  //
60
- // `DesignTokenPanelBootstrap` (#2658) is NOT threaded here: unlike DocHistory
61
- // (whose derive-level default is a no-op stub), the package-default island IS
62
- // the derive-level default (`chrome/derive.tsx`'s `deriveBodyEndIslands`,
63
- // gate-2 fix from the Wave-5 confirm #2659) — so this shim, the locked-manifest
64
- // self-contained doc stub (#2653), and every other bare `createChrome` caller
65
- // all get it without explicit wiring. Scanner reachability holds through the
66
- // static chain route → this shim → `createChrome` → `chrome/derive` →
67
- // `design-token-panel-bootstrap`.
64
+ // `DesignTokenPanelBootstrap` IS threaded here since #3396, but only to swap
65
+ // the BUILDER — the mount, the settings gate, and the slot default all still
66
+ // belong to `chrome/derive.tsx`'s `deriveBodyEndIslands` (gate-2 fix from the
67
+ // Wave-5 confirm #2659), so the locked-manifest self-contained doc stub (#2653)
68
+ // and every other bare `createChrome` caller keep getting the package-default
69
+ // island with no explicit wiring. On THESE injected routes the configured
70
+ // wrapper wins, which is what carries a host's
71
+ // `settings.designTokenPanelConfigModule` through. Scanner reachability holds
72
+ // through the static chain route → this shim → `_design-token-panel-bootstrap`.
73
+ //
74
+ // It is spread BEFORE `...chromeBindings` (unlike `DocHistory`, which is spread
75
+ // after): a host's own `DesignTokenPanelBootstrap` slot value must still win,
76
+ // exactly as it did when the derive-level default was the only package wiring.
68
77
  const chrome = createChrome(routeCtx, {
78
+ ...defineChromeBindings({
79
+ DesignTokenPanelBootstrap: ConfiguredDesignTokenPanelBootstrap,
80
+ }),
69
81
  ...chromeBindings,
70
82
  ...defineChromeBindings({ DocHistory }),
71
83
  });
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ /** @jsxRuntime automatic */
4
+ /** @jsxImportSource preact */
5
+
6
+ // routes/_design-token-panel-bootstrap — the ROUTES-ONLY configured
7
+ // design-token-panel island (#3396, epic #3394).
8
+ //
9
+ // WHY THIS FILE EXISTS: `virtual:zudo-doc-design-token-panel-config` is
10
+ // registered by the routes plugin, so any module that imports it statically
11
+ // drags the plugin along as a hard requirement. Until #3396 that importer was
12
+ // `../design-token-panel-bootstrap.tsx`, which `chrome/derive.tsx` imports —
13
+ // meaning `@takazudo/zudo-doc/chrome` could not be bundled outside a zfb build
14
+ // (or by a `packageOwnedRoutes: false` host) without aliasing the specifier.
15
+ // Moving the import HERE confines it to the routes graph, which zfb always
16
+ // bundles with the plugin active.
17
+ //
18
+ // WHY A COMPONENT AND NOT A SETTER: the route/SSR module graph and the
19
+ // hydrated island bundle are SEPARATE graphs. A `setPanelConfigBuilder(...)`
20
+ // called while `_chrome.tsx` evaluates would configure only the server-side
21
+ // copy of the bootstrap module; the client bundle re-evaluates that module
22
+ // independently and would keep the package default. Passing the builder as an
23
+ // argument from a component that lives INSIDE the island bundle is the only
24
+ // shape that survives the boundary.
25
+ //
26
+ // ISLAND-SCANNER CONTRACT (#2480 lesson): `_chrome.tsx` imports this module
27
+ // STATICALLY and threads the component into `hostBindings`, so the scanner
28
+ // walks route → _chrome → here. The exported binding identifier and the
29
+ // `displayName` must stay IDENTICAL — zfb registers islands by the
30
+ // scanner-visible export name, and a marker whose name has no registry entry
31
+ // silently never hydrates. The name must also stay DISTINCT from
32
+ // `DesignTokenPanelBootstrap`: `chrome/derive.tsx` still statically imports
33
+ // that package default (it is the slot default for bare `createChrome`
34
+ // callers), so both components are in the scanned graph and a shared name
35
+ // would trip zfb's "island marker name collision" warning and drop one of them.
36
+
37
+ import type { JSX } from "preact";
38
+ import { runDesignTokenPanelBootstrapOnce } from "@takazudo/zudo-doc/design-token-panel-bootstrap";
39
+ // Host-callables channel, third virtual module (#2658, mirrors #2501's
40
+ // chromeBindingsModule): absent `settings.designTokenPanelConfigModule` →
41
+ // re-exports the package default (`@takazudo/zudo-doc/design-token-panel-config`);
42
+ // present → re-exports the host's module. Registered unconditionally by the
43
+ // routes plugin (`../plugins/routes.ts`) whenever `packageOwnedRoutes` is on.
44
+ // Not present on disk; the package ships ambient typings for it
45
+ // (`./_virtual.d.ts`).
46
+ import { buildDesignTokenPanelConfig } from "virtual:zudo-doc-design-token-panel-config";
47
+
48
+ /**
49
+ * The design-token-panel island injected routes mount instead of the package
50
+ * default: identical behavior, except the mode-scoped `PanelConfig` builder is
51
+ * the one the routes plugin resolved — the host's
52
+ * `settings.designTokenPanelConfigModule` when set, the package default
53
+ * otherwise.
54
+ *
55
+ * Renders `null` on both SSR and client (the panel self-mounts as a side
56
+ * effect of `bootstrapDesignTokenPanel`), matching the package default's
57
+ * non-`ssrFallback` `Island({ when: "load" })` shape.
58
+ */
59
+ export function ConfiguredDesignTokenPanelBootstrap(): JSX.Element | null {
60
+ runDesignTokenPanelBootstrapOnce(buildDesignTokenPanelConfig, "configured");
61
+ return null;
62
+ }
63
+ ConfiguredDesignTokenPanelBootstrap.displayName = "ConfiguredDesignTokenPanelBootstrap";