@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,145 +1,136 @@
1
1
  <template>
2
- <div class="sa-tenants">
3
- <header class="sa-tenants__head">
4
- <div>
5
- <h1 class="sa-tenants__title">{{ msg.list.title }}</h1>
6
- <p v-if="subtitle" class="sa-tenants__sub">{{ subtitle }}</p>
7
- </div>
8
- </header>
9
-
10
- <div class="sa-tenants__filter">
11
- <div class="sa-tenants__search">
12
- <q-icon name="search" size="15px" />
13
- <input
14
- v-model="searchInput"
15
- :placeholder="msg.list.searchPlaceholder"
16
- @input="reloadDebounced"
17
- />
18
- <button v-if="searchInput" class="sa-tenants__clear" @click="onClearSearch">
19
- <q-icon name="close" size="13px" />
20
- </button>
21
- </div>
22
- <select v-model="statusFilter" class="sa-tenants__select" @change="applyFilter">
23
- <option :value="null">{{ msg.list.allStatuses }}</option>
24
- <option v-for="opt in resolvedStatusOptions" :key="opt.value" :value="opt.value">
25
- {{ opt.label }}
26
- </option>
27
- </select>
28
- <select
29
- v-if="planOptions && planOptions.length > 0"
30
- v-model="planFilter"
31
- class="sa-tenants__select"
32
- @change="applyFilter"
33
- >
34
- <option :value="null">{{ resolvedPlanFilterLabel }}</option>
35
- <option v-for="p in planOptions" :key="p" :value="p">{{ p }}</option>
36
- </select>
37
- <slot name="filters-extra" />
38
- </div>
39
-
40
- <q-banner v-if="error" class="bg-red-1 text-red-9 q-mb-md" rounded>
41
- <strong>{{ common.error }}:</strong> {{ error.message }}
42
- </q-banner>
43
-
44
- <div class="sa-tenants__card">
45
- <div class="sa-tenants__wrap">
46
- <table>
47
- <thead>
48
- <tr>
49
- <th>{{ msg.tenant }}</th>
50
- <th v-if="showPlanColumn">{{ resolvedPlanColumnLabel }}</th>
51
- <th>{{ common.status }}</th>
52
- <th v-if="usageFields.length > 0" class="num">
53
- {{ msg.list.columnUsage }}
54
- </th>
55
- <th class="num">{{ msg.list.columnCreatedAt }}</th>
56
- <th v-if="hasActions" class="num"></th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- <tr v-for="row in items" :key="row.id">
61
- <td>
62
- <div class="sa-tenants__tenant">
63
- <div class="sa-tenants__avatar" :style="avatarStyle(row)">
64
- {{ tenantInitials(row.name) }}
65
- </div>
66
- <div>
67
- <div class="sa-tenants__name">{{ row.name }}</div>
68
- <div class="sa-tenants__slug">{{ row.slug }}</div>
69
- </div>
2
+ <AdminPage class="sa-tenants">
3
+ <AdminHero :title="msg.list.title" :subtitle="subtitle" />
4
+
5
+ <AdminBody>
6
+ <q-banner v-if="error" class="bg-red-1 text-red-9 q-mb-md" rounded>
7
+ <strong>{{ common.error }}:</strong> {{ error.message }}
8
+ </q-banner>
9
+
10
+ <AdminSection class="sa-tenants__card">
11
+ <AdminFilters class="q-mb-lg">
12
+ <q-input
13
+ v-model="searchInput"
14
+ outlined
15
+ dense
16
+ clearable
17
+ :label="msg.list.searchPlaceholder"
18
+ debounce="250"
19
+ @update:model-value="applyFilter"
20
+ >
21
+ <template #prepend><q-icon name="search" size="18px" /></template>
22
+ </q-input>
23
+ <q-select
24
+ v-model="statusFilter"
25
+ outlined
26
+ dense
27
+ clearable
28
+ emit-value
29
+ map-options
30
+ :options="statusSelectOptions"
31
+ :label="msg.list.allStatuses"
32
+ @update:model-value="applyFilter"
33
+ />
34
+ <q-select
35
+ v-if="planOptions && planOptions.length > 0"
36
+ v-model="planFilter"
37
+ outlined
38
+ dense
39
+ clearable
40
+ :options="planOptions"
41
+ :label="resolvedPlanFilterLabel"
42
+ @update:model-value="applyFilter"
43
+ />
44
+ <slot name="filters-extra" />
45
+ </AdminFilters>
46
+
47
+ <AdminTable
48
+ server-side
49
+ :rows="items"
50
+ :columns="tenantColumns"
51
+ :loading="loading"
52
+ :page="page"
53
+ :rows-per-page="rowsPerPage"
54
+ :total="total"
55
+ :empty-text="msg.list.empty"
56
+ storage-key="tenants"
57
+ @update:page="goToPage"
58
+ @update:rows-per-page="setPageSize"
59
+ >
60
+ <template #body-cell-tenant="{ row }">
61
+ <q-td>
62
+ <div class="sa-tenants__tenant">
63
+ <div class="sa-tenants__avatar" :style="avatarStyle(row)">
64
+ {{ tenantInitials(row.name) }}
70
65
  </div>
71
- </td>
72
- <td v-if="showPlanColumn">
73
- <span class="sa-tenants__plan">
74
- <span
75
- class="sa-tenants__plan-dot"
76
- :style="{ background: planAccentFor(row) }"
77
- />
78
- {{ planLabel(row) }}
79
- </span>
80
- </td>
81
- <td>
82
- <div class="sa-tenants__pills">
83
- <slot name="status-pills" :row="row">
84
- <StatusPill
85
- v-for="(p, i) in resolvedPills(row)"
86
- :key="i"
87
- :label="p.label"
88
- :icon="p.icon"
89
- :tone="p.tone"
90
- />
91
- </slot>
92
- </div>
93
- </td>
94
- <td v-if="usageFields.length > 0" class="num">
95
- <div
96
- v-for="(uf, i) in usageFields"
97
- :key="i"
98
- class="sa-tenants__usage"
99
- >
100
- <q-icon :name="uf.icon" size="11px" />
101
- {{ usageValue(row, uf) }}
66
+ <div>
67
+ <div class="sa-tenants__name">{{ row.name }}</div>
68
+ <div class="sa-tenants__slug">{{ row.slug }}</div>
102
69
  </div>
103
- </td>
104
- <td class="num sa-tenants__mono">
105
- {{ formatCreatedAt(row) }}
106
- </td>
107
- <td v-if="hasActions" class="num sa-tenants__actions">
108
- <slot name="row-actions" :row="row" :actions="visibleActions(row)">
109
- <component
110
- :is="action.to ? 'a' : 'button'"
111
- v-for="action in visibleActions(row)"
112
- :key="action.id"
113
- :href="action.to ? action.to(row) : undefined"
114
- class="sa-tenants__icon-btn"
115
- :class="
116
- action.tone
117
- ? `sa-tenants__icon-btn--${action.tone}`
118
- : ''
119
- "
120
- :title="action.label"
121
- @click="action.handler ? action.handler(row) : undefined"
122
- >
123
- <q-icon :name="action.icon" size="15px" />
124
- </component>
70
+ </div>
71
+ </q-td>
72
+ </template>
73
+
74
+ <template #body-cell-plan="{ row }">
75
+ <q-td>
76
+ <span class="sa-tenants__plan">
77
+ <span
78
+ class="sa-tenants__plan-dot"
79
+ :style="{ background: planAccentFor(row) }"
80
+ />
81
+ {{ planLabel(row) }}
82
+ </span>
83
+ </q-td>
84
+ </template>
85
+
86
+ <template #body-cell-status="{ row }">
87
+ <q-td>
88
+ <div class="sa-tenants__pills">
89
+ <slot name="status-pills" :row="row">
90
+ <StatusPill
91
+ v-for="(p, i) in resolvedPills(row)"
92
+ :key="i"
93
+ :label="p.label"
94
+ :icon="p.icon"
95
+ :tone="p.tone"
96
+ />
125
97
  </slot>
126
- </td>
127
- </tr>
128
- </tbody>
129
- </table>
130
- <div v-if="items.length === 0 && !loading" class="sa-tenants__empty">
131
- <q-icon name="search_off" size="32px" />
132
- <div>{{ msg.list.empty }}</div>
133
- </div>
134
- <div v-if="loading" class="sa-tenants__loading">
135
- <q-spinner size="20px" /> {{ common.loading }}
136
- </div>
137
- </div>
138
- </div>
139
-
140
- <footer class="sa-tenants__foot">
141
- {{ paginationLabel }}
142
- </footer>
98
+ </div>
99
+ </q-td>
100
+ </template>
101
+
102
+ <template #body-cell-usage="{ row }">
103
+ <q-td class="text-right">
104
+ <div v-for="(uf, i) in usageFields" :key="i" class="sa-tenants__usage">
105
+ <q-icon :name="uf.icon" size="11px" />
106
+ {{ usageValue(row, uf) }}
107
+ </div>
108
+ </q-td>
109
+ </template>
110
+
111
+ <template #body-cell-createdAt="{ row }">
112
+ <q-td class="text-right sa-tenants__mono">{{ formatCreatedAt(row) }}</q-td>
113
+ </template>
114
+
115
+ <template v-if="hasActions" #row-actions="{ row }">
116
+ <slot name="row-actions" :row="row" :actions="visibleActions(row)">
117
+ <component
118
+ :is="action.to ? 'a' : 'button'"
119
+ v-for="action in visibleActions(row)"
120
+ :key="action.id"
121
+ :href="action.to ? action.to(row) : undefined"
122
+ class="sa-tenants__icon-btn"
123
+ :class="action.tone ? `sa-tenants__icon-btn--${action.tone}` : ''"
124
+ :title="action.label"
125
+ @click="action.handler ? action.handler(row) : undefined"
126
+ >
127
+ <q-icon :name="action.icon" size="15px" />
128
+ </component>
129
+ </slot>
130
+ </template>
131
+ </AdminTable>
132
+ </AdminSection>
133
+ </AdminBody>
143
134
 
144
135
  <!-- Manifest-Driven Action Flow: only mounted when the `manifest` prop
145
136
  is set AND `manifestActionsEnabled` is true (default when manifest
@@ -162,14 +153,20 @@
162
153
  @cancel="manifestFlow.onConfirmCancel"
163
154
  />
164
155
  </template>
165
- </div>
156
+ </AdminPage>
166
157
  </template>
167
158
 
168
159
  <script setup lang="ts">
160
+ import AdminTable from '../components/admin-page/AdminTable.vue';
169
161
  import { computed, ref, watch } from 'vue';
162
+ import type { QTableColumn } from 'quasar';
163
+ import AdminBody from '../components/admin-page/AdminBody.vue';
164
+ import AdminFilters from '../components/admin-page/AdminFilters.vue';
165
+ import AdminHero from '../components/admin-page/AdminHero.vue';
166
+ import AdminSection from '../components/admin-page/AdminSection.vue';
167
+ import AdminPage from '../components/admin-page/AdminPage.vue';
170
168
  import type { AdminManifest, TenantActionDef, TenantDto, TenantListFilter } from '@saasicat/types';
171
169
  import type { HttpClient } from '../client/types.js';
172
- import { formatMessage } from '../client/i18n/format.js';
173
170
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
174
171
  import { useTenants } from '../vue/use-tenants.js';
175
172
  import {
@@ -336,6 +333,9 @@ const msg = useSaMessages('tenants');
336
333
  const common = useSaMessages('common');
337
334
  const { intlLocale } = useSuperAdminI18n();
338
335
 
336
+ const statusSelectOptions = computed(() =>
337
+ resolvedStatusOptions.value.map((o) => ({ label: o.label, value: o.value })),
338
+ );
339
339
  const resolvedStatusOptions = computed<ReadonlyArray<{ value: string; label: string }>>(
340
340
  () =>
341
341
  props.statusOptions ?? [
@@ -357,28 +357,42 @@ const list = useTenants<TenantRow>({
357
357
  http: props.http,
358
358
  getAuthToken: props.getAuthToken,
359
359
  });
360
- const { items, page, total, loading, error, goToPage, setPageSize } = list;
360
+ // Aliased: the `pageSize` prop is the INITIAL size, `list.pageSize` is the
361
+ // current one. Sharing one name makes the template ambiguous about which of
362
+ // the two it reads.
363
+ const { items, page, pageSize: rowsPerPage, total, loading, error, goToPage, setPageSize } = list;
361
364
 
362
365
  setPageSize(props.pageSize);
363
366
 
364
- const paginationLabel = computed(() =>
365
- formatMessage(msg.value.list.pagination, {
366
- total: total.value,
367
- page: page.value,
368
- pages: Math.max(1, Math.ceil(total.value / props.pageSize)),
369
- }),
370
- );
371
-
372
- let searchTimer: ReturnType<typeof setTimeout> | null = null;
373
- function reloadDebounced(): void {
374
- if (searchTimer) clearTimeout(searchTimer);
375
- searchTimer = setTimeout(applyFilter, 250);
376
- }
377
-
378
- function onClearSearch(): void {
379
- searchInput.value = '';
380
- applyFilter();
381
- }
367
+ const tenantColumns = computed<QTableColumn[]>(() => {
368
+ const cols: QTableColumn[] = [
369
+ { name: 'tenant', label: msg.value.tenant, field: 'name', align: 'left' },
370
+ ];
371
+ if (props.showPlanColumn) {
372
+ cols.push({
373
+ name: 'plan',
374
+ label: resolvedPlanColumnLabel.value,
375
+ field: 'plan',
376
+ align: 'left',
377
+ });
378
+ }
379
+ cols.push({ name: 'status', label: common.value.status, field: 'status', align: 'left' });
380
+ if (props.usageFields.length > 0) {
381
+ cols.push({
382
+ name: 'usage',
383
+ label: msg.value.list.columnUsage,
384
+ field: 'id',
385
+ align: 'right',
386
+ });
387
+ }
388
+ cols.push({
389
+ name: 'createdAt',
390
+ label: msg.value.list.columnCreatedAt,
391
+ field: 'createdAt',
392
+ align: 'right',
393
+ });
394
+ return cols;
395
+ });
382
396
 
383
397
  function applyFilter(): void {
384
398
  // The status value is passed through unchanged. Apps configure the values
@@ -434,7 +448,6 @@ if (manifestFlow && typeof window !== 'undefined') {
434
448
  manifestFlow.realOrphans,
435
449
  (orphans) => {
436
450
  if (orphans.length > 0) {
437
- // eslint-disable-next-line no-console
438
451
  console.warn(
439
452
  '[PlatformTenantsPage] Manifest actions without a handler in createSuperAdminApp({ actions }):',
440
453
  orphans,
@@ -456,7 +469,6 @@ const combinedActions = computed<TenantRowAction[]>(() => {
456
469
  const filteredCustom = custom.filter((a) => {
457
470
  if (a.actionKey && manifestKeys.has(a.actionKey)) {
458
471
  if (typeof window !== 'undefined') {
459
- // eslint-disable-next-line no-console
460
472
  console.warn(
461
473
  `[PlatformTenantsPage] Custom action "${a.id}" (actionKey="${a.actionKey}") ` +
462
474
  `collides with a manifest action — the manifest wins.`,
@@ -518,111 +530,17 @@ function visibleActions(row: TenantRow): TenantRowAction[] {
518
530
  </script>
519
531
 
520
532
  <style scoped>
521
- .sa-tenants {
522
- min-height: calc(100vh - 56px);
523
- background: var(--sa-bg-app, #f1f5f9);
524
- }
525
- .sa-tenants__head {
526
- padding: 20px 28px 8px;
527
- }
528
- .sa-tenants__title {
529
- margin: 0;
530
- font-family: var(--sa-font-head, system-ui, sans-serif);
531
- font-weight: 700;
532
- font-size: 22px;
533
- color: var(--sa-heading, #0f172a);
534
- letter-spacing: -0.01em;
535
- }
536
- .sa-tenants__sub {
537
- margin: 4px 0 0;
538
- color: var(--sa-muted-dark, #475569);
539
- font-size: 13.5px;
540
- line-height: 1.5;
541
- }
542
-
543
- .sa-tenants__filter {
544
- padding: 12px 28px;
545
- display: flex;
546
- gap: 10px;
547
- flex-wrap: wrap;
548
- }
549
- .sa-tenants__search {
550
- flex: 1;
551
- min-width: 240px;
552
- display: flex;
553
- align-items: center;
554
- gap: 6px;
555
- background: #fff;
556
- border: 1px solid var(--sa-border, #e2e8f0);
557
- border-radius: 8px;
558
- padding: 6px 10px;
559
- }
560
- .sa-tenants__search :deep(.q-icon) {
561
- color: var(--sa-muted, #64748b);
562
- }
563
- .sa-tenants__search input {
564
- flex: 1;
565
- border: none;
566
- outline: none;
567
- background: transparent;
568
- font-size: 13px;
569
- font-family: inherit;
570
- color: var(--sa-body, #1e293b);
571
- min-width: 0;
572
- }
573
- .sa-tenants__clear {
574
- background: transparent;
575
- border: none;
576
- cursor: pointer;
577
- padding: 2px;
578
- }
579
- .sa-tenants__select {
580
- padding: 7px 10px;
581
- font-size: 12.5px;
582
- font-family: inherit;
583
- border: 1px solid var(--sa-border, #e2e8f0);
584
- border-radius: 8px;
585
- background: #fff;
586
- cursor: pointer;
587
- }
588
-
589
- .sa-tenants__card {
590
- margin: 0 28px 28px;
591
- background: #fff;
592
- border: 1px solid var(--sa-border, #e2e8f0);
593
- border-radius: 12px;
594
- overflow: hidden;
595
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
596
- }
597
- .sa-tenants__wrap {
598
- overflow-x: auto;
599
- }
600
- table {
601
- width: 100%;
602
- border-collapse: collapse;
603
- font-size: 13px;
604
- min-width: 800px;
605
- }
606
- thead tr {
607
- background: #fafbfc;
608
- }
609
- th {
610
- padding: 10px 14px;
611
- font-size: 10.5px;
612
- font-weight: 700;
613
- letter-spacing: 0.08em;
614
- color: var(--sa-muted, #64748b);
615
- text-transform: uppercase;
616
- text-align: left;
617
- }
618
- td {
619
- padding: 11px 14px;
620
- vertical-align: middle;
621
- border-top: 1px solid var(--sa-border-soft, #f1f5f9);
622
- }
623
- .num {
624
- text-align: right;
625
- }
533
+ /* No table rules here on purpose — and not because they would be inert.
534
+ Content passed into a child component's slot keeps THIS component's scope
535
+ id, so the `table`, `thead`, `th`, `td` and `.num` rules that used to sit
536
+ here did reach `AdminTable`'s cells. That was the bug: they drew a second
537
+ border on top of the one AdminTable already draws, and the tenant list was
538
+ the only list in the package with doubled row separators. Removing them
539
+ took `border-top-width` from 1px back to 0px.
540
+
541
+ The rule to carry forward: styling a table here means overriding AdminTable
542
+ for one page. If a table needs to look different, that belongs in
543
+ AdminTable, behind a prop. */
626
544
  .sa-tenants__mono {
627
545
  font-family: var(--sa-font-mono, ui-monospace, monospace);
628
546
  font-size: 12px;
@@ -648,11 +566,11 @@ td {
648
566
  .sa-tenants__name {
649
567
  font-size: 13px;
650
568
  font-weight: 600;
651
- color: var(--sa-heading, #0f172a);
569
+ color: var(--sa-heading, var(--sa-heading));
652
570
  }
653
571
  .sa-tenants__slug {
654
572
  font-size: 11px;
655
- color: var(--sa-muted, #64748b);
573
+ color: var(--sa-muted, var(--sa-muted));
656
574
  font-family: var(--sa-font-mono, ui-monospace, monospace);
657
575
  }
658
576
 
@@ -678,16 +596,13 @@ td {
678
596
  align-items: center;
679
597
  gap: 4px;
680
598
  font-size: 12px;
681
- color: var(--sa-muted-dark, #475569);
599
+ color: var(--sa-muted-dark, var(--sa-muted-dark));
682
600
  margin-left: 8px;
683
601
  }
684
602
  .sa-tenants__usage :deep(.q-icon) {
685
- color: var(--sa-muted, #64748b);
603
+ color: var(--sa-muted, var(--sa-muted));
686
604
  }
687
605
 
688
- .sa-tenants__actions {
689
- white-space: nowrap;
690
- }
691
606
  .sa-tenants__icon-btn {
692
607
  background: transparent;
693
608
  border: none;
@@ -699,10 +614,10 @@ td {
699
614
  align-items: center;
700
615
  justify-content: center;
701
616
  text-decoration: none;
702
- color: var(--sa-muted, #64748b);
617
+ color: var(--sa-muted, var(--sa-muted));
703
618
  }
704
619
  .sa-tenants__icon-btn:hover {
705
- background: #f1f5f9;
620
+ background: var(--sa-border-soft);
706
621
  }
707
622
  .sa-tenants__icon-btn :deep(.q-icon) {
708
623
  color: inherit;
@@ -722,24 +637,4 @@ td {
722
637
  .sa-tenants__icon-btn--accent {
723
638
  color: var(--sa-accent, #7c3aed);
724
639
  }
725
-
726
- .sa-tenants__empty {
727
- padding: 40px 16px;
728
- text-align: center;
729
- color: var(--sa-muted, #64748b);
730
- }
731
- .sa-tenants__loading {
732
- padding: 24px;
733
- display: flex;
734
- align-items: center;
735
- gap: 12px;
736
- color: var(--sa-muted, #64748b);
737
- }
738
-
739
- .sa-tenants__foot {
740
- padding: 0 28px 28px;
741
- text-align: right;
742
- font-size: 12px;
743
- color: var(--sa-muted, #64748b);
744
- }
745
640
  </style>