@saasicat/ui-vue 0.24.2 → 0.26.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.
Files changed (90) hide show
  1. package/README.md +32 -8
  2. package/dist/{catalog-DJcfm5N2.d.cts → catalog-Dch1Ryw0.d.cts} +353 -69
  3. package/dist/{catalog-DJcfm5N2.d.ts → catalog-Dch1Ryw0.d.ts} +353 -69
  4. package/dist/{chunk-F7NRM6KI.js → chunk-BPF2BMCQ.js} +748 -24
  5. package/dist/{chunk-D74FFCCP.js → chunk-NEXTZZRQ.js} +95 -177
  6. package/dist/{chunk-TQQMKWN6.js → chunk-O3J3ITF2.js} +119 -7
  7. package/dist/client/index.cjs +758 -83
  8. package/dist/client/index.d.cts +414 -9
  9. package/dist/client/index.d.ts +414 -9
  10. package/dist/client/index.js +84 -12
  11. package/dist/index.cjs +1325 -294
  12. package/dist/index.d.cts +205 -15
  13. package/dist/index.d.ts +205 -15
  14. package/dist/index.js +566 -240
  15. package/dist/quasar/index.cjs +574 -62
  16. package/dist/quasar/index.d.cts +61 -6
  17. package/dist/quasar/index.d.ts +61 -6
  18. package/dist/quasar/index.js +83 -19
  19. package/dist/{use-sa-theme-OUBlaqgl.d.ts → resource-registry-D7Xjs-5f.d.cts} +157 -7
  20. package/dist/{use-sa-theme-B5t7oXph.d.cts → resource-registry-TaR7rtq0.d.ts} +157 -7
  21. package/package.json +4 -3
  22. package/src/client/admin-error.ts +552 -0
  23. package/src/client/admin-resource-client.ts +9 -12
  24. package/src/client/batch-column-fetcher.ts +2 -2
  25. package/src/client/boot-loader.ts +12 -1
  26. package/src/client/http/axios-http-client.ts +383 -0
  27. package/src/client/http/fetch-http-client.ts +89 -0
  28. package/src/client/http/index.ts +6 -0
  29. package/src/client/http-json.ts +82 -16
  30. package/src/client/i18n/messages/discovery.ts +0 -6
  31. package/src/client/i18n/messages/errors.ts +44 -0
  32. package/src/client/i18n/messages/marketing.ts +0 -6
  33. package/src/client/i18n/messages/promos.ts +0 -2
  34. package/src/client/i18n/messages/shell.ts +12 -0
  35. package/src/client/i18n/messages.ts +3 -0
  36. package/src/client/index.ts +6 -0
  37. package/src/client/manifest-loader.ts +65 -15
  38. package/src/client/resources/audit.resource.ts +25 -0
  39. package/src/client/resources/define-resource.ts +166 -0
  40. package/src/client/resources/index.ts +42 -0
  41. package/src/client/resources/list-resource.ts +176 -0
  42. package/src/client/resources/plans.resource.ts +169 -0
  43. package/src/client/resources/resource-request.ts +117 -0
  44. package/src/client/resources/tenants.resource.ts +27 -0
  45. package/src/client/types.ts +10 -3
  46. package/src/components/BundleVersionPublishDialog.vue +1 -1
  47. package/src/components/MarketingPromotionsTab.vue +263 -241
  48. package/src/components/ThemeSwitcher.vue +85 -0
  49. package/src/components/admin-page/AdminAccordion.vue +125 -0
  50. package/src/components/bundle-editor/BundleCreatePanel.vue +1 -1
  51. package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
  52. package/src/components/plan/PlanCycleToggle.vue +1 -1
  53. package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
  54. package/src/index.ts +5 -0
  55. package/src/pages-standard/AdminLayout.vue +57 -0
  56. package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
  57. package/src/pages-standard/BundlesPage.vue +5 -35
  58. package/src/pages-standard/DiscoveryPage.vue +8 -1
  59. package/src/pages-standard/MarketingCatalogPage.vue +14 -3
  60. package/src/pages-standard/SuperAdminLoginPage.vue +23 -2
  61. package/src/pages-standard/SuperAdminSetupWizard.vue +37 -3
  62. package/src/pages-standard/bundles-page/BundleAccordionList.vue +30 -32
  63. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +80 -88
  64. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +73 -79
  65. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
  66. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
  67. package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
  68. package/src/quasar/confirm.ts +83 -0
  69. package/src/quasar/create-super-admin-app.ts +84 -10
  70. package/src/quasar/dark-bridge.ts +47 -10
  71. package/src/quasar/index.ts +1 -0
  72. package/src/ui/theme/components/accordion.css +118 -0
  73. package/src/ui/theme/index.css +1 -0
  74. package/src/vue/create-admin-routes.ts +9 -1
  75. package/src/vue/platform-loaders.ts +12 -2
  76. package/src/vue/resource-registry.ts +211 -0
  77. package/src/vue/super-admin-context.ts +20 -5
  78. package/src/vue/ui-confirm.ts +76 -0
  79. package/src/vue/use-api-list.ts +50 -45
  80. package/src/vue/use-async-action.ts +170 -0
  81. package/src/vue/use-async-data.ts +83 -0
  82. package/src/vue/use-bundles.ts +34 -7
  83. package/src/vue/use-catalog-entries.ts +37 -12
  84. package/src/vue/use-discovery.ts +27 -6
  85. package/src/vue/use-marketing-projections.ts +29 -7
  86. package/src/vue/use-plans.ts +36 -8
  87. package/src/vue/use-promotions.ts +27 -7
  88. package/src/vue/use-resource-list.ts +272 -0
  89. package/src/vue/use-sa-theme.ts +34 -5
  90. package/src/vue/use-tenant-subscription-bundles.ts +22 -3
@@ -23,17 +23,149 @@ __export(quasar_exports, {
23
23
  SA_PORTAL_CLASS: () => SA_PORTAL_CLASS,
24
24
  bindSaThemeToDocument: () => bindSaThemeToDocument,
25
25
  createSuperAdminApp: () => createSuperAdminApp,
26
+ quasarConfirm: () => quasarConfirm,
27
+ quasarConfirmOptions: () => quasarConfirmOptions,
26
28
  quasarNotify: () => quasarNotify,
29
+ resolveQuasarOptions: () => resolveQuasarOptions,
30
+ useSuperAdminConfirm: () => useSuperAdminConfirm,
27
31
  useSuperAdminNotify: () => useSuperAdminNotify
28
32
  });
29
33
  module.exports = __toCommonJS(quasar_exports);
30
34
 
31
35
  // src/quasar/create-super-admin-app.ts
32
- var import_vue5 = require("vue");
33
- var import_quasar3 = require("quasar");
36
+ var import_vue7 = require("vue");
37
+ var import_quasar4 = require("quasar");
34
38
  var import_pinia = require("pinia");
35
39
  var import_vue_router = require("vue-router");
36
40
 
41
+ // src/client/i18n/format.ts
42
+ var import_types = require("@saasicat/types");
43
+
44
+ // src/client/admin-error.ts
45
+ var ADMIN_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/AdminError");
46
+ var PLATFORM_ERROR = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/PlatformError");
47
+ function markPlatformError(error) {
48
+ Object.defineProperty(error, PLATFORM_ERROR, { value: true });
49
+ }
50
+ var TRANSPORT_FAILURE = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/TransportFailure");
51
+ function markTransportFailure(error) {
52
+ if (typeof error === "object" && error !== null) {
53
+ try {
54
+ Object.defineProperty(error, TRANSPORT_FAILURE, { value: true });
55
+ } catch {
56
+ }
57
+ }
58
+ return error;
59
+ }
60
+ function describe(init) {
61
+ const status = init.status ?? 0;
62
+ const where = [init.method, init.url].filter(Boolean).join(" ");
63
+ const what = status > 0 ? `HTTP ${status}` : "request failed";
64
+ const head = where ? `${where} \u2192 ${what}` : what;
65
+ const code = init.code ? ` (${init.code})` : "";
66
+ return init.detail ? `${head}${code}: ${init.detail}` : `${head}${code}`;
67
+ }
68
+ var AdminError = class extends Error {
69
+ /** HTTP status. `0` means the request never produced one. */
70
+ status;
71
+ /** Machine-readable code from the response body, when it carried one. */
72
+ code;
73
+ /** Parsed response body, when there was one. */
74
+ body;
75
+ /** Requested URL, when known. */
76
+ url;
77
+ /** Request method, when known. */
78
+ method;
79
+ /**
80
+ * Text the failing side supplied — the only candidate for user-facing
81
+ * output. `undefined` when nothing was supplied.
82
+ */
83
+ detail;
84
+ /** The request completed but the answer was unusable. See `AdminErrorInit`. */
85
+ emptyResponse;
86
+ /** The request never reached the server. See `AdminErrorInit`. */
87
+ transportFailure;
88
+ constructor(init = {}) {
89
+ super(init.message ?? describe(init), { cause: init.cause });
90
+ this.name = "AdminError";
91
+ this.status = init.status ?? 0;
92
+ this.code = init.code;
93
+ this.body = init.body;
94
+ this.url = init.url;
95
+ this.method = init.method;
96
+ this.detail = init.detail;
97
+ this.emptyResponse = init.emptyResponse ?? false;
98
+ this.transportFailure = init.transportFailure ?? false;
99
+ Object.defineProperty(this, ADMIN_ERROR, { value: true });
100
+ markPlatformError(this);
101
+ }
102
+ };
103
+ function asRecord(value) {
104
+ return typeof value === "object" && value !== null ? value : void 0;
105
+ }
106
+ function asString(value) {
107
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
108
+ }
109
+ function readErrorDetail(body) {
110
+ const record = asRecord(body);
111
+ if (!record) return asString(body);
112
+ const message = record.message;
113
+ if (Array.isArray(message)) {
114
+ const parts = message.filter((part) => typeof part === "string");
115
+ return parts.length > 0 ? parts.join(", ") : void 0;
116
+ }
117
+ return asString(message) ?? asString(record.error) ?? asString(record.reason);
118
+ }
119
+ function readErrorCode(body) {
120
+ return asString(asRecord(body)?.code);
121
+ }
122
+
123
+ // src/client/http-json.ts
124
+ function serverAnswered(status) {
125
+ return status > 0;
126
+ }
127
+ function requireServerAnswer(status, method, url, raise) {
128
+ if (serverAnswered(status)) return;
129
+ throw markTransportFailure(
130
+ raise(`${method} ${url} produced no HTTP status \u2014 the request did not reach the server`)
131
+ );
132
+ }
133
+ function trimTrailingSlashes(url) {
134
+ let end = url.length;
135
+ while (end > 0 && url[end - 1] === "/") end--;
136
+ return url.slice(0, end);
137
+ }
138
+
139
+ // src/client/http/fetch-http-client.ts
140
+ var ABSOLUTE_URL = /^[a-z][a-z0-9+.-]*:|^\/\//i;
141
+ function resolveUrl(url, baseUrl) {
142
+ if (!baseUrl || ABSOLUTE_URL.test(url)) return url;
143
+ return `${trimTrailingSlashes(baseUrl)}${url.startsWith("/") ? "" : "/"}${url}`;
144
+ }
145
+ function createFetchHttpClient(options = {}) {
146
+ return async (url, init) => {
147
+ const headers = new Headers(await options.headers?.());
148
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
149
+ for (const [name, value] of Object.entries(init?.headers ?? {})) {
150
+ headers.set(name, value);
151
+ }
152
+ if (init?.body !== void 0 && !headers.has("Content-Type")) {
153
+ headers.set("Content-Type", "application/json");
154
+ }
155
+ let response;
156
+ try {
157
+ response = await fetch(resolveUrl(url, options.baseUrl), {
158
+ method: init?.method ?? "GET",
159
+ headers,
160
+ body: init?.body
161
+ });
162
+ } catch (err) {
163
+ throw markTransportFailure(err);
164
+ }
165
+ return response;
166
+ };
167
+ }
168
+
37
169
  // src/client/types.ts
38
170
  var NOOP_KV_STORE = {
39
171
  get: () => null,
@@ -75,7 +207,7 @@ function resolveLocalStorage() {
75
207
  }
76
208
  }
77
209
  function defaultHttpClient() {
78
- return (url, init) => fetch(url, init);
210
+ return createFetchHttpClient();
79
211
  }
80
212
 
81
213
  // src/vue/super-admin-context.ts
@@ -148,15 +280,294 @@ var SUPER_ADMIN_NOTIFY_KEY = /* @__PURE__ */ Symbol.for(
148
280
  "@saasicat/ui-vue/SUPER_ADMIN_NOTIFY"
149
281
  );
150
282
 
151
- // src/vue/use-sa-theme.ts
283
+ // src/vue/ui-confirm.ts
284
+ var SUPER_ADMIN_CONFIRM_KEY = /* @__PURE__ */ Symbol.for(
285
+ "@saasicat/ui-vue/SUPER_ADMIN_CONFIRM"
286
+ );
287
+
288
+ // src/vue/resource-registry.ts
152
289
  var import_vue = require("vue");
153
- var SCHEMES = ["light", "dark", "system"];
154
- var STORAGE_KEY = "saasicat.theme.scheme";
290
+
291
+ // src/client/resources/define-resource.ts
292
+ function defineResource(key, ops, options = {}) {
293
+ return { key, ops, projectScoped: options.projectScoped ?? false };
294
+ }
295
+ function requireProjectKey(def, ctx) {
296
+ if (!def.projectScoped) return;
297
+ if (typeof ctx.projectKey === "string" && ctx.projectKey.trim() !== "") return;
298
+ throw new Error(
299
+ `Resource "${def.key}" is project-scoped, but the context it is bound to names no project. Its requests would go out as \`?projectKey=\`, which the admin API answers for no project at all: the catalogue reads as empty and the first create fails validation instead. Name the project this admin administers \u2014 \`endpoints.projectKey\` for createSuperAdminApp(), \`context.projectKey\` for createResourceRegistry().`
300
+ );
301
+ }
302
+ function bindResource(def, http, ctx) {
303
+ const readContext = typeof ctx === "function" ? ctx : () => ctx;
304
+ if (def.projectScoped) requireProjectKey(def, readContext());
305
+ const bound = {};
306
+ for (const [name, op] of Object.entries(def.ops)) {
307
+ bound[name] = async (...args) => {
308
+ const context = readContext();
309
+ requireProjectKey(def, context);
310
+ return op(http, context, ...args);
311
+ };
312
+ }
313
+ return bound;
314
+ }
315
+
316
+ // src/client/resources/resource-request.ts
317
+ async function requestJson(http, url, init = {}) {
318
+ const method = init.method ?? "GET";
319
+ const response = await http(url, {
320
+ method,
321
+ headers: { "content-type": "application/json", ...init.headers },
322
+ body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
323
+ });
324
+ requireServerAnswer(
325
+ response.status,
326
+ method,
327
+ url,
328
+ (diagnostic) => new AdminError({ status: response.status, url, method, message: diagnostic })
329
+ );
330
+ if (response.status === 204) return null;
331
+ const body = await response.json().catch(() => null);
332
+ if (response.status >= 400) {
333
+ throw new AdminError({
334
+ status: response.status,
335
+ code: readErrorCode(body),
336
+ body,
337
+ // The third place this had to be read, and the second time it was
338
+ // read a narrower way: without `detail` a page migrating onto these
339
+ // resources loses "Plan already exists" and shows the generic
340
+ // status sentence instead. One decision, one function.
341
+ detail: readErrorDetail(body),
342
+ url,
343
+ method
344
+ });
345
+ }
346
+ return body;
347
+ }
348
+ async function requestJsonBody(http, url, what, init = {}) {
349
+ const body = await requestJson(http, url, init);
350
+ if (body === null || body === void 0) {
351
+ throw new AdminError({
352
+ status: 0,
353
+ url,
354
+ method: init.method ?? "GET",
355
+ // `what` is a diagnostic — "Create returned no body" — so it goes
356
+ // on `message`, not `detail`. As `detail` it out-ranked the
357
+ // localized wording and the operator read an English internal
358
+ // string instead of being told the change may already have landed.
359
+ message: what,
360
+ emptyResponse: true
361
+ });
362
+ }
363
+ return body;
364
+ }
365
+
366
+ // src/client/resources/list-resource.ts
367
+ var LIST_FIRST_PAGE = 1;
368
+ var LIST_PAGE_SIZE_DEFAULT = 50;
369
+ function isSentInQuery(value) {
370
+ return value !== void 0 && value !== null && value !== "";
371
+ }
372
+ function appendFilter(params, filter) {
373
+ for (const [key, value] of Object.entries(filter ?? {})) {
374
+ if (!isSentInQuery(value)) continue;
375
+ params.set(key, String(value));
376
+ }
377
+ }
378
+ function listUrl(endpoint, query = {}) {
379
+ const params = new URLSearchParams();
380
+ params.set("page", String(query.page ?? LIST_FIRST_PAGE));
381
+ params.set("pageSize", String(query.pageSize ?? LIST_PAGE_SIZE_DEFAULT));
382
+ appendFilter(params, query.filter);
383
+ return `${endpoint}${endpoint.includes("?") ? "&" : "?"}${params.toString()}`;
384
+ }
385
+ function readListPage(raw) {
386
+ if (Array.isArray(raw)) return { items: raw, total: raw.length };
387
+ if (raw === null || typeof raw !== "object") return { items: [], total: 0 };
388
+ const body = raw;
389
+ const page = {
390
+ items: Array.isArray(body.items) ? body.items : []
391
+ };
392
+ if (typeof body.total === "number") page.total = body.total;
393
+ if (typeof body.page === "number") page.page = body.page;
394
+ if (typeof body.pageSize === "number") page.pageSize = body.pageSize;
395
+ return page;
396
+ }
397
+ function defineListOp(endpoint) {
398
+ return async (http, ctx, query) => readListPage(await requestJson(http, listUrl(endpoint(ctx), query)));
399
+ }
400
+
401
+ // src/client/resources/plans.resource.ts
402
+ function plansUrl(ctx) {
403
+ return `${ctx.apiBase}/catalog/plans`;
404
+ }
405
+ function versionsUrl(ctx) {
406
+ return `${ctx.apiBase}/catalog/plan-versions`;
407
+ }
408
+ function scoped(url, ctx) {
409
+ return `${url}?projectKey=${encodeURIComponent(ctx.projectKey)}`;
410
+ }
411
+ var plansResource = defineResource(
412
+ "plans",
413
+ {
414
+ list: async (http, ctx) => await requestJson(http, scoped(plansUrl(ctx), ctx)) ?? [],
415
+ tenantCounts: async (http, ctx) => await requestJson(
416
+ http,
417
+ scoped(`${plansUrl(ctx)}/tenant-counts`, ctx)
418
+ ) ?? {},
419
+ /**
420
+ * The project comes from the context, not from the caller.
421
+ *
422
+ * The endpoint takes it in the body, and the composable this replaces
423
+ * made that the caller's job — so a page had to keep hold of the
424
+ * project it was administering just to create in it, and `list()` and
425
+ * `create()` read two different sources for one value. A stale one
426
+ * creates a plan outside the project the list is showing.
427
+ */
428
+ create: (http, ctx, data) => requestJsonBody(http, plansUrl(ctx), "Create returned no body", {
429
+ method: "POST",
430
+ body: { ...data, projectKey: ctx.projectKey }
431
+ }),
432
+ update: (http, ctx, planId, data) => requestJsonBody(
433
+ http,
434
+ `${plansUrl(ctx)}/${planId}`,
435
+ "Update returned no body",
436
+ {
437
+ method: "PATCH",
438
+ body: data
439
+ }
440
+ ),
441
+ /** Marks the plan deleted. Fails while it still has versions. */
442
+ softDelete: async (http, ctx, planId) => {
443
+ await requestJson(http, `${plansUrl(ctx)}/${planId}`, { method: "DELETE" });
444
+ },
445
+ /** Removes the plan for good. 422 `PLAN_HAS_VERSIONS` when it cannot. */
446
+ hardDelete: async (http, ctx, planId) => {
447
+ await requestJson(http, `${plansUrl(ctx)}/${planId}/purge`, { method: "DELETE" });
448
+ }
449
+ },
450
+ { projectScoped: true }
451
+ );
452
+ var planVersionsResource = defineResource("planVersions", {
453
+ listForPlan: async (http, ctx, planId) => await requestJson(http, `${plansUrl(ctx)}/${planId}/versions`) ?? [],
454
+ createDraft: (http, ctx, planId, data) => requestJsonBody(
455
+ http,
456
+ `${plansUrl(ctx)}/${planId}/versions`,
457
+ "CreateDraft returned no body",
458
+ { method: "POST", body: data }
459
+ ),
460
+ updateDraft: (http, ctx, versionId, data) => requestJsonBody(
461
+ http,
462
+ `${versionsUrl(ctx)}/${versionId}`,
463
+ "UpdateDraft returned no body",
464
+ { method: "PATCH", body: data }
465
+ ),
466
+ publish: (http, ctx, versionId, options = {}) => requestJsonBody(
467
+ http,
468
+ `${versionsUrl(ctx)}/${versionId}/publish`,
469
+ "Publish returned no body",
470
+ { method: "POST", body: options }
471
+ ),
472
+ discardDraft: async (http, ctx, versionId) => {
473
+ await requestJson(http, `${versionsUrl(ctx)}/${versionId}`, { method: "DELETE" });
474
+ },
475
+ /**
476
+ * Ends a live version on `endsAt`, with no successor. Idempotent — a
477
+ * second call with another date overwrites the first.
478
+ *
479
+ * `endsAt` is the date itself, not a payload: the endpoint's body is
480
+ * `{ endsAt }` and wrapping it is this operation's job, not the caller's.
481
+ */
482
+ terminate: (http, ctx, versionId, endsAt) => requestJsonBody(
483
+ http,
484
+ `${versionsUrl(ctx)}/${versionId}/terminate`,
485
+ "Terminate returned no body",
486
+ { method: "POST", body: { endsAt } }
487
+ )
488
+ });
489
+
490
+ // src/client/resources/tenants.resource.ts
491
+ var tenantsResource = defineResource("tenants", {
492
+ list: defineListOp((ctx) => `${ctx.apiBase}/tenants`)
493
+ });
494
+
495
+ // src/client/resources/audit.resource.ts
496
+ var auditResource = defineResource("audit", {
497
+ list: defineListOp((ctx) => `${ctx.apiBase}/audit`)
498
+ });
499
+
500
+ // src/client/resources/index.ts
501
+ var platformResources = {
502
+ plans: plansResource,
503
+ planVersions: planVersionsResource,
504
+ tenants: tenantsResource,
505
+ audit: auditResource
506
+ };
507
+
508
+ // src/vue/resource-registry.ts
509
+ function boundWithOverride(def, http, readContext, override) {
510
+ const context = override?.context ? () => ({ ...readContext(), ...override.context }) : readContext;
511
+ const platform = bindResource(def, override?.http ?? http, context);
512
+ if (!override?.ops) return platform;
513
+ const result = { ...platform };
514
+ for (const [name, wrap] of Object.entries(override.ops)) {
515
+ if (typeof wrap !== "function") continue;
516
+ if (!Object.hasOwn(platform, name)) {
517
+ throw new Error(
518
+ `createResourceRegistry: resource "${def.key}" has no operation "${name}" to override. It offers: ${Object.keys(platform).join(", ")}.`
519
+ );
520
+ }
521
+ const next = platform[name];
522
+ result[name] = (...args) => wrap(next, ...args);
523
+ }
524
+ return result;
525
+ }
526
+ function createResourceRegistry(options) {
527
+ if (!options?.http) {
528
+ throw new Error(
529
+ "createResourceRegistry: `http` is required. Pass the HttpClient your app already uses \u2014 createAxiosHttpClient(instance) or createFetchHttpClient({ headers }) \u2014 so every platform request carries its auth."
530
+ );
531
+ }
532
+ const readContext = typeof options.context === "function" ? options.context : () => options.context;
533
+ const unknown = Object.keys(options.overrides ?? {}).filter(
534
+ (key) => !Object.hasOwn(options.resources, key)
535
+ );
536
+ if (unknown.length > 0) {
537
+ throw new Error(
538
+ `createResourceRegistry: no resource named ${unknown.map((k) => `"${k}"`).join(", ")} to override. The registry holds: ${Object.keys(options.resources).join(", ")}.`
539
+ );
540
+ }
541
+ const bound = /* @__PURE__ */ new Map();
542
+ for (const [key, def] of Object.entries(options.resources)) {
543
+ bound.set(key, boundWithOverride(def, options.http, readContext, options.overrides?.[key]));
544
+ }
545
+ return {
546
+ get(key) {
547
+ const ops = bound.get(key);
548
+ if (!ops) {
549
+ throw new Error(
550
+ `useResource("${String(key)}"): no such resource. The registry offers: ${[...bound.keys()].join(", ")}.`
551
+ );
552
+ }
553
+ return ops;
554
+ },
555
+ keys: () => [...bound.keys()]
556
+ };
557
+ }
558
+ var SUPER_ADMIN_RESOURCES_KEY = /* @__PURE__ */ Symbol.for(
559
+ "@saasicat/ui-vue/SUPER_ADMIN_RESOURCES"
560
+ );
561
+
562
+ // src/vue/use-sa-theme.ts
563
+ var import_vue2 = require("vue");
564
+ var SA_COLOR_SCHEMES = ["light", "dark", "system"];
565
+ var SA_THEME_STORAGE_KEY = "saasicat.theme.scheme";
155
566
  function isScheme(value) {
156
- return typeof value === "string" && SCHEMES.includes(value);
567
+ return typeof value === "string" && SA_COLOR_SCHEMES.includes(value);
157
568
  }
158
569
  function systemScheme() {
159
- const value = (0, import_vue.ref)("light");
570
+ const value = (0, import_vue2.ref)("light");
160
571
  if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
161
572
  return { value, unsubscribe: () => {
162
573
  } };
@@ -173,14 +584,20 @@ function createSaTheme(options = {}) {
173
584
  const storage = options.storage ?? defaultKvStore();
174
585
  const persist = options.persist !== false;
175
586
  const provided = options.scheme;
176
- const appOwnsIt = (0, import_vue.isRef)(provided);
177
- const stored = persist && !appOwnsIt ? storage.get(STORAGE_KEY) : null;
178
- const scheme = (0, import_vue.isRef)(provided) ? provided : (0, import_vue.ref)(isScheme(stored) ? stored : provided ?? "system");
587
+ const appOwnsIt = (0, import_vue2.isRef)(provided);
588
+ const key = `${options.storageKeyPrefix ?? ""}${SA_THEME_STORAGE_KEY}`;
589
+ const stored = persist && !appOwnsIt ? storage.get(key) : null;
590
+ const scheme = (0, import_vue2.isRef)(provided) ? provided : (0, import_vue2.ref)(isScheme(stored) ? stored : provided ?? "system");
179
591
  const { value: system, unsubscribe } = systemScheme();
180
- const stopPersisting = persist && !appOwnsIt ? (0, import_vue.watch)(scheme, (next) => storage.set(STORAGE_KEY, next)) : null;
592
+ const stopPersisting = persist && !appOwnsIt ? (0, import_vue2.watch)(scheme, (next) => storage.set(key, next)) : null;
181
593
  return {
182
594
  scheme,
183
- resolved: (0, import_vue.computed)(() => scheme.value === "system" ? system.value : scheme.value),
595
+ resolved: (0, import_vue2.computed)(() => scheme.value === "system" ? system.value : scheme.value),
596
+ // A readonly ref (typically a `computed` derived from a profile
597
+ // setting) silently swallows writes, so the switcher would be a dead
598
+ // control. TypeScript ignores `readonly` when checking assignability,
599
+ // so the guard has to be a runtime one — same as the locale next door.
600
+ switcherEnabled: (options.switcher ?? true) && !(0, import_vue2.isReadonly)(scheme),
184
601
  dispose: () => {
185
602
  unsubscribe();
186
603
  stopPersisting?.();
@@ -190,7 +607,7 @@ function createSaTheme(options = {}) {
190
607
  var SA_THEME_KEY = /* @__PURE__ */ Symbol.for("@saasicat/ui-vue/SA_THEME");
191
608
 
192
609
  // src/vue/use-super-admin-i18n.ts
193
- var import_vue2 = require("vue");
610
+ var import_vue3 = require("vue");
194
611
 
195
612
  // src/client/i18n/locale.ts
196
613
  var SA_LOCALES = ["de", "en"];
@@ -228,9 +645,6 @@ function mergeMessages(base, overrides) {
228
645
  return result;
229
646
  }
230
647
 
231
- // src/client/i18n/format.ts
232
- var import_types2 = require("@saasicat/types");
233
-
234
648
  // src/client/i18n/messages/audit.ts
235
649
  var auditMessages = defineMessages(
236
650
  {
@@ -905,9 +1319,6 @@ var discoveryMessages = defineMessages(
905
1319
  replacedBy: "ersetzt durch {key}",
906
1320
  replaces: "ersetzt: {keys}",
907
1321
  reapproveEmphasis: "erneut freigeben",
908
- errorDiscoveryHttp: "Discovery-Endpoint antwortete mit HTTP {status}",
909
- errorRescanHttp: "Discovery-Rescan antwortete mit HTTP {status}",
910
- errorCatalogHttp: "Catalog-Entries-API antwortete mit HTTP {status}",
911
1322
  capList: {
912
1323
  orphanFeature: "Feature im Katalog ohne implementierende Capability \u2014 blockiert im blocking-Strict-Mode das Plan-Publish.",
913
1324
  removedFromCode: "aus Code entfernt"
@@ -1009,9 +1420,6 @@ var discoveryMessages = defineMessages(
1009
1420
  replacedBy: "replaced by {key}",
1010
1421
  replaces: "replaces: {keys}",
1011
1422
  reapproveEmphasis: "approve again",
1012
- errorDiscoveryHttp: "Discovery endpoint responded with HTTP {status}",
1013
- errorRescanHttp: "Discovery rescan responded with HTTP {status}",
1014
- errorCatalogHttp: "Catalog entries API responded with HTTP {status}",
1015
1423
  capList: {
1016
1424
  orphanFeature: "Feature in the catalog without an implementing capability \u2014 blocks plan publishing in blocking strict mode.",
1017
1425
  removedFromCode: "removed from code"
@@ -1216,6 +1624,36 @@ var emailMessages = defineMessages(
1216
1624
  }
1217
1625
  );
1218
1626
 
1627
+ // src/client/i18n/messages/errors.ts
1628
+ var errorsMessages = defineMessages(
1629
+ {
1630
+ network: "Die Anfrage konnte nicht abgeschlossen werden. Bitte Verbindung pr\xFCfen.",
1631
+ emptyResponse: "Der Server hat nichts zur\xFCckgegeben. Bitte pr\xFCfen, ob die \xC4nderung angekommen ist.",
1632
+ unauthorized: "Die Sitzung ist abgelaufen. Bitte erneut anmelden.",
1633
+ forbidden: "F\xFCr diese Aktion fehlt die Berechtigung.",
1634
+ notFound: "Der angeforderte Eintrag existiert nicht mehr.",
1635
+ conflict: "Der Eintrag wurde zwischenzeitlich ge\xE4ndert. Bitte neu laden.",
1636
+ validation: "Die Eingaben wurden nicht akzeptiert.",
1637
+ rateLimited: "Zu viele Anfragen. Bitte kurz warten und erneut versuchen.",
1638
+ server: "Der Server konnte die Anfrage nicht verarbeiten.",
1639
+ httpStatus: "Die Anfrage ist fehlgeschlagen (HTTP {status}).",
1640
+ unexpected: "Es ist ein unerwarteter Fehler aufgetreten. Bitte erneut versuchen."
1641
+ },
1642
+ {
1643
+ network: "The request could not be completed. Please check your connection.",
1644
+ emptyResponse: "The server returned nothing. Please check whether the change was applied.",
1645
+ unauthorized: "Your session has expired. Please sign in again.",
1646
+ forbidden: "You do not have permission for this action.",
1647
+ notFound: "The requested entry no longer exists.",
1648
+ conflict: "The entry changed in the meantime. Please reload.",
1649
+ validation: "The input was not accepted.",
1650
+ rateLimited: "Too many requests. Please wait a moment and try again.",
1651
+ server: "The server could not process the request.",
1652
+ httpStatus: "The request failed (HTTP {status}).",
1653
+ unexpected: "Something went wrong. Please try again."
1654
+ }
1655
+ );
1656
+
1219
1657
  // src/client/i18n/messages/marketing.ts
1220
1658
  var marketingMessages = defineMessages(
1221
1659
  {
@@ -1340,9 +1778,6 @@ var marketingMessages = defineMessages(
1340
1778
  colorLabel: "Farbe",
1341
1779
  delete: "Aktion l\xF6schen",
1342
1780
  deleteConfirm: "Aktion \u201E{label}\u201C wirklich l\xF6schen?"
1343
- },
1344
- errors: {
1345
- projectionsApi: "MarketingProjections-API antwortete mit HTTP {status}"
1346
1781
  }
1347
1782
  },
1348
1783
  {
@@ -1467,9 +1902,6 @@ var marketingMessages = defineMessages(
1467
1902
  colorLabel: "Color",
1468
1903
  delete: "Delete promotion",
1469
1904
  deleteConfirm: 'Really delete promotion "{label}"?'
1470
- },
1471
- errors: {
1472
- projectionsApi: "MarketingProjections API responded with HTTP {status}"
1473
1905
  }
1474
1906
  }
1475
1907
  );
@@ -2713,7 +3145,6 @@ var promosMessages = defineMessages(
2713
3145
  {
2714
3146
  title: "Promo-Codes",
2715
3147
  createAction: "Promo-Code anlegen",
2716
- apiErrorHttpStatus: "Promotions-API antwortete mit HTTP {status}",
2717
3148
  list: {
2718
3149
  searchLabel: "Suche (Code)",
2719
3150
  tileScheduled: "Geplant",
@@ -2811,7 +3242,6 @@ var promosMessages = defineMessages(
2811
3242
  {
2812
3243
  title: "Promo codes",
2813
3244
  createAction: "Create promo code",
2814
- apiErrorHttpStatus: "Promotions API responded with HTTP {status}",
2815
3245
  list: {
2816
3246
  searchLabel: "Search (code)",
2817
3247
  tileScheduled: "Scheduled",
@@ -2919,7 +3349,13 @@ var shellMessages = defineMessages(
2919
3349
  subtitle: "SuperAdmin \xB7 Plattform-Verwaltung",
2920
3350
  roleBadge: "SUPER ADMIN",
2921
3351
  logout: "Abmelden",
2922
- language: "Sprache"
3352
+ language: "Sprache",
3353
+ theme: {
3354
+ label: "Darstellung",
3355
+ light: "Hell",
3356
+ dark: "Dunkel",
3357
+ system: "System"
3358
+ }
2923
3359
  },
2924
3360
  drawer: {
2925
3361
  docs: "Doku \xF6ffnen"
@@ -2988,7 +3424,13 @@ var shellMessages = defineMessages(
2988
3424
  subtitle: "SuperAdmin \xB7 Platform administration",
2989
3425
  roleBadge: "SUPER ADMIN",
2990
3426
  logout: "Sign out",
2991
- language: "Language"
3427
+ language: "Language",
3428
+ theme: {
3429
+ label: "Appearance",
3430
+ light: "Light",
3431
+ dark: "Dark",
3432
+ system: "System"
3433
+ }
2992
3434
  },
2993
3435
  drawer: {
2994
3436
  docs: "Open documentation"
@@ -3232,6 +3674,7 @@ var usersMessages = defineMessages(
3232
3674
  function catalogFor(locale) {
3233
3675
  return {
3234
3676
  common: commonMessages[locale],
3677
+ errors: errorsMessages[locale],
3235
3678
  nav: navMessages[locale],
3236
3679
  shell: shellMessages[locale],
3237
3680
  dashboard: dashboardMessages[locale],
@@ -3293,13 +3736,13 @@ var SUPER_ADMIN_I18N_KEY = /* @__PURE__ */ Symbol.for(
3293
3736
  );
3294
3737
  function createSuperAdminI18n(options = {}) {
3295
3738
  const catalog = createSaCatalog(options);
3296
- const locale = (0, import_vue2.isRef)(options.locale) ? options.locale : createOwnedLocale(options, catalog);
3297
- const active = (0, import_vue2.computed)(
3739
+ const locale = (0, import_vue3.isRef)(options.locale) ? options.locale : createOwnedLocale(options, catalog);
3740
+ const active = (0, import_vue3.computed)(
3298
3741
  () => catalog.has(locale.value) ? locale.value : catalog.defaultLocale
3299
3742
  );
3300
- const messages = (0, import_vue2.computed)(() => catalog.messagesFor(active.value));
3301
- const intlLocale = (0, import_vue2.computed)(() => catalog.intlLocaleFor(active.value));
3302
- const writable = !(0, import_vue2.isReadonly)(locale);
3743
+ const messages = (0, import_vue3.computed)(() => catalog.messagesFor(active.value));
3744
+ const intlLocale = (0, import_vue3.computed)(() => catalog.intlLocaleFor(active.value));
3745
+ const writable = !(0, import_vue3.isReadonly)(locale);
3303
3746
  return {
3304
3747
  locale,
3305
3748
  messages,
@@ -3310,35 +3753,43 @@ function createSuperAdminI18n(options = {}) {
3310
3753
  }
3311
3754
  function createOwnedLocale(options, catalog) {
3312
3755
  const initial = options.locale ?? catalog.defaultLocale;
3313
- if (options.persist === false) return (0, import_vue2.ref)(initial);
3756
+ if (options.persist === false) return (0, import_vue3.ref)(initial);
3314
3757
  const storage = options.storage ?? defaultKvStore();
3315
3758
  const key = `${options.storageKeyPrefix ?? ""}${SA_LOCALE_STORAGE_KEY}`;
3316
3759
  const stored = storage.get(key);
3317
- const locale = (0, import_vue2.ref)(stored && catalog.has(stored) ? stored : initial);
3318
- (0, import_vue2.watch)(locale, (next) => storage.set(key, next));
3760
+ const locale = (0, import_vue3.ref)(stored && catalog.has(stored) ? stored : initial);
3761
+ (0, import_vue3.watch)(locale, (next) => storage.set(key, next));
3319
3762
  return locale;
3320
3763
  }
3321
3764
 
3322
3765
  // src/quasar/dark-bridge.ts
3323
3766
  var import_quasar = require("quasar");
3324
- var import_vue3 = require("vue");
3767
+ var import_vue4 = require("vue");
3325
3768
  function bindSaThemeToDocument(theme) {
3326
- const toQuasar = (0, import_vue3.watch)(
3769
+ let mirroring = false;
3770
+ const toQuasar = (0, import_vue4.watch)(
3327
3771
  theme.resolved,
3328
3772
  (resolved) => {
3329
3773
  if (typeof document !== "undefined") {
3330
3774
  document.documentElement.setAttribute("data-sa-theme", resolved);
3331
3775
  }
3332
- import_quasar.Dark.set(resolved === "dark");
3776
+ mirroring = true;
3777
+ try {
3778
+ import_quasar.Dark.set(resolved === "dark");
3779
+ } finally {
3780
+ mirroring = false;
3781
+ }
3333
3782
  },
3334
3783
  { immediate: true }
3335
3784
  );
3336
- const fromQuasar = (0, import_vue3.watch)(
3337
- () => import_quasar.Dark.isActive,
3338
- (isDark) => {
3339
- const asScheme = isDark ? "dark" : "light";
3340
- if (theme.resolved.value !== asScheme) theme.scheme.value = asScheme;
3341
- }
3785
+ const fromQuasar = (0, import_vue4.watch)(
3786
+ () => import_quasar.Dark.mode,
3787
+ (mode) => {
3788
+ if (mirroring) return;
3789
+ const picked = mode === "auto" ? "system" : mode === true ? "dark" : "light";
3790
+ if (theme.scheme.value !== picked) theme.scheme.value = picked;
3791
+ },
3792
+ { flush: "sync" }
3342
3793
  );
3343
3794
  return () => {
3344
3795
  toQuasar();
@@ -3346,8 +3797,18 @@ function bindSaThemeToDocument(theme) {
3346
3797
  };
3347
3798
  }
3348
3799
 
3800
+ // src/vue/platform-loaders.ts
3801
+ function resolveSuperAdminEndpoints(endpoints) {
3802
+ return {
3803
+ apiBase: endpoints.apiBase,
3804
+ publicBootEndpoint: endpoints.publicBootEndpoint ?? `${endpoints.apiBase}/boot`,
3805
+ manifestEndpoint: endpoints.manifestEndpoint ?? `${endpoints.apiBase}/manifest`,
3806
+ projectKey: endpoints.projectKey ?? ""
3807
+ };
3808
+ }
3809
+
3349
3810
  // src/quasar/notify.ts
3350
- var import_vue4 = require("vue");
3811
+ var import_vue5 = require("vue");
3351
3812
  var import_quasar2 = require("quasar");
3352
3813
  var quasarNotify = (kind, message, options) => {
3353
3814
  import_quasar2.Notify.create({
@@ -3359,21 +3820,54 @@ var quasarNotify = (kind, message, options) => {
3359
3820
  });
3360
3821
  };
3361
3822
  function useSuperAdminNotify() {
3362
- return (0, import_vue4.inject)(SUPER_ADMIN_NOTIFY_KEY, quasarNotify);
3823
+ return (0, import_vue5.inject)(SUPER_ADMIN_NOTIFY_KEY, quasarNotify);
3824
+ }
3825
+
3826
+ // src/quasar/confirm.ts
3827
+ var import_vue6 = require("vue");
3828
+ var import_quasar3 = require("quasar");
3829
+ function quasarConfirmOptions(request) {
3830
+ return {
3831
+ title: request.title,
3832
+ message: request.message,
3833
+ cancel: request.cancelLabel,
3834
+ ok: { label: request.confirmLabel, color: request.tone ?? "primary" },
3835
+ ...request.prompt ? {
3836
+ prompt: {
3837
+ model: request.prompt.initial ?? "",
3838
+ type: request.prompt.type ?? "text"
3839
+ }
3840
+ } : {}
3841
+ };
3842
+ }
3843
+ var quasarConfirm = (request) => new Promise((resolve) => {
3844
+ import_quasar3.Dialog.create(quasarConfirmOptions(request)).onOk((value) => {
3845
+ resolve({ ok: true, value: typeof value === "string" ? value : void 0 });
3846
+ }).onCancel(() => resolve({ ok: false }));
3847
+ });
3848
+ function useSuperAdminConfirm() {
3849
+ return (0, import_vue6.inject)(SUPER_ADMIN_CONFIRM_KEY, quasarConfirm);
3363
3850
  }
3364
3851
 
3365
3852
  // src/quasar/create-super-admin-app.ts
3366
3853
  var SA_PORTAL_CLASS = "sa-portal";
3367
3854
  var DEFAULT_QUASAR_OPTIONS = {
3368
- plugins: { Notify: import_quasar3.Notify, Dialog: import_quasar3.Dialog, Loading: import_quasar3.Loading },
3855
+ plugins: { Notify: import_quasar4.Notify, Dialog: import_quasar4.Dialog, Loading: import_quasar4.Loading },
3369
3856
  config: { notify: { position: "top-right", timeout: 3e3 } }
3370
3857
  };
3858
+ function resolveQuasarOptions(own) {
3859
+ if (!own) return DEFAULT_QUASAR_OPTIONS;
3860
+ return {
3861
+ ...own,
3862
+ plugins: { ...DEFAULT_QUASAR_OPTIONS.plugins, ...own.plugins }
3863
+ };
3864
+ }
3371
3865
  function createSuperAdminApp(options) {
3372
- const app = (0, import_vue5.createApp)(options.rootComponent);
3373
- const quasarOptions = options.quasarOptions ?? DEFAULT_QUASAR_OPTIONS;
3866
+ const app = (0, import_vue7.createApp)(options.rootComponent);
3867
+ const quasarOptions = resolveQuasarOptions(options.quasarOptions);
3374
3868
  const config = quasarOptions.config;
3375
3869
  const existingPortalClass = config?.globalNodes?.class;
3376
- app.use(import_quasar3.Quasar, {
3870
+ app.use(import_quasar4.Quasar, {
3377
3871
  ...quasarOptions,
3378
3872
  config: {
3379
3873
  ...quasarOptions.config,
@@ -3393,11 +3887,8 @@ function createSuperAdminApp(options) {
3393
3887
  router.beforeEach(navGuard);
3394
3888
  }
3395
3889
  app.use(router);
3396
- const endpoints = {
3397
- apiBase: options.endpoints.apiBase,
3398
- publicBootEndpoint: options.endpoints.publicBootEndpoint ?? `${options.endpoints.apiBase}/boot`,
3399
- manifestEndpoint: options.endpoints.manifestEndpoint ?? `${options.endpoints.apiBase}/manifest`
3400
- };
3890
+ const endpoints = resolveSuperAdminEndpoints(options.endpoints);
3891
+ const http = options.http ?? defaultHttpClient();
3401
3892
  const i18n = createSuperAdminI18n(options.i18n);
3402
3893
  const configuredDark = options.quasarOptions?.config?.dark;
3403
3894
  const theme = createSaTheme({
@@ -3412,6 +3903,23 @@ function createSuperAdminApp(options) {
3412
3903
  app.provide(SUPER_ADMIN_EXTENSIONS_KEY, options.extensions ?? {});
3413
3904
  app.provide(SUPER_ADMIN_ACTIONS_KEY, options.actions ?? {});
3414
3905
  app.provide(SUPER_ADMIN_NOTIFY_KEY, options.notify ?? quasarNotify);
3906
+ app.provide(SUPER_ADMIN_CONFIRM_KEY, options.confirm ?? quasarConfirm);
3907
+ if (options.http) {
3908
+ app.provide(
3909
+ SUPER_ADMIN_RESOURCES_KEY,
3910
+ createResourceRegistry({
3911
+ http: options.http,
3912
+ // Read per call: the locale changes while the app runs.
3913
+ context: () => ({
3914
+ apiBase: endpoints.apiBase,
3915
+ projectKey: endpoints.projectKey,
3916
+ locale: i18n.locale.value
3917
+ }),
3918
+ resources: platformResources,
3919
+ overrides: options.resourceOverrides
3920
+ })
3921
+ );
3922
+ }
3415
3923
  if (options.manifestGuard?.getManifest) {
3416
3924
  app.provide(SUPER_ADMIN_MANIFEST_KEY, options.manifestGuard.getManifest);
3417
3925
  }
@@ -3421,7 +3929,7 @@ function createSuperAdminApp(options) {
3421
3929
  if (options.loginAdapter) {
3422
3930
  app.provide(SUPER_ADMIN_LOGIN_ADAPTER_KEY, options.loginAdapter);
3423
3931
  }
3424
- app.provide(SUPER_ADMIN_HTTP_KEY, options.http ?? defaultHttpClient());
3932
+ app.provide(SUPER_ADMIN_HTTP_KEY, http);
3425
3933
  for (const plugin of options.installPlugins ?? []) {
3426
3934
  plugin(app);
3427
3935
  }
@@ -3443,6 +3951,10 @@ function createSuperAdminApp(options) {
3443
3951
  SA_PORTAL_CLASS,
3444
3952
  bindSaThemeToDocument,
3445
3953
  createSuperAdminApp,
3954
+ quasarConfirm,
3955
+ quasarConfirmOptions,
3446
3956
  quasarNotify,
3957
+ resolveQuasarOptions,
3958
+ useSuperAdminConfirm,
3447
3959
  useSuperAdminNotify
3448
3960
  });