@saasicat/ui-vue 0.24.2 → 0.26.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 (90) hide show
  1. package/README.md +32 -8
  2. package/dist/{catalog-DJcfm5N2.d.cts → catalog-Dch1Ryw0.d.cts} +353 -69
  3. package/dist/{catalog-DJcfm5N2.d.ts → catalog-Dch1Ryw0.d.ts} +353 -69
  4. package/dist/{chunk-F7NRM6KI.js → chunk-BPF2BMCQ.js} +748 -24
  5. package/dist/{chunk-D74FFCCP.js → chunk-NEXTZZRQ.js} +95 -177
  6. package/dist/{chunk-TQQMKWN6.js → chunk-O3J3ITF2.js} +119 -7
  7. package/dist/client/index.cjs +758 -83
  8. package/dist/client/index.d.cts +414 -9
  9. package/dist/client/index.d.ts +414 -9
  10. package/dist/client/index.js +84 -12
  11. package/dist/index.cjs +1325 -294
  12. package/dist/index.d.cts +205 -15
  13. package/dist/index.d.ts +205 -15
  14. package/dist/index.js +566 -240
  15. package/dist/quasar/index.cjs +574 -62
  16. package/dist/quasar/index.d.cts +61 -6
  17. package/dist/quasar/index.d.ts +61 -6
  18. package/dist/quasar/index.js +83 -19
  19. package/dist/{use-sa-theme-OUBlaqgl.d.ts → resource-registry-D7Xjs-5f.d.cts} +157 -7
  20. package/dist/{use-sa-theme-B5t7oXph.d.cts → resource-registry-TaR7rtq0.d.ts} +157 -7
  21. package/package.json +4 -3
  22. package/src/client/admin-error.ts +552 -0
  23. package/src/client/admin-resource-client.ts +9 -12
  24. package/src/client/batch-column-fetcher.ts +2 -2
  25. package/src/client/boot-loader.ts +12 -1
  26. package/src/client/http/axios-http-client.ts +383 -0
  27. package/src/client/http/fetch-http-client.ts +89 -0
  28. package/src/client/http/index.ts +6 -0
  29. package/src/client/http-json.ts +82 -16
  30. package/src/client/i18n/messages/discovery.ts +0 -6
  31. package/src/client/i18n/messages/errors.ts +44 -0
  32. package/src/client/i18n/messages/marketing.ts +0 -6
  33. package/src/client/i18n/messages/promos.ts +0 -2
  34. package/src/client/i18n/messages/shell.ts +12 -0
  35. package/src/client/i18n/messages.ts +3 -0
  36. package/src/client/index.ts +6 -0
  37. package/src/client/manifest-loader.ts +65 -15
  38. package/src/client/resources/audit.resource.ts +25 -0
  39. package/src/client/resources/define-resource.ts +166 -0
  40. package/src/client/resources/index.ts +42 -0
  41. package/src/client/resources/list-resource.ts +176 -0
  42. package/src/client/resources/plans.resource.ts +169 -0
  43. package/src/client/resources/resource-request.ts +117 -0
  44. package/src/client/resources/tenants.resource.ts +27 -0
  45. package/src/client/types.ts +10 -3
  46. package/src/components/BundleVersionPublishDialog.vue +1 -1
  47. package/src/components/MarketingPromotionsTab.vue +263 -241
  48. package/src/components/ThemeSwitcher.vue +85 -0
  49. package/src/components/admin-page/AdminAccordion.vue +125 -0
  50. package/src/components/bundle-editor/BundleCreatePanel.vue +1 -1
  51. package/src/components/dialogs/PromoCodeDialogFields.vue +20 -20
  52. package/src/components/plan/PlanCycleToggle.vue +1 -1
  53. package/src/components/plan-detail/PlanVersionsPanel.vue +5 -2
  54. package/src/index.ts +5 -0
  55. package/src/pages-standard/AdminLayout.vue +57 -0
  56. package/src/pages-standard/AdminManifestErrorPage.vue +4 -3
  57. package/src/pages-standard/BundlesPage.vue +5 -35
  58. package/src/pages-standard/DiscoveryPage.vue +8 -1
  59. package/src/pages-standard/MarketingCatalogPage.vue +14 -3
  60. package/src/pages-standard/SuperAdminLoginPage.vue +23 -2
  61. package/src/pages-standard/SuperAdminSetupWizard.vue +37 -3
  62. package/src/pages-standard/bundles-page/BundleAccordionList.vue +30 -32
  63. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +80 -88
  64. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +73 -79
  65. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +44 -6
  66. package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +8 -2
  67. package/src/pages-tenant/PackageSnapshotPanel.vue +31 -2
  68. package/src/quasar/confirm.ts +83 -0
  69. package/src/quasar/create-super-admin-app.ts +84 -10
  70. package/src/quasar/dark-bridge.ts +47 -10
  71. package/src/quasar/index.ts +1 -0
  72. package/src/ui/theme/components/accordion.css +118 -0
  73. package/src/ui/theme/index.css +1 -0
  74. package/src/vue/create-admin-routes.ts +9 -1
  75. package/src/vue/platform-loaders.ts +12 -2
  76. package/src/vue/resource-registry.ts +211 -0
  77. package/src/vue/super-admin-context.ts +20 -5
  78. package/src/vue/ui-confirm.ts +76 -0
  79. package/src/vue/use-api-list.ts +50 -45
  80. package/src/vue/use-async-action.ts +170 -0
  81. package/src/vue/use-async-data.ts +83 -0
  82. package/src/vue/use-bundles.ts +34 -7
  83. package/src/vue/use-catalog-entries.ts +37 -12
  84. package/src/vue/use-discovery.ts +27 -6
  85. package/src/vue/use-marketing-projections.ts +29 -7
  86. package/src/vue/use-plans.ts +36 -8
  87. package/src/vue/use-promotions.ts +27 -7
  88. package/src/vue/use-resource-list.ts +272 -0
  89. package/src/vue/use-sa-theme.ts +34 -5
  90. package/src/vue/use-tenant-subscription-bundles.ts +22 -3
@@ -37,9 +37,10 @@
37
37
  <div class="mc-promo-today" :style="{ left: `${todayX}%` }">
38
38
  <span>{{ msg.promotionsTab.today }}</span>
39
39
  </div>
40
- <div
40
+ <button
41
41
  v-for="bar in bars"
42
42
  :key="bar.id"
43
+ type="button"
43
44
  class="mc-promo-bar"
44
45
  :class="bar.status"
45
46
  :style="{
@@ -49,10 +50,11 @@
49
50
  background: bar.color,
50
51
  }"
51
52
  :title="`${bar.label} · ${bar.from} → ${bar.to}`"
52
- @click="expandedId = expandedId === bar.id ? null : bar.id"
53
+ :aria-expanded="expandedId === bar.id"
54
+ @click="toggleExpanded(bar.id)"
53
55
  >
54
56
  <span class="mc-promo-bar-label">{{ bar.label }}</span>
55
- </div>
57
+ </button>
56
58
  </div>
57
59
  </div>
58
60
 
@@ -63,250 +65,251 @@
63
65
 
64
66
  <!-- List -->
65
67
  <div class="mc-promo-list">
66
- <template v-for="p in sortedPromotions" :key="p.id">
67
- <div
68
- class="mc-promo-row"
69
- :class="{ expanded: expandedId === p.id }"
70
- @click="expandedId = expandedId === p.id ? null : p.id"
71
- >
72
- <span class="mc-promo-color" :style="{ background: p.color }" />
73
- <div class="mc-promo-row-main">
74
- <div class="mc-promo-row-title">{{ p.internalLabel }}</div>
75
- <div class="mc-promo-row-sub">
76
- <span class="mc-promo-typechip">{{ typeChip(p) }}</span>
77
- <span v-if="p.appliesTo.length === 0" class="mc-promo-muted">
78
- {{ msg.promotionsTab.noPlans }}
79
- </span>
80
- <span v-for="k in p.appliesTo" :key="k" class="mc-promo-planchip">
81
- {{ k }}
68
+ <AdminAccordion
69
+ v-for="p in sortedPromotions"
70
+ :key="p.id"
71
+ :open="expandedId === p.id"
72
+ @update:open="toggleExpanded(p.id)"
73
+ >
74
+ <template #header>
75
+ <div class="mc-promo-row">
76
+ <span class="mc-promo-color" :style="{ background: p.color }" />
77
+ <div class="mc-promo-row-main">
78
+ <div class="mc-promo-row-title">{{ p.internalLabel }}</div>
79
+ <div class="mc-promo-row-sub">
80
+ <span class="mc-promo-typechip">{{ typeChip(p) }}</span>
81
+ <span v-if="p.appliesTo.length === 0" class="mc-promo-muted">
82
+ {{ msg.promotionsTab.noPlans }}
83
+ </span>
84
+ <span v-for="k in p.appliesTo" :key="k" class="mc-promo-planchip">
85
+ {{ k }}
86
+ </span>
87
+ </div>
88
+ </div>
89
+ <div class="mc-promo-row-when">
90
+ {{ p.validFrom }} → {{ p.validTo }}
91
+ <span class="mc-promo-cycle">
92
+ {{ cycleLabel(p.billingCycle) }}
93
+ <template v-if="p.onlyLocales">
94
+ · {{ msg.promotionsTab.onlyPrefix }}
95
+ {{ p.onlyLocales.join(', ').toUpperCase() }}
96
+ </template>
82
97
  </span>
83
98
  </div>
84
- </div>
85
- <div class="mc-promo-row-when">
86
- {{ p.validFrom }} → {{ p.validTo }}
87
- <span class="mc-promo-cycle">
88
- {{ cycleLabel(p.billingCycle) }}
89
- <template v-if="p.onlyLocales">
90
- · {{ msg.promotionsTab.onlyPrefix }}
91
- {{ p.onlyLocales.join(', ').toUpperCase() }}
92
- </template>
99
+ <span class="mc-promo-status" :class="statusOf(p)">
100
+ {{ statusLabel(statusOf(p)) }}
93
101
  </span>
94
102
  </div>
95
- <span class="mc-promo-status" :class="statusOf(p)">
96
- {{ statusLabel(statusOf(p)) }}
97
- </span>
98
- </div>
103
+ </template>
99
104
 
100
105
  <!-- Editor -->
101
- <div v-if="expandedId === p.id" class="mc-promo-editor">
102
- <div class="mc-promo-editor-grid">
103
- <div class="mc-promo-editor-col">
104
- <label class="mc-promo-label">
105
- {{ msg.promotionsTab.internalLabelLabel }}
106
- </label>
107
- <input
108
- class="mc-promo-input"
109
- :value="p.internalLabel"
110
- @change="patch(p, { internalLabel: inputVal($event) })"
111
- />
112
-
113
- <label class="mc-promo-label">{{ common.type }}</label>
114
- <div class="mc-promo-typegrid">
115
- <button
116
- v-for="t in typeOptions"
117
- :key="t.id"
118
- type="button"
119
- class="mc-promo-typeopt"
120
- :class="{ active: p.type === t.id }"
121
- @click="changeType(p, t.id)"
122
- >
123
- {{ t.label }}
124
- </button>
125
- </div>
126
-
127
- <label class="mc-promo-label">{{ msg.promotionsTab.valueLabel }}</label>
128
- <div class="mc-promo-valrow">
129
- <template v-if="p.type === 'percent' || p.type === 'amount'">
130
- <input
131
- class="mc-promo-input mc-promo-input--sm"
132
- type="number"
133
- :value="numValue(p)"
134
- @change="patch(p, { value: numInput($event) })"
135
- />
136
- <span class="mc-promo-muted">
137
- {{
138
- p.type === 'percent'
139
- ? msg.promotionsTab.percentUnit
140
- : msg.promotionsTab.amountUnit
141
- }}
142
- </span>
143
- </template>
144
- <template v-else-if="p.type === 'intro'">
145
- <span class="mc-promo-muted">
146
- {{ msg.promotionsTab.introForPrefix }}
147
- </span>
148
- <input
149
- class="mc-promo-input mc-promo-input--sm"
150
- type="number"
151
- :value="introMonths(p)"
152
- @change="patchIntro(p, 'months', numInput($event))"
153
- />
154
- <span class="mc-promo-muted">
155
- {{ msg.promotionsTab.introMonthsUnit }}
156
- </span>
157
- <input
158
- class="mc-promo-input mc-promo-input--sm"
159
- type="number"
160
- :value="introPrice(p)"
161
- @change="patchIntro(p, 'price', numInput($event))"
162
- />
163
- <span class="mc-promo-muted">
164
- {{ msg.promotionsTab.introPriceUnit }}
165
- </span>
166
- </template>
167
- <template v-else>
168
- <span class="mc-promo-muted">
169
- {{ msg.promotionsTab.freeMonthsPrefix }}
170
- </span>
171
- <input
172
- class="mc-promo-input mc-promo-input--sm"
173
- type="number"
174
- :value="numValue(p)"
175
- @change="patch(p, { value: numInput($event) })"
176
- />
177
- <span class="mc-promo-muted">
178
- {{ msg.promotionsTab.freeMonthsUnit }}
179
- </span>
180
- </template>
181
- </div>
106
+ <div class="mc-promo-editor-grid">
107
+ <div class="mc-promo-editor-col">
108
+ <label class="mc-promo-label">
109
+ {{ msg.promotionsTab.internalLabelLabel }}
110
+ </label>
111
+ <input
112
+ class="mc-promo-input"
113
+ :value="p.internalLabel"
114
+ @change="patch(p, { internalLabel: inputVal($event) })"
115
+ />
116
+
117
+ <label class="mc-promo-label">{{ common.type }}</label>
118
+ <div class="mc-promo-typegrid">
119
+ <button
120
+ v-for="t in typeOptions"
121
+ :key="t.id"
122
+ type="button"
123
+ class="mc-promo-typeopt"
124
+ :class="{ active: p.type === t.id }"
125
+ @click="changeType(p, t.id)"
126
+ >
127
+ {{ t.label }}
128
+ </button>
129
+ </div>
182
130
 
183
- <label class="mc-promo-label">
184
- {{ common.validity }}
185
- </label>
186
- <div class="mc-promo-valrow">
131
+ <label class="mc-promo-label">{{ msg.promotionsTab.valueLabel }}</label>
132
+ <div class="mc-promo-valrow">
133
+ <template v-if="p.type === 'percent' || p.type === 'amount'">
134
+ <input
135
+ class="mc-promo-input mc-promo-input--sm"
136
+ type="number"
137
+ :value="numValue(p)"
138
+ @change="patch(p, { value: numInput($event) })"
139
+ />
140
+ <span class="mc-promo-muted">
141
+ {{
142
+ p.type === 'percent'
143
+ ? msg.promotionsTab.percentUnit
144
+ : msg.promotionsTab.amountUnit
145
+ }}
146
+ </span>
147
+ </template>
148
+ <template v-else-if="p.type === 'intro'">
149
+ <span class="mc-promo-muted">
150
+ {{ msg.promotionsTab.introForPrefix }}
151
+ </span>
187
152
  <input
188
- class="mc-promo-input"
189
- type="date"
190
- :value="p.validFrom"
191
- @change="patch(p, { validFrom: inputVal($event) })"
153
+ class="mc-promo-input mc-promo-input--sm"
154
+ type="number"
155
+ :value="introMonths(p)"
156
+ @change="patchIntro(p, 'months', numInput($event))"
192
157
  />
193
- <span class="mc-promo-muted">→</span>
158
+ <span class="mc-promo-muted">
159
+ {{ msg.promotionsTab.introMonthsUnit }}
160
+ </span>
194
161
  <input
195
- class="mc-promo-input"
196
- type="date"
197
- :value="p.validTo"
198
- @change="patch(p, { validTo: inputVal($event) })"
162
+ class="mc-promo-input mc-promo-input--sm"
163
+ type="number"
164
+ :value="introPrice(p)"
165
+ @change="patchIntro(p, 'price', numInput($event))"
199
166
  />
200
- </div>
201
-
202
- <label class="mc-promo-label">
203
- {{ msg.promotionsTab.billingCycleLabel }}
204
- </label>
205
- <div class="mc-promo-typegrid">
206
- <button
207
- v-for="c in cycleOptions"
208
- :key="c.id"
209
- type="button"
210
- class="mc-promo-typeopt"
211
- :class="{ active: p.billingCycle === c.id }"
212
- @click="patch(p, { billingCycle: c.id })"
213
- >
214
- {{ c.label }}
215
- </button>
216
- </div>
167
+ <span class="mc-promo-muted">
168
+ {{ msg.promotionsTab.introPriceUnit }}
169
+ </span>
170
+ </template>
171
+ <template v-else>
172
+ <span class="mc-promo-muted">
173
+ {{ msg.promotionsTab.freeMonthsPrefix }}
174
+ </span>
175
+ <input
176
+ class="mc-promo-input mc-promo-input--sm"
177
+ type="number"
178
+ :value="numValue(p)"
179
+ @change="patch(p, { value: numInput($event) })"
180
+ />
181
+ <span class="mc-promo-muted">
182
+ {{ msg.promotionsTab.freeMonthsUnit }}
183
+ </span>
184
+ </template>
185
+ </div>
217
186
 
218
- <label class="mc-promo-label">
219
- {{ msg.promotionsTab.priorityLabel }}
220
- </label>
187
+ <label class="mc-promo-label">
188
+ {{ common.validity }}
189
+ </label>
190
+ <div class="mc-promo-valrow">
221
191
  <input
222
- class="mc-promo-input mc-promo-input--sm"
223
- type="number"
224
- :value="p.priority"
225
- @change="patch(p, { priority: numInput($event) })"
192
+ class="mc-promo-input"
193
+ type="date"
194
+ :value="p.validFrom"
195
+ @change="patch(p, { validFrom: inputVal($event) })"
196
+ />
197
+ <span class="mc-promo-muted">→</span>
198
+ <input
199
+ class="mc-promo-input"
200
+ type="date"
201
+ :value="p.validTo"
202
+ @change="patch(p, { validTo: inputVal($event) })"
226
203
  />
227
204
  </div>
228
205
 
229
- <div class="mc-promo-editor-col">
230
- <label class="mc-promo-label">
231
- {{ msg.promotionsTab.appliesToLabel }}
232
- </label>
233
- <div class="mc-promo-planlist">
234
- <button
235
- v-for="pl in plans"
236
- :key="pl.key"
237
- type="button"
238
- class="mc-promo-planopt"
239
- :class="{ active: p.appliesTo.includes(pl.key) }"
240
- @click="toggleApply(p, pl.key)"
241
- >
242
- {{ pl.label }}
243
- <code>{{ pl.key }}</code>
244
- </button>
245
- </div>
246
-
247
- <label class="mc-promo-label">
248
- {{ msg.promotionsTab.localeRestrictionLabel }}
249
- </label>
250
- <div class="mc-promo-typegrid">
251
- <button
252
- type="button"
253
- class="mc-promo-typeopt"
254
- :class="{ active: !p.onlyLocales }"
255
- @click="patch(p, { onlyLocales: null })"
256
- >
257
- {{ msg.promotionsTab.allLocales }}
258
- </button>
259
- <button
260
- v-for="l in activeLocales"
261
- :key="l"
262
- type="button"
263
- class="mc-promo-typeopt"
264
- :class="{ active: p.onlyLocales?.includes(l) }"
265
- @click="toggleLocale(p, l)"
266
- >
267
- {{ msg.promotionsTab.onlyPrefix }} {{ l.toUpperCase() }}
268
- </button>
269
- </div>
206
+ <label class="mc-promo-label">
207
+ {{ msg.promotionsTab.billingCycleLabel }}
208
+ </label>
209
+ <div class="mc-promo-typegrid">
210
+ <button
211
+ v-for="c in cycleOptions"
212
+ :key="c.id"
213
+ type="button"
214
+ class="mc-promo-typeopt"
215
+ :class="{ active: p.billingCycle === c.id }"
216
+ @click="patch(p, { billingCycle: c.id })"
217
+ >
218
+ {{ c.label }}
219
+ </button>
220
+ </div>
270
221
 
271
- <label class="mc-promo-label">
272
- {{ msg.promotionsTab.translationsLabel }}
273
- </label>
274
- <div v-for="l in activeLocales" :key="l" class="mc-promo-i18n-block">
275
- <span class="mc-promo-i18n-code">{{ l.toUpperCase() }}</span>
276
- <input
277
- class="mc-promo-input"
278
- :placeholder="msg.promotionsTab.badgePlaceholder"
279
- :value="p.i18n?.[l]?.badge || ''"
280
- @change="patchI18n(p, l, 'badge', inputVal($event))"
281
- />
282
- <input
283
- class="mc-promo-input"
284
- :placeholder="msg.promotionsTab.fineprintPlaceholder"
285
- :value="p.i18n?.[l]?.fineprint || ''"
286
- @change="patchI18n(p, l, 'fineprint', inputVal($event))"
287
- />
288
- </div>
222
+ <label class="mc-promo-label">
223
+ {{ msg.promotionsTab.priorityLabel }}
224
+ </label>
225
+ <input
226
+ class="mc-promo-input mc-promo-input--sm"
227
+ type="number"
228
+ :value="p.priority"
229
+ @change="patch(p, { priority: numInput($event) })"
230
+ />
231
+ </div>
289
232
 
290
- <label class="mc-promo-label">{{ msg.promotionsTab.colorLabel }}</label>
291
- <div class="mc-promo-colors">
292
- <button
293
- v-for="c in COLORS"
294
- :key="c"
295
- type="button"
296
- class="mc-promo-colorbtn"
297
- :class="{ active: p.color === c }"
298
- :style="{ background: c }"
299
- @click="patch(p, { color: c })"
300
- />
301
- </div>
233
+ <div class="mc-promo-editor-col">
234
+ <label class="mc-promo-label">
235
+ {{ msg.promotionsTab.appliesToLabel }}
236
+ </label>
237
+ <div class="mc-promo-planlist">
238
+ <button
239
+ v-for="pl in plans"
240
+ :key="pl.key"
241
+ type="button"
242
+ class="mc-promo-planopt"
243
+ :class="{ active: p.appliesTo.includes(pl.key) }"
244
+ @click="toggleApply(p, pl.key)"
245
+ >
246
+ {{ pl.label }}
247
+ <code>{{ pl.key }}</code>
248
+ </button>
249
+ </div>
302
250
 
303
- <button class="mc-promo-delete" type="button" @click="onRemove(p)">
304
- {{ msg.promotionsTab.delete }}
251
+ <label class="mc-promo-label">
252
+ {{ msg.promotionsTab.localeRestrictionLabel }}
253
+ </label>
254
+ <div class="mc-promo-typegrid">
255
+ <button
256
+ type="button"
257
+ class="mc-promo-typeopt"
258
+ :class="{ active: !p.onlyLocales }"
259
+ @click="patch(p, { onlyLocales: null })"
260
+ >
261
+ {{ msg.promotionsTab.allLocales }}
262
+ </button>
263
+ <button
264
+ v-for="l in activeLocales"
265
+ :key="l"
266
+ type="button"
267
+ class="mc-promo-typeopt"
268
+ :class="{ active: p.onlyLocales?.includes(l) }"
269
+ @click="toggleLocale(p, l)"
270
+ >
271
+ {{ msg.promotionsTab.onlyPrefix }} {{ l.toUpperCase() }}
305
272
  </button>
306
273
  </div>
274
+
275
+ <label class="mc-promo-label">
276
+ {{ msg.promotionsTab.translationsLabel }}
277
+ </label>
278
+ <div v-for="l in activeLocales" :key="l" class="mc-promo-i18n-block">
279
+ <span class="mc-promo-i18n-code">{{ l.toUpperCase() }}</span>
280
+ <input
281
+ class="mc-promo-input"
282
+ :placeholder="msg.promotionsTab.badgePlaceholder"
283
+ :value="p.i18n?.[l]?.badge || ''"
284
+ @change="patchI18n(p, l, 'badge', inputVal($event))"
285
+ />
286
+ <input
287
+ class="mc-promo-input"
288
+ :placeholder="msg.promotionsTab.fineprintPlaceholder"
289
+ :value="p.i18n?.[l]?.fineprint || ''"
290
+ @change="patchI18n(p, l, 'fineprint', inputVal($event))"
291
+ />
292
+ </div>
293
+
294
+ <label class="mc-promo-label">{{ msg.promotionsTab.colorLabel }}</label>
295
+ <div class="mc-promo-colors">
296
+ <button
297
+ v-for="c in COLORS"
298
+ :key="c"
299
+ type="button"
300
+ class="mc-promo-colorbtn"
301
+ :class="{ active: p.color === c }"
302
+ :style="{ background: c }"
303
+ @click="patch(p, { color: c })"
304
+ />
305
+ </div>
306
+
307
+ <button class="mc-promo-delete" type="button" @click="onRemove(p)">
308
+ {{ msg.promotionsTab.delete }}
309
+ </button>
307
310
  </div>
308
311
  </div>
309
- </template>
312
+ </AdminAccordion>
310
313
  </div>
311
314
  </div>
312
315
  </template>
@@ -322,6 +325,7 @@ import {
322
325
  type PromotionType,
323
326
  type UpdatePromotionData,
324
327
  } from '@saasicat/types';
328
+ import AdminAccordion from './admin-page/AdminAccordion.vue';
325
329
  import { formatMessage } from '../client/i18n/format.js';
326
330
  import { useSaMessages, useSuperAdminI18n } from '../vue/use-super-admin-i18n.js';
327
331
 
@@ -350,6 +354,30 @@ const { intlLocale } = useSuperAdminI18n();
350
354
  const busy = computed(() => props.busy ?? false);
351
355
  const expandedId = ref<string | null>(null);
352
356
 
357
+ // sa-disclosure-exempt(toggles `expandedId`, writes `aria-expanded`):
358
+ // the timeline bar is a second trigger for the accordion below it
359
+ //
360
+ // The disclosure itself IS an `AdminAccordion` — one per promotion, in the list
361
+ // below, with the `<button>` and the `aria-expanded` that come with it. What
362
+ // this file adds is a second way into the same row: the bar that charts it on
363
+ // the timeline. A Gantt segment positioned by date cannot be an accordion
364
+ // header, so there is nothing here to migrate onto one.
365
+ //
366
+ // The rule sees the click write `expandedId` and the accordion's `:open` read
367
+ // it, and from the source it cannot tell a second trigger from a hand-rolled
368
+ // disclosure. What its story is actually about — "four of the eight were a
369
+ // `<div>` with a click handler" — is fixed rather than exempted here: the bar
370
+ // is a `<button>` that reports whether the row it charts is open. It carries no
371
+ // `aria-controls`, because the body's id is generated inside `AdminAccordion`
372
+ // and never leaves it; the row's own header owns that relationship.
373
+
374
+ // One expression for "open this one, close whatever was open". The row and the
375
+ // timeline bar above it both open the same editor, and they used to say so in
376
+ // two copies of the same ternary.
377
+ function toggleExpanded(id: string): void {
378
+ expandedId.value = expandedId.value === id ? null : id;
379
+ }
380
+
353
381
  const typeOptions = computed<Array<{ id: PromotionType; label: string }>>(() => [
354
382
  { id: 'percent', label: msg.value.promotionsTab.typePercent },
355
383
  { id: 'amount', label: msg.value.promotionsTab.typeAmount },
@@ -661,15 +689,21 @@ async function onRemove(p: PromotionRow): Promise<void> {
661
689
  color: var(--sa-color-negative-strong);
662
690
  font-weight: 700;
663
691
  }
692
+ /* A `<button>` wearing a bar: the browser's own button chrome would draw a
693
+ * border and a background over the promotion colour the row is identified by. */
664
694
  .mc-promo-bar {
665
695
  position: absolute;
666
696
  height: 20px;
697
+ border: 0;
698
+ padding: 0;
667
699
  border-radius: 5px;
668
700
  cursor: pointer;
669
701
  display: flex;
670
702
  align-items: center;
671
703
  overflow: hidden;
672
704
  opacity: 0.92;
705
+ font: inherit;
706
+ text-align: left;
673
707
  }
674
708
  .mc-promo-bar.expired {
675
709
  opacity: 0.45;
@@ -694,20 +728,15 @@ async function onRemove(p: PromotionRow): Promise<void> {
694
728
  flex-direction: column;
695
729
  gap: 6px;
696
730
  }
731
+ /* Only what a PROMOTION puts in the header. The surface, border, radius, the
732
+ * padding, the open border and the hover feedback are `AdminAccordion`'s — this
733
+ * row used to state its own, and the editor below it stated a second, matching
734
+ * set so the two would read as one block. They were separated by the list's 6px
735
+ * gap all along. */
697
736
  .mc-promo-row {
698
737
  display: flex;
699
738
  align-items: center;
700
739
  gap: 12px;
701
- background: var(--sa-color-bg-surface);
702
- border: 1px solid var(--sa-color-border);
703
- border-radius: 8px;
704
- padding: 10px 12px;
705
- cursor: pointer;
706
- }
707
- .mc-promo-row.expanded {
708
- border-color: var(--sa-color-accent);
709
- border-bottom-left-radius: 0;
710
- border-bottom-right-radius: 0;
711
740
  }
712
741
  .mc-promo-color {
713
742
  width: 10px;
@@ -777,13 +806,6 @@ async function onRemove(p: PromotionRow): Promise<void> {
777
806
  background: var(--sa-color-border-soft);
778
807
  color: var(--sa-color-fg-subtle);
779
808
  }
780
- .mc-promo-editor {
781
- border: 1px solid var(--sa-color-accent);
782
- border-top: 0;
783
- border-radius: 0 0 8px 8px;
784
- background: var(--sa-color-bg-sunken);
785
- padding: 14px;
786
- }
787
809
  .mc-promo-editor-grid {
788
810
  display: grid;
789
811
  grid-template-columns: 1fr 1fr;