@saasicat/ui-vue 0.22.2 → 0.23.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 (108) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-LY52T4N4.js → chunk-TQQMKWN6.js} +58 -1
  3. package/dist/index.cjs +213 -159
  4. package/dist/index.d.cts +2 -2
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.js +7 -1
  7. package/dist/quasar/index.cjs +116 -20
  8. package/dist/quasar/index.d.cts +48 -3
  9. package/dist/quasar/index.d.ts +48 -3
  10. package/dist/quasar/index.js +58 -3
  11. package/dist/{use-super-admin-i18n-CgTuNi8h.d.cts → use-sa-theme-B5t7oXph.d.cts} +51 -1
  12. package/dist/{use-super-admin-i18n-CHNdsSlm.d.ts → use-sa-theme-OUBlaqgl.d.ts} +51 -1
  13. package/package.json +5 -2
  14. package/src/components/BundleVersionPublishDialog.vue +17 -17
  15. package/src/components/KvBlock.vue +6 -6
  16. package/src/components/MarketingPromotionsTab.vue +78 -78
  17. package/src/components/MfaPromptDialog.vue +2 -2
  18. package/src/components/TenantActionConfirmDialog.vue +5 -5
  19. package/src/components/VersionDiffPreview.vue +18 -18
  20. package/src/components/bundle-editor/BundleCreatePanel.vue +52 -52
  21. package/src/components/bundle-editor/BundleFeaturesEditor.vue +20 -20
  22. package/src/components/bundle-editor/BundlePlanCompatPicker.vue +33 -33
  23. package/src/components/bundle-editor/BundleQuotasEditor.vue +26 -26
  24. package/src/components/bundle-editor/BundleStatusBanner.vue +21 -21
  25. package/src/components/bundle-editor/BundleVersionInlineEditor.vue +48 -48
  26. package/src/components/bundle-editor/BundleVersionStrip.vue +30 -30
  27. package/src/components/dialogs/PilotCreateDialog.vue +20 -20
  28. package/src/components/dialogs/PilotEditDialog.vue +6 -6
  29. package/src/components/dialogs/PromoCodeCreateDialog.vue +9 -9
  30. package/src/components/dialogs/PromoCodeDialogFields.vue +61 -61
  31. package/src/components/dialogs/PromoCodeEditDialog.vue +11 -11
  32. package/src/components/dialogs/pilot-dialog.css +48 -48
  33. package/src/components/plan/PlanCycleToggle.vue +9 -16
  34. package/src/components/plan/PlanGrid.vue +30 -34
  35. package/src/components/plan/PriceSummary.vue +35 -35
  36. package/src/components/plan/PromoCodeInput.vue +26 -26
  37. package/src/components/plan/PublicBundleGrid.vue +32 -28
  38. package/src/components/plan-create-dialog/PlanCreateDialog.vue +49 -49
  39. package/src/components/plan-detail/PlanDetail.vue +172 -156
  40. package/src/components/plan-detail/PlanTitleEdit.vue +11 -11
  41. package/src/components/plan-list/PlanList.vue +98 -77
  42. package/src/components/plan-matrix/PlanMatrix.vue +89 -89
  43. package/src/components/plan-review/PlanReview.vue +60 -56
  44. package/src/components/plan-version-editor/PlanVersionEditor.vue +224 -200
  45. package/src/components/wizard-stepper/WizardStepper.vue +17 -17
  46. package/src/index.ts +1 -0
  47. package/src/pages-standard/AdminLayout.vue +58 -37
  48. package/src/pages-standard/AdminManifestErrorPage.vue +6 -6
  49. package/src/pages-standard/AuditPage.vue +3 -3
  50. package/src/pages-standard/BundlesPage.vue +36 -36
  51. package/src/pages-standard/DashboardPage.vue +17 -17
  52. package/src/pages-standard/DiscoveryPage.vue +55 -49
  53. package/src/pages-standard/EmailHistoryPage.vue +53 -71
  54. package/src/pages-standard/MarketingCatalogPage.vue +226 -218
  55. package/src/pages-standard/PilotsPage.vue +7 -6
  56. package/src/pages-standard/PlatformEmailPage.vue +18 -18
  57. package/src/pages-standard/PromoCodeDetailPage.vue +3 -3
  58. package/src/pages-standard/PromoCodesPage.vue +7 -6
  59. package/src/pages-standard/SuperAdminLoginPage.vue +39 -25
  60. package/src/pages-standard/SuperAdminSetupWizard.vue +44 -33
  61. package/src/pages-standard/TenantDetailPage.vue +7 -7
  62. package/src/pages-standard/TenantsPage.vue +11 -46
  63. package/src/pages-standard/bundles-page/BundleAccordionList.vue +13 -13
  64. package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +24 -24
  65. package/src/pages-standard/discovery-page/DiscoveryCapList.vue +22 -22
  66. package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +39 -39
  67. package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +26 -26
  68. package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +1 -1
  69. package/src/pages-standard/plan-versions/PlanDiffCard.vue +16 -16
  70. package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +21 -15
  71. package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +45 -45
  72. package/src/pages-standard/plans-page/PlanBundleOverview.vue +21 -15
  73. package/src/pages-standard/plans-page/PlanPublishDialog.vue +4 -4
  74. package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
  75. package/src/pages-standard/sa-theme.css +9 -687
  76. package/src/pages-standard/tenants/StatusPill.vue +13 -13
  77. package/src/pages-tenant/LimitsRow.vue +4 -4
  78. package/src/pages-tenant/MySubscriptionBundlesPage.vue +61 -61
  79. package/src/pages-tenant/OnboardingConfigurator.vue +24 -24
  80. package/src/pages-tenant/PackageSnapshotPanel.vue +10 -16
  81. package/src/pages-tenant/PendingVersionBanner.vue +12 -12
  82. package/src/pages-tenant/PlanChangeWizard.vue +20 -27
  83. package/src/pages-tenant/TenantPlanSection.vue +13 -21
  84. package/src/pages-tenant/UsageBar.vue +7 -7
  85. package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +14 -14
  86. package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +10 -10
  87. package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +2 -2
  88. package/src/quasar/create-super-admin-app.ts +96 -1
  89. package/src/quasar/dark-bridge.ts +66 -0
  90. package/src/quasar/index.ts +1 -0
  91. package/src/ui/theme/_breakpoints.scss +28 -0
  92. package/src/ui/theme/base.css +50 -0
  93. package/src/ui/theme/components/body.css +23 -0
  94. package/src/ui/theme/components/button.css +158 -0
  95. package/src/ui/theme/components/card.css +48 -0
  96. package/src/ui/theme/components/field.css +38 -0
  97. package/src/ui/theme/components/hero.css +46 -0
  98. package/src/ui/theme/components/paginator.css +63 -0
  99. package/src/ui/theme/components/section.css +67 -0
  100. package/src/ui/theme/components/statistics.css +189 -0
  101. package/src/ui/theme/components/table.css +60 -0
  102. package/src/ui/theme/index.css +42 -0
  103. package/src/ui/theme/tokens.legacy.css +147 -0
  104. package/src/ui/theme/tokens.primitive.css +108 -0
  105. package/src/ui/theme/tokens.scale.css +103 -0
  106. package/src/ui/theme/tokens.semantic.dark.css +173 -0
  107. package/src/ui/theme/tokens.semantic.light.css +212 -0
  108. package/src/vue/use-sa-theme.ts +154 -0
@@ -0,0 +1,60 @@
1
+ /* Lists (see AdminTable). The package shipped no table rules at all until
2
+ * this, which is why nine pages each looked slightly different.
3
+ */
4
+
5
+ /* The surface under the rows. Measured on the promo-codes page in dark mode:
6
+ * the `.q-table__container.q-table__card.q-dark` element resolved to
7
+ * rgb(29, 29, 29) while the section around it was rgb(15, 23, 42) — Quasar's
8
+ * neutral grey inside our slate. Only `thead th` and `tbody td` were ours, and
9
+ * neither of them is the table's background. */
10
+ .sa-page .q-table__container,
11
+ .sa-portal .q-table__container {
12
+ background: var(--sa-color-bg-surface);
13
+ color: var(--sa-color-fg-body);
14
+ }
15
+
16
+ .sa-page .q-table thead th {
17
+ font-size: var(--sa-text-xs);
18
+ font-weight: 700;
19
+ letter-spacing: 0.04em;
20
+ text-transform: uppercase;
21
+ color: var(--sa-color-fg-muted);
22
+ background: var(--sa-color-bg-sunken);
23
+ border-bottom: 1px solid var(--sa-color-border);
24
+ padding: 10px 14px;
25
+ white-space: nowrap;
26
+ }
27
+ .sa-page .q-table tbody td {
28
+ font-size: var(--sa-text-md);
29
+ color: var(--sa-color-fg-body);
30
+ border-bottom: 1px solid var(--sa-color-border-soft);
31
+ padding: 10px 14px;
32
+ }
33
+ .sa-page .q-table tbody tr:last-child td {
34
+ border-bottom: 0;
35
+ }
36
+ .sa-page .q-table tbody tr:hover td {
37
+ background: var(--sa-color-bg-sunken);
38
+ }
39
+ /* Sorted column: the header carries the state, not just the arrow. */
40
+ .sa-page .q-table thead th.sortable:hover,
41
+ .sa-page .q-table thead th[aria-sort]:not([aria-sort='none']) {
42
+ color: var(--sa-color-fg-heading);
43
+ }
44
+ .sa-page .q-table thead th[aria-sort]:not([aria-sort='none']) {
45
+ background: var(--sa-color-accent-surface-strong);
46
+ }
47
+ .sa-table__actions {
48
+ white-space: nowrap;
49
+ }
50
+ .sa-table__empty {
51
+ color: var(--sa-color-fg-muted);
52
+ }
53
+
54
+ /* The pager sits below the list, separated by a line that runs the full width
55
+ * of the surface — hence the pull-out by the section's own padding token. */
56
+ .sa-table__foot {
57
+ margin: 0 calc(-1 * var(--sa-pad-section)) calc(-1 * var(--sa-pad-section));
58
+ padding: 0 var(--sa-pad-section) 10px;
59
+ border-top: 1px solid var(--sa-color-border);
60
+ }
@@ -0,0 +1,42 @@
1
+ /* The SaaSiCat admin theme. One import: `@saasicat/ui-vue/theme.css`.
2
+ *
3
+ * ── The three layers, and why there are three ──────────────────────────────
4
+ *
5
+ * L1 tokens.primitive.css the palette references nothing
6
+ * tokens.scale.css space/type/… references nothing
7
+ * L2 tokens.semantic.*.css roles references L1 and --q-*
8
+ * L3 components/*.css the shell chrome references L2
9
+ *
10
+ * A component asks for a ROLE (`--sa-color-fg-muted`), never for a value
11
+ * (`--sa-neutral-500`), and never for a literal. That one rule is what makes
12
+ * the dark theme a file instead of a project: only L2 knows the difference
13
+ * between light and dark, so only L2 has two versions.
14
+ *
15
+ * The direction is enforced by `tests/theme-layer-discipline.test.js`
16
+ * (rule 22) and the key parity of the two L2 files by
17
+ * `tests/theme-token-parity.test.js` (rule 21).
18
+ *
19
+ * ── Order is the cascade ───────────────────────────────────────────────────
20
+ *
21
+ * Tokens first, because everything below reads them; dark after light, because
22
+ * it overrides. Within `components/`, the order is the order the old single
23
+ * stylesheet had — several rules there share a specificity and settle it by
24
+ * position, so this list is not alphabetical and must not be sorted.
25
+ */
26
+
27
+ @import './tokens.primitive.css';
28
+ @import './tokens.scale.css';
29
+ @import './tokens.semantic.light.css';
30
+ @import './tokens.semantic.dark.css';
31
+ @import './tokens.legacy.css';
32
+
33
+ @import './base.css';
34
+ @import './components/hero.css';
35
+ @import './components/button.css';
36
+ @import './components/field.css';
37
+ @import './components/statistics.css';
38
+ @import './components/body.css';
39
+ @import './components/section.css';
40
+ @import './components/card.css';
41
+ @import './components/paginator.css';
42
+ @import './components/table.css';
@@ -0,0 +1,147 @@
1
+ /* The pre-role token names, kept alive as aliases for one release.
2
+ *
3
+ * Every name below is what `sa-theme.css` published before the role layer
4
+ * existed. They are aliases, not values: each resolves to the role it turned
5
+ * out to mean, so a consumer stylesheet that still says `var(--sa-muted)` keeps
6
+ * working — and starts following the dark theme for free.
7
+ *
8
+ * ── They are aliases in ONE direction ──────────────────────────────────────
9
+ *
10
+ * Reading an old name still works. OVERRIDING one no longer does: setting
11
+ * `--sa-primary` in your own `:root` used to re-tint the admin, and now the
12
+ * role it feeds is `--sa-color-accent`, which reads Quasar's `--q-primary`.
13
+ * The replacement is better than what it removes — set `$primary` and the
14
+ * admin, Quasar's own components and the tenant-facing pages all follow one
15
+ * value instead of three. At runtime, use
16
+ * `setCssVar('primary', value, document.documentElement)`; the default target
17
+ * is `<body>`, which the roles on `:root` cannot see.
18
+ *
19
+ * To override a ROLE rather than the brand, write both of its values, on the
20
+ * same selectors the theme uses — `:root` for light, and
21
+ * `[data-sa-theme='dark'], body.body--dark` for dark. A role has two values, so
22
+ * an override has two, and `:root` alone silently loses in the Quasar-triggered
23
+ * dark path. `docs/design-guide.md#overriding-a-role` has the measurements.
24
+ *
25
+ * Nothing in this repository or in the scaffolder template overrode an `--sa-*`
26
+ * name; that was checked before choosing this direction.
27
+ *
28
+ * ── Consolidations, so a moved colour is not a surprise ────────────────────
29
+ *
30
+ * Five names had no role of their own and now share one, which is the point of
31
+ * the exercise rather than a side effect:
32
+ *
33
+ * --sa-border-strong #d1d5db → --sa-color-border-strong (#cbd5e1, the slate
34
+ * ramp — the grey ramp had exactly this one member)
35
+ * --sa-positive-soft, --sa-warning-soft translucent tints → the opaque
36
+ * `-surface` roles, so they stack predictably
37
+ * --sa-amber-light #fbbf24 → --sa-color-warning-strong (#f59e0b)
38
+ * --sa-primary-50 #eff6ff → --sa-color-accent-surface-strong, which now
39
+ * follows the host's brand instead of staying blue
40
+ *
41
+ * This file is deleted in 1.0. Its contents are the migration checklist.
42
+ */
43
+
44
+ :root {
45
+ --sa-bg-app: var(--sa-color-bg-app);
46
+ --sa-bg-surface: var(--sa-color-bg-surface);
47
+ --sa-bg-surface-2: var(--sa-color-bg-sunken);
48
+
49
+ --sa-primary: var(--sa-color-accent);
50
+ --sa-primary-strong: var(--sa-color-accent-strong);
51
+ --sa-primary-soft: var(--sa-color-accent-surface);
52
+ --sa-primary-softer: var(--sa-color-accent-surface-soft);
53
+ --sa-primary-border: var(--sa-color-accent-border);
54
+ --sa-primary-50: var(--sa-color-accent-surface-strong);
55
+
56
+ --sa-positive: var(--sa-color-positive);
57
+ --sa-positive-light: var(--sa-color-positive-strong);
58
+ --sa-positive-soft: var(--sa-color-positive-surface);
59
+ --sa-positive-bg: var(--sa-color-positive-surface);
60
+
61
+ --sa-negative: var(--sa-color-negative);
62
+ --sa-negative-soft: var(--sa-color-negative-surface);
63
+
64
+ --sa-warning: var(--sa-color-warning);
65
+ --sa-warning-soft: var(--sa-color-warning-surface);
66
+ --sa-warning-bg: var(--sa-color-warning-surface);
67
+ --sa-amber: var(--sa-color-warning-strong);
68
+ --sa-amber-light: var(--sa-color-warning-strong);
69
+ --sa-amber-border: var(--sa-color-warning-border);
70
+
71
+ /* Read by `TenantsPage` as `var(--sa-accent, #7c3aed)` and never declared
72
+ * anywhere — the fallback was the only value it ever had. It marked the
73
+ * catalogue-feature action, which is now a role. */
74
+ --sa-accent: var(--sa-color-feature);
75
+
76
+ --sa-heading: var(--sa-color-fg-heading);
77
+ --sa-body: var(--sa-color-fg-body);
78
+ --sa-muted: var(--sa-color-fg-muted);
79
+ --sa-muted-dark: var(--sa-color-fg-secondary);
80
+ --sa-muted-light: var(--sa-color-fg-subtle);
81
+
82
+ --sa-border: var(--sa-color-border);
83
+ --sa-border-soft: var(--sa-color-border-soft);
84
+ --sa-border-strong: var(--sa-color-border-strong);
85
+
86
+ --sa-section-pad: var(--sa-pad-section);
87
+ }
88
+
89
+ /* The same declarations again, under the dark triggers.
90
+ *
91
+ * Not redundancy — an alias is computed WHERE it is declared. `--sa-heading` on
92
+ * `:root` substitutes whatever `--sa-color-fg-heading` resolves to ON `:root`,
93
+ * and descendants inherit that finished value. Quasar's trigger redeclares the
94
+ * roles on `<body>`, which never re-evaluates an alias computed one level up:
95
+ * a consumer still reading `var(--sa-heading)` kept light text while everything
96
+ * around it went dark, and this file promises the opposite for one release.
97
+ *
98
+ * The attribute trigger sits on `:root` itself and would not need this, but the
99
+ * pair is written the way the design guide tells consumers to write an override
100
+ * — both values, on the selectors the theme uses. A file that documents a rule
101
+ * and then exempts itself from it teaches the exemption.
102
+ */
103
+ [data-sa-theme='dark'],
104
+ body.body--dark {
105
+ --sa-bg-app: var(--sa-color-bg-app);
106
+ --sa-bg-surface: var(--sa-color-bg-surface);
107
+ --sa-bg-surface-2: var(--sa-color-bg-sunken);
108
+
109
+ --sa-primary: var(--sa-color-accent);
110
+ --sa-primary-strong: var(--sa-color-accent-strong);
111
+ --sa-primary-soft: var(--sa-color-accent-surface);
112
+ --sa-primary-softer: var(--sa-color-accent-surface-soft);
113
+ --sa-primary-border: var(--sa-color-accent-border);
114
+ --sa-primary-50: var(--sa-color-accent-surface-strong);
115
+
116
+ --sa-positive: var(--sa-color-positive);
117
+ --sa-positive-light: var(--sa-color-positive-strong);
118
+ --sa-positive-soft: var(--sa-color-positive-surface);
119
+ --sa-positive-bg: var(--sa-color-positive-surface);
120
+
121
+ --sa-negative: var(--sa-color-negative);
122
+ --sa-negative-soft: var(--sa-color-negative-surface);
123
+
124
+ --sa-warning: var(--sa-color-warning);
125
+ --sa-warning-soft: var(--sa-color-warning-surface);
126
+ --sa-warning-bg: var(--sa-color-warning-surface);
127
+ --sa-amber: var(--sa-color-warning-strong);
128
+ --sa-amber-light: var(--sa-color-warning-strong);
129
+ --sa-amber-border: var(--sa-color-warning-border);
130
+
131
+ /* Read by `TenantsPage` as `var(--sa-accent, #7c3aed)` and never declared
132
+ * anywhere — the fallback was the only value it ever had. It marked the
133
+ * catalogue-feature action, which is now a role. */
134
+ --sa-accent: var(--sa-color-feature);
135
+
136
+ --sa-heading: var(--sa-color-fg-heading);
137
+ --sa-body: var(--sa-color-fg-body);
138
+ --sa-muted: var(--sa-color-fg-muted);
139
+ --sa-muted-dark: var(--sa-color-fg-secondary);
140
+ --sa-muted-light: var(--sa-color-fg-subtle);
141
+
142
+ --sa-border: var(--sa-color-border);
143
+ --sa-border-soft: var(--sa-color-border-soft);
144
+ --sa-border-strong: var(--sa-color-border-strong);
145
+
146
+ --sa-section-pad: var(--sa-pad-section);
147
+ }
@@ -0,0 +1,108 @@
1
+ /* Layer 1 — primitives. The raw palette, and nothing else.
2
+ *
3
+ * A primitive says WHAT a colour is (`--sa-red-600` is a particular red). It
4
+ * never says what the colour is FOR — that is layer 2's job, and it is the only
5
+ * layer that changes between light and dark. Consequently:
6
+ *
7
+ * - nothing in this file may reference another variable;
8
+ * - nothing outside `tokens.semantic.*.css` may reference this file.
9
+ *
10
+ * A component reaching for `--sa-red-600` instead of `--sa-color-negative` is
11
+ * the bug this split exists to prevent: it is invisible in review, and it is
12
+ * the reason a dark theme costs 62 files instead of one. Rule 22 in the
13
+ * layer-discipline test enforces both directions.
14
+ *
15
+ * The ramps are the ones the admin surface already used. Steps that no role
16
+ * consumes are absent on purpose: a palette with unused rungs invites people to
17
+ * pick one, and then the role layer is no longer the single answer.
18
+ */
19
+
20
+ :root {
21
+ /* ── Neutral (slate). Carries text, surfaces and lines. ─────────────── */
22
+ --sa-white: #ffffff;
23
+ --sa-black: #000000;
24
+ --sa-neutral-25: #fbfbfd;
25
+ --sa-neutral-50: #f8fafc;
26
+ --sa-neutral-100: #f1f5f9;
27
+ --sa-neutral-200: #e2e8f0;
28
+ --sa-neutral-300: #cbd5e1;
29
+ --sa-neutral-400: #94a3b8;
30
+ /* Between 400 and 500, and it exists for a measured reason: `fg-subtle` at
31
+ * slate-400 reads 2.56:1 on a white card and 2.34:1 on the page canvas —
32
+ * under the 3:1 floor, i.e. captions and price units that are decoration
33
+ * rather than information. Slate-500 is the next rung and too dark to stay
34
+ * quiet. This one clears 3:1 on both. */
35
+ --sa-neutral-450: #7c8aa0;
36
+ --sa-neutral-500: #64748b;
37
+ --sa-neutral-600: #475569;
38
+ --sa-neutral-700: #334155;
39
+ --sa-neutral-800: #1e293b;
40
+ --sa-neutral-900: #0f172a;
41
+ --sa-neutral-950: #0b1220;
42
+ --sa-neutral-975: #070d18;
43
+
44
+ /* ── Blue. The `info` tone. ─────────────────────────────────────────── */
45
+ --sa-blue-50: #eff6ff;
46
+ --sa-blue-100: #dbeafe;
47
+ --sa-blue-200: #bfdbfe;
48
+ --sa-blue-300: #93c5fd;
49
+ --sa-blue-500: #3b82f6;
50
+ --sa-blue-600: #2563eb;
51
+ --sa-blue-700: #1d4ed8;
52
+ --sa-blue-800: #1e40af;
53
+
54
+ /* The platform's own brand blue. Kept out of the blue ramp because it is
55
+ * not a step in it: it is the value the accent role falls back to when the
56
+ * host application declares no Quasar `$primary` of its own. */
57
+ --sa-brand-blue: #3f6bff;
58
+
59
+ /* ── Green (emerald). `positive`. ───────────────────────────────────── */
60
+ --sa-green-50: #ecfdf5;
61
+ --sa-green-100: #d1fae5;
62
+ --sa-green-200: #a7f3d0;
63
+ --sa-green-500: #10b981;
64
+ --sa-green-700: #047857;
65
+ --sa-green-800: #065f46;
66
+
67
+ /* ── Amber. `warning`, and every "draft / scheduled / pending" state. ─ */
68
+ --sa-amber-50: #fffbeb;
69
+ --sa-amber-100: #fef3c7;
70
+ --sa-amber-200: #fde68a;
71
+ --sa-amber-300: #fcd34d;
72
+ --sa-amber-400: #fbbf24;
73
+ --sa-amber-500: #f59e0b;
74
+ --sa-amber-600: #d97706;
75
+ --sa-amber-700: #b45309;
76
+ --sa-amber-800: #92400e;
77
+
78
+ /* ── Red. `negative`. ───────────────────────────────────────────────── */
79
+ --sa-red-50: #fef2f2;
80
+ --sa-red-100: #fee2e2;
81
+ --sa-red-200: #fecaca;
82
+ --sa-red-300: #fca5a5;
83
+ --sa-red-500: #ef4444;
84
+ --sa-red-600: #dc2626;
85
+ --sa-red-700: #b91c1c;
86
+ --sa-red-800: #991b1b;
87
+
88
+ /* ── Indigo. `scheduled` — a version that is planned but not yet live. */
89
+ --sa-indigo-50: #eef2ff;
90
+ --sa-indigo-100: #e0e7ff;
91
+ --sa-indigo-200: #c7d2fe;
92
+ --sa-indigo-400: #818cf8;
93
+ --sa-indigo-700: #4338ca;
94
+
95
+ /* ── Violet and sky. The two catalogue entities, feature and quota. ── */
96
+ --sa-violet-50: #ede9fe;
97
+ --sa-violet-200: #ddd6fe;
98
+ --sa-violet-500: #8b5cf6;
99
+ --sa-violet-700: #6d28d9;
100
+ --sa-sky-50: #f0f9ff;
101
+ --sa-sky-200: #bae6fd;
102
+ --sa-sky-500: #0ea5e9;
103
+
104
+ /* ── Type families. ─────────────────────────────────────────────────── */
105
+ --sa-font-head: 'Plus Jakarta Sans', 'Inter', system-ui, sans-serif;
106
+ --sa-font-body: 'Inter', system-ui, sans-serif;
107
+ --sa-font-mono: ui-monospace, 'SF Mono', Menlo, monospace;
108
+ }
@@ -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,173 @@
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
+ /* Identical to the light theme, and that is the point — see there. */
129
+ --sa-color-inverse-bg: var(--sa-neutral-950);
130
+ --sa-color-inverse-surface: var(--sa-neutral-800);
131
+ --sa-color-inverse-surface-soft: var(--sa-neutral-600);
132
+ --sa-color-inverse-fg: var(--sa-white);
133
+ --sa-color-inverse-fg-muted: var(--sa-neutral-300);
134
+ --sa-color-inverse-accent: var(--sa-amber-400);
135
+ --sa-color-inverse-accent-strong: var(--sa-amber-600);
136
+ --sa-color-inverse-accent-surface: color-mix(in srgb, var(--sa-amber-500) 15%, transparent);
137
+ --sa-color-inverse-accent-surface-strong: color-mix(
138
+ in srgb,
139
+ var(--sa-amber-500) 22%,
140
+ transparent
141
+ );
142
+ --sa-color-inverse-accent-fg: var(--sa-neutral-900);
143
+ --sa-color-inverse-danger: var(--sa-red-700);
144
+ --sa-color-inverse-danger-strong: var(--sa-red-600);
145
+ --sa-color-inverse-notice: var(--sa-amber-600);
146
+ --sa-color-inverse-notice-strong: var(--sa-amber-700);
147
+ --sa-color-inverse-fg-subtle: color-mix(in srgb, var(--sa-white) 55%, transparent);
148
+ --sa-color-inverse-border: color-mix(in srgb, var(--sa-white) 8%, transparent);
149
+ --sa-color-inverse-border-strong: color-mix(in srgb, var(--sa-white) 16%, transparent);
150
+
151
+ --sa-color-focus-ring: color-mix(in srgb, var(--sa-color-accent) 45%, transparent);
152
+ --sa-color-selection: color-mix(in srgb, var(--sa-color-accent) 30%, transparent);
153
+
154
+ /* Black ink and roughly double the alpha: a slate shadow at 6% is invisible
155
+ * on a slate surface, so the depth cue has to come from real darkness. */
156
+ --sa-shadow-ink: var(--sa-black);
157
+ --sa-shadow-tint-1: color-mix(in srgb, var(--sa-shadow-ink) 24%, transparent);
158
+ --sa-shadow-tint-2: color-mix(in srgb, var(--sa-shadow-ink) 32%, transparent);
159
+ --sa-shadow-tint-3: color-mix(in srgb, var(--sa-shadow-ink) 44%, transparent);
160
+ --sa-shadow-tint-4: color-mix(in srgb, var(--sa-shadow-ink) 60%, transparent);
161
+
162
+ --sa-shadow-0: none;
163
+ --sa-shadow-1: 0 1px 2px var(--sa-shadow-tint-1);
164
+ --sa-shadow-2: 0 2px 6px var(--sa-shadow-tint-2);
165
+ --sa-shadow-3: 0 8px 20px var(--sa-shadow-tint-3);
166
+ --sa-shadow-4: 0 20px 48px var(--sa-shadow-tint-4);
167
+ --sa-shadow-focus: 0 0 0 3px var(--sa-color-focus-ring);
168
+
169
+ --sa-elevation-tile: var(--sa-shadow-1);
170
+ --sa-elevation-card: var(--sa-shadow-2);
171
+ --sa-elevation-menu: var(--sa-shadow-3);
172
+ --sa-elevation-dialog: var(--sa-shadow-4);
173
+ }