create-zudo-doc 0.2.21 → 0.2.22

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 (71) hide show
  1. package/dist/features/image-enlarge.d.ts +10 -2
  2. package/dist/features/image-enlarge.js +10 -2
  3. package/dist/scaffold.js +20 -22
  4. package/dist/zfb-config-gen.d.ts +16 -10
  5. package/dist/zfb-config-gen.js +34 -239
  6. package/package.json +1 -1
  7. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  8. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  9. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  10. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  11. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  12. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  13. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  15. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  16. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  17. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  18. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  19. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  20. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  21. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  22. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  23. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  24. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  25. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  26. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  27. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  28. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  29. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  30. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  31. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  33. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  34. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  35. package/templates/base/pages/lib/locale-merge.ts +32 -145
  36. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  37. package/templates/base/pages/robots.txt.tsx +2 -26
  38. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  39. package/templates/base/src/components/doc-history.tsx +9 -8
  40. package/templates/base/src/components/image-enlarge.tsx +11 -8
  41. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  42. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  43. package/templates/base/src/components/site-tree-nav.tsx +6 -220
  44. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  45. package/templates/base/src/config/i18n.ts +9 -0
  46. package/templates/base/src/config/z-index-tokens.ts +5 -4
  47. package/templates/base/src/styles/global.css +5 -579
  48. package/templates/base/src/utils/base.ts +1 -1
  49. package/templates/base/src/utils/docs.ts +47 -16
  50. package/templates/base/src/utils/github.ts +12 -9
  51. package/templates/base/src/utils/nav-scope.ts +13 -42
  52. package/templates/base/src/utils/sidebar.ts +18 -86
  53. package/templates/base/src/utils/slug.ts +10 -53
  54. package/templates/base/src/utils/smart-break.tsx +12 -120
  55. package/templates/base/src/utils/tags.ts +25 -68
  56. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  57. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  58. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  59. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  60. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  61. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  62. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  63. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  64. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  65. package/templates/base/plugins/connect-adapter.mjs +0 -169
  66. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  67. package/templates/base/scripts/gen-z-index.mjs +0 -157
  68. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  69. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  70. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  71. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,470 +1,2 @@
1
- // Client-side script for the SiteSearch custom element.
2
- //
3
- // Implemented as a plain JavaScript IIFE (no ES-module `import` statements)
4
- // so it can be emitted via `dangerouslySetInnerHTML` without requiring bundler
5
- // support for inline scripts.
6
- //
7
- // Key differences from the bundled module approach:
8
- // - MiniSearch is NOT imported; a lightweight built-in search (fetch
9
- // index + simple word-match scoring) is used instead. This avoids the
10
- // inline-script bundling limitation. Full MiniSearch integration can be
11
- // added in a follow-up topic once the bundle pipeline is in place.
12
- // - The post-navigation rebinder pulls its event name from
13
- // `AFTER_NAVIGATE_EVENT` in
14
- // `@takazudo/zudo-doc/transitions` (today: `zfb:after-swap`)
15
- // rather than a hard-coded `astro:*` literal. See
16
- // zudolab/zudo-doc#1335 (E2 task 2 half B) for the vocabulary
17
- // introduction and zudolab/zudo-doc#1523 for the W6B flip from
18
- // `DOMContentLoaded` to the Strategy B SPA event name.
19
-
20
- import { AFTER_NAVIGATE_EVENT } from "@takazudo/zudo-doc/transitions";
21
-
22
- export const SEARCH_WIDGET_SCRIPT = /* javascript */ `(function () {
23
- if (customElements.get("site-search")) return; // guard double-registration
24
-
25
- var PAGE_SIZE = 10;
26
-
27
- // Allowlist-based href sanitizer: only relative paths and http(s) URLs are
28
- // permitted. Anything else (e.g. javascript:, data:) falls back to "#" so a
29
- // malicious entry in search-index.json cannot turn a result link into a
30
- // script-injection vector.
31
- function safeHref(url) {
32
- if (!url) return "#";
33
- var s = String(url);
34
- if (s.startsWith("/") || s.startsWith("http://") || s.startsWith("https://")) {
35
- return s;
36
- }
37
- return "#";
38
- }
39
-
40
- function escapeHtml(text) {
41
- return String(text)
42
- .replace(/&/g, "&")
43
- .replace(/</g, "&lt;")
44
- .replace(/>/g, "&gt;")
45
- .replace(/"/g, "&quot;")
46
- .replace(/'/g, "&#39;");
47
- }
48
-
49
- function escapeRegExp(text) {
50
- return text.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&");
51
- }
52
-
53
- function parseTerms(query) {
54
- return query.trim().split(/\\s+/).filter(Boolean);
55
- }
56
-
57
- // scoreEntry reads pre-lowercased fields (_titleLc, _descLc, _bodyLc)
58
- // set by prepareLc() at index-load time. Terms arrive already lowercased
59
- // from search() so no per-call toLowerCase() is needed.
60
- function scoreEntry(entry, terms) {
61
- var score = 0;
62
- var titleLc = entry._titleLc;
63
- var descLc = entry._descLc;
64
- var bodyLc = entry._bodyLc;
65
- for (var i = 0; i < terms.length; i++) {
66
- var t = terms[i];
67
- if (titleLc.indexOf(t) !== -1) score += 3;
68
- if (descLc.indexOf(t) !== -1) score += 2;
69
- if (bodyLc.indexOf(t) !== -1) score += 1;
70
- }
71
- return score;
72
- }
73
-
74
- // Pre-lowercase the searched fields on each entry once at load time so that
75
- // scoreEntry() does not re-lowercase the entire ~162 KB index on every
76
- // debounced keystroke. Original-case fields are preserved for display.
77
- function prepareLc(entries) {
78
- for (var i = 0; i < entries.length; i++) {
79
- var e = entries[i];
80
- e._titleLc = (e.title || "").toLowerCase();
81
- e._descLc = (e.description || "").toLowerCase();
82
- e._bodyLc = (e.body || "").toLowerCase();
83
- }
84
- }
85
-
86
- function highlightTerms(text, terms) {
87
- if (!terms.length) return escapeHtml(text);
88
- var escaped = terms.map(function(t) { return escapeRegExp(t); });
89
- var pattern = new RegExp("(" + escaped.join("|") + ")", "gi");
90
- return text.split(pattern).map(function(seg, i) {
91
- return i % 2 === 1
92
- ? "<mark>" + escapeHtml(seg) + "</mark>"
93
- : escapeHtml(seg);
94
- }).join("");
95
- }
96
-
97
- function truncate(text, query, max) {
98
- max = max || 200;
99
- if (text.length <= max) return text;
100
- var terms = parseTerms(query);
101
- var lower = text.toLowerCase();
102
- var best = -1;
103
- for (var i = 0; i < terms.length; i++) {
104
- var idx = lower.indexOf(terms[i].toLowerCase());
105
- if (idx !== -1 && (best === -1 || idx < best)) best = idx;
106
- }
107
- if (best === -1) return text.slice(0, max) + "\\u2026";
108
- var half = Math.floor(max / 2);
109
- var start = Math.max(0, best - half);
110
- var end = start + max;
111
- if (end > text.length) { end = text.length; start = Math.max(0, end - max); }
112
- var result = text.slice(start, end);
113
- if (start > 0) result = "\\u2026" + result;
114
- if (end < text.length) result += "\\u2026";
115
- return result;
116
- }
117
-
118
- customElements.define("site-search", class SiteSearch extends HTMLElement {
119
- constructor() {
120
- super();
121
- this._dialog = null;
122
- this._openBtn = null;
123
- this._closeBtn = null;
124
- this._input = null;
125
- this._results = null;
126
- this._countWide = null;
127
- this._countNarrow = null;
128
- this._entries = null;
129
- this._loading = false;
130
- this._indexUnavailable = false;
131
- this._debounce = null;
132
- this._currentQuery = "";
133
- this._allResults = [];
134
- this._shownCount = 0;
135
- this._shortcut = "";
136
- this._resultCountTemplate = "";
137
- this._keydownHandler = null;
138
- // Delegated click handler on the results container: closing the dialog
139
- // when a result link is activated (epic #2148). Held so disconnectedCallback
140
- // can detach it on body swap.
141
- this._resultsClickHandler = null;
142
- this._observer = null;
143
- this._sentinel = null;
144
- this._isLoadingBatch = false;
145
- // Snapshot of the initial results-area HTML (includes SSR placeholder).
146
- // Captured in connectedCallback so we can restore it on input-clear
147
- // without re-querying the DOM (the placeholder node is replaced once
148
- // search results are rendered).
149
- this._placeholderHtml = "";
150
- // Held so we can remove the document-level after-navigate listener
151
- // in disconnectedCallback. zudolab/zudo-doc#1523 — under Strategy B
152
- // SPA navigation a non-persisted <site-search> element would leak
153
- // one document listener per nav without this hook.
154
- this._afterNavHandler = null;
155
- }
156
-
157
- connectedCallback() {
158
- this._dialog = this.querySelector("[data-search-dialog]");
159
- this._openBtn = this.querySelector("[data-open-search]");
160
- this._closeBtn = this.querySelector("[data-close-search]");
161
- this._input = this.querySelector("[data-search-input]");
162
- this._results = this.querySelector("[data-search-results]");
163
- this._countWide = this.querySelector("[data-search-count]");
164
- this._countNarrow = this.querySelector("[data-search-count-narrow]");
165
- this._resultCountTemplate = this.dataset.resultCountTemplate || "{count} results";
166
- // Snapshot the placeholder HTML before any search renders overwrite it.
167
- this._placeholderHtml = this._results ? this._results.innerHTML : "";
168
-
169
- // Platform keyboard-shortcut label — injected into [data-kbd-shortcut]
170
- var nav = navigator;
171
- var isMac = /Mac|iPhone|iPad|iPod/.test(
172
- (nav.userAgentData && nav.userAgentData.platform) || nav.userAgent
173
- );
174
- this._shortcut = isMac ? "\\u2318K" : "Ctrl+K";
175
- var kbdEl = this.querySelector("[data-kbd-shortcut]");
176
- if (kbdEl) kbdEl.textContent = this._shortcut;
177
-
178
- // Wire open/close handlers
179
- var self = this;
180
- if (this._openBtn) {
181
- this._openBtn.addEventListener("click", function() { self.openDialog(); });
182
- }
183
- if (this._closeBtn) {
184
- this._closeBtn.addEventListener("click", function() { self.closeDialog(); });
185
- }
186
- if (this._dialog) {
187
- this._dialog.addEventListener("close", function() {
188
- document.documentElement.style.overflow = "";
189
- });
190
- this._dialog.addEventListener("click", function(e) {
191
- if (e.target === self._dialog) self.closeDialog();
192
- });
193
- }
194
- if (this._input) {
195
- this._input.addEventListener("input", function() { self.handleInput(); });
196
- }
197
-
198
- // Close-on-result-click (epic #2148): result links are created dynamically
199
- // in renderResult(), so use one delegated listener on the results container
200
- // instead of per-link handlers. We do NOT preventDefault — the link's own
201
- // navigation (zfb Strategy-B SPA swap or a plain load) must still proceed;
202
- // we only close the <dialog> so it does not linger over the swapped page.
203
- // closeDialog() runs synchronously before navigation; the dialog's close
204
- // restores documentElement overflow via the existing "close" listener.
205
- if (this._results) {
206
- this._resultsClickHandler = function(e) {
207
- var t = e.target;
208
- while (t && t !== self._results) {
209
- if (t.tagName === "A") { self.closeDialog(); return; }
210
- t = t.parentNode;
211
- }
212
- };
213
- this._results.addEventListener("click", this._resultsClickHandler);
214
- }
215
-
216
- // Global keyboard shortcut (⌘K / Ctrl+K to open)
217
- this._keydownHandler = function(e) {
218
- if ((e.metaKey || e.ctrlKey) && e.key === "k") {
219
- e.preventDefault();
220
- self.openDialog();
221
- }
222
- };
223
- document.addEventListener("keydown", this._keydownHandler);
224
-
225
- // View-Transitions compat: re-run on the v2 after-navigate event.
226
- // Stored on the instance so disconnectedCallback can detach it on
227
- // body swap when this element is NOT persisted via
228
- // data-zfb-transition-persist (zudolab/zudo-doc#1523).
229
- this._afterNavHandler = function() {
230
- // Backstop for the original bug (epic #2148): if the dialog is somehow
231
- // still open after an SPA body swap (e.g. a nav path that bypassed the
232
- // result-click handler), close it so it does not linger / flash over the
233
- // newly-swapped page. Safe no-op when already closed.
234
- if (self._dialog && self._dialog.open) self.closeDialog();
235
- var kbdEl2 = self.querySelector("[data-kbd-shortcut]");
236
- if (kbdEl2) kbdEl2.textContent = self._shortcut;
237
- };
238
- document.addEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
239
- }
240
-
241
- disconnectedCallback() {
242
- if (this._keydownHandler) {
243
- document.removeEventListener("keydown", this._keydownHandler);
244
- this._keydownHandler = null;
245
- }
246
- if (this._afterNavHandler) {
247
- document.removeEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
248
- this._afterNavHandler = null;
249
- }
250
- if (this._resultsClickHandler && this._results) {
251
- this._results.removeEventListener("click", this._resultsClickHandler);
252
- this._resultsClickHandler = null;
253
- }
254
- this.teardownSentinel();
255
- }
256
-
257
- openDialog() {
258
- if (!this._dialog) return;
259
- document.documentElement.style.overflow = "hidden";
260
- this._dialog.showModal();
261
- if (this._input) {
262
- this._input.focus();
263
- this._input.select();
264
- }
265
- if (!this._entries && !this._loading) {
266
- this.loadIndex();
267
- }
268
- }
269
-
270
- closeDialog() {
271
- if (!this._dialog) return;
272
- this._dialog.close();
273
- document.documentElement.style.overflow = "";
274
- }
275
-
276
- handleInput() {
277
- var self = this;
278
- if (this._debounce) clearTimeout(this._debounce);
279
- this._debounce = setTimeout(function() { self.search(); }, 150);
280
- }
281
-
282
- loadIndex() {
283
- if (this._loading) return;
284
- this._loading = true;
285
- var self = this;
286
- var base = this.dataset.base || "/";
287
- fetch(base + "search-index.json")
288
- .then(function(r) {
289
- if (!r.ok) throw new Error("HTTP " + r.status);
290
- return r.json();
291
- })
292
- .then(function(data) {
293
- self._entries = Array.isArray(data) ? data : (data.entries || []);
294
- prepareLc(self._entries);
295
- self._loading = false;
296
- // Clear the unavailable flag BEFORE re-running search so a successful
297
- // retry (e.g. via the openDialog() reload path) fully recovers (#2062).
298
- self._indexUnavailable = false;
299
- // If user already typed, search now
300
- if (self._input && self._input.value.trim()) {
301
- self.search();
302
- }
303
- })
304
- .catch(function() {
305
- self._loading = false;
306
- self._indexUnavailable = true;
307
- if (self._results) {
308
- self._results.innerHTML = "<p class=\\"text-small text-muted\\">Search unavailable</p>";
309
- }
310
- });
311
- }
312
-
313
- search() {
314
- var query = this._input ? this._input.value.trim() : "";
315
- this._currentQuery = query;
316
-
317
- if (!query) {
318
- this.teardownSentinel();
319
- this._allResults = [];
320
- this._shownCount = 0;
321
- if (this._results) this._results.innerHTML = this.placeholderHtml();
322
- this.updateCount();
323
- return;
324
- }
325
-
326
- if (!this._entries) {
327
- // Index failed to load: show the terminal "Search unavailable" state and
328
- // stop — do NOT show "Loading search index…" or refetch on every
329
- // keystroke (#2062). The openDialog() reload path is the intended retry
330
- // trigger. Clear any stale result state/count/sentinel first.
331
- if (this._indexUnavailable) {
332
- this.teardownSentinel();
333
- this._allResults = [];
334
- this._shownCount = 0;
335
- if (this._results) {
336
- this._results.innerHTML = "<p class=\\"text-small text-muted\\">Search unavailable</p>";
337
- }
338
- this.updateCount();
339
- return;
340
- }
341
- if (this._results) {
342
- this._results.innerHTML = "<p class=\\"text-small text-muted\\">Loading search index\\u2026</p>";
343
- }
344
- if (!this._loading) this.loadIndex();
345
- return;
346
- }
347
-
348
- // Lowercase the query terms once here so scoreEntry() can do plain
349
- // indexOf() against pre-lowercased entry fields without repeating
350
- // toLowerCase() across the entire index on every keystroke.
351
- var terms = parseTerms(query).map(function(t) { return t.toLowerCase(); });
352
- var scored = [];
353
- for (var i = 0; i < this._entries.length; i++) {
354
- var s = scoreEntry(this._entries[i], terms);
355
- if (s > 0) scored.push({ entry: this._entries[i], score: s });
356
- }
357
- scored.sort(function(a, b) { return b.score - a.score; });
358
- this._allResults = scored;
359
- this._shownCount = 0;
360
- this.teardownSentinel();
361
- this.updateCount();
362
-
363
- if (!scored.length) {
364
- if (this._results) {
365
- this._results.innerHTML = "<p class=\\"text-small text-muted\\">No results found.</p>";
366
- }
367
- return;
368
- }
369
-
370
- if (this._results) this._results.innerHTML = "";
371
- this.loadMore();
372
- if (this._shownCount < this._allResults.length) {
373
- this.setupSentinel();
374
- }
375
- }
376
-
377
- loadMore() {
378
- if (this._isLoadingBatch) return;
379
- if (this._shownCount >= this._allResults.length) return;
380
- this._isLoadingBatch = true;
381
- try {
382
- var batch = this._allResults.slice(this._shownCount, this._shownCount + PAGE_SIZE);
383
- var self = this;
384
- for (var i = 0; i < batch.length; i++) {
385
- var article = self.renderResult(batch[i].entry);
386
- if (self._sentinel && self._sentinel.parentNode === self._results) {
387
- self._results.insertBefore(article, self._sentinel);
388
- } else if (self._results) {
389
- self._results.appendChild(article);
390
- }
391
- }
392
- this._shownCount += batch.length;
393
- if (this._shownCount >= this._allResults.length) {
394
- this.teardownSentinel();
395
- }
396
- } finally {
397
- this._isLoadingBatch = false;
398
- }
399
- }
400
-
401
- setupSentinel() {
402
- this.teardownSentinel();
403
- if (!this._results) return;
404
- var sentinel = document.createElement("div");
405
- sentinel.setAttribute("data-search-sentinel", "");
406
- sentinel.setAttribute("aria-hidden", "true");
407
- sentinel.style.height = "1px";
408
- sentinel.style.width = "100%";
409
- this._results.appendChild(sentinel);
410
- this._sentinel = sentinel;
411
- var self = this;
412
- this._observer = new IntersectionObserver(function(entries) {
413
- for (var i = 0; i < entries.length; i++) {
414
- if (entries[i].isIntersecting) self.loadMore();
415
- }
416
- }, { root: this._results, rootMargin: "200px 0px" });
417
- this._observer.observe(sentinel);
418
- }
419
-
420
- teardownSentinel() {
421
- if (this._observer) { this._observer.disconnect(); this._observer = null; }
422
- if (this._sentinel) { this._sentinel.remove(); this._sentinel = null; }
423
- }
424
-
425
- updateCount() {
426
- var count = this._allResults.length;
427
- var template = this._resultCountTemplate;
428
- var text = count > 0 ? template.replace("{count}", String(count)) : "";
429
- var show = !!text;
430
- if (this._countWide) {
431
- this._countWide.textContent = text;
432
- this._countWide.classList.toggle("hidden", !show);
433
- }
434
- if (this._countNarrow) {
435
- this._countNarrow.textContent = text;
436
- this._countNarrow.classList.toggle("hidden", !show);
437
- }
438
- }
439
-
440
- placeholderHtml() {
441
- return this._placeholderHtml;
442
- }
443
-
444
- renderResult(entry) {
445
- var article = document.createElement("article");
446
- article.className = "-mx-hsp-lg border-b border-muted";
447
- var link = document.createElement("a");
448
- link.href = safeHref(entry.url);
449
- link.className =
450
- "group block px-hsp-lg py-vsp-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent";
451
- var title = document.createElement("span");
452
- title.className =
453
- "font-semibold text-fg group-hover:text-accent group-hover:underline group-focus-visible:underline";
454
- var terms = parseTerms(this._currentQuery);
455
- title.innerHTML = highlightTerms(entry.title || "", terms);
456
- link.appendChild(title);
457
- var text = entry.description || entry.body;
458
- if (text) {
459
- var excerpt = document.createElement("p");
460
- excerpt.className =
461
- "mt-vsp-2xs text-caption text-muted leading-relaxed group-hover:underline group-focus-visible:underline decoration-muted";
462
- var truncated = truncate(text, this._currentQuery, 200);
463
- excerpt.innerHTML = highlightTerms(truncated, terms);
464
- link.appendChild(excerpt);
465
- }
466
- article.appendChild(link);
467
- return article;
468
- }
469
- });
470
- })();`;
1
+ // Host thin-stub see @takazudo/zudo-doc/search-widget-script (epic #2344, S5).
2
+ export { SEARCH_WIDGET_SCRIPT } from "@takazudo/zudo-doc/search-widget-script";
@@ -1,203 +1,17 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // SSR-friendly search widget for the zfb host header.
4
- //
5
- // SSR search widget for the zfb host header. The key SSR requirement is that
6
- // the placeholder text ("Type to search..." / 「検索したい単語を入力」) and
7
- // the keyboard-shortcut hint ("to open search from anywhere" /
8
- // 「いつでも検索バーを開ける」) appear in the static HTML so no-JS users
9
- // and crawlers can see them.
10
- //
11
- // Architecture:
12
- // - Pure SSR markup for the dialog structure + placeholder text.
13
- // - A `<site-search>` custom element wraps everything; the inline script
14
- // at the bottom of this module registers the element and handles:
15
- // * Dialog open / close (button click, backdrop click, Escape key)
16
- // * Platform keyboard-shortcut label (⌘K / Ctrl+K) written into
17
- // `[data-kbd-shortcut]` on first `connectedCallback`
18
- // * Search-index loading + MiniSearch query dispatch (lazy-loaded
19
- // after the user types the first character)
20
- // - Locale-aware strings are passed as props from the caller so the
21
- // Japanese placeholder / shortcut-hint copy appears in JA-locale SSR
22
- // without JS execution.
23
- //
24
- // Coordination note: this file is inserted into the header via
25
- // `_header-with-defaults.tsx`'s `search` slot prop. It is self-contained
26
- // so B-10-2 (version-switcher) can touch the header file without conflict.
27
-
3
+ // Host thin-stub — see @takazudo/zudo-doc/search-widget (epic #2344, S5).
4
+ // Injects the host's base path and re-exports the package SearchWidget.
28
5
  import type { JSX } from "preact";
6
+ import {
7
+ SearchWidget as PackageSearchWidget,
8
+ type SearchWidgetProps as PackageSearchWidgetProps,
9
+ } from "@takazudo/zudo-doc/search-widget";
29
10
  import { withBase } from "@/utils/base";
30
- import { SEARCH_WIDGET_SCRIPT } from "./_search-widget-script.js";
31
11
 
32
- export interface SearchWidgetProps {
33
- /** Locale-aware placeholder: "Type to search..." / 「検索したい単語を入力」 */
34
- placeholderText: string;
35
- /** Locale-aware shortcut hint: "to open search from anywhere" / 「いつでも検索バーを開ける」 */
36
- shortcutHint: string;
37
- /** Locale-aware result count template, e.g. "{count} results" */
38
- resultCountTemplate: string;
39
- /** Accessible label for the search trigger button */
40
- searchLabel: string;
41
- }
12
+ export type SearchWidgetProps = Omit<PackageSearchWidgetProps, "base">;
42
13
 
43
- /**
44
- * Search trigger button + dialog widget.
45
- *
46
- * Pure SSR — renders the full dialog markup including the placeholder text
47
- * so static HTML contains the required copy even before JS runs.
48
- * The `<site-search>` custom element registers itself via the inline script
49
- * and activates interactive behaviour (open/close/keyboard shortcut/MiniSearch)
50
- * only on the client.
51
- */
14
+ /** Locale-aware search widget — thin host stub that injects the base path. */
52
15
  export function SearchWidget(props: SearchWidgetProps): JSX.Element {
53
- const { placeholderText, shortcutHint, resultCountTemplate, searchLabel } = props;
54
- const base = withBase("/");
55
-
56
- return (
57
- <>
58
- {/* @ts-expect-error site-search is a custom element not in JSX intrinsics */}
59
- <site-search
60
- data-base={base}
61
- data-result-count-template={resultCountTemplate}
62
- >
63
- {/* Header trigger button — search magnifier icon */}
64
- <button
65
- data-open-search
66
- type="button"
67
- class="flex items-center justify-center text-muted transition-colors hover:text-fg"
68
- aria-label={searchLabel}
69
- >
70
- <svg
71
- xmlns="http://www.w3.org/2000/svg"
72
- width="22"
73
- height="22"
74
- viewBox="0 0 24 24"
75
- fill="none"
76
- stroke="currentColor"
77
- stroke-width="2"
78
- stroke-linecap="round"
79
- stroke-linejoin="round"
80
- aria-hidden="true"
81
- >
82
- <circle cx="11" cy="11" r="8" />
83
- <path d="m21 21-4.3-4.3" />
84
- </svg>
85
- </button>
86
-
87
- {/* Search dialog — rendered in SSR so the placeholder text is in
88
- static HTML for no-JS users and crawlers. The browser treats
89
- it as a closed <dialog> until the custom element calls
90
- showModal(). */}
91
- {/* z-modal / backdrop:z-modal-backdrop are defense-in-depth for the
92
- SPA-swap window (zfb Strategy-B `zfb:after-swap`): a native
93
- showModal() dialog normally sits in the top layer, but if it is
94
- still open while the page body is swapped it can lose top-layer
95
- promotion and fall back to z-index:auto, flashing behind the
96
- header/sidebar. The explicit modal-tier z-index keeps it above all
97
- chrome during that window. Intentionally redundant in the normal
98
- (top-layer) case — do not remove as "redundant" (epic #2148). */}
99
- <dialog
100
- data-search-dialog
101
- class="z-modal m-0 h-full w-full max-w-none border-none bg-transparent p-0 backdrop:z-modal-backdrop backdrop:bg-overlay/60 sm:mx-auto sm:my-[10vh] sm:h-auto sm:max-h-[80vh] sm:max-w-[52rem] sm:rounded-lg"
102
- >
103
- <div class="flex h-full flex-col overflow-hidden bg-surface sm:rounded-lg sm:border sm:border-muted">
104
- {/* ── Dialog header (input row) ─────────────────────────── */}
105
- <div class="flex items-center gap-hsp-sm border-b border-muted px-hsp-lg py-vsp-sm">
106
- {/* Small search icon inside the input area */}
107
- <svg
108
- xmlns="http://www.w3.org/2000/svg"
109
- width="16"
110
- height="16"
111
- viewBox="0 0 24 24"
112
- fill="none"
113
- stroke="currentColor"
114
- stroke-width="2"
115
- stroke-linecap="round"
116
- stroke-linejoin="round"
117
- class="shrink-0 text-muted"
118
- aria-hidden="true"
119
- >
120
- <circle cx="11" cy="11" r="8" />
121
- <path d="m21 21-4.3-4.3" />
122
- </svg>
123
- <input
124
- data-search-input
125
- type="text"
126
- placeholder={placeholderText}
127
- class="w-full bg-transparent text-body text-fg outline-none placeholder:text-muted"
128
- autocomplete="off"
129
- spellcheck={false}
130
- />
131
- {/* Wide-viewport hit count (hidden until results arrive) */}
132
- <span
133
- data-search-count
134
- class="hidden shrink-0 text-caption text-muted"
135
- aria-live="polite"
136
- />
137
- <button
138
- data-close-search
139
- type="button"
140
- class="shrink-0 text-muted hover:text-fg"
141
- aria-label="Close search"
142
- >
143
- <svg
144
- xmlns="http://www.w3.org/2000/svg"
145
- width="20"
146
- height="20"
147
- viewBox="0 0 24 24"
148
- fill="none"
149
- stroke="currentColor"
150
- stroke-width="2"
151
- stroke-linecap="round"
152
- stroke-linejoin="round"
153
- >
154
- <path d="M18 6 6 18" />
155
- <path d="m6 6 12 12" />
156
- </svg>
157
- </button>
158
- </div>
159
-
160
- {/* Narrow-viewport hit count (below header, hidden on sm+) */}
161
- <span
162
- data-search-count-narrow
163
- class="hidden border-b border-muted px-hsp-lg py-vsp-xs text-caption text-muted"
164
- aria-live="polite"
165
- />
166
-
167
- {/* ── Results area ──────────────────────────────────────── */}
168
- {/* aria-live="polite" so screen readers announce result changes */}
169
- <div
170
- class="flex-1 overflow-y-auto px-hsp-lg pb-vsp-md"
171
- data-search-results
172
- aria-live="polite"
173
- >
174
- {/* Placeholder text — always in SSR HTML so no-JS users and
175
- crawlers see the placeholderText and shortcutHint strings
176
- in the static page source. */}
177
- <div class="text-small text-muted" data-search-placeholder>
178
- <p>{placeholderText}</p>
179
- <p class="mt-vsp-md text-caption">
180
- {/* data-kbd-shortcut is populated by the custom element on
181
- connectedCallback() with the platform shortcut string
182
- (⌘K on Mac, Ctrl+K elsewhere). Empty in SSR. */}
183
- <kbd
184
- class="rounded border border-muted bg-bg px-hsp-2xs py-[2px] font-mono text-caption"
185
- data-kbd-shortcut
186
- />{" "}
187
- {shortcutHint}
188
- </p>
189
- </div>
190
- </div>
191
- </div>
192
- </dialog>
193
- {/* @ts-expect-error closing custom element tag */}
194
- </site-search>
195
-
196
- {/* Inline script registers the SiteSearch custom element. Emitted once
197
- per page — the browser deduplicates same-id custom elements
198
- automatically; the `customElements.define` call below guards against
199
- double-registration with the `!customElements.get(...)` check. */}
200
- <script dangerouslySetInnerHTML={{ __html: SEARCH_WIDGET_SCRIPT }} />
201
- </>
202
- );
16
+ return <PackageSearchWidget {...props} base={withBase("/")} />;
203
17
  }