boltdocs 3.3.5 → 3.3.6

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 (79) hide show
  1. package/dist/{build-BVoMHMhw.mjs → build-BJnL-FHG.mjs} +1 -1
  2. package/dist/{build-B672n0hL.cjs → build-CxT38vPv.cjs} +1 -1
  3. package/dist/client/index.cjs +29 -12
  4. package/dist/client/index.d.cts +1 -1
  5. package/dist/client/index.d.ts +1 -1
  6. package/dist/client/index.js +27 -11
  7. package/dist/client/mdx.cjs +1 -1
  8. package/dist/client/mdx.js +1 -1
  9. package/dist/client/primitives.cjs +4 -4
  10. package/dist/client/primitives.d.cts +40 -9
  11. package/dist/client/primitives.d.ts +40 -9
  12. package/dist/client/primitives.js +4 -4
  13. package/dist/config-B3_iVsbi.mjs +7 -0
  14. package/dist/{config-PLvresCv.cjs → config-BC_qhXws.cjs} +1 -1
  15. package/dist/config-BJp3lGzE.cjs +7 -0
  16. package/dist/{dev-DbuhNcF5.cjs → dev-46NiL0MC.cjs} +1 -1
  17. package/dist/{dev-B49V6O6p.mjs → dev-C4kjO5xs.mjs} +1 -1
  18. package/dist/{docs-layout-CbZ7LDRe.js → docs-layout-D3Fmb0Wd.js} +16 -117
  19. package/dist/{docs-layout-vzfXed1Q.cjs → docs-layout-SgphExoE.cjs} +16 -123
  20. package/dist/{doctor-DMECtdK6.cjs → doctor-BqxOFRt3.cjs} +1 -1
  21. package/dist/{doctor-i8Qmd8DJ.mjs → doctor-Cv5lXnay.mjs} +1 -1
  22. package/dist/{doctor-FuFY1jBz.cjs → doctor-TS3JGNEU.cjs} +1 -1
  23. package/dist/{image-B-NDSDuY.js → image-BAiDReNd.js} +1 -1
  24. package/dist/{image-Hnaa2bJd.cjs → image-Bkx85rsq.cjs} +1 -1
  25. package/dist/{index-B3o307nd.d.ts → index-B1Mx0dHl.d.ts} +24 -4
  26. package/dist/{index-Bg-JtZeK.d.cts → index-DLaHgD-i.d.cts} +24 -4
  27. package/dist/{link-vZjmD-es.cjs → link-hLWOsx0x.cjs} +59 -28
  28. package/dist/{link-CgdI6O2o.js → link-q1qHNGK_.js} +54 -29
  29. package/dist/{mdx-BRT71V5Q.js → mdx-CJGWzBGy.js} +2 -2
  30. package/dist/{mdx-D0Absdq5.cjs → mdx-Qi0DmaY8.cjs} +2 -2
  31. package/dist/node/cli-entry.cjs +1 -1
  32. package/dist/node/cli-entry.mjs +1 -1
  33. package/dist/node/highlight.d.cts +2 -2
  34. package/dist/node/highlight.d.mts +2 -2
  35. package/dist/node/index.cjs +1 -1
  36. package/dist/node/index.d.cts +7 -4
  37. package/dist/node/index.d.mts +7 -4
  38. package/dist/node/index.mjs +1 -1
  39. package/dist/node/mdx/shiki-adapter.cjs +1 -1
  40. package/dist/node/mdx/shiki-adapter.d.cts +7 -2
  41. package/dist/node/mdx/shiki-adapter.d.mts +7 -2
  42. package/dist/node/mdx/shiki-adapter.mjs +1 -1
  43. package/dist/node-7Cfw6nMr.cjs +23 -0
  44. package/dist/node-CT2E84nY.mjs +23 -0
  45. package/dist/{package-CNqjEYE1.cjs → package-Bah2tHjK.cjs} +1 -1
  46. package/dist/{package-sdcKhDhe.mjs → package-D6bf68FR.mjs} +1 -1
  47. package/dist/plugin-AgNI3ktz.cjs +69 -0
  48. package/dist/plugin-BAv_pYLt.mjs +69 -0
  49. package/dist/{plugin-context-CscLcXjH.cjs → plugin-context-BZs5lvDu.cjs} +2 -2
  50. package/dist/{plugin-context-DuSg-Hax.mjs → plugin-context-b5qVVQSU.mjs} +1 -1
  51. package/dist/{plugin-lifecycle-BywrEoWK.cjs → plugin-lifecycle-s89gzTlh.cjs} +1 -1
  52. package/dist/{registry-sJqDv0kE.d.mts → registry-B8pt41zj.d.cts} +1 -1
  53. package/dist/{registry-BbB6NqSv.d.cts → registry-BrrH2X9c.d.mts} +1 -1
  54. package/dist/routes-CzhKOw67.cjs +6 -0
  55. package/dist/{routes-C3_w9fLX.cjs → routes-Dq_2EUIB.cjs} +1 -1
  56. package/dist/routes-Dw6g0bfF.mjs +6 -0
  57. package/dist/search-dialog-BYZS9aeO.cjs +747 -0
  58. package/dist/{search-dialog-x_qHU8ty.cjs → search-dialog-CguKVtUu.cjs} +145 -5
  59. package/dist/{search-dialog-B9mdxsUC.js → search-dialog-DQwod5Xc.js} +1 -1
  60. package/dist/search-dialog-DzObE-Ar.js +717 -0
  61. package/dist/{search-dialog-DCtm2dyR.cjs → search-dialog-QgKRRbsI.cjs} +1 -1
  62. package/dist/{search-dialog-BqkzDTq2.js → search-dialog-mYnqU4v7.js} +134 -6
  63. package/dist/{types-Db6zToZH.d.mts → types-74i_xUe2.d.mts} +1 -1
  64. package/dist/{types-o7iZ2BAR.d.cts → types-X_Pwk8Cg.d.cts} +1 -1
  65. package/dist/virtual-modules-C3S3Nz8r.cjs +40 -0
  66. package/dist/virtual-modules-CaxqrFd6.mjs +40 -0
  67. package/package.json +6 -5
  68. package/dist/config-CrsQJHQz.cjs +0 -7
  69. package/dist/config-DC0nMMk7.mjs +0 -7
  70. package/dist/node-BljvHMpS.cjs +0 -23
  71. package/dist/node-CWxGw4H4.mjs +0 -23
  72. package/dist/plugin-BPcm1cU0.mjs +0 -69
  73. package/dist/plugin-WUhxnpko.cjs +0 -69
  74. package/dist/routes-C0kb4ue6.cjs +0 -6
  75. package/dist/routes-C4wrLWwu.mjs +0 -6
  76. package/dist/search-dialog-Bp_ouN1c.cjs +0 -550
  77. package/dist/search-dialog-an2BNMj6.js +0 -526
  78. package/dist/virtual-modules-B2X4Ht2r.mjs +0 -39
  79. package/dist/virtual-modules-DPsdHKcu.cjs +0 -39
@@ -0,0 +1,717 @@
1
+ /**
2
+ * Boltdocs - https://boltdocs.vercel.app
3
+ * Copyright (c) 2026 Jesus Alcala
4
+ * Licensed under the MIT License.
5
+ */
6
+ import { G as useConfig, H as X, J as useBoltdocsContext, d as normalizePath, l as createRouteIndex, o as cn, u as useRoutesContext, z as Search } from "./link-q1qHNGK_.js";
7
+ import { E as useLocation, O as useNavigate, f as normalizeUrlBase, o as getVersionPrefixSegments } from "./router-CZBxMRAH.js";
8
+ import { i as Navbar, n as createSearchHighlightRegex, o as ErrorBoundary, t as SearchDialog$1 } from "./search-dialog-mYnqU4v7.js";
9
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
10
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
+ import fetchSearchData from "virtual:boltdocs-search";
12
+
13
+ //#region src/client/hooks/use-routes.ts
14
+ function stripBase(parts, base) {
15
+ const baseParts = normalizeUrlBase(base).split("/").filter(Boolean);
16
+ if (baseParts.length === 0) return parts;
17
+ if (parts.length < baseParts.length || !baseParts.every((part, index) => parts[index] === part)) return parts;
18
+ return parts.slice(baseParts.length);
19
+ }
20
+ function consumeVersion(parts, config) {
21
+ const versions = config.versions;
22
+ if (!versions) return { nextIndex: 0 };
23
+ const prefixParts = getVersionPrefixSegments(config);
24
+ let index = 0;
25
+ if (prefixParts.length > 0 && prefixParts.every((part, offset) => parts[offset] === part)) index = prefixParts.length;
26
+ else if (prefixParts.length > 0) {
27
+ if (prefixParts.length === 1 && parts.length > 0) {
28
+ const version = versions.versions.find((item) => `${prefixParts[0]}${item.path}` === parts[0]);
29
+ if (version) return {
30
+ version: version.path,
31
+ nextIndex: 1
32
+ };
33
+ }
34
+ return { nextIndex: 0 };
35
+ }
36
+ const candidate = parts[index];
37
+ if (candidate && versions.versions.some((version) => version.path === candidate)) return {
38
+ version: candidate,
39
+ nextIndex: index + 1
40
+ };
41
+ return { nextIndex: 0 };
42
+ }
43
+ function consumeLocale(parts, index, locales) {
44
+ const candidate = parts[index];
45
+ return candidate && locales.includes(candidate) ? {
46
+ locale: candidate,
47
+ nextIndex: index + 1
48
+ } : { nextIndex: index };
49
+ }
50
+ function effectiveVariantMatches(route, config, currentLocale, currentVersion) {
51
+ const localeMatches = !config.i18n || (route.locale || config.i18n.defaultLocale) === currentLocale;
52
+ const versionMatches = !config.versions || (route.version || config.versions.defaultVersion) === currentVersion;
53
+ return localeMatches && versionMatches;
54
+ }
55
+ function findCurrentCollectionRoute(routeIndex, structuralPathKey, config, currentLocale, currentVersion) {
56
+ return routeIndex.byCollectionPath?.get(structuralPathKey)?.find((route) => effectiveVariantMatches(route, config, currentLocale, currentVersion));
57
+ }
58
+ /** Filter generated variants using the URL's active locale/version contract. */
59
+ function selectRoutesForContext(routes, options) {
60
+ const { config, currentLocale, currentVersion, isCurrentLocalePrefixed, isCurrentVersionPrefixed, countsByFilePath } = options;
61
+ return routes.filter((route) => {
62
+ const localeMatches = config.i18n ? (route.locale || config.i18n.defaultLocale) === currentLocale : true;
63
+ const versionMatches = config.versions ? (route.version || config.versions.defaultVersion) === currentVersion : true;
64
+ if (!(localeMatches && versionMatches)) return false;
65
+ if ((countsByFilePath.get(route.filePath) || 0) <= 1) return true;
66
+ const localeMismatch = config.i18n && isCurrentLocalePrefixed !== Boolean(route.locale);
67
+ const versionMismatch = config.versions && isCurrentVersionPrefixed !== Boolean(route.version);
68
+ return !(localeMismatch || versionMismatch);
69
+ });
70
+ }
71
+ /**
72
+ * Hook to access the framework's routing state.
73
+ * Returns both the complete set of routes and a filtered list based on the current
74
+ * version and locale.
75
+ */
76
+ function useRoutes() {
77
+ const routeContext = useRoutesContext();
78
+ const allRoutes = routeContext.routes;
79
+ const routeIndex = useMemo(() => routeContext.index?.byCollectionPath ? routeContext.index : createRouteIndex(allRoutes), [allRoutes, routeContext.index]);
80
+ const config = useConfig();
81
+ const { pathname } = useLocation();
82
+ const { currentLocale: currentLocaleStore, currentVersion: currentVersionStore } = useBoltdocsContext();
83
+ const currentPath = useMemo(() => normalizePath(pathname || "/"), [pathname]);
84
+ const pathParts = useMemo(() => currentPath.split("/").filter(Boolean), [currentPath]);
85
+ const localeConfig = config.i18n?.locales;
86
+ const configuredLocales = useMemo(() => localeConfig ? Array.isArray(localeConfig) ? localeConfig : Object.keys(localeConfig) : [], [localeConfig]);
87
+ const configuredVersions = useMemo(() => config.versions?.versions || [], [config.versions]);
88
+ const structuralPath = useMemo(() => {
89
+ const contentParts = stripBase(pathParts, config.base);
90
+ const versionMatch = consumeVersion(contentParts, config);
91
+ const localeMatch = consumeLocale(contentParts, versionMatch.nextIndex, configuredLocales);
92
+ return {
93
+ contentParts: contentParts.slice(localeMatch.nextIndex),
94
+ locale: localeMatch.locale,
95
+ version: versionMatch.version
96
+ };
97
+ }, [
98
+ config,
99
+ configuredLocales,
100
+ pathParts
101
+ ]);
102
+ const currentLocale = config.i18n ? structuralPath.locale || (configuredLocales.includes(currentLocaleStore || "") ? currentLocaleStore : config.i18n.defaultLocale) : void 0;
103
+ const currentVersion = config.versions ? structuralPath.version || (configuredVersions.some((version) => version.path === currentVersionStore) ? currentVersionStore : config.versions.defaultVersion) : void 0;
104
+ const structuralPathKey = useMemo(() => `/${structuralPath.contentParts.join("/")}`, [structuralPath.contentParts]);
105
+ const currentRoute = useMemo(() => {
106
+ const direct = routeIndex.byPath.get(currentPath);
107
+ if (direct) return direct;
108
+ return findCurrentCollectionRoute(routeIndex, structuralPathKey, config, currentLocale, currentVersion);
109
+ }, [
110
+ config,
111
+ currentLocale,
112
+ currentPath,
113
+ currentVersion,
114
+ routeIndex,
115
+ structuralPathKey
116
+ ]);
117
+ const isCurrentLocalePrefixed = structuralPath.locale !== void 0;
118
+ const isCurrentVersionPrefixed = structuralPath.version !== void 0;
119
+ const countsByFilePath = useMemo(() => {
120
+ if (routeIndex.countsByFilePath) return routeIndex.countsByFilePath;
121
+ const counts = /* @__PURE__ */ new Map();
122
+ for (const route of allRoutes) counts.set(route.filePath, (counts.get(route.filePath) || 0) + 1);
123
+ return counts;
124
+ }, [allRoutes, routeIndex.countsByFilePath]);
125
+ const routes = useMemo(() => selectRoutesForContext(allRoutes, {
126
+ config,
127
+ currentLocale,
128
+ currentVersion,
129
+ isCurrentLocalePrefixed,
130
+ isCurrentVersionPrefixed,
131
+ countsByFilePath
132
+ }), [
133
+ allRoutes,
134
+ config,
135
+ countsByFilePath,
136
+ currentLocale,
137
+ currentVersion,
138
+ isCurrentLocalePrefixed,
139
+ isCurrentVersionPrefixed
140
+ ]);
141
+ const collections = useMemo(() => new Set(routeIndex.collectionNames.map((name) => name.toLowerCase())), [routeIndex.collectionNames]);
142
+ const currentCollection = structuralPath.contentParts[0]?.toLowerCase();
143
+ return {
144
+ routes,
145
+ allRoutes,
146
+ currentRoute,
147
+ isCollectionPage: collections.has(currentCollection || ""),
148
+ currentLocale,
149
+ currentVersion
150
+ };
151
+ }
152
+
153
+ //#endregion
154
+ //#region src/client/components/internal/error-boundary.tsx
155
+ function InternalFallback({ error, resetErrorBoundary, className }) {
156
+ return /* @__PURE__ */ jsxs("div", {
157
+ role: "alert",
158
+ className: cn("p-2 font-mono flex flex-col items-center justify-between min-h-[30vh]", className),
159
+ children: [
160
+ /* @__PURE__ */ jsx("p", {
161
+ className: "text-lg font-semibold text-danger-500",
162
+ children: "Something went wrong"
163
+ }),
164
+ error?.message && /* @__PURE__ */ jsx("pre", {
165
+ className: "text-sm mt-2 max-w-md overflow-auto whitespace-pre-wrap break-word",
166
+ children: error.message
167
+ }),
168
+ error?.stack && /* @__PURE__ */ jsxs("details", {
169
+ className: "mt-2 max-w-md text-left text-xs",
170
+ children: [/* @__PURE__ */ jsx("summary", {
171
+ className: "cursor-pointer text-muted select-none outline-none",
172
+ children: "Error details"
173
+ }), /* @__PURE__ */ jsx("pre", {
174
+ className: "mt-2 p-2 bg-soft rounded border-subtle border overflow-auto max-h-48 whitespace-pre-wrap break-words",
175
+ children: error.stack
176
+ })]
177
+ }),
178
+ /* @__PURE__ */ jsxs("div", {
179
+ className: "flex items-center gap-2 mt-2",
180
+ children: [/* @__PURE__ */ jsx("button", {
181
+ type: "button",
182
+ onClick: resetErrorBoundary,
183
+ className: "px-2 py-3 bg-soft rounded border-subtle border font-mono font-semibold text-body hover:scale-105 transition-transform active:scale-95 cursor-pointer",
184
+ children: "Try again"
185
+ }), typeof window !== "undefined" && /* @__PURE__ */ jsx("button", {
186
+ type: "button",
187
+ onClick: () => window.location.reload(),
188
+ className: "px-2 py-3 rounded font-mono font-semibold text-muted hover:text-body transition-colors cursor-pointer",
189
+ children: "Reload page"
190
+ })]
191
+ })
192
+ ]
193
+ });
194
+ }
195
+ function InternalErrorBoundary({ children, fallback, resetKeys }) {
196
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
197
+ fallback,
198
+ resetKeys,
199
+ FallbackComponent: !fallback ? InternalFallback : void 0,
200
+ children
201
+ });
202
+ }
203
+
204
+ //#endregion
205
+ //#region src/client/hooks/use-search.ts
206
+ const SEARCH_DEBOUNCE_MS = 250;
207
+ const SEARCH_RESULT_LIMIT = 10;
208
+ function foldSearchText(value) {
209
+ return value.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLocaleLowerCase();
210
+ }
211
+ function decodeAlgoliaSnippet(value) {
212
+ return value.replace(/<[^>]*>/g, "").replace(/&nbsp;/gi, " ").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, "\"").replace(/&#0?39;|&apos;/gi, "'").replace(/&amp;/gi, "&").replace(/&#(\d+);/g, (_match, code) => String.fromCodePoint(Number.parseInt(code, 10))).replace(/\s+/g, " ").trim();
213
+ }
214
+ function createSearchSnippet(content, query) {
215
+ const normalizedContent = content.replace(/\s+/g, " ").trim();
216
+ if (!normalizedContent) return "";
217
+ const terms = query.trim().split(/\s+/).filter(Boolean).map((term) => term.toLocaleLowerCase());
218
+ const lowerContent = normalizedContent.toLocaleLowerCase();
219
+ const matchIndexes = terms.map((term) => lowerContent.indexOf(term)).filter((index) => index >= 0);
220
+ const firstMatch = matchIndexes.length > 0 ? Math.min(...matchIndexes) : 0;
221
+ const start = Math.max(0, firstMatch - 48);
222
+ const end = Math.min(normalizedContent.length, firstMatch + 152);
223
+ return `${start > 0 ? "…" : ""}${normalizedContent.slice(start, end)}${end < normalizedContent.length ? "…" : ""}`;
224
+ }
225
+ function getLocalRelevance(doc, query) {
226
+ const normalizedQuery = foldSearchText(query.trim());
227
+ const terms = normalizedQuery.split(/\s+/).filter(Boolean);
228
+ if (terms.length === 0) return 0;
229
+ const title = foldSearchText(doc.title);
230
+ const display = foldSearchText(doc.display);
231
+ const content = foldSearchText(doc.content);
232
+ const combined = `${title} ${display} ${content}`;
233
+ if (!terms.every((term) => combined.includes(term))) return -1;
234
+ let score = terms.length;
235
+ if (title === normalizedQuery) score += 120;
236
+ else if (title.startsWith(normalizedQuery)) score += 80;
237
+ else if (title.includes(normalizedQuery)) score += 60;
238
+ else if (display.includes(normalizedQuery)) score += 35;
239
+ for (const term of terms) {
240
+ if (title.startsWith(term)) score += 24;
241
+ else if (title.includes(term)) score += 16;
242
+ if (content.includes(term)) score += 6;
243
+ }
244
+ return score;
245
+ }
246
+ /** Apply a frontmatter HMR delta in one immutable update. */
247
+ function applySearchDelta(documents, delta) {
248
+ const next = new Map(documents.map((document) => [document.id, document]));
249
+ for (const document of delta.updated ?? []) next.set(document.id, document);
250
+ for (const id of delta.deleted ?? []) next.delete(id);
251
+ return [...next.values()];
252
+ }
253
+ function useSearch(routes, options = {}) {
254
+ const { currentLocale, currentVersion } = useRoutes();
255
+ const config = useConfig();
256
+ const algoliaConfig = config.integrations?.search?.algolia;
257
+ const [isOpen, setIsOpen] = useState(false);
258
+ const [query, setQuery] = useState("");
259
+ const [index, setIndex] = useState(null);
260
+ const [algoliaResults, setAlgoliaResults] = useState([]);
261
+ const [searchData, setSearchData] = useState([]);
262
+ const [searchDataLoading, setSearchDataLoading] = useState(false);
263
+ const [indexLoading, setIndexLoading] = useState(false);
264
+ const [algoliaLoading, setAlgoliaLoading] = useState(false);
265
+ const [searchDataError, setSearchDataError] = useState(null);
266
+ const [algoliaError, setAlgoliaError] = useState(null);
267
+ const searchGeneration = useRef(0);
268
+ const loadingGeneration = useRef(null);
269
+ const algoliaGeneration = useRef(0);
270
+ const algoliaController = useRef(null);
271
+ const navigate = useNavigate();
272
+ useEffect(() => {
273
+ if (algoliaConfig || !isOpen || searchData.length > 0) return;
274
+ setSearchDataLoading(true);
275
+ setIndexLoading(false);
276
+ setSearchDataError(null);
277
+ const generation = ++searchGeneration.current;
278
+ loadingGeneration.current = generation;
279
+ fetchSearchData().then((data) => {
280
+ if (generation !== searchGeneration.current) return;
281
+ setSearchData(Array.isArray(data) ? data : []);
282
+ setSearchDataError(null);
283
+ setIndex(null);
284
+ }).catch((error) => {
285
+ if (generation !== searchGeneration.current) return;
286
+ const normalized = error instanceof Error ? error : new Error(String(error));
287
+ console.error("[boltdocs] Failed to load search index:", normalized);
288
+ setSearchDataError(normalized);
289
+ setIndex(null);
290
+ }).finally(() => {
291
+ if (generation !== searchGeneration.current) return;
292
+ loadingGeneration.current = null;
293
+ setSearchDataLoading(false);
294
+ });
295
+ return () => {
296
+ if (loadingGeneration.current === generation) {
297
+ searchGeneration.current += 1;
298
+ loadingGeneration.current = null;
299
+ }
300
+ };
301
+ }, [
302
+ isOpen,
303
+ algoliaConfig,
304
+ searchData.length
305
+ ]);
306
+ useEffect(() => {
307
+ const hot = options.hot ?? import.meta.hot;
308
+ if (!hot) return;
309
+ const applyFrontmatterDelta = (payload) => {
310
+ const delta = payload;
311
+ searchGeneration.current += 1;
312
+ if (loadingGeneration.current !== null) {
313
+ loadingGeneration.current = null;
314
+ setSearchDataLoading(false);
315
+ }
316
+ setSearchData((previous) => applySearchDelta(previous, delta.search ?? {}));
317
+ setSearchDataError(null);
318
+ setIndex(null);
319
+ };
320
+ const refreshFromAsset = () => {
321
+ const generation = ++searchGeneration.current;
322
+ loadingGeneration.current = generation;
323
+ setSearchDataLoading(true);
324
+ setSearchDataError(null);
325
+ fetchSearchData({ bustCache: true }).then((data) => {
326
+ if (generation !== searchGeneration.current) return;
327
+ setSearchData(Array.isArray(data) ? data : []);
328
+ setIndex(null);
329
+ }).catch((error) => {
330
+ if (generation !== searchGeneration.current) return;
331
+ const normalized = error instanceof Error ? error : new Error(String(error));
332
+ console.error("[boltdocs] Failed to refresh search index:", normalized);
333
+ setSearchDataError(normalized);
334
+ }).finally(() => {
335
+ if (generation !== searchGeneration.current) return;
336
+ loadingGeneration.current = null;
337
+ setSearchDataLoading(false);
338
+ });
339
+ };
340
+ hot.on("boltdocs:frontmatter-update", applyFrontmatterDelta);
341
+ hot.on("boltdocs:mdx-update", refreshFromAsset);
342
+ return () => {
343
+ searchGeneration.current += 1;
344
+ hot.off?.("boltdocs:frontmatter-update", applyFrontmatterDelta);
345
+ hot.off?.("boltdocs:mdx-update", refreshFromAsset);
346
+ };
347
+ }, [options.hot]);
348
+ useEffect(() => {
349
+ const handleKeyDown = (event) => {
350
+ if (event.defaultPrevented) return;
351
+ if ((/Mac|iPhone|iPad|iPod/.test(navigator.userAgent) ? event.metaKey : event.ctrlKey) && (event.key === "k" || event.key === "K" || event.key === "j" || event.key === "J")) {
352
+ event.preventDefault();
353
+ setIsOpen((previous) => !previous);
354
+ }
355
+ };
356
+ window.addEventListener("keydown", handleKeyDown);
357
+ return () => window.removeEventListener("keydown", handleKeyDown);
358
+ }, []);
359
+ const handleSelect = useCallback((key) => {
360
+ const path = String(key);
361
+ if (!path) return;
362
+ setIsOpen(false);
363
+ const url = new URL(path, window.location.origin);
364
+ if (query.trim()) url.searchParams.set("hl", query.trim());
365
+ navigate(`${url.pathname}${url.search}${url.hash}`);
366
+ }, [navigate, query]);
367
+ useEffect(() => {
368
+ if (algoliaConfig || !isOpen || searchData.length === 0 || index) return;
369
+ let cancelled = false;
370
+ const generation = searchGeneration.current;
371
+ setIndexLoading(true);
372
+ setSearchDataError(null);
373
+ import("flexsearch").then(({ Index: FlexIndex }) => {
374
+ if (cancelled || generation !== searchGeneration.current) return;
375
+ const nextIndex = new FlexIndex({
376
+ tokenize: "forward",
377
+ cache: true
378
+ });
379
+ searchData.forEach((document, id) => {
380
+ nextIndex.add(id, `${document.title} ${document.content}`);
381
+ });
382
+ setIndex(nextIndex);
383
+ }).catch((error) => {
384
+ if (cancelled || generation !== searchGeneration.current) return;
385
+ const normalized = error instanceof Error ? error : new Error(String(error));
386
+ console.error("[boltdocs] Failed to load search engine:", normalized);
387
+ setSearchDataError(normalized);
388
+ }).finally(() => {
389
+ if (cancelled || generation !== searchGeneration.current) return;
390
+ setIndexLoading(false);
391
+ });
392
+ return () => {
393
+ cancelled = true;
394
+ };
395
+ }, [
396
+ isOpen,
397
+ index,
398
+ algoliaConfig,
399
+ searchData
400
+ ]);
401
+ useEffect(() => {
402
+ const normalizedQuery = query.trim();
403
+ if (!algoliaConfig) return;
404
+ if (!normalizedQuery) {
405
+ algoliaGeneration.current += 1;
406
+ algoliaController.current?.abort();
407
+ algoliaController.current = null;
408
+ setAlgoliaResults([]);
409
+ setAlgoliaError(null);
410
+ setAlgoliaLoading(false);
411
+ return;
412
+ }
413
+ const generation = ++algoliaGeneration.current;
414
+ algoliaController.current?.abort();
415
+ const controller = new AbortController();
416
+ algoliaController.current = controller;
417
+ setAlgoliaError(null);
418
+ setAlgoliaLoading(true);
419
+ const timer = setTimeout(() => {
420
+ (async () => {
421
+ try {
422
+ const facetFilters = [];
423
+ if (currentLocale) facetFilters.push(`lang:${currentLocale}`);
424
+ if (currentVersion) facetFilters.push(`version:${currentVersion}`);
425
+ const url = `https://${algoliaConfig.appId}-dsn.algolia.net/1/indexes/${encodeURIComponent(algoliaConfig.indexName)}/query`;
426
+ const params = new URLSearchParams({
427
+ query: normalizedQuery,
428
+ hitsPerPage: "20"
429
+ });
430
+ if (facetFilters.length > 0) params.set("facetFilters", JSON.stringify(facetFilters));
431
+ const response = await fetch(url, {
432
+ method: "POST",
433
+ headers: {
434
+ "Content-Type": "application/json",
435
+ "X-Algolia-API-Key": algoliaConfig.apiKey,
436
+ "X-Algolia-Application-Id": algoliaConfig.appId
437
+ },
438
+ body: JSON.stringify({ params: params.toString() }),
439
+ signal: controller.signal
440
+ });
441
+ if (!response.ok) throw new Error(`Algolia search request failed: ${response.status} ${response.statusText}`.trim());
442
+ const data = await response.json();
443
+ if (generation !== algoliaGeneration.current || controller.signal.aborted) return;
444
+ setAlgoliaResults((Array.isArray(data.hits) ? data.hits : []).map((hit) => {
445
+ let path = hit.url || "";
446
+ try {
447
+ if (/^https?:\/\//.test(path)) {
448
+ const urlObject = new URL(path);
449
+ path = `${urlObject.pathname}${urlObject.search}${urlObject.hash}`;
450
+ }
451
+ } catch {}
452
+ const hierarchy = hit.hierarchy ?? {};
453
+ const levels = [
454
+ hierarchy.lvl0,
455
+ hierarchy.lvl1,
456
+ hierarchy.lvl2,
457
+ hierarchy.lvl3,
458
+ hierarchy.lvl4,
459
+ hierarchy.lvl5,
460
+ hierarchy.lvl6
461
+ ].filter((value) => Boolean(value));
462
+ if (!path) return null;
463
+ const title = levels.at(-1) ?? hit.content?.slice(0, 120) ?? "Documentation";
464
+ const rawSnippet = hit._snippetResult?.content?.value;
465
+ const snippet = rawSnippet ? decodeAlgoliaSnippet(rawSnippet) : void 0;
466
+ return {
467
+ id: hit.objectID || path,
468
+ title,
469
+ path,
470
+ bio: levels.join(" > "),
471
+ groupTitle: hierarchy.lvl0 ?? "Docs",
472
+ isHeading: Boolean(hit.anchor) || path.includes("#"),
473
+ ...snippet ? { snippet } : {}
474
+ };
475
+ }).filter((result) => result !== null));
476
+ } catch (error) {
477
+ if (controller.signal.aborted || generation !== algoliaGeneration.current || error instanceof DOMException && error.name === "AbortError") return;
478
+ const normalized = error instanceof Error ? error : new Error(String(error));
479
+ console.error("[boltdocs] Algolia search failed:", normalized);
480
+ setAlgoliaError(normalized);
481
+ setAlgoliaResults([]);
482
+ } finally {
483
+ if (generation === algoliaGeneration.current) {
484
+ setAlgoliaLoading(false);
485
+ if (algoliaController.current === controller) algoliaController.current = null;
486
+ }
487
+ }
488
+ })();
489
+ }, SEARCH_DEBOUNCE_MS);
490
+ return () => {
491
+ clearTimeout(timer);
492
+ controller.abort();
493
+ if (algoliaController.current === controller) algoliaController.current = null;
494
+ if (generation === algoliaGeneration.current) {
495
+ algoliaGeneration.current += 1;
496
+ setAlgoliaLoading(false);
497
+ }
498
+ };
499
+ }, [
500
+ query,
501
+ algoliaConfig,
502
+ currentLocale,
503
+ currentVersion
504
+ ]);
505
+ const list = useMemo(() => {
506
+ if (!query.trim()) return routes.filter((route) => {
507
+ const routeLocale = route.locale || config.i18n?.defaultLocale;
508
+ const routeVersion = route.version || config.versions?.defaultVersion;
509
+ return (!currentLocale || routeLocale === currentLocale) && (!currentVersion || routeVersion === currentVersion);
510
+ }).slice(0, SEARCH_RESULT_LIMIT).map((route) => ({
511
+ id: route.path,
512
+ title: route.title,
513
+ path: route.path,
514
+ bio: route.description || "",
515
+ groupTitle: route.groupTitle
516
+ }));
517
+ if (algoliaConfig) return algoliaResults;
518
+ if (!index || indexLoading) return [];
519
+ const flexRank = /* @__PURE__ */ new Map();
520
+ try {
521
+ index.search(query.trim(), {
522
+ limit: searchData.length,
523
+ suggest: true
524
+ }).forEach((id, rank) => {
525
+ const numericId = typeof id === "number" ? id : Number(id);
526
+ if (Number.isInteger(numericId)) flexRank.set(numericId, rank);
527
+ });
528
+ } catch (error) {
529
+ console.error("[boltdocs] Local search failed:", error);
530
+ }
531
+ const ranked = searchData.map((document, id) => {
532
+ const documentLocale = document.locale || config.i18n?.defaultLocale;
533
+ const documentVersion = document.version || config.versions?.defaultVersion;
534
+ if (currentLocale && documentLocale !== currentLocale) return null;
535
+ if (currentVersion && documentVersion !== currentVersion) return null;
536
+ const relevance = getLocalRelevance(document, query);
537
+ if (relevance < 0) return null;
538
+ return {
539
+ document,
540
+ score: relevance * 1e3 - (flexRank.get(id) ?? searchData.length)
541
+ };
542
+ }).filter((result) => result !== null).sort((left, right) => right.score - left.score);
543
+ const results = [];
544
+ const seen = /* @__PURE__ */ new Set();
545
+ for (const { document } of ranked) {
546
+ if (seen.has(document.url)) continue;
547
+ seen.add(document.url);
548
+ results.push({
549
+ id: document.url,
550
+ title: document.title,
551
+ path: document.url,
552
+ bio: document.display,
553
+ groupTitle: document.display.split(" > ")[0],
554
+ isHeading: document.url.includes("#"),
555
+ localeMatch: true,
556
+ snippet: createSearchSnippet(document.content, query)
557
+ });
558
+ if (results.length === SEARCH_RESULT_LIMIT) break;
559
+ }
560
+ return results;
561
+ }, [
562
+ query,
563
+ index,
564
+ indexLoading,
565
+ currentLocale,
566
+ currentVersion,
567
+ routes,
568
+ searchData,
569
+ algoliaConfig,
570
+ algoliaResults,
571
+ config
572
+ ]);
573
+ const error = algoliaConfig ? algoliaError : searchDataError;
574
+ const status = error ? "error" : algoliaConfig ? algoliaLoading ? "searching" : query.trim() ? "ready" : "idle" : searchDataLoading ? "loading" : isOpen && searchData.length > 0 && indexLoading ? "indexing" : isOpen && searchData.length > 0 && !index ? "indexing" : isOpen ? "ready" : "idle";
575
+ return {
576
+ isOpen,
577
+ setIsOpen,
578
+ query,
579
+ setQuery,
580
+ list,
581
+ status,
582
+ isLoading: status === "loading" || status === "indexing" || status === "searching",
583
+ isIndexing: status === "indexing",
584
+ error,
585
+ searchDataLoading,
586
+ searchDataError,
587
+ searchError: algoliaError,
588
+ handleSelect,
589
+ input: {
590
+ value: query,
591
+ onChange: (event) => setQuery(event.target.value)
592
+ }
593
+ };
594
+ }
595
+
596
+ //#endregion
597
+ //#region src/client/components/ui-base/search-dialog.tsx
598
+ function SearchResultHighlight({ text, query, markClassName }) {
599
+ if (!query.trim() || !text) return /* @__PURE__ */ jsx(Fragment$1, { children: text });
600
+ const regex = createSearchHighlightRegex(query.split(/\s+/));
601
+ if (!regex) return /* @__PURE__ */ jsx(Fragment$1, { children: text });
602
+ return /* @__PURE__ */ jsx(Fragment$1, { children: text.split(regex).map((part, index) => index % 2 === 1 ? /* @__PURE__ */ jsx("mark", {
603
+ className: cn("bg-primary-500/20 text-primary-600 dark:text-primary-400 font-bold px-0.5 rounded-sm", markClassName),
604
+ children: part
605
+ }, `${part}-${index}`) : part) });
606
+ }
607
+ function SearchDialog({ routes, className, markClassName }) {
608
+ const { isOpen, setIsOpen, query, setQuery, list, status, isLoading, error, handleSelect } = useSearch(routes);
609
+ const statusId = useId();
610
+ const statusMessage = status === "loading" ? "Loading search index…" : status === "indexing" ? "Preparing search…" : status === "searching" ? "Searching…" : status === "error" ? "Search is temporarily unavailable." : query.trim() ? `${list.length} result${list.length === 1 ? "" : "s"} found.` : `${list.length} suggested page${list.length === 1 ? "" : "s"}.`;
611
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
612
+ /* @__PURE__ */ jsxs(Navbar.SearchTrigger.Desktop, {
613
+ "aria-label": "Search docs",
614
+ "aria-keyshortcuts": "Control+K Meta+K",
615
+ onPress: () => setIsOpen(true),
616
+ className: "rounded-xl border border-subtle bg-surface text-muted transition-all duration-200 hover:border-primary-500/50 hover:text-body hover:bg-soft/50 hover:shadow-sm active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-primary-500/30",
617
+ children: [/* @__PURE__ */ jsxs("div", {
618
+ className: "flex items-center gap-2",
619
+ children: [/* @__PURE__ */ jsx(Search, { size: 16 }), /* @__PURE__ */ jsx("span", {
620
+ className: "text-paragraph hidden sm:inline-block",
621
+ children: "Search docs..."
622
+ })]
623
+ }), /* @__PURE__ */ jsx(Navbar.SearchTrigger.Kbd, { className: "[&_kbd]:bg-main [&_kbd]:border [&_kbd]:border-subtle [&_kbd]:rounded [&_kbd]:px-1.5 [&_kbd]:h-5 [&_kbd]:w-5" })]
624
+ }),
625
+ /* @__PURE__ */ jsx(Navbar.SearchTrigger.Mobile, {
626
+ "aria-label": "Search docs",
627
+ onPress: () => setIsOpen(true),
628
+ className: "rounded-xl text-muted transition-all duration-200 hover:text-body active:scale-95 focus-visible:ring-2 focus-visible:ring-primary-500/30",
629
+ children: /* @__PURE__ */ jsx(Search, { size: 20 })
630
+ }),
631
+ /* @__PURE__ */ jsx(InternalErrorBoundary, { children: /* @__PURE__ */ jsx(SearchDialog$1.Overlay, {
632
+ isOpen,
633
+ isDismissable: true,
634
+ onOpenChange: setIsOpen,
635
+ className: cn("fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs animate-fade-in", className),
636
+ children: /* @__PURE__ */ jsx(SearchDialog$1.Content, {
637
+ className: "w-full max-w-lg bg-main border border-subtle shadow-md rounded-2xl overflow-hidden p-6",
638
+ children: /* @__PURE__ */ jsx(SearchDialog$1.Dialog, {
639
+ "aria-label": "Search documentation",
640
+ "aria-describedby": statusId,
641
+ "aria-busy": isLoading,
642
+ className: "flex flex-col min-h-0 h-[450px]",
643
+ children: /* @__PURE__ */ jsxs(SearchDialog$1.Autocomplete, {
644
+ className: "flex flex-col min-h-0",
645
+ children: [
646
+ /* @__PURE__ */ jsxs(SearchDialog$1.Input, {
647
+ value: query,
648
+ onChange: setQuery,
649
+ className: "flex items-center gap-2 border border-subtle bg-surface px-4 py-2.5 rounded-xl focus-within:border-primary-500 mb-4",
650
+ children: [/* @__PURE__ */ jsx(SearchDialog$1.Input.SearchInput, {
651
+ "aria-label": "Search documentation",
652
+ placeholder: "Search documentation...",
653
+ className: "w-full bg-transparent outline-none text-body text-sm"
654
+ }), query && /* @__PURE__ */ jsx(SearchDialog$1.Input.Button, {
655
+ slot: "clear",
656
+ "aria-label": "Clear search",
657
+ onPress: () => setQuery(""),
658
+ className: "text-muted hover:text-body hover:bg-surface rounded-md p-1 cursor-pointer select-none transition-colors",
659
+ children: /* @__PURE__ */ jsx(X, { size: 16 })
660
+ })]
661
+ }),
662
+ /* @__PURE__ */ jsx("div", {
663
+ id: statusId,
664
+ role: status === "error" ? "alert" : "status",
665
+ "aria-live": status === "error" ? "assertive" : "polite",
666
+ className: "sr-only",
667
+ children: statusMessage
668
+ }),
669
+ isLoading ? /* @__PURE__ */ jsx("div", {
670
+ className: "flex flex-1 items-center justify-center px-4 py-8 text-sm text-muted",
671
+ "aria-hidden": "true",
672
+ children: status === "loading" ? "Loading search index…" : status === "indexing" ? "Preparing search…" : "Searching…"
673
+ }) : error ? /* @__PURE__ */ jsx("div", {
674
+ className: "flex flex-1 items-center justify-center px-4 py-8 text-center text-sm text-muted",
675
+ children: "Search is temporarily unavailable."
676
+ }) : query.trim() && list.length === 0 ? /* @__PURE__ */ jsx("div", {
677
+ className: "flex flex-1 items-center justify-center px-4 py-8 text-sm text-muted",
678
+ children: "No results found."
679
+ }) : /* @__PURE__ */ jsx(SearchDialog$1.List, {
680
+ items: list,
681
+ onAction: handleSelect,
682
+ children: (item) => /* @__PURE__ */ jsxs(SearchDialog$1.Item, {
683
+ textValue: item.title,
684
+ className: "flex items-center gap-3 px-4 py-2 rounded-xl group dark:hover:bg-primary-300/40 hover:bg-primary-200/50 transition-colors duration-100",
685
+ children: [/* @__PURE__ */ jsx(SearchDialog$1.Item.Icon, {
686
+ isHeading: item.isHeading,
687
+ className: "text-muted group-hover:text-primary-500 group-focus:text-primary-500"
688
+ }), /* @__PURE__ */ jsxs("div", {
689
+ className: "flex flex-col justify-center min-w-0",
690
+ children: [/* @__PURE__ */ jsx(SearchDialog$1.Item.Title, {
691
+ className: "text-sm font-medium text-body truncate dark:group-hover:text-primary-100",
692
+ children: /* @__PURE__ */ jsx(SearchResultHighlight, {
693
+ text: item.title,
694
+ query,
695
+ markClassName
696
+ })
697
+ }), /* @__PURE__ */ jsx(SearchDialog$1.Item.Bio, {
698
+ className: "text-xs text-muted truncate",
699
+ children: /* @__PURE__ */ jsx(SearchResultHighlight, {
700
+ text: item.snippet ?? item.bio,
701
+ query,
702
+ markClassName
703
+ })
704
+ })]
705
+ })]
706
+ }, item.id)
707
+ })
708
+ ]
709
+ })
710
+ })
711
+ })
712
+ }) })
713
+ ] });
714
+ }
715
+
716
+ //#endregion
717
+ export { useRoutes as a, InternalErrorBoundary as i, SearchResultHighlight as n, useSearch as r, SearchDialog as t };