@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,85 +1,66 @@
1
1
  <template>
2
- <div class="sa-users">
3
- <header class="sa-page-head">
4
- <div>
5
- <h1 class="sa-page-head__title">{{ resolvedTitle }}</h1>
6
- <p v-if="subtitle" class="sa-page-head__sub">{{ subtitle }}</p>
7
- </div>
8
- <div class="sa-page-head__actions">
9
- <q-btn
10
- unelevated
11
- color="primary"
12
- icon="search"
13
- :label="common.search"
14
- @click="reload"
15
- />
16
- </div>
17
- </header>
18
-
19
- <div class="sa-stats">
20
- <button
21
- v-for="tile in statTiles"
22
- :key="tile.id"
23
- class="sa-stat"
24
- :class="[
25
- tile.tone ? `sa-stat--${tile.tone}` : null,
26
- statusFilter === tile.id ? 'sa-stat--active' : null,
27
- ]"
28
- @click="statusFilter = tile.id"
29
- >
30
- <span class="sa-stat__num">{{ tile.count }}</span>
31
- <span class="sa-stat__label">{{ tile.label }}</span>
32
- </button>
33
- </div>
34
-
35
- <div class="sa-users__filter">
36
- <q-input
37
- v-model="filter.q"
38
- outlined
39
- dense
40
- :label="msg.filterQuery"
41
- clearable
42
- @keyup.enter="reload"
43
- @clear="reload"
44
- />
45
- <q-input
46
- v-model="filter.tenant"
47
- outlined
48
- dense
49
- :label="msg.filterTenant"
50
- clearable
51
- @keyup.enter="reload"
52
- @clear="reload"
53
- />
54
- <slot name="filters-extra" />
55
- </div>
56
-
57
- <div class="sa-users__card">
58
- <q-table
59
- flat
60
- :rows="filteredRows"
61
- :columns="effectiveColumns"
62
- row-key="id"
63
- :pagination="{ rowsPerPage: 0 }"
64
- :loading="loading"
65
- hide-pagination
66
- >
67
- <template #body-cell-status="{ row }">
68
- <q-td>
69
- <q-badge
70
- :color="row.isActive ? 'positive' : 'grey'"
71
- :label="row.isActive ? msg.statusActive : msg.statusDeactivated"
72
- />
73
- <q-badge
74
- v-if="row.invitationStatus === 'PENDING'"
75
- color="amber-7"
76
- :label="msg.badgePending"
77
- class="q-ml-xs"
78
- />
79
- </q-td>
80
- </template>
81
- <template #body-cell-actions="{ row }">
82
- <q-td>
2
+ <AdminPage class="sa-users">
3
+ <AdminHero :title="resolvedTitle" :subtitle="subtitle" />
4
+
5
+ <AdminBody>
6
+ <AdminSection>
7
+ <AdminStatistics :label="resolvedTitle">
8
+ <AdminKpi
9
+ v-for="tile in statTiles"
10
+ :key="tile.id"
11
+ :label="tile.label"
12
+ :value="tile.count"
13
+ :tone="tile.tone"
14
+ :selected="statusFilter === tile.id"
15
+ :action="() => (statusFilter = tile.id)"
16
+ />
17
+ </AdminStatistics>
18
+ </AdminSection>
19
+
20
+ <AdminSection :title="common.results" class="sa-users__card">
21
+ <AdminFilters class="q-mb-lg">
22
+ <q-input
23
+ v-model="filter.q"
24
+ outlined
25
+ dense
26
+ :label="msg.filterQuery"
27
+ clearable
28
+ debounce="250"
29
+ @update:model-value="reload"
30
+ />
31
+ <q-input
32
+ v-model="filter.tenant"
33
+ outlined
34
+ dense
35
+ :label="msg.filterTenant"
36
+ clearable
37
+ debounce="250"
38
+ @update:model-value="reload"
39
+ />
40
+ <slot name="filters-extra" />
41
+ </AdminFilters>
42
+
43
+ <AdminTable
44
+ :rows="filteredRows"
45
+ :columns="effectiveColumns"
46
+ :loading="loading"
47
+ storage-key="users"
48
+ >
49
+ <template #body-cell-status="{ row }">
50
+ <q-td>
51
+ <q-badge
52
+ :color="row.isActive ? 'positive' : 'grey'"
53
+ :label="row.isActive ? msg.statusActive : msg.statusDeactivated"
54
+ />
55
+ <q-badge
56
+ v-if="row.invitationStatus === 'PENDING'"
57
+ color="amber-7"
58
+ :label="msg.badgePending"
59
+ class="q-ml-xs"
60
+ />
61
+ </q-td>
62
+ </template>
63
+ <template #row-actions="{ row }">
83
64
  <slot name="row-actions" :row="row">
84
65
  <q-btn
85
66
  v-for="action in visibleActions(row)"
@@ -92,27 +73,36 @@
92
73
  @click="action.handler(row)"
93
74
  />
94
75
  </slot>
95
- </q-td>
96
- </template>
97
- </q-table>
98
- </div>
76
+ </template>
77
+ </AdminTable>
78
+ </AdminSection>
79
+ </AdminBody>
99
80
 
100
81
  <MfaPromptDialog
101
82
  v-if="needsMfaDialog"
102
- :model-value="showMfa"
103
- :description="mfaDescription"
104
- :error="mfaError"
83
+ :model-value="mfa.show.value"
84
+ :description="mfa.description.value"
85
+ :error="mfa.error.value"
105
86
  :setup-hint="mfaSetupHint"
106
- @update:model-value="onMfaDialogVisibility"
107
- @confirm="onMfaConfirm"
87
+ @update:model-value="mfa.onVisibility"
88
+ @confirm="mfa.onConfirm"
108
89
  />
109
- </div>
90
+ </AdminPage>
110
91
  </template>
111
92
 
112
93
  <script setup lang="ts">
94
+ import AdminTable from '../components/admin-page/AdminTable.vue';
113
95
  import { computed, onMounted, reactive, ref } from 'vue';
96
+ import { useMfaPrompt } from '../vue/use-mfa-prompt.js';
114
97
  import { useQuasar } from 'quasar';
115
98
  import { useSuperAdminNotify } from '../quasar/notify.js';
99
+ import AdminBody from '../components/admin-page/AdminBody.vue';
100
+ import AdminFilters from '../components/admin-page/AdminFilters.vue';
101
+ import AdminHero from '../components/admin-page/AdminHero.vue';
102
+ import AdminKpi from '../components/admin-page/AdminKpi.vue';
103
+ import AdminPage from '../components/admin-page/AdminPage.vue';
104
+ import AdminSection from '../components/admin-page/AdminSection.vue';
105
+ import AdminStatistics from '../components/admin-page/AdminStatistics.vue';
116
106
  import MfaPromptDialog from '../components/MfaPromptDialog.vue';
117
107
  import { formatMessage } from '../client/i18n/format.js';
118
108
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
@@ -191,40 +181,13 @@ const rows = ref<UserRow[]>([]);
191
181
  const loading = ref(false);
192
182
  const filter = reactive({ q: '', tenant: '' });
193
183
 
194
- // MFA dialog state for per-flow MFA (reset/deactivate). Promise-resolver
195
- // pattern analogous to PilotsPage: the action handler calls `promptMfa` and
196
- // waits for `onMfaConfirm` (or cancellation via `update:modelValue=false`).
197
- const showMfa = ref(false);
198
- const mfaError = ref('');
199
- const mfaDescription = ref('');
200
- let pendingMfaResolve: ((code: string | null) => void) | null = null;
184
+ // Per-flow MFA for reset/deactivate: the action handler awaits `mfa.prompt`,
185
+ // which resolves with the code or with null when the user closes the dialog.
186
+ const mfa = useMfaPrompt();
201
187
  const needsMfaDialog = computed(
202
188
  () => props.requireMfaForResetPassword || props.requireMfaForDeactivate,
203
189
  );
204
190
 
205
- function promptMfa(description: string): Promise<string | null> {
206
- return new Promise((resolve) => {
207
- mfaDescription.value = description;
208
- mfaError.value = '';
209
- showMfa.value = true;
210
- pendingMfaResolve = (code) => {
211
- pendingMfaResolve = null;
212
- resolve(code);
213
- };
214
- });
215
- }
216
-
217
- function onMfaConfirm(code: string): void {
218
- pendingMfaResolve?.(code);
219
- }
220
-
221
- function onMfaDialogVisibility(open: boolean): void {
222
- showMfa.value = open;
223
- if (!open && pendingMfaResolve) {
224
- pendingMfaResolve(null);
225
- }
226
- }
227
-
228
191
  // Stat-pill filter (analogous to plan simulation users.jsx):
229
192
  // all | active | blocked | never-logged-in | super-admin.
230
193
  type StatusFilter = 'all' | 'active' | 'blocked' | 'never' | 'super';
@@ -346,14 +309,6 @@ const mergedActions = computed<readonly UserRowAction[]>(() => [
346
309
 
347
310
  const effectiveColumns = computed(() => {
348
311
  const cols = [...baseColumns.value];
349
- if (mergedActions.value.length > 0) {
350
- cols.push({
351
- name: 'actions',
352
- label: '',
353
- field: 'id' as never,
354
- align: 'right' as 'left',
355
- });
356
- }
357
312
  return cols;
358
313
  });
359
314
 
@@ -407,20 +362,20 @@ async function runAction<R>(
407
362
  return;
408
363
  }
409
364
  while (true) {
410
- const code = await promptMfa(actionLabel);
365
+ const code = await mfa.prompt(actionLabel);
411
366
  if (code === null) return;
412
367
  try {
413
368
  const result = await invoke(code);
414
- showMfa.value = false;
369
+ mfa.show.value = false;
415
370
  onSuccess(result);
416
371
  return;
417
372
  } catch (err) {
418
373
  const status = (err as { response?: { status?: number } })?.response?.status;
419
374
  if (status === 401) {
420
- mfaError.value = shell.value.mfa.invalidCode;
375
+ mfa.error.value = shell.value.mfa.invalidCode;
421
376
  continue;
422
377
  }
423
- showMfa.value = false;
378
+ mfa.show.value = false;
424
379
  notify('negative', errMsg(err));
425
380
  return;
426
381
  }
@@ -496,27 +451,4 @@ function onDeactivateClick(row: UserRow): void {
496
451
  }
497
452
  </script>
498
453
 
499
- <style scoped>
500
- .sa-users {
501
- min-height: calc(100vh - 56px);
502
- background: var(--sa-bg-app, #f1f5f9);
503
- padding: 20px 28px 28px;
504
- }
505
- .sa-users__filter {
506
- display: flex;
507
- gap: 10px;
508
- margin-bottom: 12px;
509
- align-items: center;
510
- flex-wrap: wrap;
511
- }
512
- .sa-users__filter > * {
513
- flex: 1;
514
- min-width: 200px;
515
- }
516
- .sa-users__card {
517
- background: #fff;
518
- border: 1px solid var(--sa-border, #e2e8f0);
519
- border-radius: 12px;
520
- overflow: hidden;
521
- }
522
- </style>
454
+ <style scoped></style>
@@ -12,13 +12,16 @@
12
12
  <div class="sa-bd-card__titlerow">
13
13
  <span class="sa-bd-card__key">{{ bundle.bundleKey }}</span>
14
14
  <span
15
- class="sa-chip"
16
- :class="`sa-chip--${statusClass(bundle)}`"
15
+ class="sa-bundle-chip"
16
+ :class="`sa-bundle-chip--${statusClass(bundle)}`"
17
17
  :title="statusTooltip(bundle)"
18
18
  >
19
19
  {{ statusLabel(bundle) }}
20
20
  </span>
21
- <span v-if="i18nLocaleCount(bundle) > 0" class="sa-chip sa-chip--info">
21
+ <span
22
+ v-if="i18nLocaleCount(bundle) > 0"
23
+ class="sa-bundle-chip sa-bundle-chip--info"
24
+ >
22
25
  {{ translationCount(bundle) }}
23
26
  </span>
24
27
  </div>
@@ -93,34 +96,34 @@ function translationCount(bundle: BundleRow): string {
93
96
 
94
97
  <style scoped>
95
98
  /* Status chips co-located with the markup (analogous to PlanList .pl-chip*).
96
- Deliberately scoped: the identically named global .sa-chip rule in
97
- DiscoveryPage.vue would otherwise override .sa-chip--live (same specificity,
99
+ Deliberately scoped: the identically named global .sa-bundle-chip rule in
100
+ DiscoveryPage.vue would otherwise override .sa-bundle-chip--live (same specificity,
98
101
  later source order) and render the "Live" tag grey instead of green. */
99
- .sa-chip {
102
+ .sa-bundle-chip {
100
103
  font-size: 10px;
101
104
  font-weight: 600;
102
105
  padding: 2px 7px;
103
106
  border-radius: 6px;
104
- background: #f1f5f9;
105
- color: #475569;
107
+ background: var(--sa-border-soft);
108
+ color: var(--sa-muted-dark);
106
109
  }
107
- .sa-chip--info {
108
- background: #eff6ff;
109
- color: #1d4ed8;
110
+ .sa-bundle-chip--info {
111
+ background: var(--sa-primary-50);
112
+ color: var(--sa-primary-strong);
110
113
  }
111
- .sa-chip--live {
114
+ .sa-bundle-chip--live {
112
115
  background: #dcfce7;
113
116
  color: #166534;
114
117
  }
115
- .sa-chip--draft {
118
+ .sa-bundle-chip--draft {
116
119
  background: #fef3c7;
117
120
  color: #92400e;
118
121
  }
119
- .sa-chip--supersed {
120
- background: #e2e8f0;
121
- color: #64748b;
122
+ .sa-bundle-chip--supersed {
123
+ background: var(--sa-border);
124
+ color: var(--sa-muted);
122
125
  }
123
- .sa-chip--scheduled {
126
+ .sa-bundle-chip--scheduled {
124
127
  background: #fef3c7;
125
128
  color: #92400e;
126
129
  }
@@ -1,31 +1,22 @@
1
1
  <template>
2
- <div class="sa-bundles__kpis">
3
- <div class="sa-bundles__kpi">
4
- <div class="sa-bundles__kpi-label">{{ msg.kpis.total }}</div>
5
- <div class="sa-bundles__kpi-value">{{ bundlesTotal }}</div>
6
- <div class="sa-bundles__kpi-sub">{{ totalSub }}</div>
7
- </div>
8
- <div class="sa-bundles__kpi">
9
- <div class="sa-bundles__kpi-label">{{ msg.kpis.scheduled }}</div>
10
- <div class="sa-bundles__kpi-value">{{ totalScheduledVersions }}</div>
11
- <div class="sa-bundles__kpi-sub">{{ msg.kpis.scheduledSub }}</div>
12
- </div>
13
- <div class="sa-bundles__kpi">
14
- <div class="sa-bundles__kpi-label">{{ msg.kpis.drafts }}</div>
15
- <div class="sa-bundles__kpi-value">{{ totalDraftVersions }}</div>
16
- <div class="sa-bundles__kpi-sub">{{ draftsSub }}</div>
17
- </div>
18
- <div class="sa-bundles__kpi">
19
- <div class="sa-bundles__kpi-label">{{ msg.kpis.translated }}</div>
20
- <div class="sa-bundles__kpi-value">{{ translatedCount }}</div>
21
- <div class="sa-bundles__kpi-sub">{{ translatedSub }}</div>
22
- </div>
23
- </div>
2
+ <AdminStatistics :columns="4">
3
+ <AdminKpi :label="msg.kpis.total" :value="bundlesTotal" :sub="totalSub" />
4
+ <AdminKpi
5
+ :label="msg.kpis.scheduled"
6
+ :value="totalScheduledVersions"
7
+ :sub="msg.kpis.scheduledSub"
8
+ />
9
+ <AdminKpi :label="msg.kpis.drafts" :value="totalDraftVersions" :sub="draftsSub" />
10
+ <AdminKpi :label="msg.kpis.translated" :value="translatedCount" :sub="translatedSub" />
11
+ </AdminStatistics>
24
12
  </template>
25
13
 
26
14
  <script setup lang="ts">
27
15
  import { computed } from 'vue';
28
16
 
17
+ import AdminKpi from '../../components/admin-page/AdminKpi.vue';
18
+ import AdminStatistics from '../../components/admin-page/AdminStatistics.vue';
19
+
29
20
  import { formatMessage } from '../../client/i18n/format.js';
30
21
  import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
31
22
 
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <div class="sa-bundles__toolbar">
3
+ <q-select
4
+ v-if="locales.length > 1"
5
+ :model-value="displayLocale"
6
+ :options="localeOptions"
7
+ outlined
8
+ dense
9
+ emit-value
10
+ map-options
11
+ class="sa-bundles__locale"
12
+ :label="msg.header.displayLocale"
13
+ @update:model-value="(value) => emit('update:displayLocale', String(value))"
14
+ >
15
+ <template #prepend><q-icon name="translate" size="18px" /></template>
16
+ </q-select>
17
+ <button class="sa-btn sa-btn--primary" type="button" @click="emit('create')">
18
+ <q-icon name="add" size="16px" />
19
+ <span>{{ msg.header.newBundle }}</span>
20
+ </button>
21
+ <AdminRefreshBtn :loading="loading" @refresh="emit('refresh')" />
22
+ </div>
23
+ </template>
24
+
25
+ <script setup lang="ts">
26
+ import { computed } from 'vue';
27
+
28
+ import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
29
+ import AdminRefreshBtn from '../../components/admin-page/AdminRefreshBtn.vue';
30
+
31
+ // The language selector controls which locale feature/quota labels are resolved
32
+ // on the create form and in the detail view (fallback locale → DE → key). The default is the
33
+ // default locale (DE); further options come from the active project locales.
34
+
35
+ const props = defineProps<{
36
+ loading: boolean;
37
+ displayLocale: string;
38
+ locales: string[];
39
+ }>();
40
+
41
+ const emit = defineEmits<{
42
+ create: [];
43
+ refresh: [];
44
+ 'update:displayLocale': [locale: string];
45
+ }>();
46
+
47
+ const msg = useSaMessages('bundles');
48
+
49
+ const localeOptions = computed(() =>
50
+ props.locales.map((locale) => ({ label: locale.toUpperCase(), value: locale })),
51
+ );
52
+ </script>
53
+
54
+ <style scoped>
55
+ .sa-bundles__toolbar {
56
+ display: flex;
57
+ align-items: center;
58
+ gap: 12px;
59
+ }
60
+ .sa-bundles__locale {
61
+ min-width: 150px;
62
+ }
63
+ </style>
@@ -171,13 +171,13 @@ function copyFromDefault(locale: string): void {
171
171
  gap: 12px;
172
172
  }
173
173
  .sa-trans-lang {
174
- border: 1px solid #e2e8f0;
174
+ border: 1px solid var(--sa-border);
175
175
  border-radius: 10px;
176
176
  background: #fff;
177
177
  padding: 10px 12px;
178
178
  }
179
179
  .sa-trans-lang--source {
180
- background: #f8fafc;
180
+ background: var(--sa-bg-surface-2);
181
181
  border-color: #cbd5e1;
182
182
  }
183
183
  .sa-trans-lang__head {
@@ -189,7 +189,7 @@ function copyFromDefault(locale: string): void {
189
189
  .sa-trans-lang__code {
190
190
  font-size: 10px;
191
191
  font-weight: 700;
192
- background: #0f172a;
192
+ background: var(--sa-heading);
193
193
  color: #fff;
194
194
  padding: 2px 6px;
195
195
  border-radius: 5px;
@@ -205,13 +205,13 @@ function copyFromDefault(locale: string): void {
205
205
  font-weight: 700;
206
206
  text-transform: uppercase;
207
207
  letter-spacing: 0.5px;
208
- color: #475569;
209
- background: #e2e8f0;
208
+ color: var(--sa-muted-dark);
209
+ background: var(--sa-border);
210
210
  padding: 2px 6px;
211
211
  border-radius: 5px;
212
212
  }
213
213
  .sa-trans-lang__copy {
214
- color: #475569;
214
+ color: var(--sa-muted-dark);
215
215
  }
216
216
  .sa-trans-fields {
217
217
  display: flex;
@@ -228,12 +228,12 @@ function copyFromDefault(locale: string): void {
228
228
  font-weight: 600;
229
229
  text-transform: uppercase;
230
230
  letter-spacing: 0.3px;
231
- color: #94a3b8;
231
+ color: var(--sa-muted-light);
232
232
  }
233
233
  .sa-trans-locked {
234
234
  font-size: 13px;
235
235
  color: #334155;
236
- background: #f1f5f9;
236
+ background: var(--sa-border-soft);
237
237
  border: 1px dashed #cbd5e1;
238
238
  border-radius: 6px;
239
239
  padding: 6px 10px;
@@ -243,7 +243,7 @@ function copyFromDefault(locale: string): void {
243
243
  }
244
244
  .sa-trans-locked__hint {
245
245
  font-size: 10px;
246
- color: #94a3b8;
246
+ color: var(--sa-muted-light);
247
247
  text-transform: uppercase;
248
248
  }
249
249
  .sa-trans__empty {
@@ -251,7 +251,7 @@ function copyFromDefault(locale: string): void {
251
251
  padding: 16px;
252
252
  text-align: center;
253
253
  font-size: 12px;
254
- color: #94a3b8;
254
+ color: var(--sa-muted-light);
255
255
  border: 1px dashed #cbd5e1;
256
256
  border-radius: 10px;
257
257
  }
@@ -91,7 +91,7 @@ function isNew(cap: CapabilityCatalogEntryRow): boolean {
91
91
  gap: 10px;
92
92
  align-items: flex-start;
93
93
  background: #fff;
94
- border: 1px solid #e2e8f0;
94
+ border: 1px solid var(--sa-border);
95
95
  border-radius: 8px;
96
96
  padding: 8px 10px;
97
97
  }
@@ -142,8 +142,8 @@ function isNew(cap: CapabilityCatalogEntryRow): boolean {
142
142
  color: #b91c1c;
143
143
  }
144
144
  .sa-caps-row__flag--gone {
145
- background: #e2e8f0;
146
- color: #64748b;
145
+ background: var(--sa-border);
146
+ color: var(--sa-muted);
147
147
  }
148
148
  .sa-caps-row.gone {
149
149
  opacity: 0.6;
@@ -153,7 +153,7 @@ function isNew(cap: CapabilityCatalogEntryRow): boolean {
153
153
  gap: 12px;
154
154
  flex-wrap: wrap;
155
155
  font-size: 11px;
156
- color: #475569;
156
+ color: var(--sa-muted-dark);
157
157
  margin-top: 1px;
158
158
  }
159
159
  .sa-caps-row__meta code {
@@ -165,7 +165,7 @@ function isNew(cap: CapabilityCatalogEntryRow): boolean {
165
165
  gap: 6px;
166
166
  font-size: 11px;
167
167
  color: #b45309;
168
- background: #fffbeb;
168
+ background: var(--sa-warning-bg);
169
169
  border: 1px dashed #fde68a;
170
170
  border-radius: 8px;
171
171
  padding: 10px 12px;