@saasicat/ui-vue 0.15.0 → 0.16.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.
@@ -25,8 +25,11 @@ interface KvStore {
25
25
  remove(key: string): void;
26
26
  }
27
27
  /**
28
- * Returns a `KvStore` wrapper around `localStorage` (or `null` under SSR).
29
- * Tests may pass an in-memory stub.
28
+ * Returns a `KvStore` wrapper around `localStorage`, or a no-op store when the
29
+ * environment has none or refuses access. Tests may pass an in-memory stub.
30
+ *
31
+ * Every persistence built on this is a convenience — losing it must never take
32
+ * the app down, so no failure mode is allowed to escape.
30
33
  */
31
34
  declare function defaultKvStore(): KvStore;
32
35
  /**
@@ -40,6 +43,14 @@ declare const SA_LOCALES: readonly SaLocale[];
40
43
  declare const DEFAULT_SA_LOCALE: SaLocale;
41
44
  /** BCP-47 tags used for `Intl`/`toLocaleString` formatting per UI locale. */
42
45
  declare const SA_INTL_LOCALES: Record<SaLocale, string>;
46
+ /**
47
+ * Names for the language switcher. Endonyms — a language is listed in its own
48
+ * language, so the entries stay readable no matter which locale is active and
49
+ * never need translating.
50
+ */
51
+ declare const SA_LOCALE_LABELS: Record<SaLocale, string>;
52
+ /** Narrows untrusted input (persisted preference, query parameter) to a locale. */
53
+ declare function isSaLocale(value: unknown): value is SaLocale;
43
54
 
44
55
  /** Consumer implementation; receives the action inputs as a generic object. */
45
56
  type ActionHandler<TInput = unknown, TResult = unknown> = (input: TInput) => Promise<TResult>;
@@ -1405,6 +1416,7 @@ declare const shellMessages: Record<SaLocale, {
1405
1416
  subtitle: string;
1406
1417
  roleBadge: string;
1407
1418
  logout: string;
1419
+ language: string;
1408
1420
  };
1409
1421
  drawer: {
1410
1422
  docs: string;
@@ -1582,4 +1594,4 @@ declare const SA_MESSAGES: Record<SaLocale, SaMessages>;
1582
1594
  */
1583
1595
  declare function resolveMessages(locale: SaLocale, overrides?: SaMessagesOverrides): SaMessages;
1584
1596
 
1585
- export { type ActionHandler as A, DEFAULT_SA_LOCALE as D, type HttpClient as H, type KvStore as K, type MessageTree as M, type PartialMessages as P, type ResolvedAction as R, type SaLocale as S, type TranslationOf as T, type SaMessages as a, type SaMessagesOverrides as b, ActionRegistry as c, ActionDefNotInManifestError as d, type HttpResponse as e, MissingHandlerError as f, SA_INTL_LOCALES as g, SA_LOCALES as h, SA_MESSAGES as i, defaultHttpClient as j, defaultKvStore as k, defineMessages as l, mergeMessages as m, resolveMessages as r };
1597
+ export { type ActionHandler as A, DEFAULT_SA_LOCALE as D, type HttpClient as H, type KvStore as K, type MessageTree as M, type PartialMessages as P, type ResolvedAction as R, type SaLocale as S, type TranslationOf as T, type SaMessages as a, type SaMessagesOverrides as b, ActionRegistry as c, ActionDefNotInManifestError as d, type HttpResponse as e, MissingHandlerError as f, SA_INTL_LOCALES as g, SA_LOCALES as h, SA_LOCALE_LABELS as i, SA_MESSAGES as j, defaultHttpClient as k, defaultKvStore as l, defineMessages as m, isSaLocale as n, mergeMessages as o, resolveMessages as r };
@@ -25,8 +25,11 @@ interface KvStore {
25
25
  remove(key: string): void;
26
26
  }
27
27
  /**
28
- * Returns a `KvStore` wrapper around `localStorage` (or `null` under SSR).
29
- * Tests may pass an in-memory stub.
28
+ * Returns a `KvStore` wrapper around `localStorage`, or a no-op store when the
29
+ * environment has none or refuses access. Tests may pass an in-memory stub.
30
+ *
31
+ * Every persistence built on this is a convenience — losing it must never take
32
+ * the app down, so no failure mode is allowed to escape.
30
33
  */
31
34
  declare function defaultKvStore(): KvStore;
32
35
  /**
@@ -40,6 +43,14 @@ declare const SA_LOCALES: readonly SaLocale[];
40
43
  declare const DEFAULT_SA_LOCALE: SaLocale;
41
44
  /** BCP-47 tags used for `Intl`/`toLocaleString` formatting per UI locale. */
42
45
  declare const SA_INTL_LOCALES: Record<SaLocale, string>;
46
+ /**
47
+ * Names for the language switcher. Endonyms — a language is listed in its own
48
+ * language, so the entries stay readable no matter which locale is active and
49
+ * never need translating.
50
+ */
51
+ declare const SA_LOCALE_LABELS: Record<SaLocale, string>;
52
+ /** Narrows untrusted input (persisted preference, query parameter) to a locale. */
53
+ declare function isSaLocale(value: unknown): value is SaLocale;
43
54
 
44
55
  /** Consumer implementation; receives the action inputs as a generic object. */
45
56
  type ActionHandler<TInput = unknown, TResult = unknown> = (input: TInput) => Promise<TResult>;
@@ -1405,6 +1416,7 @@ declare const shellMessages: Record<SaLocale, {
1405
1416
  subtitle: string;
1406
1417
  roleBadge: string;
1407
1418
  logout: string;
1419
+ language: string;
1408
1420
  };
1409
1421
  drawer: {
1410
1422
  docs: string;
@@ -1582,4 +1594,4 @@ declare const SA_MESSAGES: Record<SaLocale, SaMessages>;
1582
1594
  */
1583
1595
  declare function resolveMessages(locale: SaLocale, overrides?: SaMessagesOverrides): SaMessages;
1584
1596
 
1585
- export { type ActionHandler as A, DEFAULT_SA_LOCALE as D, type HttpClient as H, type KvStore as K, type MessageTree as M, type PartialMessages as P, type ResolvedAction as R, type SaLocale as S, type TranslationOf as T, type SaMessages as a, type SaMessagesOverrides as b, ActionRegistry as c, ActionDefNotInManifestError as d, type HttpResponse as e, MissingHandlerError as f, SA_INTL_LOCALES as g, SA_LOCALES as h, SA_MESSAGES as i, defaultHttpClient as j, defaultKvStore as k, defineMessages as l, mergeMessages as m, resolveMessages as r };
1597
+ export { type ActionHandler as A, DEFAULT_SA_LOCALE as D, type HttpClient as H, type KvStore as K, type MessageTree as M, type PartialMessages as P, type ResolvedAction as R, type SaLocale as S, type TranslationOf as T, type SaMessages as a, type SaMessagesOverrides as b, ActionRegistry as c, ActionDefNotInManifestError as d, type HttpResponse as e, MissingHandlerError as f, SA_INTL_LOCALES as g, SA_LOCALES as h, SA_LOCALE_LABELS as i, SA_MESSAGES as j, defaultHttpClient as k, defaultKvStore as l, defineMessages as m, isSaLocale as n, mergeMessages as o, resolveMessages as r };
@@ -33,6 +33,45 @@ var import_pinia = require("pinia");
33
33
  var import_vue_router = require("vue-router");
34
34
 
35
35
  // src/client/types.ts
36
+ var NOOP_KV_STORE = {
37
+ get: () => null,
38
+ set: () => {
39
+ },
40
+ remove: () => {
41
+ }
42
+ };
43
+ function defaultKvStore() {
44
+ const ls = resolveLocalStorage();
45
+ if (!ls) return NOOP_KV_STORE;
46
+ return {
47
+ get: (k) => {
48
+ try {
49
+ return ls.getItem(k);
50
+ } catch {
51
+ return null;
52
+ }
53
+ },
54
+ set: (k, v) => {
55
+ try {
56
+ ls.setItem(k, v);
57
+ } catch {
58
+ }
59
+ },
60
+ remove: (k) => {
61
+ try {
62
+ ls.removeItem(k);
63
+ } catch {
64
+ }
65
+ }
66
+ };
67
+ }
68
+ function resolveLocalStorage() {
69
+ try {
70
+ return globalThis.localStorage ?? null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
36
75
  function defaultHttpClient() {
37
76
  return (url, init) => fetch(url, init);
38
77
  }
@@ -93,11 +132,15 @@ var SUPER_ADMIN_NOTIFY_KEY = /* @__PURE__ */ Symbol.for(
93
132
  var import_vue = require("vue");
94
133
 
95
134
  // src/client/i18n/locale.ts
135
+ var SA_LOCALES = ["de", "en"];
96
136
  var DEFAULT_SA_LOCALE = "de";
97
137
  var SA_INTL_LOCALES = {
98
138
  de: "de-DE",
99
139
  en: "en-US"
100
140
  };
141
+ function isSaLocale(value) {
142
+ return typeof value === "string" && SA_LOCALES.includes(value);
143
+ }
101
144
 
102
145
  // src/client/i18n/define.ts
103
146
  function defineMessages(de, en) {
@@ -2777,7 +2820,8 @@ var shellMessages = defineMessages(
2777
2820
  productionWarning: "Aktionen wirken sich sofort auf alle Mandanten aus.",
2778
2821
  subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
2779
2822
  roleBadge: "SUPER ADMIN",
2780
- logout: "Abmelden"
2823
+ logout: "Abmelden",
2824
+ language: "Sprache"
2781
2825
  },
2782
2826
  drawer: {
2783
2827
  docs: "Doku \xF6ffnen"
@@ -2845,7 +2889,8 @@ var shellMessages = defineMessages(
2845
2889
  productionWarning: "Actions take effect immediately for all tenants.",
2846
2890
  subtitle: "SuperAdmin \xB7 Platform administration",
2847
2891
  roleBadge: "SUPER ADMIN",
2848
- logout: "Sign out"
2892
+ logout: "Sign out",
2893
+ language: "Language"
2849
2894
  },
2850
2895
  drawer: {
2851
2896
  docs: "Open documentation"
@@ -3119,16 +3164,33 @@ function resolveMessages(locale, overrides) {
3119
3164
  }
3120
3165
 
3121
3166
  // src/vue/use-super-admin-i18n.ts
3167
+ var SA_LOCALE_STORAGE_KEY = "sa:locale";
3122
3168
  var SUPER_ADMIN_I18N_KEY = /* @__PURE__ */ Symbol.for(
3123
3169
  "@saasicat/ui-vue/SUPER_ADMIN_I18N"
3124
3170
  );
3125
3171
  function createSuperAdminI18n(options = {}) {
3126
- const locale = (0, import_vue.isRef)(options.locale) ? options.locale : (0, import_vue.ref)(options.locale ?? DEFAULT_SA_LOCALE);
3172
+ const locale = (0, import_vue.isRef)(options.locale) ? options.locale : createOwnedLocale(options);
3127
3173
  const messages = (0, import_vue.computed)(
3128
3174
  () => resolveMessages(locale.value, options.overrides?.[locale.value])
3129
3175
  );
3130
3176
  const intlLocale = (0, import_vue.computed)(() => SA_INTL_LOCALES[locale.value]);
3131
- return { locale, messages, intlLocale };
3177
+ const writable = !(0, import_vue.isReadonly)(locale);
3178
+ return {
3179
+ locale,
3180
+ messages,
3181
+ intlLocale,
3182
+ switcherEnabled: (options.switcher ?? true) && writable
3183
+ };
3184
+ }
3185
+ function createOwnedLocale(options) {
3186
+ if (options.persist === false) {
3187
+ return (0, import_vue.ref)(options.locale ?? DEFAULT_SA_LOCALE);
3188
+ }
3189
+ const storage = options.storage ?? defaultKvStore();
3190
+ const stored = storage.get(SA_LOCALE_STORAGE_KEY);
3191
+ const locale = (0, import_vue.ref)(isSaLocale(stored) ? stored : options.locale ?? DEFAULT_SA_LOCALE);
3192
+ (0, import_vue.watch)(locale, (next) => storage.set(SA_LOCALE_STORAGE_KEY, next));
3193
+ return locale;
3132
3194
  }
3133
3195
 
3134
3196
  // src/quasar/notify.ts
@@ -2,8 +2,8 @@ import { Component, App } from 'vue';
2
2
  import { QuasarPluginOptions } from 'quasar';
3
3
  import { Pinia } from 'pinia';
4
4
  import { RouteRecordRaw, RouterHistory, Router } from 'vue-router';
5
- import { H as HttpClient } from '../messages-BIYw32u1.cjs';
6
- import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-DL_qI1BY.cjs';
5
+ import { H as HttpClient } from '../messages-DetB75lX.cjs';
6
+ import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-DokHhkS_.cjs';
7
7
  import '@saasicat/types';
8
8
 
9
9
  interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
@@ -2,8 +2,8 @@ import { Component, App } from 'vue';
2
2
  import { QuasarPluginOptions } from 'quasar';
3
3
  import { Pinia } from 'pinia';
4
4
  import { RouteRecordRaw, RouterHistory, Router } from 'vue-router';
5
- import { H as HttpClient } from '../messages-BIYw32u1.js';
6
- import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-CWNu_W3u.js';
5
+ import { H as HttpClient } from '../messages-DetB75lX.js';
6
+ import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-BPC-H9jz.js';
7
7
  import '@saasicat/types';
8
8
 
9
9
  interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
@@ -10,10 +10,10 @@ import {
10
10
  SUPER_ADMIN_NOTIFY_KEY,
11
11
  buildNavigationGuard,
12
12
  createSuperAdminI18n
13
- } from "../chunk-TY2WYKME.js";
13
+ } from "../chunk-MGKJCVDU.js";
14
14
  import {
15
15
  defaultHttpClient
16
- } from "../chunk-TBLXKSTR.js";
16
+ } from "../chunk-TZ2NGQXK.js";
17
17
 
18
18
  // src/quasar/create-super-admin-app.ts
19
19
  import { createApp } from "vue";
@@ -1,7 +1,7 @@
1
1
  import { Component, App, InjectionKey, Ref, ComputedRef } from 'vue';
2
2
  import { NavigationGuardWithThis } from 'vue-router';
3
3
  import { ActionKey, ComponentKey, AdminManifest } from '@saasicat/types';
4
- import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaMessagesOverrides } from './messages-BIYw32u1.js';
4
+ import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaMessagesOverrides, K as KvStore } from './messages-DetB75lX.js';
5
5
 
6
6
  /**
7
7
  * App-specific branding data that the platform `AdminLayout` and other
@@ -182,16 +182,42 @@ interface SuperAdminI18n {
182
182
  messages: ComputedRef<SaMessages>;
183
183
  /** BCP-47 tag for `Intl`/`toLocaleString` formatting in the active locale. */
184
184
  intlLocale: ComputedRef<string>;
185
+ /**
186
+ * Whether the shell renders its language switcher. `false` when the app
187
+ * opted out or when `locale` cannot be written to — a switcher that cannot
188
+ * change anything is worse than none.
189
+ */
190
+ switcherEnabled: boolean;
185
191
  }
186
192
  interface SuperAdminI18nOptions {
187
193
  /**
188
- * Initial UI locale, default `'de'`. Pass a `Ref` when the app wants to
189
- * keep control (e.g. a user-profile setting or a header switcher).
194
+ * Starting UI locale, default `'de'` — the shell's switcher lets the user
195
+ * pick another one, and a stored pick outranks this. Pass a `Ref` when the
196
+ * app wants to keep control (e.g. a user-profile setting).
190
197
  */
191
198
  locale?: SaLocale | Ref<SaLocale>;
192
199
  /** Per-locale string overrides layered over the platform catalog. */
193
200
  overrides?: Partial<Record<SaLocale, SaMessagesOverrides>>;
201
+ /**
202
+ * Remembers the locale picked in the header switcher across reloads,
203
+ * default `true`. Ignored when `locale` is a `Ref`: the app owns that value
204
+ * and persists it wherever it belongs (user profile, its own store).
205
+ */
206
+ persist?: boolean;
207
+ /**
208
+ * Storage adapter for `persist`. Defaults to `defaultKvStore()`. Pass a
209
+ * key-prefixing wrapper when several apps share one origin.
210
+ */
211
+ storage?: KvStore;
212
+ /**
213
+ * Renders the language switcher in the shell chrome, default `true`. Set
214
+ * `false` for a deployment that ships one language. A readonly `locale`
215
+ * ref disables it on its own — no need to set this as well.
216
+ */
217
+ switcher?: boolean;
194
218
  }
219
+ /** `KvStore` key holding the locale the user picked in the switcher. */
220
+ declare const SA_LOCALE_STORAGE_KEY = "sa:locale";
195
221
  declare const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n>;
196
222
  declare function createSuperAdminI18n(options?: SuperAdminI18nOptions): SuperAdminI18n;
197
223
  /**
@@ -207,4 +233,4 @@ declare function useSuperAdminI18n(): SuperAdminI18n;
207
233
  */
208
234
  declare function useSaMessages<K extends keyof SaMessages>(namespace: K): ComputedRef<SaMessages[K]>;
209
235
 
210
- export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SUPER_ADMIN_ACTIONS_KEY as g, SUPER_ADMIN_BRAND_KEY as h, SUPER_ADMIN_ENDPOINTS_KEY as i, SUPER_ADMIN_EXTENSIONS_KEY as j, SUPER_ADMIN_HTTP_KEY as k, SUPER_ADMIN_I18N_KEY as l, SUPER_ADMIN_LOGIN_ADAPTER_KEY as m, SUPER_ADMIN_MANIFEST_KEY as n, SUPER_ADMIN_NOTIFY_KEY as o, type SuperAdminAuthGuardOptions as p, type SuperAdminLoginResult as q, type SuperAdminManifestGuardOptions as r, type UiNotifyKind as s, type UiNotifyOptions as t, buildNavigationGuard as u, createSuperAdminI18n as v, useSaMessages as w, useSuperAdminI18n as x };
236
+ export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SA_LOCALE_STORAGE_KEY as g, SUPER_ADMIN_ACTIONS_KEY as h, SUPER_ADMIN_BRAND_KEY as i, SUPER_ADMIN_ENDPOINTS_KEY as j, SUPER_ADMIN_EXTENSIONS_KEY as k, SUPER_ADMIN_HTTP_KEY as l, SUPER_ADMIN_I18N_KEY as m, SUPER_ADMIN_LOGIN_ADAPTER_KEY as n, SUPER_ADMIN_MANIFEST_KEY as o, SUPER_ADMIN_NOTIFY_KEY as p, type SuperAdminAuthGuardOptions as q, type SuperAdminLoginResult as r, type SuperAdminManifestGuardOptions as s, type UiNotifyKind as t, type UiNotifyOptions as u, buildNavigationGuard as v, createSuperAdminI18n as w, useSaMessages as x, useSuperAdminI18n as y };
@@ -1,7 +1,7 @@
1
1
  import { Component, App, InjectionKey, Ref, ComputedRef } from 'vue';
2
2
  import { NavigationGuardWithThis } from 'vue-router';
3
3
  import { ActionKey, ComponentKey, AdminManifest } from '@saasicat/types';
4
- import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaMessagesOverrides } from './messages-BIYw32u1.cjs';
4
+ import { A as ActionHandler, H as HttpClient, S as SaLocale, a as SaMessages, b as SaMessagesOverrides, K as KvStore } from './messages-DetB75lX.cjs';
5
5
 
6
6
  /**
7
7
  * App-specific branding data that the platform `AdminLayout` and other
@@ -182,16 +182,42 @@ interface SuperAdminI18n {
182
182
  messages: ComputedRef<SaMessages>;
183
183
  /** BCP-47 tag for `Intl`/`toLocaleString` formatting in the active locale. */
184
184
  intlLocale: ComputedRef<string>;
185
+ /**
186
+ * Whether the shell renders its language switcher. `false` when the app
187
+ * opted out or when `locale` cannot be written to — a switcher that cannot
188
+ * change anything is worse than none.
189
+ */
190
+ switcherEnabled: boolean;
185
191
  }
186
192
  interface SuperAdminI18nOptions {
187
193
  /**
188
- * Initial UI locale, default `'de'`. Pass a `Ref` when the app wants to
189
- * keep control (e.g. a user-profile setting or a header switcher).
194
+ * Starting UI locale, default `'de'` — the shell's switcher lets the user
195
+ * pick another one, and a stored pick outranks this. Pass a `Ref` when the
196
+ * app wants to keep control (e.g. a user-profile setting).
190
197
  */
191
198
  locale?: SaLocale | Ref<SaLocale>;
192
199
  /** Per-locale string overrides layered over the platform catalog. */
193
200
  overrides?: Partial<Record<SaLocale, SaMessagesOverrides>>;
201
+ /**
202
+ * Remembers the locale picked in the header switcher across reloads,
203
+ * default `true`. Ignored when `locale` is a `Ref`: the app owns that value
204
+ * and persists it wherever it belongs (user profile, its own store).
205
+ */
206
+ persist?: boolean;
207
+ /**
208
+ * Storage adapter for `persist`. Defaults to `defaultKvStore()`. Pass a
209
+ * key-prefixing wrapper when several apps share one origin.
210
+ */
211
+ storage?: KvStore;
212
+ /**
213
+ * Renders the language switcher in the shell chrome, default `true`. Set
214
+ * `false` for a deployment that ships one language. A readonly `locale`
215
+ * ref disables it on its own — no need to set this as well.
216
+ */
217
+ switcher?: boolean;
194
218
  }
219
+ /** `KvStore` key holding the locale the user picked in the switcher. */
220
+ declare const SA_LOCALE_STORAGE_KEY = "sa:locale";
195
221
  declare const SUPER_ADMIN_I18N_KEY: InjectionKey<SuperAdminI18n>;
196
222
  declare function createSuperAdminI18n(options?: SuperAdminI18nOptions): SuperAdminI18n;
197
223
  /**
@@ -207,4 +233,4 @@ declare function useSuperAdminI18n(): SuperAdminI18n;
207
233
  */
208
234
  declare function useSaMessages<K extends keyof SaMessages>(namespace: K): ComputedRef<SaMessages[K]>;
209
235
 
210
- export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SUPER_ADMIN_ACTIONS_KEY as g, SUPER_ADMIN_BRAND_KEY as h, SUPER_ADMIN_ENDPOINTS_KEY as i, SUPER_ADMIN_EXTENSIONS_KEY as j, SUPER_ADMIN_HTTP_KEY as k, SUPER_ADMIN_I18N_KEY as l, SUPER_ADMIN_LOGIN_ADAPTER_KEY as m, SUPER_ADMIN_MANIFEST_KEY as n, SUPER_ADMIN_NOTIFY_KEY as o, type SuperAdminAuthGuardOptions as p, type SuperAdminLoginResult as q, type SuperAdminManifestGuardOptions as r, type UiNotifyKind as s, type UiNotifyOptions as t, buildNavigationGuard as u, createSuperAdminI18n as v, useSaMessages as w, useSuperAdminI18n as x };
236
+ export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SA_LOCALE_STORAGE_KEY as g, SUPER_ADMIN_ACTIONS_KEY as h, SUPER_ADMIN_BRAND_KEY as i, SUPER_ADMIN_ENDPOINTS_KEY as j, SUPER_ADMIN_EXTENSIONS_KEY as k, SUPER_ADMIN_HTTP_KEY as l, SUPER_ADMIN_I18N_KEY as m, SUPER_ADMIN_LOGIN_ADAPTER_KEY as n, SUPER_ADMIN_MANIFEST_KEY as o, SUPER_ADMIN_NOTIFY_KEY as p, type SuperAdminAuthGuardOptions as q, type SuperAdminLoginResult as r, type SuperAdminManifestGuardOptions as s, type UiNotifyKind as t, type UiNotifyOptions as u, buildNavigationGuard as v, createSuperAdminI18n as w, useSaMessages as x, useSuperAdminI18n as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/ui-vue",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Vue 3 components, resource clients and composables for the SuperAdmin UI shell. Provides boot and manifest loaders, navigation, actions and standard pages.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -53,7 +53,7 @@
53
53
  "src"
54
54
  ],
55
55
  "dependencies": {
56
- "@saasicat/types": "^0.15.0"
56
+ "@saasicat/types": "^0.16.0"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "pinia": "^2.0.0 || ^3.0.0",
@@ -13,3 +13,18 @@ export const SA_INTL_LOCALES: Record<SaLocale, string> = {
13
13
  de: 'de-DE',
14
14
  en: 'en-US',
15
15
  };
16
+
17
+ /**
18
+ * Names for the language switcher. Endonyms — a language is listed in its own
19
+ * language, so the entries stay readable no matter which locale is active and
20
+ * never need translating.
21
+ */
22
+ export const SA_LOCALE_LABELS: Record<SaLocale, string> = {
23
+ de: 'Deutsch',
24
+ en: 'English',
25
+ };
26
+
27
+ /** Narrows untrusted input (persisted preference, query parameter) to a locale. */
28
+ export function isSaLocale(value: unknown): value is SaLocale {
29
+ return typeof value === 'string' && (SA_LOCALES as readonly string[]).includes(value);
30
+ }
@@ -10,6 +10,7 @@ export const shellMessages = defineMessages(
10
10
  subtitle: 'SuperAdmin · Plattform-Verwaltung',
11
11
  roleBadge: 'SUPER ADMIN',
12
12
  logout: 'Abmelden',
13
+ language: 'Sprache',
13
14
  },
14
15
  drawer: {
15
16
  docs: 'Doku öffnen',
@@ -81,6 +82,7 @@ export const shellMessages = defineMessages(
81
82
  subtitle: 'SuperAdmin · Platform administration',
82
83
  roleBadge: 'SUPER ADMIN',
83
84
  logout: 'Sign out',
85
+ language: 'Language',
84
86
  },
85
87
  drawer: {
86
88
  docs: 'Open documentation',
@@ -28,27 +28,64 @@ export interface KvStore {
28
28
  remove(key: string): void;
29
29
  }
30
30
 
31
+ /** Stand-in for when there is no usable `localStorage`. */
32
+ const NOOP_KV_STORE: KvStore = {
33
+ get: () => null,
34
+ set: () => {},
35
+ remove: () => {},
36
+ };
37
+
31
38
  /**
32
- * Returns a `KvStore` wrapper around `localStorage` (or `null` under SSR).
33
- * Tests may pass an in-memory stub.
39
+ * Returns a `KvStore` wrapper around `localStorage`, or a no-op store when the
40
+ * environment has none or refuses access. Tests may pass an in-memory stub.
41
+ *
42
+ * Every persistence built on this is a convenience — losing it must never take
43
+ * the app down, so no failure mode is allowed to escape.
34
44
  */
35
45
  export function defaultKvStore(): KvStore {
36
- if (typeof globalThis.localStorage === 'undefined') {
37
- // SSR / non-browser → no-op stub.
38
- return {
39
- get: () => null,
40
- set: () => {},
41
- remove: () => {},
42
- };
43
- }
44
- const ls = globalThis.localStorage;
46
+ const ls = resolveLocalStorage();
47
+ if (!ls) return NOOP_KV_STORE;
45
48
  return {
46
- get: (k) => ls.getItem(k),
47
- set: (k, v) => ls.setItem(k, v),
48
- remove: (k) => ls.removeItem(k),
49
+ get: (k) => {
50
+ try {
51
+ return ls.getItem(k);
52
+ } catch {
53
+ // See resolveLocalStorage: access can be revoked per call too.
54
+ return null;
55
+ }
56
+ },
57
+ set: (k, v) => {
58
+ try {
59
+ ls.setItem(k, v);
60
+ } catch {
61
+ // Full quota or Safari's private mode — the value is simply
62
+ // not remembered.
63
+ }
64
+ },
65
+ remove: (k) => {
66
+ try {
67
+ ls.removeItem(k);
68
+ } catch {
69
+ // Nothing to clean up if the store is unreachable.
70
+ }
71
+ },
49
72
  };
50
73
  }
51
74
 
75
+ /**
76
+ * `localStorage` is a throwing getter, not a plain property: a browser that
77
+ * blocks site data (cookie blocking, a sandboxed iframe) raises `SecurityError`
78
+ * on read, and `typeof` evaluates the getter rather than guarding it. The
79
+ * access therefore has to happen inside `try`.
80
+ */
81
+ function resolveLocalStorage(): Storage | null {
82
+ try {
83
+ return globalThis.localStorage ?? null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
52
89
  /**
53
90
  * Default `HttpClient` over `fetch`. Consumers pass their own variant
54
91
  * through when they need auth headers / tenant headers / retry logic.
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <q-btn
3
+ v-if="visible"
4
+ class="sa-locale-switcher"
5
+ flat
6
+ dense
7
+ no-caps
8
+ icon="language"
9
+ :label="SA_LOCALE_LABELS[locale]"
10
+ :aria-label="msg.header.language"
11
+ >
12
+ <q-menu auto-close anchor="bottom right" self="top right">
13
+ <q-list dense class="sa-locale-switcher__list">
14
+ <q-item
15
+ v-for="option in SA_LOCALES"
16
+ :key="option"
17
+ clickable
18
+ :active="option === locale"
19
+ @click="select(option)"
20
+ >
21
+ <q-item-section>{{ SA_LOCALE_LABELS[option] }}</q-item-section>
22
+ <q-item-section v-if="option === locale" side>
23
+ <q-icon name="check" size="16px" />
24
+ </q-item-section>
25
+ </q-item>
26
+ </q-list>
27
+ </q-menu>
28
+ </q-btn>
29
+ </template>
30
+
31
+ <script setup lang="ts">
32
+ import { SA_LOCALES, SA_LOCALE_LABELS, type SaLocale } from '../client/i18n/index.js';
33
+ import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
34
+
35
+ // Language switcher for the shell chrome. Writes straight to the shared i18n
36
+ // locale, so every catalog text, the sidebar labels and `Intl` formatting
37
+ // follow on the next render.
38
+ //
39
+ // The button inherits its color from the surrounding chrome (`currentColor`),
40
+ // so it needs no variant prop — each host sets the color it wants.
41
+ //
42
+ // It renders nothing when the app opted out of the switcher or handed over a
43
+ // locale it does not allow writing to; hosts therefore embed it unconditionally
44
+ // and never have to repeat that check.
45
+
46
+ const { locale, switcherEnabled } = useSuperAdminI18n();
47
+
48
+ // Only an explicit `false` hides the switcher. The injection key is a
49
+ // `Symbol.for`, so a context created by an older copy of this package resolves
50
+ // here just as well — and that one has no `switcherEnabled` at all. Treating
51
+ // the missing field as "off" would make every switcher vanish after such an
52
+ // upgrade, without a warning anywhere. Chrome fails open.
53
+ const visible = switcherEnabled !== false;
54
+ const msg = useSaMessages('shell');
55
+
56
+ function select(next: SaLocale): void {
57
+ locale.value = next;
58
+ }
59
+ </script>
60
+
61
+ <style scoped>
62
+ .sa-locale-switcher {
63
+ color: inherit;
64
+ }
65
+ .sa-locale-switcher__list {
66
+ min-width: 160px;
67
+ }
68
+ </style>
@@ -14,6 +14,7 @@
14
14
  </q-toolbar-title>
15
15
  <q-space />
16
16
  <slot name="header-actions" />
17
+ <LocaleSwitcher class="q-mr-sm" />
17
18
  <q-badge class="sa-admin-badge q-mr-sm">{{ msg.header.roleBadge }}</q-badge>
18
19
  <div class="sa-admin-user">
19
20
  <q-avatar size="32px" class="sa-admin-user__avatar">
@@ -97,6 +98,7 @@ import { computed, inject, ref } from 'vue';
97
98
  import { useRoute } from 'vue-router';
98
99
  import type { AdminManifest, StandardPageKey } from '@saasicat/types';
99
100
  import { buildRoutes, buildSidebar, defaultSectionOrder } from '../client/nav-builder.js';
101
+ import LocaleSwitcher from '../components/LocaleSwitcher.vue';
100
102
  import { SUPER_ADMIN_BRAND_KEY, SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
101
103
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
102
104
  import { ADMIN_UI_VERSION } from '../client/version.js';
@@ -18,6 +18,7 @@
18
18
  <div class="sa-login-brand__name">{{ brandName }}</div>
19
19
  <div class="sa-login-brand__tag">{{ tagText }}</div>
20
20
  </div>
21
+ <LocaleSwitcher class="sa-login-locale" />
21
22
  </div>
22
23
 
23
24
  <h1 class="sa-login-title">{{ msg.login.signIn }}</h1>
@@ -98,6 +99,7 @@ import {
98
99
  } from '../vue/use-super-admin-context.js';
99
100
  import { getJson } from '../client/http-json.js';
100
101
  import { useSaMessages } from '../vue/use-super-admin-i18n.js';
102
+ import LocaleSwitcher from '../components/LocaleSwitcher.vue';
101
103
  import SuperAdminSetupWizard from './SuperAdminSetupWizard.vue';
102
104
 
103
105
  interface Props {
@@ -193,6 +195,13 @@ async function handleSubmit(): Promise<void> {
193
195
  </script>
194
196
 
195
197
  <style scoped>
198
+ .sa-login-locale {
199
+ /* In the card's flow rather than floating over it: a fixed overlay lands
200
+ on top of the centered card on short viewports. */
201
+ margin-left: auto;
202
+ align-self: flex-start;
203
+ color: var(--sa-login-tag-color, #475569);
204
+ }
196
205
  .sa-login-wrap {
197
206
  min-height: 100vh;
198
207
  display: flex;
@@ -7,6 +7,7 @@
7
7
  <div class="sa-setup-title">{{ msg.setup.title }}</div>
8
8
  <div class="sa-setup-sub">{{ subtitleText }}</div>
9
9
  </div>
10
+ <LocaleSwitcher class="sa-setup-locale" />
10
11
  </div>
11
12
 
12
13
  <q-banner v-if="errorMessage" class="sa-setup-error" rounded>
@@ -160,6 +161,7 @@ import {
160
161
  import { HttpJsonError, postJson as httpPostJson } from '../client/http-json.js';
161
162
  import { formatMessage } from '../client/i18n/format.js';
162
163
  import { useSaMessages } from '../vue/use-super-admin-i18n.js';
164
+ import LocaleSwitcher from '../components/LocaleSwitcher.vue';
163
165
 
164
166
  interface Props {
165
167
  /** Display name + badge abbreviation (override the app branding, e.g. from PublicBoot). */
@@ -288,6 +290,11 @@ async function submitConfirm(): Promise<void> {
288
290
  gap: 12px;
289
291
  margin-bottom: 20px;
290
292
  }
293
+ .sa-setup-locale {
294
+ margin-left: auto;
295
+ align-self: flex-start;
296
+ color: #475569;
297
+ }
291
298
  .sa-setup-badge {
292
299
  width: 44px;
293
300
  height: 44px;