@takazudo/zudo-doc 5.5.2 → 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.
- package/CHANGELOG.md +36 -0
- package/README.md +39 -0
- package/dist/chrome/derive.d.ts +46 -0
- package/dist/chrome/derive.js +6 -2
- package/dist/config-assertions/index.d.ts +31 -0
- package/dist/config-assertions/index.js +24 -0
- package/dist/config.d.ts +23 -2
- package/dist/config.js +3 -0
- package/dist/current-path/index.d.ts +27 -0
- package/dist/current-path/index.js +11 -0
- package/dist/design-token-panel-bootstrap.d.ts +89 -23
- package/dist/design-token-panel-bootstrap.js +19 -6
- package/dist/doc-page-props/index.d.ts +5 -67
- package/dist/doc-route-entries/index.d.ts +10 -95
- package/dist/doc-route-entries/index.js +1 -78
- package/dist/doc-route-paths/index.d.ts +1 -1
- package/dist/head-with-defaults/index.d.ts +3 -1
- package/dist/head-with-defaults/index.js +76 -4
- package/dist/header/nav-active.d.ts +28 -0
- package/dist/header/nav-active.js +2 -1
- package/dist/header/nav-overflow-script.js +30 -17
- package/dist/header-with-defaults/index.js +11 -2
- package/dist/i18n-version/language-switcher.d.ts +6 -0
- package/dist/i18n-version/language-switcher.js +3 -1
- package/dist/i18n-version/version-switcher.d.ts +6 -0
- package/dist/i18n-version/version-switcher.js +3 -1
- package/dist/nav-source-docs/index.d.ts +7 -11
- package/dist/plugins/route-pages-candidates.d.ts +19 -0
- package/dist/plugins/route-pages-candidates.js +17 -0
- package/dist/plugins/routes.d.ts +46 -0
- package/dist/plugins/routes.js +72 -18
- package/dist/preset.d.ts +12 -1
- package/dist/preset.js +2 -0
- package/dist/route-context/index.js +2 -2
- package/dist/routes/_chrome.d.ts +1 -1
- package/dist/routes/_chrome.js +4 -0
- package/dist/routes/_context.d.ts +3 -3
- package/dist/routes/_design-token-panel-bootstrap.d.ts +18 -0
- package/dist/routes/_design-token-panel-bootstrap.js +11 -0
- package/dist/routes/_docs-helpers.d.ts +1 -36
- package/dist/routes/_docs-helpers.js +0 -138
- package/dist/safelist.css +1 -1
- package/dist/search-widget-script/generated-script.d.ts +8 -0
- package/dist/search-widget-script/generated-script.js +465 -0
- package/dist/search-widget-script/index.d.ts +1 -18
- package/dist/search-widget-script/index.js +1 -443
- package/dist/settings.d.ts +82 -1
- package/dist/sidebar-tree/category-meta.d.ts +9 -0
- package/dist/sidebar-tree/category-meta.js +21 -12
- package/dist/sidebar-tree-island/index.d.ts +8 -1
- package/dist/sidebar-tree-island/index.js +16 -14
- package/dist/site-schema/doc-route-entries.d.ts +89 -0
- package/dist/site-schema/doc-route-entries.js +83 -0
- package/dist/site-schema/index.d.ts +17 -0
- package/dist/site-schema/index.js +46 -0
- package/dist/site-schema/nav-tree.d.ts +28 -0
- package/dist/site-schema/nav-tree.js +138 -0
- package/dist/site-schema/types.d.ts +97 -0
- package/dist/site-schema/types.js +0 -0
- package/dist/theme/theme-pack-provider.d.ts +34 -3
- package/dist/theme/theme-pack-provider.js +30 -2
- package/eject/header/nav-active.ts +13 -1
- package/eject/header/nav-overflow-script.ts +30 -17
- package/eject/sidebar-tree-island/index.tsx +44 -20
- package/package.json +22 -12
- package/routes-src/_chrome.tsx +21 -9
- package/routes-src/_design-token-panel-bootstrap.tsx +63 -0
- package/routes-src/_docs-helpers.ts +18 -225
- package/routes-src/_virtual.d.ts +5 -2
- package/virtual-modules.d.ts +5 -2
|
@@ -1,446 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { prepareLc, scoreEntry } from "./scoring.js";
|
|
3
|
-
const SEARCH_WIDGET_SCRIPT = (
|
|
4
|
-
/* javascript */
|
|
5
|
-
`(function () {
|
|
6
|
-
if (customElements.get("site-search")) return; // guard double-registration
|
|
7
|
-
|
|
8
|
-
var PAGE_SIZE = 10;
|
|
9
|
-
|
|
10
|
-
// Allowlist-based href sanitizer: only relative paths and http(s) URLs are
|
|
11
|
-
// permitted. Anything else (e.g. javascript:, data:) falls back to "#" so a
|
|
12
|
-
// malicious entry in search-index.json cannot turn a result link into a
|
|
13
|
-
// script-injection vector.
|
|
14
|
-
function safeHref(url) {
|
|
15
|
-
if (!url) return "#";
|
|
16
|
-
var s = String(url);
|
|
17
|
-
if (s.startsWith("/") || s.startsWith("http://") || s.startsWith("https://")) {
|
|
18
|
-
return s;
|
|
19
|
-
}
|
|
20
|
-
return "#";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function escapeHtml(text) {
|
|
24
|
-
return String(text)
|
|
25
|
-
.replace(/&/g, "&")
|
|
26
|
-
.replace(/</g, "<")
|
|
27
|
-
.replace(/>/g, ">")
|
|
28
|
-
.replace(/"/g, """)
|
|
29
|
-
.replace(/'/g, "'");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function escapeRegExp(text) {
|
|
33
|
-
return text.replace(/[.*+?^\${}()|[\\]\\\\]/g, "\\\\$&");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function parseTerms(query) {
|
|
37
|
-
return query.trim().split(/\\s+/).filter(Boolean);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// scoreEntry / prepareLc: embedded verbatim (via Function.prototype.toString())
|
|
41
|
-
// from the real, unit-tested implementation in ./scoring.ts \u2014 see the module
|
|
42
|
-
// header comment there and search-widget-script/__tests__/scoring.test.ts.
|
|
43
|
-
// This keeps the shipped inline script and the tested source identical by
|
|
44
|
-
// construction instead of relying on a hand-copied mirror staying in sync.
|
|
45
|
-
${prepareLc.toString()}
|
|
46
|
-
|
|
47
|
-
${scoreEntry.toString()}
|
|
48
|
-
|
|
49
|
-
function highlightTerms(text, terms) {
|
|
50
|
-
if (!terms.length) return escapeHtml(text);
|
|
51
|
-
var escaped = terms.map(function(t) { return escapeRegExp(t); });
|
|
52
|
-
var pattern = new RegExp("(" + escaped.join("|") + ")", "gi");
|
|
53
|
-
return text.split(pattern).map(function(seg, i) {
|
|
54
|
-
return i % 2 === 1
|
|
55
|
-
? "<mark>" + escapeHtml(seg) + "</mark>"
|
|
56
|
-
: escapeHtml(seg);
|
|
57
|
-
}).join("");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function truncate(text, query, max) {
|
|
61
|
-
max = max || 200;
|
|
62
|
-
if (text.length <= max) return text;
|
|
63
|
-
var terms = parseTerms(query);
|
|
64
|
-
var lower = text.toLowerCase();
|
|
65
|
-
var best = -1;
|
|
66
|
-
for (var i = 0; i < terms.length; i++) {
|
|
67
|
-
var idx = lower.indexOf(terms[i].toLowerCase());
|
|
68
|
-
if (idx !== -1 && (best === -1 || idx < best)) best = idx;
|
|
69
|
-
}
|
|
70
|
-
if (best === -1) return text.slice(0, max) + "\\u2026";
|
|
71
|
-
var half = Math.floor(max / 2);
|
|
72
|
-
var start = Math.max(0, best - half);
|
|
73
|
-
var end = start + max;
|
|
74
|
-
if (end > text.length) { end = text.length; start = Math.max(0, end - max); }
|
|
75
|
-
var result = text.slice(start, end);
|
|
76
|
-
if (start > 0) result = "\\u2026" + result;
|
|
77
|
-
if (end < text.length) result += "\\u2026";
|
|
78
|
-
return result;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
customElements.define("site-search", class SiteSearch extends HTMLElement {
|
|
82
|
-
constructor() {
|
|
83
|
-
super();
|
|
84
|
-
this._dialog = null;
|
|
85
|
-
this._openBtn = null;
|
|
86
|
-
this._closeBtn = null;
|
|
87
|
-
this._input = null;
|
|
88
|
-
this._results = null;
|
|
89
|
-
this._countWide = null;
|
|
90
|
-
this._countNarrow = null;
|
|
91
|
-
this._entries = null;
|
|
92
|
-
this._loading = false;
|
|
93
|
-
this._indexUnavailable = false;
|
|
94
|
-
this._debounce = null;
|
|
95
|
-
this._currentQuery = "";
|
|
96
|
-
this._allResults = [];
|
|
97
|
-
this._shownCount = 0;
|
|
98
|
-
this._shortcut = "";
|
|
99
|
-
this._resultCountTemplate = "";
|
|
100
|
-
// Locale-aware status strings (threaded from the SSR component via
|
|
101
|
-
// data-* attributes, same mechanism as _resultCountTemplate). English
|
|
102
|
-
// literals below in connectedCallback are fallbacks only.
|
|
103
|
-
this._searchUnavailable = "";
|
|
104
|
-
this._loadingIndex = "";
|
|
105
|
-
this._noResults = "";
|
|
106
|
-
this._keydownHandler = null;
|
|
107
|
-
// Delegated click handler on the results container: closing the dialog
|
|
108
|
-
// when a result link is activated (epic #2148). Held so disconnectedCallback
|
|
109
|
-
// can detach it on body swap.
|
|
110
|
-
this._resultsClickHandler = null;
|
|
111
|
-
this._observer = null;
|
|
112
|
-
this._sentinel = null;
|
|
113
|
-
this._isLoadingBatch = false;
|
|
114
|
-
// Snapshot of the initial results-area HTML (includes SSR placeholder).
|
|
115
|
-
// Captured in connectedCallback so we can restore it on input-clear
|
|
116
|
-
// without re-querying the DOM (the placeholder node is replaced once
|
|
117
|
-
// search results are rendered).
|
|
118
|
-
this._placeholderHtml = "";
|
|
119
|
-
// Held so we can remove the document-level after-navigate listener
|
|
120
|
-
// in disconnectedCallback. zudolab/zudo-doc#1523 \u2014 under Strategy B
|
|
121
|
-
// SPA navigation a non-persisted <site-search> element would leak
|
|
122
|
-
// one document listener per nav without this hook.
|
|
123
|
-
this._afterNavHandler = null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
connectedCallback() {
|
|
127
|
-
this._dialog = this.querySelector("[data-search-dialog]");
|
|
128
|
-
this._openBtn = this.querySelector("[data-open-search]");
|
|
129
|
-
this._closeBtn = this.querySelector("[data-close-search]");
|
|
130
|
-
this._input = this.querySelector("[data-search-input]");
|
|
131
|
-
this._results = this.querySelector("[data-search-results]");
|
|
132
|
-
this._countWide = this.querySelector("[data-search-count]");
|
|
133
|
-
this._countNarrow = this.querySelector("[data-search-count-narrow]");
|
|
134
|
-
this._resultCountTemplate = this.dataset.resultCountTemplate || "{count} results";
|
|
135
|
-
this._searchUnavailable = this.dataset.searchUnavailable || "Search unavailable";
|
|
136
|
-
this._loadingIndex = this.dataset.loadingIndex || "Loading search index\\u2026";
|
|
137
|
-
this._noResults = this.dataset.noResults || "No results found.";
|
|
138
|
-
// Snapshot the placeholder HTML before any search renders overwrite it.
|
|
139
|
-
this._placeholderHtml = this._results ? this._results.innerHTML : "";
|
|
140
|
-
|
|
141
|
-
// Platform keyboard-shortcut label \u2014 injected into [data-kbd-shortcut]
|
|
142
|
-
var nav = navigator;
|
|
143
|
-
var isMac = /Mac|iPhone|iPad|iPod/.test(
|
|
144
|
-
(nav.userAgentData && nav.userAgentData.platform) || nav.userAgent
|
|
145
|
-
);
|
|
146
|
-
this._shortcut = isMac ? "\\u2318K" : "Ctrl+K";
|
|
147
|
-
var kbdEl = this.querySelector("[data-kbd-shortcut]");
|
|
148
|
-
if (kbdEl) kbdEl.textContent = this._shortcut;
|
|
149
|
-
|
|
150
|
-
// Wire open/close handlers
|
|
151
|
-
var self = this;
|
|
152
|
-
if (this._openBtn) {
|
|
153
|
-
this._openBtn.addEventListener("click", function() { self.openDialog(); });
|
|
154
|
-
}
|
|
155
|
-
if (this._closeBtn) {
|
|
156
|
-
this._closeBtn.addEventListener("click", function() { self.closeDialog(); });
|
|
157
|
-
}
|
|
158
|
-
if (this._dialog) {
|
|
159
|
-
this._dialog.addEventListener("close", function() {
|
|
160
|
-
document.documentElement.style.overflow = "";
|
|
161
|
-
});
|
|
162
|
-
this._dialog.addEventListener("click", function(e) {
|
|
163
|
-
if (e.target === self._dialog) self.closeDialog();
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
if (this._input) {
|
|
167
|
-
this._input.addEventListener("input", function() { self.handleInput(); });
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Close-on-result-click (epic #2148): result links are created dynamically
|
|
171
|
-
// in renderResult(), so use one delegated listener on the results container
|
|
172
|
-
// instead of per-link handlers. We do NOT preventDefault \u2014 the link's own
|
|
173
|
-
// navigation (zfb Strategy-B SPA swap or a plain load) must still proceed;
|
|
174
|
-
// we only close the <dialog> so it does not linger over the swapped page.
|
|
175
|
-
// closeDialog() runs synchronously before navigation; the dialog's close
|
|
176
|
-
// restores documentElement overflow via the existing "close" listener.
|
|
177
|
-
if (this._results) {
|
|
178
|
-
this._resultsClickHandler = function(e) {
|
|
179
|
-
var t = e.target;
|
|
180
|
-
while (t && t !== self._results) {
|
|
181
|
-
if (t.tagName === "A") { self.closeDialog(); return; }
|
|
182
|
-
t = t.parentNode;
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
this._results.addEventListener("click", this._resultsClickHandler);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Global keyboard shortcut (\u2318K / Ctrl+K to open)
|
|
189
|
-
this._keydownHandler = function(e) {
|
|
190
|
-
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
|
191
|
-
e.preventDefault();
|
|
192
|
-
self.openDialog();
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
document.addEventListener("keydown", this._keydownHandler);
|
|
196
|
-
|
|
197
|
-
// View-Transitions compat: re-run on the v2 after-navigate event.
|
|
198
|
-
// Stored on the instance so disconnectedCallback can detach it on
|
|
199
|
-
// body swap when this element is NOT persisted via
|
|
200
|
-
// data-zfb-transition-persist (zudolab/zudo-doc#1523).
|
|
201
|
-
this._afterNavHandler = function() {
|
|
202
|
-
// Backstop for the original bug (epic #2148): if the dialog is somehow
|
|
203
|
-
// still open after an SPA body swap (e.g. a nav path that bypassed the
|
|
204
|
-
// result-click handler), close it so it does not linger / flash over the
|
|
205
|
-
// newly-swapped page. Safe no-op when already closed.
|
|
206
|
-
if (self._dialog && self._dialog.open) self.closeDialog();
|
|
207
|
-
var kbdEl2 = self.querySelector("[data-kbd-shortcut]");
|
|
208
|
-
if (kbdEl2) kbdEl2.textContent = self._shortcut;
|
|
209
|
-
};
|
|
210
|
-
document.addEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
disconnectedCallback() {
|
|
214
|
-
if (this._keydownHandler) {
|
|
215
|
-
document.removeEventListener("keydown", this._keydownHandler);
|
|
216
|
-
this._keydownHandler = null;
|
|
217
|
-
}
|
|
218
|
-
if (this._afterNavHandler) {
|
|
219
|
-
document.removeEventListener(${JSON.stringify(AFTER_NAVIGATE_EVENT)}, this._afterNavHandler);
|
|
220
|
-
this._afterNavHandler = null;
|
|
221
|
-
}
|
|
222
|
-
if (this._resultsClickHandler && this._results) {
|
|
223
|
-
this._results.removeEventListener("click", this._resultsClickHandler);
|
|
224
|
-
this._resultsClickHandler = null;
|
|
225
|
-
}
|
|
226
|
-
this.teardownSentinel();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
openDialog() {
|
|
230
|
-
if (!this._dialog) return;
|
|
231
|
-
document.documentElement.style.overflow = "hidden";
|
|
232
|
-
this._dialog.showModal();
|
|
233
|
-
if (this._input) {
|
|
234
|
-
this._input.focus();
|
|
235
|
-
this._input.select();
|
|
236
|
-
}
|
|
237
|
-
if (!this._entries && !this._loading) {
|
|
238
|
-
this.loadIndex();
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
closeDialog() {
|
|
243
|
-
if (!this._dialog) return;
|
|
244
|
-
this._dialog.close();
|
|
245
|
-
document.documentElement.style.overflow = "";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
handleInput() {
|
|
249
|
-
var self = this;
|
|
250
|
-
if (this._debounce) clearTimeout(this._debounce);
|
|
251
|
-
this._debounce = setTimeout(function() { self.search(); }, 150);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
loadIndex() {
|
|
255
|
-
if (this._loading) return;
|
|
256
|
-
this._loading = true;
|
|
257
|
-
var self = this;
|
|
258
|
-
var base = this.dataset.base || "/";
|
|
259
|
-
fetch(base + "search-index.json")
|
|
260
|
-
.then(function(r) {
|
|
261
|
-
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
262
|
-
return r.json();
|
|
263
|
-
})
|
|
264
|
-
.then(function(data) {
|
|
265
|
-
self._entries = Array.isArray(data) ? data : (data.entries || []);
|
|
266
|
-
prepareLc(self._entries);
|
|
267
|
-
self._loading = false;
|
|
268
|
-
// Clear the unavailable flag BEFORE re-running search so a successful
|
|
269
|
-
// retry (e.g. via the openDialog() reload path) fully recovers (#2062).
|
|
270
|
-
self._indexUnavailable = false;
|
|
271
|
-
// If user already typed, search now
|
|
272
|
-
if (self._input && self._input.value.trim()) {
|
|
273
|
-
self.search();
|
|
274
|
-
}
|
|
275
|
-
})
|
|
276
|
-
.catch(function() {
|
|
277
|
-
self._loading = false;
|
|
278
|
-
self._indexUnavailable = true;
|
|
279
|
-
if (self._results) {
|
|
280
|
-
self._results.innerHTML = "<p class=\\"text-small text-muted\\">" + escapeHtml(self._searchUnavailable) + "</p>";
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
search() {
|
|
286
|
-
var query = this._input ? this._input.value.trim() : "";
|
|
287
|
-
this._currentQuery = query;
|
|
288
|
-
|
|
289
|
-
if (!query) {
|
|
290
|
-
this.teardownSentinel();
|
|
291
|
-
this._allResults = [];
|
|
292
|
-
this._shownCount = 0;
|
|
293
|
-
if (this._results) this._results.innerHTML = this.placeholderHtml();
|
|
294
|
-
this.updateCount();
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!this._entries) {
|
|
299
|
-
// Index failed to load: show the terminal "Search unavailable" state and
|
|
300
|
-
// stop \u2014 do NOT show "Loading search index\u2026" or refetch on every
|
|
301
|
-
// keystroke (#2062). The openDialog() reload path is the intended retry
|
|
302
|
-
// trigger. Clear any stale result state/count/sentinel first.
|
|
303
|
-
if (this._indexUnavailable) {
|
|
304
|
-
this.teardownSentinel();
|
|
305
|
-
this._allResults = [];
|
|
306
|
-
this._shownCount = 0;
|
|
307
|
-
if (this._results) {
|
|
308
|
-
this._results.innerHTML = "<p class=\\"text-small text-muted\\">" + escapeHtml(this._searchUnavailable) + "</p>";
|
|
309
|
-
}
|
|
310
|
-
this.updateCount();
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (this._results) {
|
|
314
|
-
this._results.innerHTML = "<p class=\\"text-small text-muted\\">" + escapeHtml(this._loadingIndex) + "</p>";
|
|
315
|
-
}
|
|
316
|
-
if (!this._loading) this.loadIndex();
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Lowercase the query terms once here so scoreEntry() can do plain
|
|
321
|
-
// indexOf() against pre-lowercased entry fields without repeating
|
|
322
|
-
// toLowerCase() across the entire index on every keystroke.
|
|
323
|
-
var terms = parseTerms(query).map(function(t) { return t.toLowerCase(); });
|
|
324
|
-
var scored = [];
|
|
325
|
-
for (var i = 0; i < this._entries.length; i++) {
|
|
326
|
-
var s = scoreEntry(this._entries[i], terms);
|
|
327
|
-
if (s > 0) scored.push({ entry: this._entries[i], score: s });
|
|
328
|
-
}
|
|
329
|
-
scored.sort(function(a, b) { return b.score - a.score; });
|
|
330
|
-
this._allResults = scored;
|
|
331
|
-
this._shownCount = 0;
|
|
332
|
-
this.teardownSentinel();
|
|
333
|
-
this.updateCount();
|
|
334
|
-
|
|
335
|
-
if (!scored.length) {
|
|
336
|
-
if (this._results) {
|
|
337
|
-
this._results.innerHTML = "<p class=\\"text-small text-muted\\">" + escapeHtml(this._noResults) + "</p>";
|
|
338
|
-
}
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (this._results) this._results.innerHTML = "";
|
|
343
|
-
this.loadMore();
|
|
344
|
-
if (this._shownCount < this._allResults.length) {
|
|
345
|
-
this.setupSentinel();
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
loadMore() {
|
|
350
|
-
if (this._isLoadingBatch) return;
|
|
351
|
-
if (this._shownCount >= this._allResults.length) return;
|
|
352
|
-
this._isLoadingBatch = true;
|
|
353
|
-
try {
|
|
354
|
-
var batch = this._allResults.slice(this._shownCount, this._shownCount + PAGE_SIZE);
|
|
355
|
-
var self = this;
|
|
356
|
-
for (var i = 0; i < batch.length; i++) {
|
|
357
|
-
var article = self.renderResult(batch[i].entry);
|
|
358
|
-
if (self._sentinel && self._sentinel.parentNode === self._results) {
|
|
359
|
-
self._results.insertBefore(article, self._sentinel);
|
|
360
|
-
} else if (self._results) {
|
|
361
|
-
self._results.appendChild(article);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
this._shownCount += batch.length;
|
|
365
|
-
if (this._shownCount >= this._allResults.length) {
|
|
366
|
-
this.teardownSentinel();
|
|
367
|
-
}
|
|
368
|
-
} finally {
|
|
369
|
-
this._isLoadingBatch = false;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
setupSentinel() {
|
|
374
|
-
this.teardownSentinel();
|
|
375
|
-
if (!this._results) return;
|
|
376
|
-
var sentinel = document.createElement("div");
|
|
377
|
-
sentinel.setAttribute("data-search-sentinel", "");
|
|
378
|
-
sentinel.setAttribute("aria-hidden", "true");
|
|
379
|
-
sentinel.style.height = "1px";
|
|
380
|
-
sentinel.style.width = "100%";
|
|
381
|
-
this._results.appendChild(sentinel);
|
|
382
|
-
this._sentinel = sentinel;
|
|
383
|
-
var self = this;
|
|
384
|
-
this._observer = new IntersectionObserver(function(entries) {
|
|
385
|
-
for (var i = 0; i < entries.length; i++) {
|
|
386
|
-
if (entries[i].isIntersecting) self.loadMore();
|
|
387
|
-
}
|
|
388
|
-
}, { root: this._results, rootMargin: "200px 0px" });
|
|
389
|
-
this._observer.observe(sentinel);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
teardownSentinel() {
|
|
393
|
-
if (this._observer) { this._observer.disconnect(); this._observer = null; }
|
|
394
|
-
if (this._sentinel) { this._sentinel.remove(); this._sentinel = null; }
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
updateCount() {
|
|
398
|
-
var count = this._allResults.length;
|
|
399
|
-
var template = this._resultCountTemplate;
|
|
400
|
-
var text = count > 0 ? template.replace("{count}", String(count)) : "";
|
|
401
|
-
var show = !!text;
|
|
402
|
-
if (this._countWide) {
|
|
403
|
-
this._countWide.textContent = text;
|
|
404
|
-
this._countWide.classList.toggle("hidden", !show);
|
|
405
|
-
}
|
|
406
|
-
if (this._countNarrow) {
|
|
407
|
-
this._countNarrow.textContent = text;
|
|
408
|
-
this._countNarrow.classList.toggle("hidden", !show);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
placeholderHtml() {
|
|
413
|
-
return this._placeholderHtml;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
renderResult(entry) {
|
|
417
|
-
var article = document.createElement("article");
|
|
418
|
-
article.className = "-mx-hsp-lg border-b border-muted";
|
|
419
|
-
var link = document.createElement("a");
|
|
420
|
-
link.href = safeHref(entry.url);
|
|
421
|
-
link.className =
|
|
422
|
-
"group block px-hsp-lg py-vsp-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent";
|
|
423
|
-
var title = document.createElement("span");
|
|
424
|
-
title.className =
|
|
425
|
-
"font-semibold text-fg group-hover:text-accent group-hover:underline group-focus-visible:underline";
|
|
426
|
-
var terms = parseTerms(this._currentQuery);
|
|
427
|
-
title.innerHTML = highlightTerms(entry.title || "", terms);
|
|
428
|
-
link.appendChild(title);
|
|
429
|
-
var text = entry.description || entry.body;
|
|
430
|
-
if (text) {
|
|
431
|
-
var excerpt = document.createElement("p");
|
|
432
|
-
excerpt.className =
|
|
433
|
-
"mt-vsp-2xs text-caption text-muted leading-relaxed group-hover:underline group-focus-visible:underline decoration-muted";
|
|
434
|
-
var truncated = truncate(text, this._currentQuery, 200);
|
|
435
|
-
excerpt.innerHTML = highlightTerms(truncated, terms);
|
|
436
|
-
link.appendChild(excerpt);
|
|
437
|
-
}
|
|
438
|
-
article.appendChild(link);
|
|
439
|
-
return article;
|
|
440
|
-
}
|
|
441
|
-
});
|
|
442
|
-
})();`
|
|
443
|
-
);
|
|
1
|
+
import { SEARCH_WIDGET_SCRIPT } from "./generated-script.js";
|
|
444
2
|
export {
|
|
445
3
|
SEARCH_WIDGET_SCRIPT
|
|
446
4
|
};
|
package/dist/settings.d.ts
CHANGED
|
@@ -207,6 +207,26 @@ export interface MetaTagsConfig {
|
|
|
207
207
|
/** twitter:creator handle. Optional. */
|
|
208
208
|
twitterCreator?: string;
|
|
209
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Explicit favicon link set. Each key maps to one `<link rel="icon">` slot of
|
|
212
|
+
* the default four-file convention, and **only the supplied keys are emitted**
|
|
213
|
+
* — that is how a project whose `public/` has no `favicon.ico` stops shipping
|
|
214
|
+
* a 404ing link. Emission order is fixed (`svg → ico → png32 → png16`)
|
|
215
|
+
* regardless of key order in the object; `{}` emits nothing.
|
|
216
|
+
*
|
|
217
|
+
* Values starting with `/` are passed through `withBase()`; anything else
|
|
218
|
+
* (`data:`, `https://…`) is emitted verbatim. The `"auto"` sentinel is a
|
|
219
|
+
* whole-`favicon` value only — it is not meaningful inside this object. An
|
|
220
|
+
* empty-string slot (e.g. `{ ico: "" }`) throws a `TypeError` naming the
|
|
221
|
+
* slot at config resolution rather than being emitted (#3471) — `{}` (no
|
|
222
|
+
* slots at all) stays valid and emits nothing.
|
|
223
|
+
*/
|
|
224
|
+
export interface FaviconConfig {
|
|
225
|
+
svg?: string;
|
|
226
|
+
png32?: string;
|
|
227
|
+
png16?: string;
|
|
228
|
+
ico?: string;
|
|
229
|
+
}
|
|
210
230
|
/**
|
|
211
231
|
* Site-wide custom `<head>` extras injected into every page via
|
|
212
232
|
* {@link HeadWithDefaults}. All fields are JSON-serializable (no VNodes or
|
|
@@ -270,9 +290,24 @@ export interface Settings {
|
|
|
270
290
|
* Home-hero logo. `"auto"` (the default when omitted) renders a generated
|
|
271
291
|
* deterministic SVG seeded by `siteName`; a path string (e.g.
|
|
272
292
|
* `"/img/logo.svg"`) renders that asset as a theme-adaptive CSS mask;
|
|
273
|
-
* `false` hides the logo block entirely.
|
|
293
|
+
* `false` hides the logo block entirely. The empty string `""` throws a
|
|
294
|
+
* `TypeError` at config resolution instead of silently rendering a mask
|
|
295
|
+
* of an empty path — pass `false` or omit the field instead (#3471).
|
|
274
296
|
*/
|
|
275
297
|
logo?: string | false;
|
|
298
|
+
/**
|
|
299
|
+
* Favicon links. Omitted (the default) emits the four-file convention
|
|
300
|
+
* (`/favicon.svg`, `/favicon.ico`, `/favicon-32x32.png`,
|
|
301
|
+
* `/favicon-16x16.png`); `"auto"` emits one inline SVG data-URL icon
|
|
302
|
+
* generated from `siteName` (the same glyph `logo: "auto"` renders); any
|
|
303
|
+
* other string emits one link with the `type` inferred from its extension;
|
|
304
|
+
* a {@link FaviconConfig} object emits only the slots it supplies; `false`
|
|
305
|
+
* emits no favicon links at all. The empty string `""` (top-level or any
|
|
306
|
+
* `FaviconConfig` slot) throws a `TypeError` at config resolution instead
|
|
307
|
+
* of silently emitting `<link rel="icon" href="">`, which the HTML spec
|
|
308
|
+
* resolves to the current document — pass `false` or omit instead (#3471).
|
|
309
|
+
*/
|
|
310
|
+
favicon?: string | FaviconConfig | false;
|
|
276
311
|
base: string;
|
|
277
312
|
trailingSlash: boolean;
|
|
278
313
|
/** Package-owned home-page layout. Narrow when omitted. */
|
|
@@ -432,6 +467,52 @@ export interface Settings {
|
|
|
432
467
|
* Only the PATH travels through `settings` (a string is serializable
|
|
433
468
|
* data) — the bundler imports the actual builder from the re-exported
|
|
434
469
|
* module. Irrelevant when `designTokenPanel` is `false`.
|
|
470
|
+
*
|
|
471
|
+
* SCOPE (#3396, bounded by #3414): this setting applies only to pages
|
|
472
|
+
* rendered through the package-injected routes. A doc page rendered by a
|
|
473
|
+
* SELF-CONTAINED host `pages/` stub does not reach the configured island —
|
|
474
|
+
* and, since the panel is configured once per browser session, a stub page
|
|
475
|
+
* mounting the package-default bootstrap used to decide the config for the
|
|
476
|
+
* whole session whenever it was the hard-loaded entry page (#3406).
|
|
477
|
+
*
|
|
478
|
+
* So when this setting is set and the host supplies no explicit
|
|
479
|
+
* `chromeBindings.DesignTokenPanelBootstrap`, stub-rendered pages now mount
|
|
480
|
+
* NO panel island at all (the injected routes still get the configured one,
|
|
481
|
+
* from any entry page). To get a panel on stub-rendered pages too, thread
|
|
482
|
+
* your builder through `chromeBindings.DesignTokenPanelBootstrap` — that
|
|
483
|
+
* binding wins everywhere and is unaffected by this rule. See
|
|
484
|
+
* `docs/adr/route-injection-seam.md`.
|
|
485
|
+
*
|
|
486
|
+
* EDGE CASE (zudolab/zudo-doc#3420, diagnostic added #3428, scoped by
|
|
487
|
+
* #3434/#3435): a locked-manifest host whose kept `pages/` stubs happen to
|
|
488
|
+
* shadow every injected route a READER BROWSES (Decision 6 in
|
|
489
|
+
* `plugins/routes.ts` drops a collided injected route silently — user
|
|
490
|
+
* `pages/` always wins) gets no configured DTP island on any page of its
|
|
491
|
+
* documentation when this setting is set, with no build error.
|
|
492
|
+
* `plugins/routes.ts` now probes for exactly this at plugin setup — if every
|
|
493
|
+
* derived route tagged `includedInDtpShadowDiagnostic` resolves to an
|
|
494
|
+
* existing user `pages/` file, it emits a loud `ctx.logger.warn` naming this
|
|
495
|
+
* gap and the `chromeBindings.DesignTokenPanelBootstrap` workaround above.
|
|
496
|
+
* The denominator deliberately excludes `/404`, `/sitemap.xml`,
|
|
497
|
+
* `/robots.txt` and `/api/ai-chat` — none of them is a documentation page a
|
|
498
|
+
* reader browses, and the two non-HTML ones could never be shadowed by a
|
|
499
|
+
* minimal scaffold, which is what kept the original all-routes form of this
|
|
500
|
+
* check from ever firing (#3434).
|
|
501
|
+
*
|
|
502
|
+
* Three further suppressions: the diagnostic is silent unless
|
|
503
|
+
* `designTokenPanel` is `true` (with the feature off this setting is
|
|
504
|
+
* irrelevant, per the paragraph above — #3435); silent when the resolved
|
|
505
|
+
* `chromeBindingsModule` file already contains the literal token
|
|
506
|
+
* `DesignTokenPanelBootstrap` (a best-effort static text scan — an
|
|
507
|
+
* indirectly-composed override that never spells the name out keeps
|
|
508
|
+
* warning); and a `pages/` file that is an exact default re-export of the
|
|
509
|
+
* shadowed route's OWN package entrypoint
|
|
510
|
+
* (`export { default, paths, frontmatter } from "@takazudo/zudo-doc/routes/…"`)
|
|
511
|
+
* does not count as a shadow at all, because it still reaches the configured
|
|
512
|
+
* bootstrap through `routes/_chrome.tsx` (#3451 — also a best-effort text
|
|
513
|
+
* scan, so a file that reaches the same entrypoint some other way still
|
|
514
|
+
* counts as shadowed). Partial shadowing stays silent: the config still
|
|
515
|
+
* applies on whichever reader-facing routes survive.
|
|
435
516
|
*/
|
|
436
517
|
designTokenPanelConfigModule?: string;
|
|
437
518
|
/**
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous reader for `_category_.json` files. Walks a directory tree
|
|
3
|
+
* and returns a map of `<relative-dir-path>` → {@link CategoryMeta}.
|
|
4
|
+
*
|
|
5
|
+
* Lives in this folder rather than reusing the project's existing
|
|
6
|
+
* `loadCategoryMeta` because the framework-layer must not import anything
|
|
7
|
+
* from the consumer project (`@/utils/...`). Behaviour is intentionally
|
|
8
|
+
* identical to the original so call sites can swap implementations 1:1.
|
|
9
|
+
*/
|
|
1
10
|
import type { CategoryMeta } from "./types.js";
|
|
2
11
|
/**
|
|
3
12
|
* Scan `contentDir` recursively for `_category_.json` files. Each file's
|
|
@@ -1,24 +1,33 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
const cache = /* @__PURE__ */ new Map();
|
|
4
|
-
function
|
|
5
|
-
|
|
2
|
+
function resolveBuiltins() {
|
|
3
|
+
const getBuiltinModule = globalThis.process?.getBuiltinModule;
|
|
4
|
+
if (typeof getBuiltinModule !== "function") return void 0;
|
|
6
5
|
try {
|
|
7
|
-
|
|
6
|
+
const fs = getBuiltinModule("node:fs");
|
|
7
|
+
const path = getBuiltinModule("node:path");
|
|
8
|
+
if (!fs || !path) return void 0;
|
|
9
|
+
return { fs, path };
|
|
8
10
|
} catch {
|
|
9
|
-
|
|
11
|
+
return void 0;
|
|
10
12
|
}
|
|
13
|
+
}
|
|
14
|
+
function loadCategoryMeta(contentDir) {
|
|
15
|
+
const builtins = resolveBuiltins();
|
|
16
|
+
const absolute = builtins ? builtins.path.resolve(contentDir) : contentDir;
|
|
11
17
|
const cached = cache.get(absolute);
|
|
12
18
|
if (cached) return cached;
|
|
13
19
|
const result = /* @__PURE__ */ new Map();
|
|
14
|
-
|
|
20
|
+
if (builtins) {
|
|
21
|
+
scanDir(builtins, absolute, absolute, result);
|
|
22
|
+
}
|
|
15
23
|
cache.set(absolute, result);
|
|
16
24
|
return result;
|
|
17
25
|
}
|
|
18
26
|
function clearCategoryMetaCache() {
|
|
19
27
|
cache.clear();
|
|
20
28
|
}
|
|
21
|
-
function scanDir(baseDir, currentDir, result) {
|
|
29
|
+
function scanDir(builtins, baseDir, currentDir, result) {
|
|
30
|
+
const { fs, path } = builtins;
|
|
22
31
|
let entries;
|
|
23
32
|
try {
|
|
24
33
|
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -30,19 +39,19 @@ function scanDir(baseDir, currentDir, result) {
|
|
|
30
39
|
const fullPath = path.join(currentDir, entry.name);
|
|
31
40
|
const categoryFile = path.join(fullPath, "_category_.json");
|
|
32
41
|
if (fs.existsSync(categoryFile)) {
|
|
33
|
-
const meta = readCategoryFile(categoryFile);
|
|
42
|
+
const meta = readCategoryFile(builtins, categoryFile);
|
|
34
43
|
if (meta) {
|
|
35
44
|
const relativePath = path.relative(baseDir, fullPath);
|
|
36
45
|
result.set(relativePath, meta);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
scanDir(baseDir, fullPath, result);
|
|
48
|
+
scanDir(builtins, baseDir, fullPath, result);
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
|
-
function readCategoryFile(filePath) {
|
|
51
|
+
function readCategoryFile(builtins, filePath) {
|
|
43
52
|
let raw;
|
|
44
53
|
try {
|
|
45
|
-
raw = fs.readFileSync(filePath, "utf-8");
|
|
54
|
+
raw = builtins.fs.readFileSync(filePath, "utf-8");
|
|
46
55
|
} catch {
|
|
47
56
|
return void 0;
|
|
48
57
|
}
|
|
@@ -2,12 +2,19 @@ import type { SidebarNavNode, SidebarRootMenuItem, SidebarLocaleLink } from "../
|
|
|
2
2
|
export interface SidebarTreeProps {
|
|
3
3
|
nodes: SidebarNavNode[];
|
|
4
4
|
currentSlug?: string;
|
|
5
|
+
/**
|
|
6
|
+
* Explicit current-route override, checked before the
|
|
7
|
+
* `data-zd-current-path` dataset override and `window.location.pathname`
|
|
8
|
+
* when deriving the active slug on hydration and at every View Transition.
|
|
9
|
+
* See `deriveActiveSlug` (zudolab/zudo-doc#3398).
|
|
10
|
+
*/
|
|
11
|
+
currentPath?: string;
|
|
5
12
|
rootMenuItems?: SidebarRootMenuItem[];
|
|
6
13
|
backToMenuLabel?: string;
|
|
7
14
|
localeLinks?: SidebarLocaleLink[];
|
|
8
15
|
themeDefaultMode?: "light" | "dark";
|
|
9
16
|
}
|
|
10
|
-
export declare function SidebarTree({ nodes, currentSlug, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }: SidebarTreeProps): import("preact").JSX.Element;
|
|
17
|
+
export declare function SidebarTree({ nodes, currentSlug, currentPath, rootMenuItems, backToMenuLabel, localeLinks, themeDefaultMode }: SidebarTreeProps): import("preact").JSX.Element;
|
|
11
18
|
export declare namespace SidebarTree {
|
|
12
19
|
var displayName: string;
|
|
13
20
|
}
|