@saasicat/ui-vue 1.0.0-rc.2 → 1.0.0-rc.21

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 (309) hide show
  1. package/README.md +84 -19
  2. package/dist/.build-stamp +1 -1
  3. package/dist/assets/material-icons/material-icons.css +31 -0
  4. package/dist/assets/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.woff +0 -0
  5. package/dist/assets/material-icons/web-font/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.woff2 +0 -0
  6. package/dist/assets/quasar.css +11696 -0
  7. package/dist/assets/ui-vue.css +6923 -0
  8. package/dist/auth/SuperAdminLoginPage.js +213 -0
  9. package/dist/auth/SuperAdminSetupWizard.js +5 -0
  10. package/dist/{batch-column-fetcher-VwjOxerL.d.ts → batch-column-fetcher-DQ89o95K.d.cts} +5 -5
  11. package/dist/{batch-column-fetcher-Dk9X92db.d.cts → batch-column-fetcher-Sr48UWEX.d.ts} +5 -5
  12. package/dist/{catalog-C5VKGbxk.d.cts → catalog-0eZwciPW.d.cts} +125 -86
  13. package/dist/{catalog-C5VKGbxk.d.ts → catalog-0eZwciPW.d.ts} +125 -86
  14. package/dist/{chunk-L7HPDVHV.js → chunk-47HDYN35.js} +86 -7
  15. package/dist/{chunk-A7JICGQ5.js → chunk-7CHT2ATN.js} +16 -3
  16. package/dist/{chunk-P4EJF43F.js → chunk-CZIVA536.js} +358 -300
  17. package/dist/{chunk-6X6AGH66.js → chunk-RFJ4UGOE.js} +7 -4
  18. package/dist/chunk-U7LNAX6T.js +35 -0
  19. package/dist/{chunk-4CJBABLZ.js → chunk-VLGEOPZH.js} +685 -196
  20. package/dist/chunks/AdminAccordion.vue_vue_type_script_setup_true_lang-DOwlTV5_.js +175 -0
  21. package/dist/chunks/AdminBanner.vue_vue_type_script_setup_true_lang-D27JEaak.js +83 -0
  22. package/dist/chunks/AdminBody.vue_vue_type_script_setup_true_lang-DxcVRzXE.js +41 -0
  23. package/dist/chunks/AdminDialog.vue_vue_type_script_setup_true_lang-CpxfSrx-.js +138 -0
  24. package/dist/chunks/AdminEmptyState.vue_vue_type_script_setup_true_lang-jaYkgNbE.js +48 -0
  25. package/dist/chunks/AdminErrorBanner.vue_vue_type_script_setup_true_lang-t-5uqt82.js +50 -0
  26. package/dist/chunks/AdminField.vue_vue_type_script_setup_true_lang-CtPByt2w.js +63 -0
  27. package/dist/chunks/AdminFieldGrid.vue_vue_type_script_setup_true_lang-BxuyHtBU.js +21 -0
  28. package/dist/chunks/AdminFilters.vue_vue_type_script_setup_true_lang-DSnzAlwj.js +18 -0
  29. package/dist/chunks/AdminFormDialog.vue_vue_type_script_setup_true_lang-BpWshaWv.js +84 -0
  30. package/dist/chunks/AdminHero.vue_vue_type_script_setup_true_lang-CoaRxXLF.js +46 -0
  31. package/dist/chunks/AdminKpi.vue_vue_type_script_setup_true_lang-CgCcGGmY.js +79 -0
  32. package/dist/chunks/AdminPage.vue_vue_type_script_setup_true_lang-L7St_zOY.js +15 -0
  33. package/dist/chunks/AdminPaginator.vue_vue_type_script_setup_true_lang-Cv8EEBGP.js +157 -0
  34. package/dist/chunks/AdminRefreshBtn.vue_vue_type_script_setup_true_lang-Bxk5GWOB.js +41 -0
  35. package/dist/chunks/AdminSection.vue_vue_type_script_setup_true_lang-DpjApOU-.js +62 -0
  36. package/dist/chunks/AdminStatistics.vue_vue_type_script_setup_true_lang-DwHeeYCX.js +32 -0
  37. package/dist/chunks/AdminStatusPill.vue_vue_type_script_setup_true_lang-09G3rCxN.js +32 -0
  38. package/dist/chunks/AdminTable.vue_vue_type_script_setup_true_lang-BzAusc-r.js +2007 -0
  39. package/dist/chunks/ClosePopup-iyFHWknU.js +48 -0
  40. package/dist/chunks/PromoCodeEditDialog.vue_vue_type_script_setup_true_lang-NiW-Ky7H.js +639 -0
  41. package/dist/chunks/QBadge-B8LYhEVv.js +43 -0
  42. package/dist/chunks/QBtn-CSVcAwrE.js +770 -0
  43. package/dist/chunks/QCard-g0Hwqx6i.js +30 -0
  44. package/dist/chunks/QCardSection-FvPMZdQ4.js +22 -0
  45. package/dist/chunks/QCheckbox-BwxH7EyE.js +63 -0
  46. package/dist/chunks/QDialog-CcwVEueo.js +331 -0
  47. package/dist/chunks/QIcon-DaxYRLz0.js +195 -0
  48. package/dist/chunks/QInput-SRRCebo8.js +1663 -0
  49. package/dist/chunks/QItem-DunhGoPO.js +127 -0
  50. package/dist/chunks/QList-D3jMUITp.js +37 -0
  51. package/dist/chunks/QMenu-BFP-qtEm.js +831 -0
  52. package/dist/chunks/QSelect-jOvICNCy.js +1885 -0
  53. package/dist/chunks/QSeparator-CrLemO5z.js +60 -0
  54. package/dist/chunks/QSpinner-DblEBRSQ.js +57 -0
  55. package/dist/chunks/QToggle-BBogOutP.js +40 -0
  56. package/dist/chunks/QTooltip-Ch_r2hMZ.js +320 -0
  57. package/dist/chunks/SuperAdminSetupWizard-UO4j4Eyu.js +465 -0
  58. package/dist/chunks/TenantActionConfirmDialog-DU11RlmB.js +201 -0
  59. package/dist/chunks/_plugin-vue_export-helper-1tPrXgE0.js +10 -0
  60. package/dist/chunks/catalog-i18n-tKlkm6-2.js +32 -0
  61. package/dist/chunks/create-Ck-e8kVF.js +42 -0
  62. package/dist/chunks/detached-fullscreen-DuM-R9uv.js +488 -0
  63. package/dist/chunks/focus-manager-Cc4uTjsQ.js +301 -0
  64. package/dist/chunks/format-BIQKj_qk.js +23 -0
  65. package/dist/chunks/is-DNOHJAOK.js +269 -0
  66. package/dist/chunks/plan-area-context-RiY4rHni.js +20 -0
  67. package/dist/chunks/private.use-form-BdJSI6of.js +30 -0
  68. package/dist/chunks/render-DSsuXum_.js +32 -0
  69. package/dist/chunks/symbols-Cy0ehVpr.js +67 -0
  70. package/dist/chunks/touch-B8tpYG34.js +162 -0
  71. package/dist/chunks/use-checkbox-CBtmpkdc.js +221 -0
  72. package/dist/chunks/use-dark-VmDF1SYZ.js +14 -0
  73. package/dist/chunks/use-hydration-PO-eh3w4.js +14 -0
  74. package/dist/chunks/use-prevent-scroll-B9A_pIzH.js +156 -0
  75. package/dist/chunks/use-render-cache-Bj5SbY5v.js +22 -0
  76. package/dist/chunks/use-size-C7KmPm4L.js +23 -0
  77. package/dist/chunks/vm-Dl-TBfKC.js +182 -0
  78. package/dist/client/index.cjs +488 -306
  79. package/dist/client/index.d.cts +107 -7
  80. package/dist/client/index.d.ts +107 -7
  81. package/dist/client/index.js +25 -3
  82. package/dist/index.cjs +1226 -536
  83. package/dist/index.d.cts +5 -5
  84. package/dist/index.d.ts +5 -5
  85. package/dist/index.js +53 -7
  86. package/dist/layouts/AdminLayout.js +1711 -0
  87. package/dist/pages/AdminManifestErrorPage.js +99 -0
  88. package/dist/pages/AuditPage.js +244 -0
  89. package/dist/pages/BundlesPage.js +2883 -0
  90. package/dist/pages/DashboardPage.js +299 -0
  91. package/dist/pages/DiscoveryPage.js +2563 -0
  92. package/dist/pages/EmailHistoryPage.js +509 -0
  93. package/dist/pages/MarketingCatalogPage.js +2095 -0
  94. package/dist/pages/PilotsPage.js +1197 -0
  95. package/dist/pages/PlanReviewPage.js +563 -0
  96. package/dist/pages/PlanVersionEditorPage.js +1626 -0
  97. package/dist/pages/PlansPage.js +6164 -0
  98. package/dist/pages/PlatformEmailPage.js +455 -0
  99. package/dist/pages/PromoCodeDetailPage.js +240 -0
  100. package/dist/pages/PromoCodesPage.js +567 -0
  101. package/dist/pages/SettingsPage.js +216 -0
  102. package/dist/pages/SubscriptionsPage.js +113 -0
  103. package/dist/pages/TenantDetailPage.js +393 -0
  104. package/dist/pages/TenantsPage.js +430 -0
  105. package/dist/pages/UsersPage.js +386 -0
  106. package/dist/pages/index.js +75 -0
  107. package/dist/quasar/index.cjs +388 -297
  108. package/dist/quasar/index.d.cts +13 -3
  109. package/dist/quasar/index.d.ts +13 -3
  110. package/dist/quasar/index.js +10 -6
  111. package/dist/{resource-registry-CrHUvVx6.d.cts → resource-registry-GAxu2kn6.d.ts} +32 -18
  112. package/dist/{resource-registry-Cw_c0F11.d.ts → resource-registry-zfCLvEgx.d.cts} +32 -18
  113. package/dist/testing/admin-pages-suite.cjs +4 -1
  114. package/dist/testing/admin-pages-suite.d.cts +10 -1
  115. package/dist/testing/admin-pages-suite.d.ts +10 -1
  116. package/dist/testing/admin-pages-suite.js +3 -1
  117. package/dist/testing/mount-with-quasar.cjs +65 -0
  118. package/dist/testing/mount-with-quasar.d.cts +7 -0
  119. package/dist/testing/mount-with-quasar.d.ts +7 -0
  120. package/dist/testing/mount-with-quasar.js +63 -0
  121. package/dist/ui/data/AdminKpi.js +4 -0
  122. package/dist/ui/data/AdminPaginator.js +4 -0
  123. package/dist/ui/data/AdminRowActions.js +37 -0
  124. package/dist/ui/data/AdminStatistics.js +4 -0
  125. package/dist/ui/data/AdminStatusPill.js +4 -0
  126. package/dist/ui/data/AdminTable.js +4 -0
  127. package/dist/ui/data/KvBlock.js +24 -0
  128. package/dist/ui/entitlement/FeatureGate.js +25 -0
  129. package/dist/ui/feedback/AdminBanner.js +4 -0
  130. package/dist/ui/feedback/AdminEmptyState.js +4 -0
  131. package/dist/ui/feedback/AdminErrorBanner.js +4 -0
  132. package/dist/ui/feedback/AdminRefreshBtn.js +4 -0
  133. package/dist/ui/overlay/AdminConfirmDialog.js +112 -0
  134. package/dist/ui/overlay/AdminDialog.js +4 -0
  135. package/dist/ui/overlay/AdminFormDialog.js +4 -0
  136. package/dist/ui/overlay/MfaPromptDialog.js +131 -0
  137. package/dist/ui/page/AdminAccordion.js +4 -0
  138. package/dist/ui/page/AdminBody.js +4 -0
  139. package/dist/ui/page/AdminField.js +4 -0
  140. package/dist/ui/page/AdminFieldGrid.js +4 -0
  141. package/dist/ui/page/AdminFilters.js +4 -0
  142. package/dist/ui/page/AdminHero.js +4 -0
  143. package/dist/ui/page/AdminPage.js +4 -0
  144. package/dist/ui/page/AdminSection.js +4 -0
  145. package/dist/ui/page/AdminToolbar.js +35 -0
  146. package/dist/ui/page/LocaleSwitcher.js +90 -0
  147. package/dist/ui/page/ThemeSwitcher.js +95 -0
  148. package/dist/ui/page/WizardStepper.js +55 -0
  149. package/dist/vue/index.cjs +1135 -524
  150. package/dist/vue/index.d.cts +520 -44
  151. package/dist/vue/index.d.ts +520 -44
  152. package/dist/vue/index.js +30 -6
  153. package/package.json +30 -15
  154. package/src/auth/SuperAdminLoginPage.vue +16 -16
  155. package/src/auth/SuperAdminSetupWizard.vue +33 -28
  156. package/src/client/admin-resource-client.ts +23 -8
  157. package/src/client/billing-area.ts +14 -0
  158. package/src/client/brand-bridge.ts +98 -0
  159. package/src/client/http/axios-http-client.ts +4 -3
  160. package/src/client/http-json.ts +16 -3
  161. package/src/client/i18n/messages/audit.ts +1 -0
  162. package/src/client/i18n/messages/bundles.ts +17 -4
  163. package/src/client/i18n/messages/common.ts +1 -0
  164. package/src/client/i18n/messages/dashboard.ts +1 -0
  165. package/src/client/i18n/messages/discovery.ts +10 -19
  166. package/src/client/i18n/messages/email.ts +1 -0
  167. package/src/client/i18n/messages/errors.ts +1 -0
  168. package/src/client/i18n/messages/marketing.ts +7 -2
  169. package/src/client/i18n/messages/nav.ts +3 -0
  170. package/src/client/i18n/messages/pilots.ts +1 -0
  171. package/src/client/i18n/messages/plan-detail.ts +1 -0
  172. package/src/client/i18n/messages/plan-editor.ts +1 -0
  173. package/src/client/i18n/messages/plan-versions.ts +3 -4
  174. package/src/client/i18n/messages/plans.ts +9 -4
  175. package/src/client/i18n/messages/promos.ts +3 -0
  176. package/src/client/i18n/messages/settings.ts +75 -0
  177. package/src/client/i18n/messages/shell.ts +1 -0
  178. package/src/client/i18n/messages/tenants.ts +1 -0
  179. package/src/client/i18n/messages/users.ts +1 -0
  180. package/src/client/i18n/messages.ts +3 -0
  181. package/src/client/index.ts +10 -0
  182. package/src/client/mfa-header.ts +14 -0
  183. package/src/client/nav-builder.ts +7 -4
  184. package/src/client/reorder-priorities.ts +76 -0
  185. package/src/client/resources/bundles.resource.ts +24 -38
  186. package/src/client/resources/catalog.resource.ts +91 -113
  187. package/src/client/resources/define-resource.ts +2 -75
  188. package/src/client/resources/index.ts +3 -0
  189. package/src/client/resources/marketing.resource.ts +77 -80
  190. package/src/client/resources/pilots.resource.ts +1 -11
  191. package/src/client/resources/plans.resource.ts +47 -64
  192. package/src/client/resources/platform-email.resource.ts +1 -11
  193. package/src/client/resources/promotions.resource.ts +18 -25
  194. package/src/client/resources/settings.resource.ts +68 -0
  195. package/src/client/resources/tenants.resource.ts +10 -4
  196. package/src/client/resources/users.resource.ts +3 -2
  197. package/src/client/settings-view.ts +50 -0
  198. package/src/client/version-change-fields.ts +17 -0
  199. package/src/features/bundle/BundleVersionInlineEditor.vue +112 -155
  200. package/src/features/bundle/BundleVersionPublishDialog.vue +34 -16
  201. package/src/features/bundle/BundleVersionStrip.vue +25 -31
  202. package/src/features/bundle/VersionDiffPreview.vue +21 -15
  203. package/src/features/bundle/internal/BundleCreatePanel.vue +65 -119
  204. package/src/features/bundle/internal/BundleFeaturesEditor.vue +18 -31
  205. package/src/features/bundle/internal/BundlePlanCompatPicker.vue +23 -29
  206. package/src/features/bundle/internal/BundleQuotasEditor.vue +20 -41
  207. package/src/features/bundle/internal/BundleStatusBanner.vue +16 -70
  208. package/src/features/marketing/MarketingPromotionsTab.vue +155 -122
  209. package/src/features/plan/PlanCreateDialog.vue +49 -129
  210. package/src/features/plan/PlanDetail.vue +57 -76
  211. package/src/features/plan/PlanList.vue +81 -156
  212. package/src/features/plan/PlanMatrix.vue +89 -158
  213. package/src/features/plan/PlanReview.vue +62 -80
  214. package/src/features/plan/PlanVersionEditor.vue +134 -238
  215. package/src/features/plan/internal/PlanCatalogPreview.vue +22 -68
  216. package/src/features/plan/internal/PlanComponentPool.vue +18 -88
  217. package/src/features/plan/internal/PlanTitleEdit.vue +12 -9
  218. package/src/features/plan/internal/PlanVersionBasket.vue +65 -89
  219. package/src/features/plan/internal/PlanVersionEditorHeader.vue +16 -20
  220. package/src/features/plan/internal/PlanVersionsPanel.vue +29 -75
  221. package/src/features/tenant/TenantActionConfirmDialog.vue +4 -4
  222. package/src/index.ts +9 -0
  223. package/src/internal/bundles-page/BundleAccordionList.vue +32 -2
  224. package/src/internal/bundles-page/BundleDetailPanel.vue +37 -21
  225. package/src/internal/bundles-page/BundlesToolbar.vue +9 -5
  226. package/src/internal/dialogs/PilotCreateDialog.vue +65 -104
  227. package/src/internal/dialogs/PilotEditDialog.vue +22 -23
  228. package/src/internal/dialogs/PromoCodeCreateDialog.vue +27 -23
  229. package/src/internal/dialogs/PromoCodeDialogFields.vue +203 -252
  230. package/src/internal/dialogs/PromoCodeEditDialog.vue +52 -48
  231. package/src/internal/dialogs/pilot-dialog.css +16 -111
  232. package/src/internal/discovery-page/CatalogEntryTransPanel.vue +18 -18
  233. package/src/internal/discovery-page/DiscoveryCapList.vue +20 -20
  234. package/src/internal/discovery-page/DiscoveryFeatureCard.vue +20 -20
  235. package/src/internal/discovery-page/DiscoveryQuotaCard.vue +10 -10
  236. package/src/internal/discovery-page/DiscoveryStatusControl.vue +1 -1
  237. package/src/internal/marketing-catalog-page/MarketingCatalogAdmin.vue +660 -0
  238. package/src/internal/{marketing-catalog → marketing-catalog-page}/MarketingCatalogHeader.vue +21 -16
  239. package/src/internal/{marketing-catalog → marketing-catalog-page}/MarketingCatalogPreview.vue +26 -13
  240. package/src/internal/{marketing-catalog → marketing-catalog-page}/MarketingCatalogToolbar.vue +14 -10
  241. package/src/internal/plan-versions/PlanDiffCard.vue +15 -15
  242. package/src/internal/plan-versions/PlanVersionsDiff.vue +11 -11
  243. package/src/internal/plan-versions/PlanVersionsTimeline.vue +35 -31
  244. package/src/internal/plans-page/PlanBundleOverview.vue +8 -8
  245. package/src/internal/plans-page/PlanPublishDialog.vue +2 -2
  246. package/src/internal/tenant-detail/TenantUsage.vue +3 -3
  247. package/src/internal/tenant-detail/types.ts +2 -2
  248. package/src/layouts/AdminLayout.vue +22 -22
  249. package/src/pages/AdminManifestErrorPage.vue +7 -7
  250. package/src/pages/AuditPage.vue +2 -2
  251. package/src/pages/BundlesPage.vue +81 -89
  252. package/src/pages/DashboardPage.vue +106 -97
  253. package/src/pages/DiscoveryPage.vue +22 -28
  254. package/src/pages/EmailHistoryPage.vue +26 -34
  255. package/src/pages/MarketingCatalogPage.vue +289 -281
  256. package/src/pages/PilotsPage.vue +22 -35
  257. package/src/pages/PlanVersionEditorPage.vue +1 -1
  258. package/src/pages/PlansPage.vue +181 -107
  259. package/src/pages/PlatformEmailPage.vue +33 -41
  260. package/src/pages/PromoCodeDetailPage.vue +12 -18
  261. package/src/pages/PromoCodesPage.vue +36 -14
  262. package/src/pages/SettingsPage.vue +226 -0
  263. package/src/pages/TenantDetailPage.vue +28 -53
  264. package/src/pages/TenantsPage.vue +7 -7
  265. package/src/pages/UsersPage.vue +7 -20
  266. package/src/pages/index.ts +3 -1
  267. package/src/quasar/create-super-admin-app.ts +13 -2
  268. package/src/quasar/dark-bridge.ts +8 -4
  269. package/src/quasar/index.ts +11 -0
  270. package/src/testing/admin-pages-suite.ts +11 -1
  271. package/src/testing/mount-with-quasar.ts +73 -0
  272. package/src/ui/data/AdminPaginator.vue +30 -20
  273. package/src/ui/data/KvBlock.vue +4 -4
  274. package/src/ui/entitlement/FeatureGate.vue +2 -2
  275. package/src/ui/feedback/AdminRefreshBtn.vue +10 -7
  276. package/src/ui/overlay/MfaPromptDialog.vue +5 -6
  277. package/src/ui/page/AdminAccordion.vue +27 -10
  278. package/src/ui/page/WizardStepper.vue +7 -18
  279. package/src/ui/theme/base.css +31 -0
  280. package/src/ui/theme/components/accordion.css +9 -9
  281. package/src/ui/theme/components/button.css +10 -68
  282. package/src/ui/theme/components/card.css +0 -8
  283. package/src/ui/theme/components/paginator.css +14 -28
  284. package/src/ui/theme/tokens.semantic.dark.css +13 -0
  285. package/src/ui/theme/tokens.semantic.light.css +39 -0
  286. package/src/vue/index.ts +9 -0
  287. package/src/vue/platform-loaders.ts +1 -3
  288. package/src/vue/resource-registry.ts +0 -8
  289. package/src/vue/super-admin-context.ts +14 -15
  290. package/src/vue/use-applied-settings.ts +79 -0
  291. package/src/vue/use-bulk-publish.ts +2 -5
  292. package/src/vue/use-bundles.ts +6 -10
  293. package/src/vue/use-catalog-entries.ts +9 -16
  294. package/src/vue/use-dialog.ts +221 -0
  295. package/src/vue/use-marketing-projections.ts +5 -6
  296. package/src/vue/use-mfa-prompt.ts +61 -11
  297. package/src/vue/use-plans.ts +28 -21
  298. package/src/vue/use-promotions.ts +1 -7
  299. package/src/vue/use-row-reorder.ts +109 -0
  300. package/src/vue/use-sa-theme.ts +31 -0
  301. package/src/vue/use-steps.ts +109 -0
  302. package/src/vue/use-subscription-draft.ts +63 -60
  303. package/src/vue/use-tenant-billing-details.ts +119 -0
  304. package/src/vue/use-tenant-billing.ts +250 -33
  305. package/src/vue/use-tenant-payment-method.ts +112 -0
  306. package/src/vue/use-tenant-subscription-bundles.ts +8 -0
  307. package/src/internal/marketing-catalog/MarketingCatalogAdmin.vue +0 -553
  308. package/src/internal/plans-page/PlansPageToast.vue +0 -69
  309. /package/src/internal/{marketing-catalog → marketing-catalog-page}/types.ts +0 -0
@@ -12,29 +12,37 @@
12
12
  <!-- Section: Code & Discount -->
13
13
  <div class="pc-section">
14
14
  <div class="pc-section__title">{{ msg.form.sectionCodeDiscount }}</div>
15
- <div class="pc-grid pc-grid--2">
16
- <div class="pc-field">
17
- <div class="pc-field__label">{{ msg.form.codeLabel }}</div>
18
- <div v-if="mode === 'create'" class="pc-code-input">
19
- <input
20
- :value="code"
21
- class="pc-input pc-input--code"
22
- :placeholder="msg.form.codePlaceholder"
23
- @input="onCodeInput"
24
- />
25
- <button type="button" class="pc-btn-mini" @click="onRandomCode">
26
- {{ msg.form.codeRandom }}
27
- </button>
28
- </div>
29
- <input v-else :value="code" class="pc-input pc-input--code" disabled />
30
- <div class="pc-field__hint">
31
- {{ mode === 'create' ? msg.form.codeHint : msg.form.codeStableHint }}
32
- </div>
33
- </div>
34
-
35
- <div class="pc-field">
36
- <div class="pc-field__label">{{ msg.form.valueTypeLabel }}</div>
15
+ <AdminFieldGrid>
16
+ <AdminField
17
+ :label="msg.form.codeLabel"
18
+ :hint="mode === 'create' ? msg.form.codeHint : msg.form.codeStableHint"
19
+ >
20
+ <q-input
21
+ :model-value="code"
22
+ outlined
23
+ dense
24
+ class="pc-code"
25
+ :disable="mode !== 'create'"
26
+ :placeholder="mode === 'create' ? msg.form.codePlaceholder : undefined"
27
+ @update:model-value="onCodeInput"
28
+ >
29
+ <template v-if="mode === 'create'" #after>
30
+ <q-btn
31
+ flat
32
+ dense
33
+ no-caps
34
+ :label="msg.form.codeRandom"
35
+ @click="onRandomCode"
36
+ />
37
+ </template>
38
+ </q-input>
39
+ </AdminField>
40
+
41
+ <AdminField :label="msg.form.valueTypeLabel">
37
42
  <div class="pc-type-grid">
43
+ <!-- @optionSurface
44
+ A segmented option with a label and a sub-label. The two lines are what
45
+ makes the choice legible, and `q-btn` renders one. -->
38
46
  <button
39
47
  v-for="o in typeOptions"
40
48
  :key="o.k"
@@ -47,34 +55,40 @@
47
55
  <div class="pc-type-opt__sub">{{ o.sub }}</div>
48
56
  </button>
49
57
  </div>
50
- </div>
51
- </div>
52
-
53
- <div class="pc-field" style="max-width: 280px">
54
- <div class="pc-field__label">
55
- {{
56
- form.valueType === 'PERCENT'
57
- ? msg.form.valuePercentLabel
58
- : msg.form.valueAbsoluteLabel
59
- }}
60
- </div>
61
- <input
58
+ </AdminField>
59
+ </AdminFieldGrid>
60
+
61
+ <AdminField
62
+ class="pc-field--narrow"
63
+ :label="
64
+ form.valueType === 'PERCENT'
65
+ ? msg.form.valuePercentLabel
66
+ : msg.form.valueAbsoluteLabel
67
+ "
68
+ >
69
+ <q-input
62
70
  v-model.number="form.value"
63
- class="pc-input"
71
+ outlined
72
+ dense
64
73
  type="number"
65
74
  min="1"
66
75
  :max="form.valueType === 'PERCENT' ? 100 : undefined"
67
76
  />
68
- </div>
77
+ </AdminField>
69
78
  </div>
70
79
 
71
80
  <!-- Section: Validity & Duration -->
72
81
  <div class="pc-section">
73
82
  <div class="pc-section__title">{{ msg.form.sectionValidity }}</div>
74
83
 
75
- <div v-if="plans.length > 0" class="pc-field">
76
- <div class="pc-field__label">{{ msg.form.plansLabel }}</div>
84
+ <AdminField
85
+ v-if="plans.length > 0"
86
+ :label="msg.form.plansLabel"
87
+ :hint="formatMessage(msg.form.plansHint, { count: form.appliesToPlans.length })"
88
+ >
77
89
  <div class="pc-plan-pick">
90
+ <!-- @optionSurface
91
+ A plan chip with a colour mark, one of a multi-select group. -->
78
92
  <button
79
93
  v-for="p in plans"
80
94
  :key="p.key"
@@ -91,79 +105,73 @@
91
105
  {{ p.label }}
92
106
  </button>
93
107
  </div>
94
- <div class="pc-field__hint">
95
- {{
96
- formatMessage(msg.form.plansHint, {
97
- count: form.appliesToPlans.length,
98
- })
99
- }}
100
- </div>
101
- </div>
108
+ </AdminField>
102
109
 
103
- <div class="pc-grid pc-grid--2">
104
- <div class="pc-field">
105
- <div class="pc-field__label">{{ msg.form.durationLabel }}</div>
110
+ <AdminFieldGrid>
111
+ <AdminField :label="msg.form.durationLabel">
106
112
  <div class="pc-seg pc-seg--fill">
107
- <button
113
+ <q-btn
108
114
  v-for="o in durationOptions"
109
115
  :key="o.k"
110
- type="button"
111
116
  class="pc-seg-opt"
117
+ flat
118
+ dense
119
+ no-caps
120
+ :label="o.label"
112
121
  :class="{ 'pc-seg-opt--active': form.durationType === o.k }"
113
122
  @click="form.durationType = o.k"
114
- >
115
- {{ o.label }}
116
- </button>
123
+ />
117
124
  </div>
118
- <input
125
+ <q-input
119
126
  v-if="form.durationType !== 'ONCE'"
120
127
  v-model.number="form.durationValue"
121
- class="pc-input"
128
+ outlined
129
+ dense
122
130
  type="number"
123
131
  min="1"
124
- style="margin-top: var(--sa-space-3); max-width: 120px"
132
+ class="pc-duration-value"
125
133
  :placeholder="
126
134
  form.durationType === 'MONTHS'
127
135
  ? msg.form.durationMonthsPlaceholder
128
136
  : msg.form.durationCyclesPlaceholder
129
137
  "
130
138
  />
131
- </div>
132
-
133
- <div class="pc-field">
134
- <div class="pc-field__label">{{ msg.form.maxRedemptionsLabel }}</div>
135
- <input
139
+ </AdminField>
140
+
141
+ <AdminField
142
+ :label="msg.form.maxRedemptionsLabel"
143
+ :hint="
144
+ mode === 'create'
145
+ ? msg.form.maxRedemptionsHintCreate
146
+ : msg.form.maxRedemptionsHintEdit
147
+ "
148
+ >
149
+ <q-input
136
150
  v-model.number="form.maxRedemptions"
137
- class="pc-input"
151
+ outlined
152
+ dense
138
153
  type="number"
139
154
  min="1"
140
155
  :placeholder="msg.form.maxRedemptionsPlaceholder"
141
156
  />
142
- <div class="pc-field__hint">
143
- {{
144
- mode === 'create'
145
- ? msg.form.maxRedemptionsHintCreate
146
- : msg.form.maxRedemptionsHintEdit
147
- }}
148
- </div>
149
- </div>
150
- </div>
151
-
152
- <div class="pc-grid pc-grid--2">
153
- <div class="pc-field">
154
- <div class="pc-field__label">{{ msg.form.validFromLabel }}</div>
155
- <input v-model="form.validFrom" class="pc-input" type="date" />
156
- </div>
157
- <div class="pc-field">
158
- <div class="pc-field__label">{{ msg.form.validUntilLabel }}</div>
159
- <input v-model="form.validUntil" class="pc-input" type="date" />
160
- </div>
161
- </div>
162
-
163
- <div v-if="mode === 'edit'" class="pc-grid pc-grid--2">
164
- <div class="pc-field">
165
- <div class="pc-field__label">{{ common.status }}</div>
157
+ </AdminField>
158
+ </AdminFieldGrid>
159
+
160
+ <AdminFieldGrid>
161
+ <AdminField :label="msg.form.validFromLabel">
162
+ <q-input v-model="form.validFrom" outlined dense type="date" />
163
+ </AdminField>
164
+ <AdminField :label="msg.form.validUntilLabel">
165
+ <q-input v-model="form.validUntil" outlined dense type="date" />
166
+ </AdminField>
167
+ </AdminFieldGrid>
168
+
169
+ <AdminFieldGrid v-if="mode === 'edit'">
170
+ <AdminField :label="common.status" :hint="msg.form.statusHint">
166
171
  <div class="pc-seg pc-status">
172
+ <!-- @optionSurface
173
+ A segment in a segmented control — the group carries the frame, and each
174
+ segment is a radio rather than a button. -->
167
175
  <button
168
176
  v-for="o in statusOptions"
169
177
  :key="o.k"
@@ -176,32 +184,35 @@
176
184
  {{ o.label }}
177
185
  </button>
178
186
  </div>
179
- <div class="pc-field__hint">{{ msg.form.statusHint }}</div>
180
- </div>
181
- </div>
187
+ </AdminField>
188
+ </AdminFieldGrid>
182
189
  </div>
183
190
 
184
191
  <!-- Section: Campaign & Note -->
185
192
  <div class="pc-section">
186
193
  <div class="pc-section__title">{{ msg.form.sectionCampaign }}</div>
187
- <div v-if="showCampaignTag" class="pc-field">
188
- <div class="pc-field__label">{{ msg.form.campaignLabel }}</div>
189
- <input
194
+ <AdminField
195
+ v-if="showCampaignTag"
196
+ :label="msg.form.campaignLabel"
197
+ :hint="msg.form.campaignHint"
198
+ >
199
+ <q-input
190
200
  v-model="form.campaignTag"
191
- class="pc-input"
201
+ outlined
202
+ dense
192
203
  :placeholder="msg.form.campaignPlaceholder"
193
204
  />
194
- <div class="pc-field__hint">{{ msg.form.campaignHint }}</div>
195
- </div>
196
- <div class="pc-field">
197
- <div class="pc-field__label">{{ msg.form.noteLabel }}</div>
198
- <textarea
205
+ </AdminField>
206
+ <AdminField :label="msg.form.noteLabel">
207
+ <q-input
199
208
  v-model="form.description"
200
- class="pc-input"
201
- rows="2"
209
+ outlined
210
+ dense
211
+ type="textarea"
212
+ :rows="2"
202
213
  :placeholder="msg.form.notePlaceholder"
203
214
  />
204
- </div>
215
+ </AdminField>
205
216
  </div>
206
217
 
207
218
  <!-- Section: Advanced (backend-only fields, collapsed).
@@ -218,46 +229,48 @@
218
229
  </span>
219
230
  </template>
220
231
  <div class="pc-advanced">
221
- <div class="pc-grid pc-grid--2">
222
- <div class="pc-field">
223
- <div class="pc-field__label">{{ msg.form.billingCycleLabel }}</div>
224
- <select v-model="form.appliesToBilling" class="pc-input">
225
- <option :value="mode === 'create' ? undefined : null">
226
- {{ common.both }}
227
- </option>
228
- <option value="MONTHLY">{{ common.monthly }}</option>
229
- <option value="YEARLY">{{ common.yearly }}</option>
230
- </select>
231
- </div>
232
- <div class="pc-field">
233
- <div class="pc-field__label">{{ msg.form.minAmountLabel }}</div>
234
- <input
232
+ <AdminFieldGrid>
233
+ <AdminField :label="msg.form.billingCycleLabel">
234
+ <q-select
235
+ v-model="form.appliesToBilling"
236
+ outlined
237
+ dense
238
+ emit-value
239
+ map-options
240
+ :options="billingCycleOptions"
241
+ />
242
+ </AdminField>
243
+ <AdminField :label="msg.form.minAmountLabel">
244
+ <q-input
235
245
  v-model.number="form.minimumPlanAmountGross"
236
- class="pc-input"
246
+ outlined
247
+ dense
237
248
  type="number"
238
249
  min="0"
239
250
  :placeholder="msg.form.minAmountPlaceholder"
240
251
  />
241
- </div>
242
- </div>
243
- <div class="pc-grid pc-grid--2">
244
- <label class="pc-check">
245
- <input v-model="form.firstTimeCustomersOnly" type="checkbox" />
246
- <span>{{ msg.form.firstTimeOnly }}</span>
247
- </label>
248
- <label class="pc-check">
249
- <input v-model="form.allowZeroInvoice" type="checkbox" />
250
- <span>{{ msg.form.allowZeroInvoice }}</span>
251
- </label>
252
- </div>
253
- <div class="pc-field">
254
- <div class="pc-field__label">{{ msg.form.revenueAccountLabel }}</div>
255
- <input
252
+ </AdminField>
253
+ </AdminFieldGrid>
254
+ <AdminFieldGrid>
255
+ <q-checkbox
256
+ v-model="form.firstTimeCustomersOnly"
257
+ dense
258
+ :label="msg.form.firstTimeOnly"
259
+ />
260
+ <q-checkbox
261
+ v-model="form.allowZeroInvoice"
262
+ dense
263
+ :label="msg.form.allowZeroInvoice"
264
+ />
265
+ </AdminFieldGrid>
266
+ <AdminField :label="msg.form.revenueAccountLabel">
267
+ <q-input
256
268
  v-model="form.revenueDeductionAccount"
257
- class="pc-input"
269
+ outlined
270
+ dense
258
271
  :placeholder="msg.form.revenueAccountPlaceholder"
259
272
  />
260
- </div>
273
+ </AdminField>
261
274
  </div>
262
275
  </AdminAccordion>
263
276
 
@@ -275,6 +288,8 @@
275
288
  <script setup lang="ts">
276
289
  import { computed } from 'vue';
277
290
  import AdminAccordion from '../../ui/page/AdminAccordion.vue';
291
+ import AdminField from '../../ui/page/AdminField.vue';
292
+ import AdminFieldGrid from '../../ui/page/AdminFieldGrid.vue';
278
293
  import { IDENTITY_NEUTRAL, identityChipStyle } from '../../client/identity-accents.js';
279
294
  import { formatMessage } from '../../client/i18n/format.js';
280
295
  import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
@@ -363,6 +378,19 @@ const durationOptions = computed<ReadonlyArray<{ k: PromoCodeDurationType; label
363
378
  { k: 'BILLING_CYCLES', label: msg.value.form.durationBillingCycles },
364
379
  ]);
365
380
 
381
+ /**
382
+ * `undefined` on create and `null` on edit both mean "either cycle".
383
+ *
384
+ * The two are not interchangeable here: create omits the field from the payload
385
+ * and edit clears it, which is the distinction the hand-written `<option>` also
386
+ * carried.
387
+ */
388
+ const billingCycleOptions = computed(() => [
389
+ { value: props.mode === 'create' ? undefined : null, label: common.value.both },
390
+ { value: 'MONTHLY', label: common.value.monthly },
391
+ { value: 'YEARLY', label: common.value.yearly },
392
+ ]);
393
+
366
394
  const statusOptions = computed<
367
395
  ReadonlyArray<{ k: 'ACTIVE' | 'PAUSED'; label: string; icon: string }>
368
396
  >(() => [
@@ -370,9 +398,15 @@ const statusOptions = computed<
370
398
  { k: 'PAUSED', label: msg.value.form.statusPaused, icon: 'pause' },
371
399
  ]);
372
400
 
373
- /** The code round-trips through the caller so its form stays the source of truth. */
374
- function onCodeInput(event: Event): void {
375
- const raw = (event.target as HTMLInputElement).value;
401
+ /**
402
+ * The code round-trips through the caller so its form stays the source of truth.
403
+ *
404
+ * `q-input` hands over the value rather than the event, which is also why the
405
+ * field is bound one-way: the normalisation below has to run between what was
406
+ * typed and what is shown, and `v-model` would show the raw keystroke first.
407
+ */
408
+ function onCodeInput(value: string | number | null): void {
409
+ const raw = String(value ?? '');
376
410
  emit('update:code', raw.toUpperCase().replace(/[^A-Z0-9_-]/g, ''));
377
411
  }
378
412
 
@@ -431,8 +465,8 @@ const advancedOpen = defineModel<boolean>('advancedOpen', { default: false });
431
465
  <style scoped>
432
466
  .pc-section {
433
467
  border: 1px solid var(--sa-color-border);
434
- border-radius: 10px;
435
- padding: 14px 16px;
468
+ border-radius: var(--sa-radius-tile);
469
+ padding: var(--sa-space-4) var(--sa-space-5);
436
470
  background: var(--sa-color-bg-surface-raised);
437
471
  }
438
472
 
@@ -441,8 +475,8 @@ const advancedOpen = defineModel<boolean>('advancedOpen', { default: false });
441
475
  font-weight: 700;
442
476
  font-size: var(--sa-text-md);
443
477
  color: var(--sa-color-fg-heading);
444
- margin-bottom: 12px;
445
- letter-spacing: -0.005em;
478
+ margin-bottom: var(--sa-space-4);
479
+ letter-spacing: var(--sa-tracking-normal);
446
480
  }
447
481
 
448
482
  /* The same title, in a header that already spaces itself. */
@@ -453,106 +487,32 @@ const advancedOpen = defineModel<boolean>('advancedOpen', { default: false });
453
487
  .pc-advanced {
454
488
  display: flex;
455
489
  flex-direction: column;
456
- gap: 12px;
490
+ gap: var(--sa-space-4);
457
491
  }
458
492
 
459
- .pc-grid {
460
- display: grid;
461
- gap: 12px;
462
- margin-bottom: 12px;
463
- }
464
-
465
- .pc-grid--2 {
466
- grid-template-columns: 1fr 1fr;
467
- }
468
-
469
- @media (max-width: 599.98px) {
470
- .pc-grid--2 {
471
- grid-template-columns: 1fr;
472
- }
473
- }
474
-
475
- .pc-grid:last-child {
476
- margin-bottom: 0;
477
- }
478
-
479
- .pc-field {
480
- display: flex;
481
- flex-direction: column;
482
- gap: 4px;
483
- }
484
-
485
- .pc-field__label {
486
- font-size: var(--sa-text-sm);
487
- font-weight: 600;
488
- color: var(--sa-color-fg-muted);
489
- text-transform: uppercase;
490
- letter-spacing: 0.04em;
491
- }
492
-
493
- .pc-field__hint {
494
- font-size: var(--sa-text-sm);
495
- color: var(--sa-color-fg-subtle);
496
- }
497
-
498
- .pc-input {
499
- width: 100%;
500
- border: 1px solid var(--sa-color-border);
501
- background: var(--sa-color-bg-surface);
502
- border-radius: 7px;
503
- padding: 8px 10px;
504
- font: var(--sa-text-md) var(--sa-font-body, system-ui, sans-serif);
505
- color: var(--sa-color-fg-body);
506
- outline: 0;
507
- }
508
-
509
- .pc-input:focus {
510
- border-color: var(--sa-color-accent);
511
- box-shadow: 0 0 0 3px var(--sa-shadow-tint-3);
512
- }
513
-
514
- .pc-input--code {
515
- font: 600 var(--sa-text-lg) var(--sa-font-mono, ui-monospace, monospace);
516
- letter-spacing: 0.04em;
517
- text-transform: uppercase;
493
+ /* The generator sits in the field's `after` slot; without this it reads as a
494
+ * second field rather than as part of the one it belongs to. */
495
+ .pc-code :deep(.q-field__after) {
496
+ padding-left: var(--sa-space-2);
518
497
  }
519
498
 
520
- textarea.pc-input {
521
- font: var(--sa-text-md) var(--sa-font-body, system-ui, sans-serif);
522
- resize: vertical;
523
- }
524
-
525
- .pc-code-input {
526
- display: flex;
527
- gap: 8px;
528
- align-items: center;
529
- }
530
-
531
- .pc-btn-mini {
532
- border: 1px solid var(--sa-color-border);
533
- background: var(--sa-color-bg-surface);
534
- border-radius: 7px;
535
- padding: 6px 10px;
536
- font: 500 var(--sa-text-sm) var(--sa-font-body, system-ui, sans-serif);
537
- cursor: pointer;
538
- color: var(--sa-color-fg-secondary);
539
- }
540
-
541
- .pc-btn-mini:hover {
542
- background: var(--sa-color-border-soft);
499
+ /* A count, not a measurement — the field would otherwise stretch to the grid. */
500
+ .pc-duration-value,
501
+ .pc-field--narrow {
502
+ max-width: 280px;
543
503
  }
544
504
 
545
505
  .pc-type-grid {
546
506
  display: grid;
547
507
  grid-template-columns: 1fr 1fr;
548
- gap: 6px;
508
+ gap: var(--sa-space-2);
549
509
  }
550
510
 
551
511
  .pc-type-opt {
552
512
  border: 1px solid var(--sa-color-border);
553
513
  background: var(--sa-color-bg-surface);
554
- border-radius: 8px;
555
- padding: 8px 10px;
514
+ border-radius: var(--sa-radius-field);
515
+ padding: var(--sa-space-3) var(--sa-space-3);
556
516
  text-align: left;
557
517
  cursor: pointer;
558
518
  transition:
@@ -577,23 +537,23 @@ textarea.pc-input {
577
537
  .pc-type-opt__sub {
578
538
  font: var(--sa-text-sm) var(--sa-font-mono, ui-monospace, monospace);
579
539
  color: var(--sa-color-fg-muted);
580
- margin-top: 1px;
540
+ margin-top: var(--sa-space-0);
581
541
  }
582
542
 
583
543
  .pc-plan-pick {
584
544
  display: flex;
585
545
  flex-wrap: wrap;
586
- gap: 6px;
546
+ gap: var(--sa-space-2);
587
547
  }
588
548
 
589
549
  .pc-plan-opt {
590
550
  display: inline-flex;
591
551
  align-items: center;
592
- gap: 8px;
552
+ gap: var(--sa-space-3);
593
553
  background: var(--sa-color-bg-surface);
594
554
  border: 1px solid var(--sa-color-border);
595
- border-radius: 999px;
596
- padding: 5px 12px 5px 8px;
555
+ border-radius: var(--sa-radius-pill);
556
+ padding: var(--sa-space-2) var(--sa-space-4) var(--sa-space-2) var(--sa-space-3);
597
557
  font: 600 var(--sa-text-sm) var(--sa-font-body, system-ui, sans-serif);
598
558
  cursor: pointer;
599
559
  color: var(--sa-color-fg-secondary);
@@ -625,7 +585,7 @@ textarea.pc-input {
625
585
  * restating it. */
626
586
  .pc-seg {
627
587
  display: flex;
628
- gap: 4px;
588
+ gap: var(--sa-space-2);
629
589
  }
630
590
  /* The duration options divide the row; the status options size to their label. */
631
591
  .pc-seg--fill .pc-seg-opt {
@@ -639,8 +599,8 @@ textarea.pc-input {
639
599
  gap: var(--sa-gap-inline);
640
600
  border: 1px solid var(--sa-color-border);
641
601
  background: var(--sa-color-bg-surface);
642
- border-radius: 7px;
643
- padding: 6px 10px;
602
+ border-radius: var(--sa-radius-control);
603
+ padding: var(--sa-space-2) var(--sa-space-3);
644
604
  font: 500 var(--sa-text-sm) var(--sa-font-body, system-ui, sans-serif);
645
605
  cursor: pointer;
646
606
  color: var(--sa-color-fg-secondary);
@@ -661,20 +621,11 @@ textarea.pc-input {
661
621
  color: var(--sa-color-accent-strong);
662
622
  }
663
623
 
664
- .pc-check {
665
- display: inline-flex;
666
- align-items: center;
667
- gap: 8px;
668
- font: var(--sa-text-md) var(--sa-font-body, system-ui, sans-serif);
669
- color: var(--sa-color-fg-body);
670
- cursor: pointer;
671
- }
672
-
673
624
  .pc-preview {
674
625
  background: var(--sa-color-accent-surface-soft);
675
626
  border: 1px solid var(--sa-color-accent-border);
676
- border-radius: 10px;
677
- padding: 12px 14px;
627
+ border-radius: var(--sa-radius-tile);
628
+ padding: var(--sa-space-4) var(--sa-space-4);
678
629
  }
679
630
 
680
631
  .pc-preview__eyebrow {
@@ -682,24 +633,24 @@ textarea.pc-input {
682
633
  font-weight: 700;
683
634
  color: var(--sa-color-accent);
684
635
  text-transform: uppercase;
685
- letter-spacing: 0.06em;
686
- margin-bottom: 6px;
636
+ letter-spacing: var(--sa-tracking-wider);
637
+ margin-bottom: var(--sa-space-2);
687
638
  }
688
639
 
689
640
  .pc-preview__body {
690
641
  display: flex;
691
642
  align-items: center;
692
- gap: 10px;
643
+ gap: var(--sa-space-3);
693
644
  flex-wrap: wrap;
694
645
  }
695
646
 
696
647
  .pc-preview__code {
697
648
  background: var(--sa-color-bg-surface);
698
649
  border: 1px solid var(--sa-color-border);
699
- border-radius: 6px;
700
- padding: 3px 8px;
650
+ border-radius: var(--sa-radius-badge);
651
+ padding: var(--sa-space-1) var(--sa-space-3);
701
652
  font: 600 var(--sa-text-md) var(--sa-font-mono, ui-monospace, monospace);
702
- letter-spacing: 0.04em;
653
+ letter-spacing: var(--sa-tracking-wide);
703
654
  color: var(--sa-color-fg-heading);
704
655
  }
705
656