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