@saasicat/ui-vue 0.22.2 → 0.24.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 (119) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-LY52T4N4.js → chunk-TQQMKWN6.js} +58 -1
  3. package/dist/{chunk-XMMFN2WJ.js → chunk-VYMTWCTT.js} +51 -1
  4. package/dist/client/index.cjs +57 -0
  5. package/dist/client/index.d.cts +58 -1
  6. package/dist/client/index.d.ts +58 -1
  7. package/dist/client/index.js +15 -1
  8. package/dist/index.cjs +321 -159
  9. package/dist/index.d.cts +67 -4
  10. package/dist/index.d.ts +67 -4
  11. package/dist/index.js +72 -2
  12. package/dist/quasar/index.cjs +116 -20
  13. package/dist/quasar/index.d.cts +48 -3
  14. package/dist/quasar/index.d.ts +48 -3
  15. package/dist/quasar/index.js +58 -3
  16. package/dist/{use-super-admin-i18n-CgTuNi8h.d.cts → use-sa-theme-B5t7oXph.d.cts} +51 -1
  17. package/dist/{use-super-admin-i18n-CHNdsSlm.d.ts → use-sa-theme-OUBlaqgl.d.ts} +51 -1
  18. package/package.json +5 -2
  19. package/src/client/identity-accents.ts +129 -0
  20. package/src/client/index.ts +1 -0
  21. package/src/components/BundleVersionPublishDialog.vue +17 -17
  22. package/src/components/KvBlock.vue +6 -6
  23. package/src/components/MarketingPromotionsTab.vue +84 -80
  24. package/src/components/MfaPromptDialog.vue +2 -2
  25. package/src/components/TenantActionConfirmDialog.vue +5 -5
  26. package/src/components/VersionDiffPreview.vue +18 -18
  27. package/src/components/bundle-editor/BundleCreatePanel.vue +52 -52
  28. package/src/components/bundle-editor/BundleFeaturesEditor.vue +20 -20
  29. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +33 -33
  30. package/src/components/bundle-editor/BundleQuotasEditor.vue +26 -26
  31. package/src/components/bundle-editor/BundleStatusBanner.vue +21 -21
  32. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +49 -49
  33. package/src/components/bundle-editor/BundleVersionStrip.vue +30 -30
  34. package/src/components/dialogs/PilotCreateDialog.vue +20 -20
  35. package/src/components/dialogs/PilotEditDialog.vue +6 -6
  36. package/src/components/dialogs/PromoCodeCreateDialog.vue +9 -9
  37. package/src/components/dialogs/PromoCodeDialogFields.vue +64 -63
  38. package/src/components/dialogs/PromoCodeEditDialog.vue +11 -11
  39. package/src/components/dialogs/pilot-dialog.css +48 -48
  40. package/src/components/plan/PlanCycleToggle.vue +9 -16
  41. package/src/components/plan/PlanGrid.vue +30 -34
  42. package/src/components/plan/PriceSummary.vue +35 -35
  43. package/src/components/plan/PromoCodeInput.vue +26 -26
  44. package/src/components/plan/PublicBundleGrid.vue +32 -28
  45. package/src/components/plan-create-dialog/PlanCreateDialog.vue +50 -50
  46. package/src/components/plan-detail/PlanDetail.vue +178 -157
  47. package/src/components/plan-detail/PlanTitleEdit.vue +11 -11
  48. package/src/components/plan-list/PlanList.vue +116 -100
  49. package/src/components/plan-matrix/PlanMatrix.vue +97 -114
  50. package/src/components/plan-review/PlanReview.vue +61 -57
  51. package/src/components/plan-version-editor/PlanVersionEditor.vue +246 -203
  52. package/src/components/wizard-stepper/WizardStepper.vue +17 -17
  53. package/src/index.ts +1 -0
  54. package/src/pages-standard/AdminLayout.vue +58 -37
  55. package/src/pages-standard/AdminManifestErrorPage.vue +6 -6
  56. package/src/pages-standard/AuditPage.vue +3 -3
  57. package/src/pages-standard/BundlesPage.vue +37 -37
  58. package/src/pages-standard/DashboardPage.vue +22 -19
  59. package/src/pages-standard/DiscoveryPage.vue +55 -49
  60. package/src/pages-standard/EmailHistoryPage.vue +53 -71
  61. package/src/pages-standard/MarketingCatalogPage.vue +245 -221
  62. package/src/pages-standard/PilotsPage.vue +7 -6
  63. package/src/pages-standard/PlatformEmailPage.vue +18 -18
  64. package/src/pages-standard/PromoCodeDetailPage.vue +3 -3
  65. package/src/pages-standard/PromoCodesPage.vue +10 -7
  66. package/src/pages-standard/SuperAdminLoginPage.vue +39 -25
  67. package/src/pages-standard/SuperAdminSetupWizard.vue +44 -33
  68. package/src/pages-standard/TenantDetailPage.vue +7 -7
  69. package/src/pages-standard/TenantsPage.vue +16 -50
  70. package/src/pages-standard/bundles-page/BundleAccordionList.vue +13 -13
  71. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +24 -24
  72. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +22 -22
  73. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +39 -39
  74. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +26 -26
  75. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +1 -1
  76. package/src/pages-standard/discovery-page/discovery-ui.ts +10 -7
  77. package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +2 -5
  78. package/src/pages-standard/plan-versions/PlanDiffCard.vue +27 -20
  79. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +24 -16
  80. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +45 -45
  81. package/src/pages-standard/plans-page/PlanBundleOverview.vue +21 -15
  82. package/src/pages-standard/plans-page/PlanPublishDialog.vue +4 -4
  83. package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
  84. package/src/pages-standard/sa-theme.css +9 -687
  85. package/src/pages-standard/tenants/StatusPill.vue +13 -13
  86. package/src/pages-standard/tenants/format.ts +11 -12
  87. package/src/pages-tenant/LimitsRow.vue +4 -4
  88. package/src/pages-tenant/MySubscriptionBundlesPage.vue +70 -61
  89. package/src/pages-tenant/OnboardingConfigurator.vue +25 -25
  90. package/src/pages-tenant/PackageSnapshotPanel.vue +10 -16
  91. package/src/pages-tenant/PendingVersionBanner.vue +12 -12
  92. package/src/pages-tenant/PlanChangeWizard.vue +22 -76
  93. package/src/pages-tenant/TenantPlanSection.vue +19 -69
  94. package/src/pages-tenant/UsageBar.vue +7 -7
  95. package/src/pages-tenant/default-i18n.ts +113 -0
  96. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +14 -14
  97. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +10 -10
  98. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +2 -2
  99. package/src/quasar/create-super-admin-app.ts +96 -1
  100. package/src/quasar/dark-bridge.ts +66 -0
  101. package/src/quasar/index.ts +1 -0
  102. package/src/ui/theme/_breakpoints.scss +34 -0
  103. package/src/ui/theme/base.css +50 -0
  104. package/src/ui/theme/components/body.css +23 -0
  105. package/src/ui/theme/components/button.css +158 -0
  106. package/src/ui/theme/components/card.css +74 -0
  107. package/src/ui/theme/components/field.css +38 -0
  108. package/src/ui/theme/components/hero.css +46 -0
  109. package/src/ui/theme/components/paginator.css +63 -0
  110. package/src/ui/theme/components/section.css +67 -0
  111. package/src/ui/theme/components/statistics.css +189 -0
  112. package/src/ui/theme/components/table.css +60 -0
  113. package/src/ui/theme/index.css +42 -0
  114. package/src/ui/theme/tokens.legacy.css +147 -0
  115. package/src/ui/theme/tokens.primitive.css +112 -0
  116. package/src/ui/theme/tokens.scale.css +103 -0
  117. package/src/ui/theme/tokens.semantic.dark.css +185 -0
  118. package/src/ui/theme/tokens.semantic.light.css +242 -0
  119. package/src/vue/use-sa-theme.ts +154 -0
@@ -1,9 +1,9 @@
1
- import { Component, App } from 'vue';
1
+ import { Component, App, WatchStopHandle } from 'vue';
2
2
  import { QuasarPluginOptions } from 'quasar';
3
3
  import { Pinia } from 'pinia';
4
4
  import { RouteRecordRaw, RouterHistory, Router } from 'vue-router';
5
5
  import { H as HttpClient } from '../catalog-DJcfm5N2.cjs';
6
- import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-CgTuNi8h.cjs';
6
+ import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SaThemeOptions, f as SuperAdminI18n, g as SaTheme } from '../use-sa-theme-B5t7oXph.cjs';
7
7
  import '@saasicat/types';
8
8
 
9
9
  interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
@@ -66,6 +66,17 @@ interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
66
66
  * runtime. Consumed via `useSuperAdminI18n()` / `useSaMessages()`.
67
67
  */
68
68
  i18n?: SuperAdminI18nOptions;
69
+ /**
70
+ * Optional: colour scheme. Default follows the operating system and
71
+ * remembers an explicit pick. The context is returned on the handle
72
+ * (`handle.theme`) — set `handle.theme.scheme.value = 'dark'` to switch at
73
+ * runtime, and read it anywhere via `useSaTheme()`.
74
+ *
75
+ * Whatever it resolves to is mirrored onto `<html data-sa-theme>` AND into
76
+ * Quasar's `Dark`, so the platform's surfaces and Quasar's own components
77
+ * never disagree.
78
+ */
79
+ theme?: SaThemeOptions;
69
80
  }
70
81
  interface SuperAdminAppHandle {
71
82
  app: App;
@@ -73,9 +84,27 @@ interface SuperAdminAppHandle {
73
84
  pinia: Pinia;
74
85
  /** i18n context of the shell — switch locale via `i18n.locale.value`. */
75
86
  i18n: SuperAdminI18n;
87
+ /** Colour scheme of the shell — switch via `theme.scheme.value`. */
88
+ theme: SaTheme;
76
89
  /** Mounts the app on a selector. Returns the root component instance. */
77
90
  mount: (selector: string | Element) => ReturnType<App['mount']>;
91
+ /**
92
+ * Releases what this helper attached to the DOCUMENT — the theme's
93
+ * `prefers-color-scheme` subscription and the bridge that mirrors it onto
94
+ * `data-sa-theme` and Quasar's `Dark`. Idempotent; it does not unmount the
95
+ * app, which is `handle.app.unmount()`.
96
+ *
97
+ * Only matters where a shell is torn down while the document survives: hot
98
+ * reload, a micro-frontend, a second shell in one page. Skip it and the old
99
+ * bridge keeps writing to the one document on the next OS theme change, and
100
+ * can overrule the scheme the new shell was given.
101
+ */
102
+ dispose: () => void;
78
103
  }
104
+ /** Marks Quasar's teleported nodes so the theme can reach them. Exported so a
105
+ * test fixture can install the same config a real app gets — without it the
106
+ * `.sa-portal` rules are exercised by nothing. */
107
+ declare const SA_PORTAL_CLASS = "sa-portal";
79
108
  /**
80
109
  * Universal bootstrap function for SuperAdmin apps. Replaces the `main.ts`
81
110
  * boilerplate duplicated per app today (Quasar + Pinia + Router + manifest
@@ -84,6 +113,22 @@ interface SuperAdminAppHandle {
84
113
  */
85
114
  declare function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperAdminAppHandle;
86
115
 
116
+ /**
117
+ * Mirrors a theme context onto the document and Quasar. Returns the stop
118
+ * handle; `createSuperAdminApp()` wires this up for you.
119
+ *
120
+ * `immediate` is not optional: at bootstrap the stored pick is already resolved
121
+ * and nothing has painted yet, so a first-change-only watcher would render one
122
+ * frame in the wrong theme on every reload.
123
+ *
124
+ * That first tick MIRRORS, it does not negotiate — whatever `theme.resolved`
125
+ * says is written to Quasar, including `Dark.set(false)`. So a caller that
126
+ * bootstraps with Quasar already dark has to say so when it builds the theme,
127
+ * or its own choice is erased one line after it was applied.
128
+ * `createSuperAdminApp` does exactly that, seeding from `Dark.isActive`.
129
+ */
130
+ declare function bindSaThemeToDocument(theme: SaTheme): WatchStopHandle;
131
+
87
132
  /**
88
133
  * Quasar-backed notify port: `position: 'top'` matches the convention the
89
134
  * standard pages used with their previous direct `$q.notify` calls.
@@ -92,4 +137,4 @@ declare const quasarNotify: UiNotify;
92
137
  /** Returns the app-provided notify port, falling back to the Quasar default. */
93
138
  declare function useSuperAdminNotify(): UiNotify;
94
139
 
95
- export { type CreateSuperAdminAppOptions, type SuperAdminAppHandle, createSuperAdminApp, quasarNotify, useSuperAdminNotify };
140
+ export { type CreateSuperAdminAppOptions, SA_PORTAL_CLASS, type SuperAdminAppHandle, bindSaThemeToDocument, createSuperAdminApp, quasarNotify, useSuperAdminNotify };
@@ -1,9 +1,9 @@
1
- import { Component, App } from 'vue';
1
+ import { Component, App, WatchStopHandle } from 'vue';
2
2
  import { QuasarPluginOptions } from 'quasar';
3
3
  import { Pinia } from 'pinia';
4
4
  import { RouteRecordRaw, RouterHistory, Router } from 'vue-router';
5
5
  import { H as HttpClient } from '../catalog-DJcfm5N2.js';
6
- import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SuperAdminI18n } from '../use-super-admin-i18n-CHNdsSlm.js';
6
+ import { S as SuperAdminGuardOptions, a as SuperAdminBrand, b as SuperAdminEndpoints, E as ExtensionsMap, A as ActionsMap, c as SuperAdminLoginAdapter, U as UiNotify, I as InstallPlugin, d as SuperAdminI18nOptions, e as SaThemeOptions, f as SuperAdminI18n, g as SaTheme } from '../use-sa-theme-OUBlaqgl.js';
7
7
  import '@saasicat/types';
8
8
 
9
9
  interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
@@ -66,6 +66,17 @@ interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
66
66
  * runtime. Consumed via `useSuperAdminI18n()` / `useSaMessages()`.
67
67
  */
68
68
  i18n?: SuperAdminI18nOptions;
69
+ /**
70
+ * Optional: colour scheme. Default follows the operating system and
71
+ * remembers an explicit pick. The context is returned on the handle
72
+ * (`handle.theme`) — set `handle.theme.scheme.value = 'dark'` to switch at
73
+ * runtime, and read it anywhere via `useSaTheme()`.
74
+ *
75
+ * Whatever it resolves to is mirrored onto `<html data-sa-theme>` AND into
76
+ * Quasar's `Dark`, so the platform's surfaces and Quasar's own components
77
+ * never disagree.
78
+ */
79
+ theme?: SaThemeOptions;
69
80
  }
70
81
  interface SuperAdminAppHandle {
71
82
  app: App;
@@ -73,9 +84,27 @@ interface SuperAdminAppHandle {
73
84
  pinia: Pinia;
74
85
  /** i18n context of the shell — switch locale via `i18n.locale.value`. */
75
86
  i18n: SuperAdminI18n;
87
+ /** Colour scheme of the shell — switch via `theme.scheme.value`. */
88
+ theme: SaTheme;
76
89
  /** Mounts the app on a selector. Returns the root component instance. */
77
90
  mount: (selector: string | Element) => ReturnType<App['mount']>;
91
+ /**
92
+ * Releases what this helper attached to the DOCUMENT — the theme's
93
+ * `prefers-color-scheme` subscription and the bridge that mirrors it onto
94
+ * `data-sa-theme` and Quasar's `Dark`. Idempotent; it does not unmount the
95
+ * app, which is `handle.app.unmount()`.
96
+ *
97
+ * Only matters where a shell is torn down while the document survives: hot
98
+ * reload, a micro-frontend, a second shell in one page. Skip it and the old
99
+ * bridge keeps writing to the one document on the next OS theme change, and
100
+ * can overrule the scheme the new shell was given.
101
+ */
102
+ dispose: () => void;
78
103
  }
104
+ /** Marks Quasar's teleported nodes so the theme can reach them. Exported so a
105
+ * test fixture can install the same config a real app gets — without it the
106
+ * `.sa-portal` rules are exercised by nothing. */
107
+ declare const SA_PORTAL_CLASS = "sa-portal";
79
108
  /**
80
109
  * Universal bootstrap function for SuperAdmin apps. Replaces the `main.ts`
81
110
  * boilerplate duplicated per app today (Quasar + Pinia + Router + manifest
@@ -84,6 +113,22 @@ interface SuperAdminAppHandle {
84
113
  */
85
114
  declare function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperAdminAppHandle;
86
115
 
116
+ /**
117
+ * Mirrors a theme context onto the document and Quasar. Returns the stop
118
+ * handle; `createSuperAdminApp()` wires this up for you.
119
+ *
120
+ * `immediate` is not optional: at bootstrap the stored pick is already resolved
121
+ * and nothing has painted yet, so a first-change-only watcher would render one
122
+ * frame in the wrong theme on every reload.
123
+ *
124
+ * That first tick MIRRORS, it does not negotiate — whatever `theme.resolved`
125
+ * says is written to Quasar, including `Dark.set(false)`. So a caller that
126
+ * bootstraps with Quasar already dark has to say so when it builds the theme,
127
+ * or its own choice is erased one line after it was applied.
128
+ * `createSuperAdminApp` does exactly that, seeding from `Dark.isActive`.
129
+ */
130
+ declare function bindSaThemeToDocument(theme: SaTheme): WatchStopHandle;
131
+
87
132
  /**
88
133
  * Quasar-backed notify port: `position: 'top'` matches the convention the
89
134
  * standard pages used with their previous direct `$q.notify` calls.
@@ -92,4 +137,4 @@ declare const quasarNotify: UiNotify;
92
137
  /** Returns the app-provided notify port, falling back to the Quasar default. */
93
138
  declare function useSuperAdminNotify(): UiNotify;
94
139
 
95
- export { type CreateSuperAdminAppOptions, type SuperAdminAppHandle, createSuperAdminApp, quasarNotify, useSuperAdminNotify };
140
+ export { type CreateSuperAdminAppOptions, SA_PORTAL_CLASS, type SuperAdminAppHandle, bindSaThemeToDocument, createSuperAdminApp, quasarNotify, useSuperAdminNotify };
@@ -1,4 +1,5 @@
1
1
  import {
2
+ SA_THEME_KEY,
2
3
  SUPER_ADMIN_ACTIONS_KEY,
3
4
  SUPER_ADMIN_BRAND_KEY,
4
5
  SUPER_ADMIN_ENDPOINTS_KEY,
@@ -10,8 +11,9 @@ import {
10
11
  SUPER_ADMIN_MANIFEST_KEY,
11
12
  SUPER_ADMIN_NOTIFY_KEY,
12
13
  buildNavigationGuard,
14
+ createSaTheme,
13
15
  createSuperAdminI18n
14
- } from "../chunk-LY52T4N4.js";
16
+ } from "../chunk-TQQMKWN6.js";
15
17
  import {
16
18
  defaultHttpClient
17
19
  } from "../chunk-F7NRM6KI.js";
@@ -25,6 +27,33 @@ import {
25
27
  createWebHistory
26
28
  } from "vue-router";
27
29
 
30
+ // src/quasar/dark-bridge.ts
31
+ import { Dark } from "quasar";
32
+ import { watch } from "vue";
33
+ function bindSaThemeToDocument(theme) {
34
+ const toQuasar = watch(
35
+ theme.resolved,
36
+ (resolved) => {
37
+ if (typeof document !== "undefined") {
38
+ document.documentElement.setAttribute("data-sa-theme", resolved);
39
+ }
40
+ Dark.set(resolved === "dark");
41
+ },
42
+ { immediate: true }
43
+ );
44
+ const fromQuasar = watch(
45
+ () => Dark.isActive,
46
+ (isDark) => {
47
+ const asScheme = isDark ? "dark" : "light";
48
+ if (theme.resolved.value !== asScheme) theme.scheme.value = asScheme;
49
+ }
50
+ );
51
+ return () => {
52
+ toQuasar();
53
+ fromQuasar();
54
+ };
55
+ }
56
+
28
57
  // src/quasar/notify.ts
29
58
  import { inject } from "vue";
30
59
  import { Notify } from "quasar";
@@ -42,13 +71,25 @@ function useSuperAdminNotify() {
42
71
  }
43
72
 
44
73
  // src/quasar/create-super-admin-app.ts
74
+ var SA_PORTAL_CLASS = "sa-portal";
45
75
  var DEFAULT_QUASAR_OPTIONS = {
46
76
  plugins: { Notify: Notify2, Dialog, Loading },
47
77
  config: { notify: { position: "top-right", timeout: 3e3 } }
48
78
  };
49
79
  function createSuperAdminApp(options) {
50
80
  const app = createApp(options.rootComponent);
51
- app.use(Quasar, options.quasarOptions ?? DEFAULT_QUASAR_OPTIONS);
81
+ const quasarOptions = options.quasarOptions ?? DEFAULT_QUASAR_OPTIONS;
82
+ const config = quasarOptions.config;
83
+ const existingPortalClass = config?.globalNodes?.class;
84
+ app.use(Quasar, {
85
+ ...quasarOptions,
86
+ config: {
87
+ ...quasarOptions.config,
88
+ globalNodes: {
89
+ class: [existingPortalClass, SA_PORTAL_CLASS].filter(Boolean).join(" ")
90
+ }
91
+ }
92
+ });
52
93
  const pinia = createPinia();
53
94
  app.use(pinia);
54
95
  const router = createRouter({
@@ -66,8 +107,15 @@ function createSuperAdminApp(options) {
66
107
  manifestEndpoint: options.endpoints.manifestEndpoint ?? `${options.endpoints.apiBase}/manifest`
67
108
  };
68
109
  const i18n = createSuperAdminI18n(options.i18n);
110
+ const configuredDark = options.quasarOptions?.config?.dark;
111
+ const theme = createSaTheme({
112
+ ...options.theme,
113
+ scheme: options.theme?.scheme ?? (typeof configuredDark === "boolean" ? configuredDark ? "dark" : "light" : void 0)
114
+ });
115
+ const stopThemeBridge = bindSaThemeToDocument(theme);
69
116
  app.provide(SUPER_ADMIN_BRAND_KEY, { tag: "SuperAdmin", ...options.brand });
70
117
  app.provide(SUPER_ADMIN_I18N_KEY, i18n);
118
+ app.provide(SA_THEME_KEY, theme);
71
119
  app.provide(SUPER_ADMIN_ENDPOINTS_KEY, endpoints);
72
120
  app.provide(SUPER_ADMIN_EXTENSIONS_KEY, options.extensions ?? {});
73
121
  app.provide(SUPER_ADMIN_ACTIONS_KEY, options.actions ?? {});
@@ -90,10 +138,17 @@ function createSuperAdminApp(options) {
90
138
  router,
91
139
  pinia,
92
140
  i18n,
93
- mount: (selector) => app.mount(selector)
141
+ theme,
142
+ mount: (selector) => app.mount(selector),
143
+ dispose: () => {
144
+ stopThemeBridge();
145
+ theme.dispose();
146
+ }
94
147
  };
95
148
  }
96
149
  export {
150
+ SA_PORTAL_CLASS,
151
+ bindSaThemeToDocument,
97
152
  createSuperAdminApp,
98
153
  quasarNotify,
99
154
  useSuperAdminNotify
@@ -280,4 +280,54 @@ declare function useSuperAdminI18n(): SuperAdminI18n;
280
280
  */
281
281
  declare function useSaMessages<K extends keyof SaMessages>(namespace: K): ComputedRef<SaMessages[K]>;
282
282
 
283
- export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SA_LOCALE_STORAGE_KEY as g, SUPER_ADMIN_ACTIONS_KEY as h, SUPER_ADMIN_BRAND_KEY as i, SUPER_ADMIN_ENDPOINTS_KEY as j, SUPER_ADMIN_EXTENSIONS_KEY as k, SUPER_ADMIN_HTTP_KEY as l, SUPER_ADMIN_I18N_KEY as m, SUPER_ADMIN_LOGIN_ADAPTER_KEY as n, SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY as o, SUPER_ADMIN_MANIFEST_KEY as p, SUPER_ADMIN_NOTIFY_KEY as q, type SuperAdminAuthGuardOptions as r, type SuperAdminLoginResult as s, type SuperAdminManifestGuardOptions as t, type UiNotifyKind as u, type UiNotifyOptions as v, buildNavigationGuard as w, createSuperAdminI18n as x, useSaMessages as y, useSuperAdminI18n as z };
283
+ type SaColorScheme = 'light' | 'dark' | 'system';
284
+ type SaResolvedScheme = 'light' | 'dark';
285
+ interface SaTheme {
286
+ /** What the operator picked. Writable — assigning switches the theme. */
287
+ scheme: Ref<SaColorScheme>;
288
+ /** What that means right now: `'system'` resolved against the OS. */
289
+ resolved: ComputedRef<SaResolvedScheme>;
290
+ /**
291
+ * Releases the `prefers-color-scheme` subscription and the persistence
292
+ * watcher. Idempotent.
293
+ *
294
+ * Needed because this context outlives no component — nothing unmounts it.
295
+ * A second shell in the same document (hot reload, a micro-frontend, a
296
+ * second test file) would otherwise leave the first one subscribed, and the
297
+ * next change of the operating system's theme would drive TWO bridges, both
298
+ * writing `data-sa-theme` and Quasar's `Dark` onto the one document. The
299
+ * stale one can win, and then the new shell's explicit scheme is overruled
300
+ * by a context nobody holds a reference to any more.
301
+ */
302
+ dispose: () => void;
303
+ }
304
+ interface SaThemeOptions {
305
+ /** Starting scheme. Pass a `Ref` to keep the value in your own store. */
306
+ scheme?: SaColorScheme | Ref<SaColorScheme>;
307
+ /**
308
+ * Remember the operator's pick across reloads, default `true`. Ignored when
309
+ * `scheme` is a `Ref`: the app owns that value and persists it where it
310
+ * belongs. Same rule as the locale in `createSuperAdminI18n`.
311
+ */
312
+ persist?: boolean;
313
+ /** Injection seam for tests. */
314
+ storage?: KvStore;
315
+ }
316
+ /**
317
+ * Creates the theme context. `createSuperAdminApp()` makes one and the Quasar
318
+ * bridge mirrors it onto the document; call it directly only when bootstrapping
319
+ * by hand.
320
+ */
321
+ declare function createSaTheme(options?: SaThemeOptions): SaTheme;
322
+ declare const SA_THEME_KEY: InjectionKey<SaTheme>;
323
+ /**
324
+ * The shell's theme context, or a shared unpersisted one outside a shell.
325
+ *
326
+ * Same shape as `useSuperAdminI18n()`, and for the same reason: a component
327
+ * mounted in isolation must render rather than throw, and the fallback must not
328
+ * write to real storage or two tests in one process would decide each other's
329
+ * outcome.
330
+ */
331
+ declare function useSaTheme(): SaTheme;
332
+
333
+ export { type ActionsMap as A, type UiNotifyOptions as B, buildNavigationGuard as C, createSaTheme as D, type ExtensionsMap as E, createSuperAdminI18n as F, useSaMessages as G, useSaTheme as H, type InstallPlugin as I, useSuperAdminI18n as J, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SaThemeOptions as e, type SuperAdminI18n as f, type SaTheme as g, type ExtensionLoader as h, SA_LOCALE_STORAGE_KEY as i, SA_THEME_KEY as j, SUPER_ADMIN_ACTIONS_KEY as k, SUPER_ADMIN_BRAND_KEY as l, SUPER_ADMIN_ENDPOINTS_KEY as m, SUPER_ADMIN_EXTENSIONS_KEY as n, SUPER_ADMIN_HTTP_KEY as o, SUPER_ADMIN_I18N_KEY as p, SUPER_ADMIN_LOGIN_ADAPTER_KEY as q, SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY as r, SUPER_ADMIN_MANIFEST_KEY as s, SUPER_ADMIN_NOTIFY_KEY as t, type SaColorScheme as u, type SaResolvedScheme as v, type SuperAdminAuthGuardOptions as w, type SuperAdminLoginResult as x, type SuperAdminManifestGuardOptions as y, type UiNotifyKind as z };
@@ -280,4 +280,54 @@ declare function useSuperAdminI18n(): SuperAdminI18n;
280
280
  */
281
281
  declare function useSaMessages<K extends keyof SaMessages>(namespace: K): ComputedRef<SaMessages[K]>;
282
282
 
283
- export { type ActionsMap as A, type ExtensionsMap as E, type InstallPlugin as I, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SuperAdminI18n as e, type ExtensionLoader as f, SA_LOCALE_STORAGE_KEY as g, SUPER_ADMIN_ACTIONS_KEY as h, SUPER_ADMIN_BRAND_KEY as i, SUPER_ADMIN_ENDPOINTS_KEY as j, SUPER_ADMIN_EXTENSIONS_KEY as k, SUPER_ADMIN_HTTP_KEY as l, SUPER_ADMIN_I18N_KEY as m, SUPER_ADMIN_LOGIN_ADAPTER_KEY as n, SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY as o, SUPER_ADMIN_MANIFEST_KEY as p, SUPER_ADMIN_NOTIFY_KEY as q, type SuperAdminAuthGuardOptions as r, type SuperAdminLoginResult as s, type SuperAdminManifestGuardOptions as t, type UiNotifyKind as u, type UiNotifyOptions as v, buildNavigationGuard as w, createSuperAdminI18n as x, useSaMessages as y, useSuperAdminI18n as z };
283
+ type SaColorScheme = 'light' | 'dark' | 'system';
284
+ type SaResolvedScheme = 'light' | 'dark';
285
+ interface SaTheme {
286
+ /** What the operator picked. Writable — assigning switches the theme. */
287
+ scheme: Ref<SaColorScheme>;
288
+ /** What that means right now: `'system'` resolved against the OS. */
289
+ resolved: ComputedRef<SaResolvedScheme>;
290
+ /**
291
+ * Releases the `prefers-color-scheme` subscription and the persistence
292
+ * watcher. Idempotent.
293
+ *
294
+ * Needed because this context outlives no component — nothing unmounts it.
295
+ * A second shell in the same document (hot reload, a micro-frontend, a
296
+ * second test file) would otherwise leave the first one subscribed, and the
297
+ * next change of the operating system's theme would drive TWO bridges, both
298
+ * writing `data-sa-theme` and Quasar's `Dark` onto the one document. The
299
+ * stale one can win, and then the new shell's explicit scheme is overruled
300
+ * by a context nobody holds a reference to any more.
301
+ */
302
+ dispose: () => void;
303
+ }
304
+ interface SaThemeOptions {
305
+ /** Starting scheme. Pass a `Ref` to keep the value in your own store. */
306
+ scheme?: SaColorScheme | Ref<SaColorScheme>;
307
+ /**
308
+ * Remember the operator's pick across reloads, default `true`. Ignored when
309
+ * `scheme` is a `Ref`: the app owns that value and persists it where it
310
+ * belongs. Same rule as the locale in `createSuperAdminI18n`.
311
+ */
312
+ persist?: boolean;
313
+ /** Injection seam for tests. */
314
+ storage?: KvStore;
315
+ }
316
+ /**
317
+ * Creates the theme context. `createSuperAdminApp()` makes one and the Quasar
318
+ * bridge mirrors it onto the document; call it directly only when bootstrapping
319
+ * by hand.
320
+ */
321
+ declare function createSaTheme(options?: SaThemeOptions): SaTheme;
322
+ declare const SA_THEME_KEY: InjectionKey<SaTheme>;
323
+ /**
324
+ * The shell's theme context, or a shared unpersisted one outside a shell.
325
+ *
326
+ * Same shape as `useSuperAdminI18n()`, and for the same reason: a component
327
+ * mounted in isolation must render rather than throw, and the fallback must not
328
+ * write to real storage or two tests in one process would decide each other's
329
+ * outcome.
330
+ */
331
+ declare function useSaTheme(): SaTheme;
332
+
333
+ export { type ActionsMap as A, type UiNotifyOptions as B, buildNavigationGuard as C, createSaTheme as D, type ExtensionsMap as E, createSuperAdminI18n as F, useSaMessages as G, useSaTheme as H, type InstallPlugin as I, useSuperAdminI18n as J, type SuperAdminGuardOptions as S, type UiNotify as U, type SuperAdminBrand as a, type SuperAdminEndpoints as b, type SuperAdminLoginAdapter as c, type SuperAdminI18nOptions as d, type SaThemeOptions as e, type SuperAdminI18n as f, type SaTheme as g, type ExtensionLoader as h, SA_LOCALE_STORAGE_KEY as i, SA_THEME_KEY as j, SUPER_ADMIN_ACTIONS_KEY as k, SUPER_ADMIN_BRAND_KEY as l, SUPER_ADMIN_ENDPOINTS_KEY as m, SUPER_ADMIN_EXTENSIONS_KEY as n, SUPER_ADMIN_HTTP_KEY as o, SUPER_ADMIN_I18N_KEY as p, SUPER_ADMIN_LOGIN_ADAPTER_KEY as q, SUPER_ADMIN_MANIFEST_CLEAR_CACHE_KEY as r, SUPER_ADMIN_MANIFEST_KEY as s, SUPER_ADMIN_NOTIFY_KEY as t, type SaColorScheme as u, type SaResolvedScheme as v, type SuperAdminAuthGuardOptions as w, type SuperAdminLoginResult as x, type SuperAdminManifestGuardOptions as y, type UiNotifyKind as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/ui-vue",
3
- "version": "0.22.2",
3
+ "version": "0.24.0",
4
4
  "description": "Vue 3 components, resource clients and composables for the SuperAdmin UI shell. Provides boot and manifest loaders, navigation, actions and standard pages.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -41,6 +41,9 @@
41
41
  "./pages-standard/*": "./src/pages-standard/*",
42
42
  "./pages-tenant/*": "./src/pages-tenant/*",
43
43
  "./components/*": "./src/components/*",
44
+ "./theme.css": "./src/ui/theme/index.css",
45
+ "./theme/breakpoints": "./src/ui/theme/_breakpoints.scss",
46
+ "./theme/*": "./src/ui/theme/*",
44
47
  "./sa-theme.css": "./src/pages-standard/sa-theme.css",
45
48
  "./testing-e2e/*": {
46
49
  "import": {
@@ -58,7 +61,7 @@
58
61
  "src"
59
62
  ],
60
63
  "dependencies": {
61
- "@saasicat/types": "^0.22.2"
64
+ "@saasicat/types": "^0.24.0"
62
65
  },
63
66
  "peerDependencies": {
64
67
  "pinia": "^2.0.0 || ^3.0.0",
@@ -0,0 +1,129 @@
1
+ // The one categorical palette, and the one way to tint from it.
2
+ //
3
+ // Five components used to carry their own six-colour ramp of hex literals —
4
+ // `PlanList` and `PlanMatrix` byte for byte identical, `tenants/format.ts`
5
+ // silently different (the same plan got one colour on the plans page and
6
+ // another on the tenants page), `PromoCodesPage`, `MarketingPromotionsTab` and
7
+ // `discovery-ui.ts` each with a third opinion. Every one of them was applied
8
+ // through a `:style` binding, so none of them followed the theme: the plan mark
9
+ // measured 2.96:1 on a dark surface and was carried as a named contrast
10
+ // exception because there was no role to point it at.
11
+ //
12
+ // There is now one ramp, it lives in the theme, and it has a value per theme.
13
+ //
14
+ // A consumer may still hand in its own colours — `planAccents` is a public prop
15
+ // and takes any CSS colour. That keeps working because every helper here mixes
16
+ // rather than concatenates: `#7c3aed` + `'15'` needs a hex, and
17
+ // `color-mix(in srgb, X 8%, transparent)` needs only a colour, which a
18
+ // `var(--sa-color-identity-2)` also is.
19
+
20
+ /**
21
+ * The categorical ramp, in the order an unnamed series should walk it.
22
+ *
23
+ * Seven entries, six of them hues plus the neutral. Long enough that a realistic
24
+ * plan list does not wrap, short enough that the colours stay tellable apart —
25
+ * a longer ramp is not a better one, it is a wheel of near-neighbours.
26
+ */
27
+ export const IDENTITY_ACCENTS: readonly string[] = [
28
+ 'var(--sa-color-identity-1)',
29
+ 'var(--sa-color-identity-2)',
30
+ 'var(--sa-color-identity-3)',
31
+ 'var(--sa-color-identity-4)',
32
+ 'var(--sa-color-identity-5)',
33
+ 'var(--sa-color-identity-6)',
34
+ ];
35
+
36
+ /**
37
+ * The same ramp as concrete colours, for values that are STORED rather than
38
+ * painted.
39
+ *
40
+ * A promotion's colour is data: the operator picks it, it goes over the wire,
41
+ * a database column holds it, and `CreatePromotionDto` caps it at 16
42
+ * characters. `var(--sa-color-identity-1)` is 28 and would fail validation on
43
+ * every create — which is exactly what happened when the swatches above were
44
+ * pointed at the token form. A token is paint; this is a value.
45
+ *
46
+ * These are the LIGHT theme's values, because a stored colour has to mean one
47
+ * thing wherever it is later rendered. `identity-accents-match-theme.test.js`
48
+ * binds each one to its role, so the two halves of the ramp cannot drift.
49
+ */
50
+ export const IDENTITY_ACCENT_VALUES: readonly string[] = [
51
+ '#1d4ed8',
52
+ '#6d28d9',
53
+ '#047857',
54
+ '#b45309',
55
+ '#0369a1',
56
+ '#b91c1c',
57
+ ];
58
+
59
+ /** The stored counterpart of `IDENTITY_NEUTRAL`. */
60
+ export const IDENTITY_NEUTRAL_VALUE = '#64748b';
61
+
62
+ /** For "no identity yet", and for the entry tier that should stay quiet. */
63
+ export const IDENTITY_NEUTRAL = 'var(--sa-color-identity-neutral)';
64
+
65
+ /**
66
+ * The accent for the nth member of a series, wrapping when the series is longer
67
+ * than the ramp.
68
+ *
69
+ * A negative or non-finite index yields the neutral rather than throwing:
70
+ * callers derive it from `findIndex`, which answers -1 for "not in the list",
71
+ * and a missing row is exactly the "no identity yet" case.
72
+ */
73
+ export function identityAccentAt(index: number): string {
74
+ if (!Number.isFinite(index) || index < 0) return IDENTITY_NEUTRAL;
75
+ return IDENTITY_ACCENTS[Math.floor(index) % IDENTITY_ACCENTS.length]!;
76
+ }
77
+
78
+ /**
79
+ * The stable accent for a well-known key, so the standard plan tiers look the
80
+ * same on every screen that draws them.
81
+ *
82
+ * The tiers below are the ones the three former maps agreed on. `STARTER` and
83
+ * `BASIC` share the neutral because both are the entry tier — the earlier maps
84
+ * gave them two different greys, which said "these differ" about two things
85
+ * that do not.
86
+ */
87
+ const WELL_KNOWN: Readonly<Record<string, string>> = {
88
+ STARTER: IDENTITY_NEUTRAL,
89
+ BASIC: IDENTITY_NEUTRAL,
90
+ STANDARD: 'var(--sa-color-identity-1)',
91
+ PRO: 'var(--sa-color-identity-2)',
92
+ PROFESSIONAL: 'var(--sa-color-identity-2)',
93
+ BUSINESS: 'var(--sa-color-identity-5)',
94
+ ENTERPRISE: 'var(--sa-color-identity-3)',
95
+ };
96
+
97
+ /**
98
+ * The accent for a key, in the order the product decides it.
99
+ *
100
+ * 1. what the consumer supplied, because it is their brand;
101
+ * 2. the well-known tier, so `ENTERPRISE` is the same colour everywhere;
102
+ * 3. the ramp position, so an unknown key still gets a stable colour;
103
+ * 4. the neutral.
104
+ */
105
+ export function identityAccentFor(
106
+ key: string,
107
+ overrides: Readonly<Record<string, string>> = {},
108
+ index = -1,
109
+ ): string {
110
+ return overrides[key] ?? WELL_KNOWN[key] ?? identityAccentAt(index);
111
+ }
112
+
113
+ /**
114
+ * The three-part chip style every one of these sites was hand-rolling: a wash
115
+ * of the accent, the accent as text, a firmer edge of it.
116
+ *
117
+ * `color-mix()` rather than the `accent + '15'` string concatenation it
118
+ * replaces. That trick only worked on a six-digit hex — it silently produced
119
+ * garbage for `rgb()`, for a named colour and for anything a consumer might
120
+ * reasonably pass — and it could never have worked for a `var()`, which is why
121
+ * these palettes could not follow the theme in the first place.
122
+ */
123
+ export function identityChipStyle(accent: string): Record<string, string> {
124
+ return {
125
+ background: `color-mix(in srgb, ${accent} 8%, transparent)`,
126
+ color: accent,
127
+ borderColor: `color-mix(in srgb, ${accent} 20%, transparent)`,
128
+ };
129
+ }
@@ -26,3 +26,4 @@ export * from './admin-resource-client.js';
26
26
  export * from './i18n/index.js';
27
27
  export * from './login-branding.js';
28
28
  export * from './resolve-plans.js';
29
+ export * from './identity-accents.js';
@@ -300,8 +300,8 @@ function formatValue(v: unknown): string {
300
300
  gap: 16px;
301
301
  }
302
302
  .bvpd__label {
303
- font-size: 12px;
304
- color: var(--q-grey-7, #757575);
303
+ font-size: var(--sa-text-sm);
304
+ color: var(--sa-color-fg-muted);
305
305
  text-transform: uppercase;
306
306
  letter-spacing: 0.5px;
307
307
  margin-bottom: 6px;
@@ -313,18 +313,18 @@ function formatValue(v: unknown): string {
313
313
  padding: 32px;
314
314
  }
315
315
  .bvpd__warnings {
316
- border-left: 4px solid var(--q-warning, #f2c037);
316
+ border-left: 4px solid var(--sa-color-warning-strong);
317
317
  }
318
318
  .bvpd__neutral {
319
- border-left: 4px solid var(--q-grey-7, #757575);
319
+ border-left: 4px solid var(--sa-color-border-strong);
320
320
  }
321
321
  .bvpd__regression {
322
- border-left: 4px solid var(--q-negative, #c10015);
322
+ border-left: 4px solid var(--sa-color-negative);
323
323
  }
324
324
  .bvpd__validity {
325
325
  padding: 12px;
326
- background: var(--sa-bg-surface-2);
327
- border: 1px solid var(--sa-border);
326
+ background: var(--sa-color-bg-sunken);
327
+ border: 1px solid var(--sa-color-border);
328
328
  border-radius: 8px;
329
329
  }
330
330
  .bvpd__validity-grid {
@@ -335,36 +335,36 @@ function formatValue(v: unknown): string {
335
335
  .bvpd__field {
336
336
  display: grid;
337
337
  gap: 6px;
338
- font-size: 12px;
338
+ font-size: var(--sa-text-sm);
339
339
  font-weight: 700;
340
- color: var(--sa-muted-dark);
340
+ color: var(--sa-color-fg-secondary);
341
341
  }
342
342
  .bvpd__input {
343
343
  min-height: 36px;
344
- border: 1px solid #cbd5e1;
344
+ border: 1px solid var(--sa-color-border-strong);
345
345
  border-radius: 6px;
346
346
  padding: 0 10px;
347
347
  font: inherit;
348
- color: var(--sa-heading);
349
- background: #fff;
348
+ color: var(--sa-color-fg-heading);
349
+ background: var(--sa-color-bg-surface);
350
350
  }
351
351
  .bvpd__error {
352
352
  margin: 8px 0 0;
353
- color: var(--q-negative, #c10015);
354
- font-size: 12px;
353
+ color: var(--sa-color-negative);
354
+ font-size: var(--sa-text-sm);
355
355
  font-weight: 700;
356
356
  }
357
357
  .bvpd__list {
358
358
  margin: 8px 0 0;
359
359
  padding-left: 20px;
360
- font-size: 13px;
360
+ font-size: var(--sa-text-md);
361
361
  }
362
362
  .bvpd__old {
363
- color: var(--q-negative, #c10015);
363
+ color: var(--sa-color-negative);
364
364
  text-decoration: line-through;
365
365
  }
366
366
  .bvpd__new {
367
- color: var(--q-positive, #21ba45);
367
+ color: var(--sa-color-positive);
368
368
  font-weight: 600;
369
369
  }
370
370
  </style>