@saasicat/ui-vue 0.19.0 → 0.22.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 (145) hide show
  1. package/dist/{catalog-JXdKdIqG.d.cts → catalog-DJcfm5N2.d.cts} +21 -7
  2. package/dist/{catalog-JXdKdIqG.d.ts → catalog-DJcfm5N2.d.ts} +21 -7
  3. package/dist/{chunk-DPTIOQXX.js → chunk-F7NRM6KI.js} +56 -28
  4. package/dist/{chunk-RVG6LQHL.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-ORX3ZG47.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +134 -29
  7. package/dist/client/index.d.cts +53 -3
  8. package/dist/client/index.d.ts +53 -3
  9. package/dist/client/index.js +14 -2
  10. package/dist/index.cjs +321 -162
  11. package/dist/index.d.cts +30 -8
  12. package/dist/index.d.ts +30 -8
  13. package/dist/index.js +176 -133
  14. package/dist/quasar/index.cjs +78 -29
  15. package/dist/quasar/index.d.cts +2 -2
  16. package/dist/quasar/index.d.ts +2 -2
  17. package/dist/quasar/index.js +6 -2
  18. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  19. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  20. package/dist/{use-super-admin-i18n-BlqcD0N7.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-1nCOkvgi.d.cts → use-super-admin-i18n-CgTuNi8h.d.cts} +34 -7
  22. package/package.json +18 -8
  23. package/src/client/i18n/messages/bundles.ts +0 -2
  24. package/src/client/i18n/messages/common.ts +24 -0
  25. package/src/client/i18n/messages/discovery.ts +2 -2
  26. package/src/client/i18n/messages/marketing.ts +2 -4
  27. package/src/client/i18n/messages/plan-detail.ts +0 -4
  28. package/src/client/i18n/messages/plans.ts +20 -8
  29. package/src/client/i18n/messages/tenants.ts +0 -2
  30. package/src/client/index.ts +1 -0
  31. package/src/client/manifest-loader.ts +1 -1
  32. package/src/client/resolve-plans.ts +144 -0
  33. package/src/components/BundleVersionPublishDialog.vue +6 -6
  34. package/src/components/KvBlock.vue +4 -4
  35. package/src/components/MarketingPromotionsTab.vue +34 -48
  36. package/src/components/MfaPromptDialog.vue +8 -2
  37. package/src/components/TenantActionConfirmDialog.vue +4 -4
  38. package/src/components/VersionDiffPreview.vue +7 -7
  39. package/src/components/admin-page/AdminBody.vue +40 -0
  40. package/src/components/admin-page/AdminFilters.vue +12 -0
  41. package/src/components/admin-page/AdminHero.vue +49 -0
  42. package/src/components/admin-page/AdminKpi.vue +84 -0
  43. package/src/components/admin-page/AdminPage.vue +16 -0
  44. package/src/components/admin-page/AdminPaginator.vue +195 -0
  45. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  46. package/src/components/admin-page/AdminSection.vue +53 -0
  47. package/src/components/admin-page/AdminStatistics.vue +40 -0
  48. package/src/components/admin-page/AdminTable.vue +175 -0
  49. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  50. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  51. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  52. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  53. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  54. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  55. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  56. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  57. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  58. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  59. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  60. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  61. package/src/components/plan/PlanGrid.vue +1 -1
  62. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  63. package/src/components/plan-detail/PlanAuditLog.vue +7 -8
  64. package/src/components/plan-detail/PlanDetail.vue +95 -292
  65. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  66. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  67. package/src/components/plan-detail/PlanVersionDiffPanel.vue +5 -8
  68. package/src/components/plan-detail/PlanVersionsPanel.vue +9 -11
  69. package/src/components/plan-list/PlanList.vue +206 -457
  70. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  71. package/src/components/plan-review/PlanReview.vue +191 -275
  72. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  73. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  74. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  75. package/src/index.ts +1 -0
  76. package/src/pages-standard/AdminLayout.vue +38 -12
  77. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  78. package/src/pages-standard/AuditPage.vue +65 -109
  79. package/src/pages-standard/BundlesPage.vue +146 -179
  80. package/src/pages-standard/DashboardPage.vue +59 -154
  81. package/src/pages-standard/DiscoveryPage.vue +172 -227
  82. package/src/pages-standard/EmailHistoryPage.vue +118 -159
  83. package/src/pages-standard/MarketingCatalogPage.vue +399 -440
  84. package/src/pages-standard/PilotsPage.vue +69 -117
  85. package/src/pages-standard/PlansPage.vue +240 -112
  86. package/src/pages-standard/PlatformEmailPage.vue +56 -91
  87. package/src/pages-standard/PromoCodeDetailPage.vue +49 -74
  88. package/src/pages-standard/PromoCodesPage.vue +95 -128
  89. package/src/pages-standard/SubscriptionsPage.vue +19 -52
  90. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  91. package/src/pages-standard/SuperAdminSetupWizard.vue +22 -18
  92. package/src/pages-standard/TenantDetailPage.vue +100 -191
  93. package/src/pages-standard/TenantsPage.vue +190 -295
  94. package/src/pages-standard/UsersPage.vue +89 -157
  95. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  96. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  97. package/src/pages-standard/bundles-page/BundlesToolbar.vue +63 -0
  98. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  99. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  100. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  101. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  102. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  103. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +111 -73
  104. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +43 -62
  105. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +53 -35
  106. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +7 -7
  107. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  108. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  109. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  110. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  111. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  112. package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
  113. package/src/pages-standard/sa-theme.css +552 -86
  114. package/src/pages-standard/tenant-detail/TenantMasterData.vue +28 -0
  115. package/src/pages-standard/tenant-detail/TenantUsage.vue +31 -0
  116. package/src/pages-standard/tenant-detail/TenantUsers.vue +15 -0
  117. package/src/pages-standard/tenant-detail/types.ts +38 -0
  118. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  119. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  120. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  121. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  122. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  123. package/src/quasar/create-super-admin-app.ts +4 -0
  124. package/src/vue/entitlement-provider.ts +0 -1
  125. package/src/vue/feature-router-guard.ts +0 -1
  126. package/src/vue/super-admin-context.ts +106 -1
  127. package/src/vue/use-api-list.ts +2 -1
  128. package/src/vue/use-mfa-prompt.ts +72 -0
  129. package/src/vue/use-pagination.ts +63 -0
  130. package/src/vue/use-plans.ts +1 -2
  131. package/src/vue/use-sign-out.ts +90 -0
  132. package/src/vue/use-tenant-billing.ts +1 -1
  133. package/src/vue/use-tenant-manifest.ts +0 -1
  134. package/src/components/KpiCard.vue +0 -53
  135. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1012
  136. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  137. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -100
  138. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  139. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  140. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  141. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -251
  142. package/src/components/plan-cockpit/types.ts +0 -72
  143. package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
  144. package/src/pages-standard/bundles-page/BundlesHeader.vue +0 -68
  145. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +0 -34
@@ -1,80 +1,70 @@
1
1
  <template>
2
- <div class="sa-promo-codes">
3
- <header class="sa-page-head">
4
- <div>
5
- <h1 class="sa-page-head__title">{{ msg.title }}</h1>
6
- <p v-if="subtitle" class="sa-page-head__sub">{{ subtitle }}</p>
7
- </div>
8
- <div class="sa-page-head__actions">
2
+ <AdminPage class="sa-promo-codes">
3
+ <AdminHero :title="msg.title" :subtitle="subtitle">
4
+ <template #actions>
9
5
  <slot name="head-actions">
10
- <q-btn
6
+ <button
11
7
  v-if="enableCreate"
12
- unelevated
13
- color="primary"
14
- icon="add"
15
- :label="resolvedCreateLabel"
8
+ class="sa-btn sa-btn--primary"
9
+ type="button"
16
10
  @click="showCreate = true"
17
- />
11
+ >
12
+ <q-icon name="add" size="16px" />
13
+ <span>{{ resolvedCreateLabel }}</span>
14
+ </button>
18
15
  </slot>
19
- </div>
20
- </header>
21
-
22
- <div class="sa-stats">
23
- <button
24
- v-for="tile in statTiles"
25
- :key="tile.id"
26
- class="sa-stat"
27
- :class="[
28
- tile.tone ? `sa-stat--${tile.tone}` : null,
29
- statusFilter === tile.id ? 'sa-stat--active' : null,
30
- ]"
31
- @click="onStatusTileClick(tile.id)"
32
- >
33
- <span class="sa-stat__num">{{ tile.count }}</span>
34
- <span class="sa-stat__label">{{ tile.label }}</span>
35
- <span v-if="tile.hint" class="sa-stat__hint">{{ tile.hint }}</span>
36
- </button>
37
- </div>
38
-
39
- <div class="sa-promo-codes__filter">
40
- <q-input
41
- v-model="filter.search"
42
- outlined
43
- dense
44
- :label="msg.list.searchLabel"
45
- clearable
46
- @update:model-value="reload"
47
- />
48
- <q-select
49
- v-model="filter.status"
50
- outlined
51
- dense
52
- emit-value
53
- map-options
54
- clearable
55
- :label="common.status"
56
- :options="statusOptions"
57
- @update:model-value="reload"
58
- />
59
- </div>
60
-
61
- <div class="sa-promo-codes__card">
62
- <q-table
63
- flat
64
- :rows="filteredRows"
65
- :columns="effectiveColumns"
66
- row-key="id"
67
- :pagination="{ rowsPerPage: 0 }"
68
- :loading="loading"
69
- hide-pagination
70
- >
71
- <template #body-cell-status="{ row }">
72
- <q-td>
73
- <q-badge :color="statusColor(row.status)" :label="row.status" />
74
- </q-td>
75
- </template>
76
- <template #body-cell-actions="{ row }">
77
- <q-td>
16
+ </template>
17
+ </AdminHero>
18
+
19
+ <AdminBody>
20
+ <AdminSection>
21
+ <AdminStatistics :label="msg.title">
22
+ <AdminKpi
23
+ v-for="tile in statTiles"
24
+ :key="tile.id"
25
+ :label="tile.label"
26
+ :value="tile.count"
27
+ :sub="tile.hint"
28
+ :tone="tile.tone"
29
+ :selected="statusFilter === tile.id"
30
+ :action="() => onStatusTileClick(tile.id)"
31
+ />
32
+ </AdminStatistics>
33
+ </AdminSection>
34
+
35
+ <AdminSection class="sa-promo-codes__card">
36
+ <AdminFilters class="q-mb-lg">
37
+ <q-input
38
+ v-model="filter.search"
39
+ outlined
40
+ dense
41
+ :label="msg.list.searchLabel"
42
+ clearable
43
+ />
44
+ <q-select
45
+ v-model="filter.status"
46
+ outlined
47
+ dense
48
+ emit-value
49
+ map-options
50
+ clearable
51
+ :label="common.status"
52
+ :options="statusOptions"
53
+ />
54
+ </AdminFilters>
55
+
56
+ <AdminTable
57
+ :rows="filteredRows"
58
+ :columns="effectiveColumns"
59
+ :loading="loading"
60
+ storage-key="promo-codes"
61
+ >
62
+ <template #body-cell-status="{ row }">
63
+ <q-td>
64
+ <q-badge :color="statusColor(row.status)" :label="row.status" />
65
+ </q-td>
66
+ </template>
67
+ <template #row-actions="{ row }">
78
68
  <slot name="row-actions" :row="row" :reload="reload">
79
69
  <q-btn
80
70
  v-for="action in visibleActions(row)"
@@ -87,10 +77,10 @@
87
77
  @click="action.handler(row)"
88
78
  />
89
79
  </slot>
90
- </q-td>
91
- </template>
92
- </q-table>
93
- </div>
80
+ </template>
81
+ </AdminTable>
82
+ </AdminSection>
83
+ </AdminBody>
94
84
 
95
85
  <PromoCodeCreateDialog
96
86
  v-if="enableCreate && submitCreate"
@@ -108,7 +98,7 @@
108
98
  :submit="submitEdit"
109
99
  @updated="onUpdated"
110
100
  />
111
- </div>
101
+ </AdminPage>
112
102
  </template>
113
103
 
114
104
  <script lang="ts">
@@ -116,6 +106,7 @@
116
106
  // `<script setup>` (the whole setup section is wrapped in setup()).
117
107
  // Pure helpers + constants therefore live here in the regular `<script>` block.
118
108
 
109
+ import AdminTable from '../components/admin-page/AdminTable.vue';
119
110
  import type { PromoCodePlanOption } from '../components/dialogs/types.js';
120
111
 
121
112
  const PLAN_COLOR_PALETTE = ['#0ea5e9', '#10b981', '#f59e0b', '#7c3aed', '#dc2626', '#64748b'];
@@ -137,6 +128,13 @@ export function computePlanColors(
137
128
 
138
129
  <script setup lang="ts">
139
130
  import { computed, onMounted, reactive, ref } from 'vue';
131
+ import AdminBody from '../components/admin-page/AdminBody.vue';
132
+ import AdminFilters from '../components/admin-page/AdminFilters.vue';
133
+ import AdminHero from '../components/admin-page/AdminHero.vue';
134
+ import AdminSection from '../components/admin-page/AdminSection.vue';
135
+ import AdminKpi from '../components/admin-page/AdminKpi.vue';
136
+ import AdminPage from '../components/admin-page/AdminPage.vue';
137
+ import AdminStatistics from '../components/admin-page/AdminStatistics.vue';
140
138
  import { useQuasar } from 'quasar';
141
139
  import { formatMessage } from '../client/i18n/format.js';
142
140
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
@@ -253,11 +251,25 @@ function classifyRow(row: PromoRow): Exclude<StatusFilter, 'all'> | null {
253
251
  return null;
254
252
  }
255
253
 
254
+ // The table narrows; the tiles above it deliberately do not — see statTiles.
256
255
  const filteredRows = computed(() => {
257
- if (statusFilter.value === 'all') return rows.value;
258
- return rows.value.filter((r) => classifyRow(r) === statusFilter.value);
256
+ // `clearable` emits null, not '' — see Quasar's use-field clearValue().
257
+ const q = (filter.search ?? '').trim().toLowerCase();
258
+ const status = filter.status?.toUpperCase() ?? null;
259
+
260
+ return rows.value.filter((row) => {
261
+ if (statusFilter.value !== 'all' && classifyRow(row) !== statusFilter.value) return false;
262
+ if (status && String(row.status ?? '').toUpperCase() !== status) return false;
263
+ if (!q) return true;
264
+ return [row.code, row.description, row.campaignTag]
265
+ .filter((v): v is string => typeof v === 'string')
266
+ .some((v) => v.toLowerCase().includes(q));
267
+ });
259
268
  });
260
269
 
270
+ // Absolute counts over everything the tenant has, deliberately independent of
271
+ // the filter below: these tiles are the status filter, so counting the filtered
272
+ // set would drop every unselected tile to zero the moment one is picked.
261
273
  const statTiles = computed<
262
274
  Array<{
263
275
  id: StatusFilter;
@@ -293,22 +305,8 @@ const statTiles = computed<
293
305
  ];
294
306
  });
295
307
 
296
- // Tile click: set the filter AND, if a server status match exists, pass it
297
- // through to the search so `loadPromos` can pre-filter server-side if needed.
298
308
  function onStatusTileClick(id: StatusFilter): void {
299
309
  statusFilter.value = id;
300
- const serverStatus =
301
- id === 'active'
302
- ? 'ACTIVE'
303
- : id === 'paused'
304
- ? 'PAUSED'
305
- : id === 'expired'
306
- ? 'EXPIRED'
307
- : null;
308
- if (serverStatus !== filter.status) {
309
- filter.status = serverStatus;
310
- reload();
311
- }
312
310
  }
313
311
 
314
312
  const baseColumns = computed(() => [
@@ -396,14 +394,6 @@ const mergedActions = computed<readonly PromoRowAction[]>(() => [
396
394
 
397
395
  const effectiveColumns = computed(() => {
398
396
  const cols = [...baseColumns.value];
399
- if (mergedActions.value.length > 0) {
400
- cols.push({
401
- name: 'actions',
402
- label: '',
403
- field: 'id' as never,
404
- align: 'right' as 'left',
405
- });
406
- }
407
397
  return cols;
408
398
  });
409
399
 
@@ -421,10 +411,9 @@ function statusColor(status: string): string {
421
411
  async function reload() {
422
412
  loading.value = true;
423
413
  try {
424
- rows.value = await props.loadPromos({
425
- search: filter.search || undefined,
426
- status: filter.status || undefined,
427
- });
414
+ // Unfiltered on purpose: the tiles count the tenant's full set, and
415
+ // the table narrows locally, so filtering costs no round trip.
416
+ rows.value = await props.loadPromos({});
428
417
  } catch (err) {
429
418
  rows.value = [];
430
419
  console.warn('[PromoCodesPage] loadPromos failed:', err);
@@ -561,26 +550,4 @@ function formatDate(iso: string | Date | null | undefined): string | null {
561
550
  }
562
551
  </script>
563
552
 
564
- <style scoped>
565
- .sa-promo-codes {
566
- min-height: calc(100vh - 56px);
567
- background: var(--sa-bg-app, #f1f5f9);
568
- padding: 20px 28px 28px;
569
- }
570
- .sa-promo-codes__filter {
571
- display: flex;
572
- gap: 10px;
573
- margin-bottom: 12px;
574
- flex-wrap: wrap;
575
- }
576
- .sa-promo-codes__filter > * {
577
- flex: 1;
578
- min-width: 200px;
579
- }
580
- .sa-promo-codes__card {
581
- background: #fff;
582
- border: 1px solid var(--sa-border, #e2e8f0);
583
- border-radius: 12px;
584
- overflow: hidden;
585
- }
586
- </style>
553
+ <style scoped></style>
@@ -1,31 +1,31 @@
1
1
  <template>
2
- <div class="sa-subscriptions">
3
- <header class="sa-subscriptions__head">
4
- <div>
5
- <h1 class="sa-subscriptions__title">{{ msg.subscriptions.title }}</h1>
6
- <p v-if="subtitle" class="sa-subscriptions__sub">{{ subtitle }}</p>
7
- </div>
8
- <slot name="head-actions" />
9
- </header>
2
+ <AdminPage class="sa-subscriptions">
3
+ <AdminHero :title="msg.subscriptions.title" :subtitle="subtitle">
4
+ <template #actions>
5
+ <slot name="head-actions" />
6
+ </template>
7
+ </AdminHero>
10
8
 
11
- <div class="sa-subscriptions__body">
12
- <div class="sa-subscriptions__card">
13
- <q-table
14
- flat
9
+ <AdminBody>
10
+ <AdminSection class="sa-subscriptions__body">
11
+ <AdminTable
15
12
  :rows="rows"
16
13
  :columns="effectiveColumns"
17
- row-key="id"
18
- :pagination="{ rowsPerPage: 0 }"
19
14
  :loading="loading"
20
- hide-pagination
15
+ storage-key="subscriptions"
21
16
  />
22
- </div>
23
- </div>
24
- </div>
17
+ </AdminSection>
18
+ </AdminBody>
19
+ </AdminPage>
25
20
  </template>
26
21
 
27
22
  <script setup lang="ts">
23
+ import AdminTable from '../components/admin-page/AdminTable.vue';
28
24
  import { computed, onMounted, ref } from 'vue';
25
+ import AdminBody from '../components/admin-page/AdminBody.vue';
26
+ import AdminHero from '../components/admin-page/AdminHero.vue';
27
+ import AdminSection from '../components/admin-page/AdminSection.vue';
28
+ import AdminPage from '../components/admin-page/AdminPage.vue';
29
29
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
30
30
 
31
31
  // Platform standard page: subscriptions (minimal). Apps pass
@@ -128,37 +128,4 @@ function formatDate(iso: string | null | undefined): string | null {
128
128
  }
129
129
  </script>
130
130
 
131
- <style scoped>
132
- .sa-subscriptions {
133
- min-height: calc(100vh - 56px);
134
- background: var(--sa-bg-app, #f1f5f9);
135
- }
136
- .sa-subscriptions__head {
137
- padding: 20px 28px 8px;
138
- display: flex;
139
- align-items: flex-start;
140
- justify-content: space-between;
141
- gap: 16px;
142
- }
143
- .sa-subscriptions__title {
144
- margin: 0;
145
- font-family: var(--sa-font-head, system-ui, sans-serif);
146
- font-weight: 700;
147
- font-size: 22px;
148
- color: var(--sa-heading, #0f172a);
149
- }
150
- .sa-subscriptions__sub {
151
- margin: 4px 0 0;
152
- color: var(--sa-muted-dark, #475569);
153
- font-size: 13.5px;
154
- }
155
- .sa-subscriptions__body {
156
- padding: 12px 28px 28px;
157
- }
158
- .sa-subscriptions__card {
159
- background: #fff;
160
- border: 1px solid var(--sa-border, #e2e8f0);
161
- border-radius: 12px;
162
- overflow: hidden;
163
- }
164
- </style>
131
+ <style scoped></style>
@@ -24,7 +24,7 @@
24
24
  <h1 class="sa-login-title">{{ msg.login.signIn }}</h1>
25
25
  <p v-if="subtitle" class="sa-login-subtitle">{{ subtitle }}</p>
26
26
 
27
- <q-form @submit.prevent="handleSubmit" class="sa-login-form">
27
+ <q-form class="sa-login-form" @submit.prevent="handleSubmit">
28
28
  <q-input
29
29
  v-model="form.email"
30
30
  :label="msg.emailLabel"
@@ -111,7 +111,7 @@ interface Props {
111
111
  subtitle?: string;
112
112
  }
113
113
 
114
- const props = defineProps<Props>();
114
+ defineProps<Props>();
115
115
 
116
116
  const router = useRouter();
117
117
  const msg = useSaMessages('shell');
@@ -199,7 +199,7 @@ async function handleSubmit(): Promise<void> {
199
199
  on top of the centered card on short viewports. */
200
200
  margin-left: auto;
201
201
  align-self: flex-start;
202
- color: var(--sa-login-tag-color, #475569);
202
+ color: var(--sa-login-tag-color, var(--sa-muted-dark));
203
203
  }
204
204
  .sa-login-wrap {
205
205
  min-height: 100vh;
@@ -208,7 +208,7 @@ async function handleSubmit(): Promise<void> {
208
208
  align-items: center;
209
209
  justify-content: center;
210
210
  padding: 24px;
211
- background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
211
+ background: linear-gradient(180deg, var(--sa-heading) 0%, #1e293b 100%);
212
212
  }
213
213
  .sa-login-card {
214
214
  width: 420px;
@@ -234,7 +234,7 @@ async function handleSubmit(): Promise<void> {
234
234
  overflow: hidden;
235
235
  }
236
236
  .sa-login-logo--text {
237
- background: var(--sa-login-logo-bg, linear-gradient(135deg, #475569, #1e293b));
237
+ background: var(--sa-login-logo-bg, linear-gradient(135deg, var(--sa-muted-dark), #1e293b));
238
238
  color: var(--sa-login-logo-color, #fff);
239
239
  font-weight: 800;
240
240
  font-size: 18px;
@@ -248,12 +248,12 @@ async function handleSubmit(): Promise<void> {
248
248
  .sa-login-brand__name {
249
249
  font-weight: 800;
250
250
  font-size: 16px;
251
- color: #0f172a;
251
+ color: var(--sa-heading);
252
252
  line-height: 1.1;
253
253
  }
254
254
  .sa-login-brand__tag {
255
255
  font-size: 12px;
256
- color: var(--sa-login-tag-color, #475569);
256
+ color: var(--sa-login-tag-color, var(--sa-muted-dark));
257
257
  text-transform: uppercase;
258
258
  letter-spacing: 0.08em;
259
259
  }
@@ -261,12 +261,12 @@ async function handleSubmit(): Promise<void> {
261
261
  .sa-login-title {
262
262
  font-size: 22px;
263
263
  font-weight: 800;
264
- color: #0f172a;
264
+ color: var(--sa-heading);
265
265
  margin: 0 0 6px;
266
266
  }
267
267
  .sa-login-subtitle {
268
268
  font-size: 13px;
269
- color: #475569;
269
+ color: var(--sa-muted-dark);
270
270
  margin: 0 0 20px;
271
271
  line-height: 1.5;
272
272
  }
@@ -290,13 +290,13 @@ async function handleSubmit(): Promise<void> {
290
290
  .sa-login-hint {
291
291
  margin-top: 18px;
292
292
  padding: 10px 12px;
293
- background: #f1f5f9;
293
+ background: var(--sa-border-soft);
294
294
  border-radius: 8px;
295
295
  font-size: 12px;
296
- color: #475569;
296
+ color: var(--sa-muted-dark);
297
297
  }
298
298
  .sa-login-hint code {
299
- background: #e2e8f0;
299
+ background: var(--sa-border);
300
300
  padding: 1px 5px;
301
301
  border-radius: 4px;
302
302
  }
@@ -4,7 +4,7 @@
4
4
  <div class="sa-setup-head">
5
5
  <div class="sa-setup-badge">{{ iconText }}</div>
6
6
  <div>
7
- <div class="sa-setup-title">{{ msg.setup.title }}</div>
7
+ <h1 class="sa-setup-title">{{ msg.setup.title }}</h1>
8
8
  <div class="sa-setup-sub">{{ subtitleText }}</div>
9
9
  </div>
10
10
  <LocaleSwitcher class="sa-setup-locale" />
@@ -15,7 +15,7 @@
15
15
  </q-banner>
16
16
 
17
17
  <!-- Step 1: create SuperAdmin -->
18
- <q-form v-if="step === 'form'" @submit.prevent="submitCreate" class="sa-setup-form">
18
+ <q-form v-if="step === 'form'" class="sa-setup-form" @submit.prevent="submitCreate">
19
19
  <p class="sa-setup-hint">
20
20
  {{ msg.setup.tokenHintBefore }}
21
21
  <code>SETUP_TOKEN</code>
@@ -104,7 +104,7 @@
104
104
  <code class="sa-setup-uri__value">{{ result.otpauthUri }}</code>
105
105
  </details>
106
106
 
107
- <q-form @submit.prevent="submitConfirm" class="sa-setup-form q-mt-md">
107
+ <q-form class="sa-setup-form q-mt-md" @submit.prevent="submitConfirm">
108
108
  <q-input
109
109
  v-model="mfaCode"
110
110
  :label="msg.setup.codeLabel"
@@ -216,9 +216,12 @@ async function postJson<T>(path: string, body: unknown): Promise<T> {
216
216
  return await httpPostJson<T>(http, `${endpoints.apiBase}${path}`, body);
217
217
  } catch (err) {
218
218
  if (err instanceof HttpJsonError) {
219
+ // The message is translated for the user; `cause` keeps the status
220
+ // and the machine-readable code reachable for anything upstream.
219
221
  throw new Error(
220
222
  (err.code && errorByCode.value[err.code]) ||
221
223
  formatMessage(msg.value.setup.errorHttp, { status: err.status }),
224
+ { cause: err },
222
225
  );
223
226
  }
224
227
  throw err;
@@ -274,7 +277,7 @@ async function submitConfirm(): Promise<void> {
274
277
  align-items: center;
275
278
  justify-content: center;
276
279
  padding: 24px;
277
- background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
280
+ background: linear-gradient(180deg, var(--sa-heading) 0%, #1e293b 100%);
278
281
  }
279
282
  .sa-setup-card {
280
283
  width: 460px;
@@ -293,7 +296,7 @@ async function submitConfirm(): Promise<void> {
293
296
  .sa-setup-locale {
294
297
  margin-left: auto;
295
298
  align-self: flex-start;
296
- color: #475569;
299
+ color: var(--sa-muted-dark);
297
300
  }
298
301
  .sa-setup-badge {
299
302
  width: 44px;
@@ -302,30 +305,31 @@ async function submitConfirm(): Promise<void> {
302
305
  display: flex;
303
306
  align-items: center;
304
307
  justify-content: center;
305
- background: linear-gradient(135deg, #475569, #1e293b);
308
+ background: linear-gradient(135deg, var(--sa-muted-dark), #1e293b);
306
309
  color: #fff;
307
310
  font-weight: 800;
308
311
  font-size: 18px;
309
312
  text-transform: uppercase;
310
313
  }
311
314
  .sa-setup-title {
315
+ margin: 0;
312
316
  font-weight: 800;
313
317
  font-size: 18px;
314
- color: #0f172a;
318
+ color: var(--sa-heading);
315
319
  line-height: 1.1;
316
320
  }
317
321
  .sa-setup-sub {
318
322
  font-size: 12px;
319
- color: #475569;
323
+ color: var(--sa-muted-dark);
320
324
  }
321
325
  .sa-setup-hint {
322
326
  font-size: 13px;
323
- color: #475569;
327
+ color: var(--sa-muted-dark);
324
328
  line-height: 1.5;
325
329
  margin: 0 0 16px;
326
330
  }
327
331
  .sa-setup-hint code {
328
- background: #e2e8f0;
332
+ background: var(--sa-border);
329
333
  padding: 1px 5px;
330
334
  border-radius: 4px;
331
335
  }
@@ -353,17 +357,17 @@ async function submitConfirm(): Promise<void> {
353
357
  margin-bottom: 12px;
354
358
  }
355
359
  .sa-setup-qr img {
356
- border: 1px solid #e2e8f0;
360
+ border: 1px solid var(--sa-border);
357
361
  border-radius: 8px;
358
362
  padding: 8px;
359
363
  background: #fff;
360
364
  }
361
365
  .sa-setup-qr__hint {
362
366
  font-size: 12px;
363
- color: #64748b;
367
+ color: var(--sa-muted);
364
368
  }
365
369
  .sa-setup-secret {
366
- background: #f1f5f9;
370
+ background: var(--sa-border-soft);
367
371
  border-radius: 8px;
368
372
  padding: 10px 12px;
369
373
  margin-bottom: 10px;
@@ -372,27 +376,27 @@ async function submitConfirm(): Promise<void> {
372
376
  font-size: 11px;
373
377
  text-transform: uppercase;
374
378
  letter-spacing: 0.06em;
375
- color: #64748b;
379
+ color: var(--sa-muted);
376
380
  margin-bottom: 4px;
377
381
  }
378
382
  .sa-setup-secret__value {
379
383
  font-size: 15px;
380
384
  font-weight: 700;
381
- color: #0f172a;
385
+ color: var(--sa-heading);
382
386
  word-break: break-all;
383
387
  letter-spacing: 0.04em;
384
388
  }
385
389
  .sa-setup-uri {
386
390
  margin: 4px 0 4px;
387
391
  font-size: 12px;
388
- color: #475569;
392
+ color: var(--sa-muted-dark);
389
393
  }
390
394
  .sa-setup-uri__value {
391
395
  display: block;
392
396
  margin-top: 6px;
393
397
  word-break: break-all;
394
398
  color: #334155;
395
- background: #f8fafc;
399
+ background: var(--sa-bg-surface-2);
396
400
  padding: 6px 8px;
397
401
  border-radius: 6px;
398
402
  }
@@ -401,7 +405,7 @@ async function submitConfirm(): Promise<void> {
401
405
  width: 100%;
402
406
  background: none;
403
407
  border: none;
404
- color: #64748b;
408
+ color: var(--sa-muted);
405
409
  font-size: 12px;
406
410
  cursor: pointer;
407
411
  text-decoration: underline;