@saasicat/ui-vue 0.2.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 (193) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +47 -0
  3. package/dist/index.cjs +3776 -0
  4. package/dist/index.d.cts +2395 -0
  5. package/dist/index.d.ts +2395 -0
  6. package/dist/index.js +3659 -0
  7. package/dist/testing-e2e/admin-pages-suite.cjs +134 -0
  8. package/dist/testing-e2e/admin-pages-suite.d.cts +37 -0
  9. package/dist/testing-e2e/admin-pages-suite.d.ts +37 -0
  10. package/dist/testing-e2e/admin-pages-suite.js +109 -0
  11. package/package.json +87 -0
  12. package/src/action-registry.ts +110 -0
  13. package/src/batch-column-fetcher.ts +152 -0
  14. package/src/boot-loader.ts +60 -0
  15. package/src/components/BundleVersionPublishDialog.vue +359 -0
  16. package/src/components/BusinessTypeVersionEditorDialog.vue +417 -0
  17. package/src/components/BusinessTypeVersionPublishDialog.vue +258 -0
  18. package/src/components/FeatureGate.vue +56 -0
  19. package/src/components/KpiCard.vue +53 -0
  20. package/src/components/KvBlock.vue +36 -0
  21. package/src/components/MarketingPromotionsTab.vue +861 -0
  22. package/src/components/MfaPromptDialog.vue +104 -0
  23. package/src/components/TenantActionConfirmDialog.vue +252 -0
  24. package/src/components/VersionDiffPreview.vue +150 -0
  25. package/src/components/bundle-editor/BundleCreatePanel.vue +681 -0
  26. package/src/components/bundle-editor/BundleFeaturesEditor.vue +215 -0
  27. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +316 -0
  28. package/src/components/bundle-editor/BundleQuotasEditor.vue +219 -0
  29. package/src/components/bundle-editor/BundleStatusBanner.vue +189 -0
  30. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +659 -0
  31. package/src/components/bundle-editor/BundleVersionStrip.vue +207 -0
  32. package/src/components/bundle-editor/bundle-version-status.ts +164 -0
  33. package/src/components/bundle-editor/catalog-i18n.ts +85 -0
  34. package/src/components/dialogs/PilotCreateDialog.vue +780 -0
  35. package/src/components/dialogs/PilotEditDialog.vue +548 -0
  36. package/src/components/dialogs/PromoCodeCreateDialog.vue +737 -0
  37. package/src/components/dialogs/PromoCodeEditDialog.vue +859 -0
  38. package/src/components/dialogs/types.ts +126 -0
  39. package/src/components/plan/PlanCycleToggle.vue +99 -0
  40. package/src/components/plan/PlanGrid.vue +211 -0
  41. package/src/components/plan/PriceSummary.vue +293 -0
  42. package/src/components/plan/PromoCodeInput.vue +192 -0
  43. package/src/components/plan/PublicBundleGrid.vue +287 -0
  44. package/src/components/plan-cockpit/PlanCockpit.vue +993 -0
  45. package/src/components/plan-cockpit/PlanCockpitAuditLog.vue +65 -0
  46. package/src/components/plan-cockpit/PlanCockpitDiffPanel.vue +91 -0
  47. package/src/components/plan-cockpit/PlanCockpitHeader.vue +98 -0
  48. package/src/components/plan-cockpit/PlanCockpitImpactPanel.vue +99 -0
  49. package/src/components/plan-cockpit/PlanCockpitKpis.vue +57 -0
  50. package/src/components/plan-cockpit/PlanCockpitVersions.vue +226 -0
  51. package/src/components/plan-cockpit/types.ts +72 -0
  52. package/src/components/plan-create-dialog/PlanCreateDialog.vue +508 -0
  53. package/src/components/plan-detail/PlanAuditLog.vue +59 -0
  54. package/src/components/plan-detail/PlanDetail.vue +1195 -0
  55. package/src/components/plan-detail/PlanDetailHeader.vue +185 -0
  56. package/src/components/plan-detail/PlanDetailKpis.vue +57 -0
  57. package/src/components/plan-detail/PlanTerminateDialog.vue +76 -0
  58. package/src/components/plan-detail/PlanVersionDiffPanel.vue +149 -0
  59. package/src/components/plan-detail/PlanVersionsPanel.vue +255 -0
  60. package/src/components/plan-detail/types.ts +59 -0
  61. package/src/components/plan-list/PlanList.vue +1067 -0
  62. package/src/components/plan-matrix/PlanMatrix.vue +1113 -0
  63. package/src/components/plan-review/PlanReview.vue +742 -0
  64. package/src/components/plan-version-editor/PlanCatalogPreview.vue +184 -0
  65. package/src/components/plan-version-editor/PlanComponentPool.vue +272 -0
  66. package/src/components/plan-version-editor/PlanVersionBasket.vue +281 -0
  67. package/src/components/plan-version-editor/PlanVersionDiffDialog.vue +84 -0
  68. package/src/components/plan-version-editor/PlanVersionEditor.vue +1728 -0
  69. package/src/components/plan-version-editor/PlanVersionEditorHeader.vue +94 -0
  70. package/src/components/plan-version-editor/types.ts +96 -0
  71. package/src/components/wizard-stepper/WizardStepper.vue +107 -0
  72. package/src/create-admin-routes.ts +68 -0
  73. package/src/create-super-admin-app.ts +345 -0
  74. package/src/entitlement-provider.ts +36 -0
  75. package/src/feature-router-guard.ts +77 -0
  76. package/src/http-json.ts +62 -0
  77. package/src/index.ts +98 -0
  78. package/src/manifest-loader.ts +139 -0
  79. package/src/manifest-store-factory.ts +114 -0
  80. package/src/nav-builder.ts +274 -0
  81. package/src/pages-standard/AdminLayout.vue +411 -0
  82. package/src/pages-standard/AdminManifestErrorPage.vue +79 -0
  83. package/src/pages-standard/AuditPage.vue +223 -0
  84. package/src/pages-standard/BundlesPage.vue +910 -0
  85. package/src/pages-standard/BusinessTypesPage.vue +662 -0
  86. package/src/pages-standard/DashboardPage.vue +595 -0
  87. package/src/pages-standard/DiscoveryPage.vue +663 -0
  88. package/src/pages-standard/EmailHistoryPage.vue +576 -0
  89. package/src/pages-standard/MarketingCatalogPage.vue +1771 -0
  90. package/src/pages-standard/PilotsPage.vue +577 -0
  91. package/src/pages-standard/PlanVersionsPage.vue +216 -0
  92. package/src/pages-standard/PlansPage.vue +1222 -0
  93. package/src/pages-standard/PlatformEmailPage.vue +434 -0
  94. package/src/pages-standard/PromoCodeDetailPage.vue +331 -0
  95. package/src/pages-standard/PromoCodesPage.vue +589 -0
  96. package/src/pages-standard/SubscriptionsPage.vue +159 -0
  97. package/src/pages-standard/SuperAdminLoginPage.vue +299 -0
  98. package/src/pages-standard/SuperAdminSetupWizard.vue +405 -0
  99. package/src/pages-standard/TenantDetailPage.vue +432 -0
  100. package/src/pages-standard/TenantsPage.vue +732 -0
  101. package/src/pages-standard/UsersPage.vue +504 -0
  102. package/src/pages-standard/bundles-page/BundleAccordionList.vue +116 -0
  103. package/src/pages-standard/bundles-page/BundleDetailPanel.vue +193 -0
  104. package/src/pages-standard/bundles-page/BundlesFilterBar.vue +48 -0
  105. package/src/pages-standard/bundles-page/BundlesHeader.vue +67 -0
  106. package/src/pages-standard/bundles-page/BundlesKpis.vue +39 -0
  107. package/src/pages-standard/bundles-page/types.ts +15 -0
  108. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +246 -0
  109. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +171 -0
  110. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +410 -0
  111. package/src/pages-standard/discovery-page/DiscoveryHeader.vue +31 -0
  112. package/src/pages-standard/discovery-page/DiscoveryKpis.vue +47 -0
  113. package/src/pages-standard/discovery-page/DiscoveryMetaBanner.vue +26 -0
  114. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +247 -0
  115. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +73 -0
  116. package/src/pages-standard/discovery-page/discovery-ui.ts +166 -0
  117. package/src/pages-standard/email-history.types.ts +48 -0
  118. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +448 -0
  119. package/src/pages-standard/marketing-catalog/MarketingCatalogHeader.vue +93 -0
  120. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +138 -0
  121. package/src/pages-standard/marketing-catalog/MarketingCatalogToolbar.vue +58 -0
  122. package/src/pages-standard/marketing-catalog/types.ts +43 -0
  123. package/src/pages-standard/plan-versions/PlanDiffCard.vue +132 -0
  124. package/src/pages-standard/plan-versions/PlanVersionHeader.vue +293 -0
  125. package/src/pages-standard/plan-versions/PlanVersionStatusBadge.vue +48 -0
  126. package/src/pages-standard/plan-versions/PlanVersionsAudit.vue +307 -0
  127. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +269 -0
  128. package/src/pages-standard/plan-versions/PlanVersionsKpi.vue +65 -0
  129. package/src/pages-standard/plan-versions/PlanVersionsList.vue +292 -0
  130. package/src/pages-standard/plan-versions/PlanVersionsMatrix.vue +261 -0
  131. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +300 -0
  132. package/src/pages-standard/plan-versions/VersionDiffPreview.vue +167 -0
  133. package/src/pages-standard/plan-versions/format.ts +53 -0
  134. package/src/pages-standard/plan-versions/types.ts +33 -0
  135. package/src/pages-standard/plans-page/PlanArchiveDialog.vue +50 -0
  136. package/src/pages-standard/plans-page/PlanBundleOverview.vue +147 -0
  137. package/src/pages-standard/plans-page/PlanDiscardDraftDialog.vue +46 -0
  138. package/src/pages-standard/plans-page/PlanPublishDialog.vue +106 -0
  139. package/src/pages-standard/plans-page/PlansPageToast.vue +69 -0
  140. package/src/pages-standard/plans-page/types.ts +18 -0
  141. package/src/pages-standard/platform-email.types.ts +41 -0
  142. package/src/pages-standard/sa-theme.css +223 -0
  143. package/src/pages-standard/tenants/StatusPill.vue +62 -0
  144. package/src/pages-standard/tenants/format.ts +38 -0
  145. package/src/pages-tenant/LimitsRow.vue +67 -0
  146. package/src/pages-tenant/MySubscriptionBundlesPage.vue +604 -0
  147. package/src/pages-tenant/OnboardingConfigurator.vue +417 -0
  148. package/src/pages-tenant/PackageSnapshotPanel.vue +241 -0
  149. package/src/pages-tenant/PendingVersionBanner.vue +126 -0
  150. package/src/pages-tenant/PlanChangeWizard.vue +630 -0
  151. package/src/pages-tenant/TenantPlanSection.vue +729 -0
  152. package/src/pages-tenant/UsageBar.vue +107 -0
  153. package/src/pages-tenant/default-i18n.ts +280 -0
  154. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +304 -0
  155. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +323 -0
  156. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +122 -0
  157. package/src/pages-tenant/tenant-plan-section/TenantPlanCardHeader.vue +67 -0
  158. package/src/pages-tenant/tenant-plan-section/TenantUsageGrid.vue +31 -0
  159. package/src/plan-versions-catalog.ts +368 -0
  160. package/src/platform-loaders.ts +68 -0
  161. package/src/project-page-host.ts +108 -0
  162. package/src/testing-e2e/admin-pages-suite.ts +187 -0
  163. package/src/types.ts +58 -0
  164. package/src/use-actions.ts +25 -0
  165. package/src/use-api-list.ts +151 -0
  166. package/src/use-audit-entries.ts +48 -0
  167. package/src/use-batch-columns.ts +59 -0
  168. package/src/use-bulk-publish.ts +150 -0
  169. package/src/use-bundle-versions-map.ts +112 -0
  170. package/src/use-bundles.ts +297 -0
  171. package/src/use-business-types.ts +269 -0
  172. package/src/use-catalog-entries.ts +243 -0
  173. package/src/use-discovery.ts +158 -0
  174. package/src/use-entitlement.ts +87 -0
  175. package/src/use-live-plan-versions.ts +126 -0
  176. package/src/use-manifest.ts +57 -0
  177. package/src/use-marketing-projections.ts +160 -0
  178. package/src/use-nav.ts +33 -0
  179. package/src/use-plan-editor.ts +162 -0
  180. package/src/use-plan-versions.ts +83 -0
  181. package/src/use-plans.ts +375 -0
  182. package/src/use-platform-tenant-actions.ts +286 -0
  183. package/src/use-promotions.ts +126 -0
  184. package/src/use-public-boot.ts +40 -0
  185. package/src/use-subscription-draft.ts +355 -0
  186. package/src/use-super-admin-context.ts +93 -0
  187. package/src/use-tenant-action-flow.ts +238 -0
  188. package/src/use-tenant-billing-catalog.ts +160 -0
  189. package/src/use-tenant-billing.ts +453 -0
  190. package/src/use-tenant-manifest.ts +94 -0
  191. package/src/use-tenant-subscription-bundles.ts +148 -0
  192. package/src/use-tenants.ts +61 -0
  193. package/src/version.ts +10 -0
@@ -0,0 +1,1067 @@
1
+ <template>
2
+ <div class="pl">
3
+ <!-- Page header -->
4
+ <div class="pl-page-head">
5
+ <div>
6
+ <h2 class="pl-h-title">Alle Pläne</h2>
7
+ <p class="pl-h-sub">
8
+ {{ resolvedPlans.length }} Pläne · {{ liveCount }} live ·
9
+ {{ draftCount }} offene Drafts · {{ totalTenants }} Mandanten zugeordnet
10
+ </p>
11
+ </div>
12
+ <div class="pl-head-actions">
13
+ <button class="pl-btn" type="button" @click="$emit('showMatrix')">
14
+ <svg
15
+ width="14"
16
+ height="14"
17
+ viewBox="0 0 24 24"
18
+ fill="none"
19
+ stroke="currentColor"
20
+ stroke-width="2"
21
+ >
22
+ <rect x="3" y="3" width="7" height="7" rx="1" />
23
+ <rect x="14" y="3" width="7" height="7" rx="1" />
24
+ <rect x="3" y="14" width="7" height="7" rx="1" />
25
+ <rect x="14" y="14" width="7" height="7" rx="1" />
26
+ </svg>
27
+ <span>Matrix-Ansicht</span>
28
+ </button>
29
+ <button class="pl-btn pl-btn--primary" type="button" @click="$emit('createPlan')">
30
+ <svg
31
+ width="14"
32
+ height="14"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ stroke-width="2.5"
37
+ >
38
+ <path d="M12 5v14M5 12h14" />
39
+ </svg>
40
+ <span>Neuer Plan</span>
41
+ </button>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- List wrapper -->
46
+ <div class="pl-wrap">
47
+ <div class="pl-toolbar">
48
+ <div class="pl-search">
49
+ <span class="pl-search-ico" aria-hidden="true">
50
+ <svg
51
+ width="14"
52
+ height="14"
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ >
58
+ <circle cx="11" cy="11" r="7" />
59
+ <path d="M21 21l-4.35-4.35" />
60
+ </svg>
61
+ </span>
62
+ <input v-model="search" placeholder="Plan suchen…" />
63
+ <span class="pl-kbd">⌘ K</span>
64
+ </div>
65
+ <div class="pl-toolbar-spacer" />
66
+ <div class="pl-sortinfo">Sortiert nach: Order</div>
67
+ </div>
68
+
69
+ <div class="pl-list">
70
+ <div class="pl-list-head">
71
+ <div>Plan</div>
72
+ <div>Status</div>
73
+ <div>Version</div>
74
+ <div>Preis</div>
75
+ <div>Mandanten</div>
76
+ <div />
77
+ </div>
78
+
79
+ <div v-if="filteredPlans.length === 0" class="pl-empty">
80
+ <template v-if="resolvedPlans.length === 0">
81
+ Noch keine Pläne — über „Neuer Plan" oben rechts den ersten anlegen.
82
+ </template>
83
+ <template v-else> Keine Pläne entsprechen der Suche „{{ search }}". </template>
84
+ </div>
85
+
86
+ <template v-for="p in filteredPlans" :key="p.plan.id">
87
+ <!-- Parent row: currently live version (or first future / nothing) -->
88
+ <div
89
+ :class="['pl-row', { 'pl-row--new': highlightPlanKey === p.planKey }]"
90
+ @click="$emit('openPlan', p.plan)"
91
+ >
92
+ <div class="pl-cell pl-cell--name">
93
+ <div class="pl-plan-name">
94
+ <div
95
+ class="pl-plan-mark"
96
+ :style="{
97
+ background: planAccent(p.planKey) + '15',
98
+ color: planAccent(p.planKey),
99
+ borderColor: planAccent(p.planKey) + '33',
100
+ }"
101
+ >
102
+ {{ p.planKey.slice(0, 3) }}
103
+ </div>
104
+ <div class="pl-plan-titles">
105
+ <div class="pl-plan-title">
106
+ {{ p.label }}
107
+ <span
108
+ v-if="highlightPlanKey === p.planKey"
109
+ class="pl-chip pl-chip--new"
110
+ >NEU</span
111
+ >
112
+ </div>
113
+ <div class="pl-plan-desc">{{ p.description || '—' }}</div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <div class="pl-cell pl-cell--status">
119
+ <template v-if="p.currentLive">
120
+ <span class="pl-chip pl-chip--live pl-chip--dot">live</span>
121
+ <span
122
+ v-if="!p.currentLive.marketed"
123
+ class="pl-chip pl-chip--supersed pl-chip--tiny"
124
+ >privat</span
125
+ >
126
+ </template>
127
+ <template v-else-if="p.primary && p.primary.publishedAt">
128
+ <span class="pl-chip pl-chip--scheduled pl-chip--dot">
129
+ ab {{ p.primary.validFrom?.slice(0, 10) }}
130
+ </span>
131
+ </template>
132
+ <template v-else>
133
+ <span class="pl-chip pl-chip--supersed pl-chip--dot"
134
+ >keine live</span
135
+ >
136
+ </template>
137
+ </div>
138
+
139
+ <div class="pl-cell">
140
+ <div v-if="p.primary" class="pl-version-num">
141
+ v{{ p.primary.version }}
142
+ </div>
143
+ <div v-else class="pl-version-num pl-version-num--muted">—</div>
144
+ <div v-if="p.primary?.validFrom" class="pl-version-sub">
145
+ {{ p.currentLive ? 'seit' : 'ab' }}
146
+ {{ p.primary.validFrom.slice(0, 10)
147
+ }}<template v-if="p.primary.validUntil">
148
+ bis {{ p.primary.validUntil.slice(0, 10) }}</template
149
+ >
150
+ </div>
151
+ </div>
152
+
153
+ <div class="pl-cell">
154
+ <template v-if="!p.primary">
155
+ <span class="pl-price-text">—</span>
156
+ </template>
157
+ <template
158
+ v-else-if="
159
+ Number(p.primary.monthlyNet) === 0 &&
160
+ Number(p.primary.yearlyNet) === 0
161
+ "
162
+ >
163
+ <span class="pl-price-text">Kostenlos</span>
164
+ </template>
165
+ <template v-else>
166
+ <div>
167
+ <span class="pl-price-big">{{
168
+ formatMoney(p.primary.monthlyNet)
169
+ }}</span>
170
+ <span class="pl-price-unit"> / Mo</span>
171
+ </div>
172
+ <div class="pl-price-sub">
173
+ {{ formatMoney(p.primary.yearlyNet) }} / Jahr
174
+ </div>
175
+ </template>
176
+ </div>
177
+
178
+ <div class="pl-cell pl-cell--tenants">
179
+ <span class="pl-tenant-num">{{ p.tenantCount }}</span>
180
+ <div class="pl-tenant-bar">
181
+ <div
182
+ class="pl-tenant-bar-fill"
183
+ :style="{
184
+ width: tenantBarWidth(p.tenantCount),
185
+ background: planAccent(p.planKey),
186
+ }"
187
+ />
188
+ </div>
189
+ </div>
190
+
191
+ <div class="pl-cell pl-cell--actions" @click.stop>
192
+ <button
193
+ v-if="hasAnyPublished(p)"
194
+ class="pl-btn pl-btn--sm pl-btn--ghost"
195
+ type="button"
196
+ disabled
197
+ title="Plan hat published Versionen — kann nicht gelöscht werden (Vertragsschutz P1)"
198
+ >
199
+ <svg
200
+ width="14"
201
+ height="14"
202
+ viewBox="0 0 24 24"
203
+ fill="none"
204
+ stroke="currentColor"
205
+ stroke-width="2"
206
+ opacity="0.4"
207
+ >
208
+ <rect x="3" y="11" width="18" height="11" rx="2" />
209
+ <path d="M7 11V7a5 5 0 0110 0v4" />
210
+ </svg>
211
+ </button>
212
+ <button
213
+ v-else
214
+ class="pl-btn pl-btn--sm pl-btn--ghost pl-btn--danger"
215
+ type="button"
216
+ title="Plan komplett löschen"
217
+ @click="$emit('archivePlan', p.plan, false)"
218
+ >
219
+ <svg
220
+ width="14"
221
+ height="14"
222
+ viewBox="0 0 24 24"
223
+ fill="none"
224
+ stroke="currentColor"
225
+ stroke-width="2"
226
+ >
227
+ <path
228
+ d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"
229
+ />
230
+ </svg>
231
+ </button>
232
+ <button
233
+ class="pl-btn pl-btn--sm pl-btn--ghost"
234
+ type="button"
235
+ title="Plan klonen"
236
+ @click="$emit('clonePlan', p.plan)"
237
+ >
238
+ <svg
239
+ width="14"
240
+ height="14"
241
+ viewBox="0 0 24 24"
242
+ fill="none"
243
+ stroke="currentColor"
244
+ stroke-width="2"
245
+ >
246
+ <rect x="9" y="9" width="11" height="11" rx="2" />
247
+ <path d="M5 15V5a2 2 0 012-2h10" />
248
+ </svg>
249
+ </button>
250
+ <button
251
+ class="pl-btn pl-btn--sm pl-btn--ghost"
252
+ type="button"
253
+ title="Neue Version anlegen"
254
+ :disabled="!!p.draft"
255
+ @click="onNewVersion(p)"
256
+ >
257
+ <svg
258
+ width="14"
259
+ height="14"
260
+ viewBox="0 0 24 24"
261
+ fill="none"
262
+ stroke="currentColor"
263
+ stroke-width="2"
264
+ >
265
+ <path
266
+ d="M12 20h9M16.5 3.5a2.12 2.12 0 113 3L7 19l-4 1 1-4 12.5-12.5z"
267
+ />
268
+ </svg>
269
+ </button>
270
+ <button
271
+ class="pl-btn pl-btn--sm"
272
+ type="button"
273
+ title="Plan öffnen"
274
+ @click="$emit('openPlan', p.plan)"
275
+ >
276
+ <svg
277
+ width="14"
278
+ height="14"
279
+ viewBox="0 0 24 24"
280
+ fill="none"
281
+ stroke="currentColor"
282
+ stroke-width="2"
283
+ >
284
+ <path d="M9 18l6-6-6-6" />
285
+ </svg>
286
+ </button>
287
+ </div>
288
+ </div>
289
+
290
+ <!-- Sub rows: drafts + future-scheduled versions, indented -->
291
+ <div
292
+ v-for="sub in p.subRows"
293
+ :key="`${p.plan.id}-${sub.id}`"
294
+ class="pl-row pl-row--sub"
295
+ @click.stop="$emit('openPlan', p.plan)"
296
+ >
297
+ <div class="pl-cell pl-cell--name pl-cell--sub-name">
298
+ <div class="pl-sub-tree" aria-hidden="true">
299
+ <span class="pl-sub-tree-elbow" />
300
+ </div>
301
+ <div class="pl-sub-titles">
302
+ <div class="pl-sub-title">
303
+ v{{ sub.version }}
304
+ <template v-if="sub.publishedAt === null">
305
+ <span
306
+ class="pl-chip pl-chip--draft pl-chip--dot pl-chip--tiny"
307
+ >Draft</span
308
+ >
309
+ </template>
310
+ <template v-else>
311
+ <span
312
+ class="pl-chip pl-chip--scheduled pl-chip--dot pl-chip--tiny"
313
+ >geplant</span
314
+ >
315
+ </template>
316
+ </div>
317
+ <div class="pl-sub-desc">
318
+ {{ sub.changeNote || 'keine Change-Note' }}
319
+ </div>
320
+ </div>
321
+ </div>
322
+
323
+ <div class="pl-cell pl-cell--status">
324
+ <span
325
+ v-if="sub.publishedAt === null"
326
+ class="pl-chip pl-chip--draft pl-chip--dot pl-chip--tiny"
327
+ >Draft</span
328
+ >
329
+ <span
330
+ v-else
331
+ class="pl-chip pl-chip--scheduled pl-chip--dot pl-chip--tiny"
332
+ >ab {{ sub.validFrom?.slice(0, 10) }}</span
333
+ >
334
+ </div>
335
+
336
+ <div class="pl-cell">
337
+ <div class="pl-version-num pl-version-num--sub">v{{ sub.version }}</div>
338
+ <div v-if="sub.validFrom" class="pl-version-sub">
339
+ ab {{ sub.validFrom.slice(0, 10)
340
+ }}<template v-if="sub.validUntil">
341
+ bis {{ sub.validUntil.slice(0, 10) }}</template
342
+ >
343
+ </div>
344
+ </div>
345
+
346
+ <div class="pl-cell">
347
+ <template
348
+ v-if="Number(sub.monthlyNet) === 0 && Number(sub.yearlyNet) === 0"
349
+ >
350
+ <span class="pl-price-text">Kostenlos</span>
351
+ </template>
352
+ <template v-else>
353
+ <div>
354
+ <span class="pl-price-big">{{
355
+ formatMoney(sub.monthlyNet)
356
+ }}</span>
357
+ <span class="pl-price-unit"> / Mo</span>
358
+ </div>
359
+ <div class="pl-price-sub">
360
+ {{ formatMoney(sub.yearlyNet) }} / Jahr
361
+ </div>
362
+ </template>
363
+ </div>
364
+
365
+ <div class="pl-cell pl-cell--sub-impact">
366
+ <span class="pl-version-sub">{{ p.tenantCount }} Mandanten</span>
367
+ </div>
368
+
369
+ <div class="pl-cell pl-cell--actions" @click.stop>
370
+ <button
371
+ v-if="sub.publishedAt === null"
372
+ class="pl-btn pl-btn--sm pl-btn--ghost pl-btn--danger"
373
+ type="button"
374
+ :title="`Draft v${sub.version} verwerfen`"
375
+ @click="$emit('discardDraft', p.plan, sub)"
376
+ >
377
+ <svg
378
+ width="14"
379
+ height="14"
380
+ viewBox="0 0 24 24"
381
+ fill="none"
382
+ stroke="currentColor"
383
+ stroke-width="2"
384
+ >
385
+ <path
386
+ d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"
387
+ />
388
+ </svg>
389
+ </button>
390
+ <button
391
+ v-if="sub.publishedAt === null"
392
+ class="pl-btn pl-btn--sm pl-btn--ghost"
393
+ type="button"
394
+ title="Draft bearbeiten"
395
+ @click="$emit('editDraft', p.plan, sub)"
396
+ >
397
+ <svg
398
+ width="14"
399
+ height="14"
400
+ viewBox="0 0 24 24"
401
+ fill="none"
402
+ stroke="currentColor"
403
+ stroke-width="2"
404
+ >
405
+ <path
406
+ d="M12 20h9M16.5 3.5a2.12 2.12 0 113 3L7 19l-4 1 1-4 12.5-12.5z"
407
+ />
408
+ </svg>
409
+ </button>
410
+ <button
411
+ class="pl-btn pl-btn--sm"
412
+ type="button"
413
+ title="Im Cockpit öffnen"
414
+ @click="$emit('openPlan', p.plan)"
415
+ >
416
+ <svg
417
+ width="14"
418
+ height="14"
419
+ viewBox="0 0 24 24"
420
+ fill="none"
421
+ stroke="currentColor"
422
+ stroke-width="2"
423
+ >
424
+ <path d="M9 18l6-6-6-6" />
425
+ </svg>
426
+ </button>
427
+ </div>
428
+ </div>
429
+ </template>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ </template>
434
+
435
+ <script setup lang="ts">
436
+ import { computed, ref } from 'vue';
437
+ import type { PlanRow, PlanVersionRow } from '@saasicat/types';
438
+
439
+ // PlanList — list view of all plans (default view in PlansPage,
440
+ // corresponds to the ListScreen from the plan simulation). One row per
441
+ // plan with mark chip, status, version, price, tenant progress, row
442
+ // actions (clone · new version · open).
443
+
444
+ const props = withDefaults(
445
+ defineProps<{
446
+ plans: PlanRow[];
447
+ versionsByPlanId: Record<string, PlanVersionRow[]>;
448
+ tenantCountsByPlanKey?: Record<string, number>;
449
+ planAccents?: Record<string, string>;
450
+ /** Plan key that was last created/updated — gets the NEU highlight. */
451
+ highlightPlanKey?: string | null;
452
+ }>(),
453
+ {
454
+ tenantCountsByPlanKey: () => ({}),
455
+ planAccents: () => ({}),
456
+ highlightPlanKey: null,
457
+ },
458
+ );
459
+
460
+ const emit = defineEmits<{
461
+ (e: 'openPlan', plan: PlanRow): void;
462
+ (e: 'createPlan'): void;
463
+ (e: 'clonePlan', plan: PlanRow): void;
464
+ (e: 'newVersion', plan: PlanRow, basis: PlanVersionRow): void;
465
+ (e: 'editDraft', plan: PlanRow, draft: PlanVersionRow): void;
466
+ (e: 'discardDraft', plan: PlanRow, draft: PlanVersionRow): void;
467
+ (e: 'archivePlan', plan: PlanRow, hasLive: boolean): void;
468
+ (e: 'showMatrix'): void;
469
+ }>();
470
+
471
+ const search = ref('');
472
+
473
+ interface ResolvedPlan {
474
+ plan: PlanRow;
475
+ planKey: string;
476
+ label: string;
477
+ description: string | null;
478
+ /** Version currently active for new bookings (validFrom ≤ today < validUntil or validUntil null). */
479
+ currentLive: PlanVersionRow | null;
480
+ /** Version shown on the parent row (currentLive preferred). */
481
+ primary: PlanVersionRow | null;
482
+ /** Drafts + future-published versions (sorted: future ASC by validFrom, drafts at the end). */
483
+ subRows: PlanVersionRow[];
484
+ /** True if the plan has any versions at all (for hide logic). */
485
+ hasAnyVersion: boolean;
486
+ /** True if all versions are expired (validUntil < today, no draft / no future). */
487
+ allExpired: boolean;
488
+ /** First open draft (for the header KPI "offene Drafts"). */
489
+ draft: PlanVersionRow | null;
490
+ tenantCount: number;
491
+ }
492
+
493
+ const DEFAULT_ACCENTS: Record<string, string> = {
494
+ STARTER: '#64748b',
495
+ STANDARD: '#2563eb',
496
+ PRO: '#7c3aed',
497
+ PROFESSIONAL: '#7c3aed',
498
+ BUSINESS: '#0ea5e9',
499
+ ENTERPRISE: '#0f766e',
500
+ BASIC: '#475569',
501
+ };
502
+ const FALLBACK_ACCENTS = ['#2563eb', '#7c3aed', '#0f766e', '#f59e0b', '#0ea5e9', '#ef4444'];
503
+
504
+ function planAccent(planKey: string): string {
505
+ const provided = props.planAccents[planKey];
506
+ if (provided) return provided;
507
+ const def = DEFAULT_ACCENTS[planKey];
508
+ if (def) return def;
509
+ const idx = props.plans.findIndex((p) => p.planKey === planKey);
510
+ return FALLBACK_ACCENTS[idx % FALLBACK_ACCENTS.length] ?? FALLBACK_ACCENTS[0]!;
511
+ }
512
+
513
+ function todayIsoDate(): string {
514
+ return new Date().toISOString().slice(0, 10);
515
+ }
516
+
517
+ function isCurrentlyValid(v: PlanVersionRow, today: string): boolean {
518
+ if (v.publishedAt === null) return false;
519
+ if (v.validFrom && v.validFrom.slice(0, 10) > today) return false;
520
+ if (v.validUntil && v.validUntil.slice(0, 10) < today) return false;
521
+ return true;
522
+ }
523
+
524
+ function isFutureScheduled(v: PlanVersionRow, today: string): boolean {
525
+ if (v.publishedAt === null) return false;
526
+ if (!v.validFrom) return false;
527
+ return v.validFrom.slice(0, 10) > today;
528
+ }
529
+
530
+ function isExpired(v: PlanVersionRow, today: string): boolean {
531
+ if (v.publishedAt === null) return false;
532
+ if (!v.validUntil) return false;
533
+ return v.validUntil.slice(0, 10) < today;
534
+ }
535
+
536
+ const resolvedPlans = computed<ResolvedPlan[]>(() => {
537
+ const today = todayIsoDate();
538
+ return [...props.plans]
539
+ .sort((a, b) => a.sortOrder - b.sortOrder || a.planKey.localeCompare(b.planKey))
540
+ .map<ResolvedPlan>((plan) => {
541
+ const versions = props.versionsByPlanId[plan.id] ?? [];
542
+ const drafts = versions.filter((v) => v.publishedAt === null);
543
+ const published = versions.filter((v) => v.publishedAt !== null);
544
+ const currentLive = published.find((v) => isCurrentlyValid(v, today)) ?? null;
545
+ const futureScheduled = published
546
+ .filter((v) => isFutureScheduled(v, today))
547
+ .sort((a, b) => (a.validFrom ?? '').localeCompare(b.validFrom ?? ''));
548
+
549
+ // Parent = currentLive preferred; otherwise the next upcoming
550
+ // future version; otherwise null (drafts only → root row without a version).
551
+ const primary = currentLive ?? futureScheduled[0] ?? null;
552
+
553
+ // Sub-rows = all visible versions except the parent
554
+ const subSet = new Set<string>();
555
+ if (primary) subSet.add(primary.id);
556
+ const subRows: PlanVersionRow[] = [];
557
+ for (const v of futureScheduled) {
558
+ if (!subSet.has(v.id)) {
559
+ subRows.push(v);
560
+ subSet.add(v.id);
561
+ }
562
+ }
563
+ for (const d of drafts) {
564
+ if (!subSet.has(d.id)) {
565
+ subRows.push(d);
566
+ subSet.add(d.id);
567
+ }
568
+ }
569
+
570
+ const allExpired =
571
+ versions.length > 0 &&
572
+ drafts.length === 0 &&
573
+ published.every((v) => isExpired(v, today)) &&
574
+ futureScheduled.length === 0;
575
+
576
+ return {
577
+ plan,
578
+ planKey: plan.planKey,
579
+ label: plan.label,
580
+ description: plan.description ?? null,
581
+ currentLive,
582
+ primary,
583
+ subRows,
584
+ hasAnyVersion: versions.length > 0,
585
+ allExpired,
586
+ draft: drafts[0] ?? null,
587
+ tenantCount: props.tenantCountsByPlanKey[plan.planKey] ?? 0,
588
+ };
589
+ });
590
+ });
591
+
592
+ const filteredPlans = computed(() => {
593
+ // Plans with only expired versions are hidden entirely
594
+ // (SPEC_V2 §4.2.1: only currently-valid + future ones stay visible
595
+ // in the admin listing).
596
+ const base = resolvedPlans.value.filter((p) => !p.allExpired);
597
+ const q = search.value.trim().toLocaleLowerCase('de-DE');
598
+ if (!q) return base;
599
+ return base.filter(
600
+ (p) =>
601
+ p.planKey.toLocaleLowerCase('de-DE').includes(q) ||
602
+ p.label.toLocaleLowerCase('de-DE').includes(q),
603
+ );
604
+ });
605
+
606
+ const liveCount = computed(() => resolvedPlans.value.filter((p) => p.currentLive !== null).length);
607
+ const draftCount = computed(() => resolvedPlans.value.filter((p) => p.draft !== null).length);
608
+ const totalTenants = computed(() => resolvedPlans.value.reduce((sum, p) => sum + p.tenantCount, 0));
609
+
610
+ function formatMoney(raw: string | number): string {
611
+ const num = typeof raw === 'string' ? Number(raw) : raw;
612
+ if (!Number.isFinite(num)) return String(raw);
613
+ if (Number.isInteger(num)) return `${num} €`;
614
+ return `${num.toFixed(2).replace('.', ',')} €`;
615
+ }
616
+
617
+ function tenantBarWidth(count: number): string {
618
+ if (count <= 0) return '0%';
619
+ return `${Math.min(100, count / 1.5)}%`;
620
+ }
621
+
622
+ function onNewVersion(row: ResolvedPlan): void {
623
+ if (row.draft) return; // already an open draft → no new one
624
+ const basis = row.currentLive;
625
+ if (!basis) {
626
+ // without a live version: via the cockpit path
627
+ emit('openPlan', row.plan);
628
+ return;
629
+ }
630
+ emit('newVersion', row.plan, basis);
631
+ }
632
+
633
+ function hasAnyPublished(row: ResolvedPlan): boolean {
634
+ // Superseded or expired counts too — the plan root stays in the DB
635
+ // forever for contract-protection P1 reasons.
636
+ const versions = props.versionsByPlanId[row.plan.id] ?? [];
637
+ return versions.some((v) => v.publishedAt !== null);
638
+ }
639
+ </script>
640
+
641
+ <style scoped>
642
+ .pl {
643
+ --pl-bg: #f6f7f9;
644
+ --pl-surface: #ffffff;
645
+ --pl-surface-2: #f8fafc;
646
+ --pl-border: #e5e7eb;
647
+ --pl-border-strong: #d1d5db;
648
+ --pl-text: #0f172a;
649
+ --pl-text-2: #475569;
650
+ --pl-text-3: #94a3b8;
651
+ --pl-primary: #2563eb;
652
+ --pl-primary-700: #1d4ed8;
653
+ --pl-live-bg: #ecfdf5;
654
+ --pl-draft-bg: #fffbeb;
655
+ --pl-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
656
+ --pl-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
657
+
658
+ padding: 22px 26px;
659
+ background: var(--pl-bg);
660
+ color: var(--pl-text);
661
+ font-family: var(--pl-font-sans);
662
+ min-height: 100%;
663
+ box-sizing: border-box;
664
+ }
665
+ .pl * {
666
+ box-sizing: border-box;
667
+ }
668
+
669
+ .pl-page-head {
670
+ display: flex;
671
+ align-items: flex-end;
672
+ gap: 20px;
673
+ margin-bottom: 18px;
674
+ }
675
+ .pl-h-title {
676
+ font-size: 22px;
677
+ font-weight: 700;
678
+ letter-spacing: -0.02em;
679
+ margin: 0;
680
+ }
681
+ .pl-h-sub {
682
+ font-size: 12.5px;
683
+ color: var(--pl-text-2);
684
+ margin: 3px 0 0;
685
+ }
686
+ .pl-head-actions {
687
+ margin-left: auto;
688
+ display: flex;
689
+ gap: 8px;
690
+ }
691
+
692
+ /* Buttons */
693
+ .pl-btn {
694
+ display: inline-flex;
695
+ align-items: center;
696
+ gap: 7px;
697
+ padding: 8px 14px;
698
+ border-radius: 7px;
699
+ font: 500 13px var(--pl-font-sans);
700
+ cursor: pointer;
701
+ border: 1px solid var(--pl-border-strong);
702
+ background: #fff;
703
+ color: var(--pl-text);
704
+ transition:
705
+ background 0.12s,
706
+ border-color 0.12s;
707
+ }
708
+ .pl-btn:hover:not(:disabled) {
709
+ background: var(--pl-surface-2);
710
+ }
711
+ .pl-btn:disabled {
712
+ cursor: not-allowed;
713
+ opacity: 0.4;
714
+ }
715
+ .pl-btn--primary {
716
+ background: var(--pl-primary);
717
+ border-color: var(--pl-primary);
718
+ color: #fff;
719
+ }
720
+ .pl-btn--primary:hover {
721
+ background: var(--pl-primary-700);
722
+ }
723
+ .pl-btn--ghost {
724
+ border-color: transparent;
725
+ background: transparent;
726
+ }
727
+ .pl-btn--ghost:hover:not(:disabled) {
728
+ background: rgba(15, 23, 42, 0.05);
729
+ }
730
+ .pl-btn--danger {
731
+ color: #ef4444;
732
+ }
733
+ .pl-btn--danger:hover:not(:disabled) {
734
+ background: #fef2f2;
735
+ color: #b91c1c;
736
+ }
737
+ .pl-btn--sm {
738
+ padding: 5px 8px;
739
+ font-size: 12px;
740
+ gap: 5px;
741
+ }
742
+
743
+ /* List wrap */
744
+ .pl-wrap {
745
+ background: var(--pl-surface);
746
+ border: 1px solid var(--pl-border);
747
+ border-radius: 10px;
748
+ overflow: hidden;
749
+ }
750
+ .pl-toolbar {
751
+ display: flex;
752
+ align-items: center;
753
+ gap: 10px;
754
+ padding: 12px 16px;
755
+ border-bottom: 1px solid var(--pl-border);
756
+ background: #fbfbfd;
757
+ }
758
+ .pl-search {
759
+ flex: 1;
760
+ max-width: 360px;
761
+ display: flex;
762
+ align-items: center;
763
+ gap: 8px;
764
+ background: #fff;
765
+ border: 1px solid var(--pl-border);
766
+ border-radius: 7px;
767
+ padding: 7px 10px;
768
+ }
769
+ .pl-search-ico {
770
+ color: var(--pl-text-3);
771
+ display: inline-flex;
772
+ }
773
+ .pl-search input {
774
+ flex: 1;
775
+ border: 0;
776
+ outline: 0;
777
+ font: 13px var(--pl-font-sans);
778
+ background: transparent;
779
+ color: var(--pl-text);
780
+ }
781
+ .pl-kbd {
782
+ font: 600 10.5px var(--pl-font-mono);
783
+ background: #f1f5f9;
784
+ color: var(--pl-text-2);
785
+ padding: 2px 6px;
786
+ border-radius: 4px;
787
+ border: 1px solid #e2e8f0;
788
+ }
789
+ .pl-toolbar-spacer {
790
+ flex: 1;
791
+ }
792
+ .pl-sortinfo {
793
+ margin-left: auto;
794
+ font-size: 11.5px;
795
+ color: var(--pl-text-3);
796
+ }
797
+
798
+ /* List grid */
799
+ .pl-list {
800
+ display: grid;
801
+ grid-template-columns: 1.6fr 0.9fr 0.7fr 0.9fr 1.4fr 160px;
802
+ align-items: center;
803
+ }
804
+ .pl-list-head {
805
+ display: contents;
806
+ }
807
+ .pl-list-head > div {
808
+ background: #fbfbfd;
809
+ padding: 10px 16px;
810
+ font-size: 10.5px;
811
+ text-transform: uppercase;
812
+ letter-spacing: 0.1em;
813
+ color: var(--pl-text-2);
814
+ font-weight: 700;
815
+ border-bottom: 1px solid var(--pl-border);
816
+ }
817
+ .pl-empty {
818
+ grid-column: 1 / -1;
819
+ padding: 32px 24px;
820
+ text-align: center;
821
+ color: var(--pl-text-3);
822
+ font-style: italic;
823
+ font-size: 13px;
824
+ }
825
+ .pl-row {
826
+ display: contents;
827
+ cursor: pointer;
828
+ }
829
+ .pl-row > .pl-cell {
830
+ padding: 14px 16px;
831
+ border-bottom: 1px solid #f1f5f9;
832
+ transition: background 0.12s;
833
+ }
834
+ .pl-row:hover > .pl-cell {
835
+ background: #fcfcfd;
836
+ }
837
+ .pl-row--new > .pl-cell {
838
+ background: var(--pl-live-bg) !important;
839
+ animation: pl-flashNew 2.4s ease-out;
840
+ }
841
+ @keyframes pl-flashNew {
842
+ 0% {
843
+ background: #d1fae5 !important;
844
+ }
845
+ 100% {
846
+ background: var(--pl-live-bg) !important;
847
+ }
848
+ }
849
+
850
+ /* Sub-rows (drafts + future-scheduled versions, indented under the parent) */
851
+ .pl-row--sub > .pl-cell {
852
+ background: #fafbfd;
853
+ padding-top: 10px;
854
+ padding-bottom: 10px;
855
+ }
856
+ .pl-row--sub:hover > .pl-cell {
857
+ background: #f1f5f9;
858
+ }
859
+ .pl-cell--sub-name {
860
+ display: flex;
861
+ align-items: center;
862
+ gap: 12px;
863
+ padding-left: 32px !important;
864
+ }
865
+ .pl-sub-tree {
866
+ display: inline-flex;
867
+ align-items: center;
868
+ width: 28px;
869
+ height: 28px;
870
+ flex: 0 0 auto;
871
+ position: relative;
872
+ }
873
+ .pl-sub-tree-elbow {
874
+ position: absolute;
875
+ top: 0;
876
+ bottom: 50%;
877
+ left: 12px;
878
+ width: 14px;
879
+ border-left: 1.5px solid #cbd5e1;
880
+ border-bottom: 1.5px solid #cbd5e1;
881
+ border-bottom-left-radius: 6px;
882
+ }
883
+ .pl-sub-titles {
884
+ min-width: 0;
885
+ }
886
+ .pl-sub-title {
887
+ font-size: 13px;
888
+ font-weight: 600;
889
+ color: var(--pl-text-2);
890
+ display: flex;
891
+ align-items: center;
892
+ gap: 8px;
893
+ }
894
+ .pl-sub-desc {
895
+ font-size: 11.5px;
896
+ color: #64748b;
897
+ margin-top: 2px;
898
+ overflow: hidden;
899
+ text-overflow: ellipsis;
900
+ white-space: nowrap;
901
+ }
902
+ .pl-version-num--sub {
903
+ font-size: 13px;
904
+ font-weight: 600;
905
+ color: var(--pl-text-2);
906
+ }
907
+ .pl-cell--sub-impact {
908
+ color: var(--pl-text-3);
909
+ }
910
+
911
+ .pl-plan-name {
912
+ display: flex;
913
+ align-items: center;
914
+ gap: 12px;
915
+ }
916
+ .pl-plan-mark {
917
+ width: 36px;
918
+ height: 36px;
919
+ border-radius: 8px;
920
+ display: grid;
921
+ place-items: center;
922
+ font: 700 11px var(--pl-font-mono);
923
+ letter-spacing: 0.04em;
924
+ flex: 0 0 auto;
925
+ border: 1px solid;
926
+ }
927
+ .pl-plan-titles {
928
+ min-width: 0;
929
+ }
930
+ .pl-plan-title {
931
+ font-size: 14.5px;
932
+ font-weight: 700;
933
+ color: var(--pl-text);
934
+ letter-spacing: -0.01em;
935
+ display: flex;
936
+ align-items: center;
937
+ gap: 8px;
938
+ flex-wrap: wrap;
939
+ }
940
+ .pl-plan-desc {
941
+ font-size: 11.5px;
942
+ color: #64748b;
943
+ margin-top: 2px;
944
+ overflow: hidden;
945
+ text-overflow: ellipsis;
946
+ white-space: nowrap;
947
+ }
948
+
949
+ .pl-cell--status {
950
+ display: flex;
951
+ flex-direction: column;
952
+ gap: 3px;
953
+ align-items: flex-start;
954
+ }
955
+
956
+ /* Chips */
957
+ .pl-chip {
958
+ display: inline-flex;
959
+ align-items: center;
960
+ gap: 5px;
961
+ padding: 2px 8px;
962
+ border-radius: 999px;
963
+ font-size: 11px;
964
+ font-weight: 600;
965
+ background: var(--pl-surface-2);
966
+ color: var(--pl-text-2);
967
+ border: 1px solid var(--pl-border);
968
+ }
969
+ .pl-chip--tiny {
970
+ padding: 1px 6px;
971
+ font-size: 10px;
972
+ }
973
+ .pl-chip--new {
974
+ background: #dbeafe;
975
+ color: #1e40af;
976
+ border-color: #bfdbfe;
977
+ font-size: 10px;
978
+ }
979
+ .pl-chip--live {
980
+ background: var(--pl-live-bg);
981
+ color: #047857;
982
+ border-color: #a7f3d0;
983
+ }
984
+ .pl-chip--draft {
985
+ background: var(--pl-draft-bg);
986
+ color: #b45309;
987
+ border-color: #fde68a;
988
+ }
989
+ .pl-chip--supersed {
990
+ background: #f1f5f9;
991
+ color: var(--pl-text-2);
992
+ border-color: #cbd5e1;
993
+ }
994
+ .pl-chip--scheduled {
995
+ background: #eef2ff;
996
+ color: #4338ca;
997
+ border-color: #c7d2fe;
998
+ }
999
+ .pl-chip--dot::before {
1000
+ content: '';
1001
+ width: 6px;
1002
+ height: 6px;
1003
+ border-radius: 50%;
1004
+ background: currentColor;
1005
+ }
1006
+
1007
+ .pl-version-num {
1008
+ font-size: 13px;
1009
+ font-weight: 700;
1010
+ color: var(--pl-text);
1011
+ }
1012
+ .pl-version-num--muted {
1013
+ color: #cbd5e1;
1014
+ }
1015
+ .pl-version-sub {
1016
+ font-size: 10.5px;
1017
+ color: var(--pl-text-3);
1018
+ margin-top: 2px;
1019
+ }
1020
+
1021
+ .pl-price-big {
1022
+ font-size: 14px;
1023
+ font-weight: 700;
1024
+ color: var(--pl-text);
1025
+ }
1026
+ .pl-price-unit {
1027
+ font-size: 11px;
1028
+ color: var(--pl-text-3);
1029
+ }
1030
+ .pl-price-sub {
1031
+ font-size: 10.5px;
1032
+ color: var(--pl-text-3);
1033
+ margin-top: 2px;
1034
+ }
1035
+ .pl-price-text {
1036
+ font-size: 13px;
1037
+ font-weight: 600;
1038
+ color: var(--pl-text);
1039
+ }
1040
+
1041
+ .pl-cell--tenants {
1042
+ display: flex;
1043
+ align-items: center;
1044
+ gap: 10px;
1045
+ }
1046
+ .pl-tenant-num {
1047
+ font-size: 16px;
1048
+ font-weight: 700;
1049
+ color: var(--pl-text);
1050
+ }
1051
+ .pl-tenant-bar {
1052
+ flex: 1;
1053
+ height: 6px;
1054
+ background: #f1f5f9;
1055
+ border-radius: 999px;
1056
+ overflow: hidden;
1057
+ }
1058
+ .pl-tenant-bar-fill {
1059
+ height: 100%;
1060
+ }
1061
+
1062
+ .pl-cell--actions {
1063
+ display: flex;
1064
+ gap: 4px;
1065
+ justify-content: flex-end;
1066
+ }
1067
+ </style>