@octabits-io/nuxt-ui-kit 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,526 @@
1
+ import { UserManager, WebStorageStateStore } from "oidc-client-ts";
2
+ import { computed, ref, watch } from "vue";
3
+ import { treaty } from "@elysiajs/eden";
4
+ //#region src/auth/oidc.ts
5
+ /**
6
+ * Lazily-created `UserManager` singleton bound to `window.localStorage`.
7
+ * Call the returned getter from plugins/stores/composables — the manager is
8
+ * constructed on first call (client-side only; requires `window`).
9
+ */
10
+ function createUserManagerFactory(options) {
11
+ let userManager = null;
12
+ return function getUserManager() {
13
+ if (userManager) return userManager;
14
+ const { issuerUrl, clientId } = options.getConfig();
15
+ if (!issuerUrl || !clientId) (options.onMissingConfig ?? console.error)("Missing OIDC issuer URL or client id in runtime config");
16
+ userManager = new UserManager({
17
+ authority: issuerUrl,
18
+ client_id: clientId,
19
+ redirect_uri: `${window.location.origin}${options.redirectPath ?? "/auth/callback"}`,
20
+ post_logout_redirect_uri: `${window.location.origin}${options.postLogoutRedirectPath ?? "/login"}`,
21
+ response_type: "code",
22
+ scope: options.scope,
23
+ automaticSilentRenew: options.automaticSilentRenew ?? true,
24
+ ...options.refreshTokenAllowedScope ? { refreshTokenAllowedScope: options.refreshTokenAllowedScope } : {},
25
+ userStore: new WebStorageStateStore({ store: window.localStorage }),
26
+ ...options.settings
27
+ });
28
+ return userManager;
29
+ };
30
+ }
31
+ /**
32
+ * Remove any `oidc.user:` storage keys that don't belong to the current
33
+ * authority+clientId — leftovers from environment switches would otherwise
34
+ * shadow or bloat the session storage.
35
+ */
36
+ function removeStaleOidcKeys(authority, clientId, storage = globalThis.localStorage) {
37
+ const currentKey = `oidc.user:${authority}:${clientId}`;
38
+ for (let i = storage.length - 1; i >= 0; i--) {
39
+ const key = storage.key(i);
40
+ if (key && key.startsWith("oidc.user:") && key !== currentKey) storage.removeItem(key);
41
+ }
42
+ }
43
+ /** Refresh tokens fail unrecoverably with these OIDC error codes — user must re-auth. */
44
+ function isUnrecoverableRenewError(message) {
45
+ return message.includes("login_required") || message.includes("invalid_grant") || message.includes("interaction_required") || message.includes("consent_required");
46
+ }
47
+ /**
48
+ * Build a `redirectToLogin()` that starts an OIDC signin redirect carrying the
49
+ * current path as returnUrl state, with a plain `/login?redirect=` navigation
50
+ * fallback when the IdP redirect cannot even be started. No-ops on auth routes.
51
+ */
52
+ function createLoginRedirector(options) {
53
+ const loginPath = options.loginPath ?? "/login";
54
+ const isAuthRoute = options.isAuthRoute ?? ((path) => path === loginPath || path.startsWith("/auth/"));
55
+ return async function redirectToLogin() {
56
+ const path = window.location.pathname;
57
+ if (isAuthRoute(path)) return;
58
+ const returnUrl = path + window.location.search;
59
+ try {
60
+ await options.getUserManager().signinRedirect({ state: returnUrl });
61
+ } catch (err) {
62
+ (options.log ?? console.error)("[oidc] signinRedirect failed, falling back to login navigation", err);
63
+ window.location.href = `${loginPath}?redirect=${encodeURIComponent(returnUrl)}`;
64
+ }
65
+ };
66
+ }
67
+ /**
68
+ * Wire oidc-client-ts session events to app callbacks:
69
+ *
70
+ * - silent-renew error → `notify` (`renew-failed`, or `session-expired` when the
71
+ * error is unrecoverable — see {@link isUnrecoverableRenewError}); an
72
+ * unrecoverable error also triggers the login redirect
73
+ * - access token expired without renewal → `notify(session-expired)` +
74
+ * `onSessionLost` + login redirect
75
+ * - back-channel signout at the IdP → `onSessionLost` + login redirect (no notice
76
+ * — the user initiated it elsewhere)
77
+ *
78
+ * Returns a detach function.
79
+ */
80
+ function attachSessionLifecycleHandlers(userManager, handlers) {
81
+ const log = handlers.log ?? console.warn;
82
+ const onSilentRenewError = (error) => {
83
+ log("[oidc] silent token renew failed:", error);
84
+ const unrecoverable = isUnrecoverableRenewError(error.message);
85
+ handlers.notify?.(unrecoverable ? {
86
+ kind: "session-expired",
87
+ error
88
+ } : {
89
+ kind: "renew-failed",
90
+ error
91
+ });
92
+ if (unrecoverable) handlers.redirectToLogin();
93
+ };
94
+ const onAccessTokenExpired = () => {
95
+ log("[oidc] access token expired without silent renewal");
96
+ handlers.notify?.({ kind: "session-expired" });
97
+ handlers.onSessionLost?.();
98
+ handlers.redirectToLogin();
99
+ };
100
+ const onUserSignedOut = () => {
101
+ log("[oidc] user signed out at IdP (back-channel)");
102
+ handlers.onSessionLost?.();
103
+ handlers.redirectToLogin();
104
+ };
105
+ userManager.events.addSilentRenewError(onSilentRenewError);
106
+ userManager.events.addAccessTokenExpired(onAccessTokenExpired);
107
+ userManager.events.addUserSignedOut(onUserSignedOut);
108
+ return () => {
109
+ userManager.events.removeSilentRenewError(onSilentRenewError);
110
+ userManager.events.removeAccessTokenExpired(onAccessTokenExpired);
111
+ userManager.events.removeUserSignedOut(onUserSignedOut);
112
+ };
113
+ }
114
+ //#endregion
115
+ //#region src/auth/zitadel.ts
116
+ /**
117
+ * Zitadel scope presets for {@link createUserManagerFactory}.
118
+ *
119
+ * The URN scopes request the resource-owner (organization) claim and the
120
+ * project role grants; `offline_access` requests a refresh token.
121
+ */
122
+ const ZITADEL_ORG_PROJECT_SCOPE = "openid profile email urn:zitadel:iam:user:resourceowner urn:zitadel:iam:org:project:roles offline_access";
123
+ /**
124
+ * Zitadel only accepts standard OIDC scopes on the refresh-token grant —
125
+ * sending `offline_access` or the `urn:zitadel:*` scopes returns
126
+ * `invalid_scope` even though they were granted at the initial auth. The
127
+ * URN-based claims still need to land in the refreshed access token; that
128
+ * depends on "Assert Roles on Authentication" being enabled at the Zitadel
129
+ * project level.
130
+ */
131
+ const ZITADEL_REFRESH_TOKEN_ALLOWED_SCOPE = "openid profile email";
132
+ //#endregion
133
+ //#region src/auth/bypass.ts
134
+ /**
135
+ * Dev/E2E auth bypass: seed storage with a fake oidc-client-ts user whose
136
+ * `access_token` is the bypass secret, so the app considers the session
137
+ * authenticated and the API client sends the secret as Bearer.
138
+ *
139
+ * Call from a plugin that runs before the OIDC plugin. Skips seeding when a
140
+ * valid (non-expired) session already exists; overwrites corrupt entries.
141
+ * Returns whether a session was seeded.
142
+ */
143
+ function seedAuthBypassSession(options) {
144
+ if (options.isProductionBuild) return false;
145
+ if (!options.bypassSecret) return false;
146
+ const storage = options.storage ?? globalThis.localStorage;
147
+ const storageKey = `oidc.user:${options.issuerUrl}:${options.clientId}`;
148
+ const existing = storage.getItem(storageKey);
149
+ if (existing) try {
150
+ if ((JSON.parse(existing).expires_at ?? 0) > Date.now() / 1e3) return false;
151
+ } catch {}
152
+ (options.warn ?? console.warn)("[auth-bypass] Seeding storage with bypass token for dev/E2E testing");
153
+ storage.setItem(storageKey, JSON.stringify({
154
+ access_token: options.bypassSecret,
155
+ token_type: "Bearer",
156
+ expires_at: Math.floor(Date.now() / 1e3) + (options.sessionTtlSeconds ?? 86400),
157
+ profile: options.profile ?? {
158
+ sub: "e2e-test-user",
159
+ email: "e2e@example.test",
160
+ name: "E2E Test User"
161
+ },
162
+ scope: "openid profile email"
163
+ }));
164
+ return true;
165
+ }
166
+ //#endregion
167
+ //#region src/auth/session.ts
168
+ function defaultAuthUserMapper(profile) {
169
+ return {
170
+ id: profile.sub,
171
+ email: profile.email ?? "",
172
+ name: profile.name ?? null,
173
+ picture: profile.picture ?? null
174
+ };
175
+ }
176
+ /**
177
+ * Reactive OIDC session state + actions — the setup body of an auth store.
178
+ * Wrap it in the app's own store so naming and registration stay app-owned:
179
+ *
180
+ * ```ts
181
+ * export const useAuthStore = defineStore('auth', () =>
182
+ * createAuthSessionCore({ getUserManager, mapUser: defaultAuthUserMapper }),
183
+ * )
184
+ * ```
185
+ */
186
+ function createAuthSessionCore(options) {
187
+ const log = options.log ?? console.warn;
188
+ const user = ref(null);
189
+ const initialized = ref(false);
190
+ const loading = ref(false);
191
+ const isAuthenticated = computed(() => !!user.value);
192
+ async function checkAuth() {
193
+ loading.value = true;
194
+ try {
195
+ const um = options.getUserManager();
196
+ let oidcUser = await um.getUser();
197
+ if (oidcUser && oidcUser.expired && oidcUser.refresh_token) try {
198
+ oidcUser = await um.signinSilent();
199
+ } catch (err) {
200
+ log("[auth] signinSilent failed during checkAuth", err);
201
+ oidcUser = null;
202
+ }
203
+ if (oidcUser && !oidcUser.expired) user.value = options.mapUser(oidcUser.profile);
204
+ else user.value = null;
205
+ } catch {
206
+ user.value = null;
207
+ } finally {
208
+ initialized.value = true;
209
+ loading.value = false;
210
+ }
211
+ }
212
+ async function login(returnUrl) {
213
+ await options.getUserManager().signinRedirect({ state: returnUrl ?? "/" });
214
+ }
215
+ async function handleCallback() {
216
+ loading.value = true;
217
+ try {
218
+ const oidcUser = await options.getUserManager().signinRedirectCallback();
219
+ user.value = options.mapUser(oidcUser.profile);
220
+ return oidcUser.state || "/";
221
+ } finally {
222
+ initialized.value = true;
223
+ loading.value = false;
224
+ }
225
+ }
226
+ async function logout() {
227
+ loading.value = true;
228
+ try {
229
+ const manager = options.getUserManager();
230
+ const idTokenHint = (await manager.getUser())?.id_token;
231
+ await manager.removeUser();
232
+ user.value = null;
233
+ await manager.signoutRedirect(idTokenHint ? { id_token_hint: idTokenHint } : void 0);
234
+ } finally {
235
+ loading.value = false;
236
+ }
237
+ }
238
+ return {
239
+ user,
240
+ initialized,
241
+ loading,
242
+ isAuthenticated,
243
+ checkAuth,
244
+ login,
245
+ handleCallback,
246
+ logout
247
+ };
248
+ }
249
+ //#endregion
250
+ //#region src/auth/guard.ts
251
+ /**
252
+ * Build the body of a global auth route-middleware. The returned handler
253
+ * yields a redirect target path or `undefined` to allow navigation; the app's
254
+ * middleware maps that onto its router:
255
+ *
256
+ * ```ts
257
+ * export default defineNuxtRouteMiddleware(async (to) => {
258
+ * const target = await guard(to)
259
+ * if (target) return navigateTo(target)
260
+ * })
261
+ * ```
262
+ */
263
+ function createAuthGuard(options) {
264
+ const isPublicRoute = options.isPublicRoute ?? ((to) => to.path === "/login" || to.path.startsWith("/auth/"));
265
+ const loginRedirect = options.loginRedirect ?? ((returnTo) => `/login?redirect=${encodeURIComponent(returnTo)}`);
266
+ return async function guard(to) {
267
+ if (isPublicRoute(to)) return void 0;
268
+ if (!await options.ensureAuthenticated()) return loginRedirect(to.fullPath);
269
+ const target = await options.afterAuthenticated?.(to);
270
+ return typeof target === "string" ? target : void 0;
271
+ };
272
+ }
273
+ //#endregion
274
+ //#region src/api/client.ts
275
+ /**
276
+ * Resolve the API base URL: configured value, else the page origin in
277
+ * production builds (same-host ingress), else a localhost dev port.
278
+ */
279
+ function resolveApiBaseUrl(options) {
280
+ if (options.configuredUrl) return options.configuredUrl;
281
+ return options.isProductionBuild ? options.origin ?? window.location.origin : `http://localhost:${options.devFallbackPort}`;
282
+ }
283
+ /**
284
+ * Bearer-token provider backed by the OIDC session: resolves to the current
285
+ * access token, or `null` when there is no non-expired session.
286
+ */
287
+ function createAccessTokenProvider(getUserManager) {
288
+ return async function getAccessToken() {
289
+ const user = await getUserManager().getUser();
290
+ if (!user || user.expired) return null;
291
+ return user.access_token;
292
+ };
293
+ }
294
+ /**
295
+ * Lazily-created Eden Treaty client singleton with OIDC bearer injection.
296
+ *
297
+ * ```ts
298
+ * const getClient = createTreatyClientFactory<App>({ getBaseUrl, getAccessToken })
299
+ * export function useApi() {
300
+ * const client = getClient()
301
+ * return { api: client.api, client }
302
+ * }
303
+ * ```
304
+ */
305
+ function createTreatyClientFactory(options) {
306
+ let client = null;
307
+ return function getClient() {
308
+ if (client) return client;
309
+ client = treaty(options.getBaseUrl(), {
310
+ parseDate: options.parseDate ?? false,
311
+ headers: async () => {
312
+ const token = await options.getAccessToken();
313
+ if (token) return { authorization: `Bearer ${token}` };
314
+ },
315
+ ...options.treatyConfig
316
+ });
317
+ return client;
318
+ };
319
+ }
320
+ //#endregion
321
+ //#region src/org/orgStore.ts
322
+ /**
323
+ * Reactive granted-organizations state + switching — the setup body of an
324
+ * org/tenant store. Wrap it in the app's own store (and alias names there):
325
+ *
326
+ * ```ts
327
+ * export const useTenantStore = defineStore('tenant', () => {
328
+ * const core = createOrgStoreCore<Tenant>({ fetchOrganizations, getSlug: t => t.slug })
329
+ * return { ...core, fetchTenants: core.fetchOrganizations }
330
+ * })
331
+ * ```
332
+ */
333
+ function createOrgStoreCore(options) {
334
+ const persistenceKey = options.persistenceKey ?? "currentOrgSlug";
335
+ const getStorage = () => options.storage ?? globalThis.localStorage;
336
+ const organizations = ref([]);
337
+ const currentSlug = ref(null);
338
+ const loading = ref(false);
339
+ const fetchError = ref(null);
340
+ const currentOrganization = computed(() => {
341
+ if (!currentSlug.value) return null;
342
+ return organizations.value.find((org) => options.getSlug(org) === currentSlug.value) ?? null;
343
+ });
344
+ async function fetchOrganizations() {
345
+ loading.value = true;
346
+ fetchError.value = null;
347
+ try {
348
+ const result = await options.fetchOrganizations();
349
+ if (result.items === void 0) {
350
+ fetchError.value = result.error;
351
+ return;
352
+ }
353
+ organizations.value = result.items;
354
+ if (currentSlug.value) {
355
+ if (!organizations.value.some((org) => options.getSlug(org) === currentSlug.value)) setCurrent(null);
356
+ }
357
+ } finally {
358
+ loading.value = false;
359
+ }
360
+ }
361
+ function setCurrent(slug) {
362
+ currentSlug.value = slug;
363
+ if (slug) getStorage().setItem(persistenceKey, slug);
364
+ else getStorage().removeItem(persistenceKey);
365
+ }
366
+ function loadPersisted() {
367
+ const stored = getStorage().getItem(persistenceKey);
368
+ if (stored) currentSlug.value = stored;
369
+ }
370
+ return {
371
+ organizations,
372
+ currentSlug,
373
+ currentOrganization,
374
+ loading,
375
+ fetchError,
376
+ fetchOrganizations,
377
+ setCurrent,
378
+ loadPersisted
379
+ };
380
+ }
381
+ //#endregion
382
+ //#region src/composables/useConfirm.ts
383
+ const isOpen = ref(false);
384
+ const currentOptions = ref({ title: "" });
385
+ let resolvePromise = null;
386
+ /** Promise-based confirmation: `if (await confirm({ title, dangerous: true })) …` */
387
+ function useConfirm() {
388
+ function confirm(options) {
389
+ currentOptions.value = options;
390
+ isOpen.value = true;
391
+ return new Promise((resolve) => {
392
+ resolvePromise = resolve;
393
+ });
394
+ }
395
+ return { confirm };
396
+ }
397
+ /** State + handlers for the dialog renderer component. */
398
+ function useConfirmState() {
399
+ function handleConfirm() {
400
+ isOpen.value = false;
401
+ resolvePromise?.(true);
402
+ resolvePromise = null;
403
+ }
404
+ function handleCancel() {
405
+ isOpen.value = false;
406
+ resolvePromise?.(false);
407
+ resolvePromise = null;
408
+ }
409
+ return {
410
+ isOpen,
411
+ options: currentOptions,
412
+ handleConfirm,
413
+ handleCancel
414
+ };
415
+ }
416
+ //#endregion
417
+ //#region src/composables/apiErrorMessenger.ts
418
+ /**
419
+ * Map API error bodies to user-facing i18n strings using a fixed key
420
+ * convention the consumer's locale files fulfil:
421
+ *
422
+ * - `errors.<key>` — one entry per API error key (fallback: the raw
423
+ * server `message`, and `errors.internal_server_error` for non-API errors)
424
+ * - `validation.fields.<path>` — display names for validated fields
425
+ * - `validation.messages.<snake_cased_message>` — validation message texts
426
+ *
427
+ * Framework-free: pass `t`/`te` from your i18n instance (the app-side
428
+ * composable is typically `const { t, te } = useI18n()` + this factory).
429
+ * Eden Treaty error envelopes (`{ value }`) are unwrapped automatically.
430
+ */
431
+ function createApiErrorMessenger(options) {
432
+ const { t, te } = options;
433
+ const log = options.log ?? ((message, error) => console.error(message, error));
434
+ function isApiError(error) {
435
+ return typeof error === "object" && error !== null && "key" in error && "message" in error;
436
+ }
437
+ function isValidationError(error) {
438
+ return isApiError(error) && error.key === "validation_error";
439
+ }
440
+ function getErrorMessage(error) {
441
+ log("API Error:", error);
442
+ let actualError = error;
443
+ if (typeof error === "object" && error !== null && "value" in error) actualError = error.value;
444
+ if (isValidationError(actualError)) return actualError.fields.map((f) => {
445
+ const fieldKey = `validation.fields.${f.path}`;
446
+ const fieldName = te(fieldKey) ? t(fieldKey) : f.path;
447
+ const messageKey = `validation.messages.${f.message.toLowerCase().replace(/\s+/g, "_")}`;
448
+ return `${fieldName}: ${te(messageKey) ? t(messageKey) : f.message}`;
449
+ }).join(", ") || t("errors.validation_error");
450
+ if (!isApiError(actualError)) return t("errors.internal_server_error");
451
+ const errorKey = `errors.${actualError.key}`;
452
+ if (te(errorKey)) return t(errorKey);
453
+ return actualError.message;
454
+ }
455
+ return {
456
+ getErrorMessage,
457
+ isValidationError
458
+ };
459
+ }
460
+ //#endregion
461
+ //#region src/composables/useDirtyTracking.ts
462
+ function deepClone(obj) {
463
+ return JSON.parse(JSON.stringify(obj));
464
+ }
465
+ /**
466
+ * Form change-detection over a reactive state object via JSON deep-compare:
467
+ * `isDirty` flips when any field differs from the snapshot; `resetInitial()`
468
+ * re-snapshots after load/save (optionally assigning new values first);
469
+ * `getDirtyFields()` yields a minimal PATCH payload.
470
+ */
471
+ function useDirtyTracking(state) {
472
+ const initial = ref(deepClone(state));
473
+ const isDirty = computed(() => JSON.stringify(state) !== JSON.stringify(initial.value));
474
+ function getDirtyFields() {
475
+ const dirty = {};
476
+ for (const key of Object.keys(state)) if (JSON.stringify(state[key]) !== JSON.stringify(initial.value[key])) dirty[key] = state[key];
477
+ return dirty;
478
+ }
479
+ function resetInitial(values) {
480
+ if (values) Object.assign(state, values);
481
+ initial.value = deepClone(state);
482
+ }
483
+ return {
484
+ isDirty,
485
+ getDirtyFields,
486
+ resetInitial
487
+ };
488
+ }
489
+ //#endregion
490
+ //#region src/composables/usePagination.ts
491
+ /**
492
+ * Offset-based table pagination: `page`/`itemsPerPage`/`total` state with a
493
+ * derived `offset` and ready-to-spread `queryParams { limit, offset }`.
494
+ * `onPaginationChange` fires whenever page or page size changes (refetch hook).
495
+ */
496
+ function usePagination(options = {}) {
497
+ const { defaultLimit = 50, onPaginationChange } = options;
498
+ const page = ref(1);
499
+ const itemsPerPage = ref(defaultLimit);
500
+ const total = ref(0);
501
+ const offset = computed(() => (page.value - 1) * itemsPerPage.value);
502
+ const queryParams = computed(() => ({
503
+ limit: itemsPerPage.value,
504
+ offset: offset.value
505
+ }));
506
+ function setTotal(value) {
507
+ total.value = value;
508
+ }
509
+ function resetPagination() {
510
+ page.value = 1;
511
+ }
512
+ watch([page, itemsPerPage], () => {
513
+ onPaginationChange?.();
514
+ });
515
+ return {
516
+ page,
517
+ itemsPerPage,
518
+ total,
519
+ offset,
520
+ queryParams,
521
+ setTotal,
522
+ resetPagination
523
+ };
524
+ }
525
+ //#endregion
526
+ export { ZITADEL_ORG_PROJECT_SCOPE, ZITADEL_REFRESH_TOKEN_ALLOWED_SCOPE, attachSessionLifecycleHandlers, createAccessTokenProvider, createApiErrorMessenger, createAuthGuard, createAuthSessionCore, createLoginRedirector, createOrgStoreCore, createTreatyClientFactory, createUserManagerFactory, defaultAuthUserMapper, isUnrecoverableRenewError, removeStaleOidcKeys, resolveApiBaseUrl, seedAuthBypassSession, useConfirm, useConfirmState, useDirtyTracking, usePagination };
@@ -0,0 +1,24 @@
1
+ import * as z from "zod";
2
+ //#region src/zod/index.d.ts
3
+ type ZodLocaleFactory = () => Parameters<typeof z.config>[0];
4
+ interface ZodLocaleSyncOptions {
5
+ /** Locale code → zod locale factory (from `zod/locales`), e.g. `{ de, en }`. */
6
+ locales: Record<string, ZodLocaleFactory>;
7
+ /** Applied when the active locale has no entry in `locales`. */
8
+ defaultLocale: string;
9
+ /** Read the active UI locale code. */
10
+ getLocale: () => string;
11
+ /**
12
+ * Wire locale-change reactivity — call `apply` with the new code whenever
13
+ * the UI locale changes (e.g. `apply => watch(() => i18n.locale.value, apply)`).
14
+ */
15
+ onLocaleChange: (apply: (code: string) => void) => void;
16
+ }
17
+ /**
18
+ * Keep Zod's built-in error messages in the user's language: applies the
19
+ * matching `zod/locales` config immediately and re-applies on every locale
20
+ * change. Call once from an app plugin.
21
+ */
22
+ declare function setupZodLocaleSync(options: ZodLocaleSyncOptions): void;
23
+ //#endregion
24
+ export { ZodLocaleSyncOptions, setupZodLocaleSync };
@@ -0,0 +1,17 @@
1
+ import * as z from "zod";
2
+ //#region src/zod/index.ts
3
+ /**
4
+ * Keep Zod's built-in error messages in the user's language: applies the
5
+ * matching `zod/locales` config immediately and re-applies on every locale
6
+ * change. Call once from an app plugin.
7
+ */
8
+ function setupZodLocaleSync(options) {
9
+ const apply = (code) => {
10
+ const factory = options.locales[code] ?? options.locales[options.defaultLocale];
11
+ if (factory) z.config(factory());
12
+ };
13
+ apply(options.getLocale());
14
+ options.onLocaleChange(apply);
15
+ }
16
+ //#endregion
17
+ export { setupZodLocaleSync };
package/package.json ADDED
@@ -0,0 +1,102 @@
1
+ {
2
+ "name": "@octabits-io/nuxt-ui-kit",
3
+ "version": "0.2.0",
4
+ "description": "Frontend kit for Nuxt/Vue admin SPAs: OIDC session harness (oidc-client-ts), Eden Treaty client factory, auth/org store cores, and a route-guard builder — factory-style seams the app wires into its own plugins, stores, and middleware",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./zod": {
14
+ "types": "./dist/zod/index.d.ts",
15
+ "import": "./dist/zod/index.js",
16
+ "default": "./dist/zod/index.js"
17
+ },
18
+ "./dates": {
19
+ "types": "./dist/dates/index.d.ts",
20
+ "import": "./dist/dates/index.js",
21
+ "default": "./dist/dates/index.js"
22
+ },
23
+ "./ai": {
24
+ "types": "./dist/ai/index.d.ts",
25
+ "import": "./dist/ai/index.js",
26
+ "default": "./dist/ai/index.js"
27
+ },
28
+ "./components/*": "./src/components/*"
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "src/components",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/octabits-io/platform.git",
42
+ "directory": "packages/nuxt-ui-kit"
43
+ },
44
+ "homepage": "https://github.com/octabits-io/platform/tree/main/packages/nuxt-ui-kit",
45
+ "bugs": {
46
+ "url": "https://github.com/octabits-io/platform/issues"
47
+ },
48
+ "devDependencies": {
49
+ "@elysiajs/eden": "^1.4.9",
50
+ "date-fns": "^4.4.0",
51
+ "elysia": "^1.4.29",
52
+ "oidc-client-ts": "^3.5.0",
53
+ "vitest": "^4.1.10",
54
+ "vue": "^3.5.39",
55
+ "zod": "^4.4.3"
56
+ },
57
+ "peerDependencies": {
58
+ "@elysiajs/eden": "^1.4.0",
59
+ "@internationalized/date": "^3",
60
+ "@nuxt/ui": "^4",
61
+ "date-fns": "^3 || ^4",
62
+ "elysia": ">=1.4 <3",
63
+ "oidc-client-ts": "^3.3.0",
64
+ "typescript": "^5 || ^6 || ^7",
65
+ "vue": "^3.5.0",
66
+ "vue-i18n": "^11",
67
+ "vue-router": "^4",
68
+ "zod": "^4"
69
+ },
70
+ "peerDependenciesMeta": {
71
+ "@internationalized/date": {
72
+ "optional": true
73
+ },
74
+ "@nuxt/ui": {
75
+ "optional": true
76
+ },
77
+ "date-fns": {
78
+ "optional": true
79
+ },
80
+ "elysia": {
81
+ "optional": true
82
+ },
83
+ "typescript": {
84
+ "optional": true
85
+ },
86
+ "vue-i18n": {
87
+ "optional": true
88
+ },
89
+ "vue-router": {
90
+ "optional": true
91
+ },
92
+ "zod": {
93
+ "optional": true
94
+ }
95
+ },
96
+ "scripts": {
97
+ "build": "tsdown",
98
+ "clean": "rm -rf dist",
99
+ "test": "vitest run",
100
+ "typecheck": "tsc --noEmit"
101
+ }
102
+ }