@ubean/client 0.2.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/dist/index.js ADDED
@@ -0,0 +1,1024 @@
1
+ import { A as getI18nConfig, B as setLocale, C as addLocale, D as detectLocale, E as detectBrowserLocale, F as initClientI18n, G as useSwitchLocalePath, H as t, I as localizePath, K as createUbeanRouter, L as mergeLocale, M as getLocaleDir, N as getLocaleName, O as extractLocaleFromPath, P as getRegisteredLocales, R as onLocaleChange, S as definePage, T as defineLocale, U as useI18n, V as switchLocalePath, W as useLocalePath, _ as useSeoMeta, b as defineMeta, j as getLocale, k as getDefaultLocale, m as createUbeanSSRApp, n as Head, p as createUbeanClientApp, q as usePage, v as useUnheadHead, w as clearLocales, x as defineMiddleware, z as setI18nConfig } from "./app-Bc1d5Svw.js";
2
+ import { applyAppConfig, createDefaultAppConfig, defineApp, mergeAppConfig } from "./define-app.js";
3
+ import { computed, getCurrentInstance, markRaw, onMounted, onScopeDispose, onUnmounted, ref, shallowRef, watch } from "vue";
4
+ import { useRouter } from "vue-router";
5
+ import { ERROR_KEY, ErrorBoundary, LAYOUT_CHAIN_KEY, LOADING_KEY, LOCALIZE_PATH_KEY, LayoutChainRenderer, Link, PAGE_KEY, PageView, SSR_KEY, SlotView, TRANSITION_KEY, clearPageTransition, disablePageCache, enablePageCache, excludePageCache, getCacheEnabled, getCachedViewNames, getExcludedViewNames, getNamedPageWrapper, getNavigationType, getPageTransitionName, getReloadCounter, includePageCache, initCachedViewsFromRoutes, invalidatePageCache, isActiveRoute, isCacheEnabled, isPageCached, isPageExcluded, isReloading, reloadPage, resetNamedPageWrappers, resetRouteCache, resolveRoute, setPageTransition, supportsViewTransitions, ubeanVue, useCacheViews, usePageTransition, useReloadSignal, useViewTransition, useViewTransitionState, withViewTransition } from "@ubean/vue";
6
+ import { injectHead, injectHead as injectHead$1 } from "@unhead/vue";
7
+ import { createHead as createClientHead } from "@unhead/vue/client";
8
+ import { defineDataKey as defineDataKey$1 } from "@ubean/pages";
9
+ import { collectIslands, hydrateIsland, hydrateIslands } from "@ubean/islands/runtime";
10
+ //#region src/client.ts
11
+ const _global$1 = globalThis;
12
+ function getInitialPageData() {
13
+ if (typeof _global$1.document === "undefined") return null;
14
+ const el = _global$1.document.getElementById("__UBEAN_PAGE_DATA__");
15
+ if (!el) return _global$1.__UBEAN_PAGE_DATA__ ?? null;
16
+ try {
17
+ return JSON.parse(el.textContent || "null");
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ /**
23
+ * 从 DOM 中的 `<script id="__UBEAN_STATE__">` 读取 SSR 序列化的状态。
24
+ *
25
+ * 该状态由服务端 `defineApp({ serializeState })` 产生,用于在客户端 mount 前
26
+ * 水合到对应的库实例(如 Pinia 的 `pinia.state.value`)。
27
+ *
28
+ * 必须在 `app.mount()` 之前调用(在 `defineApp({ hydrateState })` 内部使用)。
29
+ *
30
+ * @returns 解析后的状态对象,或 `null`(无状态 / 解析失败 / 非 DOM 环境)
31
+ */
32
+ function getInitialState() {
33
+ if (typeof _global$1.document === "undefined") return null;
34
+ const el = _global$1.document.getElementById("__UBEAN_STATE__");
35
+ if (!el) return _global$1.__UBEAN_STATE__ ?? null;
36
+ try {
37
+ const text = el.textContent || "";
38
+ if (!text.trim()) return null;
39
+ return JSON.parse(text);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ //#endregion
45
+ //#region src/head.ts
46
+ function useHeadInstance() {
47
+ return injectHead$1();
48
+ }
49
+ //#endregion
50
+ //#region src/composables.ts
51
+ const _global = globalThis;
52
+ function createDataCacheStore() {
53
+ const cache = /* @__PURE__ */ new Map();
54
+ const tagIndex = /* @__PURE__ */ new Map();
55
+ const listeners = /* @__PURE__ */ new Set();
56
+ function notify() {
57
+ for (const fn of listeners) fn();
58
+ }
59
+ return {
60
+ get(key) {
61
+ return cache.get(key)?.data;
62
+ },
63
+ set(key, value, tags) {
64
+ cache.set(key, {
65
+ data: value,
66
+ timestamp: Date.now(),
67
+ tags
68
+ });
69
+ if (tags) for (const tag of tags) {
70
+ let set = tagIndex.get(tag);
71
+ if (!set) {
72
+ set = /* @__PURE__ */ new Set();
73
+ tagIndex.set(tag, set);
74
+ }
75
+ set.add(key);
76
+ }
77
+ notify();
78
+ },
79
+ has(key) {
80
+ return cache.has(key);
81
+ },
82
+ invalidate(keyOrTag) {
83
+ let count = 0;
84
+ if (typeof keyOrTag === "string") {
85
+ if (cache.has(keyOrTag)) {
86
+ const entry = cache.get(keyOrTag);
87
+ if (entry.tags) for (const t of entry.tags) {
88
+ const ts = tagIndex.get(t);
89
+ if (ts) ts.delete(keyOrTag);
90
+ }
91
+ cache.delete(keyOrTag);
92
+ count = 1;
93
+ }
94
+ const keys = tagIndex.get(keyOrTag);
95
+ if (keys) {
96
+ for (const k of keys) {
97
+ const entry = cache.get(k);
98
+ if (entry) {
99
+ if (entry.tags) for (const t of entry.tags) {
100
+ const ts = tagIndex.get(t);
101
+ if (ts) ts.delete(k);
102
+ }
103
+ cache.delete(k);
104
+ count++;
105
+ }
106
+ }
107
+ tagIndex.delete(keyOrTag);
108
+ }
109
+ } else {
110
+ const entry = cache.get(keyOrTag);
111
+ if (entry) {
112
+ if (entry.tags) for (const t of entry.tags) {
113
+ const ts = tagIndex.get(t);
114
+ if (ts) ts.delete(keyOrTag);
115
+ }
116
+ cache.delete(keyOrTag);
117
+ count = 1;
118
+ }
119
+ }
120
+ if (count > 0) notify();
121
+ return count;
122
+ },
123
+ clear() {
124
+ cache.clear();
125
+ tagIndex.clear();
126
+ notify();
127
+ },
128
+ subscribe(fn) {
129
+ listeners.add(fn);
130
+ return () => listeners.delete(fn);
131
+ },
132
+ getTimestamp(key) {
133
+ return cache.get(key)?.timestamp;
134
+ }
135
+ };
136
+ }
137
+ function createUseAsyncData(store) {
138
+ return function useAsyncData(keyOrFetcher, fetcherOrOptions, options) {
139
+ let key;
140
+ let fetcher;
141
+ let opts = {};
142
+ if (typeof keyOrFetcher === "string") {
143
+ key = keyOrFetcher;
144
+ fetcher = fetcherOrOptions;
145
+ opts = options || {};
146
+ } else {
147
+ fetcher = keyOrFetcher;
148
+ key = fetcherOrOptions?.key || Math.random().toString(36).slice(2);
149
+ opts = fetcherOrOptions || {};
150
+ }
151
+ const state = {
152
+ data: { value: store.get(key) ?? (opts.default ? opts.default() : void 0) },
153
+ error: { value: null },
154
+ loading: { value: false }
155
+ };
156
+ async function execute() {
157
+ state.loading.value = true;
158
+ state.error.value = null;
159
+ try {
160
+ const raw = await fetcher();
161
+ const value = opts.transform ? opts.transform(raw) : raw;
162
+ store.set(key, value, opts.tags);
163
+ state.data.value = value;
164
+ } catch (err) {
165
+ state.error.value = err instanceof Error ? err : new Error(String(err));
166
+ } finally {
167
+ state.loading.value = false;
168
+ }
169
+ }
170
+ function invalidate() {
171
+ store.invalidate(key);
172
+ }
173
+ if (!opts.lazy) {
174
+ if (!store.has(key)) execute();
175
+ }
176
+ store.subscribe(() => {
177
+ state.data.value = store.get(key) ?? (opts.default ? opts.default() : void 0);
178
+ });
179
+ return {
180
+ data: state.data,
181
+ error: state.error,
182
+ loading: state.loading,
183
+ refresh: execute,
184
+ invalidate
185
+ };
186
+ };
187
+ }
188
+ function invalidateCache(store, keyOrTag) {
189
+ return store.invalidate(keyOrTag);
190
+ }
191
+ function clearCache(store) {
192
+ store.clear();
193
+ }
194
+ function defineDataKey(key) {
195
+ return defineDataKey$1(key);
196
+ }
197
+ function createLinkHandler(ctx) {
198
+ return {
199
+ getProps: () => ({}),
200
+ async navigate(href, opts = {}) {
201
+ if (!ctx.__ubean_router) {
202
+ const loc = _global.location;
203
+ if (opts.replace) loc.replace(href);
204
+ else loc.href = href;
205
+ return;
206
+ }
207
+ if (opts.replace) await ctx.__ubean_router.replace(href);
208
+ else await ctx.__ubean_router.push(href);
209
+ },
210
+ async prefetch(_href) {}
211
+ };
212
+ }
213
+ function extractPageData() {
214
+ if (typeof _global.document === "undefined") return null;
215
+ const el = _global.document.getElementById("__UBEAN_PAGE_DATA__");
216
+ if (!el) return null;
217
+ try {
218
+ return JSON.parse(el.textContent || "null");
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
223
+ async function useServerData(fetcher) {
224
+ return fetcher();
225
+ }
226
+ function getInvalidatedKeysForAction(actionName, invalidationMap, store) {
227
+ const toInvalidate = invalidationMap[actionName];
228
+ if (!toInvalidate) return 0;
229
+ let count = 0;
230
+ for (const key of toInvalidate) count += store.invalidate(key);
231
+ return count;
232
+ }
233
+ //#endregion
234
+ //#region src/color-mode.ts
235
+ /**
236
+ * P9-21: Color mode (dark/light) support.
237
+ *
238
+ * Aligns with Nuxt `@nuxtjs/color-mode`. Provides:
239
+ * - A no-FOUC inline script injected into `<head>` that resolves the color
240
+ * mode (from cookie / localStorage / system preference) and sets the
241
+ * `<html>` class or `data-*` attribute **before** the page renders.
242
+ * - A `useColorMode()` composable for reactive access and toggling.
243
+ *
244
+ * The script runs synchronously in `<head>` so the correct class is set
245
+ * before the browser paints, preventing a flash of unstyled content.
246
+ *
247
+ * Usage (composable):
248
+ * ```typescript
249
+ * import { useColorMode } from 'ubean/runtime/color-mode';
250
+ *
251
+ * const colorMode = useColorMode();
252
+ * colorMode.value; // 'light' | 'dark' | ...
253
+ * colorMode.preference; // 'system' | 'light' | 'dark' | ...
254
+ * colorMode.toggle(); // cycle through modes
255
+ * ```
256
+ */
257
+ const DEFAULT_CONFIG$1 = {
258
+ preference: "system",
259
+ fallback: "light",
260
+ classPrefix: "",
261
+ classSuffix: "-mode",
262
+ storageKey: "ubean-color-mode",
263
+ cookieName: "ubean-color-mode",
264
+ dataValue: false,
265
+ modes: ["light", "dark"]
266
+ };
267
+ /** Merge user config with defaults. */
268
+ function resolveColorModeConfig(config) {
269
+ return {
270
+ ...DEFAULT_CONFIG$1,
271
+ ...config
272
+ };
273
+ }
274
+ /**
275
+ * Generate the no-FOUC inline script that resolves the color mode and sets
276
+ * the `<html>` class/attribute **before** the page renders.
277
+ *
278
+ * The script:
279
+ * 1. Reads the user's preference from a cookie (SSR-friendly) or localStorage.
280
+ * 2. If preference is 'system', checks `prefers-color-scheme`.
281
+ * 3. Falls back to `config.fallback` if system preference is unknown.
282
+ * 4. Sets the class or `data-*` attribute on `<html>`.
283
+ *
284
+ * This script is injected into `<head>` and runs synchronously.
285
+ */
286
+ function getColorModeScript(config) {
287
+ const modes = JSON.stringify(config.modes);
288
+ const fallback = JSON.stringify(config.fallback);
289
+ const storageKey = JSON.stringify(config.storageKey);
290
+ const cookieName = JSON.stringify(config.cookieName);
291
+ const classPrefix = JSON.stringify(config.classPrefix);
292
+ const classSuffix = JSON.stringify(config.classSuffix);
293
+ const setAttr = config.dataValue ? `el.setAttribute('data-color-mode', value);` : `var className = ${classPrefix} + value + ${classSuffix};
294
+ el.classList.add(className);`;
295
+ return `<script>(() => {
296
+ const modes = ${modes};
297
+ const fallback = ${fallback};
298
+ const storageKey = ${storageKey};
299
+ const cookieName = ${cookieName};
300
+ const classPrefix = ${classPrefix};
301
+ const classSuffix = ${classSuffix};
302
+ const el = document.documentElement;
303
+
304
+ function getCookie(name) {
305
+ const match = document.cookie.match(new RegExp('(^|;\\\\s*)' + name + '=([^;]+)'));
306
+ return match ? decodeURIComponent(match[2]) : null;
307
+ }
308
+
309
+ // 1. Read preference: cookie first (SSR), then localStorage
310
+ let preference = getCookie(cookieName) || localStorage.getItem(storageKey) || ${JSON.stringify(config.preference)};
311
+
312
+ // 2. Resolve the actual mode
313
+ let value;
314
+ if (preference === 'system') {
315
+ const media = window.matchMedia('(prefers-color-scheme: dark)');
316
+ value = media.matches ? 'dark' : (media.media !== '(prefers-color-scheme: dark)' ? 'light' : fallback);
317
+ } else {
318
+ value = modes.includes(preference) ? preference : fallback;
319
+ }
320
+
321
+ // 3. Set the class or data attribute on <html>
322
+ ${setAttr}
323
+ })();<\/script>`;
324
+ }
325
+ let _config$1 = DEFAULT_CONFIG$1;
326
+ let _preference = null;
327
+ let _value = null;
328
+ let _unknown = null;
329
+ let _forced = null;
330
+ let _initialized = false;
331
+ /** Configure the color mode module (called once during app initialization). */
332
+ function configureColorMode(config) {
333
+ _config$1 = resolveColorModeConfig(config);
334
+ return _config$1;
335
+ }
336
+ /** Get the current color mode config. */
337
+ function getColorModeConfig() {
338
+ return _config$1;
339
+ }
340
+ /**
341
+ * Read the current color mode from the DOM (set by the no-FOUC script).
342
+ * Returns `null` if not running in a browser or the mode can't be determined.
343
+ */
344
+ function readModeFromDom() {
345
+ if (typeof document === "undefined") return null;
346
+ if (_config$1.dataValue) return document.documentElement.getAttribute("data-color-mode");
347
+ for (const mode of _config$1.modes) {
348
+ const className = `${_config$1.classPrefix}${mode}${_config$1.classSuffix}`;
349
+ if (document.documentElement.classList.contains(className)) return mode;
350
+ }
351
+ return null;
352
+ }
353
+ /**
354
+ * Apply a color mode to the DOM (set class or data attribute on `<html>`).
355
+ */
356
+ function applyModeToDom(mode) {
357
+ if (typeof document === "undefined") return;
358
+ const el = document.documentElement;
359
+ if (_config$1.dataValue) {
360
+ el.setAttribute("data-color-mode", mode);
361
+ return;
362
+ }
363
+ for (const m of _config$1.modes) {
364
+ const className = `${_config$1.classPrefix}${m}${_config$1.classSuffix}`;
365
+ el.classList.remove(className);
366
+ }
367
+ const className = `${_config$1.classPrefix}${mode}${_config$1.classSuffix}`;
368
+ el.classList.add(className);
369
+ }
370
+ /**
371
+ * Detect system color preference via `prefers-color-scheme`.
372
+ * Returns the mode string or `null` if unknown.
373
+ */
374
+ function detectSystemMode() {
375
+ if (typeof window === "undefined" || !window.matchMedia) return null;
376
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
377
+ if (media.matches) return "dark";
378
+ if (media.media === "(prefers-color-scheme: dark)") return "light";
379
+ return null;
380
+ }
381
+ /**
382
+ * Persist the user's preference to localStorage and cookie.
383
+ */
384
+ function persistPreference(preference) {
385
+ if (typeof window === "undefined") return;
386
+ try {
387
+ localStorage.setItem(_config$1.storageKey, preference);
388
+ } catch {}
389
+ const expires = /* @__PURE__ */ new Date();
390
+ expires.setFullYear(expires.getFullYear() + 1);
391
+ document.cookie = `${_config$1.cookieName}=${encodeURIComponent(preference)}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
392
+ }
393
+ /**
394
+ * Initialize the color mode state. Called on first `useColorMode()` invocation.
395
+ * Reads the initial mode from the DOM (set by the no-FOUC script).
396
+ */
397
+ function initColorMode() {
398
+ if (_initialized) return;
399
+ _initialized = true;
400
+ let preference = _config$1.preference;
401
+ if (typeof window !== "undefined") try {
402
+ const stored = localStorage.getItem(_config$1.storageKey);
403
+ if (stored) preference = stored;
404
+ } catch {}
405
+ _preference = ref(preference);
406
+ _forced = ref(false);
407
+ const domMode = readModeFromDom();
408
+ _value = ref(domMode || _config$1.fallback);
409
+ _unknown = ref(false);
410
+ if (preference === "system") _unknown.value = detectSystemMode() === null;
411
+ if (!domMode) {
412
+ if (preference === "system") {
413
+ const system = detectSystemMode();
414
+ if (system) {
415
+ _value.value = system;
416
+ applyModeToDom(system);
417
+ } else applyModeToDom(_config$1.fallback);
418
+ } else applyModeToDom(preference);
419
+ }
420
+ }
421
+ /**
422
+ * Reactively access and control the color mode.
423
+ *
424
+ * Returns a `ColorMode` object with reactive refs and methods:
425
+ * - `preference` — the user's preference ('system', 'light', 'dark', ...)
426
+ * - `value` — the resolved mode (never 'system')
427
+ * - `unknown` — whether the system preference is unknown
428
+ * - `forced` — whether the mode is forced (e.g. by route meta)
429
+ * - `set(mode)` — set the preference and persist it
430
+ * - `toggle()` — cycle to the next mode
431
+ *
432
+ * @example
433
+ * ```typescript
434
+ * const colorMode = useColorMode();
435
+ * console.log(colorMode.value); // 'dark'
436
+ * colorMode.set('light');
437
+ * colorMode.toggle(); // cycles: light → dark → light
438
+ * ```
439
+ */
440
+ function useColorMode() {
441
+ initColorMode();
442
+ const preference = _preference;
443
+ const forced = _forced;
444
+ const unknown = _unknown;
445
+ const valueRef = _value;
446
+ const value = computed(() => {
447
+ if (forced.value) return valueRef.value;
448
+ if (preference.value === "system") return detectSystemMode() || _config$1.fallback;
449
+ return preference.value;
450
+ });
451
+ const stopWatch = watch(preference, (newPref) => {
452
+ if (forced.value) return;
453
+ persistPreference(newPref);
454
+ if (newPref === "system") {
455
+ const system = detectSystemMode();
456
+ unknown.value = system === null;
457
+ if (system) applyModeToDom(system);
458
+ } else {
459
+ unknown.value = false;
460
+ applyModeToDom(newPref);
461
+ }
462
+ }, { flush: "sync" });
463
+ let mediaQuery = null;
464
+ const handleMediaChange = (e) => {
465
+ if (preference.value !== "system" || forced.value) return;
466
+ const mode = e.matches ? "dark" : "light";
467
+ applyModeToDom(mode);
468
+ valueRef.value = mode;
469
+ };
470
+ if (typeof window !== "undefined" && window.matchMedia) {
471
+ mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
472
+ mediaQuery.addEventListener("change", handleMediaChange);
473
+ }
474
+ onScopeDispose(() => {
475
+ stopWatch();
476
+ if (mediaQuery) mediaQuery.removeEventListener("change", handleMediaChange);
477
+ });
478
+ function set(mode) {
479
+ preference.value = mode;
480
+ }
481
+ function toggle() {
482
+ const current = value.value;
483
+ const modes = _config$1.modes;
484
+ set(modes[(modes.indexOf(current) + 1) % modes.length]);
485
+ }
486
+ return {
487
+ preference,
488
+ value,
489
+ unknown,
490
+ forced,
491
+ set,
492
+ toggle
493
+ };
494
+ }
495
+ /**
496
+ * Force a color mode (e.g. from route meta `colorMode: 'dark'`).
497
+ * When forced, user changes are ignored until `unforceColorMode()` is called.
498
+ */
499
+ function forceColorMode(mode) {
500
+ if (!_initialized) initColorMode();
501
+ if (_value) _value.value = mode;
502
+ if (_forced) _forced.value = true;
503
+ applyModeToDom(mode);
504
+ }
505
+ /**
506
+ * Remove the forced color mode, returning to user preference.
507
+ */
508
+ function unforceColorMode() {
509
+ if (!_initialized) return;
510
+ if (_forced) _forced.value = false;
511
+ if (_preference) {
512
+ const pref = _preference.value;
513
+ if (pref === "system") {
514
+ const system = detectSystemMode();
515
+ if (system) applyModeToDom(system);
516
+ } else applyModeToDom(pref);
517
+ }
518
+ }
519
+ /**
520
+ * Reset the color mode module state (for testing).
521
+ * @internal
522
+ */
523
+ function _resetColorMode() {
524
+ _config$1 = DEFAULT_CONFIG$1;
525
+ _preference = null;
526
+ _value = null;
527
+ _unknown = null;
528
+ _forced = null;
529
+ _initialized = false;
530
+ }
531
+ //#endregion
532
+ //#region src/party-town.ts
533
+ /**
534
+ * P9-22: Third-party script optimization (Partytown integration).
535
+ *
536
+ * Aligns with Nuxt `@nuxtjs/scripts` and Astro Partytown integration.
537
+ *
538
+ * Provides:
539
+ * - `useScript(src, options)` composable for loading third-party scripts with
540
+ * strategies: `'load'` (immediate), `'idle'` (requestIdleCallback),
541
+ * `'visible'` (IntersectionObserver on a ref), `'manual'` (explicit call).
542
+ * - `getPartyTownScript(config)` generates the inline Partytown config script
543
+ * injected into `<head>` to initialize the Web Worker.
544
+ * - Partytown runs third-party scripts off the main thread, improving
545
+ * performance by preventing analytics/tag-manager scripts from blocking
546
+ * the main thread.
547
+ *
548
+ * Usage (composable):
549
+ * ```typescript
550
+ * import { useScript } from 'ubean/runtime/party-town';
551
+ *
552
+ * // Load Google Analytics via Partytown when browser is idle
553
+ * const { load, remove } = useScript('https://www.googletagmanager.com/gtag/js?id=GA_ID', {
554
+ * trigger: 'idle',
555
+ * partytown: true,
556
+ * attrs: { 'data-ga-id': 'GA_ID' }
557
+ * });
558
+ * ```
559
+ */
560
+ const DEFAULT_CONFIG = {
561
+ enabled: false,
562
+ libPath: "~partytown",
563
+ forward: [],
564
+ debug: false,
565
+ logScriptExecution: false,
566
+ nonBlocking: true
567
+ };
568
+ /** Merge user config with defaults. */
569
+ function resolvePartyTownConfig(config) {
570
+ return {
571
+ ...DEFAULT_CONFIG,
572
+ ...config
573
+ };
574
+ }
575
+ /**
576
+ * Generate the inline Partytown config script for `<head>`.
577
+ *
578
+ * This script configures `window.partytown` **before** the Partytown lib
579
+ * snippet loads. The Partytown lib is loaded via a separate `<script>`
580
+ * tag that reads `window.partytown` for configuration.
581
+ *
582
+ * Returns an empty string if Partytown is not enabled.
583
+ */
584
+ function getPartyTownScript(config) {
585
+ if (!config.enabled) return "";
586
+ const settings = [];
587
+ if (config.libPath !== DEFAULT_CONFIG.libPath) settings.push(`lib: ${JSON.stringify(`${config.libPath}/`)}`);
588
+ if (config.forward.length > 0) settings.push(`forward: ${JSON.stringify(config.forward)}`);
589
+ if (config.mainAccess && config.mainAccess.length > 0) settings.push(`mainAccess: ${JSON.stringify(config.mainAccess)}`);
590
+ if (config.debug) settings.push(`debug: true`);
591
+ if (config.logScriptExecution) settings.push(`logScriptExecution: true`);
592
+ if (!config.nonBlocking) settings.push(`nonBlocking: false`);
593
+ return `<script>partytown = ${settings.length > 0 ? `{ ${settings.join(", ")} }` : "{}"};<\/script>
594
+ <script src="${`${config.libPath}/partytown.js`}" defer><\/script>`;
595
+ }
596
+ /**
597
+ * Generate the full HTML for Partytown initialization.
598
+ * Used by the Vite plugin to inject into `<head>`.
599
+ */
600
+ function getPartyTownHeadContent(config) {
601
+ return getPartyTownScript(config);
602
+ }
603
+ /** Create a script element with the given src and options. */
604
+ function createScriptElement(src, options) {
605
+ const script = document.createElement("script");
606
+ script.src = src;
607
+ if (options.partytown) script.type = "text/partytown";
608
+ else if (options.type) script.type = options.type;
609
+ if (options.async !== void 0) script.async = options.async;
610
+ else if (!options.partytown && options.type !== "module") script.async = true;
611
+ if (options.defer) script.defer = true;
612
+ if (options.crossorigin) script.crossOrigin = options.crossorigin;
613
+ if (options.referrerPolicy) script.referrerPolicy = options.referrerPolicy;
614
+ if (options.attrs) for (const [key, value] of Object.entries(options.attrs)) script.setAttribute(key, value);
615
+ return script;
616
+ }
617
+ /** Load a script element and return a promise that resolves on load. */
618
+ function loadScriptElement(script, container) {
619
+ return new Promise((resolve, reject) => {
620
+ script.addEventListener("load", () => resolve(), { once: true });
621
+ script.addEventListener("error", (e) => reject(e), { once: true });
622
+ container.appendChild(script);
623
+ });
624
+ }
625
+ /**
626
+ * Load a third-party script with a loading strategy.
627
+ *
628
+ * @param src - The script URL
629
+ * @param options - Loading options
630
+ * @returns Script control object with `load()`, `remove()`, and reactive state
631
+ *
632
+ * @example
633
+ * ```typescript
634
+ * // Load immediately with Partytown
635
+ * useScript('https://www.googletagmanager.com/gtag/js?id=GA_ID', {
636
+ * partytown: true,
637
+ * trigger: 'load'
638
+ * });
639
+ *
640
+ * // Load when browser is idle
641
+ * const { load } = useScript('/heavy-script.js', { trigger: 'idle' });
642
+ *
643
+ * // Load when element is visible
644
+ * const targetRef = ref<HTMLElement | null>(null);
645
+ * useScript('/analytics.js', { trigger: 'visible', target: targetRef });
646
+ * ```
647
+ */
648
+ function useScript(src, options = {}) {
649
+ const script = ref(null);
650
+ const loaded = ref(false);
651
+ const error = ref(false);
652
+ const trigger = options.trigger || "load";
653
+ let observer = null;
654
+ let idleCallbackId = null;
655
+ let loadPromise = null;
656
+ let isRemoved = false;
657
+ function doLoad() {
658
+ if (script.value || loaded.value || error.value || isRemoved) return;
659
+ if (typeof document === "undefined") return;
660
+ const el = createScriptElement(src, options);
661
+ script.value = markRaw(el);
662
+ loadPromise = loadScriptElement(el, document.head).then(() => {
663
+ if (isRemoved) return;
664
+ loaded.value = true;
665
+ }).catch(() => {
666
+ if (isRemoved) return;
667
+ error.value = true;
668
+ });
669
+ }
670
+ function load() {
671
+ doLoad();
672
+ }
673
+ function remove() {
674
+ isRemoved = true;
675
+ if (observer) {
676
+ observer.disconnect();
677
+ observer = null;
678
+ }
679
+ if (idleCallbackId !== null && typeof cancelIdleCallback === "function") {
680
+ cancelIdleCallback(idleCallbackId);
681
+ idleCallbackId = null;
682
+ }
683
+ if (script.value && script.value.parentNode) script.value.parentNode.removeChild(script.value);
684
+ script.value = null;
685
+ loaded.value = false;
686
+ error.value = false;
687
+ loadPromise = null;
688
+ }
689
+ function waitForLoad() {
690
+ if (!loadPromise) doLoad();
691
+ return loadPromise || Promise.resolve();
692
+ }
693
+ if (typeof window !== "undefined") {
694
+ if (trigger === "load") {
695
+ if (getCurrentInstance()) onMounted(() => doLoad());
696
+ else doLoad();
697
+ } else if (trigger === "idle") {
698
+ const scheduleLoad = () => {
699
+ if (typeof requestIdleCallback === "function") idleCallbackId = requestIdleCallback(() => {
700
+ idleCallbackId = null;
701
+ doLoad();
702
+ });
703
+ else setTimeout(() => doLoad(), 1);
704
+ };
705
+ if (getCurrentInstance()) onMounted(() => scheduleLoad());
706
+ else scheduleLoad();
707
+ } else if (trigger === "visible") {
708
+ const targetRef = options.target;
709
+ if (!targetRef) {
710
+ if (getCurrentInstance()) onMounted(() => doLoad());
711
+ else doLoad();
712
+ } else {
713
+ const setupObserver = () => {
714
+ const el = targetRef.value;
715
+ if (!el) return;
716
+ observer = new IntersectionObserver((entries) => {
717
+ for (const entry of entries) if (entry.isIntersecting) {
718
+ doLoad();
719
+ observer?.disconnect();
720
+ observer = null;
721
+ break;
722
+ }
723
+ }, {
724
+ rootMargin: options.rootMargin || "0px",
725
+ threshold: options.threshold ?? 0
726
+ });
727
+ observer.observe(el);
728
+ };
729
+ if (getCurrentInstance()) onMounted(() => {
730
+ if (targetRef.value) setupObserver();
731
+ else setTimeout(setupObserver, 0);
732
+ });
733
+ else setupObserver();
734
+ }
735
+ }
736
+ }
737
+ if (getCurrentInstance()) onUnmounted(() => {
738
+ if (options.removeOnUnmount) remove();
739
+ else {
740
+ if (observer) {
741
+ observer.disconnect();
742
+ observer = null;
743
+ }
744
+ if (idleCallbackId !== null && typeof cancelIdleCallback === "function") {
745
+ cancelIdleCallback(idleCallbackId);
746
+ idleCallbackId = null;
747
+ }
748
+ }
749
+ });
750
+ return {
751
+ script,
752
+ loaded,
753
+ error,
754
+ load,
755
+ remove,
756
+ waitForLoad
757
+ };
758
+ }
759
+ /**
760
+ * Global Partytown configuration state.
761
+ * Set by `configurePartyTown()` during app initialization.
762
+ */
763
+ let _config = DEFAULT_CONFIG;
764
+ /** Configure the Partytown module globally. */
765
+ function configurePartyTown(config) {
766
+ _config = resolvePartyTownConfig(config);
767
+ return _config;
768
+ }
769
+ /** Get the current Partytown config. */
770
+ function getPartyTownConfig() {
771
+ return _config;
772
+ }
773
+ /** Check if Partytown is enabled. */
774
+ function isPartyTownEnabled() {
775
+ return _config.enabled;
776
+ }
777
+ /** Reset Partytown module state (for testing). @internal */
778
+ function _resetPartyTown() {
779
+ _config = DEFAULT_CONFIG;
780
+ }
781
+ //#endregion
782
+ //#region src/search.ts
783
+ /**
784
+ * P9-26: Full-text search via Pagefind integration.
785
+ *
786
+ * Aligns with Astro Pagefind integration. Pagefind is a static-site search
787
+ * tool that indexes built HTML files at build time and provides a client-side
788
+ * search API at runtime.
789
+ *
790
+ * Provides:
791
+ * - `useSearch(options?)` composable wrapping the Pagefind browser API with
792
+ * reactive `results` / `loading` / `error` state and built-in debounce.
793
+ * - `initPagefind(options)` loads and initializes the Pagefind browser library
794
+ * from the generated `/pagefind/` assets.
795
+ * - `resolveSearchConfig(config)` merges user config with defaults.
796
+ *
797
+ * The Pagefind index is generated at build time by the Vite plugin's
798
+ * `closeBundle` hook (runs `npx pagefind --site <outputDir>`). At runtime,
799
+ * the browser dynamically imports `/pagefind/pagefind-modern.js`.
800
+ *
801
+ * Usage (composable):
802
+ * ```typescript
803
+ * const { results, search, loading, clear } = useSearch({ debounce: 200 });
804
+ *
805
+ * // In a template: <input @input="search($event.target.value)" />
806
+ * // results.value is an array of SearchResult
807
+ * ```
808
+ */
809
+ const DEFAULT_RUNTIME_CONFIG = {
810
+ pagefindPath: "/pagefind/pagefind-modern.js",
811
+ debounce: 150,
812
+ limit: 10
813
+ };
814
+ let runtimeConfig = { ...DEFAULT_RUNTIME_CONFIG };
815
+ /**
816
+ * Configure the search runtime globally. Call this in `app.ts` (client-side)
817
+ * to override defaults before any `useSearch()` call.
818
+ */
819
+ function configureSearch(config) {
820
+ runtimeConfig = {
821
+ ...runtimeConfig,
822
+ ...config
823
+ };
824
+ }
825
+ /** Get the current runtime search config. */
826
+ function getSearchConfig() {
827
+ return runtimeConfig;
828
+ }
829
+ /** Resolve a user-provided config object into a full SearchRuntimeConfig. */
830
+ function resolveSearchConfig(config) {
831
+ if (config === true || config === void 0) return { ...DEFAULT_RUNTIME_CONFIG };
832
+ return {
833
+ ...DEFAULT_RUNTIME_CONFIG,
834
+ ...config
835
+ };
836
+ }
837
+ let pagefindPromise = null;
838
+ let pagefindLoaded = false;
839
+ /**
840
+ * Dynamically import the Pagefind browser library.
841
+ *
842
+ * The library is generated at build time into `<outputDir>/pagefind/` and
843
+ * served at `/pagefind/pagefind-modern.js`. In dev mode or when Pagefind is
844
+ * not enabled, the import will fail — callers should catch and show a message.
845
+ */
846
+ async function initPagefind(options = {}) {
847
+ if (pagefindPromise) return pagefindPromise;
848
+ const path = options.pagefindPath || runtimeConfig.pagefindPath;
849
+ pagefindPromise = import(
850
+ /* @vite-ignore */
851
+ path
852
+ ).then((mod) => {
853
+ pagefindLoaded = true;
854
+ if (typeof mod?.init === "function") mod.init();
855
+ return mod;
856
+ }).catch((err) => {
857
+ pagefindPromise = null;
858
+ throw new Error(`[ubean/search] Failed to load Pagefind from "${path}". Ensure \`search: true\` is set in ubean.config.ts and the site has been built. Original error: ${err instanceof Error ? err.message : String(err)}`);
859
+ });
860
+ return pagefindPromise;
861
+ }
862
+ /** Check whether the Pagefind browser library has been loaded. */
863
+ function isPagefindLoaded() {
864
+ return pagefindLoaded;
865
+ }
866
+ /**
867
+ * Execute a raw Pagefind search. Returns normalized `SearchResult[]`.
868
+ *
869
+ * Exposed for advanced use cases where the reactive composable is not needed
870
+ * (e.g. server-side API routes that proxy search requests).
871
+ */
872
+ async function executeSearch(query, options = {}) {
873
+ if (!query.trim()) return [];
874
+ const pagefind = await initPagefind({ pagefindPath: options.pagefindPath });
875
+ const searchOptions = {};
876
+ if (options.filters?.filters) searchOptions.filters = options.filters.filters;
877
+ if (options.filters?.sort) searchOptions.sort = options.filters.sort;
878
+ const searchResult = await pagefind.search(query, searchOptions);
879
+ const limit = options.limit ?? runtimeConfig.limit;
880
+ return await Promise.all((searchResult.results || []).slice(0, limit).map(async (r, index) => {
881
+ const data = typeof r.data === "function" ? await r.data() : r.data || {};
882
+ return {
883
+ id: r.id ?? index,
884
+ url: data.url || r.url || "",
885
+ excerpt: data.excerpt || r.excerpt || "",
886
+ meta: data.meta || r.meta || {},
887
+ score: r.score ?? 0,
888
+ wordCount: data.word_count || r.word_count,
889
+ data: r.data
890
+ };
891
+ }));
892
+ }
893
+ /**
894
+ * `useSearch` — reactive Pagefind search composable.
895
+ *
896
+ * Lazily loads the Pagefind browser library on first search and provides
897
+ * reactive `results`, `loading`, and `error` state. Built-in debounce
898
+ * prevents excessive searches while the user is typing.
899
+ *
900
+ * @example
901
+ * ```vue
902
+ * <script setup>
903
+ * const { search, results, loading } = useSearch({ debounce: 200 });
904
+ * <\/script>
905
+ *
906
+ * <template>
907
+ * <input
908
+ * type="search"
909
+ * placeholder="Search..."
910
+ * @input="search($event.target.value)"
911
+ * />
912
+ * <div v-if="loading">Searching...</div>
913
+ * <ul v-else>
914
+ * <li v-for="r in results" :key="r.id">
915
+ * <a :href="r.url" v-html="r.meta.title || r.url" />
916
+ * <p v-html="r.excerpt" />
917
+ * </li>
918
+ * </ul>
919
+ * </template>
920
+ * ```
921
+ */
922
+ function useSearch(options = {}) {
923
+ const debounce = options.debounce ?? runtimeConfig.debounce;
924
+ const limit = options.limit ?? runtimeConfig.limit;
925
+ const defaultFilters = options.filters;
926
+ const query = ref("");
927
+ const results = shallowRef([]);
928
+ const loading = ref(false);
929
+ const error = ref(null);
930
+ const ready = ref(false);
931
+ let debounceTimer = null;
932
+ let lastQuery = "";
933
+ async function doSearch(q, filters) {
934
+ const trimmed = q.trim();
935
+ if (!trimmed) {
936
+ results.value = [];
937
+ query.value = "";
938
+ error.value = null;
939
+ loading.value = false;
940
+ return;
941
+ }
942
+ if (trimmed === lastQuery) return;
943
+ lastQuery = trimmed;
944
+ loading.value = true;
945
+ error.value = null;
946
+ try {
947
+ const found = await executeSearch(trimmed, {
948
+ filters: filters ?? defaultFilters,
949
+ limit,
950
+ pagefindPath: options.pagefindPath
951
+ });
952
+ results.value = found;
953
+ ready.value = true;
954
+ } catch (err) {
955
+ error.value = err instanceof Error ? err.message : String(err);
956
+ results.value = [];
957
+ } finally {
958
+ loading.value = false;
959
+ }
960
+ }
961
+ function search(q, filters) {
962
+ query.value = q;
963
+ if (debounce > 0) {
964
+ if (debounceTimer) clearTimeout(debounceTimer);
965
+ return new Promise((resolve) => {
966
+ debounceTimer = setTimeout(() => {
967
+ debounceTimer = null;
968
+ doSearch(q, filters).finally(resolve);
969
+ }, debounce);
970
+ });
971
+ }
972
+ return doSearch(q, filters);
973
+ }
974
+ function clear() {
975
+ if (debounceTimer) {
976
+ clearTimeout(debounceTimer);
977
+ debounceTimer = null;
978
+ }
979
+ query.value = "";
980
+ lastQuery = "";
981
+ results.value = [];
982
+ loading.value = false;
983
+ error.value = null;
984
+ }
985
+ async function preload() {
986
+ try {
987
+ await initPagefind({ pagefindPath: options.pagefindPath });
988
+ ready.value = true;
989
+ } catch (err) {
990
+ error.value = err instanceof Error ? err.message : String(err);
991
+ }
992
+ }
993
+ if (options.immediate) search(options.immediate);
994
+ return {
995
+ query,
996
+ results,
997
+ loading,
998
+ error,
999
+ ready,
1000
+ search,
1001
+ clear,
1002
+ preload
1003
+ };
1004
+ }
1005
+ /** Reset all internal state (for unit tests only). */
1006
+ function _resetSearch() {
1007
+ pagefindPromise = null;
1008
+ pagefindLoaded = false;
1009
+ runtimeConfig = { ...DEFAULT_RUNTIME_CONFIG };
1010
+ }
1011
+ /**
1012
+ * Inject a mock Pagefind module for testing.
1013
+ *
1014
+ * The mock replaces the dynamic `import()` so tests can simulate search
1015
+ * results without a real Pagefind build. Also calls `mock.init()` to mimic
1016
+ * the real library's initialization.
1017
+ */
1018
+ function _setPagefindMock(mock) {
1019
+ pagefindLoaded = true;
1020
+ if (typeof mock?.init === "function") mock.init();
1021
+ pagefindPromise = Promise.resolve(mock);
1022
+ }
1023
+ //#endregion
1024
+ export { ERROR_KEY, ErrorBoundary, Head, LAYOUT_CHAIN_KEY, LOADING_KEY, LOCALIZE_PATH_KEY, LayoutChainRenderer, Link, PAGE_KEY, PageView, SSR_KEY, SlotView, TRANSITION_KEY, _resetColorMode, _resetPartyTown, _resetSearch, _setPagefindMock, addLocale, applyAppConfig, clearCache, clearLocales, clearPageTransition, collectIslands, configureColorMode, configurePartyTown, configureSearch, createClientHead, createDataCacheStore, createDefaultAppConfig, createLinkHandler, createUbeanClientApp, createUbeanRouter, createUbeanSSRApp, createUseAsyncData, defineApp, defineDataKey, defineLocale, defineMeta, defineMiddleware, definePage, detectBrowserLocale, detectLocale, disablePageCache, enablePageCache, excludePageCache, executeSearch, extractLocaleFromPath, extractPageData, forceColorMode, getCacheEnabled, getCachedViewNames, getColorModeConfig, getColorModeScript, getDefaultLocale, getExcludedViewNames, getI18nConfig, getInitialPageData, getInitialState, getInvalidatedKeysForAction, getLocale, getLocaleDir, getLocaleName, getNamedPageWrapper, getNavigationType, getPageTransitionName, getPartyTownConfig, getPartyTownHeadContent, getPartyTownScript, getRegisteredLocales, getReloadCounter, getSearchConfig, hydrateIsland, hydrateIslands, includePageCache, initCachedViewsFromRoutes, initClientI18n, initPagefind, injectHead, invalidateCache, invalidatePageCache, isActiveRoute, isCacheEnabled, isPageCached, isPageExcluded, isPagefindLoaded, isPartyTownEnabled, isReloading, localizePath, mergeAppConfig, mergeLocale, onLocaleChange, reloadPage, resetNamedPageWrappers, resetRouteCache, resolveColorModeConfig, resolvePartyTownConfig, resolveRoute, resolveSearchConfig, setI18nConfig, setLocale, setPageTransition, supportsViewTransitions, switchLocalePath, t, ubeanVue, unforceColorMode, useCacheViews, useColorMode, useUnheadHead as useHead, useHeadInstance, useI18n, useLocalePath, usePage, usePageTransition, useReloadSignal, useRouter, useScript, useSearch, useSeoMeta, useServerData, useSwitchLocalePath, useViewTransition, useViewTransitionState, withViewTransition };