@antelopejs/dms-frontend 0.9.1 → 0.9.3

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 (103) hide show
  1. package/README.md +9 -1
  2. package/dist/cancellation.js +2 -1
  3. package/dist/config.js +11 -7
  4. package/dist/derived-outputs.js +6 -113
  5. package/dist/dev-modules.js +3 -2
  6. package/dist/discovery.js +19 -12
  7. package/dist/fs-sync.js +1 -2
  8. package/dist/internal/guards.d.ts +5 -0
  9. package/dist/internal/guards.js +22 -0
  10. package/dist/internal/locale-catalogs.d.ts +6 -0
  11. package/dist/internal/locale-catalogs.js +115 -0
  12. package/dist/layer-aliases.js +2 -1
  13. package/dist/layers.js +2 -1
  14. package/dist/locale-files.js +9 -4
  15. package/dist/manifest-secrets.js +11 -4
  16. package/dist/manifest.js +11 -5
  17. package/dist/materialize.js +6 -5
  18. package/dist/module-set.js +2 -4
  19. package/dist/ports.js +8 -1
  20. package/dist/private-values.js +26 -17
  21. package/dist/project-modules.js +17 -11
  22. package/dist/renderer-range.js +2 -1
  23. package/dist/server-process.js +17 -16
  24. package/dist/update-check.js +23 -16
  25. package/dist/verify-source-runner.js +2 -2
  26. package/dist/workspace-setup.js +1 -2
  27. package/dist/workspace.js +11 -10
  28. package/package.json +7 -4
  29. package/skills/dms-frontend/SKILL.md +1 -1
  30. package/skills/dms-frontend/cases.json +77 -0
  31. package/templates/vue/app-runtime.ts +88 -7
  32. package/templates/vue/client-chunking.mjs +0 -5
  33. package/templates/vue/client-server-guard.mjs +10 -13
  34. package/templates/vue/entry-weight.mjs +12 -14
  35. package/templates/vue/frontend-module.ts +159 -3563
  36. package/templates/vue/frontend-runtime/async-data-entry.ts +320 -0
  37. package/templates/vue/frontend-runtime/async-data.ts +404 -0
  38. package/templates/vue/frontend-runtime/components.ts +386 -0
  39. package/templates/vue/frontend-runtime/cookies.ts +125 -0
  40. package/templates/vue/frontend-runtime/data-access.ts +132 -0
  41. package/templates/vue/frontend-runtime/dms-error.ts +11 -0
  42. package/templates/vue/frontend-runtime/errors.ts +145 -0
  43. package/templates/vue/frontend-runtime/fetch.ts +306 -0
  44. package/templates/vue/frontend-runtime/module-registration.ts +247 -0
  45. package/templates/vue/frontend-runtime/paged-data.ts +191 -0
  46. package/templates/vue/frontend-runtime/payload.ts +172 -0
  47. package/templates/vue/frontend-runtime/route.ts +328 -0
  48. package/templates/vue/frontend-runtime/sdk.ts +300 -0
  49. package/templates/vue/frontend-runtime/state.ts +167 -0
  50. package/templates/vue/frontend-runtime/types.ts +361 -0
  51. package/templates/vue/frontend-runtime/ui.ts +247 -0
  52. package/templates/vue/globals.d.ts +5 -0
  53. package/templates/vue/head-order.mjs +11 -13
  54. package/templates/vue/icon-bundle.ts +31 -0
  55. package/templates/vue/message-subset.ts +65 -0
  56. package/templates/vue/network-error.ts +4 -0
  57. package/templates/vue/prepare-types.mjs +0 -8
  58. package/templates/vue/server/auth/backend.mjs +4 -2
  59. package/templates/vue/server/auth/establish-endpoints.mjs +14 -9
  60. package/templates/vue/server/auth/session.mjs +4 -3
  61. package/templates/vue/server/auth-hooks.mjs +6 -6
  62. package/templates/vue/server/backend-proxies.mjs +6 -6
  63. package/templates/vue/server/backend-response.mjs +15 -14
  64. package/templates/vue/server/built-files.mjs +10 -8
  65. package/templates/vue/server/cli-report.mjs +7 -5
  66. package/templates/vue/server/client-error.mjs +11 -8
  67. package/templates/vue/server/client-manifest.mjs +9 -8
  68. package/templates/vue/server/content.mjs +2 -1
  69. package/templates/vue/server/dev-private-files.mjs +20 -15
  70. package/templates/vue/server/dev-styles.mjs +22 -25
  71. package/templates/vue/server/email-bundle-watch.mjs +0 -12
  72. package/templates/vue/server/email.mjs +20 -15
  73. package/templates/vue/server/favicon.mjs +12 -11
  74. package/templates/vue/server/homepage.mjs +8 -10
  75. package/templates/vue/server/icons.mjs +18 -9
  76. package/templates/vue/server/in-place-render.mjs +6 -8
  77. package/templates/vue/server/listen-host.mjs +2 -2
  78. package/templates/vue/server/module-event.mjs +5 -5
  79. package/templates/vue/server/module-routes.mjs +11 -7
  80. package/templates/vue/server/module-server.mjs +8 -7
  81. package/templates/vue/server/module-testing.mjs +7 -7
  82. package/templates/vue/server/page-props.mjs +14 -14
  83. package/templates/vue/server/path-patterns.mjs +4 -4
  84. package/templates/vue/server/proxy-compression.mjs +21 -13
  85. package/templates/vue/server/proxy-headers.mjs +5 -3
  86. package/templates/vue/server/proxy-redirects.mjs +7 -6
  87. package/templates/vue/server/proxy-transfers.mjs +15 -11
  88. package/templates/vue/server/public-endpoints.mjs +27 -19
  89. package/templates/vue/server/public-options.mjs +9 -7
  90. package/templates/vue/server/request-failure.mjs +4 -6
  91. package/templates/vue/server/request-routing.mjs +8 -11
  92. package/templates/vue/server/request-target.mjs +6 -6
  93. package/templates/vue/server/route-rules.mjs +7 -5
  94. package/templates/vue/server/static-files.mjs +19 -10
  95. package/templates/vue/server/web-body.mjs +3 -4
  96. package/templates/vue/server.mjs +32 -19
  97. package/templates/vue/sfc-global-types.mjs +27 -24
  98. package/templates/vue/ssr-renderer.ts +10 -1
  99. package/templates/vue/ui-inertia-page.mjs +7 -7
  100. package/templates/vue/ui-kbd.mjs +10 -10
  101. package/templates/vue/ui-lazy-icons.mjs +44 -0
  102. package/templates/vue/ui-request-state.mjs +9 -9
  103. package/templates/vue/vite.config.ts +2 -0
@@ -0,0 +1,361 @@
1
+ import { type InertiaLinkProps } from "@inertiajs/vue3";
2
+ import { type FetchOptions, ofetch } from "ofetch";
3
+ import {
4
+ type App,
5
+ type ComputedRef,
6
+ type MaybeRefOrGetter,
7
+ type Ref,
8
+ watch,
9
+ } from "vue";
10
+ import type { Composer } from "vue-i18n";
11
+ import { type $fetch } from "./fetch";
12
+
13
+ export interface DmsPageRoute {
14
+ displayName?: string;
15
+ description?: string;
16
+ fullSlug?: string;
17
+ icon?: string;
18
+ layoutUrl?: string;
19
+ }
20
+ export interface DmsPagePayload {
21
+ route?: DmsPageRoute;
22
+ componentName?: string;
23
+ layoutUrl?: string;
24
+ layout?: DmsPageLayout;
25
+ [key: string]: unknown;
26
+ }
27
+ export interface DmsSharedPagePayload {
28
+ siteLayout?: unknown;
29
+ siteLayoutTree?: unknown;
30
+ quickActions?: unknown;
31
+ modules?: unknown;
32
+ isOwner?: boolean;
33
+ }
34
+ export interface DmsLayoutDefinition {
35
+ componentName?: string;
36
+ options?: Record<string, unknown>;
37
+ }
38
+ export interface DmsPageLayout {
39
+ componentName?: string;
40
+ layout?: DmsLayoutDefinition;
41
+ [key: string]: unknown;
42
+ }
43
+ export interface DmsPageProps {
44
+ path: string;
45
+ page: DmsPagePayload;
46
+ /**
47
+ * The site layout. An Inertia once prop: a visit carries it over from the
48
+ * previous page until it expires, unless the server sends a newer one.
49
+ */
50
+ shared?: DmsSharedPagePayload;
51
+ user?: DmsUser;
52
+ session?: DmsSession;
53
+ error?: DmsErrorData;
54
+ }
55
+ export interface PublicRuntimeConfig extends Record<string, unknown> {}
56
+ export interface RuntimeConfig extends Record<string, unknown> {}
57
+ export interface DmsAppConfig extends Record<string, unknown> {}
58
+ export interface DmsUser extends Record<string, unknown> {}
59
+ export interface DmsSession {
60
+ accountId: string;
61
+ activeTenantId?: string;
62
+ /**
63
+ * When the frontend server last wrote this session, in milliseconds since
64
+ * the epoch. A page restored from history or from the prefetch cache
65
+ * carries an older session than the one the app already holds, and does not
66
+ * replace it.
67
+ */
68
+ updatedAt?: number;
69
+ }
70
+ export interface DmsRuntimeConfig extends RuntimeConfig {
71
+ public: PublicRuntimeConfig;
72
+ }
73
+ export interface DmsModuleOptions {
74
+ public: Record<string, unknown>;
75
+ }
76
+ export type DmsLocaleLoader = (
77
+ locale: string,
78
+ ) => Promise<Record<string, unknown>>;
79
+ export type DmsNavigationOptions = Pick<
80
+ InertiaLinkProps,
81
+ | "async"
82
+ | "component"
83
+ | "data"
84
+ | "except"
85
+ | "headers"
86
+ | "method"
87
+ | "onBefore"
88
+ | "onCancel"
89
+ | "onCancelToken"
90
+ | "onError"
91
+ | "onFinish"
92
+ | "onProgress"
93
+ | "onStart"
94
+ | "onSuccess"
95
+ | "only"
96
+ | "preserveScroll"
97
+ | "preserveState"
98
+ | "preserveUrl"
99
+ | "queryStringArrayFormat"
100
+ | "replace"
101
+ | "viewTransition"
102
+ >;
103
+ export interface RouteLocationObject {
104
+ path?: string;
105
+ query?: Record<string, unknown>;
106
+ hash?: string;
107
+ }
108
+ export type LocationQueryValue = string | null;
109
+ export type LocationQuery = Record<
110
+ string,
111
+ LocationQueryValue | LocationQueryValue[]
112
+ >;
113
+ export type LocationQueryRaw = Record<
114
+ string,
115
+ | LocationQueryValue
116
+ | number
117
+ | undefined
118
+ | (LocationQueryValue | number | undefined)[]
119
+ >;
120
+ export type RouteLocationRaw = string | RouteLocationObject;
121
+ export interface DmsRoute {
122
+ name?: string;
123
+ fullPath: string;
124
+ path: string;
125
+ query: Record<string, string>;
126
+ params: Record<string, string>;
127
+ meta: Record<string, unknown>;
128
+ matched: unknown[];
129
+ }
130
+ export interface DmsRouter {
131
+ currentRoute: Ref<DmsRoute>;
132
+ push(to: RouteLocationRaw, options?: DmsNavigationOptions): Promise<void>;
133
+ replace(to: RouteLocationRaw, options?: DmsNavigationOptions): Promise<void>;
134
+ back(): void;
135
+ }
136
+ export type DmsAsyncDataStatus = "idle" | "pending" | "success" | "error";
137
+ /**
138
+ * What a new run does with the one still in flight: `cancel` aborts it and
139
+ * starts over, `defer` waits for it instead.
140
+ */
141
+ export type DmsAsyncDataDedupe = "cancel" | "defer";
142
+ export interface DmsAsyncDataRefreshOptions {
143
+ dedupe?: DmsAsyncDataDedupe;
144
+ }
145
+ export interface DmsAsyncData<T> {
146
+ data: Ref<T | null>;
147
+ /**
148
+ * The key the data is loaded under, a reactive key's current one; none for
149
+ * a call without a key.
150
+ */
151
+ key: Readonly<Ref<string | undefined>>;
152
+ /**
153
+ * `true` while `data` is still the previous key's, shown in place of the
154
+ * default until the current key's first answer (a reactive key only).
155
+ */
156
+ placeholder: Readonly<Ref<boolean>>;
157
+ /**
158
+ * The last run's failure. It stays while the next run is in flight, and
159
+ * goes back to `null` once a run answers.
160
+ */
161
+ error: Ref<unknown>;
162
+ pending: Ref<boolean>;
163
+ status: Ref<DmsAsyncDataStatus>;
164
+ execute(options?: DmsAsyncDataRefreshOptions): Promise<void>;
165
+ refresh(options?: DmsAsyncDataRefreshOptions): Promise<void>;
166
+ /** Aborts the run in flight and puts the data back to its default. */
167
+ clear(): void;
168
+ }
169
+ /** Usable at once, or awaited until its data is there. */
170
+ export type DmsAsyncDataResult<T> = DmsAsyncData<T> & Promise<DmsAsyncData<T>>;
171
+ export interface DmsAsyncDataContext {
172
+ /**
173
+ * Aborted once the result is no longer wanted: a newer run replaced it, it
174
+ * was cleared, or no component shows its key any more.
175
+ */
176
+ signal: AbortSignal;
177
+ }
178
+ export type DmsAsyncDataHandler<T> = (
179
+ context: DmsAsyncDataContext,
180
+ ) => Promise<T>;
181
+ /** A key, or `null` (or `undefined`) while there is nothing to load. */
182
+ export type DmsAsyncDataKey = string | null | undefined;
183
+ /** What `useDmsData` gives of a key's data. */
184
+ export interface DmsData<T> {
185
+ /**
186
+ * The data loaded under the key, shared with every caller showing it:
187
+ * `undefined` while the key has none of its own (no caller loaded it yet,
188
+ * or it shows the previous key's data meanwhile). Assigning it replaces the
189
+ * key's data; a key nobody loaded keeps none.
190
+ */
191
+ data: Ref<T | undefined>;
192
+ }
193
+ /** What `transform` knows of the run whose result it transforms. */
194
+ export interface DmsAsyncDataTransformContext {
195
+ /** The key the data is loaded under; none for a call without a key. */
196
+ key: string | undefined;
197
+ }
198
+ /** What `useDmsFetch`'s `transform` knows of the request it answers. */
199
+ export interface DmsFetchTransformContext extends DmsAsyncDataTransformContext {
200
+ /**
201
+ * The URL requested, as given (before the base URL and the query), even
202
+ * when a reactive URL changed since.
203
+ */
204
+ url: string;
205
+ }
206
+ /**
207
+ * The request options a ref or a getter may stand for: read again for each
208
+ * request, and part of the key the response is shared under.
209
+ */
210
+ type DmsFetchRequestSources = "method" | "query" | "params" | "body";
211
+ export interface UseAsyncDataOptions<T> {
212
+ default?: () => T;
213
+ /** `false` waits for `execute()` (a dialog, a drawer) before the first run. */
214
+ immediate?: boolean;
215
+ /**
216
+ * A client navigation does not wait for the data. A server render still
217
+ * does: its HTML shows the data, and the browser takes it over.
218
+ */
219
+ lazy?: boolean;
220
+ /** `false` leaves the data to the browser: the server renders the default. */
221
+ server?: boolean;
222
+ watch?: false | unknown[];
223
+ transform?: (
224
+ value: unknown,
225
+ context: DmsAsyncDataTransformContext,
226
+ ) => T | Promise<T>;
227
+ pick?: string[];
228
+ /**
229
+ * `true` tracks changes inside the value too. By default only the
230
+ * replacement of the whole value triggers, as in Nuxt 4.
231
+ */
232
+ deep?: boolean;
233
+ dedupe?: DmsAsyncDataDedupe;
234
+ /**
235
+ * `false` leaves the data out of `refreshDmsPageData()`, which reads again
236
+ * the data of the components on screen once the page changed (a save, a
237
+ * realtime event).
238
+ */
239
+ refreshOnPageRefresh?: boolean;
240
+ /**
241
+ * Reads again every this many milliseconds, in the browser, while the
242
+ * caller shows the data and the page is visible. A ref or a getter changes
243
+ * the interval as it changes; `0`, `null` or `undefined` stops the polling
244
+ * until it has a value again. A call outside a component (a plugin, a
245
+ * callback) polls for as long as the application runs.
246
+ */
247
+ refreshInterval?: MaybeRefOrGetter<number | null | undefined>;
248
+ }
249
+ /**
250
+ * `useDmsFetch`'s options: ofetch's request options, of which the method, the
251
+ * query, the params and the body may be refs or getters, and the async data
252
+ * options.
253
+ */
254
+ export interface UseFetchOptions<T>
255
+ extends
256
+ Omit<FetchOptions, DmsFetchRequestSources>,
257
+ Omit<UseAsyncDataOptions<T>, "transform"> {
258
+ /**
259
+ * The key the response is shared and transferred under, in place of the
260
+ * one the request gives: its method, base URL, URL, query and body. A ref
261
+ * or a getter moves the caller to the data of each key it takes; `null` or
262
+ * `undefined` requests nothing until it has a value.
263
+ */
264
+ key?: MaybeRefOrGetter<DmsAsyncDataKey>;
265
+ method?: MaybeRefOrGetter<FetchOptions["method"]>;
266
+ query?: MaybeRefOrGetter<FetchOptions["query"]>;
267
+ params?: MaybeRefOrGetter<FetchOptions["params"]>;
268
+ body?: MaybeRefOrGetter<FetchOptions["body"]>;
269
+ transform?: (
270
+ value: unknown,
271
+ context: DmsFetchTransformContext,
272
+ ) => T | Promise<T>;
273
+ /** Replaces the fetch function; it receives the run's signal. */
274
+ $fetch?: typeof ofetch;
275
+ }
276
+ export interface DmsErrorData {
277
+ statusCode?: number;
278
+ statusMessage?: string;
279
+ message?: string;
280
+ data?: unknown;
281
+ fatal?: boolean;
282
+ }
283
+ /** What a server render hands the browser along with its HTML. */
284
+ export interface DmsPayload {
285
+ data: Record<string, unknown>;
286
+ errors: Record<string, DmsErrorData>;
287
+ state: Record<string, unknown>;
288
+ }
289
+ export interface DmsStateOptions {
290
+ /**
291
+ * Hands the server's value to the browser, which then starts from it
292
+ * instead of calling `init` again. Never for tokens or secrets: the value
293
+ * is written into the page.
294
+ */
295
+ transfer?: boolean;
296
+ }
297
+ export interface DmsUserSession<TUser = DmsUser, TSession = DmsSession> {
298
+ user: Ref<TUser | null>;
299
+ session: Ref<TSession | null>;
300
+ loggedIn: Ref<boolean>;
301
+ fetch(): Promise<void>;
302
+ clear(): Promise<void>;
303
+ }
304
+ export interface DmsCookieOptions<T> {
305
+ default?: () => T;
306
+ maxAge?: number;
307
+ path?: string;
308
+ sameSite?: "strict" | "lax" | "none";
309
+ secure?: boolean;
310
+ }
311
+ type DmsLinkPrefetchMode = "mount" | "hover" | "click";
312
+ export type DmsLinkPrefetch =
313
+ | boolean
314
+ | DmsLinkPrefetchMode
315
+ | DmsLinkPrefetchMode[];
316
+ export interface DmsAppContext {
317
+ provide<T>(key: string, value: T): void;
318
+ vueApp: App;
319
+ runWithContext<T>(callback: () => T): T;
320
+ hook(name: string, callback: () => void | Promise<void>): void;
321
+ $i18n: DmsI18n;
322
+ }
323
+ export interface DmsLocale {
324
+ code: string;
325
+ name?: string;
326
+ }
327
+ export type DmsI18n = Composer & {
328
+ locales: ComputedRef<DmsLocale[]>;
329
+ setLocale(locale: string): Promise<void>;
330
+ };
331
+ export type DmsPluginSetup = (context: DmsAppContext) => void | Promise<void>;
332
+ export interface DmsMiddlewareRegistrationOptions {
333
+ global?: boolean;
334
+ /**
335
+ * Runs at the same time as the parallel middlewares next to it, for one
336
+ * that reads nothing another middleware sets (a check against the backend,
337
+ * say). Their results still apply in registration order: the first that
338
+ * redirects or aborts wins, and the next middlewares wait for all of them.
339
+ */
340
+ parallel?: boolean;
341
+ }
342
+ export interface DmsPluginRegistrationOptions {
343
+ clientOnly?: boolean;
344
+ /**
345
+ * When the plugin runs, as Nuxt's `enforce`: every module's `"pre"`
346
+ * plugins first, then the `"default"` ones, then the `"post"` ones, each
347
+ * group in the modules' priority order. A plugin that the others depend on
348
+ * whatever their module's priority, such as the one setting the async data
349
+ * scope (`setDmsAsyncDataScope`), is `"pre"`.
350
+ */
351
+ enforce?: "pre" | "default" | "post";
352
+ }
353
+ export type DmsComponentPreloader = () => Promise<unknown>;
354
+ export interface DmsComponentRegistrationOptions {
355
+ /**
356
+ * Registers the component under `name` as given, without the module's
357
+ * `componentPrefix`: the way to replace a component another module
358
+ * registers, from a module of higher priority. Defaults to `true`.
359
+ */
360
+ prefix?: boolean;
361
+ }
@@ -0,0 +1,247 @@
1
+ import { Link as InertiaLink } from "@inertiajs/vue3";
2
+ import {
3
+ useHead as useUnhead,
4
+ useSeoMeta as useUnheadSeoMeta,
5
+ } from "@unhead/vue";
6
+ import {
7
+ type App,
8
+ type Component,
9
+ type ComponentInternalInstance,
10
+ type ComputedRef,
11
+ defineComponent,
12
+ getCurrentInstance,
13
+ h,
14
+ type InjectionKey,
15
+ inject,
16
+ onMounted,
17
+ type PropType,
18
+ type Ref,
19
+ ref,
20
+ } from "vue";
21
+ import { useI18n as useVueI18n } from "vue-i18n";
22
+ import { type DmsFrontendRuntime } from "./module-registration";
23
+ import {
24
+ type DmsAppContext,
25
+ type DmsI18n,
26
+ type DmsLinkPrefetch,
27
+ type DmsNavigationOptions,
28
+ type DmsPluginSetup,
29
+ type RouteLocationRaw,
30
+ } from "./types";
31
+ import { useDmsRuntime } from "./state";
32
+ import { locationToUrl, visit } from "./route";
33
+
34
+ /**
35
+ * What Vue's development build passes `errorHandler` for an error thrown by a
36
+ * component's `setup`, sync or async (production passes a link instead; it has
37
+ * no warnings to scope either).
38
+ */
39
+ export const SETUP_FUNCTION_ERROR = "setup function";
40
+ /**
41
+ * Whether a Vue warning comes from a component whose `setup` failed, or from
42
+ * under it. Vue still renders such a component, against the setup state that
43
+ * never came: in development, where a single-file component's template is
44
+ * compiled apart from its setup, the template reads the missing bindings, and
45
+ * every component it uses becomes `undefined` ("Invalid vnode type when
46
+ * creating vnode: undefined", then "Component <Anonymous> is missing template
47
+ * or render function" for the comment Vue renders in its place). The error
48
+ * page replaces that output (a server renders the page again, the browser
49
+ * swaps it), and the failure itself is already reported.
50
+ *
51
+ * Walks internal instances: a `<script setup>` component exposes itself, so
52
+ * its children's `$parent` is an expose proxy, not the proxy `errorHandler`
53
+ * received.
54
+ */
55
+ export function withinFailedSetup(
56
+ instance: ComponentInternalInstance | null,
57
+ failedSetups: WeakSet<ComponentInternalInstance>,
58
+ ): boolean {
59
+ for (let current = instance; current; current = current.parent)
60
+ if (failedSetups.has(current)) return true;
61
+ return false;
62
+ }
63
+ /** Shows the error page in place of the page, keeping the first failure. */
64
+ export function failPage(runtime: DmsFrontendRuntime, reason: unknown): void {
65
+ runtime.currentError.value ??=
66
+ reason instanceof Error ? reason : new Error(String(reason));
67
+ }
68
+ export function abortDmsNavigation(): false {
69
+ return false;
70
+ }
71
+ export const useHead = useUnhead;
72
+ export const useSeoMeta = useUnheadSeoMeta;
73
+ export function useI18n(): DmsI18n {
74
+ return useVueI18n() as DmsI18n;
75
+ }
76
+
77
+ function shouldNavigateDmsLink(event: MouseEvent): boolean {
78
+ const element = event.currentTarget;
79
+ return !(
80
+ event.defaultPrevented ||
81
+ event.button !== 0 ||
82
+ event.altKey ||
83
+ event.ctrlKey ||
84
+ event.metaKey ||
85
+ event.shiftKey ||
86
+ (element instanceof HTMLAnchorElement &&
87
+ Boolean(element.target) &&
88
+ element.target !== "_self")
89
+ );
90
+ }
91
+
92
+ function dmsLinkNavigationOptions(
93
+ attributes: Record<string, unknown>,
94
+ ): DmsNavigationOptions {
95
+ const options = attributes as DmsNavigationOptions;
96
+ return {
97
+ async: options.async,
98
+ component: options.component,
99
+ data: options.data,
100
+ except: options.except,
101
+ headers: options.headers,
102
+ method: options.method,
103
+ onBefore: options.onBefore,
104
+ onCancel: options.onCancel,
105
+ onCancelToken: options.onCancelToken,
106
+ onError: options.onError,
107
+ onFinish: options.onFinish,
108
+ onProgress: options.onProgress,
109
+ onStart: options.onStart,
110
+ onSuccess: options.onSuccess,
111
+ only: options.only,
112
+ preserveScroll: options.preserveScroll,
113
+ preserveState: options.preserveState,
114
+ preserveUrl: options.preserveUrl,
115
+ queryStringArrayFormat: options.queryStringArrayFormat,
116
+ replace: options.replace,
117
+ viewTransition: options.viewTransition,
118
+ };
119
+ }
120
+
121
+ export const DmsLink: Component = defineComponent({
122
+ name: "DmsLink",
123
+ inheritAttrs: false,
124
+ props: {
125
+ to: {
126
+ type: [String, Object] as PropType<RouteLocationRaw>,
127
+ required: true,
128
+ },
129
+ },
130
+ setup(props, { attrs, slots }) {
131
+ const runtime = useDmsRuntime();
132
+ const prefetch = attrs.prefetch as DmsLinkPrefetch | undefined;
133
+ const onClick = (event: MouseEvent) => {
134
+ if (typeof attrs.onClick === "function") attrs.onClick(event);
135
+ if (!shouldNavigateDmsLink(event)) return;
136
+ event.preventDefault();
137
+ void visit(runtime, props.to, dmsLinkNavigationOptions(attrs));
138
+ };
139
+ return (): ReturnType<typeof h> =>
140
+ h(
141
+ InertiaLink,
142
+ {
143
+ ...attrs,
144
+ href: locationToUrl(runtime, props.to),
145
+ onClick,
146
+ prefetch: prefetch ?? "hover",
147
+ },
148
+ slots,
149
+ );
150
+ },
151
+ });
152
+
153
+ /** How a keyboard hint prints its modifiers: macOS symbols, or PC key names. */
154
+ export type DmsKbdPlatform = "mac" | "other";
155
+ /**
156
+ * Where Nuxt UI's key hints read the platform: called in the setup of each
157
+ * component that prints one, it returns the platform that component renders.
158
+ * It must read on the server the platform it renders there, and keep it while
159
+ * its caller hydrates, so the browser's first render matches the server's.
160
+ */
161
+ export type DmsKbdPlatformSource = () => Readonly<Ref<DmsKbdPlatform>>;
162
+
163
+ const KBD_PLATFORM_KEY: InjectionKey<DmsKbdPlatformSource> =
164
+ Symbol("dms:kbd-platform");
165
+ const MODIFIER_KBD_KEYS: Record<
166
+ DmsKbdPlatform,
167
+ Record<"meta" | "ctrl" | "alt", string>
168
+ > = {
169
+ mac: { meta: "⌘", ctrl: "⌃", alt: "⌥" },
170
+ other: { meta: "Ctrl", ctrl: "Ctrl", alt: "Alt" },
171
+ };
172
+ const isModifierKbdKey = (value: string): value is "meta" | "ctrl" | "alt" =>
173
+ value === "meta" || value === "ctrl" || value === "alt";
174
+
175
+ /**
176
+ * Gives Nuxt UI's key hints (`UKbd`, the `kbds` of its menus, the dashboard
177
+ * search button) the application's keyboard platform, so the server prints
178
+ * `meta` as "⌘" or "Ctrl" from the first render instead of a blank the
179
+ * browser fills in after mounting.
180
+ */
181
+ export function provideDmsKbdPlatform(
182
+ app: App,
183
+ source: DmsKbdPlatformSource,
184
+ ): void {
185
+ app.provide(KBD_PLATFORM_KEY, source);
186
+ }
187
+
188
+ interface NuxtUiKbd {
189
+ macOS: ComputedRef<unknown>;
190
+ getKbdKey: (value?: string) => string | undefined;
191
+ }
192
+
193
+ /**
194
+ * Nuxt UI's `useKbd` as the DMS runs it (`ui-kbd.mjs` routes it here): a hint
195
+ * renders the same on the server and in the hydrating browser. Nuxt UI prints
196
+ * `meta`, `ctrl` and `alt` from a map shared by every hint and filled by the
197
+ * first one's `onMounted`, so a hint hydrating after that (in an async
198
+ * component) printed "⌘" over the server's blank. Each hint now keeps the
199
+ * server's value until its own `onMounted`; with a platform provided
200
+ * (`provideDmsKbdPlatform`), that value is the platform's from the start.
201
+ *
202
+ * @internal
203
+ */
204
+ export function useDmsKbd(
205
+ shared: () => NuxtUiKbd,
206
+ keys: Readonly<Record<string, string>>,
207
+ ): NuxtUiKbd {
208
+ const { macOS } = shared();
209
+ const instance = getCurrentInstance();
210
+ const source = instance ? inject(KBD_PLATFORM_KEY, null) : null;
211
+ const platform = source ? source() : null;
212
+ // A hydrating component's vnode is bound to its server node before setup.
213
+ const hydrating = ref(!!instance?.vnode.el);
214
+ if (hydrating.value) onMounted(() => (hydrating.value = false));
215
+ return {
216
+ macOS,
217
+ getKbdKey(value) {
218
+ if (!value) return undefined;
219
+ if (!isModifierKbdKey(value)) return keys[value] || value;
220
+ if (platform) return MODIFIER_KBD_KEYS[platform.value][value];
221
+ if (typeof window === "undefined" || hydrating.value) return " ";
222
+ return MODIFIER_KBD_KEYS[macOS.value ? "mac" : "other"][value];
223
+ },
224
+ };
225
+ }
226
+
227
+ export const DmsClientOnly = defineComponent({
228
+ name: "DmsClientOnly",
229
+ setup(_, { slots }) {
230
+ const isMounted = ref(false);
231
+ onMounted(() => {
232
+ isMounted.value = true;
233
+ });
234
+ return () =>
235
+ isMounted.value ? slots.default?.() : (slots.fallback?.() ?? null);
236
+ },
237
+ });
238
+
239
+ export function defineDmsPlugin(setup: DmsPluginSetup): DmsPluginSetup {
240
+ return setup;
241
+ }
242
+ export function useDmsApp(): DmsAppContext {
243
+ const instance = getCurrentInstance();
244
+ const contextual = instance?.appContext.config.globalProperties
245
+ .$dms as DmsAppContext;
246
+ return contextual ?? (useDmsRuntime().appContext as DmsAppContext);
247
+ }
@@ -1 +1,6 @@
1
1
  declare const defineDmsAppConfig: typeof import("./frontend-module").defineDmsAppConfig;
2
+ declare module "virtual:nuxt-ui-icons" {
3
+ export function init(
4
+ add: (name: string, data: import("@iconify/vue").IconifyIcon) => boolean,
5
+ ): void;
6
+ }
@@ -1,16 +1,3 @@
1
- // Restoring "paint before hydrate" in the built document.
2
- //
3
- // The source index.html marks the entry script `fetchpriority="low"` so the
4
- // browser spends its bandwidth on the render-blocking stylesheet first. Vite
5
- // rewrites that script tag when it builds and drops the attribute, and it emits
6
- // the script *before* the stylesheet — so the production document did the
7
- // opposite of what the template asked: it discovered a 250 kB hydration bundle
8
- // first and let it compete with the 27 kB the first paint actually waits on,
9
- // over the plain HTTP/1.1 the generated server speaks.
10
- //
11
- // Kept beside vite.config.ts rather than inside it so the transform can be
12
- // exercised on its own, the way client-manifest.mjs is.
13
-
14
1
  const SCRIPT = /<script[^>]*\stype="module"[^>]*><\/script>/;
15
2
  const STYLESHEET = /[ \t]*<link rel="stylesheet"[^>]*>\n?/;
16
3
 
@@ -26,6 +13,17 @@ function deprioritize(html) {
26
13
  * Give the entry script back its low fetch priority and move the stylesheet
27
14
  * ahead of it. A document that already has them in that order is returned
28
15
  * unchanged, so the transform is safe to run twice.
16
+ *
17
+ * Restores "paint before hydrate" in the built document. The source index.html
18
+ * marks the entry script `fetchpriority="low"` so the browser spends its
19
+ * bandwidth on the render-blocking stylesheet first. Vite rewrites that script
20
+ * tag when it builds and drops the attribute, and it emits the script before
21
+ * the stylesheet, so the production document did the opposite of what the
22
+ * template asked: it discovered a 250 kB hydration bundle first and let it
23
+ * compete with the 27 kB the first paint actually waits on, over the plain
24
+ * HTTP/1.1 the generated server speaks. Kept beside vite.config.ts rather than
25
+ * inside it so the transform can be exercised on its own, the way
26
+ * client-manifest.mjs is.
29
27
  */
30
28
  export function orderHeadForFirstPaint(html) {
31
29
  const prioritized = deprioritize(html);
@@ -0,0 +1,31 @@
1
+ import { addIcon, type IconifyIcon } from "@iconify/vue";
2
+
3
+ function register(name: string, data: IconifyIcon): boolean {
4
+ const colon = name.indexOf(":");
5
+ if (colon !== -1 && name.slice(0, colon).includes("-")) {
6
+ addIcon(name.replace(":", "-"), data);
7
+ }
8
+ return addIcon(name, data);
9
+ }
10
+
11
+ let loading: Promise<void> | undefined;
12
+
13
+ /**
14
+ * Loads the icons Nuxt UI bundles for the client, once: every icon the
15
+ * application's sources name. A server render has them from the start (Nuxt
16
+ * UI's own plugin, left in place by `ui-lazy-icons.mjs`), so there is nothing
17
+ * to load.
18
+ *
19
+ * A failed load is forgotten and resolves: an icon the bundle would have
20
+ * held is then fetched from the icon API, as one outside the bundle always is.
21
+ */
22
+ export function loadIconBundle(): Promise<void> {
23
+ if (import.meta.env.SSR) return Promise.resolve();
24
+ loading ??= import("virtual:nuxt-ui-icons").then(
25
+ ({ init }) => init(register),
26
+ () => {
27
+ loading = undefined;
28
+ },
29
+ );
30
+ return loading;
31
+ }