@saasicat/ui-vue 0.25.0 → 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 (84) 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/bundle-editor/BundleCreatePanel.vue +1 -1
  50. package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
  51. package/src/components/plan/PlanCycleToggle.vue +1 -1
  52. package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
  53. package/src/index.ts +5 -0
  54. package/src/pages-standard/AdminLayout.vue +57 -0
  55. package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
  56. package/src/pages-standard/DiscoveryPage.vue +8 -1
  57. package/src/pages-standard/MarketingCatalogPage.vue +14 -3
  58. package/src/pages-standard/SuperAdminLoginPage.vue +23 -2
  59. package/src/pages-standard/SuperAdminSetupWizard.vue +37 -3
  60. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +7 -2
  61. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
  62. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
  63. package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
  64. package/src/quasar/confirm.ts +83 -0
  65. package/src/quasar/create-super-admin-app.ts +84 -10
  66. package/src/quasar/dark-bridge.ts +47 -10
  67. package/src/quasar/index.ts +1 -0
  68. package/src/vue/create-admin-routes.ts +9 -1
  69. package/src/vue/platform-loaders.ts +12 -2
  70. package/src/vue/resource-registry.ts +211 -0
  71. package/src/vue/super-admin-context.ts +20 -5
  72. package/src/vue/ui-confirm.ts +76 -0
  73. package/src/vue/use-api-list.ts +50 -45
  74. package/src/vue/use-async-action.ts +170 -0
  75. package/src/vue/use-async-data.ts +83 -0
  76. package/src/vue/use-bundles.ts +34 -7
  77. package/src/vue/use-catalog-entries.ts +37 -12
  78. package/src/vue/use-discovery.ts +27 -6
  79. package/src/vue/use-marketing-projections.ts +29 -7
  80. package/src/vue/use-plans.ts +36 -8
  81. package/src/vue/use-promotions.ts +27 -7
  82. package/src/vue/use-resource-list.ts +272 -0
  83. package/src/vue/use-sa-theme.ts +34 -5
  84. package/src/vue/use-tenant-subscription-bundles.ts +22 -3
package/README.md CHANGED
@@ -15,12 +15,12 @@ The package is layered so that each entry only loads what it names.
15
15
  Lower layers never import upward; ESLint (`no-restricted-imports`, repo
16
16
  root config) enforces the boundaries in CI.
17
17
 
18
- | Entry | Source | May import | Contents |
19
- | ----------------------------------------------------------------------- | -------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
- | `@saasicat/ui-vue/client` | `src/client/` | `@saasicat/types` only | Framework-free core: `BootLoader`, `ManifestLoader` (ETag cache), nav builder, action registry, batch column fetcher, `HttpClient`/`KvStore` contract. Usable from any framework or plain Node/TypeScript. |
21
- | `@saasicat/ui-vue` (main) | `src/vue/` (+ client re-exports) | Vue, `vue-router`, Pinia — **no Quasar** | Composables (`useTenants`, `usePlanEditor`, …), router guards, injection keys + shell contract, notify-port type, optional Pinia store factory. |
22
- | `@saasicat/ui-vue/quasar` | `src/quasar/` | everything above + Quasar | `createSuperAdminApp()` bootstrap and the Quasar notify-port implementation. |
23
- | `@saasicat/ui-vue/pages-standard/*`, `/pages-tenant/*`, `/components/*` | SFC directories | everything | The Quasar reference UI, shipped as raw `.vue` from `src/` (compiled by the consumer's Vite). |
18
+ | Entry | Source | May import | Contents |
19
+ | ----------------------------------------------------------------------- | -------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20
+ | `@saasicat/ui-vue/client` | `src/client/` | `@saasicat/types` only | Framework-free core: `BootLoader`, `ManifestLoader` (ETag cache), nav builder, action registry, batch column fetcher, the `HttpClient`/`KvStore` contract with its `fetch` and axios adapters, `AdminError`. Usable from any framework or plain Node/TypeScript. |
21
+ | `@saasicat/ui-vue` (main) | `src/vue/` (+ client re-exports) | Vue, `vue-router`, Pinia — **no Quasar** | Composables (`useTenants`, `usePlanEditor`, …), router guards, injection keys + shell contract, notify-port type, optional Pinia store factory. |
22
+ | `@saasicat/ui-vue/quasar` | `src/quasar/` | everything above + Quasar | `createSuperAdminApp()` bootstrap and the Quasar notify-port implementation. |
23
+ | `@saasicat/ui-vue/pages-standard/*`, `/pages-tenant/*`, `/components/*` | SFC directories | everything | The Quasar reference UI, shipped as raw `.vue` from `src/` (compiled by the consumer's Vite). |
24
24
 
25
25
  Rules of thumb when contributing:
26
26
 
@@ -48,8 +48,32 @@ createSuperAdminApp({
48
48
  });
49
49
  ```
50
50
 
51
- Confirm dialogs inside the reference pages intentionally keep using Quasar's
52
- `Dialog` — the pages _are_ the Quasar layer.
51
+ ### Confirm port
52
+
53
+ A second seam, `UiConfirm` (`SUPER_ADMIN_CONFIRM_KEY`), works the same way:
54
+ `createSuperAdminApp()` provides a Quasar `Dialog` default, and an app with its
55
+ own modal system replaces it.
56
+
57
+ It is available to your own pages today. The standard pages do **not** use it
58
+ yet — `UsersPage`, `PilotsPage`, `PromoCodesPage` and `PlatformEmailPage` still
59
+ call `q.dialog()` directly, so replacing the port does not change what they show.
60
+ Moving them onto it belongs to the page migration.
61
+
62
+ ```ts
63
+ createSuperAdminApp({
64
+ // ...
65
+ confirm: (request) => myModals.ask(request),
66
+ });
67
+ ```
68
+
69
+ A request may ask for a value as well as a yes — an audit reason before a
70
+ password reset, the date a pilot is extended to — so the port resolves
71
+ `{ ok, value? }` rather than a bare boolean. An implementation must actually
72
+ ask: one that resolves `{ ok: true }` outright turns every guarded action into
73
+ an unguarded one.
74
+
75
+ Dialogs that collect a form, and the one that displays a generated one-time
76
+ password, are not confirmations and stay as they are.
53
77
 
54
78
  ## Usage
55
79
 
@@ -1,4 +1,5 @@
1
- import { StandardPageKey, AdminManifest, TenantActionDef } from '@saasicat/types';
1
+ import * as _saasicat_types from '@saasicat/types';
2
+ import { AuditQuery, AuditEntry, TenantListFilter, TenantDto, PlanVersionRow, CreatePlanVersionDraftData, PlanVersionMutationResult, UpdatePlanVersionDraftData, PlanRow, CreatePlanData, UpdatePlanData, StandardPageKey, AdminManifest, TenantActionDef } from '@saasicat/types';
2
3
 
3
4
  /**
4
5
  * Minimal abstraction over `fetch`. Consumers may pass their own
@@ -33,11 +34,280 @@ interface KvStore {
33
34
  */
34
35
  declare function defaultKvStore(): KvStore;
35
36
  /**
36
- * Default `HttpClient` over `fetch`. Consumers pass their own variant
37
- * through when they need auth headers / tenant headers / retry logic.
37
+ * Default `HttpClient` over `fetch`, with no base URL and no headers of its
38
+ * own. Consumers pass their own variant when they need auth headers, tenant
39
+ * headers or retry logic — `createFetchHttpClient({ headers })` and
40
+ * `createAxiosHttpClient(instance)` are the two the package ships.
41
+ *
42
+ * It delegates rather than calling `fetch` itself so that there is one
43
+ * implementation to reason about instead of two that drift.
38
44
  */
39
45
  declare function defaultHttpClient(): HttpClient;
40
46
 
47
+ /**
48
+ * What an operation needs to know about the app it runs in, beyond its own
49
+ * arguments.
50
+ *
51
+ * Today each composable takes these as its own options and each consumer
52
+ * passes them again per page. They are app-wide constants — which is why they
53
+ * belong in one context rather than in every signature.
54
+ */
55
+ interface ResourceContext {
56
+ /** Admin API root, e.g. `/api/v1/admin`. No trailing slash. */
57
+ readonly apiBase: string;
58
+ /** The project whose catalogue is being administered. */
59
+ readonly projectKey: string;
60
+ /** Active UI locale, for the operations whose payload is per-language. */
61
+ readonly locale: string;
62
+ }
63
+ /** One operation: everything it needs, in the order it needs it. */
64
+ type ResourceOp<A extends unknown[], R> = (http: HttpClient, ctx: ResourceContext, ...args: A) => Promise<R>;
65
+ /** The operations of one resource, keyed by name. */
66
+ type ResourceOps = Record<string, ResourceOp<never[], unknown>>;
67
+ interface ResourceDef<TOps extends ResourceOps> {
68
+ /** Name this resource is registered and overridden under. */
69
+ readonly key: string;
70
+ readonly ops: TOps;
71
+ /**
72
+ * Whether these operations read `ctx.projectKey`.
73
+ *
74
+ * Declared per descriptor rather than known centrally, because it is not a
75
+ * property of the registry: one of the four platform resources is
76
+ * project-scoped and three are not, and the tenant list would be refused
77
+ * for a project it never asks about. `bindResource` refuses to bind a
78
+ * descriptor that declares this against a context that names no project.
79
+ *
80
+ * `resources-declare-their-project-scope.test.js` drives every operation
81
+ * and fails when a declaration and what the operations actually read
82
+ * disagree — in either direction.
83
+ */
84
+ readonly projectScoped: boolean;
85
+ }
86
+ /** What a descriptor declares beyond its name and its operations. */
87
+ interface DefineResourceOptions {
88
+ /** See `ResourceDef.projectScoped`. Default `false`. */
89
+ projectScoped?: boolean;
90
+ }
91
+ /**
92
+ * The same operations with `(http, ctx)` already supplied — the form a page or
93
+ * a composable calls.
94
+ */
95
+ type Bound<TOps extends ResourceOps> = {
96
+ [K in keyof TOps]: TOps[K] extends ResourceOp<infer A, infer R> ? (...args: A) => Promise<R> : never;
97
+ };
98
+ /**
99
+ * Declares a resource.
100
+ *
101
+ * Deliberately thin — it exists to name the set and to fix the type, not to
102
+ * add behaviour. Anything it did at definition time would be a thing an
103
+ * override could not replace.
104
+ */
105
+ declare function defineResource<const TOps extends ResourceOps>(key: string, ops: TOps, options?: DefineResourceOptions): ResourceDef<TOps>;
106
+ /**
107
+ * Supplies `(http, ctx)` once and hands back the callable operations.
108
+ *
109
+ * The context is read at call time rather than captured per operation, so a
110
+ * context that changes — a locale the operator switches, a project the shell
111
+ * re-scopes — is picked up by calls made afterwards without rebinding.
112
+ */
113
+ declare function bindResource<TOps extends ResourceOps>(def: ResourceDef<TOps>, http: HttpClient, ctx: ResourceContext | (() => ResourceContext)): Bound<TOps>;
114
+
115
+ /** 1-based: the page a list starts on, and the one a changed filter returns to. */
116
+ declare const LIST_FIRST_PAGE = 1;
117
+ /**
118
+ * What the admin API falls back to when `pageSize` is absent. Sent explicitly
119
+ * rather than relied on, so the URL states what it asked for.
120
+ */
121
+ declare const LIST_PAGE_SIZE_DEFAULT = 50;
122
+ /**
123
+ * The largest page the admin API serves. There is no "all" to pass on, which is
124
+ * why `SERVER_PAGE_SIZE_OPTIONS` leaves Quasar's `ALL_ROWS` out as well.
125
+ */
126
+ declare const LIST_PAGE_SIZE_MAX = 200;
127
+ /** The two parameters the pagination owns; a filter may not carry them. */
128
+ declare const LIST_PAGINATION_PARAMS: readonly ["page", "pageSize"];
129
+ /**
130
+ * A filter without the two keys the pagination owns.
131
+ *
132
+ * The platform's filter types (`TenantListFilter`, `AuditQuery`) declare
133
+ * `page` and `pageSize` because they were once passed as one object. Serialised
134
+ * after the pagination they silently overwrite it — `goToPage(3)` moves the ref
135
+ * while the request asks for page 7 — so a descriptor takes the filter without
136
+ * them and the page number travels in its own field.
137
+ */
138
+ type ListFilterOf<TFilter> = Omit<TFilter, 'page' | 'pageSize'>;
139
+ /** Everything one list request varies: which page, how big, filtered by what. */
140
+ interface ListQuery<TFilter extends object = Record<string, unknown>> {
141
+ /** 1-based. Defaults to `LIST_FIRST_PAGE`. */
142
+ page?: number;
143
+ /** Defaults to `LIST_PAGE_SIZE_DEFAULT`. */
144
+ pageSize?: number;
145
+ /** Serialised as `?key=value`; see `isSentInQuery` for what is left out. */
146
+ filter?: TFilter;
147
+ }
148
+ /**
149
+ * One page of a list, as the admin API answers it.
150
+ *
151
+ * `total` is optional because it is genuinely absent from some answers: a
152
+ * controller returning a bare array has reported the rows it sent and nothing
153
+ * about the rest, and an envelope may omit the field altogether. A number
154
+ * invented here would read as "rows matching the filter" under a paginator that
155
+ * then offers pages nobody can reach.
156
+ */
157
+ interface ResourceListPage<T> {
158
+ items: T[];
159
+ /** Rows matching the filter — only when the server said so. */
160
+ total?: number;
161
+ /** What the server echoed back about the page it served, when it echoed anything. */
162
+ page?: number;
163
+ pageSize?: number;
164
+ }
165
+ /** A list operation: the query in, one page out. */
166
+ type ListOp<TRow, TFilter extends object = Record<string, unknown>> = ResourceOp<[
167
+ query?: ListQuery<TFilter>
168
+ ], ResourceListPage<TRow>>;
169
+ /**
170
+ * Whether a filter value reaches the query string at all.
171
+ *
172
+ * `undefined`, `null` and `''` are the three spellings of "not filtered" that
173
+ * a form produces, and sending them would filter on the literal text `null`.
174
+ * Everything else goes as `String(value)` — including `0` and `false`, which
175
+ * are answers, not absences.
176
+ */
177
+ declare function isSentInQuery(value: unknown): boolean;
178
+ /**
179
+ * `?a=b` for a filter on its own, or `''` when nothing survives — the form the
180
+ * admin endpoints that do not page take their filter in.
181
+ */
182
+ declare function filterQueryString(filter: object): string;
183
+ /**
184
+ * The URL one page of a list is requested from.
185
+ *
186
+ * `page` and `pageSize` are always present and always first — an endpoint that
187
+ * ignores them is free to, but the request says which page it meant. The filter
188
+ * follows in its own insertion order, and the endpoint may already carry a
189
+ * query of its own.
190
+ */
191
+ declare function listUrl<TFilter extends object>(endpoint: string, query?: ListQuery<TFilter>): string;
192
+ /**
193
+ * Reads whatever a list endpoint answered into one page.
194
+ *
195
+ * Both shapes are real: some controllers return a bare array, others the
196
+ * `{ items, page, pageSize, total }` envelope. Accepting only the envelope is
197
+ * what once rendered three tenants as an empty table (see
198
+ * `use-api-list-shape.test.js`). What the answer did not state stays absent
199
+ * rather than being filled in — the caller decides what "no total" means for
200
+ * its paginator.
201
+ */
202
+ declare function readListPage<T>(raw: unknown): ResourceListPage<T>;
203
+ /** Keeps a requested page on the 1-based scale the API counts on. */
204
+ declare function clampListPage(page: number): number;
205
+ /** Keeps a requested page size inside the 1..`LIST_PAGE_SIZE_MAX` the API serves. */
206
+ declare function clampListPageSize(size: number): number;
207
+ /**
208
+ * A list operation over one endpoint: the second of a pair, and the reason it
209
+ * is a function rather than two hand-written descriptors.
210
+ *
211
+ * The endpoint is a function of the context so a `context` override on the
212
+ * registry — an app pointing one resource at a legacy base — reaches it without
213
+ * the descriptor knowing.
214
+ */
215
+ declare function defineListOp<TRow, TFilter extends object = Record<string, unknown>>(endpoint: (ctx: ResourceContext) => string): ListOp<TRow, TFilter>;
216
+
217
+ /** What the audit list can be narrowed by. The page number is not a filter. */
218
+ type AuditListFilter = ListFilterOf<AuditQuery>;
219
+ declare const auditResource: ResourceDef<{
220
+ readonly list: ListOp<AuditEntry, AuditListFilter>;
221
+ }>;
222
+
223
+ /** What the tenants list can be narrowed by. The page number is not a filter. */
224
+ type TenantsListFilter = ListFilterOf<TenantListFilter>;
225
+ declare const tenantsResource: ResourceDef<{
226
+ readonly list: ListOp<TenantDto, TenantsListFilter>;
227
+ }>;
228
+
229
+ /** What `publish` may override at the moment a version goes live. */
230
+ interface PublishPlanVersionOptions {
231
+ forceRegressive?: boolean;
232
+ allowZeroPrice?: boolean;
233
+ validFrom?: string | null;
234
+ validUntil?: string | null;
235
+ }
236
+ /**
237
+ * The plan catalogue — the one platform resource that is read per project.
238
+ *
239
+ * `list`, `tenantCounts` and `create` all carry `ctx.projectKey`, so binding
240
+ * this against a context that names no project is refused rather than sent (see
241
+ * `bindResource`). Its three siblings address a plan or a version by id and are
242
+ * not declared here.
243
+ */
244
+ declare const plansResource: ResourceDef<{
245
+ readonly list: (http: HttpClient, ctx: ResourceContext) => Promise<PlanRow[]>;
246
+ readonly tenantCounts: (http: HttpClient, ctx: ResourceContext) => Promise<Record<string, number>>;
247
+ /**
248
+ * The project comes from the context, not from the caller.
249
+ *
250
+ * The endpoint takes it in the body, and the composable this replaces
251
+ * made that the caller's job — so a page had to keep hold of the
252
+ * project it was administering just to create in it, and `list()` and
253
+ * `create()` read two different sources for one value. A stale one
254
+ * creates a plan outside the project the list is showing.
255
+ */
256
+ readonly create: (http: HttpClient, ctx: ResourceContext, data: Omit<CreatePlanData, "projectKey">) => Promise<PlanRow>;
257
+ readonly update: (http: HttpClient, ctx: ResourceContext, planId: string, data: UpdatePlanData) => Promise<PlanRow>;
258
+ /** Marks the plan deleted. Fails while it still has versions. */
259
+ readonly softDelete: (http: HttpClient, ctx: ResourceContext, planId: string) => Promise<void>;
260
+ /** Removes the plan for good. 422 `PLAN_HAS_VERSIONS` when it cannot. */
261
+ readonly hardDelete: (http: HttpClient, ctx: ResourceContext, planId: string) => Promise<void>;
262
+ }>;
263
+ declare const planVersionsResource: ResourceDef<{
264
+ readonly listForPlan: (http: HttpClient, ctx: ResourceContext, planId: string) => Promise<PlanVersionRow[]>;
265
+ readonly createDraft: (http: HttpClient, ctx: ResourceContext, planId: string, data: Omit<CreatePlanVersionDraftData, "planId">) => Promise<PlanVersionMutationResult>;
266
+ readonly updateDraft: (http: HttpClient, ctx: ResourceContext, versionId: string, data: UpdatePlanVersionDraftData) => Promise<PlanVersionMutationResult>;
267
+ readonly publish: (http: HttpClient, ctx: ResourceContext, versionId: string, options?: PublishPlanVersionOptions) => Promise<PlanVersionMutationResult>;
268
+ readonly discardDraft: (http: HttpClient, ctx: ResourceContext, versionId: string) => Promise<void>;
269
+ /**
270
+ * Ends a live version on `endsAt`, with no successor. Idempotent — a
271
+ * second call with another date overwrites the first.
272
+ *
273
+ * `endsAt` is the date itself, not a payload: the endpoint's body is
274
+ * `{ endsAt }` and wrapping it is this operation's job, not the caller's.
275
+ */
276
+ readonly terminate: (http: HttpClient, ctx: ResourceContext, versionId: string, endsAt: string) => Promise<PlanVersionRow>;
277
+ }>;
278
+
279
+ /**
280
+ * Every resource the shell offers by default.
281
+ *
282
+ * The registry binds these; an app overrides by key. Partial while the roster
283
+ * grows — a page whose resource is not here yet keeps its props.
284
+ */
285
+ declare const platformResources: {
286
+ plans: ResourceDef<{
287
+ readonly list: (http: HttpClient, ctx: ResourceContext) => Promise<_saasicat_types.PlanRow[]>;
288
+ readonly tenantCounts: (http: HttpClient, ctx: ResourceContext) => Promise<Record<string, number>>;
289
+ readonly create: (http: HttpClient, ctx: ResourceContext, data: Omit<_saasicat_types.CreatePlanData, "projectKey">) => Promise<_saasicat_types.PlanRow>;
290
+ readonly update: (http: HttpClient, ctx: ResourceContext, planId: string, data: _saasicat_types.UpdatePlanData) => Promise<_saasicat_types.PlanRow>;
291
+ readonly softDelete: (http: HttpClient, ctx: ResourceContext, planId: string) => Promise<void>;
292
+ readonly hardDelete: (http: HttpClient, ctx: ResourceContext, planId: string) => Promise<void>;
293
+ }>;
294
+ planVersions: ResourceDef<{
295
+ readonly listForPlan: (http: HttpClient, ctx: ResourceContext, planId: string) => Promise<_saasicat_types.PlanVersionRow[]>;
296
+ readonly createDraft: (http: HttpClient, ctx: ResourceContext, planId: string, data: Omit<_saasicat_types.CreatePlanVersionDraftData, "planId">) => Promise<_saasicat_types.PlanVersionMutationResult>;
297
+ readonly updateDraft: (http: HttpClient, ctx: ResourceContext, versionId: string, data: _saasicat_types.UpdatePlanVersionDraftData) => Promise<_saasicat_types.PlanVersionMutationResult>;
298
+ readonly publish: (http: HttpClient, ctx: ResourceContext, versionId: string, options?: PublishPlanVersionOptions) => Promise<_saasicat_types.PlanVersionMutationResult>;
299
+ readonly discardDraft: (http: HttpClient, ctx: ResourceContext, versionId: string) => Promise<void>;
300
+ readonly terminate: (http: HttpClient, ctx: ResourceContext, versionId: string, endsAt: string) => Promise<_saasicat_types.PlanVersionRow>;
301
+ }>;
302
+ tenants: ResourceDef<{
303
+ readonly list: ListOp<_saasicat_types.TenantDto, TenantsListFilter>;
304
+ }>;
305
+ audit: ResourceDef<{
306
+ readonly list: ListOp<_saasicat_types.AuditEntry, AuditListFilter>;
307
+ }>;
308
+ };
309
+ type PlatformResources = typeof platformResources;
310
+
41
311
  /** Locales the platform itself ships a catalog for. */
42
312
  type SaBuiltinLocale = 'de' | 'en';
43
313
  /**
@@ -60,64 +330,6 @@ declare const SA_LOCALE_LABELS: Record<SaBuiltinLocale, string>;
60
330
  /** Narrows untrusted input to a locale the platform ships a catalog for. */
61
331
  declare function isSaBuiltinLocale(value: unknown): value is SaBuiltinLocale;
62
332
 
63
- type SaNavMessages = {
64
- readonly pages: Record<StandardPageKey, string>;
65
- readonly sections: {
66
- readonly overview: string;
67
- readonly catalog: string;
68
- readonly customers: string;
69
- readonly system: string;
70
- };
71
- };
72
- declare const navMessages: Record<SaBuiltinLocale, SaNavMessages>;
73
-
74
- /** Consumer implementation; receives the action inputs as a generic object. */
75
- type ActionHandler<TInput = unknown, TResult = unknown> = (input: TInput) => Promise<TResult>;
76
- interface ResolvedAction<TInput = unknown, TResult = unknown> {
77
- def: TenantActionDef;
78
- handler: ActionHandler<TInput, TResult>;
79
- }
80
- declare class MissingHandlerError extends Error {
81
- constructor(actionKey: string);
82
- }
83
- declare class ActionDefNotInManifestError extends Error {
84
- constructor(actionKey: string);
85
- }
86
- declare class ActionRegistry {
87
- private readonly defs;
88
- private readonly handlers;
89
- constructor(manifest: AdminManifest, handlers?: Record<string, ActionHandler>);
90
- /**
91
- * Registers a handler after the fact (e.g. when consumer code loads
92
- * lazily). Throws `ActionDefNotInManifestError` if the `actionKey` is not
93
- * declared in the manifest — prevents dead registrations.
94
- */
95
- register<TInput, TResult>(actionKey: string, handler: ActionHandler<TInput, TResult>): void;
96
- /**
97
- * Returns the `{def, handler}` pair. Throws if the key is missing from
98
- * the manifest or no handler is registered. The shell's UI layer calls
99
- * the method, checks `def.requiresMfa` / `def.confirmType` for the
100
- * pre-flow, and then calls `handler(input)`.
101
- */
102
- get<TInput = unknown, TResult = unknown>(actionKey: string): ResolvedAction<TInput, TResult>;
103
- /**
104
- * Convenience: `get(key).handler(input)`. UI convenience for actions
105
- * that need neither MFA nor confirm.
106
- */
107
- dispatch<TInput, TResult>(actionKey: string, input: TInput): Promise<TResult>;
108
- /**
109
- * List of actionKeys that are declared in the manifest but have no
110
- * handler. Consumers use this in a doctor check to detect drift between
111
- * the manifest and the shell build.
112
- */
113
- listOrphanedDefs(): string[];
114
- /**
115
- * List of registered handlers that are missing from the manifest. Drift
116
- * in the other direction.
117
- */
118
- listOrphanedHandlers(): string[];
119
- }
120
-
121
333
  /** Nested string map — the shape of every message namespace. */
122
334
  interface MessageTree {
123
335
  readonly [key: string]: string | MessageTree;
@@ -478,9 +690,6 @@ declare const discoveryMessages: Record<SaBuiltinLocale, {
478
690
  replacedBy: string;
479
691
  replaces: string;
480
692
  reapproveEmphasis: string;
481
- errorDiscoveryHttp: string;
482
- errorRescanHttp: string;
483
- errorCatalogHttp: string;
484
693
  capList: {
485
694
  orphanFeature: string;
486
695
  removedFromCode: string;
@@ -622,6 +831,20 @@ declare const emailMessages: Record<SaBuiltinLocale, {
622
831
  };
623
832
  }>;
624
833
 
834
+ declare const errorsMessages: Record<SaBuiltinLocale, {
835
+ network: string;
836
+ emptyResponse: string;
837
+ unauthorized: string;
838
+ forbidden: string;
839
+ notFound: string;
840
+ conflict: string;
841
+ validation: string;
842
+ rateLimited: string;
843
+ server: string;
844
+ httpStatus: string;
845
+ unexpected: string;
846
+ }>;
847
+
625
848
  declare const marketingMessages: Record<SaBuiltinLocale, {
626
849
  topFeatures: string;
627
850
  tabs: {
@@ -745,11 +968,19 @@ declare const marketingMessages: Record<SaBuiltinLocale, {
745
968
  delete: string;
746
969
  deleteConfirm: string;
747
970
  };
748
- errors: {
749
- projectionsApi: string;
750
- };
751
971
  }>;
752
972
 
973
+ type SaNavMessages = {
974
+ readonly pages: Record<StandardPageKey, string>;
975
+ readonly sections: {
976
+ readonly overview: string;
977
+ readonly catalog: string;
978
+ readonly customers: string;
979
+ readonly system: string;
980
+ };
981
+ };
982
+ declare const navMessages: Record<SaBuiltinLocale, SaNavMessages>;
983
+
753
984
  declare const pilotsMessages: Record<SaBuiltinLocale, {
754
985
  title: string;
755
986
  subtitleCount: string;
@@ -1334,7 +1565,6 @@ declare const plansMessages: Record<SaBuiltinLocale, {
1334
1565
  declare const promosMessages: Record<SaBuiltinLocale, {
1335
1566
  title: string;
1336
1567
  createAction: string;
1337
- apiErrorHttpStatus: string;
1338
1568
  list: {
1339
1569
  searchLabel: string;
1340
1570
  tileScheduled: string;
@@ -1440,6 +1670,12 @@ declare const shellMessages: Record<SaBuiltinLocale, {
1440
1670
  roleBadge: string;
1441
1671
  logout: string;
1442
1672
  language: string;
1673
+ theme: {
1674
+ label: string;
1675
+ light: string;
1676
+ dark: string;
1677
+ system: string;
1678
+ };
1443
1679
  };
1444
1680
  drawer: {
1445
1681
  docs: string;
@@ -1589,6 +1825,7 @@ declare const usersMessages: Record<SaBuiltinLocale, {
1589
1825
  /** Complete platform catalog for one locale, keyed by namespace. */
1590
1826
  type SaMessages = {
1591
1827
  readonly common: (typeof commonMessages)['de'];
1828
+ readonly errors: (typeof errorsMessages)['de'];
1592
1829
  readonly nav: (typeof navMessages)['de'];
1593
1830
  readonly shell: (typeof shellMessages)['de'];
1594
1831
  readonly dashboard: (typeof dashboardMessages)['de'];
@@ -1616,6 +1853,53 @@ declare const SA_MESSAGES: Record<SaBuiltinLocale, SaMessages>;
1616
1853
  */
1617
1854
  declare function resolveMessages(locale: SaBuiltinLocale, overrides?: SaMessagesOverrides): SaMessages;
1618
1855
 
1856
+ /** Consumer implementation; receives the action inputs as a generic object. */
1857
+ type ActionHandler<TInput = unknown, TResult = unknown> = (input: TInput) => Promise<TResult>;
1858
+ interface ResolvedAction<TInput = unknown, TResult = unknown> {
1859
+ def: TenantActionDef;
1860
+ handler: ActionHandler<TInput, TResult>;
1861
+ }
1862
+ declare class MissingHandlerError extends Error {
1863
+ constructor(actionKey: string);
1864
+ }
1865
+ declare class ActionDefNotInManifestError extends Error {
1866
+ constructor(actionKey: string);
1867
+ }
1868
+ declare class ActionRegistry {
1869
+ private readonly defs;
1870
+ private readonly handlers;
1871
+ constructor(manifest: AdminManifest, handlers?: Record<string, ActionHandler>);
1872
+ /**
1873
+ * Registers a handler after the fact (e.g. when consumer code loads
1874
+ * lazily). Throws `ActionDefNotInManifestError` if the `actionKey` is not
1875
+ * declared in the manifest — prevents dead registrations.
1876
+ */
1877
+ register<TInput, TResult>(actionKey: string, handler: ActionHandler<TInput, TResult>): void;
1878
+ /**
1879
+ * Returns the `{def, handler}` pair. Throws if the key is missing from
1880
+ * the manifest or no handler is registered. The shell's UI layer calls
1881
+ * the method, checks `def.requiresMfa` / `def.confirmType` for the
1882
+ * pre-flow, and then calls `handler(input)`.
1883
+ */
1884
+ get<TInput = unknown, TResult = unknown>(actionKey: string): ResolvedAction<TInput, TResult>;
1885
+ /**
1886
+ * Convenience: `get(key).handler(input)`. UI convenience for actions
1887
+ * that need neither MFA nor confirm.
1888
+ */
1889
+ dispatch<TInput, TResult>(actionKey: string, input: TInput): Promise<TResult>;
1890
+ /**
1891
+ * List of actionKeys that are declared in the manifest but have no
1892
+ * handler. Consumers use this in a doctor check to detect drift between
1893
+ * the manifest and the shell build.
1894
+ */
1895
+ listOrphanedDefs(): string[];
1896
+ /**
1897
+ * List of registered handlers that are missing from the manifest. Drift
1898
+ * in the other direction.
1899
+ */
1900
+ listOrphanedHandlers(): string[];
1901
+ }
1902
+
1619
1903
  /** A language an app brings along, on top of the two the platform ships. */
1620
1904
  interface SaLocaleDefinition {
1621
1905
  /**
@@ -1672,4 +1956,4 @@ interface SaCatalog {
1672
1956
  */
1673
1957
  declare function createSaCatalog(options?: SaCatalogOptions): SaCatalog;
1674
1958
 
1675
- 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 SaLocaleOption as b, type SaLocaleDefinition as c, type SaMessagesOverrides as d, ActionRegistry as e, ActionDefNotInManifestError as f, type HttpResponse as g, MissingHandlerError as h, SA_INTL_LOCALES as i, SA_LOCALES as j, SA_LOCALE_LABELS as k, SA_MESSAGES as l, type SaBuiltinLocale as m, type SaCatalog as n, type SaCatalogOptions as o, createSaCatalog as p, defaultHttpClient as q, defaultKvStore as r, defineMessages as s, isSaBuiltinLocale as t, mergeMessages as u, resolveMessages as v, type SaNavMessages as w };
1959
+ export { filterQueryString as $, type ActionHandler as A, type Bound as B, SA_LOCALE_LABELS as C, DEFAULT_SA_LOCALE as D, SA_MESSAGES as E, type SaBuiltinLocale as F, type SaCatalog as G, type HttpClient as H, type SaCatalogOptions as I, type TranslationOf as J, type KvStore as K, LIST_FIRST_PAGE as L, type MessageTree as M, auditResource as N, bindResource as O, type PlatformResources as P, clampListPage as Q, type ResourceDef as R, type SaLocale as S, type TenantsListFilter as T, clampListPageSize as U, createSaCatalog as V, defaultHttpClient as W, defaultKvStore as X, defineListOp as Y, defineMessages as Z, defineResource as _, type SaMessages as a, isSaBuiltinLocale as a0, isSentInQuery as a1, listUrl as a2, mergeMessages as a3, planVersionsResource as a4, plansResource as a5, readListPage as a6, resolveMessages as a7, tenantsResource as a8, type SaNavMessages as a9, type SaLocaleOption as b, type SaLocaleDefinition as c, type SaMessagesOverrides as d, type ResourceOps as e, type ResourceContext as f, type ResourceListPage as g, ActionRegistry as h, ActionDefNotInManifestError as i, type AuditListFilter as j, type DefineResourceOptions as k, type HttpResponse as l, LIST_PAGE_SIZE_DEFAULT as m, LIST_PAGE_SIZE_MAX as n, LIST_PAGINATION_PARAMS as o, platformResources as p, type ListFilterOf as q, type ListOp as r, type ListQuery as s, MissingHandlerError as t, type PartialMessages as u, type PublishPlanVersionOptions as v, type ResolvedAction as w, type ResourceOp as x, SA_INTL_LOCALES as y, SA_LOCALES as z };