@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,910 @@
1
+ <template>
2
+ <div class="sa-bundles">
3
+ <BundlesHeader
4
+ :loading="loading"
5
+ :display-locale="displayLocale"
6
+ :locales="locales"
7
+ @create="openCreatePanel"
8
+ @refresh="load"
9
+ @update:display-locale="(value) => (displayLocale = value)"
10
+ />
11
+
12
+ <q-banner v-if="error" class="sa-bundles__error" rounded>
13
+ <template #avatar><q-icon name="warning" color="negative" /></template>
14
+ Fehler beim Laden: {{ error.message }}
15
+ </q-banner>
16
+
17
+ <BundlesKpis
18
+ :bundles-total="bundles.length"
19
+ :live-count="liveCount"
20
+ :scheduled-bundles-count="scheduledBundlesCount"
21
+ :total-scheduled-versions="totalScheduledVersions"
22
+ :total-draft-versions="totalDraftVersions"
23
+ :draft-bundles-count="draftBundlesCount"
24
+ :translated-count="translatedCount"
25
+ :locales-count="locales.length"
26
+ />
27
+
28
+ <BundlesFilterBar
29
+ v-model:query="query"
30
+ v-model:status-filter="statusFilter"
31
+ :status-filter-options="statusFilterOptions"
32
+ />
33
+
34
+ <!-- Inline creation (collapsible panel, replaces the former dialog modal) -->
35
+ <BundleCreatePanel
36
+ v-if="createOpen"
37
+ :project-key="projectKey"
38
+ :available-features="snapshot?.features ?? []"
39
+ :available-quotas="snapshot?.quotas ?? []"
40
+ :plans="plans"
41
+ :live-plan-versions="livePlanVersions"
42
+ :feature-registry="featureRegistryResolved"
43
+ :quota-registry="quotaRegistryResolved"
44
+ :existing-bundle-keys="existingBundleKeys"
45
+ :create="create"
46
+ :create-draft="createDraft"
47
+ @cancel="createOpen = false"
48
+ @created="onWizardCreated"
49
+ />
50
+
51
+ <q-banner
52
+ v-if="bundles.length === 0 && !loading && !error"
53
+ class="sa-bundles__empty"
54
+ rounded
55
+ >
56
+ <template #avatar><q-icon name="info" color="info" /></template>
57
+ Noch keine Bundles angelegt. Über <strong>Neues Bundle</strong> einen Bundle-Stamm
58
+ anlegen, dann Features &amp; Quotas in einer Draft-Version kuratieren.
59
+ </q-banner>
60
+
61
+ <BundleAccordionList
62
+ :filtered-bundles="filteredBundles"
63
+ :bundles-total="bundles.length"
64
+ :open-key="openKey"
65
+ :aggregate-status-of="aggregateStatusOf"
66
+ :i18n-locale-count="i18nLocaleCount"
67
+ @toggle="toggle"
68
+ >
69
+ <template #detail="{ bundle }">
70
+ <BundleDetailPanel
71
+ :bundle="bundle"
72
+ v-model:edit-form="editForm"
73
+ :i18n-draft="i18nDraft"
74
+ :translatable-locales="translatableLocales"
75
+ :edit-submitting="editSubmitting"
76
+ :detail-versions="detailVersions"
77
+ :selected-version-id="selectedVersionIdByBundle[bundle.id] ?? null"
78
+ :selected-version="selectedVersion"
79
+ :available-features="snapshot?.features ?? []"
80
+ :available-quotas="snapshot?.quotas ?? []"
81
+ :plans="plans"
82
+ :live-plan-versions="livePlanVersions"
83
+ :feature-registry="featureRegistryResolved"
84
+ :quota-registry="quotaRegistryResolved"
85
+ :inline-editor-saving="inlineEditorSaving"
86
+ :inline-editor-error="inlineEditorError"
87
+ @set-i18n="setI18n"
88
+ @submit-edit="submitEdit"
89
+ @select-version="onSelectVersion"
90
+ @add-version="onAddVersion"
91
+ @inline-save="onInlineSave"
92
+ @discard-version="onDiscardVersion"
93
+ @publish-version="openPublish"
94
+ @delete-bundle="confirmDelete"
95
+ />
96
+ </template>
97
+ </BundleAccordionList>
98
+
99
+ <!-- Strict mode warnings after the last mutation -->
100
+ <q-banner
101
+ v-if="lastWarnings.length > 0"
102
+ class="sa-bundles__warnings-banner"
103
+ inline-actions
104
+ rounded
105
+ >
106
+ <template #avatar><q-icon name="warning" color="warning" /></template>
107
+ <strong>{{ lastWarnings.length }} Strict-Mode-Warnung(en) bei letzter Operation</strong>
108
+ <ul class="sa-bundles__warnings-list">
109
+ <li v-for="(w, i) in lastWarnings" :key="i">
110
+ <code>{{ w.code }}</code>
111
+ <template v-if="w.value">
112
+ · <code>{{ w.value }}</code></template
113
+ >
114
+ — {{ w.message }}
115
+ </li>
116
+ </ul>
117
+ <template #action>
118
+ <q-btn flat dense label="Schließen" @click="lastWarnings = []" />
119
+ </template>
120
+ </q-banner>
121
+
122
+ <!-- Publish confirmation modal -->
123
+ <BundleVersionPublishDialog
124
+ v-if="detailBundle && publishOpen && publishDraft"
125
+ v-model="publishOpen"
126
+ :bundle-key="detailBundle.bundleKey"
127
+ :draft="publishDraft"
128
+ :previous="publishPrevious"
129
+ :warnings="lastWarnings"
130
+ :classify-diff="classifyDiff"
131
+ :submit="onPublishSubmit"
132
+ @submitted="onPublishSubmitted"
133
+ />
134
+ </div>
135
+ </template>
136
+
137
+ <script setup lang="ts">
138
+ import { computed, ref, watch } from 'vue';
139
+ import type {
140
+ BundleRow,
141
+ BundleVersionMutationResult,
142
+ BundleVersionRow,
143
+ CatalogEntryI18n,
144
+ CreateBundleData,
145
+ CreateBundleVersionDraftData,
146
+ DiscoverySnapshot,
147
+ FeatureCatalogEntryRow,
148
+ PlanRow,
149
+ PlanVersionRow,
150
+ QuotaCatalogEntryRow,
151
+ StrictModeWarning,
152
+ UpdateBundleData,
153
+ UpdateBundleVersionDraftData,
154
+ VersionChange,
155
+ } from '@saasicat/types';
156
+
157
+ import BundleVersionPublishDialog from '../components/BundleVersionPublishDialog.vue';
158
+ import BundleCreatePanel from '../components/bundle-editor/BundleCreatePanel.vue';
159
+ import type { FeatureMeta } from '../components/bundle-editor/BundleFeaturesEditor.vue';
160
+ import {
161
+ buildFeatureRegistry,
162
+ buildQuotaRegistry,
163
+ type QuotaMeta,
164
+ } from '../components/bundle-editor/catalog-i18n.js';
165
+ import {
166
+ bundleActiveVersionAt,
167
+ bundleAggregateStatus,
168
+ bundleVersionStatus,
169
+ bundleVersionsSorted,
170
+ type BundleAggregateStatus,
171
+ } from '../components/bundle-editor/bundle-version-status';
172
+ import BundleAccordionList from './bundles-page/BundleAccordionList.vue';
173
+ import BundleDetailPanel from './bundles-page/BundleDetailPanel.vue';
174
+ import BundlesFilterBar from './bundles-page/BundlesFilterBar.vue';
175
+ import BundlesHeader from './bundles-page/BundlesHeader.vue';
176
+ import BundlesKpis from './bundles-page/BundlesKpis.vue';
177
+ import type {
178
+ BundleEditForm,
179
+ BundlesStatusFilter,
180
+ BundlesStatusFilterOption,
181
+ } from './bundles-page/types.js';
182
+
183
+ // Platform standard page: Bundles (SPEC_V2 §6.4, §11.1 M6 Pack 2c +
184
+ // Pack 2d inline editor after plan simulation). Dumb component —
185
+ // the consumer wrapper supplies the composable results + plan-root
186
+ // list + live PlanVersion index as props.
187
+
188
+ const DEFAULT_LOCALE = 'de';
189
+
190
+ const props = withDefaults(
191
+ defineProps<{
192
+ projectKey: string;
193
+ bundles: BundleRow[];
194
+ loading: boolean;
195
+ error: Error | null;
196
+ /** Active locales from the project (incl. default). Default: only `de`. */
197
+ activeLocales?: string[];
198
+ /** Plan roots for the compat picker in the inline editor. */
199
+ plans?: PlanRow[];
200
+ /**
201
+ * Live (or latest) PlanVersion per `planKey` — source for the
202
+ * Plan↔Bundle overlap calculation. The wrapper can assemble it via
203
+ * `usePlanVersions`.
204
+ */
205
+ livePlanVersions?: Record<string, PlanVersionRow | null>;
206
+ /**
207
+ * Optional manual feature label/group mapping. Overrides the
208
+ * labels derived from `featureCatalog` (rarely needed).
209
+ */
210
+ featureRegistry?: Record<string, FeatureMeta>;
211
+ /**
212
+ * Feature catalog entries (incl. `i18n`) for label resolution in the
213
+ * chosen display language. The wrapper supplies them via `useCatalogEntries`.
214
+ */
215
+ featureCatalog?: FeatureCatalogEntryRow[];
216
+ /** Quota catalog entries (incl. `i18n`) for label/unit resolution. */
217
+ quotaCatalog?: QuotaCatalogEntryRow[];
218
+ load: () => Promise<void>;
219
+ create: (data: CreateBundleData) => Promise<BundleRow>;
220
+ update: (bundleId: string, data: UpdateBundleData) => Promise<BundleRow>;
221
+ softDelete: (bundleId: string) => Promise<void>;
222
+ loadVersions: (bundleId: string) => Promise<BundleVersionRow[]>;
223
+ createDraft: (
224
+ bundleId: string,
225
+ data: Omit<CreateBundleVersionDraftData, 'bundleId'>,
226
+ ) => Promise<BundleVersionMutationResult>;
227
+ updateDraft: (
228
+ versionId: string,
229
+ data: UpdateBundleVersionDraftData,
230
+ ) => Promise<BundleVersionMutationResult>;
231
+ publish: (
232
+ versionId: string,
233
+ opts: {
234
+ forceRegressive?: boolean;
235
+ validFrom?: string | null;
236
+ validUntil?: string | null;
237
+ },
238
+ ) => Promise<BundleVersionMutationResult>;
239
+ /** Optional: discard a draft (DELETE /catalog/bundle-versions/:id). */
240
+ discardDraft?: (versionId: string) => Promise<void>;
241
+ /**
242
+ * Optional: mapping `bundleId → BundleVersionRow[]` for correct
243
+ * KPI/status-filter display across all bundles. Without this prop
244
+ * the KPIs fall back to "0", because the lazily loaded
245
+ * `detailVersions` only cover the open bundle. The wrapper usually
246
+ * supplies it via `useBundleVersionsMap`.
247
+ */
248
+ versionsByBundle?: Record<string, BundleVersionRow[]>;
249
+ snapshot: DiscoverySnapshot | null;
250
+ classifyDiff: (
251
+ previous: BundleVersionRow,
252
+ draft: BundleVersionRow,
253
+ ) => { changes: VersionChange[]; nonRegressive: boolean };
254
+ }>(),
255
+ {
256
+ plans: () => [],
257
+ livePlanVersions: () => ({}),
258
+ featureRegistry: () => ({}),
259
+ featureCatalog: () => [],
260
+ quotaCatalog: () => [],
261
+ activeLocales: () => [DEFAULT_LOCALE],
262
+ versionsByBundle: () => ({}),
263
+ },
264
+ );
265
+
266
+ const locales = computed(() => props.activeLocales ?? [DEFAULT_LOCALE]);
267
+ const translatableLocales = computed(() => locales.value.filter((l) => l !== DEFAULT_LOCALE));
268
+
269
+ // ─── Display language for feature/quota labels (creation + detail) ───
270
+ const displayLocale = ref(DEFAULT_LOCALE);
271
+
272
+ // Catalog-derived registries in the chosen display language; a
273
+ // manually passed `featureRegistry` takes precedence (override).
274
+ const featureRegistryResolved = computed<Record<string, FeatureMeta>>(() => ({
275
+ ...buildFeatureRegistry(props.featureCatalog, displayLocale.value),
276
+ ...props.featureRegistry,
277
+ }));
278
+ const quotaRegistryResolved = computed<Record<string, QuotaMeta>>(() =>
279
+ buildQuotaRegistry(props.quotaCatalog, displayLocale.value),
280
+ );
281
+
282
+ const query = ref('');
283
+
284
+ // ─── Status filter + KPI aggregates across all bundles ───
285
+ const statusFilter = ref<BundlesStatusFilter>('all');
286
+
287
+ function versionsOf(bundleId: string): BundleVersionRow[] {
288
+ // Prefers the versionsByBundle supplied by the wrapper; falls back to
289
+ // detailVersions when the open bundle was just loaded
290
+ // (between wrapper refresh and map update).
291
+ const fromMap = props.versionsByBundle[bundleId];
292
+ if (fromMap && fromMap.length > 0) return fromMap;
293
+ if (openKey.value === bundleId) return detailVersions.value;
294
+ return fromMap ?? [];
295
+ }
296
+
297
+ function aggregateStatusOf(b: BundleRow): BundleAggregateStatus {
298
+ return bundleAggregateStatus(versionsOf(b.id), b.deletedAt);
299
+ }
300
+
301
+ const filteredBundles = computed(() => {
302
+ const q = query.value?.trim().toLowerCase() ?? '';
303
+ return props.bundles.filter((b) => {
304
+ if (q && !b.bundleKey.toLowerCase().includes(q) && !b.label.toLowerCase().includes(q)) {
305
+ return false;
306
+ }
307
+ if (statusFilter.value !== 'all' && aggregateStatusOf(b) !== statusFilter.value) {
308
+ return false;
309
+ }
310
+ return true;
311
+ });
312
+ });
313
+
314
+ function i18nLocaleCount(b: BundleRow): number {
315
+ return Object.keys(b.i18n ?? {}).length;
316
+ }
317
+ const translatedCount = computed(() => props.bundles.filter((b) => i18nLocaleCount(b) > 0).length);
318
+
319
+ const liveCount = computed(
320
+ () => props.bundles.filter((b) => aggregateStatusOf(b) === 'live').length,
321
+ );
322
+ const scheduledBundlesCount = computed(
323
+ () => props.bundles.filter((b) => aggregateStatusOf(b) === 'scheduled').length,
324
+ );
325
+ const draftBundlesCount = computed(
326
+ () => props.bundles.filter((b) => aggregateStatusOf(b) === 'draft').length,
327
+ );
328
+ const totalDraftVersions = computed(() => {
329
+ let n = 0;
330
+ for (const b of props.bundles) {
331
+ for (const v of versionsOf(b.id)) {
332
+ if (v.publishedAt === null) n += 1;
333
+ }
334
+ }
335
+ return n;
336
+ });
337
+ const totalScheduledVersions = computed(() => {
338
+ let n = 0;
339
+ for (const b of props.bundles) {
340
+ for (const v of versionsOf(b.id)) {
341
+ if (bundleVersionStatus(v) === 'scheduled') n += 1;
342
+ }
343
+ }
344
+ return n;
345
+ });
346
+
347
+ // ─── Inline creation panel (5 sections, at the top of the list) ───
348
+ const createOpen = ref(false);
349
+
350
+ /** For the bundle-key conflict check in the wizard. */
351
+ const existingBundleKeys = computed(() => props.bundles.map((b) => b.bundleKey));
352
+
353
+ const statusFilterOptions: BundlesStatusFilterOption[] = [
354
+ { label: 'Alle Status', value: 'all' as const },
355
+ { label: 'Nur Live', value: 'live' as const },
356
+ { label: 'Mit geplanter Version', value: 'scheduled' as const },
357
+ { label: 'Drafts', value: 'draft' as const },
358
+ { label: 'Abgelöste', value: 'superseded' as const },
359
+ { label: 'Retired', value: 'retired' as const },
360
+ ];
361
+
362
+ function openCreatePanel(): void {
363
+ createOpen.value = true;
364
+ }
365
+
366
+ async function onWizardCreated(bundle: BundleRow): Promise<void> {
367
+ // Creation wrote the root + v1 draft in a single atom. Close the
368
+ // panel, reload the list, then expand the freshly created bundle
369
+ // so the user can keep working directly in the inline editor.
370
+ createOpen.value = false;
371
+ await props.load();
372
+ if (openKey.value !== bundle.id) {
373
+ openKey.value = null;
374
+ const next = props.bundles.find((b) => b.id === bundle.id);
375
+ if (next) await toggle(next);
376
+ }
377
+ }
378
+
379
+ // ─── Accordion / detail ───
380
+ const openKey = ref<string | null>(null);
381
+ const detailVersions = ref<BundleVersionRow[]>([]);
382
+ const editForm = ref<BundleEditForm>({
383
+ label: '',
384
+ description: '',
385
+ icon: '',
386
+ sortOrder: 0,
387
+ });
388
+ const i18nDraft = ref<CatalogEntryI18n>({});
389
+ const editSubmitting = ref(false);
390
+
391
+ const detailBundle = computed<BundleRow | null>(
392
+ () => props.bundles.find((b) => b.id === openKey.value) ?? null,
393
+ );
394
+
395
+ async function toggle(bundle: BundleRow): Promise<void> {
396
+ if (openKey.value === bundle.id) {
397
+ openKey.value = null;
398
+ return;
399
+ }
400
+ openKey.value = bundle.id;
401
+ editForm.value = {
402
+ label: bundle.label,
403
+ description: bundle.description ?? '',
404
+ icon: bundle.icon ?? '',
405
+ sortOrder: bundle.sortOrder,
406
+ };
407
+ i18nDraft.value = JSON.parse(JSON.stringify(bundle.i18n ?? {})) as CatalogEntryI18n;
408
+ detailVersions.value = [];
409
+ inlineEditorError.value = null;
410
+ detailVersions.value = await props.loadVersions(bundle.id);
411
+ if (!selectedVersionIdByBundle.value[bundle.id]) {
412
+ const defaultVersion = defaultSelectedVersion(detailVersions.value);
413
+ if (defaultVersion) {
414
+ onSelectVersion(bundle.id, defaultVersion.id);
415
+ }
416
+ }
417
+ }
418
+
419
+ function setI18n(locale: string, field: 'label' | 'description', value: string): void {
420
+ const next: CatalogEntryI18n = { ...i18nDraft.value };
421
+ const entry = { ...(next[locale] ?? {}) };
422
+ if (value) {
423
+ entry[field] = value;
424
+ } else {
425
+ delete entry[field];
426
+ }
427
+ next[locale] = entry;
428
+ i18nDraft.value = next;
429
+ }
430
+
431
+ async function submitEdit(): Promise<void> {
432
+ if (!detailBundle.value) return;
433
+ editSubmitting.value = true;
434
+ try {
435
+ await props.update(detailBundle.value.id, {
436
+ label: editForm.value.label,
437
+ description: editForm.value.description || null,
438
+ icon: editForm.value.icon || null,
439
+ sortOrder: editForm.value.sortOrder,
440
+ i18n: i18nDraft.value,
441
+ });
442
+ } finally {
443
+ editSubmitting.value = false;
444
+ }
445
+ }
446
+
447
+ async function confirmDelete(bundle: BundleRow): Promise<void> {
448
+ const ok = window.confirm(
449
+ `Bundle '${bundle.bundleKey}' wirklich soft-deleten? Bestand bleibt durch ` +
450
+ `published BundleVersions geschützt.`,
451
+ );
452
+ if (!ok) return;
453
+ await props.softDelete(bundle.id);
454
+ if (openKey.value === bundle.id) openKey.value = null;
455
+ }
456
+
457
+ watch(
458
+ () => props.bundles,
459
+ (next) => {
460
+ if (openKey.value && !next.some((b) => b.id === openKey.value)) {
461
+ openKey.value = null;
462
+ }
463
+ },
464
+ );
465
+
466
+ // ─── Strict mode warnings ───
467
+ const lastWarnings = ref<StrictModeWarning[]>([]);
468
+
469
+ // ─── Inline editor (selected version per bundle) ───
470
+ const selectedVersionIdByBundle = ref<Record<string, string | null>>({});
471
+ const inlineEditorSaving = ref(false);
472
+ const inlineEditorError = ref<string | null>(null);
473
+
474
+ const selectedVersion = computed<BundleVersionRow | null>(() => {
475
+ if (!detailBundle.value) return null;
476
+ const id = selectedVersionIdByBundle.value[detailBundle.value.id];
477
+ if (!id) return null;
478
+ return detailVersions.value.find((v) => v.id === id) ?? null;
479
+ });
480
+
481
+ function defaultSelectedVersion(versions: BundleVersionRow[]): BundleVersionRow | null {
482
+ if (versions.length === 0) return null;
483
+ // Preferred: the draft (exactly one allowed) → then scheduled →
484
+ // then live → then latest.
485
+ const draft = versions.find((v) => v.publishedAt === null);
486
+ if (draft) return draft;
487
+ const sorted = bundleVersionsSorted(versions);
488
+ const scheduled = sorted.find(
489
+ (v) => v.validFrom && new Date(v.validFrom).getTime() > Date.now(),
490
+ );
491
+ if (scheduled) return scheduled;
492
+ const live = bundleActiveVersionAt(versions);
493
+ if (live) return live;
494
+ return sorted[sorted.length - 1];
495
+ }
496
+
497
+ function onSelectVersion(bundleId: string, versionId: string): void {
498
+ selectedVersionIdByBundle.value = {
499
+ ...selectedVersionIdByBundle.value,
500
+ [bundleId]: versionId,
501
+ };
502
+ inlineEditorError.value = null;
503
+ }
504
+
505
+ /**
506
+ * "Neue Version" button in the version strip: creates a draft (defaults
507
+ * from the last live version) and selects it in the inline editor.
508
+ * The backend blocks a second draft via a partial unique index — the strip
509
+ * disables the button accordingly, but we double-check defensively.
510
+ */
511
+ async function onAddVersion(bundleId: string): Promise<void> {
512
+ if (detailVersions.value.some((v) => v.publishedAt === null)) return;
513
+ const sortedVersions = bundleVersionsSorted(detailVersions.value);
514
+ const previous =
515
+ bundleActiveVersionAt(detailVersions.value) ??
516
+ sortedVersions[sortedVersions.length - 1] ??
517
+ null;
518
+ inlineEditorError.value = null;
519
+ inlineEditorSaving.value = true;
520
+ try {
521
+ const result = await props.createDraft(bundleId, {
522
+ features: previous ? [...previous.features] : [],
523
+ quotas: previous ? { ...previous.quotas } : {},
524
+ compatibility: previous?.compatibility ?? {},
525
+ pricingOverrides: previous?.pricingOverrides ?? [],
526
+ monthlyNet: previous?.monthlyNet ?? null,
527
+ yearlyNet: previous?.yearlyNet ?? null,
528
+ marketed: previous?.marketed ?? true,
529
+ changeNote: '',
530
+ });
531
+ lastWarnings.value = result.warnings;
532
+ detailVersions.value = await props.loadVersions(bundleId);
533
+ onSelectVersion(bundleId, result.bundleVersion.id);
534
+ } catch (err) {
535
+ inlineEditorError.value = err instanceof Error ? err.message : String(err);
536
+ } finally {
537
+ inlineEditorSaving.value = false;
538
+ }
539
+ }
540
+
541
+ async function onInlineSave(
542
+ bundleId: string,
543
+ versionId: string,
544
+ data: UpdateBundleVersionDraftData,
545
+ ): Promise<void> {
546
+ inlineEditorSaving.value = true;
547
+ inlineEditorError.value = null;
548
+ try {
549
+ const result = await props.updateDraft(versionId, data);
550
+ lastWarnings.value = result.warnings;
551
+ detailVersions.value = await props.loadVersions(bundleId);
552
+ // If the backend does not change the ID, the selection stays put.
553
+ } catch (err) {
554
+ inlineEditorError.value = err instanceof Error ? err.message : String(err);
555
+ } finally {
556
+ inlineEditorSaving.value = false;
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Discards a draft or scheduled version. Works only if the
562
+ * wrapper passes `discardDraft` through; without the prop we show a
563
+ * clear error message instead of a silent no-op.
564
+ */
565
+ async function onDiscardVersion(bundleId: string, versionId: string): Promise<void> {
566
+ if (!props.discardDraft) {
567
+ inlineEditorError.value =
568
+ 'Discard ist im Wrapper nicht verdrahtet — bitte `discardDraft`-Prop ergänzen.';
569
+ return;
570
+ }
571
+ const ok = window.confirm('Diese Draft-Version verwerfen? Der Inhalt geht verloren.');
572
+ if (!ok) return;
573
+ inlineEditorSaving.value = true;
574
+ inlineEditorError.value = null;
575
+ try {
576
+ await props.discardDraft(versionId);
577
+ // Remove from local list + re-select sensibly.
578
+ detailVersions.value = await props.loadVersions(bundleId);
579
+ const next = defaultSelectedVersion(detailVersions.value);
580
+ selectedVersionIdByBundle.value = {
581
+ ...selectedVersionIdByBundle.value,
582
+ [bundleId]: next?.id ?? null,
583
+ };
584
+ } catch (err) {
585
+ inlineEditorError.value = err instanceof Error ? err.message : String(err);
586
+ } finally {
587
+ inlineEditorSaving.value = false;
588
+ }
589
+ }
590
+
591
+ // ─── Publish confirmation modal ───
592
+ const publishOpen = ref(false);
593
+ const publishDraft = ref<BundleVersionRow | null>(null);
594
+
595
+ const publishPrevious = computed<BundleVersionRow | null>(() => {
596
+ if (!publishDraft.value) return null;
597
+ return (
598
+ detailVersions.value
599
+ .filter((v) => v.publishedAt !== null && v.supersededAt === null)
600
+ .sort((a, b) => b.version - a.version)[0] ?? null
601
+ );
602
+ });
603
+
604
+ function openPublish(version: BundleVersionRow): void {
605
+ if (version.publishedAt !== null) return;
606
+ publishDraft.value = version;
607
+ publishOpen.value = true;
608
+ }
609
+
610
+ async function onPublishSubmit(opts: {
611
+ forceRegressive: boolean;
612
+ allowZeroPrice?: boolean;
613
+ validFrom?: string | null;
614
+ validUntil?: string | null;
615
+ }): Promise<BundleVersionMutationResult> {
616
+ if (!publishDraft.value) {
617
+ throw new Error('BundlesPage: publish submit ohne Draft-Kontext');
618
+ }
619
+ return props.publish(publishDraft.value.id, opts);
620
+ }
621
+
622
+ async function onPublishSubmitted(result: BundleVersionMutationResult): Promise<void> {
623
+ lastWarnings.value = result.warnings;
624
+ publishDraft.value = null;
625
+ if (detailBundle.value) {
626
+ detailVersions.value = await props.loadVersions(detailBundle.value.id);
627
+ }
628
+ }
629
+
630
+ const classifyDiff = computed(() => props.classifyDiff);
631
+ </script>
632
+
633
+ <style>
634
+ .sa-bundles {
635
+ padding: 16px;
636
+ display: flex;
637
+ flex-direction: column;
638
+ gap: 14px;
639
+ }
640
+ .sa-bundles__head {
641
+ display: flex;
642
+ justify-content: space-between;
643
+ align-items: flex-end;
644
+ gap: 16px;
645
+ }
646
+ .sa-bundles__title {
647
+ font-size: 20px;
648
+ font-weight: 700;
649
+ margin: 0;
650
+ }
651
+ .sa-bundles__sub {
652
+ margin: 4px 0 0;
653
+ color: #64748b;
654
+ font-size: 12px;
655
+ max-width: 560px;
656
+ }
657
+ .sa-bundles__head-actions {
658
+ display: flex;
659
+ align-items: center;
660
+ gap: 12px;
661
+ }
662
+ .sa-bundles__error {
663
+ border-left: 4px solid #dc2626;
664
+ }
665
+ .sa-bundles__empty {
666
+ border-left: 4px solid #31ccec;
667
+ }
668
+ .sa-bundles__kpis {
669
+ display: grid;
670
+ /* Responsive: as many KPIs as fit (up to 4), then 3 → 2 → 1. */
671
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
672
+ gap: 10px;
673
+ }
674
+ .sa-bundles__kpi {
675
+ background: #fff;
676
+ border: 1px solid #e2e8f0;
677
+ border-radius: 10px;
678
+ padding: 12px 14px;
679
+ }
680
+ .sa-bundles__kpi-label {
681
+ font-size: 10px;
682
+ text-transform: uppercase;
683
+ letter-spacing: 0.5px;
684
+ color: #64748b;
685
+ }
686
+ .sa-bundles__kpi-value {
687
+ font-size: 26px;
688
+ font-weight: 700;
689
+ }
690
+ .sa-bundles__kpi-sub {
691
+ font-size: 11px;
692
+ color: #94a3b8;
693
+ }
694
+ .sa-bundles__filter-row {
695
+ display: flex;
696
+ align-items: center;
697
+ gap: 10px;
698
+ flex-wrap: wrap;
699
+ }
700
+ .sa-bundles__search {
701
+ max-width: 420px;
702
+ flex: 1 1 280px;
703
+ }
704
+ .sa-bundles__status-filter {
705
+ min-width: 220px;
706
+ }
707
+ .sa-bundles__list {
708
+ display: flex;
709
+ flex-direction: column;
710
+ gap: 8px;
711
+ }
712
+ .sa-bd-card {
713
+ background: #fff;
714
+ border: 1px solid #e2e8f0;
715
+ border-radius: 10px;
716
+ overflow: hidden;
717
+ }
718
+ .sa-bd-card.open {
719
+ border-color: #3f6bff;
720
+ }
721
+ .sa-bd-card__head {
722
+ display: flex;
723
+ gap: 12px;
724
+ align-items: center;
725
+ padding: 12px 14px;
726
+ cursor: pointer;
727
+ }
728
+ .sa-bd-card__mark {
729
+ width: 34px;
730
+ height: 34px;
731
+ border-radius: 8px;
732
+ background: #eff6ff;
733
+ color: #1d4ed8;
734
+ display: flex;
735
+ align-items: center;
736
+ justify-content: center;
737
+ flex-shrink: 0;
738
+ }
739
+ .sa-bd-card__titlewrap {
740
+ flex: 1;
741
+ min-width: 0;
742
+ }
743
+ .sa-bd-card__titlerow {
744
+ display: flex;
745
+ gap: 8px;
746
+ align-items: center;
747
+ }
748
+ .sa-bd-card__key {
749
+ font-weight: 700;
750
+ font-size: 12px;
751
+ font-family: var(--sa-font-mono, ui-monospace, monospace);
752
+ }
753
+ .sa-bd-card__name {
754
+ font-size: 13px;
755
+ font-weight: 600;
756
+ margin-top: 2px;
757
+ }
758
+ .sa-bd-card__desc {
759
+ font-size: 12px;
760
+ color: #64748b;
761
+ }
762
+ .sa-bd-card__chev {
763
+ transition: transform 0.15s;
764
+ color: #94a3b8;
765
+ }
766
+ .sa-bd-card__chev.open {
767
+ transform: rotate(90deg);
768
+ }
769
+ .sa-bd-card__body {
770
+ border-top: 1px solid #e2e8f0;
771
+ padding: 14px;
772
+ background: #f8fafc;
773
+ }
774
+ .sa-bd-grid {
775
+ display: grid;
776
+ grid-template-columns: minmax(280px, 360px) 1fr;
777
+ gap: 18px;
778
+ }
779
+ @media (max-width: 980px) {
780
+ .sa-bd-grid {
781
+ grid-template-columns: 1fr;
782
+ }
783
+ }
784
+ .sa-bd-col {
785
+ display: flex;
786
+ flex-direction: column;
787
+ gap: 8px;
788
+ }
789
+ .sa-bd-col--versions {
790
+ gap: 14px;
791
+ }
792
+ .sa-bd-version-actions {
793
+ display: flex;
794
+ justify-content: space-between;
795
+ gap: 12px;
796
+ margin-top: 4px;
797
+ }
798
+ .sa-bd-section-label {
799
+ font-size: 11px;
800
+ font-weight: 700;
801
+ text-transform: uppercase;
802
+ letter-spacing: 0.5px;
803
+ color: #475569;
804
+ display: flex;
805
+ justify-content: space-between;
806
+ }
807
+ .sa-bd-section-label--mt {
808
+ margin-top: 10px;
809
+ }
810
+ .sa-bd-section-count {
811
+ color: #94a3b8;
812
+ font-weight: 600;
813
+ }
814
+ .sa-bundles__form {
815
+ display: flex;
816
+ flex-direction: column;
817
+ gap: 10px;
818
+ }
819
+ .sa-bd-i18n-hint {
820
+ font-size: 11px;
821
+ color: #94a3b8;
822
+ }
823
+ .sa-bd-i18n-block {
824
+ display: flex;
825
+ flex-direction: column;
826
+ gap: 6px;
827
+ padding: 8px;
828
+ border: 1px solid #e2e8f0;
829
+ border-radius: 8px;
830
+ background: #fff;
831
+ }
832
+ .sa-bd-i18n-head {
833
+ display: flex;
834
+ gap: 8px;
835
+ align-items: center;
836
+ }
837
+ .sa-bd-i18n-code {
838
+ font-size: 10px;
839
+ font-weight: 700;
840
+ background: #f1f5f9;
841
+ padding: 2px 7px;
842
+ border-radius: 5px;
843
+ }
844
+ .sa-bd-i18n-fallback {
845
+ font-size: 10px;
846
+ color: #b45309;
847
+ }
848
+ .sa-bd-save {
849
+ margin-top: 8px;
850
+ align-self: flex-start;
851
+ }
852
+ .sa-bd-delete {
853
+ margin-top: 8px;
854
+ align-self: flex-start;
855
+ }
856
+ .sa-bd-version {
857
+ display: flex;
858
+ gap: 10px;
859
+ align-items: flex-start;
860
+ background: #fff;
861
+ border: 1px solid #e2e8f0;
862
+ border-radius: 8px;
863
+ padding: 10px;
864
+ }
865
+ .sa-bd-version__main {
866
+ flex: 1;
867
+ min-width: 0;
868
+ }
869
+ .sa-bd-version__titlerow {
870
+ display: flex;
871
+ gap: 8px;
872
+ align-items: center;
873
+ }
874
+ .sa-bd-version__sub {
875
+ font-size: 11px;
876
+ color: #64748b;
877
+ margin: 2px 0;
878
+ }
879
+ .sa-bd-version__feats {
880
+ display: flex;
881
+ flex-wrap: wrap;
882
+ gap: 4px;
883
+ }
884
+ .sa-bd-feat-chip {
885
+ font-size: 10px;
886
+ background: #f1f5f9;
887
+ padding: 2px 6px;
888
+ border-radius: 5px;
889
+ }
890
+ .sa-bd-version__act {
891
+ display: flex;
892
+ gap: 2px;
893
+ }
894
+ .sa-bd-empty-row {
895
+ padding: 20px;
896
+ text-align: center;
897
+ color: #94a3b8;
898
+ font-size: 12px;
899
+ border: 1px dashed #cbd5e1;
900
+ border-radius: 8px;
901
+ }
902
+ .sa-bundles__warnings-banner {
903
+ border-left: 4px solid #f2c037;
904
+ }
905
+ .sa-bundles__warnings-list {
906
+ margin: 8px 0 0;
907
+ padding-left: 20px;
908
+ font-size: 13px;
909
+ }
910
+ </style>