@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,50 @@
1
+ /* The page canvas and the type baseline. Everything else is a component.
2
+ *
3
+ * Nothing here styles a bare element outside `.sa-page`, which is what makes
4
+ * the whole theme safe to load into an application that has its own design —
5
+ * including the tenant-facing pages, which are embedded in a consumer's own
6
+ * app rather than in the admin shell.
7
+ */
8
+
9
+ /* The app canvas. It sits on the layout rather than on the page so that the
10
+ * gutters beside `max-width: 1600px` are painted too — and so a page can be a
11
+ * transparent frame while sections carry the surfaces. Declared here rather
12
+ * than on `body`: both known consumer apps load their own stylesheet after
13
+ * this one, and a `body` rule would lose to them. */
14
+ .sa-admin-layout,
15
+ .sa-admin-content {
16
+ background: var(--sa-color-bg-app);
17
+ }
18
+
19
+ /* Page frame — min-height, the page padding and the admin type, in one place
20
+ * instead of once per page (see AdminPage). No surface of its own.
21
+ *
22
+ * The font and text colour belong here because the hosting app's stylesheet
23
+ * loads after this one: without a rule on the page itself, admin pages would
24
+ * inherit whatever the consumer sets. Two full-page views used to pin both on
25
+ * their own root, which is exactly the divergence this prevents. */
26
+ .sa-page {
27
+ min-height: calc(100vh - 56px);
28
+ padding: 20px 28px 28px;
29
+ font-family: var(--sa-font-body);
30
+ color: var(--sa-color-fg-body);
31
+ }
32
+
33
+ /* Consumer apps load Quasar, whose element-level h1–h6 rules carry display
34
+ * sizes and huge line-heights. Neutralised here so a bare heading inside a
35
+ * page is legible without a class of its own.
36
+ *
37
+ * `:where()` keeps this at the specificity of a single class, so any heading
38
+ * class in the package still wins — this is a baseline, not a verdict. Three
39
+ * views used to carry their own copy of it, each covering a different subset
40
+ * of h1–h6. */
41
+ .sa-page :where(h1, h2, h3, h4, h5, h6) {
42
+ margin: 0;
43
+ line-height: 1.2;
44
+ }
45
+
46
+ /* Border-box for the whole page. Six views declared this for their own
47
+ * subtree, which is the same rule six times with six different roots. */
48
+ .sa-page * {
49
+ box-sizing: border-box;
50
+ }
@@ -0,0 +1,23 @@
1
+ /* Page body — everything below the hero (see AdminBody), including the
2
+ * loading and empty states the pages used to spell out one by one.
3
+ *
4
+ * Inset from the page edge, so the hero frames the page and everything below
5
+ * sits inside it. The inset lives here rather than on the section, because
6
+ * AdminStatistics is the other thing allowed at this level and would otherwise
7
+ * be the only block sticking out.
8
+ */
9
+
10
+ .sa-page-body {
11
+ padding-left: 20px;
12
+ padding-right: 20px;
13
+ }
14
+ .sa-page-body__state {
15
+ padding: 32px 0;
16
+ color: var(--sa-color-fg-muted);
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 12px;
20
+ }
21
+ .sa-page-body__state--empty {
22
+ font-size: var(--sa-text-md);
23
+ }
@@ -0,0 +1,158 @@
1
+ /* One button look across the admin surface, with two ways in.
2
+ *
3
+ * `.sa-btn` is for the hand-built buttons — eight files declared the same
4
+ * bordered pill independently, differing only in which local token they named.
5
+ *
6
+ * The Quasar selectors are the second way: any `q-btn` placed in a hero's or a
7
+ * section's action area adopts the same shape. That is what reaches the buttons
8
+ * a consumer app puts into those slots — a component could only style what the
9
+ * platform itself renders, and autohauspro's `#card-actions` button would have
10
+ * stayed behind in the Quasar look while everything around it changed.
11
+ *
12
+ * Colour is left to Quasar on its own buttons: `color="primary"` stays a filled
13
+ * primary, `flat` becomes the bordered surface below. Only shape and typography
14
+ * are unified, because those are what read as "a different design".
15
+ */
16
+
17
+ .sa-btn,
18
+ .sa-page-head__actions .q-btn,
19
+ .sa-section__actions .q-btn {
20
+ padding: 8px 14px;
21
+ min-height: 0;
22
+ border-radius: var(--sa-radius-control);
23
+ font-family: var(--sa-font-body);
24
+ font-size: var(--sa-text-md);
25
+ font-weight: 500;
26
+ line-height: 1.3;
27
+ letter-spacing: 0;
28
+ text-transform: none;
29
+ }
30
+
31
+ .sa-btn {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: 7px;
35
+ cursor: pointer;
36
+ color: var(--sa-color-fg-body);
37
+ transition:
38
+ background 0.12s,
39
+ border-color 0.12s;
40
+ }
41
+
42
+ /* The default is a bordered surface, not a bare label. */
43
+ .sa-btn,
44
+ .sa-page-head__actions .q-btn--flat,
45
+ .sa-section__actions .q-btn--flat {
46
+ border: 1px solid var(--sa-color-border-strong);
47
+ background: var(--sa-color-bg-surface);
48
+ }
49
+ .sa-btn:hover:not(:disabled),
50
+ .sa-page-head__actions .q-btn--flat:hover,
51
+ .sa-section__actions .q-btn--flat:hover {
52
+ background: var(--sa-color-bg-sunken);
53
+ }
54
+
55
+ .sa-btn--primary {
56
+ background: var(--sa-color-accent);
57
+ border-color: var(--sa-color-accent);
58
+ color: var(--sa-color-fg-on-accent);
59
+ }
60
+ .sa-btn--primary:hover:not(:disabled) {
61
+ background: var(--sa-color-accent-strong);
62
+ border-color: var(--sa-color-accent-strong);
63
+ }
64
+
65
+ .sa-btn:disabled {
66
+ opacity: 0.5;
67
+ cursor: not-allowed;
68
+ }
69
+
70
+ /* Size and intent modifiers, carried over from the families that merged. */
71
+ .sa-btn--sm {
72
+ padding: 5px 8px;
73
+ font-size: var(--sa-text-sm);
74
+ gap: 5px;
75
+ }
76
+ .sa-btn--ghost {
77
+ border-color: transparent;
78
+ background: transparent;
79
+ }
80
+ .sa-btn--ghost:hover:not(:disabled) {
81
+ background: var(--sa-color-bg-sunken);
82
+ }
83
+ .sa-btn--danger {
84
+ color: var(--sa-color-negative);
85
+ }
86
+ .sa-btn--danger:hover:not(:disabled) {
87
+ background: var(--sa-color-negative-surface);
88
+ }
89
+ .sa-btn--flex {
90
+ flex: 1;
91
+ }
92
+
93
+ /* Icon-only — the reload control, which carries no label and therefore needs
94
+ * an `aria-label` at the call site. */
95
+ .sa-btn--icon {
96
+ padding: 8px;
97
+ gap: 0;
98
+ }
99
+ .sa-btn--positive {
100
+ color: var(--sa-color-positive);
101
+ }
102
+ .sa-btn--positive:hover:not(:disabled) {
103
+ background: var(--sa-color-positive-surface);
104
+ }
105
+
106
+ /* The row-action icon — one recipe for the whole admin.
107
+ *
108
+ * There were three. `TenantsPage` had this bordered-less square with a muted
109
+ * icon; `PilotsPage` and `PromoCodesPage` rendered `<q-btn :color="…">`, which
110
+ * paints Quasar's brand colours and made a promo-code row read as a row of
111
+ * traffic lights beside a plans row of quiet greys; `EmailHistoryPage` and
112
+ * `PlatformEmailPage` pinned `color="primary"`.
113
+ *
114
+ * The quiet one wins. A row of actions is chrome, not a set of announcements —
115
+ * the colour belongs on the ONE action that is destructive, which is what the
116
+ * tone modifiers are for.
117
+ */
118
+ .sa-icon-btn {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ width: 28px;
123
+ height: 28px;
124
+ border: none;
125
+ border-radius: var(--sa-radius-control);
126
+ background: transparent;
127
+ color: var(--sa-color-fg-muted);
128
+ cursor: pointer;
129
+ text-decoration: none;
130
+ transition: background 0.12s;
131
+ }
132
+ .sa-icon-btn:hover:not(:disabled) {
133
+ background: var(--sa-color-border-soft);
134
+ }
135
+ .sa-icon-btn:disabled {
136
+ opacity: 0.45;
137
+ cursor: not-allowed;
138
+ }
139
+ /* The icon inherits, so a tone below colours the glyph without a second rule. */
140
+ .sa-icon-btn .q-icon {
141
+ color: inherit;
142
+ }
143
+
144
+ .sa-icon-btn--negative {
145
+ color: var(--sa-color-negative);
146
+ }
147
+ .sa-icon-btn--positive {
148
+ color: var(--sa-color-positive-fg);
149
+ }
150
+ .sa-icon-btn--warning {
151
+ color: var(--sa-color-warning-fg);
152
+ }
153
+ .sa-icon-btn--primary {
154
+ color: var(--sa-color-accent);
155
+ }
156
+ .sa-icon-btn--accent {
157
+ color: var(--sa-color-feature);
158
+ }
@@ -0,0 +1,48 @@
1
+ /* Cards — the generic block inside dashboard and detail sections, and the
2
+ * corner correction for Quasar's own card.
3
+ *
4
+ * Quasar cards round at 4px, which reads sharper than every surface around
5
+ * them. One rule so a card inside the admin agrees with the sections and tiles
6
+ * it sits between, without each page saying so.
7
+ */
8
+
9
+ /* Quasar paints its own card: white in light, and `var(--q-dark)` — a neutral
10
+ * #1d1d1d — as soon as `Dark` is active. That grey sits wrong on a slate page,
11
+ * so the surface is ours.
12
+ *
13
+ * The `.sa-portal` half is not decoration: Quasar teleports every dialog into a
14
+ * div appended to `<body>`, so a rule prefixed with `.sa-page` never reaches a
15
+ * dialog's card. `createSuperAdminApp` marks those portals — every portal in the
16
+ * document, the shell's own and those opened by a page contributed into it,
17
+ * which is the intent: a page inside the admin shell is admin UI. An app that
18
+ * never calls `createSuperAdminApp` is untouched. */
19
+ .sa-page .q-card,
20
+ .sa-portal .q-card {
21
+ border-radius: var(--sa-radius-card);
22
+ background: var(--sa-color-bg-surface);
23
+ color: var(--sa-color-fg-body);
24
+ }
25
+
26
+ /* A dropdown is teleported like a dialog, and QMenu adds `q-dark` to itself in
27
+ * dark mode (QMenu.js), which resolves to the same neutral grey. Without this
28
+ * the select in the paginator under every table opens a #1d1d1d panel over a
29
+ * slate page — the reported defect, one portal type further along. */
30
+ .sa-portal .q-menu {
31
+ background: var(--sa-color-bg-surface);
32
+ color: var(--sa-color-fg-body);
33
+ }
34
+
35
+ .sa-card {
36
+ background: var(--sa-color-bg-surface);
37
+ border: 1px solid var(--sa-color-border);
38
+ border-radius: var(--sa-radius-card);
39
+ padding: 16px 18px;
40
+ }
41
+ .sa-card__title {
42
+ margin: 0 0 12px;
43
+ font-family: var(--sa-font-head);
44
+ font-size: var(--sa-text-lg);
45
+ font-weight: 700;
46
+ color: var(--sa-color-fg-heading);
47
+ letter-spacing: -0.005em;
48
+ }
@@ -0,0 +1,38 @@
1
+ /* Form fields and the filter row.
2
+ *
3
+ * Quasar's outlined input rounds at 4px, which reads sharper than everything
4
+ * around it; the admin surface settles on `--sa-radius-field`, the radius the
5
+ * hand-built search boxes already used. Declared on the field control so it
6
+ * covers inputs, selects and date pickers alike.
7
+ */
8
+
9
+ .sa-page .q-field--outlined .q-field__control,
10
+ .sa-page .q-field--outlined .q-field__control:before,
11
+ .sa-page .q-field--outlined .q-field__control:after,
12
+ .sa-portal .q-field--outlined .q-field__control,
13
+ .sa-portal .q-field--outlined .q-field__control:before,
14
+ .sa-portal .q-field--outlined .q-field__control:after {
15
+ border-radius: var(--sa-radius-field);
16
+ }
17
+ /* Quasar's outlined control is transparent, which reads as a hole wherever it
18
+ * sits on a section surface rather than on the page canvas.
19
+ *
20
+ * `.sa-portal` because a dialog is teleported out of `.sa-page` — this
21
+ * correction had silently never applied to a field inside a dialog. */
22
+ .sa-page .q-field--outlined .q-field__control,
23
+ .sa-portal .q-field--outlined .q-field__control {
24
+ background: var(--sa-color-bg-surface);
25
+ }
26
+
27
+ /* The filter row (see AdminFilters). Declared once so no page writes its own
28
+ * flex rule — which is how five pages ended up with five filter bars. */
29
+ .sa-filters .sa-filters__body {
30
+ display: flex;
31
+ gap: 10px;
32
+ flex-wrap: wrap;
33
+ align-items: center;
34
+ }
35
+ .sa-filters .sa-filters__body > * {
36
+ flex: 1;
37
+ min-width: 200px;
38
+ }
@@ -0,0 +1,46 @@
1
+ /* The page hero — the banner that answers "which page am I on" (AdminHero).
2
+ *
3
+ * A tinted, bordered block rather than a first paragraph, so identity is
4
+ * visually separate from content. Tint and border come from the accent role,
5
+ * which reads the host application's brand: an app that sets `$primary` gets
6
+ * its own hero without touching this file.
7
+ */
8
+
9
+ .sa-page-head {
10
+ /* Top-aligned, so the controls sit level with the title rather than
11
+ * drifting down with the subtitle's height. */
12
+ display: flex;
13
+ align-items: flex-start;
14
+ gap: 20px;
15
+ margin-bottom: 20px;
16
+ flex-wrap: wrap;
17
+ padding: 13px 22px 15px;
18
+ border: 1px solid var(--sa-color-accent-border);
19
+ border-radius: var(--sa-radius-hero);
20
+ background: linear-gradient(
21
+ 135deg,
22
+ var(--sa-color-accent-surface) 0%,
23
+ var(--sa-color-accent-surface-soft) 100%
24
+ );
25
+ }
26
+ .sa-page-head__title {
27
+ margin: 0;
28
+ font-family: var(--sa-font-head);
29
+ font-size: var(--sa-text-2xl);
30
+ font-weight: 700;
31
+ letter-spacing: -0.02em;
32
+ line-height: 1.25;
33
+ color: var(--sa-color-fg-heading);
34
+ }
35
+ .sa-page-head__sub {
36
+ margin: 1px 0 0;
37
+ font-size: var(--sa-text-md);
38
+ line-height: 1.35;
39
+ color: var(--sa-color-fg-muted);
40
+ }
41
+ .sa-page-head__actions {
42
+ margin-left: auto;
43
+ display: flex;
44
+ gap: 8px;
45
+ align-items: center;
46
+ }
@@ -0,0 +1,63 @@
1
+ /* The pager below a list (see AdminPaginator). Right-aligned, one rule, so a
2
+ * list contributes its counts and nothing about where paging sits.
3
+ */
4
+
5
+ .sa-paginator {
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: flex-end;
9
+ gap: 8px;
10
+ padding: 12px 0 0;
11
+ font-size: var(--sa-text-md);
12
+ color: var(--sa-color-fg-muted);
13
+ }
14
+ .sa-paginator__size-label {
15
+ font-weight: 500;
16
+ }
17
+ .sa-paginator__select {
18
+ min-width: 92px;
19
+ }
20
+ .sa-paginator__step,
21
+ .sa-paginator__page {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ min-height: 30px;
26
+ padding: 0 8px;
27
+ border: 1px solid var(--sa-color-border);
28
+ border-radius: var(--sa-radius-control);
29
+ background: var(--sa-color-bg-surface);
30
+ color: var(--sa-color-fg-body);
31
+ font: inherit;
32
+ cursor: pointer;
33
+ transition:
34
+ background 0.12s,
35
+ border-color 0.12s;
36
+ }
37
+ .sa-paginator__page {
38
+ font-weight: 600;
39
+ color: var(--sa-color-fg-heading);
40
+ min-width: 96px;
41
+ }
42
+ .sa-paginator__step:hover:not(:disabled),
43
+ .sa-paginator__page:hover:not(:disabled) {
44
+ background: var(--sa-color-bg-sunken);
45
+ border-color: var(--sa-color-border-strong);
46
+ }
47
+ .sa-paginator__step:disabled,
48
+ .sa-paginator__page:disabled {
49
+ opacity: 0.45;
50
+ cursor: default;
51
+ }
52
+ .sa-paginator__input {
53
+ min-width: 96px;
54
+ min-height: 30px;
55
+ padding: 0 8px;
56
+ border: 1px solid var(--sa-color-accent);
57
+ border-radius: var(--sa-radius-control);
58
+ background: var(--sa-color-bg-surface);
59
+ color: var(--sa-color-fg-heading);
60
+ font: inherit;
61
+ font-weight: 600;
62
+ text-align: center;
63
+ }
@@ -0,0 +1,67 @@
1
+ /* Section — a labelled block within a page (see AdminSection). It carries the
2
+ * surface: the page frame is transparent, so what a reader sees as "a block of
3
+ * this page" is exactly one section. A section that needs an inner card still
4
+ * adds `.sa-card`; a section IS one, so the two do not stack.
5
+ */
6
+
7
+ /* Rhythm only for sections sitting directly in the page flow. Deliberately a
8
+ * child selector: sections placed inside a grid or flex container get their
9
+ * spacing from that container, and a blanket margin would fight it. */
10
+ .sa-page > .sa-section,
11
+ .sa-page-body > .sa-section {
12
+ margin-bottom: 16px;
13
+ }
14
+ .sa-page > .sa-section:last-child,
15
+ .sa-page-body > .sa-section:last-child {
16
+ margin-bottom: 0;
17
+ }
18
+
19
+ .sa-section {
20
+ background: var(--sa-color-bg-surface);
21
+ border: 1px solid var(--sa-color-border);
22
+ border-radius: var(--sa-radius-section);
23
+ }
24
+ .sa-section__body {
25
+ padding: var(--sa-pad-section);
26
+ }
27
+
28
+ /* The head is the hero's block one level down: same typography, same 22px
29
+ * inset — so a section title sits exactly under the page title — but no tinted
30
+ * box of its own. The section already is a surface; a box inside it would read
31
+ * as a panel within a panel.
32
+ *
33
+ * The size steps down because the level does. Everything else is the hero's. */
34
+ .sa-section__head {
35
+ display: flex;
36
+ align-items: flex-start;
37
+ gap: 20px;
38
+ padding: 18px 22px 0;
39
+ }
40
+ .sa-section__title {
41
+ margin: 0;
42
+ font-family: var(--sa-font-head);
43
+ font-size: var(--sa-text-xl);
44
+ font-weight: 700;
45
+ letter-spacing: -0.02em;
46
+ line-height: 1.25;
47
+ color: var(--sa-color-fg-heading);
48
+ }
49
+ .sa-section__sub {
50
+ margin: 1px 0 0;
51
+ font-size: var(--sa-text-md);
52
+ line-height: 1.35;
53
+ color: var(--sa-color-fg-muted);
54
+ }
55
+ .sa-section__actions {
56
+ margin-left: auto;
57
+ display: flex;
58
+ gap: 8px;
59
+ align-items: center;
60
+ }
61
+
62
+ /* Key-value grid — the layout `KvBlock` is meant to sit in. */
63
+ .sa-kv-grid {
64
+ display: grid;
65
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
66
+ gap: 12px;
67
+ }
@@ -0,0 +1,189 @@
1
+ /* Statistics — one tile look for the whole admin surface (see AdminStatistics
2
+ * and AdminKpi). Seven near-copies preceded this; two of them were byte
3
+ * identical. Every radius here resolves to `--sa-radius-tile`, which is the
4
+ * single place a tile's corner is decided.
5
+ *
6
+ * Two arrangements, because the tiles serve two jobs: `grid` for informational
7
+ * rows that should share a baseline, `strip` for the clickable filter pills
8
+ * that wrap along a toolbar.
9
+ */
10
+
11
+ .sa-stats {
12
+ display: grid;
13
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
14
+ /* Kept in step with GAP_PX in AdminStatistics, which subtracts the gap
15
+ * total when it caps the track count. */
16
+ gap: 10px;
17
+ }
18
+
19
+ .sa-kpi {
20
+ display: flex;
21
+ align-items: stretch;
22
+ gap: 12px;
23
+ background: var(--sa-color-bg-surface);
24
+ border: 1px solid var(--sa-color-border);
25
+ border-radius: var(--sa-radius-tile);
26
+ padding: 10px 14px 9px;
27
+ font: inherit;
28
+ color: inherit;
29
+ text-align: left;
30
+ }
31
+ /* The figure column. It carries the stacking so the tile itself can put an
32
+ * icon beside it without a second set of rules. */
33
+ .sa-kpi__text {
34
+ display: flex;
35
+ flex-direction: column;
36
+ align-items: flex-start;
37
+ gap: 2px;
38
+ flex: 1;
39
+ min-width: 0;
40
+ }
41
+ /* Subs line up across a row rather than floating under uneven values. */
42
+ .sa-stats .sa-kpi__sub {
43
+ margin-top: auto;
44
+ }
45
+
46
+ .sa-kpi__label {
47
+ font-size: var(--sa-text-sm);
48
+ font-weight: 600;
49
+ color: var(--sa-color-fg-muted);
50
+ order: 2;
51
+ }
52
+ .sa-kpi__value {
53
+ font-size: var(--sa-text-2xl);
54
+ font-weight: 700;
55
+ letter-spacing: -0.02em;
56
+ line-height: 1.05;
57
+ color: var(--sa-color-fg-heading);
58
+ order: 1;
59
+ }
60
+ .sa-kpi__sub {
61
+ font: 500 var(--sa-text-2xs) var(--sa-font-mono);
62
+ letter-spacing: 0.04em;
63
+ color: var(--sa-color-fg-muted);
64
+ padding-top: 1px;
65
+ order: 3;
66
+ }
67
+ .sa-kpi__icon {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ color: var(--sa-color-fg-muted);
72
+ }
73
+
74
+ /* Inline tiles: the icon leads in a tinted square and the label sits above the
75
+ * figure, for rows where the icon carries the identity of the number. */
76
+ .sa-kpi--inline {
77
+ align-items: center;
78
+ padding: 12px 14px;
79
+ }
80
+ .sa-kpi--inline .sa-kpi__icon {
81
+ width: 34px;
82
+ height: 34px;
83
+ flex: none;
84
+ border-radius: var(--sa-radius-tile);
85
+ background: var(--sa-color-accent-surface-strong);
86
+ color: var(--sa-color-accent);
87
+ }
88
+ .sa-kpi--inline .sa-kpi__label {
89
+ order: 1;
90
+ text-transform: uppercase;
91
+ letter-spacing: 0.06em;
92
+ }
93
+ .sa-kpi--inline .sa-kpi__value {
94
+ order: 2;
95
+ }
96
+
97
+ /* Interactive tiles — rendered as <button> by AdminKpi when it has an action. */
98
+ .sa-kpi--action {
99
+ cursor: pointer;
100
+ transition:
101
+ border-color 0.12s,
102
+ background 0.12s,
103
+ box-shadow 0.12s;
104
+ }
105
+ .sa-kpi--action:hover {
106
+ background: var(--sa-color-bg-sunken);
107
+ border-color: var(--sa-color-border-strong);
108
+ }
109
+ .sa-kpi--selected {
110
+ border-color: var(--sa-color-fg-heading);
111
+ box-shadow:
112
+ 0 0 0 1px var(--sa-color-fg-heading) inset,
113
+ var(--sa-shadow-2);
114
+ }
115
+
116
+ /* Tone. By default only the value takes the colour; `--surface` tints the
117
+ * whole tile, which is what the discovery and draft tiles need to read as a
118
+ * state rather than as a number. */
119
+ .sa-kpi--positive .sa-kpi__value {
120
+ color: var(--sa-color-positive);
121
+ }
122
+ .sa-kpi--warn .sa-kpi__value {
123
+ color: var(--sa-color-warning);
124
+ }
125
+ .sa-kpi--danger .sa-kpi__value {
126
+ color: var(--sa-color-negative);
127
+ }
128
+ .sa-kpi--info .sa-kpi__value {
129
+ color: var(--sa-color-info-fg);
130
+ }
131
+ .sa-kpi--muted .sa-kpi__value {
132
+ color: var(--sa-color-fg-secondary);
133
+ }
134
+ .sa-kpi--purple .sa-kpi__value {
135
+ color: var(--sa-color-feature-fg);
136
+ }
137
+
138
+ .sa-kpi--surface.sa-kpi--positive {
139
+ border-color: var(--sa-color-positive-border);
140
+ background: var(--sa-color-positive-surface);
141
+ }
142
+ .sa-kpi--surface.sa-kpi--warn {
143
+ border-color: var(--sa-color-warning-border);
144
+ background: var(--sa-color-warning-surface);
145
+ }
146
+ .sa-kpi--surface.sa-kpi--danger {
147
+ border-color: var(--sa-color-negative-border);
148
+ background: var(--sa-color-negative-surface);
149
+ }
150
+ .sa-kpi--surface.sa-kpi--positive .sa-kpi__label,
151
+ .sa-kpi--surface.sa-kpi--warn .sa-kpi__label,
152
+ .sa-kpi--surface.sa-kpi--danger .sa-kpi__label {
153
+ color: inherit;
154
+ }
155
+
156
+ /* Selected tiles keep the tone in the outline and take the neutral elevation.
157
+ * Five tinted shadows preceded this — one per tone, each a different rgba() —
158
+ * and at 6% alpha under a 1px outline none of them was distinguishable from
159
+ * the others. The outline is what carries the state. */
160
+ .sa-kpi--selected.sa-kpi--positive {
161
+ border-color: var(--sa-color-positive-strong);
162
+ box-shadow:
163
+ 0 0 0 1px var(--sa-color-positive-strong) inset,
164
+ var(--sa-shadow-2);
165
+ }
166
+ .sa-kpi--selected.sa-kpi--warn {
167
+ border-color: var(--sa-color-warning-strong);
168
+ box-shadow:
169
+ 0 0 0 1px var(--sa-color-warning-strong) inset,
170
+ var(--sa-shadow-2);
171
+ }
172
+ .sa-kpi--selected.sa-kpi--danger {
173
+ border-color: var(--sa-color-negative);
174
+ box-shadow:
175
+ 0 0 0 1px var(--sa-color-negative) inset,
176
+ var(--sa-shadow-2);
177
+ }
178
+ .sa-kpi--selected.sa-kpi--info {
179
+ border-color: var(--sa-color-info-strong);
180
+ box-shadow:
181
+ 0 0 0 1px var(--sa-color-info-strong) inset,
182
+ var(--sa-shadow-2);
183
+ }
184
+ .sa-kpi--selected.sa-kpi--purple {
185
+ border-color: var(--sa-color-feature);
186
+ box-shadow:
187
+ 0 0 0 1px var(--sa-color-feature) inset,
188
+ var(--sa-shadow-2);
189
+ }