@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
@@ -0,0 +1,103 @@
1
+ /* Layer 1 — scales. Sizes, not colours, and identical in every theme.
2
+ *
3
+ * The point of a scale is that the choice is already made. You do not decide
4
+ * between 11px and 11.5px; you decide whether a piece of text is a caption or a
5
+ * body line, and the scale answers with a number. That is why the admin surface
6
+ * had 23 font sizes and 80 pixel values: with no scale, every author decided
7
+ * again, and nobody could tell a decision from a leftover.
8
+ *
9
+ * Breakpoints are the one scale that CANNOT live here — custom properties are
10
+ * not valid in a `@media` condition. They are in `_breakpoints.scss` for SCSS
11
+ * blocks, and for plain CSS the five values are an allow-list the lint enforces.
12
+ */
13
+
14
+ :root {
15
+ /* ── Spacing. A 4px base with two half-steps below it, because hairline
16
+ * gaps (2px) and icon insets (4px) are real and rounding them up moves
17
+ * every dense row in the product. ───────────────────────────────────── */
18
+ --sa-space-0: 0;
19
+ --sa-space-1: 2px;
20
+ --sa-space-2: 4px;
21
+ --sa-space-3: 8px;
22
+ --sa-space-4: 12px;
23
+ --sa-space-5: 16px;
24
+ --sa-space-6: 20px;
25
+ --sa-space-7: 24px;
26
+ --sa-space-8: 32px;
27
+ --sa-space-9: 40px;
28
+ --sa-space-10: 48px;
29
+ --sa-space-11: 64px;
30
+
31
+ /* Named spacings, so a rule can say what the gap is for rather than how
32
+ * big it is. These are the six the admin surface actually distinguishes. */
33
+ --sa-gap-inline: var(--sa-space-3);
34
+ --sa-gap-stack: var(--sa-space-5);
35
+ --sa-pad-page: var(--sa-space-6);
36
+ --sa-pad-section: var(--sa-space-6);
37
+ --sa-pad-control: var(--sa-space-3);
38
+ --sa-pad-cell: var(--sa-space-4);
39
+
40
+ /* ── Type. Eight steps, each with the line-height it was drawn for.
41
+ *
42
+ * The ladder is dense at the bottom because that is where the admin lives:
43
+ * table cells, chips and captions are three quarters of every page, and one
44
+ * pixel there is the difference between a dense list and a cramped one. It
45
+ * spreads out at the top, where the steps are titles and there are few. */
46
+ --sa-text-2xs: 10px;
47
+ --sa-text-xs: 11px;
48
+ --sa-text-sm: 12px;
49
+ --sa-text-md: 13px;
50
+ --sa-text-lg: 15px;
51
+ --sa-text-xl: 18px;
52
+ --sa-text-2xl: 22px;
53
+ --sa-text-3xl: 28px;
54
+ /* A ninth rung the plan did not foresee, added for a measured reason: the
55
+ * price totals and the marketing headline sit at 32–38px, and collapsing
56
+ * them into 28px would have shrunk the biggest number in the product to
57
+ * hit a target of eight. A step the product uses for a distinct job is a
58
+ * step. */
59
+ --sa-text-4xl: 32px;
60
+
61
+ --sa-leading-2xs: 1.4;
62
+ --sa-leading-xs: 1.45;
63
+ --sa-leading-sm: 1.5;
64
+ --sa-leading-md: 1.55;
65
+ --sa-leading-lg: 1.5;
66
+ --sa-leading-xl: 1.4;
67
+ --sa-leading-2xl: 1.3;
68
+ --sa-leading-3xl: 1.25;
69
+ --sa-leading-4xl: 1.15;
70
+
71
+ --sa-weight-regular: 400;
72
+ --sa-weight-medium: 500;
73
+ --sa-weight-semibold: 600;
74
+ --sa-weight-bold: 700;
75
+
76
+ --sa-tracking-tight: -0.02em;
77
+ --sa-tracking-normal: 0;
78
+ --sa-tracking-wide: 0.04em;
79
+ --sa-tracking-wider: 0.06em;
80
+
81
+ /* ── Corner radii, largest to smallest, so the nesting reads as hierarchy:
82
+ * a section is a bigger block than the cards inside it, a card bigger than
83
+ * a tile. `--sa-radius-tile` is the single declaration every statistic tile
84
+ * in the admin surface resolves to. ─────────────────────────────────── */
85
+ --sa-radius-hero: 16px;
86
+ --sa-radius-section: 14px;
87
+ --sa-radius-card: 12px;
88
+ --sa-radius-head: 10px;
89
+ --sa-radius-tile: 10px;
90
+ --sa-radius-field: 8px;
91
+ --sa-radius-control: 7px;
92
+ --sa-radius-pill: 999px;
93
+
94
+ /* ── Stacking. Aligned with Quasar's own layers rather than invented above
95
+ * them: a platform overlay that outranks Quasar's dialog would cover the
96
+ * dialog a consumer opened from it. ─────────────────────────────────── */
97
+ --sa-z-base: 0;
98
+ --sa-z-sticky: 100;
99
+ --sa-z-drawer: 3000;
100
+ --sa-z-overlay: 5900;
101
+ --sa-z-dialog: 6000;
102
+ --sa-z-toast: 9500;
103
+ }
@@ -0,0 +1,185 @@
1
+ /* Layer 2 — roles, dark theme. The same keys as `tokens.semantic.light.css`,
2
+ * with other values. That sentence is the whole design: nothing else in the
3
+ * package knows a dark theme exists.
4
+ *
5
+ * ── Two ways in, and why there is no third ─────────────────────────────────
6
+ *
7
+ * [data-sa-theme='dark'] the app decided — `handle.theme.scheme.value`
8
+ * body.body--dark Quasar decided — `$q.dark.set(true)`
9
+ *
10
+ * Both mean "the application decided", and that is the point: the application
11
+ * is the only party that can decide for BOTH halves of the screen. Quasar
12
+ * paints its own cards, dialogs, steppers and separators, and it does so only
13
+ * under `body--dark` — it has no `prefers-color-scheme` support of its own.
14
+ *
15
+ * ── Why this file does NOT follow the operating system ─────────────────────
16
+ *
17
+ * It used to, via `@media (prefers-color-scheme: dark)`, and that was wrong in
18
+ * the one case it was supposed to serve. An app that loads this stylesheet
19
+ * without running the bridge — every consumer embedding `pages-tenant/*` into
20
+ * its own frontend — would have had the platform's surfaces go dark from the
21
+ * operating system while Quasar's stayed light. Measured, on an embedded page
22
+ * with the OS set to dark:
23
+ *
24
+ * .sa-section rgb(15, 23, 42) ← dark
25
+ * .q-card rgb(255, 255, 255) ← white, with rgb(226, 232, 240) text
26
+ * .q-separator rgba(0, 0, 0, 0.12) ← a black hairline on near-black
27
+ *
28
+ * That is not "light cards beside dark surfaces", it is white-on-white at
29
+ * about 1.15:1. A stylesheet must not overrule the application that loaded it,
30
+ * because it cannot see the half of the screen it does not paint.
31
+ *
32
+ * Following the operating system is not lost — it moved to where both halves
33
+ * can be moved together. `createSaTheme({ scheme: 'system' })` is the default,
34
+ * it subscribes to `prefers-color-scheme` live, and `bindSaThemeToDocument`
35
+ * sets the attribute AND Quasar's `Dark`. `createSuperAdminApp` wires that up,
36
+ * so the admin shell follows the OS out of the box. An app that wants the same
37
+ * for an embedded surface calls those two functions; that is the one line that
38
+ * makes it actually work.
39
+ *
40
+ * `tests-e2e/theme-contrast.spec.ts` holds the counter-proof: with the OS set
41
+ * to dark and no bridge, the platform stays in step with Quasar.
42
+ */
43
+
44
+ [data-sa-theme='dark'],
45
+ body.body--dark {
46
+ --sa-color-bg-app: var(--sa-neutral-975);
47
+ --sa-color-bg-surface: var(--sa-neutral-900);
48
+ --sa-color-bg-surface-raised: var(--sa-neutral-800);
49
+ --sa-color-bg-sunken: var(--sa-neutral-950);
50
+ --sa-color-bg-overlay: color-mix(in srgb, var(--sa-black) 70%, transparent);
51
+
52
+ --sa-color-fg-heading: var(--sa-neutral-50);
53
+ --sa-color-fg-body: var(--sa-neutral-200);
54
+ --sa-color-fg-secondary: var(--sa-neutral-300);
55
+ --sa-color-fg-muted: var(--sa-neutral-400);
56
+ --sa-color-fg-subtle: var(--sa-neutral-500);
57
+ --sa-color-fg-disabled: var(--sa-neutral-600);
58
+ --sa-color-fg-on-accent: var(--sa-white);
59
+
60
+ --sa-color-border: var(--sa-neutral-700);
61
+ --sa-color-border-soft: var(--sa-neutral-800);
62
+ --sa-color-border-strong: var(--sa-neutral-600);
63
+ --sa-color-border-focus: var(--sa-color-accent);
64
+
65
+ /* The brand does not flip — a green product stays green in the dark. Only
66
+ * the derivations change: `-strong` lightens instead of darkening, because
67
+ * "more emphatic" means "closer to the paper" here, and every tint needs
68
+ * more of the accent to register against a dark surface. */
69
+ --sa-color-accent: var(--q-primary, var(--sa-brand-blue));
70
+ --sa-color-accent-strong: color-mix(in srgb, var(--sa-color-accent) 82%, var(--sa-white));
71
+ --sa-color-accent-surface: color-mix(in srgb, var(--sa-color-accent) 16%, transparent);
72
+ --sa-color-accent-surface-soft: color-mix(in srgb, var(--sa-color-accent) 8%, transparent);
73
+ --sa-color-accent-surface-strong: color-mix(
74
+ in srgb,
75
+ var(--sa-color-accent) 22%,
76
+ var(--sa-color-bg-surface)
77
+ );
78
+ --sa-color-accent-border: color-mix(in srgb, var(--sa-color-accent) 32%, transparent);
79
+
80
+ /* Each tone rises one or two steps up its ramp, because the dark shades
81
+ * that read as "deep red" on white read as "almost black" on slate. The
82
+ * surfaces become translucent so that a chip on a raised card and a chip on
83
+ * the page canvas both keep their contrast. */
84
+ --sa-color-positive: var(--sa-green-500);
85
+ --sa-color-positive-strong: var(--sa-green-500);
86
+ --sa-color-positive-fg: var(--sa-green-200);
87
+ --sa-color-positive-surface: color-mix(in srgb, var(--sa-green-500) 14%, transparent);
88
+ --sa-color-positive-surface-strong: color-mix(in srgb, var(--sa-green-500) 24%, transparent);
89
+ --sa-color-positive-border: color-mix(in srgb, var(--sa-green-500) 36%, transparent);
90
+
91
+ --sa-color-warning: var(--sa-amber-400);
92
+ --sa-color-warning-strong: var(--sa-amber-400);
93
+ --sa-color-warning-fg: var(--sa-amber-200);
94
+ --sa-color-warning-surface: color-mix(in srgb, var(--sa-amber-500) 14%, transparent);
95
+ --sa-color-warning-surface-strong: color-mix(in srgb, var(--sa-amber-500) 24%, transparent);
96
+ --sa-color-warning-border: color-mix(in srgb, var(--sa-amber-500) 36%, transparent);
97
+
98
+ --sa-color-negative: var(--sa-red-500);
99
+ --sa-color-negative-strong: var(--sa-red-500);
100
+ --sa-color-negative-fg: var(--sa-red-300);
101
+ --sa-color-negative-surface: color-mix(in srgb, var(--sa-red-500) 14%, transparent);
102
+ --sa-color-negative-surface-strong: color-mix(in srgb, var(--sa-red-500) 24%, transparent);
103
+ --sa-color-negative-border: color-mix(in srgb, var(--sa-red-500) 36%, transparent);
104
+
105
+ --sa-color-info: var(--sa-blue-500);
106
+ --sa-color-info-strong: var(--sa-blue-500);
107
+ --sa-color-info-fg: var(--sa-blue-200);
108
+ --sa-color-info-surface: color-mix(in srgb, var(--sa-blue-500) 14%, transparent);
109
+ --sa-color-info-surface-strong: color-mix(in srgb, var(--sa-blue-500) 24%, transparent);
110
+ --sa-color-info-border: color-mix(in srgb, var(--sa-blue-500) 36%, transparent);
111
+
112
+ --sa-color-scheduled: var(--sa-indigo-400);
113
+ --sa-color-scheduled-strong: var(--sa-indigo-400);
114
+ --sa-color-scheduled-fg: var(--sa-indigo-200);
115
+ --sa-color-scheduled-surface: color-mix(in srgb, var(--sa-indigo-400) 14%, transparent);
116
+ --sa-color-scheduled-surface-strong: color-mix(in srgb, var(--sa-indigo-400) 24%, transparent);
117
+ --sa-color-scheduled-border: color-mix(in srgb, var(--sa-indigo-400) 36%, transparent);
118
+
119
+ --sa-color-feature: var(--sa-violet-500);
120
+ --sa-color-feature-fg: var(--sa-violet-200);
121
+ --sa-color-feature-surface: color-mix(in srgb, var(--sa-violet-500) 16%, transparent);
122
+ --sa-color-quota: var(--sa-sky-500);
123
+ --sa-color-quota-surface: color-mix(in srgb, var(--sa-sky-500) 16%, transparent);
124
+ --sa-color-quota-border: color-mix(in srgb, var(--sa-sky-500) 36%, transparent);
125
+ --sa-color-bundle: var(--sa-amber-500);
126
+ --sa-color-bundle-surface: color-mix(in srgb, var(--sa-amber-500) 16%, transparent);
127
+
128
+ /* The identity ramp, one rung lighter throughout — see the light file for
129
+ * what it is for. This is the half that did not exist while the ramps lived
130
+ * as hex literals in TypeScript, and its absence is the whole reason the
131
+ * plan mark sat at 2.96:1 here. */
132
+ --sa-color-identity-1: var(--sa-blue-300);
133
+ --sa-color-identity-2: var(--sa-violet-200);
134
+ --sa-color-identity-3: var(--sa-green-200);
135
+ --sa-color-identity-4: var(--sa-amber-300);
136
+ --sa-color-identity-5: var(--sa-sky-200);
137
+ --sa-color-identity-6: var(--sa-red-300);
138
+ --sa-color-identity-neutral: var(--sa-neutral-400);
139
+
140
+ /* Identical to the light theme, and that is the point — see there. */
141
+ --sa-color-inverse-bg: var(--sa-neutral-950);
142
+ --sa-color-inverse-surface: var(--sa-neutral-800);
143
+ --sa-color-inverse-surface-soft: var(--sa-neutral-600);
144
+ --sa-color-inverse-fg: var(--sa-white);
145
+ --sa-color-inverse-fg-muted: var(--sa-neutral-300);
146
+ --sa-color-inverse-accent: var(--sa-amber-400);
147
+ --sa-color-inverse-accent-strong: var(--sa-amber-600);
148
+ --sa-color-inverse-accent-surface: color-mix(in srgb, var(--sa-amber-500) 15%, transparent);
149
+ --sa-color-inverse-accent-surface-strong: color-mix(
150
+ in srgb,
151
+ var(--sa-amber-500) 22%,
152
+ transparent
153
+ );
154
+ --sa-color-inverse-accent-fg: var(--sa-neutral-900);
155
+ --sa-color-inverse-danger: var(--sa-red-700);
156
+ --sa-color-inverse-danger-strong: var(--sa-red-600);
157
+ --sa-color-inverse-notice: var(--sa-amber-600);
158
+ --sa-color-inverse-notice-strong: var(--sa-amber-700);
159
+ --sa-color-inverse-fg-subtle: color-mix(in srgb, var(--sa-white) 55%, transparent);
160
+ --sa-color-inverse-border: color-mix(in srgb, var(--sa-white) 8%, transparent);
161
+ --sa-color-inverse-border-strong: color-mix(in srgb, var(--sa-white) 16%, transparent);
162
+
163
+ --sa-color-focus-ring: color-mix(in srgb, var(--sa-color-accent) 45%, transparent);
164
+ --sa-color-selection: color-mix(in srgb, var(--sa-color-accent) 30%, transparent);
165
+
166
+ /* Black ink and roughly double the alpha: a slate shadow at 6% is invisible
167
+ * on a slate surface, so the depth cue has to come from real darkness. */
168
+ --sa-shadow-ink: var(--sa-black);
169
+ --sa-shadow-tint-1: color-mix(in srgb, var(--sa-shadow-ink) 24%, transparent);
170
+ --sa-shadow-tint-2: color-mix(in srgb, var(--sa-shadow-ink) 32%, transparent);
171
+ --sa-shadow-tint-3: color-mix(in srgb, var(--sa-shadow-ink) 44%, transparent);
172
+ --sa-shadow-tint-4: color-mix(in srgb, var(--sa-shadow-ink) 60%, transparent);
173
+
174
+ --sa-shadow-0: none;
175
+ --sa-shadow-1: 0 1px 2px var(--sa-shadow-tint-1);
176
+ --sa-shadow-2: 0 2px 6px var(--sa-shadow-tint-2);
177
+ --sa-shadow-3: 0 8px 20px var(--sa-shadow-tint-3);
178
+ --sa-shadow-4: 0 20px 48px var(--sa-shadow-tint-4);
179
+ --sa-shadow-focus: 0 0 0 3px var(--sa-color-focus-ring);
180
+
181
+ --sa-elevation-tile: var(--sa-shadow-1);
182
+ --sa-elevation-card: var(--sa-shadow-2);
183
+ --sa-elevation-menu: var(--sa-shadow-3);
184
+ --sa-elevation-dialog: var(--sa-shadow-4);
185
+ }
@@ -0,0 +1,242 @@
1
+ /* Layer 2 — roles, light theme. What a colour is FOR.
2
+ *
3
+ * This is the only layer that changes between light and dark, and that is the
4
+ * whole reason the split exists: a dark theme is this file with other values,
5
+ * not 62 stylesheets with other values.
6
+ *
7
+ * Two rules, both enforced by the layer-discipline test (rule 22):
8
+ *
9
+ * 1. No colour literal may appear here. Every value is a primitive, a
10
+ * `--q-*` brand variable from the host, or a `color-mix()` of those.
11
+ * A literal here is a value nobody can find from the outside.
12
+ * 2. `tokens.semantic.dark.css` declares exactly the same keys. A role that
13
+ * exists in one theme and not the other is a page that goes unreadable the
14
+ * moment somebody flips the switch (rule 21).
15
+ *
16
+ * ── Where the accent comes from ────────────────────────────────────────────
17
+ *
18
+ * `--sa-color-accent` reads Quasar's `--q-primary`, which Quasar publishes from
19
+ * the host application's `$primary`. That is the fix for the worst finding in
20
+ * the audit: the platform painted `#3f6bff` while the scaffolded app painted
21
+ * `#1e40af`, in the same surface, because each had its own source of truth.
22
+ * Now there is one — the host's — and `--sa-brand-blue` is what it falls back
23
+ * to when the host declares nothing. Setting `$primary` re-tints the admin with
24
+ * it, and there is no second switch to remember.
25
+ *
26
+ * To change the brand AT RUNTIME, write to the root:
27
+ *
28
+ * setCssVar('primary', value, document.documentElement)
29
+ *
30
+ * The third argument is not optional here, and the reason is the same one that
31
+ * shapes the rest of this file: Quasar's `setCssVar` defaults to `<body>`, and
32
+ * this role is computed on `:root`, one level above it. A custom property is
33
+ * resolved where it is DECLARED, so the root substitution never sees a value
34
+ * written below it — Quasar's own components would recolour and the admin would
35
+ * not. Measured, and pinned by a test.
36
+ *
37
+ * Tints derive from the accent with `color-mix()` for the same reason: a fixed
38
+ * `rgba(63, 107, 255, 0.08)` would stay platform-blue on a host that is green.
39
+ */
40
+
41
+ :root {
42
+ /* ── Surfaces, from the page canvas up to what floats above it. ─────── */
43
+ --sa-color-bg-app: var(--sa-neutral-100);
44
+ --sa-color-bg-surface: var(--sa-white);
45
+ --sa-color-bg-surface-raised: var(--sa-neutral-25);
46
+ --sa-color-bg-sunken: var(--sa-neutral-50);
47
+ --sa-color-bg-overlay: color-mix(in srgb, var(--sa-neutral-900) 55%, transparent);
48
+
49
+ /* ── Text, from the loudest to the quietest.
50
+ *
51
+ * Six rungs, not five, because the surface uses six. `-secondary` was very
52
+ * nearly folded into `-muted` on the argument that two greys one step apart
53
+ * are a duplicate — the baseline said otherwise: it carries the figures and
54
+ * meta lines of over a hundred elements, and reading it one step lighter
55
+ * made them quieter than the captions beside them. A rung the product uses
56
+ * for a distinct job is not a duplicate, it is a rung. ───────────────── */
57
+ --sa-color-fg-heading: var(--sa-neutral-900);
58
+ --sa-color-fg-body: var(--sa-neutral-800);
59
+ --sa-color-fg-secondary: var(--sa-neutral-600);
60
+ --sa-color-fg-muted: var(--sa-neutral-500);
61
+ --sa-color-fg-subtle: var(--sa-neutral-450);
62
+ --sa-color-fg-disabled: var(--sa-neutral-300);
63
+ /* Text lying on the accent. White, and that is an ASSUMPTION about your
64
+ * brand rather than a derivation from it: CSS cannot branch on a colour's
65
+ * luminance, so nothing here can notice that `$primary` is a light amber
66
+ * and white on it reads 2.15:1. `contrast-color()` will answer this without
67
+ * JavaScript once it is safe to ship; until then it is a knob.
68
+ *
69
+ * If your brand is light, set this — in BOTH themes, like any role
70
+ * (docs/design-guide.md#overriding-a-role):
71
+ *
72
+ * :root, [data-sa-theme='dark'], body.body--dark {
73
+ * --sa-color-fg-on-accent: #1a1300;
74
+ * }
75
+ */
76
+ --sa-color-fg-on-accent: var(--sa-white);
77
+
78
+ /* ── Lines. ─────────────────────────────────────────────────────────── */
79
+ --sa-color-border: var(--sa-neutral-200);
80
+ --sa-color-border-soft: var(--sa-neutral-100);
81
+ --sa-color-border-strong: var(--sa-neutral-300);
82
+ --sa-color-border-focus: var(--sa-color-accent);
83
+
84
+ /* ── Accent — the host's brand, and the surfaces derived from it. ───── */
85
+ --sa-color-accent: var(--q-primary, var(--sa-brand-blue));
86
+ --sa-color-accent-strong: color-mix(in srgb, var(--sa-color-accent) 82%, var(--sa-black));
87
+ --sa-color-accent-surface: color-mix(in srgb, var(--sa-color-accent) 8%, transparent);
88
+ --sa-color-accent-surface-soft: color-mix(in srgb, var(--sa-color-accent) 4%, transparent);
89
+ --sa-color-accent-surface-strong: color-mix(
90
+ in srgb,
91
+ var(--sa-color-accent) 10%,
92
+ var(--sa-color-bg-surface)
93
+ );
94
+ --sa-color-accent-border: color-mix(in srgb, var(--sa-color-accent) 18%, transparent);
95
+
96
+ /* ── Status tones. Five, and all five have the same six slots, so a page
97
+ * that knows one knows the others:
98
+ *
99
+ * <tone> the solid graphic colour — an icon, a bar, a dot
100
+ * <tone>-strong the brighter variant, for a selected outline
101
+ * <tone>-fg text ON the tinted surface, contrast-checked
102
+ * <tone>-surface the quiet tint — a banner across a section
103
+ * <tone>-surface-strong the loud tint — a chip, a pill, a single cell
104
+ * <tone>-border the tinted line that closes either surface
105
+ * ──────────────────────────────────────────────────────────────────── */
106
+ --sa-color-positive: var(--sa-green-700);
107
+ --sa-color-positive-strong: var(--sa-green-500);
108
+ --sa-color-positive-fg: var(--sa-green-700);
109
+ --sa-color-positive-surface: var(--sa-green-50);
110
+ --sa-color-positive-surface-strong: var(--sa-green-100);
111
+ --sa-color-positive-border: var(--sa-green-200);
112
+
113
+ --sa-color-warning: var(--sa-amber-700);
114
+ --sa-color-warning-strong: var(--sa-amber-500);
115
+ --sa-color-warning-fg: var(--sa-amber-700);
116
+ --sa-color-warning-surface: var(--sa-amber-50);
117
+ --sa-color-warning-surface-strong: var(--sa-amber-100);
118
+ --sa-color-warning-border: var(--sa-amber-200);
119
+
120
+ --sa-color-negative: var(--sa-red-600);
121
+ --sa-color-negative-strong: var(--sa-red-500);
122
+ --sa-color-negative-fg: var(--sa-red-700);
123
+ --sa-color-negative-surface: var(--sa-red-50);
124
+ --sa-color-negative-surface-strong: var(--sa-red-100);
125
+ --sa-color-negative-border: var(--sa-red-200);
126
+
127
+ --sa-color-info: var(--sa-blue-600);
128
+ --sa-color-info-strong: var(--sa-blue-500);
129
+ --sa-color-info-fg: var(--sa-blue-700);
130
+ --sa-color-info-surface: var(--sa-blue-50);
131
+ --sa-color-info-surface-strong: var(--sa-blue-100);
132
+ --sa-color-info-border: var(--sa-blue-200);
133
+
134
+ /* `scheduled` is a tone rather than a status colour reused: a version that
135
+ * is planned but not yet live is neither a warning nor an achievement, and
136
+ * six surfaces had each picked their own blue-ish colour to say so. */
137
+ --sa-color-scheduled: var(--sa-indigo-700);
138
+ --sa-color-scheduled-strong: var(--sa-indigo-400);
139
+ --sa-color-scheduled-fg: var(--sa-indigo-700);
140
+ --sa-color-scheduled-surface: var(--sa-indigo-50);
141
+ --sa-color-scheduled-surface-strong: var(--sa-indigo-100);
142
+ --sa-color-scheduled-border: var(--sa-indigo-200);
143
+
144
+ /* ── Catalogue entities. Not statuses: these two say WHAT a row is, and
145
+ * the plan editor, the matrix, the review and the discovery page all need
146
+ * to agree, or the same dot means two things on two screens. ────────── */
147
+ --sa-color-feature: var(--sa-violet-500);
148
+ --sa-color-feature-fg: var(--sa-violet-700);
149
+ --sa-color-feature-surface: var(--sa-violet-50);
150
+ --sa-color-quota: var(--sa-sky-500);
151
+ --sa-color-quota-surface: var(--sa-sky-50);
152
+ --sa-color-quota-border: var(--sa-sky-200);
153
+ --sa-color-bundle: var(--sa-amber-500);
154
+ --sa-color-bundle-surface: var(--sa-amber-100);
155
+
156
+ /* ── Identity. A CATEGORICAL ramp: the only job of these seven is to be
157
+ * told apart.
158
+ *
159
+ * Use them when a colour distinguishes one row from the next and means
160
+ * nothing beyond that — a plan's mark, a promotion badge, a capability
161
+ * kind. Use `feature`/`quota`/`bundle`/`scheduled` above when the category
162
+ * carries meaning; a reader should never have to guess which of the two a
163
+ * colour is doing.
164
+ *
165
+ * They exist because the package had FIVE of these ramps, all six colours
166
+ * long, none agreeing: `PlanList` and `PlanMatrix` shared one byte for
167
+ * byte, `tenants/format.ts` had a third that gave the same plan a
168
+ * different colour on the tenants page, and `PromoCodesPage`,
169
+ * `MarketingPromotionsTab` and `discovery-ui.ts` each invented their own.
170
+ * Every one of them was a hex literal in TypeScript, so none followed the
171
+ * theme: the plan mark measured 2.96:1 on a dark surface and was carried
172
+ * as a named contrast exception because there was nowhere to fix it.
173
+ *
174
+ * Each rung is chosen to clear 3:1 as TEXT on this theme's card surface,
175
+ * because that is how they are used — as the label, not only as a dot. */
176
+ --sa-color-identity-1: var(--sa-blue-700);
177
+ --sa-color-identity-2: var(--sa-violet-700);
178
+ --sa-color-identity-3: var(--sa-green-700);
179
+ --sa-color-identity-4: var(--sa-amber-700);
180
+ --sa-color-identity-5: var(--sa-sky-700);
181
+ --sa-color-identity-6: var(--sa-red-700);
182
+ /* The entry tier, and the answer for "no identity yet". Quiet on purpose:
183
+ * a starter plan reading as loud as an enterprise one is a wrong signal. */
184
+ --sa-color-identity-neutral: var(--sa-neutral-500);
185
+
186
+ /* ── Inverse chrome — the dark header, drawer and login backdrop.
187
+ *
188
+ * Declared identically in both themes ON PURPOSE. This surface is dark in
189
+ * a light theme too, so a role that flipped would turn the drawer's labels
190
+ * black on black the moment somebody enabled dark mode. Same reasoning as
191
+ * for the brand accent: what is dark by design does not follow the theme.
192
+ * ──────────────────────────────────────────────────────────────────── */
193
+ --sa-color-inverse-bg: var(--sa-neutral-950);
194
+ --sa-color-inverse-surface: var(--sa-neutral-800);
195
+ --sa-color-inverse-surface-soft: var(--sa-neutral-600);
196
+ --sa-color-inverse-fg: var(--sa-white);
197
+ --sa-color-inverse-fg-muted: var(--sa-neutral-300);
198
+ --sa-color-inverse-accent: var(--sa-amber-400);
199
+ --sa-color-inverse-accent-strong: var(--sa-amber-600);
200
+ --sa-color-inverse-accent-surface: color-mix(in srgb, var(--sa-amber-500) 15%, transparent);
201
+ --sa-color-inverse-accent-surface-strong: color-mix(
202
+ in srgb,
203
+ var(--sa-amber-500) 22%,
204
+ transparent
205
+ );
206
+ --sa-color-inverse-accent-fg: var(--sa-neutral-900);
207
+ --sa-color-inverse-danger: var(--sa-red-700);
208
+ --sa-color-inverse-danger-strong: var(--sa-red-600);
209
+ --sa-color-inverse-notice: var(--sa-amber-600);
210
+ --sa-color-inverse-notice-strong: var(--sa-amber-700);
211
+ --sa-color-inverse-fg-subtle: color-mix(in srgb, var(--sa-white) 55%, transparent);
212
+ --sa-color-inverse-border: color-mix(in srgb, var(--sa-white) 8%, transparent);
213
+ --sa-color-inverse-border-strong: color-mix(in srgb, var(--sa-white) 16%, transparent);
214
+
215
+ /* ── Focus and selection. ───────────────────────────────────────────── */
216
+ --sa-color-focus-ring: color-mix(in srgb, var(--sa-color-accent) 35%, transparent);
217
+ --sa-color-selection: color-mix(in srgb, var(--sa-color-accent) 22%, transparent);
218
+
219
+ /* ── Shadow. One ink, four tints, five elevations.
220
+ *
221
+ * The ink is a token rather than black because a slate-tinted shadow reads
222
+ * as depth on a slate-tinted page, while pure black reads as dirt. In the
223
+ * dark theme it becomes black and the alphas rise, because a light shadow
224
+ * on a dark surface is invisible at 6%. ─────────────────────────────── */
225
+ --sa-shadow-ink: var(--sa-neutral-900);
226
+ --sa-shadow-tint-1: color-mix(in srgb, var(--sa-shadow-ink) 6%, transparent);
227
+ --sa-shadow-tint-2: color-mix(in srgb, var(--sa-shadow-ink) 8%, transparent);
228
+ --sa-shadow-tint-3: color-mix(in srgb, var(--sa-shadow-ink) 12%, transparent);
229
+ --sa-shadow-tint-4: color-mix(in srgb, var(--sa-shadow-ink) 20%, transparent);
230
+
231
+ --sa-shadow-0: none;
232
+ --sa-shadow-1: 0 1px 2px var(--sa-shadow-tint-1);
233
+ --sa-shadow-2: 0 2px 6px var(--sa-shadow-tint-2);
234
+ --sa-shadow-3: 0 8px 20px var(--sa-shadow-tint-3);
235
+ --sa-shadow-4: 0 20px 48px var(--sa-shadow-tint-4);
236
+ --sa-shadow-focus: 0 0 0 3px var(--sa-color-focus-ring);
237
+
238
+ --sa-elevation-tile: var(--sa-shadow-1);
239
+ --sa-elevation-card: var(--sa-shadow-2);
240
+ --sa-elevation-menu: var(--sa-shadow-3);
241
+ --sa-elevation-dialog: var(--sa-shadow-4);
242
+ }