@saasicat/ui-vue 0.25.0 → 0.26.1

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