@saasicat/ui-vue 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +47 -0
  3. package/dist/index.cjs +3776 -0
  4. package/dist/index.d.cts +2395 -0
  5. package/dist/index.d.ts +2395 -0
  6. package/dist/index.js +3659 -0
  7. package/dist/testing-e2e/admin-pages-suite.cjs +134 -0
  8. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  9. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  10. package/dist/testing-e2e/admin-pages-suite.js +109 -0
  11. package/package.json +87 -0
  12. package/src/action-registry.ts +110 -0
  13. package/src/batch-column-fetcher.ts +152 -0
  14. package/src/boot-loader.ts +60 -0
  15. package/src/components/BundleVersionPublishDialog.vue +359 -0
  16. package/src/components/BusinessTypeVersionEditorDialog.vue +417 -0
  17. package/src/components/BusinessTypeVersionPublishDialog.vue +258 -0
  18. package/src/components/FeatureGate.vue +56 -0
  19. package/src/components/KpiCard.vue +53 -0
  20. package/src/components/KvBlock.vue +36 -0
  21. package/src/components/MarketingPromotionsTab.vue +861 -0
  22. package/src/components/MfaPromptDialog.vue +104 -0
  23. package/src/components/TenantActionConfirmDialog.vue +252 -0
  24. package/src/components/VersionDiffPreview.vue +150 -0
  25. package/src/components/bundle-editor/BundleCreatePanel.vue +681 -0
  26. package/src/components/bundle-editor/BundleFeaturesEditor.vue +215 -0
  27. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +316 -0
  28. package/src/components/bundle-editor/BundleQuotasEditor.vue +219 -0
  29. package/src/components/bundle-editor/BundleStatusBanner.vue +189 -0
  30. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +659 -0
  31. package/src/components/bundle-editor/BundleVersionStrip.vue +207 -0
  32. package/src/components/bundle-editor/bundle-version-status.ts +164 -0
  33. package/src/components/bundle-editor/catalog-i18n.ts +85 -0
  34. package/src/components/dialogs/PilotCreateDialog.vue +780 -0
  35. package/src/components/dialogs/PilotEditDialog.vue +548 -0
  36. package/src/components/dialogs/PromoCodeCreateDialog.vue +737 -0
  37. package/src/components/dialogs/PromoCodeEditDialog.vue +859 -0
  38. package/src/components/dialogs/types.ts +126 -0
  39. package/src/components/plan/PlanCycleToggle.vue +99 -0
  40. package/src/components/plan/PlanGrid.vue +211 -0
  41. package/src/components/plan/PriceSummary.vue +293 -0
  42. package/src/components/plan/PromoCodeInput.vue +192 -0
  43. package/src/components/plan/PublicBundleGrid.vue +287 -0
  44. package/src/components/plan-cockpit/PlanCockpit.vue +993 -0
  45. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +65 -0
  46. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +91 -0
  47. package/src/components/plan-cockpit/PlanCockpitHeader.vue +98 -0
  48. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +99 -0
  49. package/src/components/plan-cockpit/PlanCockpitKpis.vue +57 -0
  50. package/src/components/plan-cockpit/PlanCockpitVersions.vue +226 -0
  51. package/src/components/plan-cockpit/types.ts +72 -0
  52. package/src/components/plan-create-dialog/PlanCreateDialog.vue +508 -0
  53. package/src/components/plan-detail/PlanAuditLog.vue +59 -0
  54. package/src/components/plan-detail/PlanDetail.vue +1195 -0
  55. package/src/components/plan-detail/PlanDetailHeader.vue +185 -0
  56. package/src/components/plan-detail/PlanDetailKpis.vue +57 -0
  57. package/src/components/plan-detail/PlanTerminateDialog.vue +76 -0
  58. package/src/components/plan-detail/PlanVersionDiffPanel.vue +149 -0
  59. package/src/components/plan-detail/PlanVersionsPanel.vue +255 -0
  60. package/src/components/plan-detail/types.ts +59 -0
  61. package/src/components/plan-list/PlanList.vue +1067 -0
  62. package/src/components/plan-matrix/PlanMatrix.vue +1113 -0
  63. package/src/components/plan-review/PlanReview.vue +742 -0
  64. package/src/components/plan-version-editor/PlanCatalogPreview.vue +184 -0
  65. package/src/components/plan-version-editor/PlanComponentPool.vue +272 -0
  66. package/src/components/plan-version-editor/PlanVersionBasket.vue +281 -0
  67. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +84 -0
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +1728 -0
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +94 -0
  70. package/src/components/plan-version-editor/types.ts +96 -0
  71. package/src/components/wizard-stepper/WizardStepper.vue +107 -0
  72. package/src/create-admin-routes.ts +68 -0
  73. package/src/create-super-admin-app.ts +345 -0
  74. package/src/entitlement-provider.ts +36 -0
  75. package/src/feature-router-guard.ts +77 -0
  76. package/src/http-json.ts +62 -0
  77. package/src/index.ts +98 -0
  78. package/src/manifest-loader.ts +139 -0
  79. package/src/manifest-store-factory.ts +114 -0
  80. package/src/nav-builder.ts +274 -0
  81. package/src/pages-standard/AdminLayout.vue +411 -0
  82. package/src/pages-standard/AdminManifestErrorPage.vue +79 -0
  83. package/src/pages-standard/AuditPage.vue +223 -0
  84. package/src/pages-standard/BundlesPage.vue +910 -0
  85. package/src/pages-standard/BusinessTypesPage.vue +662 -0
  86. package/src/pages-standard/DashboardPage.vue +595 -0
  87. package/src/pages-standard/DiscoveryPage.vue +663 -0
  88. package/src/pages-standard/EmailHistoryPage.vue +576 -0
  89. package/src/pages-standard/MarketingCatalogPage.vue +1771 -0
  90. package/src/pages-standard/PilotsPage.vue +577 -0
  91. package/src/pages-standard/PlanVersionsPage.vue +216 -0
  92. package/src/pages-standard/PlansPage.vue +1222 -0
  93. package/src/pages-standard/PlatformEmailPage.vue +434 -0
  94. package/src/pages-standard/PromoCodeDetailPage.vue +331 -0
  95. package/src/pages-standard/PromoCodesPage.vue +589 -0
  96. package/src/pages-standard/SubscriptionsPage.vue +159 -0
  97. package/src/pages-standard/SuperAdminLoginPage.vue +299 -0
  98. package/src/pages-standard/SuperAdminSetupWizard.vue +405 -0
  99. package/src/pages-standard/TenantDetailPage.vue +432 -0
  100. package/src/pages-standard/TenantsPage.vue +732 -0
  101. package/src/pages-standard/UsersPage.vue +504 -0
  102. package/src/pages-standard/bundles-page/BundleAccordionList.vue +116 -0
  103. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +193 -0
  104. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +48 -0
  105. package/src/pages-standard/bundles-page/BundlesHeader.vue +67 -0
  106. package/src/pages-standard/bundles-page/BundlesKpis.vue +39 -0
  107. package/src/pages-standard/bundles-page/types.ts +15 -0
  108. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +246 -0
  109. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +171 -0
  110. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +410 -0
  111. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +31 -0
  112. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +47 -0
  113. package/src/pages-standard/discovery-page/DiscoveryMetaBanner.vue +26 -0
  114. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +247 -0
  115. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +73 -0
  116. package/src/pages-standard/discovery-page/discovery-ui.ts +166 -0
  117. package/src/pages-standard/email-history.types.ts +48 -0
  118. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +448 -0
  119. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +93 -0
  120. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +138 -0
  121. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +58 -0
  122. package/src/pages-standard/marketing-catalog/types.ts +43 -0
  123. package/src/pages-standard/plan-versions/PlanDiffCard.vue +132 -0
  124. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +293 -0
  125. package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +48 -0
  126. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +307 -0
  127. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +269 -0
  128. package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +65 -0
  129. package/src/pages-standard/plan-versions/PlanVersionsList.vue +292 -0
  130. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +261 -0
  131. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +300 -0
  132. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +167 -0
  133. package/src/pages-standard/plan-versions/format.ts +53 -0
  134. package/src/pages-standard/plan-versions/types.ts +33 -0
  135. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +50 -0
  136. package/src/pages-standard/plans-page/PlanBundleOverview.vue +147 -0
  137. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +46 -0
  138. package/src/pages-standard/plans-page/PlanPublishDialog.vue +106 -0
  139. package/src/pages-standard/plans-page/PlansPageToast.vue +69 -0
  140. package/src/pages-standard/plans-page/types.ts +18 -0
  141. package/src/pages-standard/platform-email.types.ts +41 -0
  142. package/src/pages-standard/sa-theme.css +223 -0
  143. package/src/pages-standard/tenants/StatusPill.vue +62 -0
  144. package/src/pages-standard/tenants/format.ts +38 -0
  145. package/src/pages-tenant/LimitsRow.vue +67 -0
  146. package/src/pages-tenant/MySubscriptionBundlesPage.vue +604 -0
  147. package/src/pages-tenant/OnboardingConfigurator.vue +417 -0
  148. package/src/pages-tenant/PackageSnapshotPanel.vue +241 -0
  149. package/src/pages-tenant/PendingVersionBanner.vue +126 -0
  150. package/src/pages-tenant/PlanChangeWizard.vue +630 -0
  151. package/src/pages-tenant/TenantPlanSection.vue +729 -0
  152. package/src/pages-tenant/UsageBar.vue +107 -0
  153. package/src/pages-tenant/default-i18n.ts +280 -0
  154. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +304 -0
  155. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +323 -0
  156. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +122 -0
  157. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +67 -0
  158. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +31 -0
  159. package/src/plan-versions-catalog.ts +368 -0
  160. package/src/platform-loaders.ts +68 -0
  161. package/src/project-page-host.ts +108 -0
  162. package/src/testing-e2e/admin-pages-suite.ts +187 -0
  163. package/src/types.ts +58 -0
  164. package/src/use-actions.ts +25 -0
  165. package/src/use-api-list.ts +151 -0
  166. package/src/use-audit-entries.ts +48 -0
  167. package/src/use-batch-columns.ts +59 -0
  168. package/src/use-bulk-publish.ts +150 -0
  169. package/src/use-bundle-versions-map.ts +112 -0
  170. package/src/use-bundles.ts +297 -0
  171. package/src/use-business-types.ts +269 -0
  172. package/src/use-catalog-entries.ts +243 -0
  173. package/src/use-discovery.ts +158 -0
  174. package/src/use-entitlement.ts +87 -0
  175. package/src/use-live-plan-versions.ts +126 -0
  176. package/src/use-manifest.ts +57 -0
  177. package/src/use-marketing-projections.ts +160 -0
  178. package/src/use-nav.ts +33 -0
  179. package/src/use-plan-editor.ts +162 -0
  180. package/src/use-plan-versions.ts +83 -0
  181. package/src/use-plans.ts +375 -0
  182. package/src/use-platform-tenant-actions.ts +286 -0
  183. package/src/use-promotions.ts +126 -0
  184. package/src/use-public-boot.ts +40 -0
  185. package/src/use-subscription-draft.ts +355 -0
  186. package/src/use-super-admin-context.ts +93 -0
  187. package/src/use-tenant-action-flow.ts +238 -0
  188. package/src/use-tenant-billing-catalog.ts +160 -0
  189. package/src/use-tenant-billing.ts +453 -0
  190. package/src/use-tenant-manifest.ts +94 -0
  191. package/src/use-tenant-subscription-bundles.ts +148 -0
  192. package/src/use-tenants.ts +61 -0
  193. package/src/version.ts +10 -0
@@ -0,0 +1,345 @@
1
+ // createSuperAdminApp — universal bootstrap helper for SuperAdmin Vue apps.
2
+ // Bundles the Quasar/Pinia/Router setup that every consumer app rebuilds itself
3
+ // today, and exposes the platform contract maps (`extensions:`, `actions:`)
4
+ // via `provide()`.
5
+ //
6
+ // What the helper does NOT do:
7
+ // - Auth stores, login flows, JWT refresh — app-specific.
8
+ // - CSS imports (Quasar CSS, Material Icons, app theme) — these stay in the
9
+ // app's own `main.ts`, because tsup does not bundle CSS.
10
+
11
+ import { createApp, type App, type Component, type InjectionKey } from 'vue';
12
+ import { Quasar, Notify, Dialog, Loading, type QuasarPluginOptions } from 'quasar';
13
+ import { createPinia, type Pinia } from 'pinia';
14
+ import {
15
+ createRouter,
16
+ createWebHistory,
17
+ type NavigationGuardWithThis,
18
+ type RouteLocationNormalized,
19
+ type RouteRecordRaw,
20
+ type Router,
21
+ type RouterHistory,
22
+ } from 'vue-router';
23
+
24
+ import type { ActionKey, AdminManifest, ComponentKey } from '@saasicat/types';
25
+ import type { ActionHandler } from './action-registry.js';
26
+ import { defaultHttpClient, type HttpClient } from './types.js';
27
+
28
+ /**
29
+ * App-specific branding data that the platform `AdminLayout` and other
30
+ * consumers read via `useSuperAdminBrand()`.
31
+ */
32
+ export interface SuperAdminBrand {
33
+ /** 2-letter abbreviation in the logo badge (`'ma'`, `'da'`, …). */
34
+ logoText: string;
35
+ /** Full display name (`'DemoApp'`, `'ClubApp'`, …). */
36
+ name: string;
37
+ /** Optional: tag to the right of the name, default `'SuperAdmin'`. */
38
+ tag?: string;
39
+ }
40
+
41
+ /**
42
+ * Endpoint configuration. `apiBase` is the shared prefix under which
43
+ * `/manifest`, `/boot` and extra routes live.
44
+ */
45
+ export interface SuperAdminEndpoints {
46
+ /** Shared prefix, e.g. `'/api/admin'` or `'/api/v1/admin'`. */
47
+ apiBase: string;
48
+ /** Pre-login branding endpoint, default `${apiBase}/boot`. */
49
+ publicBootEndpoint?: string;
50
+ /** Post-login full-manifest endpoint, default `${apiBase}/manifest`. */
51
+ manifestEndpoint?: string;
52
+ }
53
+
54
+ export type ExtensionLoader = () => Promise<Component | { default: Component }>;
55
+ export type ExtensionsMap = Record<ComponentKey, ExtensionLoader>;
56
+ export type ActionsMap = Record<ActionKey, ActionHandler>;
57
+
58
+ export interface SuperAdminAuthGuardOptions {
59
+ /** App provides: is the user currently logged in? */
60
+ isAuthenticated: () => boolean;
61
+ /** App provides: does the user have the SuperAdmin role? Default: only check `isAuthenticated`. */
62
+ isSuperAdmin?: () => boolean;
63
+ /** App provides: redirect path for unauthenticated calls (e.g. `'/login'`). */
64
+ onUnauthenticated: () => string;
65
+ }
66
+
67
+ /**
68
+ * Result of a login attempt. Apps pass this back from their auth store to the
69
+ * platform LoginPage; the page renders an appropriate error message.
70
+ *
71
+ * `ok: true` → login succeeded, page redirects to `redirectAfterLogin`.
72
+ * `ok: false` → page shows `message` or a translation derived from `code`.
73
+ *
74
+ * Known codes:
75
+ * - `BAD_CREDENTIALS` — wrong email/password combination.
76
+ * - `NOT_SUPER_ADMIN` — account does not have the SuperAdmin role.
77
+ * - otherwise — app-specific; `message` is displayed directly.
78
+ */
79
+ export type SuperAdminLoginResult =
80
+ | { ok: true }
81
+ | { ok: false; code?: 'BAD_CREDENTIALS' | 'NOT_SUPER_ADMIN' | string; message?: string };
82
+
83
+ /**
84
+ * Login adapter. The app passes its auth-store call through here. The platform
85
+ * LoginPage consumes it via `useSuperAdminLoginAdapter()`, without knowledge
86
+ * of app-specific stores (Pinia, auth API routes, MFA hooks).
87
+ */
88
+ export interface SuperAdminLoginAdapter {
89
+ /**
90
+ * Performs the login. The app store encapsulates the API call, token
91
+ * storage, MFA hops etc.
92
+ */
93
+ login(email: string, password: string): Promise<SuperAdminLoginResult>;
94
+ /**
95
+ * Target route after a successful login. Default: `/admin/dashboard`
96
+ * (platform convention for the standard pages — apps with a different
97
+ * default mount override this here).
98
+ */
99
+ redirectAfterLogin?: string;
100
+ /**
101
+ * Optional: dev hint (test account), shown below the form. Deliberately
102
+ * rendered only when `environment !== 'production'`.
103
+ */
104
+ devHint?: { email: string; password: string };
105
+ }
106
+
107
+ export interface SuperAdminManifestGuardOptions {
108
+ /**
109
+ * App provides: loads the manifest into the app store. The router guard
110
+ * `await`s the promise before the route is resolved.
111
+ *
112
+ * **On loader error:** the promise REJECTS. The router guard catches the
113
+ * rejection and decides depending on `errorRoute`:
114
+ * - `errorRoute` set → redirect to this route (fail-closed).
115
+ * - `errorRoute` not set → `console.error` + render allowed
116
+ * (defensive default behavior; the app must render the manifest gap
117
+ * itself).
118
+ */
119
+ ensureLoaded: () => Promise<void>;
120
+ /**
121
+ * Optional: read accessor on the loaded manifest. When set, it is exposed
122
+ * via `provide(SUPER_ADMIN_MANIFEST_KEY)` — the `<ProjectPageHost>`
123
+ * resolves manifest `projectPages` through it against the
124
+ * `extensions:` map.
125
+ */
126
+ getManifest?: () => AdminManifest | null;
127
+ /**
128
+ * Optional: path that the router guard redirects to on a manifest load
129
+ * error (fail-closed mode). The app must register the route in `appRoutes`
130
+ * and mark it as `meta.public = true`, otherwise it runs through the
131
+ * manifest guard again and produces a redirect loop.
132
+ */
133
+ errorRoute?: string;
134
+ }
135
+
136
+ export interface CreateSuperAdminAppOptions {
137
+ /** App root component (`App.vue`). */
138
+ rootComponent: Component;
139
+ /** App branding (logo, name). */
140
+ brand: SuperAdminBrand;
141
+ /** Endpoint configuration. */
142
+ endpoints: SuperAdminEndpoints;
143
+ /** App's own routes (login, standard pages, bundle pages, …). */
144
+ appRoutes: RouteRecordRaw[];
145
+ /**
146
+ * Static `extensions:` map. Manifest `projectPages[].componentKey` is
147
+ * looked up in it (see Spec §4.4).
148
+ */
149
+ extensions?: ExtensionsMap;
150
+ /**
151
+ * Static `actions:` map. Manifest `tenants.actions[].actionKey` is looked
152
+ * up in it.
153
+ */
154
+ actions?: ActionsMap;
155
+ /**
156
+ * Optional: auth guard. When set, `router.beforeEach` is wired up
157
+ * automatically — `to.meta.public === true` bypasses the guard.
158
+ */
159
+ authGuard?: SuperAdminAuthGuardOptions;
160
+ /**
161
+ * Optional: manifest guard. Runs after a successful auth guard, blocks the
162
+ * render until the manifest is loaded (prevents sidebar flicker).
163
+ */
164
+ manifestGuard?: SuperAdminManifestGuardOptions;
165
+ /**
166
+ * Optional: login adapter. When set, it is exposed via `provide()` for the
167
+ * shared `<SuperAdminLoginPage>` (from `pages-standard/`) — the app no
168
+ * longer needs to hold its own LoginPage Vue component.
169
+ */
170
+ loginAdapter?: SuperAdminLoginAdapter;
171
+ /**
172
+ * Optional: router history variant, default `createWebHistory()`.
173
+ * Apps with a subpath mount override this.
174
+ */
175
+ routerHistory?: RouterHistory;
176
+ /**
177
+ * Optional: Quasar configuration. Default loads `Notify`/`Dialog`/`Loading`
178
+ * with the established consumer convention (`top-right`, 3 s).
179
+ */
180
+ quasarOptions?: Partial<QuasarPluginOptions>;
181
+ /**
182
+ * Optional: additional Vue plugins (e.g. an app's own NotificationCenter)
183
+ * that are installed after the platform setup, before the mount.
184
+ */
185
+ installPlugins?: Array<(app: App) => void>;
186
+ /**
187
+ * Optional: `HttpClient` for all pre-login calls (boot, first-run setup).
188
+ * Default `defaultHttpClient()` (= `fetch`). Consumers pass their own
189
+ * variant through (auth header, baseURL, retry) — it then applies uniformly,
190
+ * including for the setup wizard. Consumed via `useSuperAdminHttp()`.
191
+ */
192
+ http?: HttpClient;
193
+ }
194
+
195
+ export interface SuperAdminAppHandle {
196
+ app: App;
197
+ router: Router;
198
+ pinia: Pinia;
199
+ /** Mounts the app on a selector. Returns the root component instance. */
200
+ mount: (selector: string | Element) => ReturnType<App['mount']>;
201
+ }
202
+
203
+ // Vue inject keys are registered globally via `Symbol.for(...)`. Reason:
204
+ // apps import `createSuperAdminApp` (and thus `app.provide(KEY, ...)`) from
205
+ // the built `dist/index.js` bundle, but shared `.vue` pages in the
206
+ // `pages-standard/` directory directly from `src/`. Both paths produce two
207
+ // independent module instances — with local `Symbol(...)` the identity would
208
+ // differ, and `inject()` would always throw "not found". `Symbol.for(...)`
209
+ // resolves both imports to the same symbol.
210
+
211
+ /** Vue inject key for `useSuperAdminBrand()`. */
212
+ export const SUPER_ADMIN_BRAND_KEY: InjectionKey<SuperAdminBrand> = Symbol.for(
213
+ '@saasicat/ui-vue/SUPER_ADMIN_BRAND',
214
+ );
215
+ /** Vue inject key for `useSuperAdminEndpoints()`. */
216
+ export const SUPER_ADMIN_ENDPOINTS_KEY: InjectionKey<Required<SuperAdminEndpoints>> = Symbol.for(
217
+ '@saasicat/ui-vue/SUPER_ADMIN_ENDPOINTS',
218
+ );
219
+ /** Vue inject key for `useSuperAdminExtensions()`. */
220
+ export const SUPER_ADMIN_EXTENSIONS_KEY: InjectionKey<ExtensionsMap> = Symbol.for(
221
+ '@saasicat/ui-vue/SUPER_ADMIN_EXTENSIONS',
222
+ );
223
+ /** Vue inject key for `useSuperAdminActions()`. */
224
+ export const SUPER_ADMIN_ACTIONS_KEY: InjectionKey<ActionsMap> = Symbol.for(
225
+ '@saasicat/ui-vue/SUPER_ADMIN_ACTIONS',
226
+ );
227
+ /**
228
+ * Vue inject key for the manifest accessor. Only provided when
229
+ * `manifestGuard.getManifest` was passed to `createSuperAdminApp()` — the
230
+ * `<ProjectPageHost>` needs it to resolve project pages.
231
+ */
232
+ export const SUPER_ADMIN_MANIFEST_KEY: InjectionKey<() => AdminManifest | null> = Symbol.for(
233
+ '@saasicat/ui-vue/SUPER_ADMIN_MANIFEST',
234
+ );
235
+ /** Vue inject key for `useSuperAdminLoginAdapter()`. */
236
+ export const SUPER_ADMIN_LOGIN_ADAPTER_KEY: InjectionKey<SuperAdminLoginAdapter> = Symbol.for(
237
+ '@saasicat/ui-vue/SUPER_ADMIN_LOGIN_ADAPTER',
238
+ );
239
+ /** Vue inject key for `useSuperAdminHttp()` (pre-login HttpClient). */
240
+ export const SUPER_ADMIN_HTTP_KEY: InjectionKey<HttpClient> = Symbol.for(
241
+ '@saasicat/ui-vue/SUPER_ADMIN_HTTP',
242
+ );
243
+
244
+ const DEFAULT_QUASAR_OPTIONS: QuasarPluginOptions = {
245
+ plugins: { Notify, Dialog, Loading },
246
+ config: { notify: { position: 'top-right', timeout: 3000 } },
247
+ };
248
+
249
+ /**
250
+ * Universal bootstrap function for SuperAdmin apps. Replaces the `main.ts`
251
+ * boilerplate duplicated per app today (Quasar + Pinia + Router + manifest
252
+ * guard) and exposes the platform maps via `provide()` for downstream
253
+ * components.
254
+ */
255
+ export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperAdminAppHandle {
256
+ const app = createApp(options.rootComponent);
257
+
258
+ app.use(Quasar, options.quasarOptions ?? DEFAULT_QUASAR_OPTIONS);
259
+
260
+ const pinia = createPinia();
261
+ app.use(pinia);
262
+
263
+ const router = createRouter({
264
+ history: options.routerHistory ?? createWebHistory(),
265
+ routes: options.appRoutes,
266
+ });
267
+
268
+ const navGuard = buildNavigationGuard(options);
269
+ if (navGuard) {
270
+ router.beforeEach(navGuard);
271
+ }
272
+
273
+ app.use(router);
274
+
275
+ const endpoints: Required<SuperAdminEndpoints> = {
276
+ apiBase: options.endpoints.apiBase,
277
+ publicBootEndpoint:
278
+ options.endpoints.publicBootEndpoint ?? `${options.endpoints.apiBase}/boot`,
279
+ manifestEndpoint:
280
+ options.endpoints.manifestEndpoint ?? `${options.endpoints.apiBase}/manifest`,
281
+ };
282
+
283
+ app.provide(SUPER_ADMIN_BRAND_KEY, { tag: 'SuperAdmin', ...options.brand });
284
+ app.provide(SUPER_ADMIN_ENDPOINTS_KEY, endpoints);
285
+ app.provide(SUPER_ADMIN_EXTENSIONS_KEY, options.extensions ?? {});
286
+ app.provide(SUPER_ADMIN_ACTIONS_KEY, options.actions ?? {});
287
+ if (options.manifestGuard?.getManifest) {
288
+ app.provide(SUPER_ADMIN_MANIFEST_KEY, options.manifestGuard.getManifest);
289
+ }
290
+ if (options.loginAdapter) {
291
+ app.provide(SUPER_ADMIN_LOGIN_ADAPTER_KEY, options.loginAdapter);
292
+ }
293
+ app.provide(SUPER_ADMIN_HTTP_KEY, options.http ?? defaultHttpClient());
294
+
295
+ for (const plugin of options.installPlugins ?? []) {
296
+ plugin(app);
297
+ }
298
+
299
+ return {
300
+ app,
301
+ router,
302
+ pinia,
303
+ mount: (selector) => app.mount(selector),
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Internal helper, exported for isolated unit tests of the navigation
309
+ * behavior (auth redirect, manifest fail-closed path). Consumers should call
310
+ * `createSuperAdminApp()`, not this helper directly.
311
+ */
312
+ export function buildNavigationGuard(
313
+ options: Pick<CreateSuperAdminAppOptions, 'authGuard' | 'manifestGuard'>,
314
+ ): NavigationGuardWithThis<undefined> | null {
315
+ const { authGuard, manifestGuard } = options;
316
+ if (!authGuard && !manifestGuard) return null;
317
+
318
+ return async (to: RouteLocationNormalized) => {
319
+ if (to.meta?.public === true) return true;
320
+
321
+ if (authGuard) {
322
+ if (!authGuard.isAuthenticated()) return authGuard.onUnauthenticated();
323
+ if (authGuard.isSuperAdmin && !authGuard.isSuperAdmin()) {
324
+ return authGuard.onUnauthenticated();
325
+ }
326
+ }
327
+
328
+ if (manifestGuard) {
329
+ try {
330
+ await manifestGuard.ensureLoaded();
331
+ } catch (err) {
332
+ if (manifestGuard.errorRoute && to.path !== manifestGuard.errorRoute) {
333
+ return manifestGuard.errorRoute;
334
+ }
335
+ // No errorRoute configured — defensive fallback: warn, allow
336
+ // render. The app can detect the error via the store
337
+ // (`error.value`) and render a banner/stub.
338
+
339
+ console.error('[SuperAdmin] manifest load failed', err);
340
+ }
341
+ }
342
+
343
+ return true;
344
+ };
345
+ }
@@ -0,0 +1,36 @@
1
+ // Entitlement provider — app-wide inject key for FeatureGate / router guard.
2
+ //
3
+ // Consumers call `provideEntitlement(app, ent)` once at app bootstrap,
4
+ // where `ent` is the result of `useEntitlement({...})`. Afterwards
5
+ // <FeatureGate> components and the router guard can access it without
6
+ // reconfiguring the endpoint.
7
+ //
8
+ // `Symbol.for(...)` so that multi-bundle setups (app bundle + pages bundle)
9
+ // resolve to the same identity — analogous to the inject keys in
10
+ // create-super-admin-app.
11
+ //
12
+ // Spec: handoff/superadmin/QUICKSTART_SIMPLIFICATIONS.md §P8.
13
+
14
+ import { inject, type App, type InjectionKey } from 'vue';
15
+ import type { UseEntitlementResult } from './use-entitlement.js';
16
+
17
+ export const ENTITLEMENT_INJECTION_KEY: InjectionKey<UseEntitlementResult> = Symbol.for(
18
+ '@saasicat/ui-vue/ENTITLEMENT',
19
+ );
20
+
21
+ /**
22
+ * Binds a `useEntitlement(...)` result for the whole app so that
23
+ * `<FeatureGate>` and router guards can inject it.
24
+ */
25
+ export function provideEntitlement(app: App, entitlement: UseEntitlementResult): void {
26
+ app.provide(ENTITLEMENT_INJECTION_KEY, entitlement);
27
+ }
28
+
29
+ /**
30
+ * Fetches the bound entitlement result. Returns `null` when the
31
+ * consumer did not call `provideEntitlement(...)` — `<FeatureGate>`
32
+ * then falls back to "everything allowed" (with a dev warning).
33
+ */
34
+ export function useInjectedEntitlement(): UseEntitlementResult | null {
35
+ return inject(ENTITLEMENT_INJECTION_KEY, null);
36
+ }
@@ -0,0 +1,77 @@
1
+ // Feature router guard — Vue Router `beforeEach` that checks routes with
2
+ // `meta.requiresFeature` against the entitlement source of truth.
3
+ //
4
+ // Usage (in the app router setup, alternatively via createSuperAdminApp):
5
+ //
6
+ // ```ts
7
+ // import { useEntitlement, buildFeatureRouterGuard } from '@saasicat/ui-vue';
8
+ // const ent = useEntitlement({ endpoint: '/api/billing/entitlement' });
9
+ // router.beforeEach(buildFeatureRouterGuard({
10
+ // getEntitlement: () => ent,
11
+ // redirectTo: '/upgrade',
12
+ // }));
13
+ // ```
14
+ //
15
+ // Route meta:
16
+ //
17
+ // ```ts
18
+ // { path: '/dms', component: DmsPage, meta: { requiresFeature: 'DMS' } }
19
+ // { path: '/x', component: XPage, meta: { requiresFeature: ['DMS', 'STORAGE_PRO'] } }
20
+ // ```
21
+ //
22
+ // Spec: handoff/superadmin/QUICKSTART_SIMPLIFICATIONS.md §P8.
23
+
24
+ import type { NavigationGuardWithThis, RouteLocationNormalized } from 'vue-router';
25
+ import type { UseEntitlementResult } from './use-entitlement.js';
26
+
27
+ export interface FeatureRouterGuardOptions {
28
+ /**
29
+ * Returns the current entitlement. Deliberately a factory (instead of
30
+ * passing a ref directly), so the app can lazy-load the entitlement and
31
+ * re-bind it — e.g. after a plan change or logout/re-login.
32
+ */
33
+ getEntitlement: () => UseEntitlementResult | null;
34
+ /**
35
+ * Where to redirect when a feature is missing. Default: no redirect, but
36
+ * `next(false)` (route blocks). Apps with an upgrade page set
37
+ * `'/upgrade'`.
38
+ */
39
+ redirectTo?: string;
40
+ /**
41
+ * `true` (default): entitlement not yet loaded → let through,
42
+ * so the first render doesn't hang. `false`: blocks until the entitlement
43
+ * is there (user sees a white screen if the endpoint is slow — only
44
+ * sensible in apps with pre-login load).
45
+ */
46
+ allowWhileLoading?: boolean;
47
+ }
48
+
49
+ function extractRequired(to: RouteLocationNormalized): string[] {
50
+ const raw = (to.meta as { requiresFeature?: string | string[] } | undefined)?.requiresFeature;
51
+ if (!raw) return [];
52
+ return Array.isArray(raw) ? raw : [raw];
53
+ }
54
+
55
+ export function buildFeatureRouterGuard(
56
+ options: FeatureRouterGuardOptions,
57
+ ): NavigationGuardWithThis<undefined> {
58
+ return function featureGuard(to, _from, next) {
59
+ const required = extractRequired(to);
60
+ if (required.length === 0) return next();
61
+ const ent = options.getEntitlement();
62
+ if (!ent) {
63
+ // No provider — let through (this prevents the whole app
64
+ // from stalling if the app registered the guard but the
65
+ // entitlement isn't there yet).
66
+ return next();
67
+ }
68
+ const allowLoading = options.allowWhileLoading !== false;
69
+ if (allowLoading && ent.loading.value && ent.entitlement.value === null) {
70
+ return next();
71
+ }
72
+ const allowed = required.some((f) => ent.hasFeature(f));
73
+ if (allowed) return next();
74
+ if (options.redirectTo) return next(options.redirectTo);
75
+ return next(false);
76
+ };
77
+ }
@@ -0,0 +1,62 @@
1
+ // Shared JSON helpers over the injectable `HttpClient`. A single
2
+ // request/error path for all pre-login calls (boot, setup status, setup),
3
+ // instead of raw `fetch` with divergent error handling per component. A
4
+ // consumer `HttpClient` (auth header, baseURL, retry) then applies everywhere.
5
+
6
+ import type { HttpClient } from './types.js';
7
+
8
+ /**
9
+ * Error for non-2xx responses. `code` is the machine-readable error code
10
+ * from the JSON body (`{ code }`), if present — callers map it to
11
+ * a message (e.g. via `SETUP_ERROR_CODES`).
12
+ */
13
+ export class HttpJsonError extends Error {
14
+ constructor(
15
+ readonly status: number,
16
+ readonly code?: string,
17
+ ) {
18
+ super(code ?? `HTTP ${status}`);
19
+ this.name = 'HttpJsonError';
20
+ }
21
+ }
22
+
23
+ async function extractCode(res: { json(): Promise<unknown> }): Promise<string | undefined> {
24
+ try {
25
+ const body = (await res.json()) as { code?: unknown } | null;
26
+ return typeof body?.code === 'string' ? body.code : undefined;
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Removes trailing slashes so an API prefix can be concatenated with paths
34
+ * that start with `/`. Deliberately index-based instead of
35
+ * `replace(/\/+$/, '')`: the regex backtracks quadratically on inputs that
36
+ * end in many slashes.
37
+ */
38
+ export function trimTrailingSlashes(url: string): string {
39
+ let end = url.length;
40
+ while (end > 0 && url[end - 1] === '/') end--;
41
+ return url.slice(0, end);
42
+ }
43
+
44
+ export async function getJson<T>(http: HttpClient, url: string): Promise<T> {
45
+ const res = await http(url);
46
+ if (res.status < 200 || res.status >= 300) {
47
+ throw new HttpJsonError(res.status, await extractCode(res));
48
+ }
49
+ return (await res.json()) as T;
50
+ }
51
+
52
+ export async function postJson<T>(http: HttpClient, url: string, body: unknown): Promise<T> {
53
+ const res = await http(url, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json' },
56
+ body: JSON.stringify(body),
57
+ });
58
+ if (res.status < 200 || res.status >= 300) {
59
+ throw new HttpJsonError(res.status, await extractCode(res));
60
+ }
61
+ return (await res.json()) as T;
62
+ }
package/src/index.ts ADDED
@@ -0,0 +1,98 @@
1
+ // @saasicat/ui-vue — Vue 3 components + composables for the
2
+ // SuperAdmin UI shell.
3
+ //
4
+ // P4.1 (Phase 30): public boot loader + manifest loader (ETag cache).
5
+ // P4.2 (Phase 31): nav builder + extension host + action registry.
6
+ // P4.3 (Phase 32): batch column fetcher.
7
+ // P4.4 (Phase 33): composables for standard pages (Tenants, Audit, Entitlement).
8
+ // P4.5 (Phase 34): plan-versions list composables + bulk-publish orchestration.
9
+ //
10
+ // Contents:
11
+ // - types: HttpClient, KvStore, defaultHttpClient, defaultKvStore
12
+ // - boot-loader: BootLoader (framework-agnostic) + BootLoadError
13
+ // - manifest-loader: ManifestLoader with ETag cache + ManifestLoadError
14
+ // - nav-builder: buildRoutes, buildSidebar, resolveExtension
15
+ // - action-registry: ActionRegistry, MissingHandlerError, ActionDefNotInManifestError
16
+ // - batch-column-fetcher: BatchColumnFetcher + BatchColumnDriftError
17
+ // - use-api-list: useApiList<T> generic list composable
18
+ // - use-tenants: useTenants typed wrapper
19
+ // - use-audit-entries: useAuditEntries typed wrapper
20
+ // - use-entitlement: useEntitlement composable
21
+ // - use-plan-versions: usePlanVersionsCatalog
22
+ // - use-bulk-publish: useBulkPublish (parallel publishes with status tracking)
23
+ // - use-plan-editor: usePlanEditor composable (feature discovery, plannedOnly filter)
24
+ // - use-public-boot: usePublicBoot composable
25
+ // - use-manifest: useManifest composable
26
+ // - use-nav: useNav composable
27
+ // - use-actions: useActions composable
28
+ // - use-batch-columns: useBatchColumns composable
29
+
30
+ export * from './version.js';
31
+ export * from './types.js';
32
+ export * from './http-json.js';
33
+ export * from './create-admin-routes.js';
34
+ export * from './boot-loader.js';
35
+ export * from './manifest-loader.js';
36
+ export * from './nav-builder.js';
37
+ export * from './action-registry.js';
38
+ export * from './batch-column-fetcher.js';
39
+ export * from './use-api-list.js';
40
+ export * from './use-tenants.js';
41
+ export * from './use-audit-entries.js';
42
+ export * from './use-entitlement.js';
43
+ export * from './use-tenant-manifest.js';
44
+ export * from './entitlement-provider.js';
45
+ export * from './feature-router-guard.js';
46
+ // FeatureGate.vue is not bundled — consumers import it directly:
47
+ // import FeatureGate from '@saasicat/ui-vue/components/FeatureGate.vue';
48
+ export * from './use-tenant-billing-catalog.js';
49
+ export * from './use-tenant-billing.js';
50
+ export * from './use-subscription-draft.js';
51
+ export * from './pages-tenant/default-i18n.js';
52
+ export * from './use-plan-versions.js';
53
+ export * from './use-bulk-publish.js';
54
+ export * from './use-plan-editor.js';
55
+ export * from './use-public-boot.js';
56
+ export * from './use-discovery.js';
57
+ export * from './use-catalog-entries.js';
58
+ export * from './use-bundles.js';
59
+ export * from './use-bundle-versions-map.js';
60
+ export * from './use-tenant-subscription-bundles.js';
61
+ export * from './use-business-types.js';
62
+ export * from './use-marketing-projections.js';
63
+ export * from './use-promotions.js';
64
+ export * from './use-plans.js';
65
+ export * from './use-live-plan-versions.js';
66
+ export * from './use-manifest.js';
67
+ export * from './use-nav.js';
68
+ export * from './use-actions.js';
69
+ export * from './use-tenant-action-flow.js';
70
+ export * from './use-platform-tenant-actions.js';
71
+ export * from './use-batch-columns.js';
72
+ export * from './plan-versions-catalog.js';
73
+ // Platform dialog types for app wrappers that type submit handlers.
74
+ export * from './components/dialogs/types.js';
75
+ // Pure resolvers for translated feature/quota labels in the bundle editors.
76
+ export * from './components/bundle-editor/catalog-i18n.js';
77
+ // Types of the shared PlatformEmailPage (platform email sender).
78
+ export * from './pages-standard/platform-email.types.js';
79
+ // Types of the shared EmailHistoryPage (platform email history).
80
+ export * from './pages-standard/email-history.types.js';
81
+ // Universal bootstrap helper for SuperAdmin apps + associated composables.
82
+ export * from './create-super-admin-app.js';
83
+ export * from './use-super-admin-context.js';
84
+ // Loader factory that builds BootLoader + ManifestLoader from the same
85
+ // endpoint configuration as createSuperAdminApp() — so that endpoints live
86
+ // in a single place per app.
87
+ export * from './platform-loaders.js';
88
+ // ProjectPageHost: resolves manifest projectPages dynamically against the
89
+ // extensions: map — replaces static route duplication in the apps.
90
+ export * from './project-page-host.js';
91
+ // Manifest Pinia store factory — standardizes ensureLoaded/reload/clearCache.
92
+ export * from './manifest-store-factory.js';
93
+
94
+ // The shared SuperAdmin LoginPage lives at
95
+ // `@saasicat/ui-vue/pages-standard/SuperAdminLoginPage.vue` —
96
+ // not via bundle (tsup ignores .vue), but directly from src/ through the
97
+ // subpath export. Apps pass a `loginAdapter` via createSuperAdminApp()
98
+ // and route `/login` to this component.