@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,70 +1,22 @@
1
1
  <template>
2
- <div class="pl">
3
- <!-- Page header -->
4
- <div class="pl-page-head">
5
- <div>
6
- <h2 class="pl-h-title">{{ msg.list.title }}</h2>
7
- <p class="pl-h-sub">{{ summary }}</p>
8
- </div>
9
- <div class="pl-head-actions">
10
- <button class="pl-btn" type="button" @click="$emit('showMatrix')">
11
- <svg
12
- width="14"
13
- height="14"
14
- viewBox="0 0 24 24"
15
- fill="none"
16
- stroke="currentColor"
17
- stroke-width="2"
18
- >
19
- <rect x="3" y="3" width="7" height="7" rx="1" />
20
- <rect x="14" y="3" width="7" height="7" rx="1" />
21
- <rect x="3" y="14" width="7" height="7" rx="1" />
22
- <rect x="14" y="14" width="7" height="7" rx="1" />
23
- </svg>
24
- <span>{{ msg.list.matrixView }}</span>
25
- </button>
26
- <button class="pl-btn pl-btn--primary" type="button" @click="$emit('createPlan')">
27
- <svg
28
- width="14"
29
- height="14"
30
- viewBox="0 0 24 24"
31
- fill="none"
32
- stroke="currentColor"
33
- stroke-width="2.5"
34
- >
35
- <path d="M12 5v14M5 12h14" />
36
- </svg>
37
- <span>{{ msg.list.newPlan }}</span>
38
- </button>
39
- </div>
40
- </div>
41
-
2
+ <div class="sa-plan-list">
42
3
  <!-- List wrapper -->
43
- <div class="pl-wrap">
44
- <div class="pl-toolbar">
45
- <div class="pl-search">
46
- <span class="pl-search-ico" aria-hidden="true">
47
- <svg
48
- width="14"
49
- height="14"
50
- viewBox="0 0 24 24"
51
- fill="none"
52
- stroke="currentColor"
53
- stroke-width="2"
54
- >
55
- <circle cx="11" cy="11" r="7" />
56
- <path d="M21 21l-4.35-4.35" />
57
- </svg>
58
- </span>
59
- <input v-model="search" :placeholder="msg.list.searchPlaceholder" />
60
- <span class="pl-kbd">⌘ K</span>
61
- </div>
62
- <div class="pl-toolbar-spacer" />
63
- <div class="pl-sortinfo">{{ msg.list.sortedBy }}</div>
4
+ <div class="sa-plan-list-wrap">
5
+ <div class="sa-plan-list-toolbar">
6
+ <q-input
7
+ v-model="search"
8
+ dense
9
+ outlined
10
+ clearable
11
+ :placeholder="msg.list.searchPlaceholder"
12
+ >
13
+ <template #prepend><q-icon name="search" /></template>
14
+ </q-input>
15
+ <div class="sa-plan-list-sortinfo">{{ msg.list.sortedBy }}</div>
64
16
  </div>
65
17
 
66
- <div class="pl-list">
67
- <div class="pl-list-head">
18
+ <div class="sa-plan-list-list">
19
+ <div class="sa-plan-list-list-head">
68
20
  <div>{{ msg.list.columnPlan }}</div>
69
21
  <div>{{ common.status }}</div>
70
22
  <div>{{ msg.list.columnVersion }}</div>
@@ -73,7 +25,7 @@
73
25
  <div />
74
26
  </div>
75
27
 
76
- <div v-if="filteredPlans.length === 0" class="pl-empty">
28
+ <div v-if="filteredPlans.length === 0" class="sa-plan-list-empty">
77
29
  <template v-if="resolvedPlans.length === 0">
78
30
  {{ msg.list.emptyNoPlans }}
79
31
  </template>
@@ -83,13 +35,16 @@
83
35
  <template v-for="p in filteredPlans" :key="p.plan.id">
84
36
  <!-- Parent row: currently live version (or first future / nothing) -->
85
37
  <div
86
- :class="['pl-row', { 'pl-row--new': highlightPlanKey === p.planKey }]"
38
+ :class="[
39
+ 'sa-plan-list-row',
40
+ { 'sa-plan-list-row--new': highlightPlanKey === p.planKey },
41
+ ]"
87
42
  @click="$emit('openPlan', p.plan)"
88
43
  >
89
- <div class="pl-cell pl-cell--name">
90
- <div class="pl-plan-name">
44
+ <div class="sa-plan-list-cell sa-plan-list-cell--name">
45
+ <div class="sa-plan-list-plan-name">
91
46
  <div
92
- class="pl-plan-mark"
47
+ class="sa-plan-list-plan-mark"
93
48
  :style="{
94
49
  background: planAccent(p.planKey) + '15',
95
50
  color: planAccent(p.planKey),
@@ -98,49 +53,60 @@
98
53
  >
99
54
  {{ p.planKey.slice(0, 3) }}
100
55
  </div>
101
- <div class="pl-plan-titles">
102
- <div class="pl-plan-title">
56
+ <div class="sa-plan-list-plan-titles">
57
+ <div class="sa-plan-list-plan-title">
103
58
  {{ p.label }}
104
59
  <span
105
60
  v-if="highlightPlanKey === p.planKey"
106
- class="pl-chip pl-chip--new"
61
+ class="sa-plan-list-chip sa-plan-list-chip--new"
107
62
  >{{ msg.list.badgeNew }}</span
108
63
  >
109
64
  </div>
110
- <div class="pl-plan-desc">{{ p.description || '—' }}</div>
65
+ <div class="sa-plan-list-plan-desc">
66
+ {{ p.description || '—' }}
67
+ </div>
111
68
  </div>
112
69
  </div>
113
70
  </div>
114
71
 
115
- <div class="pl-cell pl-cell--status">
72
+ <div class="sa-plan-list-cell sa-plan-list-cell--status">
116
73
  <template v-if="p.currentLive">
117
- <span class="pl-chip pl-chip--live pl-chip--dot">{{
118
- msg.list.chipLive
119
- }}</span>
74
+ <span
75
+ class="sa-plan-list-chip sa-plan-list-chip--live sa-plan-list-chip--dot"
76
+ >{{ msg.list.chipLive }}</span
77
+ >
120
78
  <span
121
79
  v-if="!p.currentLive.marketed"
122
- class="pl-chip pl-chip--supersed pl-chip--tiny"
80
+ class="sa-plan-list-chip sa-plan-list-chip--supersed sa-plan-list-chip--tiny"
123
81
  >{{ msg.list.chipPrivate }}</span
124
82
  >
125
83
  </template>
126
84
  <template v-else-if="p.primary && p.primary.publishedAt">
127
- <span class="pl-chip pl-chip--scheduled pl-chip--dot">
85
+ <span
86
+ class="sa-plan-list-chip sa-plan-list-chip--scheduled sa-plan-list-chip--dot"
87
+ >
128
88
  {{ validFromLabel(p.primary.validFrom) }}
129
89
  </span>
130
90
  </template>
131
91
  <template v-else>
132
- <span class="pl-chip pl-chip--supersed pl-chip--dot">{{
133
- msg.list.chipNoLive
134
- }}</span>
92
+ <span
93
+ class="sa-plan-list-chip sa-plan-list-chip--supersed sa-plan-list-chip--dot"
94
+ >{{ msg.list.chipNoLive }}</span
95
+ >
135
96
  </template>
136
97
  </div>
137
98
 
138
- <div class="pl-cell">
139
- <div v-if="p.primary" class="pl-version-num">
99
+ <div class="sa-plan-list-cell">
100
+ <div v-if="p.primary" class="sa-plan-list-version-num">
140
101
  v{{ p.primary.version }}
141
102
  </div>
142
- <div v-else class="pl-version-num pl-version-num--muted">—</div>
143
- <div v-if="p.primary?.validFrom" class="pl-version-sub">
103
+ <div
104
+ v-else
105
+ class="sa-plan-list-version-num sa-plan-list-version-num--muted"
106
+ >
107
+ —
108
+ </div>
109
+ <div v-if="p.primary?.validFrom" class="sa-plan-list-version-sub">
144
110
  {{
145
111
  validityRange(
146
112
  p.currentLive ? msg.list.since : msg.list.from,
@@ -151,9 +117,9 @@
151
117
  </div>
152
118
  </div>
153
119
 
154
- <div class="pl-cell">
120
+ <div class="sa-plan-list-cell">
155
121
  <template v-if="!p.primary">
156
- <span class="pl-price-text">—</span>
122
+ <span class="sa-plan-list-price-text">—</span>
157
123
  </template>
158
124
  <template
159
125
  v-else-if="
@@ -161,29 +127,31 @@
161
127
  Number(p.primary.yearlyNet) === 0
162
128
  "
163
129
  >
164
- <span class="pl-price-text">{{ msg.list.priceFree }}</span>
130
+ <span class="sa-plan-list-price-text">{{
131
+ msg.list.priceFree
132
+ }}</span>
165
133
  </template>
166
134
  <template v-else>
167
135
  <div>
168
- <span class="pl-price-big">{{
136
+ <span class="sa-plan-list-price-big">{{
169
137
  formatMoney(p.primary.monthlyNet)
170
138
  }}</span>
171
- <span class="pl-price-unit">{{
139
+ <span class="sa-plan-list-price-unit">{{
172
140
  ' ' + msg.list.perMonthShort
173
141
  }}</span>
174
142
  </div>
175
- <div class="pl-price-sub">
143
+ <div class="sa-plan-list-price-sub">
176
144
  {{ formatMoney(p.primary.yearlyNet) }}
177
145
  {{ msg.list.perYearShort }}
178
146
  </div>
179
147
  </template>
180
148
  </div>
181
149
 
182
- <div class="pl-cell pl-cell--tenants">
183
- <span class="pl-tenant-num">{{ p.tenantCount }}</span>
184
- <div class="pl-tenant-bar">
150
+ <div class="sa-plan-list-cell sa-plan-list-cell--tenants">
151
+ <span class="sa-plan-list-tenant-num">{{ p.tenantCount }}</span>
152
+ <div class="sa-plan-list-tenant-bar">
185
153
  <div
186
- class="pl-tenant-bar-fill"
154
+ class="sa-plan-list-tenant-bar-fill"
187
155
  :style="{
188
156
  width: tenantBarWidth(p.tenantCount),
189
157
  background: planAccent(p.planKey),
@@ -192,10 +160,10 @@
192
160
  </div>
193
161
  </div>
194
162
 
195
- <div class="pl-cell pl-cell--actions" @click.stop>
163
+ <div class="sa-plan-list-cell sa-plan-list-cell--actions" @click.stop>
196
164
  <button
197
165
  v-if="hasAnyPublished(p)"
198
- class="pl-btn pl-btn--sm pl-btn--ghost"
166
+ class="sa-btn sa-btn--sm sa-btn--ghost"
199
167
  type="button"
200
168
  disabled
201
169
  :title="msg.list.actionDeleteBlocked"
@@ -215,7 +183,7 @@
215
183
  </button>
216
184
  <button
217
185
  v-else
218
- class="pl-btn pl-btn--sm pl-btn--ghost pl-btn--danger"
186
+ class="sa-btn sa-btn--sm sa-btn--ghost sa-btn--danger"
219
187
  type="button"
220
188
  :title="msg.list.actionDeletePlan"
221
189
  @click="$emit('archivePlan', p.plan, false)"
@@ -234,7 +202,7 @@
234
202
  </svg>
235
203
  </button>
236
204
  <button
237
- class="pl-btn pl-btn--sm pl-btn--ghost"
205
+ class="sa-btn sa-btn--sm sa-btn--ghost"
238
206
  type="button"
239
207
  :title="msg.list.actionClonePlan"
240
208
  @click="$emit('clonePlan', p.plan)"
@@ -252,7 +220,7 @@
252
220
  </svg>
253
221
  </button>
254
222
  <button
255
- class="pl-btn pl-btn--sm pl-btn--ghost"
223
+ class="sa-btn sa-btn--sm sa-btn--ghost"
256
224
  type="button"
257
225
  :title="msg.list.actionNewVersion"
258
226
  :disabled="!!p.draft"
@@ -272,7 +240,7 @@
272
240
  </svg>
273
241
  </button>
274
242
  <button
275
- class="pl-btn pl-btn--sm"
243
+ class="sa-btn sa-btn--sm"
276
244
  type="button"
277
245
  :title="msg.list.actionOpenPlan"
278
246
  @click="$emit('openPlan', p.plan)"
@@ -295,86 +263,92 @@
295
263
  <div
296
264
  v-for="sub in p.subRows"
297
265
  :key="`${p.plan.id}-${sub.id}`"
298
- class="pl-row pl-row--sub"
266
+ class="sa-plan-list-row sa-plan-list-row--sub"
299
267
  @click.stop="$emit('openPlan', p.plan)"
300
268
  >
301
- <div class="pl-cell pl-cell--name pl-cell--sub-name">
302
- <div class="pl-sub-tree" aria-hidden="true">
303
- <span class="pl-sub-tree-elbow" />
269
+ <div
270
+ class="sa-plan-list-cell sa-plan-list-cell--name sa-plan-list-cell--sub-name"
271
+ >
272
+ <div class="sa-plan-list-sub-tree" aria-hidden="true">
273
+ <span class="sa-plan-list-sub-tree-elbow" />
304
274
  </div>
305
- <div class="pl-sub-titles">
306
- <div class="pl-sub-title">
275
+ <div class="sa-plan-list-sub-titles">
276
+ <div class="sa-plan-list-sub-title">
307
277
  v{{ sub.version }}
308
278
  <template v-if="sub.publishedAt === null">
309
279
  <span
310
- class="pl-chip pl-chip--draft pl-chip--dot pl-chip--tiny"
280
+ class="sa-plan-list-chip sa-plan-list-chip--draft sa-plan-list-chip--dot sa-plan-list-chip--tiny"
311
281
  >{{ msg.list.chipDraft }}</span
312
282
  >
313
283
  </template>
314
284
  <template v-else>
315
285
  <span
316
- class="pl-chip pl-chip--scheduled pl-chip--dot pl-chip--tiny"
286
+ class="sa-plan-list-chip sa-plan-list-chip--scheduled sa-plan-list-chip--dot sa-plan-list-chip--tiny"
317
287
  >{{ msg.list.chipScheduled }}</span
318
288
  >
319
289
  </template>
320
290
  </div>
321
- <div class="pl-sub-desc">
291
+ <div class="sa-plan-list-sub-desc">
322
292
  {{ sub.changeNote || msg.list.noChangeNote }}
323
293
  </div>
324
294
  </div>
325
295
  </div>
326
296
 
327
- <div class="pl-cell pl-cell--status">
297
+ <div class="sa-plan-list-cell sa-plan-list-cell--status">
328
298
  <span
329
299
  v-if="sub.publishedAt === null"
330
- class="pl-chip pl-chip--draft pl-chip--dot pl-chip--tiny"
300
+ class="sa-plan-list-chip sa-plan-list-chip--draft sa-plan-list-chip--dot sa-plan-list-chip--tiny"
331
301
  >{{ msg.list.chipDraft }}</span
332
302
  >
333
303
  <span
334
304
  v-else
335
- class="pl-chip pl-chip--scheduled pl-chip--dot pl-chip--tiny"
305
+ class="sa-plan-list-chip sa-plan-list-chip--scheduled sa-plan-list-chip--dot sa-plan-list-chip--tiny"
336
306
  >{{ validFromLabel(sub.validFrom) }}</span
337
307
  >
338
308
  </div>
339
309
 
340
- <div class="pl-cell">
341
- <div class="pl-version-num pl-version-num--sub">v{{ sub.version }}</div>
342
- <div v-if="sub.validFrom" class="pl-version-sub">
310
+ <div class="sa-plan-list-cell">
311
+ <div class="sa-plan-list-version-num sa-plan-list-version-num--sub">
312
+ v{{ sub.version }}
313
+ </div>
314
+ <div v-if="sub.validFrom" class="sa-plan-list-version-sub">
343
315
  {{ validityRange(msg.list.from, sub.validFrom, sub.validUntil) }}
344
316
  </div>
345
317
  </div>
346
318
 
347
- <div class="pl-cell">
319
+ <div class="sa-plan-list-cell">
348
320
  <template
349
321
  v-if="Number(sub.monthlyNet) === 0 && Number(sub.yearlyNet) === 0"
350
322
  >
351
- <span class="pl-price-text">{{ msg.list.priceFree }}</span>
323
+ <span class="sa-plan-list-price-text">{{
324
+ msg.list.priceFree
325
+ }}</span>
352
326
  </template>
353
327
  <template v-else>
354
328
  <div>
355
- <span class="pl-price-big">{{
329
+ <span class="sa-plan-list-price-big">{{
356
330
  formatMoney(sub.monthlyNet)
357
331
  }}</span>
358
- <span class="pl-price-unit">{{
332
+ <span class="sa-plan-list-price-unit">{{
359
333
  ' ' + msg.list.perMonthShort
360
334
  }}</span>
361
335
  </div>
362
- <div class="pl-price-sub">
336
+ <div class="sa-plan-list-price-sub">
363
337
  {{ formatMoney(sub.yearlyNet) }} {{ msg.list.perYearShort }}
364
338
  </div>
365
339
  </template>
366
340
  </div>
367
341
 
368
- <div class="pl-cell pl-cell--sub-impact">
369
- <span class="pl-version-sub">{{
342
+ <div class="sa-plan-list-cell sa-plan-list-cell--sub-impact">
343
+ <span class="sa-plan-list-version-sub">{{
370
344
  tenantCountLabel(p.tenantCount)
371
345
  }}</span>
372
346
  </div>
373
347
 
374
- <div class="pl-cell pl-cell--actions" @click.stop>
348
+ <div class="sa-plan-list-cell sa-plan-list-cell--actions" @click.stop>
375
349
  <button
376
350
  v-if="sub.publishedAt === null"
377
- class="pl-btn pl-btn--sm pl-btn--ghost pl-btn--danger"
351
+ class="sa-btn sa-btn--sm sa-btn--ghost sa-btn--danger"
378
352
  type="button"
379
353
  :title="discardDraftTitle(sub.version)"
380
354
  @click="$emit('discardDraft', p.plan, sub)"
@@ -394,7 +368,7 @@
394
368
  </button>
395
369
  <button
396
370
  v-if="sub.publishedAt === null"
397
- class="pl-btn pl-btn--sm pl-btn--ghost"
371
+ class="sa-btn sa-btn--sm sa-btn--ghost"
398
372
  type="button"
399
373
  :title="msg.list.actionEditDraft"
400
374
  @click="$emit('editDraft', p.plan, sub)"
@@ -413,7 +387,7 @@
413
387
  </svg>
414
388
  </button>
415
389
  <button
416
- class="pl-btn pl-btn--sm"
390
+ class="sa-btn sa-btn--sm"
417
391
  type="button"
418
392
  :title="msg.list.actionOpenInCockpit"
419
393
  @click="$emit('openPlan', p.plan)"
@@ -438,6 +412,7 @@
438
412
  </template>
439
413
 
440
414
  <script setup lang="ts">
415
+ import { resolvePlans, type ResolvedPlan } from '../../client/resolve-plans.js';
441
416
  import { computed, ref } from 'vue';
442
417
  import type { PlanRow, PlanVersionRow } from '@saasicat/types';
443
418
  import { formatMessage } from '../../client/i18n/format.js';
@@ -467,40 +442,19 @@ const props = withDefaults(
467
442
 
468
443
  const emit = defineEmits<{
469
444
  (e: 'openPlan', plan: PlanRow): void;
470
- (e: 'createPlan'): void;
471
445
  (e: 'clonePlan', plan: PlanRow): void;
472
446
  (e: 'newVersion', plan: PlanRow, basis: PlanVersionRow): void;
473
447
  (e: 'editDraft', plan: PlanRow, draft: PlanVersionRow): void;
474
448
  (e: 'discardDraft', plan: PlanRow, draft: PlanVersionRow): void;
475
449
  (e: 'archivePlan', plan: PlanRow, hasLive: boolean): void;
476
- (e: 'showMatrix'): void;
477
450
  }>();
478
451
 
479
452
  const msg = useSaMessages('plans');
480
453
  const { locale, intlLocale } = useSuperAdminI18n();
481
454
  const common = useSaMessages('common');
482
455
 
483
- const search = ref('');
484
-
485
- interface ResolvedPlan {
486
- plan: PlanRow;
487
- planKey: string;
488
- label: string;
489
- description: string | null;
490
- /** Version currently active for new bookings (validFrom ≤ today < validUntil or validUntil null). */
491
- currentLive: PlanVersionRow | null;
492
- /** Version shown on the parent row (currentLive preferred). */
493
- primary: PlanVersionRow | null;
494
- /** Drafts + future-published versions (sorted: future ASC by validFrom, drafts at the end). */
495
- subRows: PlanVersionRow[];
496
- /** True if the plan has any versions at all (for hide logic). */
497
- hasAnyVersion: boolean;
498
- /** True if all versions are expired (validUntil < today, no draft / no future). */
499
- allExpired: boolean;
500
- /** First open draft (for the header KPI "offene Drafts"). */
501
- draft: PlanVersionRow | null;
502
- tenantCount: number;
503
- }
456
+ // `clearable` emits null, not '' — see Quasar's use-field clearValue().
457
+ const search = ref<string | null>('');
504
458
 
505
459
  const DEFAULT_ACCENTS: Record<string, string> = {
506
460
  STARTER: '#64748b',
@@ -522,91 +476,19 @@ function planAccent(planKey: string): string {
522
476
  return FALLBACK_ACCENTS[idx % FALLBACK_ACCENTS.length] ?? FALLBACK_ACCENTS[0]!;
523
477
  }
524
478
 
525
- function todayIsoDate(): string {
526
- return new Date().toISOString().slice(0, 10);
527
- }
528
-
529
- function isCurrentlyValid(v: PlanVersionRow, today: string): boolean {
530
- if (v.publishedAt === null) return false;
531
- if (v.validFrom && v.validFrom.slice(0, 10) > today) return false;
532
- if (v.validUntil && v.validUntil.slice(0, 10) < today) return false;
533
- return true;
534
- }
535
-
536
- function isFutureScheduled(v: PlanVersionRow, today: string): boolean {
537
- if (v.publishedAt === null) return false;
538
- if (!v.validFrom) return false;
539
- return v.validFrom.slice(0, 10) > today;
540
- }
541
-
542
- function isExpired(v: PlanVersionRow, today: string): boolean {
543
- if (v.publishedAt === null) return false;
544
- if (!v.validUntil) return false;
545
- return v.validUntil.slice(0, 10) < today;
546
- }
547
-
548
- const resolvedPlans = computed<ResolvedPlan[]>(() => {
549
- const today = todayIsoDate();
550
- return [...props.plans]
551
- .sort((a, b) => a.sortOrder - b.sortOrder || a.planKey.localeCompare(b.planKey))
552
- .map<ResolvedPlan>((plan) => {
553
- const versions = props.versionsByPlanId[plan.id] ?? [];
554
- const drafts = versions.filter((v) => v.publishedAt === null);
555
- const published = versions.filter((v) => v.publishedAt !== null);
556
- const currentLive = published.find((v) => isCurrentlyValid(v, today)) ?? null;
557
- const futureScheduled = published
558
- .filter((v) => isFutureScheduled(v, today))
559
- .sort((a, b) => (a.validFrom ?? '').localeCompare(b.validFrom ?? ''));
560
-
561
- // Parent = currentLive preferred; otherwise the next upcoming
562
- // future version; otherwise null (drafts only → root row without a version).
563
- const primary = currentLive ?? futureScheduled[0] ?? null;
564
-
565
- // Sub-rows = all visible versions except the parent
566
- const subSet = new Set<string>();
567
- if (primary) subSet.add(primary.id);
568
- const subRows: PlanVersionRow[] = [];
569
- for (const v of futureScheduled) {
570
- if (!subSet.has(v.id)) {
571
- subRows.push(v);
572
- subSet.add(v.id);
573
- }
574
- }
575
- for (const d of drafts) {
576
- if (!subSet.has(d.id)) {
577
- subRows.push(d);
578
- subSet.add(d.id);
579
- }
580
- }
581
-
582
- const allExpired =
583
- versions.length > 0 &&
584
- drafts.length === 0 &&
585
- published.every((v) => isExpired(v, today)) &&
586
- futureScheduled.length === 0;
587
-
588
- return {
589
- plan,
590
- planKey: plan.planKey,
591
- label: plan.label,
592
- description: plan.description ?? null,
593
- currentLive,
594
- primary,
595
- subRows,
596
- hasAnyVersion: versions.length > 0,
597
- allExpired,
598
- draft: drafts[0] ?? null,
599
- tenantCount: props.tenantCountsByPlanKey[plan.planKey] ?? 0,
600
- };
601
- });
602
- });
479
+ const resolvedPlans = computed(() =>
480
+ resolvePlans({
481
+ plans: props.plans,
482
+ versionsByPlanId: props.versionsByPlanId,
483
+ tenantCountsByPlanKey: props.tenantCountsByPlanKey,
484
+ }),
485
+ );
603
486
 
604
487
  const filteredPlans = computed(() => {
605
488
  // Plans with only expired versions are hidden entirely
606
- // (SPEC_V2 §4.2.1: only currently-valid + future ones stay visible
607
- // in the admin listing).
489
+ // — only currently-valid and future ones stay visible in the admin listing.
608
490
  const base = resolvedPlans.value.filter((p) => !p.allExpired);
609
- const q = search.value.trim().toLocaleLowerCase(intlLocale.value);
491
+ const q = (search.value ?? '').trim().toLocaleLowerCase(intlLocale.value);
610
492
  if (!q) return base;
611
493
  return base.filter(
612
494
  (p) =>
@@ -615,21 +497,8 @@ const filteredPlans = computed(() => {
615
497
  );
616
498
  });
617
499
 
618
- const liveCount = computed(() => resolvedPlans.value.filter((p) => p.currentLive !== null).length);
619
- const draftCount = computed(() => resolvedPlans.value.filter((p) => p.draft !== null).length);
620
- const totalTenants = computed(() => resolvedPlans.value.reduce((sum, p) => sum + p.tenantCount, 0));
621
-
622
- const summary = computed(() =>
623
- formatMessage(msg.value.list.summary, {
624
- plans: resolvedPlans.value.length,
625
- live: liveCount.value,
626
- drafts: draftCount.value,
627
- tenants: totalTenants.value,
628
- }),
629
- );
630
-
631
500
  const emptyNoMatch = computed(() =>
632
- formatMessage(msg.value.list.emptyNoMatch, { query: search.value }),
501
+ formatMessage(msg.value.list.emptyNoMatch, { query: search.value ?? '' }),
633
502
  );
634
503
 
635
504
  function validFromLabel(validFrom: string | null | undefined): string {
@@ -661,7 +530,7 @@ function tenantBarWidth(count: number): string {
661
530
  return `${Math.min(100, count / 1.5)}%`;
662
531
  }
663
532
 
664
- function onNewVersion(row: ResolvedPlan): void {
533
+ function onNewVersion(row: ResolvedPlan<PlanRow, PlanVersionRow>): void {
665
534
  if (row.draft) return; // already an open draft → no new one
666
535
  const basis = row.currentLive;
667
536
  if (!basis) {
@@ -672,7 +541,7 @@ function onNewVersion(row: ResolvedPlan): void {
672
541
  emit('newVersion', row.plan, basis);
673
542
  }
674
543
 
675
- function hasAnyPublished(row: ResolvedPlan): boolean {
544
+ function hasAnyPublished(row: ResolvedPlan<PlanRow, PlanVersionRow>): boolean {
676
545
  // Superseded or expired counts too — the plan root stays in the DB
677
546
  // forever for contract-protection P1 reasons.
678
547
  const versions = props.versionsByPlanId[row.plan.id] ?? [];
@@ -681,230 +550,110 @@ function hasAnyPublished(row: ResolvedPlan): boolean {
681
550
  </script>
682
551
 
683
552
  <style scoped>
684
- .pl {
685
- --pl-bg: #f6f7f9;
686
- --pl-surface: #ffffff;
687
- --pl-surface-2: #f8fafc;
688
- --pl-border: #e5e7eb;
689
- --pl-border-strong: #d1d5db;
690
- --pl-text: #0f172a;
691
- --pl-text-2: #475569;
692
- --pl-text-3: #94a3b8;
693
- --pl-primary: #2563eb;
694
- --pl-primary-700: #1d4ed8;
695
- --pl-live-bg: #ecfdf5;
696
- --pl-draft-bg: #fffbeb;
697
- --pl-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
698
- --pl-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
699
-
700
- padding: 22px 26px;
701
- background: var(--pl-bg);
702
- color: var(--pl-text);
703
- font-family: var(--pl-font-sans);
553
+ .sa-plan-list {
554
+ /* padding: 22px 26px; */
555
+ background: var(--sa-bg-app);
556
+ color: var(--sa-heading);
557
+ font-family: var(--sa-font-body);
704
558
  min-height: 100%;
705
559
  box-sizing: border-box;
706
560
  }
707
- .pl * {
708
- box-sizing: border-box;
709
- }
710
-
711
- .pl-page-head {
712
- display: flex;
713
- align-items: flex-end;
714
- gap: 20px;
715
- margin-bottom: 18px;
716
- }
717
- .pl-h-title {
718
- font-size: 22px;
719
- font-weight: 700;
720
- letter-spacing: -0.02em;
721
- margin: 0;
722
- }
723
- .pl-h-sub {
724
- font-size: 12.5px;
725
- color: var(--pl-text-2);
726
- margin: 3px 0 0;
727
- }
728
- .pl-head-actions {
729
- margin-left: auto;
730
- display: flex;
731
- gap: 8px;
732
- }
733
561
 
734
562
  /* Buttons */
735
- .pl-btn {
736
- display: inline-flex;
737
- align-items: center;
738
- gap: 7px;
739
- padding: 8px 14px;
740
- border-radius: 7px;
741
- font: 500 13px var(--pl-font-sans);
742
- cursor: pointer;
743
- border: 1px solid var(--pl-border-strong);
744
- background: #fff;
745
- color: var(--pl-text);
746
- transition:
747
- background 0.12s,
748
- border-color 0.12s;
749
- }
750
- .pl-btn:hover:not(:disabled) {
751
- background: var(--pl-surface-2);
752
- }
753
- .pl-btn:disabled {
754
- cursor: not-allowed;
755
- opacity: 0.4;
756
- }
757
- .pl-btn--primary {
758
- background: var(--pl-primary);
759
- border-color: var(--pl-primary);
760
- color: #fff;
761
- }
762
- .pl-btn--primary:hover {
763
- background: var(--pl-primary-700);
764
- }
765
- .pl-btn--ghost {
766
- border-color: transparent;
767
- background: transparent;
768
- }
769
- .pl-btn--ghost:hover:not(:disabled) {
770
- background: rgba(15, 23, 42, 0.05);
771
- }
772
- .pl-btn--danger {
773
- color: #ef4444;
774
- }
775
- .pl-btn--danger:hover:not(:disabled) {
776
- background: #fef2f2;
777
- color: #b91c1c;
778
- }
779
- .pl-btn--sm {
780
- padding: 5px 8px;
781
- font-size: 12px;
782
- gap: 5px;
783
- }
784
563
 
785
564
  /* List wrap */
786
- .pl-wrap {
787
- background: var(--pl-surface);
788
- border: 1px solid var(--pl-border);
565
+ .sa-plan-list-wrap {
566
+ background: var(--sa-bg-surface);
567
+ border: 1px solid var(--sa-border);
789
568
  border-radius: 10px;
790
569
  overflow: hidden;
791
570
  }
792
- .pl-toolbar {
571
+ .sa-plan-list-toolbar {
793
572
  display: flex;
794
573
  align-items: center;
795
574
  gap: 10px;
796
575
  padding: 12px 16px;
797
- border-bottom: 1px solid var(--pl-border);
798
- background: #fbfbfd;
799
- }
800
- .pl-search {
801
- flex: 1;
802
- max-width: 360px;
803
- display: flex;
804
- align-items: center;
805
- gap: 8px;
806
- background: #fff;
807
- border: 1px solid var(--pl-border);
808
- border-radius: 7px;
809
- padding: 7px 10px;
810
- }
811
- .pl-search-ico {
812
- color: var(--pl-text-3);
813
- display: inline-flex;
814
- }
815
- .pl-search input {
816
- flex: 1;
817
- border: 0;
818
- outline: 0;
819
- font: 13px var(--pl-font-sans);
820
- background: transparent;
821
- color: var(--pl-text);
822
- }
823
- .pl-kbd {
824
- font: 600 10.5px var(--pl-font-mono);
825
- background: #f1f5f9;
826
- color: var(--pl-text-2);
827
- padding: 2px 6px;
828
- border-radius: 4px;
829
- border: 1px solid #e2e8f0;
576
+ border-bottom: 1px solid var(--sa-border);
577
+ background: var(--sa-bg-surface-2);
830
578
  }
831
- .pl-toolbar-spacer {
579
+ /* The search takes the row; the sort hint keeps its content width. */
580
+ .sa-plan-list-toolbar > .q-field {
832
581
  flex: 1;
833
582
  }
834
- .pl-sortinfo {
583
+ .sa-plan-list-sortinfo {
835
584
  margin-left: auto;
836
585
  font-size: 11.5px;
837
- color: var(--pl-text-3);
586
+ color: var(--sa-muted-light);
838
587
  }
839
588
 
840
589
  /* List grid */
841
- .pl-list {
590
+ .sa-plan-list-list {
842
591
  display: grid;
843
592
  grid-template-columns: 1.6fr 0.9fr 0.7fr 0.9fr 1.4fr 160px;
844
593
  align-items: center;
845
594
  }
846
- .pl-list-head {
595
+ .sa-plan-list-list-head {
847
596
  display: contents;
848
597
  }
849
- .pl-list-head > div {
598
+ .sa-plan-list-list-head > div {
850
599
  background: #fbfbfd;
851
600
  padding: 10px 16px;
852
601
  font-size: 10.5px;
853
602
  text-transform: uppercase;
854
603
  letter-spacing: 0.1em;
855
- color: var(--pl-text-2);
604
+ color: var(--sa-muted-dark);
856
605
  font-weight: 700;
857
- border-bottom: 1px solid var(--pl-border);
606
+ border-bottom: 1px solid var(--sa-border);
858
607
  }
859
- .pl-empty {
608
+ .sa-plan-list-empty {
860
609
  grid-column: 1 / -1;
861
610
  padding: 32px 24px;
862
611
  text-align: center;
863
- color: var(--pl-text-3);
612
+ color: var(--sa-muted-light);
864
613
  font-style: italic;
865
614
  font-size: 13px;
866
615
  }
867
- .pl-row {
616
+ .sa-plan-list-row {
868
617
  display: contents;
869
618
  cursor: pointer;
870
619
  }
871
- .pl-row > .pl-cell {
620
+ .sa-plan-list-row > .sa-plan-list-cell {
872
621
  padding: 14px 16px;
873
- border-bottom: 1px solid #f1f5f9;
622
+ border-bottom: 1px solid var(--sa-border-soft);
874
623
  transition: background 0.12s;
875
624
  }
876
- .pl-row:hover > .pl-cell {
625
+ .sa-plan-list-row:hover > .sa-plan-list-cell {
877
626
  background: #fcfcfd;
878
627
  }
879
- .pl-row--new > .pl-cell {
880
- background: var(--pl-live-bg) !important;
881
- animation: pl-flashNew 2.4s ease-out;
628
+ .sa-plan-list-row--new > .sa-plan-list-cell {
629
+ background: var(--sa-positive-bg) !important;
630
+ animation: sa-plan-list-flashNew 2.4s ease-out;
882
631
  }
883
- @keyframes pl-flashNew {
632
+ @keyframes sa-plan-list-flashNew {
884
633
  0% {
885
634
  background: #d1fae5 !important;
886
635
  }
887
636
  100% {
888
- background: var(--pl-live-bg) !important;
637
+ background: var(--sa-positive-bg) !important;
889
638
  }
890
639
  }
891
640
 
892
641
  /* Sub-rows (drafts + future-scheduled versions, indented under the parent) */
893
- .pl-row--sub > .pl-cell {
642
+ .sa-plan-list-row--sub > .sa-plan-list-cell {
894
643
  background: #fafbfd;
895
644
  padding-top: 10px;
896
645
  padding-bottom: 10px;
897
646
  }
898
- .pl-row--sub:hover > .pl-cell {
899
- background: #f1f5f9;
647
+ .sa-plan-list-row--sub:hover > .sa-plan-list-cell {
648
+ background: var(--sa-border-soft);
900
649
  }
901
- .pl-cell--sub-name {
650
+ .sa-plan-list-cell--sub-name {
902
651
  display: flex;
903
652
  align-items: center;
904
653
  gap: 12px;
905
654
  padding-left: 32px !important;
906
655
  }
907
- .pl-sub-tree {
656
+ .sa-plan-list-sub-tree {
908
657
  display: inline-flex;
909
658
  align-items: center;
910
659
  width: 28px;
@@ -912,7 +661,7 @@ function hasAnyPublished(row: ResolvedPlan): boolean {
912
661
  flex: 0 0 auto;
913
662
  position: relative;
914
663
  }
915
- .pl-sub-tree-elbow {
664
+ .sa-plan-list-sub-tree-elbow {
916
665
  position: absolute;
917
666
  top: 0;
918
667
  bottom: 50%;
@@ -922,73 +671,73 @@ function hasAnyPublished(row: ResolvedPlan): boolean {
922
671
  border-bottom: 1.5px solid #cbd5e1;
923
672
  border-bottom-left-radius: 6px;
924
673
  }
925
- .pl-sub-titles {
674
+ .sa-plan-list-sub-titles {
926
675
  min-width: 0;
927
676
  }
928
- .pl-sub-title {
677
+ .sa-plan-list-sub-title {
929
678
  font-size: 13px;
930
679
  font-weight: 600;
931
- color: var(--pl-text-2);
680
+ color: var(--sa-muted-dark);
932
681
  display: flex;
933
682
  align-items: center;
934
683
  gap: 8px;
935
684
  }
936
- .pl-sub-desc {
685
+ .sa-plan-list-sub-desc {
937
686
  font-size: 11.5px;
938
- color: #64748b;
687
+ color: var(--sa-muted);
939
688
  margin-top: 2px;
940
689
  overflow: hidden;
941
690
  text-overflow: ellipsis;
942
691
  white-space: nowrap;
943
692
  }
944
- .pl-version-num--sub {
693
+ .sa-plan-list-version-num--sub {
945
694
  font-size: 13px;
946
695
  font-weight: 600;
947
- color: var(--pl-text-2);
696
+ color: var(--sa-muted-dark);
948
697
  }
949
- .pl-cell--sub-impact {
950
- color: var(--pl-text-3);
698
+ .sa-plan-list-cell--sub-impact {
699
+ color: var(--sa-muted-light);
951
700
  }
952
701
 
953
- .pl-plan-name {
702
+ .sa-plan-list-plan-name {
954
703
  display: flex;
955
704
  align-items: center;
956
705
  gap: 12px;
957
706
  }
958
- .pl-plan-mark {
707
+ .sa-plan-list-plan-mark {
959
708
  width: 36px;
960
709
  height: 36px;
961
710
  border-radius: 8px;
962
711
  display: grid;
963
712
  place-items: center;
964
- font: 700 11px var(--pl-font-mono);
713
+ font: 700 11px var(--sa-font-mono);
965
714
  letter-spacing: 0.04em;
966
715
  flex: 0 0 auto;
967
716
  border: 1px solid;
968
717
  }
969
- .pl-plan-titles {
718
+ .sa-plan-list-plan-titles {
970
719
  min-width: 0;
971
720
  }
972
- .pl-plan-title {
721
+ .sa-plan-list-plan-title {
973
722
  font-size: 14.5px;
974
723
  font-weight: 700;
975
- color: var(--pl-text);
724
+ color: var(--sa-heading);
976
725
  letter-spacing: -0.01em;
977
726
  display: flex;
978
727
  align-items: center;
979
728
  gap: 8px;
980
729
  flex-wrap: wrap;
981
730
  }
982
- .pl-plan-desc {
731
+ .sa-plan-list-plan-desc {
983
732
  font-size: 11.5px;
984
- color: #64748b;
733
+ color: var(--sa-muted);
985
734
  margin-top: 2px;
986
735
  overflow: hidden;
987
736
  text-overflow: ellipsis;
988
737
  white-space: nowrap;
989
738
  }
990
739
 
991
- .pl-cell--status {
740
+ .sa-plan-list-cell--status {
992
741
  display: flex;
993
742
  flex-direction: column;
994
743
  gap: 3px;
@@ -996,7 +745,7 @@ function hasAnyPublished(row: ResolvedPlan): boolean {
996
745
  }
997
746
 
998
747
  /* Chips */
999
- .pl-chip {
748
+ .sa-plan-list-chip {
1000
749
  display: inline-flex;
1001
750
  align-items: center;
1002
751
  gap: 5px;
@@ -1004,41 +753,41 @@ function hasAnyPublished(row: ResolvedPlan): boolean {
1004
753
  border-radius: 999px;
1005
754
  font-size: 11px;
1006
755
  font-weight: 600;
1007
- background: var(--pl-surface-2);
1008
- color: var(--pl-text-2);
1009
- border: 1px solid var(--pl-border);
756
+ background: var(--sa-bg-surface-2);
757
+ color: var(--sa-muted-dark);
758
+ border: 1px solid var(--sa-border);
1010
759
  }
1011
- .pl-chip--tiny {
760
+ .sa-plan-list-chip--tiny {
1012
761
  padding: 1px 6px;
1013
762
  font-size: 10px;
1014
763
  }
1015
- .pl-chip--new {
764
+ .sa-plan-list-chip--new {
1016
765
  background: #dbeafe;
1017
766
  color: #1e40af;
1018
767
  border-color: #bfdbfe;
1019
768
  font-size: 10px;
1020
769
  }
1021
- .pl-chip--live {
1022
- background: var(--pl-live-bg);
770
+ .sa-plan-list-chip--live {
771
+ background: var(--sa-positive-bg);
1023
772
  color: #047857;
1024
773
  border-color: #a7f3d0;
1025
774
  }
1026
- .pl-chip--draft {
1027
- background: var(--pl-draft-bg);
775
+ .sa-plan-list-chip--draft {
776
+ background: var(--sa-warning-bg);
1028
777
  color: #b45309;
1029
778
  border-color: #fde68a;
1030
779
  }
1031
- .pl-chip--supersed {
1032
- background: #f1f5f9;
1033
- color: var(--pl-text-2);
780
+ .sa-plan-list-chip--supersed {
781
+ background: var(--sa-border-soft);
782
+ color: var(--sa-muted-dark);
1034
783
  border-color: #cbd5e1;
1035
784
  }
1036
- .pl-chip--scheduled {
785
+ .sa-plan-list-chip--scheduled {
1037
786
  background: #eef2ff;
1038
787
  color: #4338ca;
1039
788
  border-color: #c7d2fe;
1040
789
  }
1041
- .pl-chip--dot::before {
790
+ .sa-plan-list-chip--dot::before {
1042
791
  content: '';
1043
792
  width: 6px;
1044
793
  height: 6px;
@@ -1046,62 +795,62 @@ function hasAnyPublished(row: ResolvedPlan): boolean {
1046
795
  background: currentColor;
1047
796
  }
1048
797
 
1049
- .pl-version-num {
798
+ .sa-plan-list-version-num {
1050
799
  font-size: 13px;
1051
800
  font-weight: 700;
1052
- color: var(--pl-text);
801
+ color: var(--sa-heading);
1053
802
  }
1054
- .pl-version-num--muted {
803
+ .sa-plan-list-version-num--muted {
1055
804
  color: #cbd5e1;
1056
805
  }
1057
- .pl-version-sub {
806
+ .sa-plan-list-version-sub {
1058
807
  font-size: 10.5px;
1059
- color: var(--pl-text-3);
808
+ color: var(--sa-muted-light);
1060
809
  margin-top: 2px;
1061
810
  }
1062
811
 
1063
- .pl-price-big {
812
+ .sa-plan-list-price-big {
1064
813
  font-size: 14px;
1065
814
  font-weight: 700;
1066
- color: var(--pl-text);
815
+ color: var(--sa-heading);
1067
816
  }
1068
- .pl-price-unit {
817
+ .sa-plan-list-price-unit {
1069
818
  font-size: 11px;
1070
- color: var(--pl-text-3);
819
+ color: var(--sa-muted-light);
1071
820
  }
1072
- .pl-price-sub {
821
+ .sa-plan-list-price-sub {
1073
822
  font-size: 10.5px;
1074
- color: var(--pl-text-3);
823
+ color: var(--sa-muted-light);
1075
824
  margin-top: 2px;
1076
825
  }
1077
- .pl-price-text {
826
+ .sa-plan-list-price-text {
1078
827
  font-size: 13px;
1079
828
  font-weight: 600;
1080
- color: var(--pl-text);
829
+ color: var(--sa-heading);
1081
830
  }
1082
831
 
1083
- .pl-cell--tenants {
832
+ .sa-plan-list-cell--tenants {
1084
833
  display: flex;
1085
834
  align-items: center;
1086
835
  gap: 10px;
1087
836
  }
1088
- .pl-tenant-num {
837
+ .sa-plan-list-tenant-num {
1089
838
  font-size: 16px;
1090
839
  font-weight: 700;
1091
- color: var(--pl-text);
840
+ color: var(--sa-heading);
1092
841
  }
1093
- .pl-tenant-bar {
842
+ .sa-plan-list-tenant-bar {
1094
843
  flex: 1;
1095
844
  height: 6px;
1096
- background: #f1f5f9;
845
+ background: var(--sa-border-soft);
1097
846
  border-radius: 999px;
1098
847
  overflow: hidden;
1099
848
  }
1100
- .pl-tenant-bar-fill {
849
+ .sa-plan-list-tenant-bar-fill {
1101
850
  height: 100%;
1102
851
  }
1103
852
 
1104
- .pl-cell--actions {
853
+ .sa-plan-list-cell--actions {
1105
854
  display: flex;
1106
855
  gap: 4px;
1107
856
  justify-content: flex-end;