@ubean/runtime 0.1.2 → 0.1.4

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.
@@ -0,0 +1,892 @@
1
+ import { useHead as useUnheadHead, useSeoMeta } from "@unhead/vue";
2
+ import { LOCALE_DATA_ID } from "@ubean/pages";
3
+ import { KeepAlive, Transition, computed, createApp, createSSRApp, defineComponent, h, inject, markRaw, provide, reactive, ref, shallowRef, watchEffect } from "vue";
4
+ import { RouterLink, RouterView, createMemoryHistory, createRouter, createWebHistory, useRoute, useRouter } from "vue-router";
5
+ import { Head } from "@unhead/vue/components";
6
+ import { addLocale, clearLocales, defineLocale, detectBrowserLocale, detectLocale, extractLocaleFromPath, getDefaultLocale, getI18nConfig, getLocale, getLocaleDir, getLocaleName, getRegisteredLocales, localizePath, mergeLocale, onLocaleChange, setI18nConfig, setLocale, switchLocalePath, t, useI18n } from "@ubean/i18n";
7
+ //#region src/i18n.ts
8
+ const _global$1 = globalThis;
9
+ function hydrateLocale() {
10
+ if (typeof _global$1.document === "undefined") return {
11
+ locale: null,
12
+ dir: "ltr"
13
+ };
14
+ const el = _global$1.document.getElementById(LOCALE_DATA_ID);
15
+ if (!el) return {
16
+ locale: null,
17
+ dir: "ltr"
18
+ };
19
+ try {
20
+ const data = JSON.parse(el.textContent || "null");
21
+ if (data && typeof data.locale === "string") return {
22
+ locale: data.locale,
23
+ dir: data.dir === "rtl" ? "rtl" : "ltr",
24
+ messages: data.messages,
25
+ availableLocales: Array.isArray(data.availableLocales) ? data.availableLocales : void 0
26
+ };
27
+ } catch {
28
+ return {
29
+ locale: null,
30
+ dir: "ltr"
31
+ };
32
+ }
33
+ return {
34
+ locale: null,
35
+ dir: "ltr"
36
+ };
37
+ }
38
+ function syncHtmlLang(locale, dir) {
39
+ if (typeof _global$1.document === "undefined") return;
40
+ const html = _global$1.document.documentElement;
41
+ if (html) {
42
+ html.setAttribute("lang", locale);
43
+ html.setAttribute("dir", dir);
44
+ }
45
+ }
46
+ const { locale: hydratedLocale, dir: hydratedDir, messages: hydratedMessages, availableLocales: hydratedAvailable } = hydrateLocale();
47
+ if (hydratedLocale) {
48
+ if (hydratedAvailable && hydratedAvailable.length > 0) {
49
+ const sorted = [...hydratedAvailable].sort((a, b) => (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0));
50
+ for (const loc of sorted) {
51
+ const isCurrent = loc.code === hydratedLocale;
52
+ defineLocale({
53
+ code: loc.code,
54
+ messages: isCurrent && hydratedMessages ? hydratedMessages : {},
55
+ name: loc.name,
56
+ dir: loc.dir,
57
+ isDefault: loc.isDefault
58
+ });
59
+ }
60
+ } else if (hydratedMessages && typeof hydratedMessages === "object") defineLocale({
61
+ code: hydratedLocale,
62
+ messages: hydratedMessages,
63
+ dir: hydratedDir
64
+ });
65
+ setLocale(hydratedLocale);
66
+ syncHtmlLang(hydratedLocale, hydratedDir);
67
+ }
68
+ const localeRef = ref(getLocale());
69
+ onLocaleChange((newLocale) => {
70
+ localeRef.value = newLocale;
71
+ syncHtmlLang(newLocale, getLocaleDir(newLocale));
72
+ });
73
+ function useI18n$1() {
74
+ const core = useI18n();
75
+ const localeDir = computed(() => getLocaleDir(localeRef.value));
76
+ const localeName = computed(() => getLocaleName(localeRef.value));
77
+ function translate(key, params) {
78
+ localeRef.value;
79
+ return core.t(key, params);
80
+ }
81
+ return {
82
+ locale: localeRef,
83
+ get fallbackLocale() {
84
+ return core.fallbackLocale;
85
+ },
86
+ get availableLocales() {
87
+ return core.availableLocales;
88
+ },
89
+ t: translate,
90
+ setLocale(locale) {
91
+ core.setLocale(locale);
92
+ },
93
+ getLocale() {
94
+ return localeRef.value;
95
+ },
96
+ onLocaleChange(callback) {
97
+ return core.onLocaleChange(callback);
98
+ },
99
+ getLocaleDir(locale) {
100
+ return core.getLocaleDir(locale);
101
+ },
102
+ getLocaleName(locale) {
103
+ return core.getLocaleName(locale);
104
+ },
105
+ localeDir,
106
+ localeName
107
+ };
108
+ }
109
+ function defineLocale$1(definition) {
110
+ const result = defineLocale(definition);
111
+ localeRef.value = getLocale();
112
+ return result;
113
+ }
114
+ function t$1(key, params) {
115
+ return t(key, params);
116
+ }
117
+ function setLocale$1(locale) {
118
+ setLocale(locale);
119
+ }
120
+ function getLocale$1() {
121
+ return localeRef.value;
122
+ }
123
+ function onLocaleChange$1(callback) {
124
+ return onLocaleChange(callback);
125
+ }
126
+ function getLocaleDir$1(locale) {
127
+ return getLocaleDir(locale);
128
+ }
129
+ function getLocaleName$1(locale) {
130
+ return getLocaleName(locale);
131
+ }
132
+ function getRegisteredLocales$1() {
133
+ return getRegisteredLocales();
134
+ }
135
+ function detectLocale$1(acceptLanguage) {
136
+ return detectLocale(acceptLanguage);
137
+ }
138
+ function detectBrowserLocale$1() {
139
+ return detectBrowserLocale();
140
+ }
141
+ function addLocale$1(code, messages, options) {
142
+ addLocale(code, messages, options);
143
+ }
144
+ function mergeLocale$1(code, messages) {
145
+ mergeLocale(code, messages);
146
+ }
147
+ function clearLocales$1() {
148
+ clearLocales();
149
+ localeRef.value = getLocale();
150
+ }
151
+ function getI18nConfig$1() {
152
+ return getI18nConfig();
153
+ }
154
+ function setI18nConfig$1(config) {
155
+ setI18nConfig(config);
156
+ }
157
+ function localizePath$1(path, locale) {
158
+ return localizePath(path, locale);
159
+ }
160
+ function switchLocalePath$1(newLocale, currentPath) {
161
+ return switchLocalePath(newLocale, currentPath);
162
+ }
163
+ function getDefaultLocale$1() {
164
+ return getDefaultLocale();
165
+ }
166
+ function extractLocaleFromPath$1(path) {
167
+ return extractLocaleFromPath(path);
168
+ }
169
+ function useSwitchLocalePath() {
170
+ return computed(() => (newLocale) => {
171
+ return switchLocalePath(newLocale, typeof _global$1.window !== "undefined" ? _global$1.window.location.pathname : "/");
172
+ });
173
+ }
174
+ function useLocalePath() {
175
+ return computed(() => (path, locale) => {
176
+ return localizePath(path, locale || localeRef.value);
177
+ });
178
+ }
179
+ //#endregion
180
+ //#region src/router.ts
181
+ function createUbeanRouter(options) {
182
+ const base = options.base || "/";
183
+ const router = createRouter({
184
+ history: options.ssr ? createMemoryHistory(base) : createWebHistory(base),
185
+ routes: options.routes,
186
+ scrollBehavior(_to, _from, savedPosition) {
187
+ if (savedPosition) return savedPosition;
188
+ if (_to.hash) return {
189
+ el: _to.hash,
190
+ behavior: "smooth"
191
+ };
192
+ return { top: 0 };
193
+ }
194
+ });
195
+ if (options.setup) options.setup(router);
196
+ if (options.ssr && options.initialUrl) router.push(options.initialUrl);
197
+ return router;
198
+ }
199
+ //#endregion
200
+ //#region src/cache-views.ts
201
+ /**
202
+ * Page cache (keep-alive) store — a module-level singleton.
203
+ *
204
+ * ubean uses flat single-layer layout, so all page components are direct
205
+ * children of a single layout's `<RouterView>`. A `<keep-alive :include="...">`
206
+ * wraps that `<RouterView>`; the `include` list is the reactive array exposed
207
+ * here. Caching is keyed by the page's **route name**, which is injected as the
208
+ * component `name` at render time (see `createLayoutWrapper` in `app.ts`).
209
+ *
210
+ * In addition to the `include` list, an `exclude` list is also maintained.
211
+ * `<keep-alive :exclude="...">` skips caching for matched names even if they
212
+ * appear in `include`. This is useful for the "reload current page" pattern:
213
+ * temporarily push a route name into `exclude`, wait a tick, then remove it —
214
+ * the cached instance is pruned and the page remounts fresh on next visit.
215
+ *
216
+ * Usage:
217
+ * // declarative (compile-time): enable caching for a page
218
+ * definePage({ cache: true })
219
+ *
220
+ * // runtime: toggle caching imperatively
221
+ * import { enablePageCache, disablePageCache } from '@ubean/runtime';
222
+ * enablePageCache('DashboardIndex');
223
+ * disablePageCache('DashboardIndex');
224
+ *
225
+ * // reload a cached page (clears its instance, forces remount)
226
+ * import { resetRouteCache } from '@ubean/runtime';
227
+ * await resetRouteCache('DashboardIndex');
228
+ */
229
+ const _g = globalThis;
230
+ const _cachedViewNames = _g.__ubeanCachedViewNames ??= ref([]);
231
+ const _excludedViewNames = _g.__ubeanExcludedViewNames ??= ref([]);
232
+ const _cacheEnabled = _g.__ubeanCacheEnabled ??= ref(true);
233
+ /**
234
+ * Composable for reactive access to the page cache store.
235
+ *
236
+ * Safe to call inside any component `setup()`; the returned computeds are
237
+ * tracked by the rendering effect so `<keep-alive :include>` stays in sync.
238
+ */
239
+ function useCacheViews() {
240
+ return {
241
+ cachedViews: computed(() => _cacheEnabled.value ? _cachedViewNames.value : []),
242
+ cachedViewNames: computed(() => _cachedViewNames.value),
243
+ excludedViews: computed(() => _excludedViewNames.value),
244
+ enabled: computed(() => _cacheEnabled.value),
245
+ enable: () => {
246
+ _cacheEnabled.value = true;
247
+ },
248
+ disable: () => {
249
+ _cacheEnabled.value = false;
250
+ },
251
+ add: (name) => {
252
+ if (name && !_cachedViewNames.value.includes(name)) _cachedViewNames.value = [..._cachedViewNames.value, name];
253
+ },
254
+ remove: (name) => {
255
+ _cachedViewNames.value = _cachedViewNames.value.filter((n) => n !== name);
256
+ },
257
+ has: (name) => _cachedViewNames.value.includes(name),
258
+ clear: () => {
259
+ _cachedViewNames.value = [];
260
+ },
261
+ addExclude: (name) => {
262
+ if (name && !_excludedViewNames.value.includes(name)) _excludedViewNames.value = [..._excludedViewNames.value, name];
263
+ },
264
+ removeExclude: (name) => {
265
+ _excludedViewNames.value = _excludedViewNames.value.filter((n) => n !== name);
266
+ },
267
+ hasExclude: (name) => _excludedViewNames.value.includes(name),
268
+ clearExclude: () => {
269
+ _excludedViewNames.value = [];
270
+ }
271
+ };
272
+ }
273
+ /** Enable keep-alive caching for a specific page (by route name). */
274
+ function enablePageCache(name) {
275
+ if (name && !_cachedViewNames.value.includes(name)) _cachedViewNames.value = [..._cachedViewNames.value, name];
276
+ }
277
+ /** Disable keep-alive caching for a specific page (by route name). */
278
+ function disablePageCache(name) {
279
+ _cachedViewNames.value = _cachedViewNames.value.filter((n) => n !== name);
280
+ }
281
+ /**
282
+ * Temporarily exclude a page from keep-alive (forces its cached instance
283
+ * to be pruned on next render). Pair with `includePageCache()` to restore.
284
+ *
285
+ * Useful for the "reload current page" pattern: push the route name into
286
+ * the exclude list, wait a tick, then remove it — the cached instance is
287
+ * pruned and the page remounts fresh on next render.
288
+ */
289
+ function excludePageCache(name) {
290
+ if (name && !_excludedViewNames.value.includes(name)) _excludedViewNames.value = [..._excludedViewNames.value, name];
291
+ }
292
+ /** Remove a route name from the cache exclude list (restores caching). */
293
+ function includePageCache(name) {
294
+ _excludedViewNames.value = _excludedViewNames.value.filter((n) => n !== name);
295
+ }
296
+ /** Whether a specific page is currently excluded from cache. */
297
+ function isPageExcluded(name) {
298
+ return _excludedViewNames.value.includes(name);
299
+ }
300
+ /**
301
+ * Reload a cached page by temporarily excluding it from keep-alive.
302
+ *
303
+ * Pattern (mirrors `routeStore.resetRouteCache` in soybean-unify):
304
+ * 1. Push the route name into the exclude list (prunes cached instance).
305
+ * 2. Wait one tick (let `<keep-alive>` reconcile).
306
+ * 3. Remove the route name from the exclude list.
307
+ *
308
+ * The page's `definePage({ cache: true })` declaration (or a prior
309
+ * `enablePageCache(name)`) is still in effect, so the page will be re-cached
310
+ * on the next visit.
311
+ *
312
+ * @param name Route name. If omitted, uses the current route's name
313
+ * (requires a Vue component setup context — fall back to
314
+ * passing the name explicitly from outside Vue).
315
+ * @param delay Milliseconds to wait between exclude and include. Defaults
316
+ * to 50ms (matches soybean-unify's pattern).
317
+ */
318
+ async function resetRouteCache(name, delay = 50) {
319
+ const target = name;
320
+ if (!target) return;
321
+ if (!_cachedViewNames.value.includes(target)) return;
322
+ excludePageCache(target);
323
+ await new Promise((resolve) => setTimeout(resolve, delay));
324
+ includePageCache(target);
325
+ }
326
+ /**
327
+ * Invalidate cached page instance(s).
328
+ *
329
+ * - With a `name`: removes that page from the cache (its instance is pruned
330
+ * on next navigation).
331
+ * - Without a `name`: clears all cached pages.
332
+ *
333
+ * Note: the page will be re-cached on next visit if its `definePage({ cache: true })`
334
+ * is still in effect or `enablePageCache(name)` is called again.
335
+ */
336
+ function invalidatePageCache(name) {
337
+ if (name) disablePageCache(name);
338
+ else _cachedViewNames.value = [];
339
+ }
340
+ /** Whether a specific page is currently cached. */
341
+ function isPageCached(name) {
342
+ return _cachedViewNames.value.includes(name);
343
+ }
344
+ /** Whether page caching is globally enabled. */
345
+ function isCacheEnabled() {
346
+ return _cacheEnabled.value;
347
+ }
348
+ /** Reactive ref of cached route names — for advanced use cases. */
349
+ function getCachedViewNames() {
350
+ return _cachedViewNames;
351
+ }
352
+ /** Reactive ref of excluded route names — for advanced use cases. */
353
+ function getExcludedViewNames() {
354
+ return _excludedViewNames;
355
+ }
356
+ /** Reactive ref of the global cache-enabled flag. */
357
+ function getCacheEnabled() {
358
+ return _cacheEnabled;
359
+ }
360
+ /**
361
+ * Seed the cache include list from route metadata.
362
+ *
363
+ * Called once during app bootstrap (`createUbeanApp`) to honor pages that
364
+ * declared `definePage({ cache: true })`. Routes without `meta.cache` are
365
+ * left untouched so runtime toggling remains possible.
366
+ */
367
+ function initCachedViewsFromRoutes(routes) {
368
+ const initial = [];
369
+ for (const route of routes) if (route.meta?.cache === true && typeof route.name === "string" && route.name) {
370
+ if (!initial.includes(route.name)) initial.push(route.name);
371
+ }
372
+ if (initial.length > 0) {
373
+ const existing = new Set(_cachedViewNames.value);
374
+ for (const name of initial) if (!existing.has(name)) existing.add(name);
375
+ _cachedViewNames.value = [...existing];
376
+ }
377
+ }
378
+ //#endregion
379
+ //#region src/page-runtime.ts
380
+ /**
381
+ * Global page runtime config — animation mode + reload signal.
382
+ *
383
+ * These module-level singletons mirror the patterns found in soybean-unify's
384
+ * `themeStore.pageAnimateMode` and `appStore.reloadSignal`:
385
+ *
386
+ * - `pageAnimateMode` is a global transition name applied to every
387
+ * `<PageView />` unless overridden by the `transition` prop or
388
+ * `route.meta.transition`. Set to `'none'` (or empty string) to disable
389
+ * transitions globally.
390
+ *
391
+ * - `reloadSignal` is a counter that increments each time `reloadPage()` is
392
+ * called. `<PageView />` watches this counter (via `:key`) and remounts
393
+ * the current page when it changes — combined with `resetRouteCache()` to
394
+ * prune the cached keep-alive instance so the remount is fresh.
395
+ *
396
+ * These are intentionally framework-provided singletons (not pinia stores)
397
+ * because ubean ships without a state management library and the values are
398
+ * globally unique by nature.
399
+ */
400
+ const _pageTransitionName = ref("");
401
+ const _reloadCounter = ref(0);
402
+ /**
403
+ * Composable for reactive access to the global page transition name.
404
+ *
405
+ * Used by `<PageView />` as the fallback transition name when neither the
406
+ * `transition` prop nor `route.meta.transition` is set.
407
+ *
408
+ * Layout components can also read this to display a settings UI:
409
+ *
410
+ * ```ts
411
+ * const transition = usePageTransition();
412
+ * // transition.name.value === 'fade-slide'
413
+ * transition.set('zoom-fadein');
414
+ * transition.clear();
415
+ * ```
416
+ */
417
+ function usePageTransition() {
418
+ return {
419
+ name: computed(() => _pageTransitionName.value),
420
+ set: (name) => {
421
+ _pageTransitionName.value = name === "none" ? "" : name;
422
+ },
423
+ clear: () => {
424
+ _pageTransitionName.value = "";
425
+ },
426
+ enabled: computed(() => _pageTransitionName.value !== "")
427
+ };
428
+ }
429
+ /** Imperatively set the global page transition name. */
430
+ function setPageTransition(name) {
431
+ _pageTransitionName.value = name === "none" ? "" : name;
432
+ }
433
+ /** Imperatively clear the global page transition name (disables transitions). */
434
+ function clearPageTransition() {
435
+ _pageTransitionName.value = "";
436
+ }
437
+ /** Reactive ref of the global transition name — for advanced use cases. */
438
+ function getPageTransitionName() {
439
+ return _pageTransitionName;
440
+ }
441
+ /**
442
+ * Internal flag — set to `true` while a reload is in progress so consumers
443
+ * can show a loading indicator if they wish.
444
+ */
445
+ const _reloading = ref(false);
446
+ /**
447
+ * Composable for reactive access to the page reload signal.
448
+ *
449
+ * `<PageView />` uses the returned `counter` as part of its render key, so
450
+ * each `reload()` call forces the current page to remount. Combined with
451
+ * `resetRouteCache()` (called internally), the cached keep-alive instance is
452
+ * pruned first, so the remount is fresh — equivalent to the
453
+ * `v-if="appStore.reloadSignal"` pattern in soybean-unify's layout-content.
454
+ *
455
+ * Usage from any component:
456
+ *
457
+ * ```ts
458
+ * const { reload } = useReloadSignal();
459
+ * async function onReloadClick() {
460
+ * await reload(); // reloads the current page
461
+ * }
462
+ * ```
463
+ *
464
+ * @param routeNameGetter Optional function returning the current route name.
465
+ * If provided, used to call `resetRouteCache(name)`.
466
+ * If omitted, `reload()` only bumps the counter.
467
+ */
468
+ function useReloadSignal(routeNameGetter) {
469
+ return {
470
+ counter: computed(() => _reloadCounter.value),
471
+ reloading: computed(() => _reloading.value),
472
+ reload: async (routeName, duration = 300) => {
473
+ _reloading.value = true;
474
+ try {
475
+ const name = routeName ?? routeNameGetter?.();
476
+ if (name) await resetRouteCache(name, 50);
477
+ await new Promise((resolve) => setTimeout(resolve, duration));
478
+ _reloadCounter.value++;
479
+ } finally {
480
+ _reloading.value = false;
481
+ }
482
+ }
483
+ };
484
+ }
485
+ /**
486
+ * Imperatively trigger a page reload.
487
+ *
488
+ * - `name`: route name to reload. If provided, the cached keep-alive instance
489
+ * is pruned first via `resetRouteCache(name)`. If omitted, only the reload
490
+ * counter is bumped (forces remount but does not prune cache).
491
+ * - `duration`: milliseconds to wait for transition/cleanup. Defaults to 300.
492
+ */
493
+ async function reloadPage(name, duration = 300) {
494
+ _reloading.value = true;
495
+ try {
496
+ if (name) await resetRouteCache(name, 50);
497
+ await new Promise((resolve) => setTimeout(resolve, duration));
498
+ _reloadCounter.value++;
499
+ } finally {
500
+ _reloading.value = false;
501
+ }
502
+ }
503
+ /** Reactive ref of the reload counter — for advanced use cases. */
504
+ function getReloadCounter() {
505
+ return _reloadCounter;
506
+ }
507
+ /** Whether a reload is currently in progress. */
508
+ function isReloading() {
509
+ return _reloading.value;
510
+ }
511
+ //#endregion
512
+ //#region src/view-transitions.ts
513
+ const _global = globalThis;
514
+ let _typesSupported = null;
515
+ function supportsTransitionTypes() {
516
+ if (_typesSupported !== null) return _typesSupported;
517
+ if (!supportsViewTransitions()) {
518
+ _typesSupported = false;
519
+ return false;
520
+ }
521
+ try {
522
+ let calledWithObject = false;
523
+ const doc = _global.document;
524
+ const orig = doc.startViewTransition;
525
+ doc.startViewTransition = function(opts) {
526
+ if (opts && typeof opts === "object" && "update" in opts) calledWithObject = true;
527
+ return {
528
+ finished: Promise.resolve(),
529
+ ready: Promise.resolve(),
530
+ updateCallbackDone: Promise.resolve(),
531
+ skipTransition() {}
532
+ };
533
+ };
534
+ try {
535
+ doc.startViewTransition({
536
+ update: () => {},
537
+ types: ["test"]
538
+ });
539
+ } catch {}
540
+ doc.startViewTransition = orig;
541
+ _typesSupported = calledWithObject;
542
+ } catch {
543
+ _typesSupported = false;
544
+ }
545
+ return _typesSupported;
546
+ }
547
+ function supportsViewTransitions() {
548
+ if (typeof _global.document === "undefined") return false;
549
+ return typeof _global.document.startViewTransition === "function";
550
+ }
551
+ async function withViewTransition(callback, options = {}) {
552
+ const { enabled = true, types } = options;
553
+ if (!enabled || !supportsViewTransitions()) return callback();
554
+ const doc = _global.document;
555
+ const useTypesApi = types && types.length > 0 && supportsTransitionTypes();
556
+ let result;
557
+ let callbackError;
558
+ let callbackFailed = false;
559
+ const updateCallback = async () => {
560
+ try {
561
+ result = await callback();
562
+ } catch (e) {
563
+ callbackFailed = true;
564
+ callbackError = e;
565
+ throw e;
566
+ }
567
+ };
568
+ let transition;
569
+ if (useTypesApi) transition = doc.startViewTransition({
570
+ update: updateCallback,
571
+ types
572
+ });
573
+ else transition = doc.startViewTransition(updateCallback);
574
+ try {
575
+ await transition.finished;
576
+ } catch (err) {
577
+ if (callbackFailed) throw callbackError;
578
+ throw err;
579
+ }
580
+ if (callbackFailed) throw callbackError;
581
+ return result;
582
+ }
583
+ function getNavigationType() {
584
+ try {
585
+ const nav = _global.navigation;
586
+ if (nav?.currentEntry && nav?.transitionType) return nav.transitionType;
587
+ } catch {}
588
+ return "push";
589
+ }
590
+ function useViewTransitionState(name) {
591
+ return { style: `view-transition-name: ${name};` };
592
+ }
593
+ //#endregion
594
+ //#region src/app.ts
595
+ const PAGE_KEY = Symbol("ubean-page");
596
+ const TRANSITION_KEY = Symbol("ubean-transition");
597
+ const SSR_KEY = Symbol("ubean-ssr");
598
+ function createLayoutWrapper(resolveLayoutComponent, defaultLayout) {
599
+ const layoutCache = /* @__PURE__ */ new Map();
600
+ const layoutComp = shallowRef(null);
601
+ const currentLayoutName = ref(null);
602
+ async function loadLayout(name) {
603
+ const resolved = name === void 0 ? defaultLayout : name;
604
+ if (resolved === false || resolved == null) {
605
+ layoutComp.value = null;
606
+ return;
607
+ }
608
+ if (layoutCache.has(resolved)) {
609
+ layoutComp.value = layoutCache.get(resolved) || null;
610
+ return;
611
+ }
612
+ const comp = await resolveLayoutComponent(resolved);
613
+ if (comp) {
614
+ const raw = markRaw(comp);
615
+ layoutCache.set(resolved, raw);
616
+ layoutComp.value = raw;
617
+ } else {
618
+ layoutCache.set(resolved, null);
619
+ layoutComp.value = null;
620
+ }
621
+ }
622
+ return defineComponent({
623
+ name: "UbeanLayoutView",
624
+ setup() {
625
+ const route = useRoute();
626
+ watchEffect(() => {
627
+ const layoutName = route.meta.layout;
628
+ if (layoutName !== currentLayoutName.value) {
629
+ currentLayoutName.value = layoutName === void 0 ? defaultLayout : layoutName;
630
+ loadLayout(currentLayoutName.value);
631
+ }
632
+ });
633
+ return () => {
634
+ if (!layoutComp.value) return h(PageView);
635
+ return h(layoutComp.value);
636
+ };
637
+ }
638
+ });
639
+ }
640
+ /**
641
+ * PageView — a globally-registered component that renders the matched route
642
+ * page, wrapped with `<Transition>` and `<KeepAlive>`.
643
+ *
644
+ * Layouts use `<PageView />` (instead of `<slot />`) to render the matched
645
+ * page. This follows vue-router's v-slot pattern, mirroring soybean-unify's
646
+ * `layouts/modules/layout-content/index.vue`:
647
+ *
648
+ * ```html
649
+ * <RouterView v-slot="{ Component, route }">
650
+ * <Transition :name="pageAnimateMode" mode="out-in">
651
+ * <KeepAlive :include="cachedRoutes" :exclude="excludeCachedRoutes">
652
+ * <component :is="Component" v-if="reloadSignal" :key="tabId" />
653
+ * </KeepAlive>
654
+ * </Transition>
655
+ * </RouterView>
656
+ * ```
657
+ *
658
+ * In ubean, the equivalents are:
659
+ * - `pageAnimateMode` → global `usePageTransition()` ref + `transition` prop + `route.meta.transition`
660
+ * - `cachedRoutes` / `excludeCachedRoutes` → `useCacheViews().cachedViews` / `excludedViews`
661
+ * - `reloadSignal` → `useReloadSignal().counter` (incremented by `reloadPage()`)
662
+ * - `tabId` → `reloadKey` prop + reload counter (forces remount when changed)
663
+ *
664
+ * Props:
665
+ * - `transition`: transition name (string) or `false` to disable.
666
+ * Priority: prop > `route.meta.transition` > global `usePageTransition()`.
667
+ * - `reloadKey`: a reactive key — changing it forces remount (reload).
668
+ * When omitted, falls back to `route.fullPath` + global reload counter.
669
+ * - `mode`: Vue `<Transition>` mode. Defaults to `'out-in'`.
670
+ *
671
+ * On SSR, KeepAlive and Transition are skipped (client-only concepts).
672
+ */
673
+ const PageView = defineComponent({
674
+ name: "PageView",
675
+ props: {
676
+ transition: {
677
+ type: [String, Boolean],
678
+ default: void 0
679
+ },
680
+ reloadKey: {
681
+ type: [String, Number],
682
+ default: void 0
683
+ },
684
+ mode: {
685
+ type: String,
686
+ default: "out-in"
687
+ }
688
+ },
689
+ setup(props) {
690
+ const { cachedViews, excludedViews } = useCacheViews();
691
+ const globalTransition = usePageTransition();
692
+ const { counter: reloadCounter } = useReloadSignal();
693
+ const ssr = inject(SSR_KEY, false);
694
+ return () => {
695
+ return h(RouterView, null, { default: ({ Component, route }) => {
696
+ if (!Component) return null;
697
+ if (ssr) return Component;
698
+ const comp = Component.type;
699
+ const componentProps = Component.props || {};
700
+ const routeTransition = route.meta?.transition;
701
+ const transitionName = props.transition === false ? void 0 : props.transition || routeTransition || globalTransition.name.value || void 0;
702
+ const pageVNode = h(comp, {
703
+ ...componentProps,
704
+ key: props.reloadKey ?? `${route.fullPath}#${reloadCounter.value}`
705
+ });
706
+ const keepAliveProps = { include: cachedViews.value };
707
+ if (excludedViews.value.length > 0) keepAliveProps.exclude = excludedViews.value;
708
+ const keepAliveVNode = h(KeepAlive, keepAliveProps, { default: () => pageVNode });
709
+ if (transitionName) return h(Transition, {
710
+ name: transitionName,
711
+ mode: props.mode
712
+ }, { default: () => keepAliveVNode });
713
+ return keepAliveVNode;
714
+ } });
715
+ };
716
+ }
717
+ });
718
+ const Link = defineComponent({
719
+ name: "Link",
720
+ props: {
721
+ to: {
722
+ type: [String, Object],
723
+ default: void 0
724
+ },
725
+ href: {
726
+ type: String,
727
+ default: void 0
728
+ },
729
+ replace: {
730
+ type: Boolean,
731
+ default: false
732
+ },
733
+ prefetch: {
734
+ type: Boolean,
735
+ default: false
736
+ },
737
+ activeClass: {
738
+ type: String,
739
+ default: "router-link-active"
740
+ },
741
+ exactActiveClass: {
742
+ type: String,
743
+ default: "router-link-exact-active"
744
+ },
745
+ noActiveClass: {
746
+ type: Boolean,
747
+ default: false
748
+ }
749
+ },
750
+ setup(props, { slots, attrs }) {
751
+ const resolvedTo = computed(() => {
752
+ const path = props.to ?? props.href ?? "";
753
+ if (typeof path === "string" && (path.startsWith("http") || path.startsWith("//") || path.startsWith("#"))) return path;
754
+ return localizePath$1(path);
755
+ });
756
+ const isExternal = computed(() => {
757
+ const path = String(resolvedTo.value);
758
+ return path.startsWith("http") || path.startsWith("//") || path.startsWith("#");
759
+ });
760
+ return () => {
761
+ if (isExternal.value) return h("a", {
762
+ ...attrs,
763
+ href: resolvedTo.value,
764
+ target: "_blank",
765
+ rel: "noopener noreferrer"
766
+ }, slots.default ? slots.default() : void 0);
767
+ return h(RouterLink, {
768
+ ...attrs,
769
+ to: resolvedTo.value,
770
+ replace: props.replace,
771
+ activeClass: props.noActiveClass ? "" : props.activeClass,
772
+ exactActiveClass: props.noActiveClass ? "" : props.exactActiveClass
773
+ }, slots.default);
774
+ };
775
+ }
776
+ });
777
+ const Head$1 = Head;
778
+ function createRootComponent(LayoutWrapper, page, transitionOpts, isSSR) {
779
+ return defineComponent({
780
+ name: isSSR ? "UbeanSSRApp" : "UbeanAppRoot",
781
+ setup() {
782
+ provide(PAGE_KEY, page);
783
+ provide(TRANSITION_KEY, transitionOpts);
784
+ provide(SSR_KEY, isSSR);
785
+ return () => h(LayoutWrapper);
786
+ }
787
+ });
788
+ }
789
+ function createUbeanApp(options) {
790
+ if (!options.head) throw new Error("[ubean] createUbeanApp requires a head instance from @unhead/vue/client");
791
+ const initial = options.initialPage || {
792
+ component: "",
793
+ props: {},
794
+ params: {},
795
+ url: "/"
796
+ };
797
+ const head = options.head;
798
+ const transitionOpts = options.viewTransitions === false ? { enabled: false } : options.viewTransitions === true || options.viewTransitions == null ? { enabled: true } : options.viewTransitions;
799
+ const page = reactive({ ...initial });
800
+ const router = createUbeanRouter({
801
+ routes: options.routes,
802
+ ssr: false,
803
+ setup: options.routerSetup
804
+ });
805
+ const LayoutWrapper = createLayoutWrapper(options.resolveLayoutComponent, options.defaultLayout || null);
806
+ initCachedViewsFromRoutes(options.routes);
807
+ const RootComponent = createRootComponent(LayoutWrapper, page, transitionOpts, false);
808
+ const app = options.hydrate ? createSSRApp(RootComponent) : createApp(RootComponent);
809
+ app.use(head);
810
+ app.use(router);
811
+ app.component("Link", Link);
812
+ app.component("PageView", PageView);
813
+ app.config.globalProperties.$ubean = {
814
+ page,
815
+ head,
816
+ router
817
+ };
818
+ return {
819
+ app,
820
+ router,
821
+ head,
822
+ page
823
+ };
824
+ }
825
+ function createUbeanSSRApp(initialPage, options) {
826
+ if (!options.head) throw new Error("[ubean] createUbeanSSRApp requires a head instance from @unhead/vue/server");
827
+ const head = options.head;
828
+ const page = reactive({ ...initialPage });
829
+ const router = createUbeanRouter({
830
+ routes: options.routes,
831
+ ssr: true,
832
+ initialUrl: initialPage.url,
833
+ setup: options.routerSetup
834
+ });
835
+ const LayoutWrapper = createLayoutWrapper(options.resolveLayoutComponent, options.defaultLayout || null);
836
+ initCachedViewsFromRoutes(options.routes);
837
+ const app = createSSRApp(createRootComponent(LayoutWrapper, page, { enabled: false }, true));
838
+ app.use(head);
839
+ app.use(router);
840
+ app.component("Link", Link);
841
+ app.component("PageView", PageView);
842
+ app.config.globalProperties.$ubean = {
843
+ page,
844
+ head,
845
+ router
846
+ };
847
+ return {
848
+ app,
849
+ router,
850
+ head
851
+ };
852
+ }
853
+ function usePage() {
854
+ const route = useRoute();
855
+ const pageData = inject(PAGE_KEY, null);
856
+ return reactive({
857
+ url: computed(() => route.fullPath),
858
+ params: computed(() => route.params),
859
+ query: computed(() => route.query),
860
+ meta: computed(() => route.meta),
861
+ props: computed(() => pageData?.props || {}),
862
+ component: computed(() => route.meta.pageName || pageData?.component || ""),
863
+ errors: computed(() => pageData?.errors || null)
864
+ });
865
+ }
866
+ function useRouter$1() {
867
+ const router = useRouter();
868
+ return {
869
+ ...router,
870
+ push: (url, opts) => opts?.replace ? router.replace(url) : router.push(url),
871
+ replace: (url) => router.replace(url),
872
+ back: () => router.back(),
873
+ forward: () => router.forward(),
874
+ refresh: () => router.go(0),
875
+ get current() {
876
+ return router.currentRoute.value;
877
+ },
878
+ get navigating() {
879
+ return false;
880
+ }
881
+ };
882
+ }
883
+ function useViewTransition() {
884
+ const opts = inject(TRANSITION_KEY, { enabled: true });
885
+ return {
886
+ enabled: opts.enabled !== false && supportsViewTransitions(),
887
+ supports: supportsViewTransitions(),
888
+ options: opts
889
+ };
890
+ }
891
+ //#endregion
892
+ export { setI18nConfig$1 as $, initCachedViewsFromRoutes as A, defineLocale$1 as B, disablePageCache as C, getCachedViewNames as D, getCacheEnabled as E, resetRouteCache as F, getI18nConfig$1 as G, detectLocale$1 as H, useCacheViews as I, getLocaleName$1 as J, getLocale$1 as K, createUbeanRouter as L, isCacheEnabled as M, isPageCached as N, getExcludedViewNames as O, isPageExcluded as P, onLocaleChange$1 as Q, addLocale$1 as R, useReloadSignal as S, excludePageCache as T, extractLocaleFromPath$1 as U, detectBrowserLocale$1 as V, getDefaultLocale$1 as W, localizePath$1 as X, getRegisteredLocales$1 as Y, mergeLocale$1 as Z, getReloadCounter as _, createUbeanSSRApp as a, useSwitchLocalePath as at, setPageTransition as b, useSeoMeta as c, getNavigationType as d, setLocale$1 as et, supportsViewTransitions as f, getPageTransitionName as g, clearPageTransition as h, createUbeanApp as i, useLocalePath as it, invalidatePageCache as j, includePageCache as k, useUnheadHead as l, withViewTransition as m, Link as n, t$1 as nt, usePage as o, useViewTransitionState as p, getLocaleDir$1 as q, PageView as r, useI18n$1 as rt, useRouter$1 as s, Head$1 as t, switchLocalePath$1 as tt, useViewTransition as u, isReloading as v, enablePageCache as w, usePageTransition as x, reloadPage as y, clearLocales$1 as z };