@saasicat/ui-vue 0.21.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 (132) hide show
  1. package/dist/{catalog-BY_t9prv.d.cts → catalog-DJcfm5N2.d.cts} +20 -7
  2. package/dist/{catalog-BY_t9prv.d.ts → catalog-DJcfm5N2.d.ts} +20 -7
  3. package/dist/{chunk-JN6HZGLV.js → chunk-F7NRM6KI.js} +54 -28
  4. package/dist/{chunk-7IFKVGZV.js → chunk-LY52T4N4.js} +21 -2
  5. package/dist/{chunk-OTP3OSSW.js → chunk-XMMFN2WJ.js} +74 -3
  6. package/dist/client/index.cjs +132 -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 +319 -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 +76 -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-c_Eqsu9u.d.ts → use-super-admin-i18n-CHNdsSlm.d.ts} +34 -7
  21. package/dist/{use-super-admin-i18n-DlJRGIVq.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 +22 -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/AdminFilters.vue +12 -0
  40. package/src/components/admin-page/AdminHero.vue +22 -5
  41. package/src/components/admin-page/AdminKpi.vue +84 -0
  42. package/src/components/admin-page/AdminPaginator.vue +195 -0
  43. package/src/components/admin-page/AdminRefreshBtn.vue +39 -0
  44. package/src/components/admin-page/AdminSection.vue +31 -16
  45. package/src/components/admin-page/AdminStatistics.vue +40 -0
  46. package/src/components/admin-page/AdminTable.vue +175 -0
  47. package/src/components/bundle-editor/BundleCreatePanel.vue +23 -23
  48. package/src/components/bundle-editor/BundleFeaturesEditor.vue +7 -7
  49. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +9 -9
  50. package/src/components/bundle-editor/BundleQuotasEditor.vue +11 -11
  51. package/src/components/bundle-editor/BundleStatusBanner.vue +6 -6
  52. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +18 -18
  53. package/src/components/bundle-editor/BundleVersionStrip.vue +12 -12
  54. package/src/components/dialogs/PilotCreateDialog.vue +12 -12
  55. package/src/components/dialogs/PilotEditDialog.vue +7 -7
  56. package/src/components/dialogs/PromoCodeCreateDialog.vue +8 -8
  57. package/src/components/dialogs/PromoCodeDialogFields.vue +33 -19
  58. package/src/components/dialogs/PromoCodeEditDialog.vue +10 -10
  59. package/src/components/plan/PlanGrid.vue +1 -1
  60. package/src/components/plan-create-dialog/PlanCreateDialog.vue +34 -46
  61. package/src/components/plan-detail/PlanDetail.vue +92 -302
  62. package/src/components/plan-detail/PlanDetailKpis.vue +22 -27
  63. package/src/components/plan-detail/PlanTitleEdit.vue +133 -0
  64. package/src/components/plan-list/PlanList.vue +72 -290
  65. package/src/components/plan-matrix/PlanMatrix.vue +66 -183
  66. package/src/components/plan-review/PlanReview.vue +191 -275
  67. package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -5
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +200 -295
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +23 -55
  70. package/src/index.ts +1 -0
  71. package/src/pages-standard/AdminLayout.vue +34 -10
  72. package/src/pages-standard/AdminManifestErrorPage.vue +79 -9
  73. package/src/pages-standard/AuditPage.vue +53 -72
  74. package/src/pages-standard/BundlesPage.vue +128 -144
  75. package/src/pages-standard/DashboardPage.vue +43 -107
  76. package/src/pages-standard/DiscoveryPage.vue +158 -200
  77. package/src/pages-standard/EmailHistoryPage.vue +98 -88
  78. package/src/pages-standard/MarketingCatalogPage.vue +237 -267
  79. package/src/pages-standard/PilotsPage.vue +44 -60
  80. package/src/pages-standard/PlansPage.vue +230 -105
  81. package/src/pages-standard/PlatformEmailPage.vue +37 -40
  82. package/src/pages-standard/PromoCodeDetailPage.vue +16 -30
  83. package/src/pages-standard/PromoCodesPage.vue +86 -112
  84. package/src/pages-standard/SubscriptionsPage.vue +10 -17
  85. package/src/pages-standard/SuperAdminLoginPage.vue +12 -12
  86. package/src/pages-standard/SuperAdminSetupWizard.vue +20 -17
  87. package/src/pages-standard/TenantDetailPage.vue +45 -41
  88. package/src/pages-standard/TenantsPage.vue +184 -264
  89. package/src/pages-standard/UsersPage.vue +69 -104
  90. package/src/pages-standard/bundles-page/BundleAccordionList.vue +20 -17
  91. package/src/pages-standard/bundles-page/BundlesKpis.vue +13 -22
  92. package/src/pages-standard/bundles-page/BundlesToolbar.vue +6 -10
  93. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +10 -10
  94. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +5 -5
  95. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +16 -16
  96. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +31 -27
  97. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +7 -7
  98. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +1 -1
  99. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +2 -13
  100. package/src/pages-standard/plan-versions/PlanDiffCard.vue +6 -6
  101. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +6 -6
  102. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +16 -16
  103. package/src/pages-standard/plans-page/PlanBundleOverview.vue +14 -29
  104. package/src/pages-standard/plans-page/PlanPublishDialog.vue +1 -1
  105. package/src/pages-standard/plans-page/PlansPageToast.vue +1 -1
  106. package/src/pages-standard/sa-theme.css +481 -71
  107. package/src/pages-standard/tenant-detail/TenantUsers.vue +2 -8
  108. package/src/pages-standard/tenants/StatusPill.vue +2 -2
  109. package/src/pages-tenant/MySubscriptionBundlesPage.vue +25 -25
  110. package/src/pages-tenant/OnboardingConfigurator.vue +1 -5
  111. package/src/pages-tenant/PackageSnapshotPanel.vue +1 -1
  112. package/src/pages-tenant/TenantPlanSection.vue +12 -20
  113. package/src/quasar/create-super-admin-app.ts +4 -0
  114. package/src/vue/entitlement-provider.ts +0 -1
  115. package/src/vue/feature-router-guard.ts +0 -1
  116. package/src/vue/super-admin-context.ts +106 -1
  117. package/src/vue/use-api-list.ts +2 -1
  118. package/src/vue/use-pagination.ts +63 -0
  119. package/src/vue/use-plans.ts +1 -2
  120. package/src/vue/use-sign-out.ts +90 -0
  121. package/src/vue/use-tenant-billing.ts +1 -1
  122. package/src/vue/use-tenant-manifest.ts +0 -1
  123. package/src/components/KpiCard.vue +0 -53
  124. package/src/components/plan-cockpit/PlanCockpit.vue +0 -1027
  125. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +0 -72
  126. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +0 -99
  127. package/src/components/plan-cockpit/PlanCockpitHeader.vue +0 -101
  128. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +0 -112
  129. package/src/components/plan-cockpit/PlanCockpitKpis.vue +0 -68
  130. package/src/components/plan-cockpit/PlanCockpitVersions.vue +0 -249
  131. package/src/components/plan-cockpit/types.ts +0 -72
  132. package/src/components/plan-detail/PlanDetailHeader.vue +0 -194
@@ -1,81 +1,66 @@
1
1
  <template>
2
2
  <AdminPage class="sa-users">
3
- <AdminHero :title="resolvedTitle" :subtitle="subtitle">
4
- <template #actions>
5
- <q-btn
6
- unelevated
7
- color="primary"
8
- icon="search"
9
- :label="common.search"
10
- @click="reload"
11
- />
12
- </template>
13
- </AdminHero>
3
+ <AdminHero :title="resolvedTitle" :subtitle="subtitle" />
14
4
 
15
- <div class="sa-stats">
16
- <button
17
- v-for="tile in statTiles"
18
- :key="tile.id"
19
- class="sa-stat"
20
- :class="[
21
- tile.tone ? `sa-stat--${tile.tone}` : null,
22
- statusFilter === tile.id ? 'sa-stat--active' : null,
23
- ]"
24
- @click="statusFilter = tile.id"
25
- >
26
- <span class="sa-stat__num">{{ tile.count }}</span>
27
- <span class="sa-stat__label">{{ tile.label }}</span>
28
- </button>
29
- </div>
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>
30
19
 
31
- <div class="sa-users__filter">
32
- <q-input
33
- v-model="filter.q"
34
- outlined
35
- dense
36
- :label="msg.filterQuery"
37
- clearable
38
- @keyup.enter="reload"
39
- @clear="reload"
40
- />
41
- <q-input
42
- v-model="filter.tenant"
43
- outlined
44
- dense
45
- :label="msg.filterTenant"
46
- clearable
47
- @keyup.enter="reload"
48
- @clear="reload"
49
- />
50
- <slot name="filters-extra" />
51
- </div>
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>
52
42
 
53
- <div class="sa-users__card">
54
- <q-table
55
- flat
56
- :rows="filteredRows"
57
- :columns="effectiveColumns"
58
- row-key="id"
59
- :pagination="{ rowsPerPage: 0 }"
60
- :loading="loading"
61
- hide-pagination
62
- >
63
- <template #body-cell-status="{ row }">
64
- <q-td>
65
- <q-badge
66
- :color="row.isActive ? 'positive' : 'grey'"
67
- :label="row.isActive ? msg.statusActive : msg.statusDeactivated"
68
- />
69
- <q-badge
70
- v-if="row.invitationStatus === 'PENDING'"
71
- color="amber-7"
72
- :label="msg.badgePending"
73
- class="q-ml-xs"
74
- />
75
- </q-td>
76
- </template>
77
- <template #body-cell-actions="{ row }">
78
- <q-td>
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 }">
79
64
  <slot name="row-actions" :row="row">
80
65
  <q-btn
81
66
  v-for="action in visibleActions(row)"
@@ -88,10 +73,10 @@
88
73
  @click="action.handler(row)"
89
74
  />
90
75
  </slot>
91
- </q-td>
92
- </template>
93
- </q-table>
94
- </div>
76
+ </template>
77
+ </AdminTable>
78
+ </AdminSection>
79
+ </AdminBody>
95
80
 
96
81
  <MfaPromptDialog
97
82
  v-if="needsMfaDialog"
@@ -106,12 +91,18 @@
106
91
  </template>
107
92
 
108
93
  <script setup lang="ts">
94
+ import AdminTable from '../components/admin-page/AdminTable.vue';
109
95
  import { computed, onMounted, reactive, ref } from 'vue';
110
96
  import { useMfaPrompt } from '../vue/use-mfa-prompt.js';
111
97
  import { useQuasar } from 'quasar';
112
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';
113
101
  import AdminHero from '../components/admin-page/AdminHero.vue';
102
+ import AdminKpi from '../components/admin-page/AdminKpi.vue';
114
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';
115
106
  import MfaPromptDialog from '../components/MfaPromptDialog.vue';
116
107
  import { formatMessage } from '../client/i18n/format.js';
117
108
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
@@ -318,14 +309,6 @@ const mergedActions = computed<readonly UserRowAction[]>(() => [
318
309
 
319
310
  const effectiveColumns = computed(() => {
320
311
  const cols = [...baseColumns.value];
321
- if (mergedActions.value.length > 0) {
322
- cols.push({
323
- name: 'actions',
324
- label: '',
325
- field: 'id' as never,
326
- align: 'right' as 'left',
327
- });
328
- }
329
312
  return cols;
330
313
  });
331
314
 
@@ -468,22 +451,4 @@ function onDeactivateClick(row: UserRow): void {
468
451
  }
469
452
  </script>
470
453
 
471
- <style scoped>
472
- .sa-users__filter {
473
- display: flex;
474
- gap: 10px;
475
- margin-bottom: 12px;
476
- align-items: center;
477
- flex-wrap: wrap;
478
- }
479
- .sa-users__filter > * {
480
- flex: 1;
481
- min-width: 200px;
482
- }
483
- .sa-users__card {
484
- background: #fff;
485
- border: 1px solid var(--sa-border, #e2e8f0);
486
- border-radius: 12px;
487
- overflow: hidden;
488
- }
489
- </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
 
@@ -14,16 +14,11 @@
14
14
  >
15
15
  <template #prepend><q-icon name="translate" size="18px" /></template>
16
16
  </q-select>
17
- <q-btn
18
- unelevated
19
- color="primary"
20
- icon="add"
21
- :label="msg.header.newBundle"
22
- @click="emit('create')"
23
- />
24
- <q-btn flat icon="refresh" :loading="loading" @click="emit('refresh')">
25
- <q-tooltip>{{ msg.header.reload }}</q-tooltip>
26
- </q-btn>
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')" />
27
22
  </div>
28
23
  </template>
29
24
 
@@ -31,6 +26,7 @@
31
26
  import { computed } from 'vue';
32
27
 
33
28
  import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
29
+ import AdminRefreshBtn from '../../components/admin-page/AdminRefreshBtn.vue';
34
30
 
35
31
  // The language selector controls which locale feature/quota labels are resolved
36
32
  // on the create form and in the detail view (fallback locale → DE → key). The default is the
@@ -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;
@@ -269,7 +269,7 @@ function coverage(locale: string): number {
269
269
 
270
270
  <style scoped>
271
271
  .sa-fc {
272
- border: 1px solid #e2e8f0;
272
+ border: 1px solid var(--sa-border);
273
273
  border-radius: 10px;
274
274
  background: #fff;
275
275
  overflow: hidden;
@@ -286,7 +286,7 @@ function coverage(locale: string): number {
286
286
  cursor: pointer;
287
287
  }
288
288
  .sa-fc__icon {
289
- color: #475569;
289
+ color: var(--sa-muted-dark);
290
290
  flex-shrink: 0;
291
291
  }
292
292
  .sa-fc__main {
@@ -302,8 +302,8 @@ function coverage(locale: string): number {
302
302
  .sa-fc__key {
303
303
  font-size: 11px;
304
304
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
305
- background: #f1f5f9;
306
- color: #475569;
305
+ background: var(--sa-border-soft);
306
+ color: var(--sa-muted-dark);
307
307
  padding: 2px 6px;
308
308
  border-radius: 5px;
309
309
  white-space: nowrap;
@@ -311,7 +311,7 @@ function coverage(locale: string): number {
311
311
  .sa-fc__label {
312
312
  font-size: 13px;
313
313
  font-weight: 600;
314
- color: #0f172a;
314
+ color: var(--sa-heading);
315
315
  }
316
316
  .sa-fc__flag {
317
317
  font-size: 10px;
@@ -337,7 +337,7 @@ function coverage(locale: string): number {
337
337
  }
338
338
  .sa-fc__sub {
339
339
  font-size: 11px;
340
- color: #64748b;
340
+ color: var(--sa-muted);
341
341
  margin-top: 1px;
342
342
  }
343
343
  .sa-fc__dot {
@@ -356,16 +356,16 @@ function coverage(locale: string): number {
356
356
  flex-shrink: 0;
357
357
  }
358
358
  .sa-fc__chev {
359
- color: #94a3b8;
359
+ color: var(--sa-muted-light);
360
360
  transition: transform 0.15s;
361
361
  }
362
362
  .sa-fc__chev.open {
363
363
  transform: rotate(90deg);
364
364
  }
365
365
  .sa-fc__body {
366
- border-top: 1px solid #f1f5f9;
366
+ border-top: 1px solid var(--sa-border-soft);
367
367
  padding: 12px;
368
- background: #f8fafc;
368
+ background: var(--sa-bg-surface-2);
369
369
  }
370
370
  .sa-fc__banner {
371
371
  display: flex;
@@ -377,18 +377,18 @@ function coverage(locale: string): number {
377
377
  margin-bottom: 10px;
378
378
  }
379
379
  .sa-fc__banner.warn {
380
- background: #fffbeb;
380
+ background: var(--sa-warning-bg);
381
381
  border: 1px solid #fde68a;
382
382
  color: #92400e;
383
383
  }
384
384
  .sa-fc__banner.mute {
385
- background: #f1f5f9;
386
- border: 1px solid #e2e8f0;
387
- color: #475569;
385
+ background: var(--sa-border-soft);
386
+ border: 1px solid var(--sa-border);
387
+ color: var(--sa-muted-dark);
388
388
  }
389
389
  .sa-fc__subtabs {
390
390
  margin-bottom: 12px;
391
- border-bottom: 1px solid #e2e8f0;
391
+ border-bottom: 1px solid var(--sa-border);
392
392
  }
393
393
  .sa-fc__split {
394
394
  display: grid;
@@ -400,7 +400,7 @@ function coverage(locale: string): number {
400
400
  font-weight: 700;
401
401
  text-transform: uppercase;
402
402
  letter-spacing: 0.5px;
403
- color: #94a3b8;
403
+ color: var(--sa-muted-light);
404
404
  margin-bottom: 8px;
405
405
  }
406
406
  .sa-fc__split-count {
@@ -424,6 +424,6 @@ function coverage(locale: string): number {
424
424
  font-size: 11px;
425
425
  font-weight: 600;
426
426
  text-transform: uppercase;
427
- color: #94a3b8;
427
+ color: var(--sa-muted-light);
428
428
  }
429
429
  </style>
@@ -1,36 +1,40 @@
1
1
  <template>
2
- <div class="sa-discovery__kpis">
3
- <div class="sa-discovery__kpi">
4
- <div class="sa-discovery__kpi-label">{{ msg.kpi.featuresTotal }}</div>
5
- <div class="sa-discovery__kpi-value">{{ featuresCount }}</div>
6
- <div class="sa-discovery__kpi-sub">{{ featuresTotalSub }}</div>
7
- </div>
8
- <div class="sa-discovery__kpi good">
9
- <div class="sa-discovery__kpi-label">{{ msg.kpi.approved }}</div>
10
- <div class="sa-discovery__kpi-value">{{ approvedCount }}</div>
11
- <div class="sa-discovery__kpi-sub">{{ msg.kpi.approvedSub }}</div>
12
- </div>
13
- <div class="sa-discovery__kpi" :class="{ warn: pendingCount > 0 }">
14
- <div class="sa-discovery__kpi-label">{{ msg.kpi.pending }}</div>
15
- <div class="sa-discovery__kpi-value">{{ pendingCount }}</div>
16
- <div class="sa-discovery__kpi-sub">{{ msg.kpi.pendingSub }}</div>
17
- </div>
18
- <div class="sa-discovery__kpi">
19
- <div class="sa-discovery__kpi-label">{{ msg.kpi.outdated }}</div>
20
- <div class="sa-discovery__kpi-value">{{ outdatedCount + obsoleteCount }}</div>
21
- <div class="sa-discovery__kpi-sub">{{ outdatedSub }}</div>
22
- </div>
23
- <div class="sa-discovery__kpi" :class="{ bad: orphanCount > 0 }">
24
- <div class="sa-discovery__kpi-label">{{ msg.kpi.orphans }}</div>
25
- <div class="sa-discovery__kpi-value">{{ orphanCount }}</div>
26
- <div class="sa-discovery__kpi-sub">{{ msg.orphansTitle }}</div>
27
- </div>
28
- </div>
2
+ <AdminStatistics :columns="5">
3
+ <AdminKpi :label="msg.kpi.featuresTotal" :value="featuresCount" :sub="featuresTotalSub" />
4
+ <AdminKpi
5
+ :label="msg.kpi.approved"
6
+ :value="approvedCount"
7
+ :sub="msg.kpi.approvedSub"
8
+ tone="positive"
9
+ />
10
+ <AdminKpi
11
+ :label="msg.kpi.pending"
12
+ :value="pendingCount"
13
+ :sub="msg.kpi.pendingSub"
14
+ :tone="pendingCount > 0 ? 'warn' : 'neutral'"
15
+ emphasis="surface"
16
+ />
17
+ <AdminKpi
18
+ :label="msg.kpi.outdated"
19
+ :value="outdatedCount + obsoleteCount"
20
+ :sub="outdatedSub"
21
+ />
22
+ <AdminKpi
23
+ :label="msg.kpi.orphans"
24
+ :value="orphanCount"
25
+ :sub="msg.orphansTitle"
26
+ :tone="orphanCount > 0 ? 'danger' : 'neutral'"
27
+ emphasis="surface"
28
+ />
29
+ </AdminStatistics>
29
30
  </template>
30
31
 
31
32
  <script setup lang="ts">
32
33
  import { computed } from 'vue';
33
34
 
35
+ import AdminKpi from '../../components/admin-page/AdminKpi.vue';
36
+ import AdminStatistics from '../../components/admin-page/AdminStatistics.vue';
37
+
34
38
  import { formatMessage } from '../../client/i18n/format.js';
35
39
  import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
36
40