@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,993 @@
1
+ <template>
2
+ <div class="pc">
3
+ <PlanCockpitHeader
4
+ :plan="plan"
5
+ :accent-bg="accentBg"
6
+ :accent-fg="accentFg"
7
+ :has-open-draft="hasOpenDraft"
8
+ @back="$emit('back')"
9
+ @clone-plan="$emit('clonePlan')"
10
+ @view-catalog="$emit('viewCatalog')"
11
+ @create-draft="$emit('createDraft')"
12
+ />
13
+
14
+ <PlanCockpitKpis
15
+ :live-version="liveVersion"
16
+ :tenant-impact-total="tenantImpactTotal"
17
+ :versions-count="versions.length"
18
+ :published-count="publishedCount"
19
+ :draft-count="draftCount"
20
+ :draft-version="draftVersion"
21
+ />
22
+
23
+ <!-- Two columns -->
24
+ <div class="pc-grid">
25
+ <PlanCockpitVersions
26
+ :versions="versions"
27
+ :sorted-versions="sortedVersions"
28
+ :timeline-segments="timelineSegments"
29
+ :timeline-axis="timelineAxis"
30
+ :has-open-draft="hasOpenDraft"
31
+ :next-draft-version="nextDraftVersion"
32
+ :impact-by-version="impactByVersion"
33
+ :draft-version="draftVersion"
34
+ @create-draft="$emit('createDraft')"
35
+ @publish="$emit('publish', $event)"
36
+ @edit-draft="$emit('editDraft', $event)"
37
+ @view-diff="forwardViewDiff"
38
+ />
39
+
40
+ <!-- RIGHT — Diff + Impact -->
41
+ <div class="pc-right-col">
42
+ <PlanCockpitDiffPanel :diff-pair="diffPair" :diff="diff" :diff-rows="diffRows" />
43
+
44
+ <PlanCockpitImpactPanel
45
+ :tenant-impact="tenantImpact"
46
+ :tenant-impact-total="tenantImpactTotal"
47
+ />
48
+ </div>
49
+ </div>
50
+
51
+ <PlanCockpitAuditLog :audit-rows="auditRows" :loading-audit="loadingAudit" />
52
+ </div>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { computed } from 'vue';
57
+ import type { PlanRow, PlanVersionRow } from '@saasicat/types';
58
+ import PlanCockpitAuditLog from './PlanCockpitAuditLog.vue';
59
+ import PlanCockpitDiffPanel from './PlanCockpitDiffPanel.vue';
60
+ import PlanCockpitHeader from './PlanCockpitHeader.vue';
61
+ import PlanCockpitImpactPanel from './PlanCockpitImpactPanel.vue';
62
+ import PlanCockpitKpis from './PlanCockpitKpis.vue';
63
+ import PlanCockpitVersions from './PlanCockpitVersions.vue';
64
+ import type {
65
+ AuditRow,
66
+ DiffRow,
67
+ DiffSummary,
68
+ DiscoveryQuota,
69
+ FeatureMeta,
70
+ PlanVersionPair,
71
+ TenantImpact,
72
+ TimelineSegment,
73
+ } from './types';
74
+
75
+ // PlanCockpit — V3 drill-in for a single plan: version timeline,
76
+ // diff draft→live, tenant impact, audit log. Data comes from the consumer
77
+ // (PlansPage). The diff is computed locally from two PlanVersionRows.
78
+
79
+ const props = withDefaults(
80
+ defineProps<{
81
+ plan: PlanRow;
82
+ versions: PlanVersionRow[];
83
+ /** Accent color for the plan header. */
84
+ accent?: string;
85
+ /** Optional: tenant impact data for the publish preview. */
86
+ tenantImpact?: TenantImpact | null;
87
+ /** Tenant count per version (for the "Impact" column in the versions table). */
88
+ impactByVersion?: Record<number, number>;
89
+ /** Audit log entries for this plan. */
90
+ auditRows?: AuditRow[];
91
+ loadingAudit?: boolean;
92
+ /** Discovery quotas (for diff labels). */
93
+ availableQuotas?: DiscoveryQuota[];
94
+ featureRegistry?: Record<string, FeatureMeta>;
95
+ }>(),
96
+ {
97
+ accent: '#2563eb',
98
+ tenantImpact: null,
99
+ impactByVersion: () => ({}),
100
+ auditRows: () => [],
101
+ loadingAudit: false,
102
+ availableQuotas: () => [],
103
+ featureRegistry: () => ({}),
104
+ },
105
+ );
106
+
107
+ const emit = defineEmits<{
108
+ (e: 'back'): void;
109
+ (e: 'createDraft'): void;
110
+ (e: 'editDraft', version: PlanVersionRow): void;
111
+ (e: 'publish', version: PlanVersionRow): void;
112
+ (e: 'clonePlan'): void;
113
+ (e: 'viewCatalog'): void;
114
+ (e: 'viewDiff', from: PlanVersionRow, to: PlanVersionRow): void;
115
+ }>();
116
+
117
+ function forwardViewDiff(from: PlanVersionRow, to: PlanVersionRow): void {
118
+ emit('viewDiff', from, to);
119
+ }
120
+
121
+ const accentBg = computed(() => hexToTint(props.accent, 0.15));
122
+ const accentFg = computed(() => props.accent);
123
+
124
+ function hexToTint(hex: string, opacity: number): string {
125
+ if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return '#dbeafe';
126
+ const r = parseInt(hex.slice(1, 3), 16);
127
+ const g = parseInt(hex.slice(3, 5), 16);
128
+ const b = parseInt(hex.slice(5, 7), 16);
129
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
130
+ }
131
+
132
+ const sortedVersions = computed<PlanVersionRow[]>(() =>
133
+ [...props.versions].sort((a, b) => b.version - a.version),
134
+ );
135
+
136
+ const liveVersion = computed<PlanVersionRow | null>(
137
+ () => props.versions.find((v) => v.publishedAt !== null && v.supersededAt === null) ?? null,
138
+ );
139
+
140
+ const draftVersion = computed<PlanVersionRow | null>(
141
+ () => props.versions.find((v) => v.publishedAt === null) ?? null,
142
+ );
143
+
144
+ const hasOpenDraft = computed(() => draftVersion.value !== null);
145
+
146
+ const publishedCount = computed(() => props.versions.filter((v) => v.publishedAt !== null).length);
147
+ const draftCount = computed(() => props.versions.filter((v) => v.publishedAt === null).length);
148
+
149
+ const nextDraftVersion = computed(() => {
150
+ const max = props.versions.reduce((m, v) => Math.max(m, v.version), 0);
151
+ return max + 1;
152
+ });
153
+
154
+ const tenantImpactTotal = computed(() => {
155
+ if (props.tenantImpact) {
156
+ return props.tenantImpact.auto + props.tenantImpact.review + props.tenantImpact.conflict;
157
+ }
158
+ return Object.values(props.impactByVersion).reduce((s, n) => s + n, 0);
159
+ });
160
+
161
+ // ── Timeline ────────────────────────────────────────────────────────
162
+ const timelineSegments = computed<TimelineSegment[]>(() => {
163
+ return [...props.versions]
164
+ .sort((a, b) => a.version - b.version)
165
+ .map((v) => ({
166
+ key: v.id,
167
+ version: v.version,
168
+ status:
169
+ v.publishedAt === null ? 'draft' : v.supersededAt !== null ? 'supersed' : 'live',
170
+ flex: 1,
171
+ }));
172
+ });
173
+
174
+ const timelineAxis = computed(() => {
175
+ const labels: string[] = [];
176
+ for (const v of [...props.versions].sort((a, b) => a.version - b.version)) {
177
+ if (v.validFrom) labels.push(v.validFrom.slice(0, 7));
178
+ }
179
+ labels.push('jetzt');
180
+ return labels;
181
+ });
182
+
183
+ // ── Formatting ──────────────────────────────────────────────────────
184
+ function formatMoney(raw: string | number): string {
185
+ const num = typeof raw === 'string' ? Number(raw) : raw;
186
+ if (!Number.isFinite(num)) return String(raw);
187
+ if (Number.isInteger(num)) return `${num} €`;
188
+ return `${num.toFixed(2).replace('.', ',')} €`;
189
+ }
190
+
191
+ // ── Diff ────────────────────────────────────────────────────────────
192
+ const diffPair = computed<PlanVersionPair | null>(() => {
193
+ const to = draftVersion.value ?? sortedVersions.value[0] ?? null;
194
+ if (!to) return null;
195
+ const from = sortedVersions.value.find((v) => v.version < to.version && v.publishedAt !== null);
196
+ if (!from || from.id === to.id) return null;
197
+ return { from, to };
198
+ });
199
+
200
+ function quotasOf(v: PlanVersionRow): Record<string, number> {
201
+ if (v.quotas && Object.keys(v.quotas).length > 0) return v.quotas;
202
+ const legacy: Record<string, number> = {};
203
+ if (typeof v.maxUsers === 'number') legacy.users = v.maxUsers;
204
+ if (typeof v.maxVehicles === 'number') legacy.vehicles = v.maxVehicles;
205
+ if (typeof v.maxStorageGb === 'number') legacy.storageGb = v.maxStorageGb;
206
+ return legacy;
207
+ }
208
+
209
+ function featureLabel(key: string): string {
210
+ return props.featureRegistry[key]?.label ?? key;
211
+ }
212
+ function quotaLabel(key: string): string {
213
+ return props.availableQuotas.find((q) => q.quotaKey === key)?.label || key;
214
+ }
215
+ function quotaUnit(key: string): string {
216
+ return props.availableQuotas.find((q) => q.quotaKey === key)?.unit || '';
217
+ }
218
+
219
+ const diff = computed<DiffSummary>(() => {
220
+ if (!diffPair.value) {
221
+ return {
222
+ addedFeatures: [] as string[],
223
+ removedFeatures: [] as string[],
224
+ addedQuotas: [] as Array<{ key: string; value: number }>,
225
+ removedQuotas: [] as Array<{ key: string; value: number }>,
226
+ changedQuotas: [] as Array<{ key: string; from: number; to: number }>,
227
+ priceChanged: false,
228
+ priceFrom: '',
229
+ priceTo: '',
230
+ };
231
+ }
232
+ const { from, to } = diffPair.value;
233
+ const addedFeatures = to.features.filter((f) => !from.features.includes(f));
234
+ const removedFeatures = from.features.filter((f) => !to.features.includes(f));
235
+ const fromQ = quotasOf(from);
236
+ const toQ = quotasOf(to);
237
+ const addedQuotas: Array<{ key: string; value: number }> = [];
238
+ const removedQuotas: Array<{ key: string; value: number }> = [];
239
+ const changedQuotas: Array<{ key: string; from: number; to: number }> = [];
240
+ const keys = new Set([...Object.keys(fromQ), ...Object.keys(toQ)]);
241
+ for (const k of keys) {
242
+ const fv = fromQ[k];
243
+ const tv = toQ[k];
244
+ if (fv === undefined && tv !== undefined) addedQuotas.push({ key: k, value: tv });
245
+ else if (fv !== undefined && tv === undefined) removedQuotas.push({ key: k, value: fv });
246
+ else if (fv !== undefined && tv !== undefined && fv !== tv) {
247
+ changedQuotas.push({ key: k, from: fv, to: tv });
248
+ }
249
+ }
250
+ const priceChanged = from.monthlyNet !== to.monthlyNet || from.yearlyNet !== to.yearlyNet;
251
+ return {
252
+ addedFeatures,
253
+ removedFeatures,
254
+ addedQuotas,
255
+ removedQuotas,
256
+ changedQuotas,
257
+ priceChanged,
258
+ priceFrom: `${formatMoney(from.monthlyNet)} / Mo · ${formatMoney(from.yearlyNet)} / J`,
259
+ priceTo: `${formatMoney(to.monthlyNet)} / Mo · ${formatMoney(to.yearlyNet)} / J`,
260
+ };
261
+ });
262
+
263
+ const STYLES = {
264
+ added: { bg: '#ecfdf5', border: '#a7f3d0', color: '#047857', sign: '+', tag: 'neu' },
265
+ removed: { bg: '#fef2f2', border: '#fecaca', color: '#b91c1c', sign: '−', tag: 'entfernt' },
266
+ changed: { bg: '#fffbeb', border: '#fde68a', color: '#b45309', sign: '~', tag: 'geändert' },
267
+ } as const;
268
+
269
+ const diffRows = computed<DiffRow[]>(() => {
270
+ const out: DiffRow[] = [];
271
+ for (const f of diff.value.addedFeatures) {
272
+ out.push({
273
+ id: `add-f-${f}`,
274
+ section: 'Features',
275
+ label: featureLabel(f),
276
+ sub: f,
277
+ ...STYLES.added,
278
+ });
279
+ }
280
+ for (const f of diff.value.removedFeatures) {
281
+ out.push({
282
+ id: `rem-f-${f}`,
283
+ section: 'Features',
284
+ label: featureLabel(f),
285
+ sub: f,
286
+ ...STYLES.removed,
287
+ });
288
+ }
289
+ for (const q of diff.value.changedQuotas) {
290
+ const unit = quotaUnit(q.key);
291
+ out.push({
292
+ id: `chg-q-${q.key}`,
293
+ section: 'Quotas',
294
+ label: quotaLabel(q.key),
295
+ sub: q.key,
296
+ from: `${q.from}${unit ? ' ' + unit : ''}`,
297
+ to: `${q.to}${unit ? ' ' + unit : ''}`,
298
+ ...STYLES.changed,
299
+ });
300
+ }
301
+ for (const q of diff.value.addedQuotas) {
302
+ const unit = quotaUnit(q.key);
303
+ out.push({
304
+ id: `add-q-${q.key}`,
305
+ section: 'Quotas',
306
+ label: quotaLabel(q.key),
307
+ sub: q.key,
308
+ to: `${q.value}${unit ? ' ' + unit : ''}`,
309
+ ...STYLES.added,
310
+ });
311
+ }
312
+ for (const q of diff.value.removedQuotas) {
313
+ const unit = quotaUnit(q.key);
314
+ out.push({
315
+ id: `rem-q-${q.key}`,
316
+ section: 'Quotas',
317
+ label: quotaLabel(q.key),
318
+ sub: q.key,
319
+ from: `${q.value}${unit ? ' ' + unit : ''}`,
320
+ ...STYLES.removed,
321
+ });
322
+ }
323
+ if (diff.value.priceChanged) {
324
+ out.push({
325
+ id: 'chg-price',
326
+ section: 'Preis',
327
+ label: 'Jahres- + Monatspreis',
328
+ from: diff.value.priceFrom,
329
+ to: diff.value.priceTo,
330
+ ...STYLES.changed,
331
+ });
332
+ }
333
+ return out;
334
+ });
335
+ </script>
336
+
337
+ <style>
338
+ .pc {
339
+ --pc-bg: #f6f7f9;
340
+ --pc-surface: #ffffff;
341
+ --pc-surface-2: #f8fafc;
342
+ --pc-border: #e5e7eb;
343
+ --pc-border-strong: #d1d5db;
344
+ --pc-text: #0f172a;
345
+ --pc-text-2: #475569;
346
+ --pc-text-3: #94a3b8;
347
+ --pc-primary: #2563eb;
348
+ --pc-live: #10b981;
349
+ --pc-live-bg: #ecfdf5;
350
+ --pc-draft: #f59e0b;
351
+ --pc-draft-bg: #fffbeb;
352
+ --pc-supersed: #94a3b8;
353
+ --pc-supersed-bg: #f1f5f9;
354
+ --pc-font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
355
+ --pc-font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;
356
+
357
+ padding: 18px 26px;
358
+ background: var(--pc-bg);
359
+ color: var(--pc-text);
360
+ font-family: var(--pc-font-sans);
361
+ min-height: 100%;
362
+ box-sizing: border-box;
363
+ }
364
+ .pc * {
365
+ box-sizing: border-box;
366
+ }
367
+ .pc-mono {
368
+ font: 500 11px var(--pc-font-mono);
369
+ }
370
+ .pc-mono--xs {
371
+ font-size: 10.5px;
372
+ color: #64748b;
373
+ }
374
+
375
+ /* Header */
376
+ .pc-header {
377
+ display: flex;
378
+ align-items: center;
379
+ gap: 16px;
380
+ margin-bottom: 18px;
381
+ }
382
+ .pc-header-left {
383
+ display: flex;
384
+ align-items: center;
385
+ gap: 14px;
386
+ flex: 1;
387
+ }
388
+ .pc-header-titles {
389
+ min-width: 0;
390
+ }
391
+ .pc-header-right {
392
+ display: flex;
393
+ gap: 8px;
394
+ }
395
+ .pc-backbar {
396
+ margin-bottom: 12px;
397
+ }
398
+ .pc-back {
399
+ display: inline-flex;
400
+ align-items: center;
401
+ gap: 6px;
402
+ padding: 6px 12px 6px 8px;
403
+ border-radius: 7px;
404
+ background: transparent;
405
+ border: 1px solid transparent;
406
+ color: var(--pc-text-2);
407
+ font: 500 13px var(--pc-font-sans);
408
+ cursor: pointer;
409
+ transition: background 0.12s;
410
+ }
411
+ .pc-back:hover {
412
+ background: rgba(15, 23, 42, 0.05);
413
+ }
414
+ .pc-back-ico {
415
+ display: inline-flex;
416
+ transform: rotate(180deg);
417
+ }
418
+ .pc-bigchip {
419
+ padding: 6px 12px;
420
+ border-radius: 7px;
421
+ font: 700 12px var(--pc-font-mono);
422
+ letter-spacing: 0.06em;
423
+ }
424
+ .pc-h-title {
425
+ font-size: 24px;
426
+ font-weight: 700;
427
+ letter-spacing: -0.02em;
428
+ margin: 0;
429
+ }
430
+ .pc-h-sub {
431
+ font-size: 12.5px;
432
+ color: var(--pc-text-2);
433
+ margin: 3px 0 0;
434
+ }
435
+
436
+ /* Buttons + chips */
437
+ .pc-btn {
438
+ display: inline-flex;
439
+ align-items: center;
440
+ gap: 7px;
441
+ padding: 8px 14px;
442
+ border-radius: 7px;
443
+ font: 500 13px var(--pc-font-sans);
444
+ cursor: pointer;
445
+ border: 1px solid var(--pc-border-strong);
446
+ background: #fff;
447
+ color: var(--pc-text);
448
+ transition:
449
+ background 0.12s,
450
+ border-color 0.12s;
451
+ }
452
+ .pc-btn:hover:not(:disabled) {
453
+ background: var(--pc-surface-2);
454
+ }
455
+ .pc-btn:disabled {
456
+ cursor: not-allowed;
457
+ opacity: 0.5;
458
+ }
459
+ .pc-btn--primary {
460
+ background: var(--pc-primary);
461
+ border-color: var(--pc-primary);
462
+ color: #fff;
463
+ }
464
+ .pc-btn--primary:hover:not(:disabled) {
465
+ background: #1d4ed8;
466
+ }
467
+ .pc-btn--sm {
468
+ padding: 5px 9px;
469
+ font-size: 12px;
470
+ gap: 5px;
471
+ }
472
+
473
+ .pc-chip {
474
+ display: inline-flex;
475
+ align-items: center;
476
+ gap: 5px;
477
+ padding: 2px 8px;
478
+ border-radius: 999px;
479
+ font-size: 11px;
480
+ font-weight: 600;
481
+ background: var(--pc-surface-2);
482
+ color: var(--pc-text-2);
483
+ border: 1px solid var(--pc-border);
484
+ }
485
+ .pc-chip--mid {
486
+ vertical-align: middle;
487
+ margin-left: 6px;
488
+ }
489
+ .pc-chip--live {
490
+ background: var(--pc-live-bg);
491
+ color: #047857;
492
+ border-color: #a7f3d0;
493
+ }
494
+ .pc-chip--draft {
495
+ background: var(--pc-draft-bg);
496
+ color: #b45309;
497
+ border-color: #fde68a;
498
+ }
499
+ .pc-chip--supersed {
500
+ background: var(--pc-supersed-bg);
501
+ color: var(--pc-text-2);
502
+ border-color: #cbd5e1;
503
+ }
504
+ .pc-chip--conflict {
505
+ background: #fef2f2;
506
+ color: #b91c1c;
507
+ border-color: #fecaca;
508
+ }
509
+ .pc-chip--dot::before {
510
+ content: '';
511
+ width: 6px;
512
+ height: 6px;
513
+ border-radius: 50%;
514
+ background: currentColor;
515
+ }
516
+
517
+ /* KPI strip */
518
+ .pc-kpis {
519
+ display: grid;
520
+ grid-template-columns: repeat(4, 1fr);
521
+ gap: 12px;
522
+ margin-bottom: 18px;
523
+ }
524
+ .pc-kpi {
525
+ background: #fff;
526
+ border: 1px solid var(--pc-border);
527
+ border-radius: 10px;
528
+ padding: 14px 16px;
529
+ }
530
+ .pc-kpi--draft {
531
+ background: linear-gradient(180deg, var(--pc-draft-bg) 0%, #fff 100%);
532
+ border-color: #fde68a;
533
+ }
534
+ .pc-kpi-label {
535
+ font-size: 11px;
536
+ text-transform: uppercase;
537
+ letter-spacing: 0.08em;
538
+ color: #64748b;
539
+ font-weight: 600;
540
+ }
541
+ .pc-kpi-val {
542
+ font-size: 24px;
543
+ font-weight: 700;
544
+ letter-spacing: -0.02em;
545
+ margin-top: 4px;
546
+ }
547
+ .pc-kpi-sub {
548
+ font-size: 11.5px;
549
+ color: var(--pc-text-3);
550
+ margin-top: 2px;
551
+ }
552
+
553
+ /* Grid */
554
+ .pc-grid {
555
+ display: grid;
556
+ grid-template-columns: 1.45fr 1fr;
557
+ gap: 14px;
558
+ }
559
+ .pc-right-col {
560
+ display: flex;
561
+ flex-direction: column;
562
+ gap: 14px;
563
+ }
564
+ .pc-card {
565
+ background: #fff;
566
+ border: 1px solid var(--pc-border);
567
+ border-radius: 10px;
568
+ overflow: hidden;
569
+ }
570
+ .pc-card-head {
571
+ padding: 14px 18px;
572
+ border-bottom: 1px solid var(--pc-border);
573
+ display: flex;
574
+ align-items: center;
575
+ gap: 10px;
576
+ }
577
+ .pc-card-head--audit {
578
+ border-bottom: 0;
579
+ padding-bottom: 8px;
580
+ }
581
+ .pc-card-head-text {
582
+ flex: 1;
583
+ min-width: 0;
584
+ }
585
+ .pc-card-title {
586
+ font-size: 14px;
587
+ font-weight: 700;
588
+ letter-spacing: -0.01em;
589
+ }
590
+ .pc-card-sub {
591
+ font-size: 11.5px;
592
+ color: #64748b;
593
+ margin-top: 2px;
594
+ }
595
+ .pc-pillrow {
596
+ display: flex;
597
+ gap: 4px;
598
+ }
599
+ .pc-pill {
600
+ font-size: 10.5px;
601
+ font-weight: 600;
602
+ padding: 2px 8px;
603
+ border-radius: 4px;
604
+ }
605
+
606
+ /* Timeline */
607
+ .pc-vtimeline {
608
+ padding: 16px 18px 6px;
609
+ }
610
+ .pc-vtl-track {
611
+ display: flex;
612
+ gap: 2px;
613
+ height: 26px;
614
+ border-radius: 6px;
615
+ overflow: hidden;
616
+ background: #f1f5f9;
617
+ }
618
+ .pc-vtl-seg {
619
+ display: grid;
620
+ place-items: center;
621
+ font: 600 10.5px var(--pc-font-mono);
622
+ letter-spacing: 0.06em;
623
+ }
624
+ .pc-vtl-supersed {
625
+ background: #e2e8f0;
626
+ color: var(--pc-text-2);
627
+ }
628
+ .pc-vtl-live {
629
+ background: var(--pc-live);
630
+ color: #fff;
631
+ }
632
+ .pc-vtl-draft {
633
+ background: repeating-linear-gradient(45deg, #fcd34d, #fcd34d 4px, #fde68a 4px, #fde68a 8px);
634
+ color: #78350f;
635
+ }
636
+ .pc-vtl-axis {
637
+ display: flex;
638
+ justify-content: space-between;
639
+ margin-top: 6px;
640
+ font: 500 10px var(--pc-font-mono);
641
+ color: var(--pc-text-3);
642
+ }
643
+
644
+ /* Version row — flexible columns so the action buttons (Publish /
645
+ Edit) aren't clipped on a narrow card. Actions =
646
+ `auto` (content-width), the rest flexes. */
647
+ .pc-vrow {
648
+ display: grid;
649
+ grid-template-columns:
650
+ 92px
651
+ minmax(112px, 1fr)
652
+ minmax(82px, 0.8fr)
653
+ 56px
654
+ minmax(90px, 1.3fr)
655
+ auto;
656
+ align-items: center;
657
+ gap: 10px;
658
+ padding: 11px 18px;
659
+ border-top: 1px solid #f1f5f9;
660
+ }
661
+ .pc-vrow--head {
662
+ background: #fbfbfd;
663
+ padding: 8px 18px;
664
+ }
665
+ .pc-vrow--active {
666
+ background: var(--pc-draft-bg);
667
+ }
668
+ .pc-vrow--active:hover {
669
+ background: #fef3c7;
670
+ }
671
+ .pc-vrow:hover:not(.pc-vrow--head):not(.pc-vrow--active) {
672
+ background: #fcfcfd;
673
+ }
674
+ .pc-vrow-headlabel {
675
+ font-size: 10px;
676
+ text-transform: uppercase;
677
+ letter-spacing: 0.08em;
678
+ color: var(--pc-text-3);
679
+ }
680
+ .pc-vrow-version {
681
+ display: flex;
682
+ align-items: center;
683
+ gap: 8px;
684
+ }
685
+ .pc-vrow-vlabel {
686
+ font: 700 14px var(--pc-font-mono);
687
+ color: var(--pc-text);
688
+ }
689
+ .pc-vrow-cell {
690
+ min-width: 0;
691
+ }
692
+ .pc-vrow-cell--right {
693
+ text-align: right;
694
+ }
695
+ .pc-vrow-validity {
696
+ font-size: 12.5px;
697
+ font-weight: 500;
698
+ color: var(--pc-text);
699
+ }
700
+ .pc-vrow-arrow {
701
+ color: var(--pc-text-3);
702
+ margin: 0 4px;
703
+ }
704
+ .pc-vrow-inf {
705
+ color: var(--pc-live);
706
+ font-weight: 700;
707
+ }
708
+ .pc-vrow-sub {
709
+ font-size: 10.5px;
710
+ color: var(--pc-text-3);
711
+ margin-top: 2px;
712
+ }
713
+ .pc-vrow-price {
714
+ font-size: 13px;
715
+ font-weight: 700;
716
+ color: var(--pc-text);
717
+ }
718
+ .pc-vrow-price-unit {
719
+ font-weight: 500;
720
+ color: var(--pc-text-3);
721
+ font-size: 11px;
722
+ }
723
+ .pc-vrow-impact {
724
+ font-size: 13px;
725
+ font-weight: 700;
726
+ color: var(--pc-text);
727
+ }
728
+ .pc-vrow-impact--zero {
729
+ color: #cbd5e1;
730
+ }
731
+ .pc-vrow-note {
732
+ font-size: 12px;
733
+ color: var(--pc-text-2);
734
+ min-width: 0;
735
+ overflow: hidden;
736
+ text-overflow: ellipsis;
737
+ white-space: nowrap;
738
+ }
739
+ .pc-vrow-actions {
740
+ display: flex;
741
+ gap: 6px;
742
+ justify-content: flex-end;
743
+ flex-wrap: wrap;
744
+ }
745
+ .pc-vrow-actions .pc-btn {
746
+ white-space: nowrap;
747
+ }
748
+
749
+ /* Diff */
750
+ .pc-diff-body {
751
+ padding: 12px 16px 16px;
752
+ display: flex;
753
+ flex-direction: column;
754
+ gap: 6px;
755
+ }
756
+ .pc-diff-row {
757
+ display: flex;
758
+ align-items: center;
759
+ gap: 10px;
760
+ padding: 9px 11px;
761
+ border: 1px solid;
762
+ border-radius: 7px;
763
+ }
764
+ .pc-diff-sign {
765
+ width: 20px;
766
+ height: 20px;
767
+ border-radius: 5px;
768
+ color: #fff;
769
+ font-weight: 800;
770
+ font-size: 13px;
771
+ display: grid;
772
+ place-items: center;
773
+ flex: 0 0 auto;
774
+ }
775
+ .pc-diff-main {
776
+ flex: 1;
777
+ min-width: 0;
778
+ }
779
+ .pc-diff-headline {
780
+ display: flex;
781
+ align-items: baseline;
782
+ gap: 8px;
783
+ flex-wrap: wrap;
784
+ }
785
+ .pc-diff-section {
786
+ font-size: 10.5px;
787
+ text-transform: uppercase;
788
+ letter-spacing: 0.08em;
789
+ font-weight: 700;
790
+ }
791
+ .pc-diff-label {
792
+ font-size: 13px;
793
+ font-weight: 600;
794
+ color: var(--pc-text);
795
+ }
796
+ .pc-diff-change {
797
+ display: flex;
798
+ align-items: center;
799
+ gap: 8px;
800
+ margin-top: 3px;
801
+ font-size: 12px;
802
+ }
803
+ .pc-diff-from {
804
+ text-decoration: line-through;
805
+ color: var(--pc-text-3);
806
+ }
807
+ .pc-diff-to {
808
+ color: var(--pc-text);
809
+ font-weight: 600;
810
+ }
811
+ .pc-diff-arrow {
812
+ display: inline-flex;
813
+ }
814
+ .pc-diff-tag {
815
+ font-size: 10px;
816
+ }
817
+
818
+ /* Impact */
819
+ .pc-impact-strip {
820
+ display: flex;
821
+ align-items: center;
822
+ gap: 20px;
823
+ padding: 16px 18px;
824
+ border-bottom: 1px solid #f1f5f9;
825
+ }
826
+ .pc-impact-num {
827
+ font-size: 44px;
828
+ font-weight: 700;
829
+ letter-spacing: -0.03em;
830
+ color: var(--pc-text);
831
+ line-height: 1;
832
+ }
833
+ .pc-impact-bars {
834
+ flex: 1;
835
+ display: flex;
836
+ flex-direction: column;
837
+ gap: 6px;
838
+ }
839
+ .pc-impact-bar {
840
+ display: flex;
841
+ align-items: center;
842
+ gap: 10px;
843
+ height: 22px;
844
+ position: relative;
845
+ }
846
+ .pc-impact-bar-fill {
847
+ height: 8px;
848
+ border-radius: 999px;
849
+ min-width: 16px;
850
+ }
851
+ .pc-impact-bar-label {
852
+ font-size: 11.5px;
853
+ color: var(--pc-text-2);
854
+ }
855
+ .pc-impact-tenants {
856
+ padding: 12px 18px 16px;
857
+ display: flex;
858
+ flex-direction: column;
859
+ gap: 4px;
860
+ }
861
+ .pc-impact-tenant {
862
+ display: flex;
863
+ align-items: center;
864
+ gap: 10px;
865
+ padding: 7px 9px;
866
+ border-radius: 6px;
867
+ }
868
+ .pc-impact-tenant:hover {
869
+ background: var(--pc-surface-2);
870
+ }
871
+ .pc-impact-avatar {
872
+ width: 28px;
873
+ height: 28px;
874
+ border-radius: 6px;
875
+ background: #f1f5f9;
876
+ color: var(--pc-text-2);
877
+ display: grid;
878
+ place-items: center;
879
+ font-size: 11px;
880
+ font-weight: 700;
881
+ flex: 0 0 auto;
882
+ }
883
+ .pc-impact-body {
884
+ flex: 1;
885
+ min-width: 0;
886
+ }
887
+ .pc-impact-name {
888
+ font-size: 13px;
889
+ font-weight: 600;
890
+ color: var(--pc-text);
891
+ overflow: hidden;
892
+ text-overflow: ellipsis;
893
+ white-space: nowrap;
894
+ }
895
+ .pc-impact-sub {
896
+ font: 500 10.5px var(--pc-font-mono);
897
+ color: var(--pc-text-3);
898
+ }
899
+
900
+ /* Audit */
901
+ .pc-audit-card {
902
+ margin-top: 14px;
903
+ }
904
+ .pc-audit {
905
+ padding: 4px 18px 16px;
906
+ }
907
+ .pc-audit-row {
908
+ display: flex;
909
+ align-items: center;
910
+ gap: 14px;
911
+ padding: 9px 0;
912
+ border-top: 1px solid #f1f5f9;
913
+ }
914
+ .pc-audit-row:first-child {
915
+ border-top: 0;
916
+ }
917
+ .pc-audit-dot {
918
+ width: 7px;
919
+ height: 7px;
920
+ border-radius: 50%;
921
+ flex: 0 0 auto;
922
+ }
923
+ .pc-audit-draft {
924
+ background: #f59e0b;
925
+ }
926
+ .pc-audit-add {
927
+ background: #10b981;
928
+ }
929
+ .pc-audit-change {
930
+ background: #2563eb;
931
+ }
932
+ .pc-audit-publish {
933
+ background: #8b5cf6;
934
+ }
935
+ .pc-audit-remove {
936
+ background: #ef4444;
937
+ }
938
+ .pc-audit-when {
939
+ font-size: 11.5px;
940
+ color: #64748b;
941
+ min-width: 130px;
942
+ }
943
+ .pc-audit-avatar {
944
+ width: 24px;
945
+ height: 24px;
946
+ border-radius: 50%;
947
+ background: var(--pc-text);
948
+ color: #fff;
949
+ display: grid;
950
+ place-items: center;
951
+ font: 700 11px var(--pc-font-sans);
952
+ flex: 0 0 auto;
953
+ }
954
+ .pc-audit-who {
955
+ font-size: 12px;
956
+ color: var(--pc-text-2);
957
+ min-width: 64px;
958
+ }
959
+ .pc-audit-what {
960
+ font-size: 13px;
961
+ color: var(--pc-text);
962
+ flex: 1;
963
+ }
964
+
965
+ /* Empty */
966
+ .pc-empty {
967
+ padding: 22px 18px;
968
+ text-align: center;
969
+ color: var(--pc-text-3);
970
+ font-style: italic;
971
+ font-size: 13px;
972
+ }
973
+ .pc-empty--inline {
974
+ padding: 12px;
975
+ }
976
+
977
+ @media (max-width: 1100px) {
978
+ .pc-grid {
979
+ grid-template-columns: 1fr;
980
+ }
981
+ .pc-kpis {
982
+ grid-template-columns: repeat(2, 1fr);
983
+ }
984
+ .pc-vrow {
985
+ grid-template-columns: 88px 1fr 1fr 52px minmax(80px, 1.2fr);
986
+ gap: 8px;
987
+ }
988
+ .pc-vrow-actions {
989
+ grid-column: 1 / -1;
990
+ justify-content: flex-start;
991
+ }
992
+ }
993
+ </style>