@saasicat/ui-vue 0.21.0 → 0.22.1

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
@@ -13,115 +13,123 @@
13
13
  </template>
14
14
  </AdminHero>
15
15
 
16
- <q-banner v-if="error" class="sa-bundles__error" rounded>
17
- <template #avatar><q-icon name="warning" color="negative" /></template>
18
- {{ loadErrorText }}
19
- </q-banner>
20
-
21
- <BundlesKpis
22
- :bundles-total="bundles.length"
23
- :live-count="liveCount"
24
- :scheduled-bundles-count="scheduledBundlesCount"
25
- :total-scheduled-versions="totalScheduledVersions"
26
- :total-draft-versions="totalDraftVersions"
27
- :draft-bundles-count="draftBundlesCount"
28
- :translated-count="translatedCount"
29
- :locales-count="locales.length"
30
- />
31
-
32
- <BundlesFilterBar
33
- v-model:query="query"
34
- v-model:status-filter="statusFilter"
35
- :status-filter-options="statusFilterOptions"
36
- />
37
-
38
- <!-- Inline creation (collapsible panel, replaces the former dialog modal) -->
39
- <BundleCreatePanel
40
- v-if="createOpen"
41
- :project-key="projectKey"
42
- :available-features="snapshot?.features ?? []"
43
- :available-quotas="snapshot?.quotas ?? []"
44
- :plans="plans"
45
- :live-plan-versions="livePlanVersions"
46
- :feature-registry="featureRegistryResolved"
47
- :quota-registry="quotaRegistryResolved"
48
- :existing-bundle-keys="existingBundleKeys"
49
- :create="create"
50
- :create-draft="createDraft"
51
- @cancel="createOpen = false"
52
- @created="onWizardCreated"
53
- />
16
+ <AdminBody>
17
+ <q-banner v-if="error" class="sa-bundles__error" rounded>
18
+ <template #avatar><q-icon name="warning" color="negative" /></template>
19
+ {{ loadErrorText }}
20
+ </q-banner>
21
+
22
+ <AdminSection>
23
+ <BundlesKpis
24
+ :bundles-total="bundles.length"
25
+ :live-count="liveCount"
26
+ :scheduled-bundles-count="scheduledBundlesCount"
27
+ :total-scheduled-versions="totalScheduledVersions"
28
+ :total-draft-versions="totalDraftVersions"
29
+ :draft-bundles-count="draftBundlesCount"
30
+ :translated-count="translatedCount"
31
+ :locales-count="locales.length"
32
+ />
33
+ </AdminSection>
34
+
35
+ <AdminSection class="sa-bundles__filter">
36
+ <BundlesFilterBar
37
+ v-model:query="query"
38
+ v-model:status-filter="statusFilter"
39
+ class="q-mb-md"
40
+ :status-filter-options="statusFilterOptions"
41
+ />
54
42
 
55
- <q-banner
56
- v-if="bundles.length === 0 && !loading && !error"
57
- class="sa-bundles__empty"
58
- rounded
59
- >
60
- <template #avatar><q-icon name="info" color="info" /></template>
61
- {{ msg.page.emptyBefore }} <strong>{{ msg.header.newBundle }}</strong>
62
- {{ msg.page.emptyAfter }}
63
- </q-banner>
64
-
65
- <BundleAccordionList
66
- :filtered-bundles="filteredBundles"
67
- :bundles-total="bundles.length"
68
- :open-key="openKey"
69
- :aggregate-status-of="aggregateStatusOf"
70
- :i18n-locale-count="i18nLocaleCount"
71
- @toggle="toggle"
72
- >
73
- <template #detail="{ bundle }">
74
- <BundleDetailPanel
75
- :bundle="bundle"
76
- v-model:edit-form="editForm"
77
- :i18n-draft="i18nDraft"
78
- :translatable-locales="translatableLocales"
79
- :edit-submitting="editSubmitting"
80
- :detail-versions="detailVersions"
81
- :selected-version-id="selectedVersionIdByBundle[bundle.id] ?? null"
82
- :selected-version="selectedVersion"
43
+ <!-- Inline creation (collapsible panel, replaces the former dialog modal) -->
44
+ <BundleCreatePanel
45
+ v-if="createOpen"
46
+ class="q-mb-sm"
47
+ :project-key="projectKey"
83
48
  :available-features="snapshot?.features ?? []"
84
49
  :available-quotas="snapshot?.quotas ?? []"
85
50
  :plans="plans"
86
51
  :live-plan-versions="livePlanVersions"
87
52
  :feature-registry="featureRegistryResolved"
88
53
  :quota-registry="quotaRegistryResolved"
89
- :inline-editor-saving="inlineEditorSaving"
90
- :inline-editor-error="inlineEditorError"
91
- @set-i18n="setI18n"
92
- @submit-edit="submitEdit"
93
- @select-version="onSelectVersion"
94
- @add-version="onAddVersion"
95
- @inline-save="onInlineSave"
96
- @discard-version="onDiscardVersion"
97
- @publish-version="openPublish"
98
- @delete-bundle="confirmDelete"
54
+ :existing-bundle-keys="existingBundleKeys"
55
+ :create="create"
56
+ :create-draft="createDraft"
57
+ @cancel="createOpen = false"
58
+ @created="onWizardCreated"
99
59
  />
100
- </template>
101
- </BundleAccordionList>
102
-
103
- <!-- Strict mode warnings after the last mutation -->
104
- <q-banner
105
- v-if="lastWarnings.length > 0"
106
- class="sa-bundles__warnings-banner"
107
- inline-actions
108
- rounded
109
- >
110
- <template #avatar><q-icon name="warning" color="warning" /></template>
111
- <strong>{{ strictWarningsText }}</strong>
112
- <ul class="sa-bundles__warnings-list">
113
- <li v-for="(w, i) in lastWarnings" :key="i">
114
- <code>{{ w.code }}</code>
115
- <template v-if="w.value">
116
- · <code>{{ w.value }}</code></template
117
- >
118
- — {{ w.message }}
119
- </li>
120
- </ul>
121
- <template #action>
122
- <q-btn flat dense :label="common.close" @click="lastWarnings = []" />
123
- </template>
124
- </q-banner>
60
+
61
+ <q-banner
62
+ v-if="bundles.length === 0 && !loading && !error"
63
+ class="sa-bundles__empty"
64
+ rounded
65
+ >
66
+ <template #avatar><q-icon name="info" color="info" /></template>
67
+ {{ msg.page.emptyBefore }} <strong>{{ msg.header.newBundle }}</strong>
68
+ {{ msg.page.emptyAfter }}
69
+ </q-banner>
70
+
71
+ <BundleAccordionList
72
+ :filtered-bundles="filteredBundles"
73
+ :bundles-total="bundles.length"
74
+ :open-key="openKey"
75
+ :aggregate-status-of="aggregateStatusOf"
76
+ :i18n-locale-count="i18nLocaleCount"
77
+ @toggle="toggle"
78
+ >
79
+ <template #detail="{ bundle }">
80
+ <BundleDetailPanel
81
+ v-model:edit-form="editForm"
82
+ :bundle="bundle"
83
+ :i18n-draft="i18nDraft"
84
+ :translatable-locales="translatableLocales"
85
+ :edit-submitting="editSubmitting"
86
+ :detail-versions="detailVersions"
87
+ :selected-version-id="selectedVersionIdByBundle[bundle.id] ?? null"
88
+ :selected-version="selectedVersion"
89
+ :available-features="snapshot?.features ?? []"
90
+ :available-quotas="snapshot?.quotas ?? []"
91
+ :plans="plans"
92
+ :live-plan-versions="livePlanVersions"
93
+ :feature-registry="featureRegistryResolved"
94
+ :quota-registry="quotaRegistryResolved"
95
+ :inline-editor-saving="inlineEditorSaving"
96
+ :inline-editor-error="inlineEditorError"
97
+ @set-i18n="setI18n"
98
+ @submit-edit="submitEdit"
99
+ @select-version="onSelectVersion"
100
+ @add-version="onAddVersion"
101
+ @inline-save="onInlineSave"
102
+ @discard-version="onDiscardVersion"
103
+ @publish-version="openPublish"
104
+ @delete-bundle="confirmDelete"
105
+ />
106
+ </template>
107
+ </BundleAccordionList>
108
+ </AdminSection>
109
+
110
+ <!-- Strict mode warnings after the last mutation -->
111
+ <q-banner
112
+ v-if="lastWarnings.length > 0"
113
+ class="sa-bundles__warnings-banner"
114
+ inline-actions
115
+ rounded
116
+ >
117
+ <template #avatar><q-icon name="warning" color="warning" /></template>
118
+ <strong>{{ strictWarningsText }}</strong>
119
+ <ul class="sa-bundles__warnings-list">
120
+ <li v-for="(w, i) in lastWarnings" :key="i">
121
+ <code>{{ w.code }}</code>
122
+ <template v-if="w.value">
123
+ · <code>{{ w.value }}</code></template
124
+ >
125
+ — {{ w.message }}
126
+ </li>
127
+ </ul>
128
+ <template #action>
129
+ <q-btn flat dense :label="common.close" @click="lastWarnings = []" />
130
+ </template>
131
+ </q-banner>
132
+ </AdminBody>
125
133
 
126
134
  <!-- Publish confirmation modal -->
127
135
  <BundleVersionPublishDialog
@@ -178,7 +186,9 @@ import { useSaMessages } from '../vue/use-super-admin-i18n.js';
178
186
  import BundleAccordionList from './bundles-page/BundleAccordionList.vue';
179
187
  import BundleDetailPanel from './bundles-page/BundleDetailPanel.vue';
180
188
  import BundlesFilterBar from './bundles-page/BundlesFilterBar.vue';
189
+ import AdminBody from '../components/admin-page/AdminBody.vue';
181
190
  import AdminHero from '../components/admin-page/AdminHero.vue';
191
+ import AdminSection from '../components/admin-page/AdminSection.vue';
182
192
  import AdminPage from '../components/admin-page/AdminPage.vue';
183
193
  import BundlesToolbar from './bundles-page/BundlesToolbar.vue';
184
194
  import BundlesKpis from './bundles-page/BundlesKpis.vue';
@@ -188,7 +198,7 @@ import type {
188
198
  BundlesStatusFilterOption,
189
199
  } from './bundles-page/types.js';
190
200
 
191
- // Platform standard page: Bundles (SPEC_V2 §6.4, §11.1 M6 Pack 2c +
201
+ // Platform standard page: Bundles (§11.1 M6 Pack 2c +
192
202
  // Pack 2d inline editor after plan simulation). Dumb component —
193
203
  // the consumer wrapper supplies the composable results + plan-root
194
204
  // list + live PlanVersion index as props.
@@ -659,32 +669,6 @@ const classifyDiff = computed(() => props.classifyDiff);
659
669
  .sa-bundles__empty {
660
670
  border-left: 4px solid #31ccec;
661
671
  }
662
- .sa-bundles__kpis {
663
- display: grid;
664
- /* Responsive: as many KPIs as fit (up to 4), then 3 → 2 → 1. */
665
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
666
- gap: 10px;
667
- }
668
- .sa-bundles__kpi {
669
- background: #fff;
670
- border: 1px solid #e2e8f0;
671
- border-radius: 10px;
672
- padding: 12px 14px;
673
- }
674
- .sa-bundles__kpi-label {
675
- font-size: 10px;
676
- text-transform: uppercase;
677
- letter-spacing: 0.5px;
678
- color: #64748b;
679
- }
680
- .sa-bundles__kpi-value {
681
- font-size: 26px;
682
- font-weight: 700;
683
- }
684
- .sa-bundles__kpi-sub {
685
- font-size: 11px;
686
- color: #94a3b8;
687
- }
688
672
  .sa-bundles__filter-row {
689
673
  display: flex;
690
674
  align-items: center;
@@ -705,7 +689,7 @@ const classifyDiff = computed(() => props.classifyDiff);
705
689
  }
706
690
  .sa-bd-card {
707
691
  background: #fff;
708
- border: 1px solid #e2e8f0;
692
+ border: 1px solid var(--sa-border);
709
693
  border-radius: 10px;
710
694
  overflow: hidden;
711
695
  }
@@ -723,8 +707,8 @@ const classifyDiff = computed(() => props.classifyDiff);
723
707
  width: 34px;
724
708
  height: 34px;
725
709
  border-radius: 8px;
726
- background: #eff6ff;
727
- color: #1d4ed8;
710
+ background: var(--sa-primary-50);
711
+ color: var(--sa-primary-strong);
728
712
  display: flex;
729
713
  align-items: center;
730
714
  justify-content: center;
@@ -751,19 +735,19 @@ const classifyDiff = computed(() => props.classifyDiff);
751
735
  }
752
736
  .sa-bd-card__desc {
753
737
  font-size: 12px;
754
- color: #64748b;
738
+ color: var(--sa-muted);
755
739
  }
756
740
  .sa-bd-card__chev {
757
741
  transition: transform 0.15s;
758
- color: #94a3b8;
742
+ color: var(--sa-muted-light);
759
743
  }
760
744
  .sa-bd-card__chev.open {
761
745
  transform: rotate(90deg);
762
746
  }
763
747
  .sa-bd-card__body {
764
- border-top: 1px solid #e2e8f0;
748
+ border-top: 1px solid var(--sa-border);
765
749
  padding: 14px;
766
- background: #f8fafc;
750
+ background: var(--sa-bg-surface-2);
767
751
  }
768
752
  .sa-bd-grid {
769
753
  display: grid;
@@ -794,7 +778,7 @@ const classifyDiff = computed(() => props.classifyDiff);
794
778
  font-weight: 700;
795
779
  text-transform: uppercase;
796
780
  letter-spacing: 0.5px;
797
- color: #475569;
781
+ color: var(--sa-muted-dark);
798
782
  display: flex;
799
783
  justify-content: space-between;
800
784
  }
@@ -802,7 +786,7 @@ const classifyDiff = computed(() => props.classifyDiff);
802
786
  margin-top: 10px;
803
787
  }
804
788
  .sa-bd-section-count {
805
- color: #94a3b8;
789
+ color: var(--sa-muted-light);
806
790
  font-weight: 600;
807
791
  }
808
792
  .sa-bundles__form {
@@ -812,14 +796,14 @@ const classifyDiff = computed(() => props.classifyDiff);
812
796
  }
813
797
  .sa-bd-i18n-hint {
814
798
  font-size: 11px;
815
- color: #94a3b8;
799
+ color: var(--sa-muted-light);
816
800
  }
817
801
  .sa-bd-i18n-block {
818
802
  display: flex;
819
803
  flex-direction: column;
820
804
  gap: 6px;
821
805
  padding: 8px;
822
- border: 1px solid #e2e8f0;
806
+ border: 1px solid var(--sa-border);
823
807
  border-radius: 8px;
824
808
  background: #fff;
825
809
  }
@@ -831,7 +815,7 @@ const classifyDiff = computed(() => props.classifyDiff);
831
815
  .sa-bd-i18n-code {
832
816
  font-size: 10px;
833
817
  font-weight: 700;
834
- background: #f1f5f9;
818
+ background: var(--sa-border-soft);
835
819
  padding: 2px 7px;
836
820
  border-radius: 5px;
837
821
  }
@@ -852,7 +836,7 @@ const classifyDiff = computed(() => props.classifyDiff);
852
836
  gap: 10px;
853
837
  align-items: flex-start;
854
838
  background: #fff;
855
- border: 1px solid #e2e8f0;
839
+ border: 1px solid var(--sa-border);
856
840
  border-radius: 8px;
857
841
  padding: 10px;
858
842
  }
@@ -867,7 +851,7 @@ const classifyDiff = computed(() => props.classifyDiff);
867
851
  }
868
852
  .sa-bd-version__sub {
869
853
  font-size: 11px;
870
- color: #64748b;
854
+ color: var(--sa-muted);
871
855
  margin: 2px 0;
872
856
  }
873
857
  .sa-bd-version__feats {
@@ -877,7 +861,7 @@ const classifyDiff = computed(() => props.classifyDiff);
877
861
  }
878
862
  .sa-bd-feat-chip {
879
863
  font-size: 10px;
880
- background: #f1f5f9;
864
+ background: var(--sa-border-soft);
881
865
  padding: 2px 6px;
882
866
  border-radius: 5px;
883
867
  }
@@ -888,7 +872,7 @@ const classifyDiff = computed(() => props.classifyDiff);
888
872
  .sa-bd-empty-row {
889
873
  padding: 20px;
890
874
  text-align: center;
891
- color: #94a3b8;
875
+ color: var(--sa-muted-light);
892
876
  font-size: 12px;
893
877
  border: 1px dashed #cbd5e1;
894
878
  border-radius: 8px;
@@ -2,14 +2,7 @@
2
2
  <AdminPage class="sa-dashboard">
3
3
  <AdminHero :title="msg.title" :subtitle="subtitle">
4
4
  <template #actions>
5
- <q-btn
6
- flat
7
- dense
8
- icon="refresh"
9
- :loading="loading"
10
- :aria-label="common.reload"
11
- @click="reload"
12
- />
5
+ <AdminRefreshBtn :loading="loading" @refresh="reload" />
13
6
  </template>
14
7
  </AdminHero>
15
8
 
@@ -25,30 +18,28 @@
25
18
  {{ msg.emptyKpiCards }}
26
19
  </div>
27
20
 
28
- <div v-else class="sa-dashboard__strip">
29
- <article v-for="card in cards" :key="card.id" class="sa-kpi" :data-card-id="card.id">
30
- <div class="sa-kpi__icon" v-if="card.displayHint?.icon">
31
- <q-icon :name="card.displayHint.icon" size="20px" />
32
- </div>
33
- <div class="sa-kpi__body">
34
- <div class="sa-kpi__label">{{ card.label }}</div>
35
- <div class="sa-kpi__value">
21
+ <AdminSection v-else>
22
+ <AdminStatistics :label="msg.title">
23
+ <AdminKpi
24
+ v-for="card in cards"
25
+ :key="card.id"
26
+ :data-card-id="card.id"
27
+ :label="card.label"
28
+ :sub="card.sub"
29
+ :icon="card.displayHint?.icon"
30
+ layout="inline"
31
+ >
32
+ <template #value>
36
33
  <q-spinner v-if="card.loading" size="18px" />
37
- <span v-else-if="card.error" class="sa-kpi__error">—</span>
38
- <span v-else>{{ formatValue(card) }}</span>
39
- </div>
40
- <div v-if="card.sub" class="sa-kpi__sub">{{ card.sub }}</div>
41
- </div>
42
- </article>
43
- </div>
34
+ <template v-else-if="card.error">—</template>
35
+ <template v-else>{{ formatValue(card) }}</template>
36
+ </template>
37
+ </AdminKpi>
38
+ </AdminStatistics>
39
+ </AdminSection>
44
40
 
45
41
  <div v-if="distributions && distributions.length > 0" class="sa-dashboard__rows">
46
- <AdminSection
47
- v-for="dist in distributions"
48
- :key="dist.id"
49
- :title="dist.label"
50
- class="sa-card sa-dashboard__row-card"
51
- >
42
+ <AdminSection v-for="dist in distributions" :key="dist.id" :title="dist.label">
52
43
  <template v-if="dist.total" #actions>
53
44
  <span class="sa-dashboard__count">{{ dist.total }}</span>
54
45
  </template>
@@ -77,7 +68,7 @@
77
68
  <AdminSection
78
69
  v-if="resolvedShortcuts.length > 0"
79
70
  :title="msg.shortcutsTitle"
80
- class="sa-card sa-dashboard__shortcuts"
71
+ class="sa-dashboard__shortcuts"
81
72
  >
82
73
  <div class="sa-dashboard__shortcut-grid">
83
74
  <a
@@ -108,9 +99,12 @@ import { formatMessage } from '../client/i18n/format.js';
108
99
  import { SUPER_ADMIN_MANIFEST_KEY } from '../vue/super-admin-context.js';
109
100
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
110
101
  import { useSuperAdminHttp } from '../vue/use-super-admin-context.js';
102
+ import AdminRefreshBtn from '../components/admin-page/AdminRefreshBtn.vue';
111
103
  import AdminHero from '../components/admin-page/AdminHero.vue';
104
+ import AdminKpi from '../components/admin-page/AdminKpi.vue';
112
105
  import AdminPage from '../components/admin-page/AdminPage.vue';
113
106
  import AdminSection from '../components/admin-page/AdminSection.vue';
107
+ import AdminStatistics from '../components/admin-page/AdminStatistics.vue';
114
108
 
115
109
  // Platform standard page: Dashboard.
116
110
  //
@@ -336,17 +330,22 @@ function defaultFormat(card: KpiCardDef, body: unknown): KpiFormatted {
336
330
  };
337
331
  }
338
332
 
339
- async function callHttp(url: string): Promise<{ status: number; json: () => Promise<unknown> }> {
333
+ // Every KPI request goes through an `HttpClient`. `useSuperAdminHttp()` always
334
+ // resolves — to the client the app registered, or to `defaultHttpClient()` —
335
+ // so there is no path on which a card could reach the network without the
336
+ // app's auth. A bare `fetch()` here used to silently drop it.
337
+ //
338
+ // The token rides along as a header regardless of which client transports it,
339
+ // the same way `useApiList` and the other standard pages do it. The two are
340
+ // orthogonal, and a page that mounted with the documented `getAuthToken` but
341
+ // no `http` of its own would otherwise send every KPI request unauthenticated
342
+ // and render an error in all of its cards.
343
+ function callHttp(url: string): Promise<{ status: number; json: () => Promise<unknown> }> {
340
344
  const http = props.http ?? shellHttp;
341
- if (http) return http(url, { method: 'GET' });
342
345
  const headers: Record<string, string> = {};
343
346
  const token = props.getAuthToken?.();
344
347
  if (token) headers.Authorization = `Bearer ${token}`;
345
- const resp = await fetch(url, { method: 'GET', headers });
346
- return {
347
- status: resp.status,
348
- json: () => resp.json(),
349
- };
348
+ return http(url, { method: 'GET', headers });
350
349
  }
351
350
 
352
351
  function extractValue(body: Record<string, unknown>): string | number | null {
@@ -401,73 +400,10 @@ function barWidth(value: number, max?: number): number {
401
400
 
402
401
  <style scoped>
403
402
  /* Header look comes from the global .sa-page-head — only a margin tweak here. */
404
- .sa-dashboard__strip {
405
- display: grid;
406
- /* Responsive: 4 cards while there is space, then automatically 3 → 2 → 1. */
407
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
408
- gap: 12px;
409
- margin-bottom: 14px;
410
- }
411
403
 
412
- .sa-kpi {
413
- background: #fff;
414
- border: 1px solid var(--sa-border, #e2e8f0);
415
- border-radius: 12px;
416
- padding: 14px 16px;
417
- display: flex;
418
- align-items: center;
419
- gap: 12px;
420
- transition:
421
- border-color 0.12s,
422
- box-shadow 0.12s;
423
- }
424
- .sa-kpi:hover {
425
- border-color: #94a3b8;
426
- box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
427
- }
428
- .sa-kpi__icon {
429
- width: 36px;
430
- height: 36px;
431
- border-radius: 9px;
432
- background: var(--sa-primary-soft, rgba(63, 107, 255, 0.08));
433
- display: flex;
434
- align-items: center;
435
- justify-content: center;
436
- }
437
404
  .sa-kpi__icon :deep(.q-icon) {
438
405
  color: var(--sa-primary, #3f6bff);
439
406
  }
440
- .sa-kpi__body {
441
- flex: 1;
442
- min-width: 0;
443
- }
444
- .sa-kpi__label {
445
- font-size: 11.5px;
446
- font-weight: 600;
447
- letter-spacing: 0.06em;
448
- color: var(--sa-muted, #64748b);
449
- text-transform: uppercase;
450
- }
451
- .sa-kpi__value {
452
- font-family: var(--sa-font-head, system-ui, sans-serif);
453
- font-weight: 700;
454
- font-size: 28px;
455
- color: var(--sa-heading, #0f172a);
456
- line-height: 1;
457
- letter-spacing: -0.02em;
458
- margin-top: 4px;
459
- min-height: 30px;
460
- display: flex;
461
- align-items: center;
462
- }
463
- .sa-kpi__sub {
464
- font-size: 11.5px;
465
- color: #94a3b8;
466
- margin-top: 4px;
467
- }
468
- .sa-kpi__error {
469
- color: var(--sa-negative, #dc2626);
470
- }
471
407
 
472
408
  .sa-dashboard__rows {
473
409
  display: grid;
@@ -508,10 +444,10 @@ function barWidth(value: number, max?: number): number {
508
444
  }
509
445
  .sa-dashboard__bar-name {
510
446
  font-weight: 600;
511
- color: var(--sa-heading, #0f172a);
447
+ color: var(--sa-heading, var(--sa-heading));
512
448
  }
513
449
  .sa-dashboard__bar-track {
514
- background: #f1f5f9;
450
+ background: var(--sa-border-soft);
515
451
  height: 8px;
516
452
  border-radius: 999px;
517
453
  overflow: hidden;
@@ -525,7 +461,7 @@ function barWidth(value: number, max?: number): number {
525
461
  .sa-dashboard__bar-count {
526
462
  font-variant-numeric: tabular-nums;
527
463
  text-align: right;
528
- color: var(--sa-muted-dark, #475569);
464
+ color: var(--sa-muted-dark, var(--sa-muted-dark));
529
465
  }
530
466
 
531
467
  .sa-dashboard__shortcuts {
@@ -541,7 +477,7 @@ function barWidth(value: number, max?: number): number {
541
477
  gap: 12px;
542
478
  align-items: flex-start;
543
479
  padding: 12px 14px;
544
- border: 1px solid var(--sa-border, #e2e8f0);
480
+ border: 1px solid var(--sa-border, var(--sa-border));
545
481
  border-radius: 10px;
546
482
  text-decoration: none;
547
483
  color: inherit;
@@ -558,19 +494,19 @@ function barWidth(value: number, max?: number): number {
558
494
  }
559
495
  .sa-dashboard__shortcut-title {
560
496
  font-weight: 700;
561
- color: var(--sa-heading, #0f172a);
497
+ color: var(--sa-heading, var(--sa-heading));
562
498
  font-family: var(--sa-font-head, system-ui, sans-serif);
563
499
  }
564
500
  .sa-dashboard__shortcut-sub {
565
501
  font-size: 12px;
566
- color: var(--sa-muted, #64748b);
502
+ color: var(--sa-muted, var(--sa-muted));
567
503
  margin-top: 2px;
568
504
  }
569
505
 
570
506
  .sa-dashboard__loading,
571
507
  .sa-dashboard__empty {
572
508
  padding: 32px 28px;
573
- color: var(--sa-muted, #64748b);
509
+ color: var(--sa-muted, var(--sa-muted));
574
510
  display: flex;
575
511
  align-items: center;
576
512
  gap: 12px;