@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.
- package/README.md +1 -1
- package/dist/{chunk-LY52T4N4.js → chunk-TQQMKWN6.js} +58 -1
- package/dist/{chunk-XMMFN2WJ.js → chunk-VYMTWCTT.js} +51 -1
- package/dist/client/index.cjs +57 -0
- package/dist/client/index.d.cts +58 -1
- package/dist/client/index.d.ts +58 -1
- package/dist/client/index.js +15 -1
- package/dist/index.cjs +321 -159
- package/dist/index.d.cts +67 -4
- package/dist/index.d.ts +67 -4
- package/dist/index.js +72 -2
- package/dist/quasar/index.cjs +116 -20
- package/dist/quasar/index.d.cts +48 -3
- package/dist/quasar/index.d.ts +48 -3
- package/dist/quasar/index.js +58 -3
- package/dist/{use-super-admin-i18n-CgTuNi8h.d.cts → use-sa-theme-B5t7oXph.d.cts} +51 -1
- package/dist/{use-super-admin-i18n-CHNdsSlm.d.ts → use-sa-theme-OUBlaqgl.d.ts} +51 -1
- package/package.json +5 -2
- package/src/client/identity-accents.ts +129 -0
- package/src/client/index.ts +1 -0
- package/src/components/BundleVersionPublishDialog.vue +17 -17
- package/src/components/KvBlock.vue +6 -6
- package/src/components/MarketingPromotionsTab.vue +84 -80
- package/src/components/MfaPromptDialog.vue +2 -2
- package/src/components/TenantActionConfirmDialog.vue +5 -5
- package/src/components/VersionDiffPreview.vue +18 -18
- package/src/components/bundle-editor/BundleCreatePanel.vue +52 -52
- package/src/components/bundle-editor/BundleFeaturesEditor.vue +20 -20
- package/src/components/bundle-editor/BundlePlanCompatPicker.vue +33 -33
- package/src/components/bundle-editor/BundleQuotasEditor.vue +26 -26
- package/src/components/bundle-editor/BundleStatusBanner.vue +21 -21
- package/src/components/bundle-editor/BundleVersionInlineEditor.vue +49 -49
- package/src/components/bundle-editor/BundleVersionStrip.vue +30 -30
- package/src/components/dialogs/PilotCreateDialog.vue +20 -20
- package/src/components/dialogs/PilotEditDialog.vue +6 -6
- package/src/components/dialogs/PromoCodeCreateDialog.vue +9 -9
- package/src/components/dialogs/PromoCodeDialogFields.vue +64 -63
- package/src/components/dialogs/PromoCodeEditDialog.vue +11 -11
- package/src/components/dialogs/pilot-dialog.css +48 -48
- package/src/components/plan/PlanCycleToggle.vue +9 -16
- package/src/components/plan/PlanGrid.vue +30 -34
- package/src/components/plan/PriceSummary.vue +35 -35
- package/src/components/plan/PromoCodeInput.vue +26 -26
- package/src/components/plan/PublicBundleGrid.vue +32 -28
- package/src/components/plan-create-dialog/PlanCreateDialog.vue +50 -50
- package/src/components/plan-detail/PlanDetail.vue +178 -157
- package/src/components/plan-detail/PlanTitleEdit.vue +11 -11
- package/src/components/plan-list/PlanList.vue +116 -100
- package/src/components/plan-matrix/PlanMatrix.vue +97 -114
- package/src/components/plan-review/PlanReview.vue +61 -57
- package/src/components/plan-version-editor/PlanVersionEditor.vue +246 -203
- package/src/components/wizard-stepper/WizardStepper.vue +17 -17
- package/src/index.ts +1 -0
- package/src/pages-standard/AdminLayout.vue +58 -37
- package/src/pages-standard/AdminManifestErrorPage.vue +6 -6
- package/src/pages-standard/AuditPage.vue +3 -3
- package/src/pages-standard/BundlesPage.vue +37 -37
- package/src/pages-standard/DashboardPage.vue +22 -19
- package/src/pages-standard/DiscoveryPage.vue +55 -49
- package/src/pages-standard/EmailHistoryPage.vue +53 -71
- package/src/pages-standard/MarketingCatalogPage.vue +245 -221
- package/src/pages-standard/PilotsPage.vue +7 -6
- package/src/pages-standard/PlatformEmailPage.vue +18 -18
- package/src/pages-standard/PromoCodeDetailPage.vue +3 -3
- package/src/pages-standard/PromoCodesPage.vue +10 -7
- package/src/pages-standard/SuperAdminLoginPage.vue +39 -25
- package/src/pages-standard/SuperAdminSetupWizard.vue +44 -33
- package/src/pages-standard/TenantDetailPage.vue +7 -7
- package/src/pages-standard/TenantsPage.vue +16 -50
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +13 -13
- package/src/pages-standard/discovery-page/CatalogEntryTransPanel.vue +24 -24
- package/src/pages-standard/discovery-page/DiscoveryCapList.vue +22 -22
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +39 -39
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +26 -26
- package/src/pages-standard/discovery-page/DiscoveryStatusControl.vue +1 -1
- package/src/pages-standard/discovery-page/discovery-ui.ts +10 -7
- package/src/pages-standard/marketing-catalog/MarketingCatalogAdmin.vue +2 -5
- package/src/pages-standard/plan-versions/PlanDiffCard.vue +27 -20
- package/src/pages-standard/plan-versions/PlanVersionsDiff.vue +24 -16
- package/src/pages-standard/plan-versions/PlanVersionsTimeline.vue +45 -45
- package/src/pages-standard/plans-page/PlanBundleOverview.vue +21 -15
- package/src/pages-standard/plans-page/PlanPublishDialog.vue +4 -4
- package/src/pages-standard/plans-page/PlansPageToast.vue +6 -6
- package/src/pages-standard/sa-theme.css +9 -687
- package/src/pages-standard/tenants/StatusPill.vue +13 -13
- package/src/pages-standard/tenants/format.ts +11 -12
- package/src/pages-tenant/LimitsRow.vue +4 -4
- package/src/pages-tenant/MySubscriptionBundlesPage.vue +70 -61
- package/src/pages-tenant/OnboardingConfigurator.vue +25 -25
- package/src/pages-tenant/PackageSnapshotPanel.vue +10 -16
- package/src/pages-tenant/PendingVersionBanner.vue +12 -12
- package/src/pages-tenant/PlanChangeWizard.vue +22 -76
- package/src/pages-tenant/TenantPlanSection.vue +19 -69
- package/src/pages-tenant/UsageBar.vue +7 -7
- package/src/pages-tenant/default-i18n.ts +113 -0
- package/src/pages-tenant/tenant-plan-section/BundlePreviewDialog.vue +14 -14
- package/src/pages-tenant/tenant-plan-section/TenantBundleStore.vue +10 -10
- package/src/pages-tenant/tenant-plan-section/TenantFeatureMatrix.vue +2 -2
- package/src/quasar/create-super-admin-app.ts +96 -1
- package/src/quasar/dark-bridge.ts +66 -0
- package/src/quasar/index.ts +1 -0
- package/src/ui/theme/_breakpoints.scss +34 -0
- package/src/ui/theme/base.css +50 -0
- package/src/ui/theme/components/body.css +23 -0
- package/src/ui/theme/components/button.css +158 -0
- package/src/ui/theme/components/card.css +74 -0
- package/src/ui/theme/components/field.css +38 -0
- package/src/ui/theme/components/hero.css +46 -0
- package/src/ui/theme/components/paginator.css +63 -0
- package/src/ui/theme/components/section.css +67 -0
- package/src/ui/theme/components/statistics.css +189 -0
- package/src/ui/theme/components/table.css +60 -0
- package/src/ui/theme/index.css +42 -0
- package/src/ui/theme/tokens.legacy.css +147 -0
- package/src/ui/theme/tokens.primitive.css +112 -0
- package/src/ui/theme/tokens.scale.css +103 -0
- package/src/ui/theme/tokens.semantic.dark.css +185 -0
- package/src/ui/theme/tokens.semantic.light.css +242 -0
- package/src/vue/use-sa-theme.ts +154 -0
|
@@ -44,12 +44,19 @@ import {
|
|
|
44
44
|
type SuperAdminLoginAdapter,
|
|
45
45
|
} from '../vue/super-admin-context.js';
|
|
46
46
|
import { SUPER_ADMIN_NOTIFY_KEY, type UiNotify } from '../vue/ui-notify.js';
|
|
47
|
+
import {
|
|
48
|
+
SA_THEME_KEY,
|
|
49
|
+
createSaTheme,
|
|
50
|
+
type SaTheme,
|
|
51
|
+
type SaThemeOptions,
|
|
52
|
+
} from '../vue/use-sa-theme.js';
|
|
47
53
|
import {
|
|
48
54
|
SUPER_ADMIN_I18N_KEY,
|
|
49
55
|
createSuperAdminI18n,
|
|
50
56
|
type SuperAdminI18n,
|
|
51
57
|
type SuperAdminI18nOptions,
|
|
52
58
|
} from '../vue/use-super-admin-i18n.js';
|
|
59
|
+
import { bindSaThemeToDocument } from './dark-bridge.js';
|
|
53
60
|
import { quasarNotify } from './notify.js';
|
|
54
61
|
|
|
55
62
|
export interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
|
|
@@ -112,6 +119,17 @@ export interface CreateSuperAdminAppOptions extends SuperAdminGuardOptions {
|
|
|
112
119
|
* runtime. Consumed via `useSuperAdminI18n()` / `useSaMessages()`.
|
|
113
120
|
*/
|
|
114
121
|
i18n?: SuperAdminI18nOptions;
|
|
122
|
+
/**
|
|
123
|
+
* Optional: colour scheme. Default follows the operating system and
|
|
124
|
+
* remembers an explicit pick. The context is returned on the handle
|
|
125
|
+
* (`handle.theme`) — set `handle.theme.scheme.value = 'dark'` to switch at
|
|
126
|
+
* runtime, and read it anywhere via `useSaTheme()`.
|
|
127
|
+
*
|
|
128
|
+
* Whatever it resolves to is mirrored onto `<html data-sa-theme>` AND into
|
|
129
|
+
* Quasar's `Dark`, so the platform's surfaces and Quasar's own components
|
|
130
|
+
* never disagree.
|
|
131
|
+
*/
|
|
132
|
+
theme?: SaThemeOptions;
|
|
115
133
|
}
|
|
116
134
|
|
|
117
135
|
export interface SuperAdminAppHandle {
|
|
@@ -120,10 +138,29 @@ export interface SuperAdminAppHandle {
|
|
|
120
138
|
pinia: Pinia;
|
|
121
139
|
/** i18n context of the shell — switch locale via `i18n.locale.value`. */
|
|
122
140
|
i18n: SuperAdminI18n;
|
|
141
|
+
/** Colour scheme of the shell — switch via `theme.scheme.value`. */
|
|
142
|
+
theme: SaTheme;
|
|
123
143
|
/** Mounts the app on a selector. Returns the root component instance. */
|
|
124
144
|
mount: (selector: string | Element) => ReturnType<App['mount']>;
|
|
145
|
+
/**
|
|
146
|
+
* Releases what this helper attached to the DOCUMENT — the theme's
|
|
147
|
+
* `prefers-color-scheme` subscription and the bridge that mirrors it onto
|
|
148
|
+
* `data-sa-theme` and Quasar's `Dark`. Idempotent; it does not unmount the
|
|
149
|
+
* app, which is `handle.app.unmount()`.
|
|
150
|
+
*
|
|
151
|
+
* Only matters where a shell is torn down while the document survives: hot
|
|
152
|
+
* reload, a micro-frontend, a second shell in one page. Skip it and the old
|
|
153
|
+
* bridge keeps writing to the one document on the next OS theme change, and
|
|
154
|
+
* can overrule the scheme the new shell was given.
|
|
155
|
+
*/
|
|
156
|
+
dispose: () => void;
|
|
125
157
|
}
|
|
126
158
|
|
|
159
|
+
/** Marks Quasar's teleported nodes so the theme can reach them. Exported so a
|
|
160
|
+
* test fixture can install the same config a real app gets — without it the
|
|
161
|
+
* `.sa-portal` rules are exercised by nothing. */
|
|
162
|
+
export const SA_PORTAL_CLASS = 'sa-portal';
|
|
163
|
+
|
|
127
164
|
const DEFAULT_QUASAR_OPTIONS: QuasarPluginOptions = {
|
|
128
165
|
plugins: { Notify, Dialog, Loading },
|
|
129
166
|
config: { notify: { position: 'top-right', timeout: 3000 } },
|
|
@@ -138,7 +175,35 @@ const DEFAULT_QUASAR_OPTIONS: QuasarPluginOptions = {
|
|
|
138
175
|
export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperAdminAppHandle {
|
|
139
176
|
const app = createApp(options.rootComponent);
|
|
140
177
|
|
|
141
|
-
|
|
178
|
+
// Quasar teleports every dialog, menu and tooltip into a div appended to
|
|
179
|
+
// `<body>`, so a rule prefixed with `.sa-page` never reaches them — which is
|
|
180
|
+
// why dialog cards kept Quasar's neutral grey in dark mode while the page
|
|
181
|
+
// behind them was slate, and why the outlined-field correction had silently
|
|
182
|
+
// never applied inside a dialog.
|
|
183
|
+
//
|
|
184
|
+
// Scope, stated plainly: `globalNodes` is document-wide, not per-owner —
|
|
185
|
+
// `createGlobalNode` stamps the class on every portal Quasar opens, the
|
|
186
|
+
// shell's own and any a contributed project page opens inside it. That is
|
|
187
|
+
// intended: a page mounted in the admin shell is admin UI, and the same
|
|
188
|
+
// theme already repaints its cards via `.sa-page`. What the class buys is
|
|
189
|
+
// that an app which never calls `createSuperAdminApp` stays untouched —
|
|
190
|
+
// which a bare `.q-dialog .q-card` rule could not promise.
|
|
191
|
+
const quasarOptions = options.quasarOptions ?? DEFAULT_QUASAR_OPTIONS;
|
|
192
|
+
// `globalNodes` is a documented Quasar config option that its TypeScript
|
|
193
|
+
// types do not declare (see quasar/src/utils/private.config/nodes.js, which
|
|
194
|
+
// reads `globalConfig.globalNodes.class`). The cast is the exception this
|
|
195
|
+
// codebase allows for an API that exists and is simply untyped upstream.
|
|
196
|
+
const config = quasarOptions.config as Record<string, unknown> | undefined;
|
|
197
|
+
const existingPortalClass = (config?.globalNodes as { class?: string } | undefined)?.class;
|
|
198
|
+
app.use(Quasar, {
|
|
199
|
+
...quasarOptions,
|
|
200
|
+
config: {
|
|
201
|
+
...quasarOptions.config,
|
|
202
|
+
globalNodes: {
|
|
203
|
+
class: [existingPortalClass, SA_PORTAL_CLASS].filter(Boolean).join(' '),
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
} as QuasarPluginOptions);
|
|
142
207
|
|
|
143
208
|
const pinia = createPinia();
|
|
144
209
|
app.use(pinia);
|
|
@@ -164,9 +229,34 @@ export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperA
|
|
|
164
229
|
};
|
|
165
230
|
|
|
166
231
|
const i18n = createSuperAdminI18n(options.i18n);
|
|
232
|
+
// An app that configured Quasar's dark mode has STATED a preference; the
|
|
233
|
+
// platform's default of 'system' has not. Seeding from it keeps the
|
|
234
|
+
// documented "Quasar decided" path working here — without it the bridge's
|
|
235
|
+
// first, immediate tick resolved 'system' against the machine and called
|
|
236
|
+
// `Dark.set()` with the answer, erasing the app's own choice one line after
|
|
237
|
+
// it was applied. In both directions: a configured `true` was undone on a
|
|
238
|
+
// light machine, a configured `false` on a dark one.
|
|
239
|
+
//
|
|
240
|
+
// Read from the OPTION rather than from `Dark.isActive`, because the option
|
|
241
|
+
// distinguishes three states and the resulting flag only two: `'auto'` means
|
|
242
|
+
// "follow the machine", which is what 'system' already does, and reading the
|
|
243
|
+
// flag would freeze whatever the machine happened to say at boot.
|
|
244
|
+
//
|
|
245
|
+
// A stored operator pick still outranks this — `createSaTheme` gives storage
|
|
246
|
+
// precedence over the `scheme` option — because that is a person choosing
|
|
247
|
+
// rather than a default.
|
|
248
|
+
const configuredDark = options.quasarOptions?.config?.dark;
|
|
249
|
+
const theme = createSaTheme({
|
|
250
|
+
...options.theme,
|
|
251
|
+
scheme:
|
|
252
|
+
options.theme?.scheme ??
|
|
253
|
+
(typeof configuredDark === 'boolean' ? (configuredDark ? 'dark' : 'light') : undefined),
|
|
254
|
+
});
|
|
255
|
+
const stopThemeBridge = bindSaThemeToDocument(theme);
|
|
167
256
|
|
|
168
257
|
app.provide(SUPER_ADMIN_BRAND_KEY, { tag: 'SuperAdmin', ...options.brand });
|
|
169
258
|
app.provide(SUPER_ADMIN_I18N_KEY, i18n);
|
|
259
|
+
app.provide(SA_THEME_KEY, theme);
|
|
170
260
|
app.provide(SUPER_ADMIN_ENDPOINTS_KEY, endpoints);
|
|
171
261
|
app.provide(SUPER_ADMIN_EXTENSIONS_KEY, options.extensions ?? {});
|
|
172
262
|
app.provide(SUPER_ADMIN_ACTIONS_KEY, options.actions ?? {});
|
|
@@ -191,6 +281,11 @@ export function createSuperAdminApp(options: CreateSuperAdminAppOptions): SuperA
|
|
|
191
281
|
router,
|
|
192
282
|
pinia,
|
|
193
283
|
i18n,
|
|
284
|
+
theme,
|
|
194
285
|
mount: (selector) => app.mount(selector),
|
|
286
|
+
dispose: () => {
|
|
287
|
+
stopThemeBridge();
|
|
288
|
+
theme.dispose();
|
|
289
|
+
},
|
|
195
290
|
};
|
|
196
291
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The half of the theme switch that touches the DOM and Quasar.
|
|
2
|
+
//
|
|
3
|
+
// Two things have to agree, or the screen ends up half-dark:
|
|
4
|
+
//
|
|
5
|
+
// `data-sa-theme` on <html> drives the platform's own role tokens
|
|
6
|
+
// Quasar's `Dark.set()` drives q-card, q-table, q-menu, q-dialog …
|
|
7
|
+
//
|
|
8
|
+
// The stylesheet already accepts Quasar's `body--dark` as a dark trigger, so an
|
|
9
|
+
// app that only calls `$q.dark.set(true)` is complete without this module. What
|
|
10
|
+
// the bridge adds is the other direction — an operator switching the theme in
|
|
11
|
+
// the admin's own header — and, once both exist, keeping them from disagreeing:
|
|
12
|
+
// it watches BOTH and writes each into the other.
|
|
13
|
+
|
|
14
|
+
import { Dark } from 'quasar';
|
|
15
|
+
import { watch, type WatchStopHandle } from 'vue';
|
|
16
|
+
|
|
17
|
+
import type { SaTheme } from '../vue/use-sa-theme.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Mirrors a theme context onto the document and Quasar. Returns the stop
|
|
21
|
+
* handle; `createSuperAdminApp()` wires this up for you.
|
|
22
|
+
*
|
|
23
|
+
* `immediate` is not optional: at bootstrap the stored pick is already resolved
|
|
24
|
+
* and nothing has painted yet, so a first-change-only watcher would render one
|
|
25
|
+
* frame in the wrong theme on every reload.
|
|
26
|
+
*
|
|
27
|
+
* That first tick MIRRORS, it does not negotiate — whatever `theme.resolved`
|
|
28
|
+
* says is written to Quasar, including `Dark.set(false)`. So a caller that
|
|
29
|
+
* bootstraps with Quasar already dark has to say so when it builds the theme,
|
|
30
|
+
* or its own choice is erased one line after it was applied.
|
|
31
|
+
* `createSuperAdminApp` does exactly that, seeding from `Dark.isActive`.
|
|
32
|
+
*/
|
|
33
|
+
export function bindSaThemeToDocument(theme: SaTheme): WatchStopHandle {
|
|
34
|
+
const toQuasar = watch(
|
|
35
|
+
theme.resolved,
|
|
36
|
+
(resolved) => {
|
|
37
|
+
if (typeof document !== 'undefined') {
|
|
38
|
+
document.documentElement.setAttribute('data-sa-theme', resolved);
|
|
39
|
+
}
|
|
40
|
+
Dark.set(resolved === 'dark');
|
|
41
|
+
},
|
|
42
|
+
{ immediate: true },
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// …and back. An app with its own toggle calls `$q.dark.set(false)`, which
|
|
46
|
+
// this side of the bridge never hears: the theme has not changed, so
|
|
47
|
+
// `data-sa-theme="dark"` stays on `<html>` while Quasar goes light. That is
|
|
48
|
+
// the half-dark screen the bridge exists to prevent, arriving from the
|
|
49
|
+
// other direction.
|
|
50
|
+
//
|
|
51
|
+
// No loop: the write-back only fires when Quasar and the theme actually
|
|
52
|
+
// disagree, and setting the scheme drives `resolved` to the value Quasar
|
|
53
|
+
// already has, so the outbound watcher's `Dark.set` is a no-op.
|
|
54
|
+
const fromQuasar = watch(
|
|
55
|
+
() => Dark.isActive,
|
|
56
|
+
(isDark) => {
|
|
57
|
+
const asScheme = isDark ? 'dark' : 'light';
|
|
58
|
+
if (theme.resolved.value !== asScheme) theme.scheme.value = asScheme;
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
toQuasar();
|
|
64
|
+
fromQuasar();
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/quasar/index.ts
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Breakpoints — the one scale that cannot be a custom property.
|
|
2
|
+
//
|
|
3
|
+
// `@media (min-width: var(--sa-bp-md))` is invalid CSS: custom properties are
|
|
4
|
+
// not substituted inside a media condition. So the values live here for SCSS
|
|
5
|
+
// blocks, and plain-CSS `<style>` blocks use the five literals, which the lint
|
|
6
|
+
// rule allows and nothing else.
|
|
7
|
+
//
|
|
8
|
+
// They are Quasar's own breakpoints, deliberately. A component that changes
|
|
9
|
+
// layout at 980px inside an app whose grid changes at 1024px produces a band
|
|
10
|
+
// 44px wide where the two disagree — and the admin surface had six such values
|
|
11
|
+
// (540, 600, 980, 1100, 1180, 1280), so it had several such bands. All six are
|
|
12
|
+
// now on the bounds below; the audit fails on a seventh.
|
|
13
|
+
//
|
|
14
|
+
// Moving 1100 and 1180 DOWN to 1023.98 keeps the wide layout for another 76 and
|
|
15
|
+
// 156 pixels respectively, which is the one direction of this change that could
|
|
16
|
+
// push content off the page. `visual-baseline.spec.ts` measures horizontal
|
|
17
|
+
// overflow at both edges of every band, on every page, for exactly that reason.
|
|
18
|
+
//
|
|
19
|
+
// Usage:
|
|
20
|
+
// @use '@saasicat/ui-vue/theme/breakpoints' as bp;
|
|
21
|
+
// @media (max-width: bp.$sm-max) { … }
|
|
22
|
+
|
|
23
|
+
$xs: 0;
|
|
24
|
+
$sm: 600px;
|
|
25
|
+
$md: 1024px;
|
|
26
|
+
$lg: 1440px;
|
|
27
|
+
$xl: 1920px;
|
|
28
|
+
|
|
29
|
+
// Upper bounds, for the `max-width` direction. Quasar uses the same 0.02px
|
|
30
|
+
// step back, so the two never both match at an integer viewport width.
|
|
31
|
+
$xs-max: 599.98px;
|
|
32
|
+
$sm-max: 1023.98px;
|
|
33
|
+
$md-max: 1439.98px;
|
|
34
|
+
$lg-max: 1919.98px;
|
|
@@ -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,74 @@
|
|
|
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
|
+
/* Quasar's stepper, for the same reason and one component further along.
|
|
36
|
+
*
|
|
37
|
+
* Only the steps you are NOT on. Their titles are a hard-coded grey —
|
|
38
|
+
* `rgb(158,158,158)`, 2.68:1 on a white card — which is below the floor for
|
|
39
|
+
* text whose whole job is to say which steps exist. The ACTIVE step is left
|
|
40
|
+
* alone: Quasar marks it `text-primary`, and that accent is the "you are here"
|
|
41
|
+
* affordance. A first attempt here painted every title with the heading role
|
|
42
|
+
* and quietly took that affordance away; the baseline diff showed it.
|
|
43
|
+
*
|
|
44
|
+
* The theme could not see any of this until the contrast checker learned to
|
|
45
|
+
* look inside teleported nodes: the only stepper in the package is in a dialog.
|
|
46
|
+
*/
|
|
47
|
+
.sa-page .q-stepper,
|
|
48
|
+
.sa-portal .q-stepper {
|
|
49
|
+
background: var(--sa-color-bg-surface);
|
|
50
|
+
color: var(--sa-color-fg-body);
|
|
51
|
+
}
|
|
52
|
+
.sa-page .q-stepper__tab:not(.q-stepper__tab--active) .q-stepper__title,
|
|
53
|
+
.sa-portal .q-stepper__tab:not(.q-stepper__tab--active) .q-stepper__title {
|
|
54
|
+
color: var(--sa-color-fg-muted);
|
|
55
|
+
}
|
|
56
|
+
.sa-page .q-stepper__caption,
|
|
57
|
+
.sa-portal .q-stepper__caption {
|
|
58
|
+
color: var(--sa-color-fg-subtle);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.sa-card {
|
|
62
|
+
background: var(--sa-color-bg-surface);
|
|
63
|
+
border: 1px solid var(--sa-color-border);
|
|
64
|
+
border-radius: var(--sa-radius-card);
|
|
65
|
+
padding: 16px 18px;
|
|
66
|
+
}
|
|
67
|
+
.sa-card__title {
|
|
68
|
+
margin: 0 0 12px;
|
|
69
|
+
font-family: var(--sa-font-head);
|
|
70
|
+
font-size: var(--sa-text-lg);
|
|
71
|
+
font-weight: 700;
|
|
72
|
+
color: var(--sa-color-fg-heading);
|
|
73
|
+
letter-spacing: -0.005em;
|
|
74
|
+
}
|
|
@@ -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
|
+
}
|