@saasicat/ui-vue 0.24.0 → 0.24.2
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/dist/{chunk-VYMTWCTT.js → chunk-D74FFCCP.js} +2 -1
- package/dist/client/index.cjs +2 -1
- package/dist/client/index.d.cts +13 -1
- package/dist/client/index.d.ts +13 -1
- package/dist/client/index.js +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/client/identity-accents.ts +40 -2
- package/src/components/bundle-editor/BundleFeaturesEditor.vue +6 -1
- package/src/components/dialogs/PilotCreateDialog.vue +2 -1
- package/src/components/dialogs/PilotEditDialog.vue +2 -1
- package/src/components/dialogs/PromoCodeDialogFields.vue +40 -16
- package/src/components/plan/PromoCodeInput.vue +1 -1
- package/src/components/plan-detail/PlanDetail.vue +41 -3
- package/src/components/plan-detail/PlanVersionDiffPanel.vue +3 -3
- package/src/components/plan-matrix/PlanMatrix.vue +1 -1
- package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -3
- package/src/pages-standard/AdminManifestErrorPage.vue +4 -1
- package/src/pages-standard/DashboardPage.vue +3 -3
- package/src/pages-standard/DiscoveryPage.vue +42 -8
- package/src/pages-standard/MarketingCatalogPage.vue +20 -7
- package/src/pages-standard/SuperAdminLoginPage.vue +6 -1
- package/src/pages-standard/SuperAdminSetupWizard.vue +1 -1
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +17 -3
- package/src/pages-standard/tenants/StatusPill.vue +1 -1
- package/src/pages-tenant/OnboardingConfigurator.vue +1 -1
- package/src/ui/theme/base.css +13 -1
- package/src/ui/theme/components/field.css +44 -0
- package/src/ui/theme/components/statistics.css +1 -1
- package/src/ui/theme/tokens.semantic.dark.css +2 -0
- package/src/ui/theme/tokens.semantic.light.css +11 -2
|
@@ -665,10 +665,11 @@ var WELL_KNOWN = {
|
|
|
665
665
|
function identityAccentFor(key, overrides = {}, index = -1) {
|
|
666
666
|
return overrides[key] ?? WELL_KNOWN[key] ?? identityAccentAt(index);
|
|
667
667
|
}
|
|
668
|
+
var READABLE_ACCENT_SHARE = 50;
|
|
668
669
|
function identityChipStyle(accent) {
|
|
669
670
|
return {
|
|
670
671
|
background: `color-mix(in srgb, ${accent} 8%, transparent)`,
|
|
671
|
-
color: accent
|
|
672
|
+
color: `color-mix(in srgb, ${accent} ${READABLE_ACCENT_SHARE}%, var(--sa-color-fg-heading))`,
|
|
672
673
|
borderColor: `color-mix(in srgb, ${accent} 20%, transparent)`
|
|
673
674
|
};
|
|
674
675
|
}
|
package/dist/client/index.cjs
CHANGED
|
@@ -3899,10 +3899,11 @@ var WELL_KNOWN = {
|
|
|
3899
3899
|
function identityAccentFor(key, overrides = {}, index = -1) {
|
|
3900
3900
|
return overrides[key] ?? WELL_KNOWN[key] ?? identityAccentAt(index);
|
|
3901
3901
|
}
|
|
3902
|
+
var READABLE_ACCENT_SHARE = 50;
|
|
3902
3903
|
function identityChipStyle(accent) {
|
|
3903
3904
|
return {
|
|
3904
3905
|
background: `color-mix(in srgb, ${accent} 8%, transparent)`,
|
|
3905
|
-
color: accent
|
|
3906
|
+
color: `color-mix(in srgb, ${accent} ${READABLE_ACCENT_SHARE}%, var(--sa-color-fg-heading))`,
|
|
3906
3907
|
borderColor: `color-mix(in srgb, ${accent} 20%, transparent)`
|
|
3907
3908
|
};
|
|
3908
3909
|
}
|
package/dist/client/index.d.cts
CHANGED
|
@@ -436,13 +436,25 @@ declare function identityAccentAt(index: number): string;
|
|
|
436
436
|
declare function identityAccentFor(key: string, overrides?: Readonly<Record<string, string>>, index?: number): string;
|
|
437
437
|
/**
|
|
438
438
|
* The three-part chip style every one of these sites was hand-rolling: a wash
|
|
439
|
-
* of the accent, the accent as text, a firmer edge
|
|
439
|
+
* of the accent, a readable rendering of the accent as text, a firmer edge.
|
|
440
440
|
*
|
|
441
441
|
* `color-mix()` rather than the `accent + '15'` string concatenation it
|
|
442
442
|
* replaces. That trick only worked on a six-digit hex — it silently produced
|
|
443
443
|
* garbage for `rgb()`, for a named colour and for anything a consumer might
|
|
444
444
|
* reasonably pass — and it could never have worked for a `var()`, which is why
|
|
445
445
|
* these palettes could not follow the theme in the first place.
|
|
446
|
+
*
|
|
447
|
+
* The text is a mix rather than the accent itself. A `var(--sa-color-identity-N)`
|
|
448
|
+
* would not need that — the ramp has a value per theme, so it already suits both
|
|
449
|
+
* surfaces. A concrete hex has one value for both, and a concrete hex is what
|
|
450
|
+
* reaches here from a stored plan colour or from a consumer's `planAccents`; on
|
|
451
|
+
* whichever theme it lands nearest the surface it fades into its own wash.
|
|
452
|
+
*
|
|
453
|
+
* Callers may still override any of the three. `PromoCodeDialogFields` replaces
|
|
454
|
+
* the border with the plan's undiluted colour, and `PlanDiffCard` takes only the
|
|
455
|
+
* wash and the edge — an inline style beats the class beneath it, which is how
|
|
456
|
+
* the chip kept its own unreadable foreground through a fix applied to the
|
|
457
|
+
* selected-state rules around it.
|
|
446
458
|
*/
|
|
447
459
|
declare function identityChipStyle(accent: string): Record<string, string>;
|
|
448
460
|
|
package/dist/client/index.d.ts
CHANGED
|
@@ -436,13 +436,25 @@ declare function identityAccentAt(index: number): string;
|
|
|
436
436
|
declare function identityAccentFor(key: string, overrides?: Readonly<Record<string, string>>, index?: number): string;
|
|
437
437
|
/**
|
|
438
438
|
* The three-part chip style every one of these sites was hand-rolling: a wash
|
|
439
|
-
* of the accent, the accent as text, a firmer edge
|
|
439
|
+
* of the accent, a readable rendering of the accent as text, a firmer edge.
|
|
440
440
|
*
|
|
441
441
|
* `color-mix()` rather than the `accent + '15'` string concatenation it
|
|
442
442
|
* replaces. That trick only worked on a six-digit hex — it silently produced
|
|
443
443
|
* garbage for `rgb()`, for a named colour and for anything a consumer might
|
|
444
444
|
* reasonably pass — and it could never have worked for a `var()`, which is why
|
|
445
445
|
* these palettes could not follow the theme in the first place.
|
|
446
|
+
*
|
|
447
|
+
* The text is a mix rather than the accent itself. A `var(--sa-color-identity-N)`
|
|
448
|
+
* would not need that — the ramp has a value per theme, so it already suits both
|
|
449
|
+
* surfaces. A concrete hex has one value for both, and a concrete hex is what
|
|
450
|
+
* reaches here from a stored plan colour or from a consumer's `planAccents`; on
|
|
451
|
+
* whichever theme it lands nearest the surface it fades into its own wash.
|
|
452
|
+
*
|
|
453
|
+
* Callers may still override any of the three. `PromoCodeDialogFields` replaces
|
|
454
|
+
* the border with the plan's undiluted colour, and `PlanDiffCard` takes only the
|
|
455
|
+
* wash and the edge — an inline style beats the class beneath it, which is how
|
|
456
|
+
* the chip kept its own unreadable foreground through a fix applied to the
|
|
457
|
+
* selected-state rules around it.
|
|
446
458
|
*/
|
|
447
459
|
declare function identityChipStyle(accent: string): Record<string, string>;
|
|
448
460
|
|
package/dist/client/index.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -3984,10 +3984,11 @@ var WELL_KNOWN = {
|
|
|
3984
3984
|
function identityAccentFor(key, overrides = {}, index = -1) {
|
|
3985
3985
|
return overrides[key] ?? WELL_KNOWN[key] ?? identityAccentAt(index);
|
|
3986
3986
|
}
|
|
3987
|
+
var READABLE_ACCENT_SHARE = 50;
|
|
3987
3988
|
function identityChipStyle(accent) {
|
|
3988
3989
|
return {
|
|
3989
3990
|
background: `color-mix(in srgb, ${accent} 8%, transparent)`,
|
|
3990
|
-
color: accent
|
|
3991
|
+
color: `color-mix(in srgb, ${accent} ${READABLE_ACCENT_SHARE}%, var(--sa-color-fg-heading))`,
|
|
3991
3992
|
borderColor: `color-mix(in srgb, ${accent} 20%, transparent)`
|
|
3992
3993
|
};
|
|
3993
3994
|
}
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/ui-vue",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"description": "Vue 3 components, resource clients and composables for the SuperAdmin UI shell. Provides boot and manifest loaders, navigation, actions and standard pages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"src"
|
|
62
62
|
],
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@saasicat/types": "^0.24.
|
|
64
|
+
"@saasicat/types": "^0.24.2"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"pinia": "^2.0.0 || ^3.0.0",
|
|
@@ -110,20 +110,58 @@ export function identityAccentFor(
|
|
|
110
110
|
return overrides[key] ?? WELL_KNOWN[key] ?? identityAccentAt(index);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* How much of the accent survives when the accent becomes TEXT.
|
|
115
|
+
*
|
|
116
|
+
* The remainder comes from `--sa-color-fg-heading`, which is near-black in the
|
|
117
|
+
* light theme and near-white in the dark one. So a single expression darkens
|
|
118
|
+
* the accent in one theme and lightens it in the other, which is the same move
|
|
119
|
+
* `--sa-color-accent-strong` makes for the brand.
|
|
120
|
+
*
|
|
121
|
+
* It is not the brand's 82 %, and the difference is the point. The brand is one
|
|
122
|
+
* curated colour, checked by a person against both surfaces. This helper's
|
|
123
|
+
* input is uncontrolled: a plan's stored hex, a consumer's `planAccents`, a
|
|
124
|
+
* colour an operator picked out of a swatch. A share tuned for one colour is
|
|
125
|
+
* not a bound over all of them, and 82 % measurably is not one — three of the
|
|
126
|
+
* six colours the promotion editor itself stores stay under 3:1 on a raised
|
|
127
|
+
* dark card (2.81 to 2.98), and a light brand pick such as lime-400 only
|
|
128
|
+
* reaches 2.03:1 on a raised light one.
|
|
129
|
+
*
|
|
130
|
+
* At half, every colour in sRGB stays at or above 3.3:1 in light and 3.7:1 in
|
|
131
|
+
* dark, both extremes being the degenerate accents — an accent that already IS
|
|
132
|
+
* the theme's own extreme cannot be pushed further from the surface, so pure
|
|
133
|
+
* white decides the light bound and pure black the dark one. That is a bound
|
|
134
|
+
* over the whole cube rather than over a sample, which is what an uncontrolled
|
|
135
|
+
* input needs. `theme-role-contrast` measures it and fails if this share rises.
|
|
136
|
+
*/
|
|
137
|
+
const READABLE_ACCENT_SHARE = 50;
|
|
138
|
+
|
|
113
139
|
/**
|
|
114
140
|
* The three-part chip style every one of these sites was hand-rolling: a wash
|
|
115
|
-
* of the accent, the accent as text, a firmer edge
|
|
141
|
+
* of the accent, a readable rendering of the accent as text, a firmer edge.
|
|
116
142
|
*
|
|
117
143
|
* `color-mix()` rather than the `accent + '15'` string concatenation it
|
|
118
144
|
* replaces. That trick only worked on a six-digit hex — it silently produced
|
|
119
145
|
* garbage for `rgb()`, for a named colour and for anything a consumer might
|
|
120
146
|
* reasonably pass — and it could never have worked for a `var()`, which is why
|
|
121
147
|
* these palettes could not follow the theme in the first place.
|
|
148
|
+
*
|
|
149
|
+
* The text is a mix rather than the accent itself. A `var(--sa-color-identity-N)`
|
|
150
|
+
* would not need that — the ramp has a value per theme, so it already suits both
|
|
151
|
+
* surfaces. A concrete hex has one value for both, and a concrete hex is what
|
|
152
|
+
* reaches here from a stored plan colour or from a consumer's `planAccents`; on
|
|
153
|
+
* whichever theme it lands nearest the surface it fades into its own wash.
|
|
154
|
+
*
|
|
155
|
+
* Callers may still override any of the three. `PromoCodeDialogFields` replaces
|
|
156
|
+
* the border with the plan's undiluted colour, and `PlanDiffCard` takes only the
|
|
157
|
+
* wash and the edge — an inline style beats the class beneath it, which is how
|
|
158
|
+
* the chip kept its own unreadable foreground through a fix applied to the
|
|
159
|
+
* selected-state rules around it.
|
|
122
160
|
*/
|
|
123
161
|
export function identityChipStyle(accent: string): Record<string, string> {
|
|
124
162
|
return {
|
|
125
163
|
background: `color-mix(in srgb, ${accent} 8%, transparent)`,
|
|
126
|
-
color: accent
|
|
164
|
+
color: `color-mix(in srgb, ${accent} ${READABLE_ACCENT_SHARE}%, var(--sa-color-fg-heading))`,
|
|
127
165
|
borderColor: `color-mix(in srgb, ${accent} 20%, transparent)`,
|
|
128
166
|
};
|
|
129
167
|
}
|
|
@@ -208,7 +208,12 @@ function onToggle(featureKey: string): void {
|
|
|
208
208
|
margin-left: 4px;
|
|
209
209
|
}
|
|
210
210
|
.bd-feature-pill.on .bd-feature-key {
|
|
211
|
-
|
|
211
|
+
/* `-strong`, not the bare accent: the pill's `on` surface is a 24 % tint of
|
|
212
|
+
* blue over the dark card, and the brand does not change between themes —
|
|
213
|
+
* 2.92:1 there. The selected states around this one were moved to
|
|
214
|
+
* `-strong` already; this descendant was missed because it sets a colour
|
|
215
|
+
* and no background, which is a shape no contrast check reads. */
|
|
216
|
+
color: var(--sa-color-accent-strong);
|
|
212
217
|
}
|
|
213
218
|
.bd-feature-pill.overlap .bd-feature-key {
|
|
214
219
|
color: var(--sa-color-negative-fg);
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
>
|
|
171
171
|
<span
|
|
172
172
|
class="pl-plan-opt__dot"
|
|
173
|
-
:style="{ background: p.color ??
|
|
173
|
+
:style="{ background: p.color ?? IDENTITY_NEUTRAL }"
|
|
174
174
|
/>
|
|
175
175
|
<div class="pl-plan-opt__text">
|
|
176
176
|
<span class="pl-plan-opt__key">{{ p.value }}</span>
|
|
@@ -263,6 +263,7 @@
|
|
|
263
263
|
<script setup lang="ts">
|
|
264
264
|
import { computed, reactive, ref, watch } from 'vue';
|
|
265
265
|
import MfaPromptDialog from '../MfaPromptDialog.vue';
|
|
266
|
+
import { IDENTITY_NEUTRAL } from '../../client/identity-accents.js';
|
|
266
267
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
267
268
|
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
268
269
|
import type { PilotCopy, PilotCreatePayload, PilotCreateResult } from './types.js';
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
>
|
|
47
47
|
<span
|
|
48
48
|
class="pl-plan-opt__dot"
|
|
49
|
-
:style="{ background: p.color ??
|
|
49
|
+
:style="{ background: p.color ?? IDENTITY_NEUTRAL }"
|
|
50
50
|
/>
|
|
51
51
|
<div class="pl-plan-opt__text">
|
|
52
52
|
<span class="pl-plan-opt__key">{{ p.value }}</span>
|
|
@@ -148,6 +148,7 @@
|
|
|
148
148
|
<script setup lang="ts">
|
|
149
149
|
import { computed, reactive, ref, watch } from 'vue';
|
|
150
150
|
import MfaPromptDialog from '../MfaPromptDialog.vue';
|
|
151
|
+
import { IDENTITY_NEUTRAL } from '../../client/identity-accents.js';
|
|
151
152
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
152
153
|
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
153
154
|
import type { PilotRow } from '../../pages-standard/PilotsPage.vue';
|
|
@@ -85,7 +85,10 @@
|
|
|
85
85
|
:style="planChipStyle(p)"
|
|
86
86
|
@click="togglePlan(p.key)"
|
|
87
87
|
>
|
|
88
|
-
<span
|
|
88
|
+
<span
|
|
89
|
+
class="pc-plan-opt__mark"
|
|
90
|
+
:style="{ background: p.color ?? IDENTITY_NEUTRAL }"
|
|
91
|
+
/>
|
|
89
92
|
{{ p.label }}
|
|
90
93
|
</button>
|
|
91
94
|
</div>
|
|
@@ -101,13 +104,13 @@
|
|
|
101
104
|
<div class="pc-grid pc-grid--2">
|
|
102
105
|
<div class="pc-field">
|
|
103
106
|
<div class="pc-field__label">{{ msg.form.durationLabel }}</div>
|
|
104
|
-
<div class="pc-
|
|
107
|
+
<div class="pc-seg pc-seg--fill">
|
|
105
108
|
<button
|
|
106
109
|
v-for="o in durationOptions"
|
|
107
110
|
:key="o.k"
|
|
108
111
|
type="button"
|
|
109
|
-
class="pc-
|
|
110
|
-
:class="{ 'pc-
|
|
112
|
+
class="pc-seg-opt"
|
|
113
|
+
:class="{ 'pc-seg-opt--active': form.durationType === o.k }"
|
|
111
114
|
@click="form.durationType = o.k"
|
|
112
115
|
>
|
|
113
116
|
{{ o.label }}
|
|
@@ -161,13 +164,13 @@
|
|
|
161
164
|
<div v-if="mode === 'edit'" class="pc-grid pc-grid--2">
|
|
162
165
|
<div class="pc-field">
|
|
163
166
|
<div class="pc-field__label">{{ common.status }}</div>
|
|
164
|
-
<div class="pc-status">
|
|
167
|
+
<div class="pc-seg pc-status">
|
|
165
168
|
<button
|
|
166
169
|
v-for="o in statusOptions"
|
|
167
170
|
:key="o.k"
|
|
168
171
|
type="button"
|
|
169
|
-
class="pc-
|
|
170
|
-
:class="{ 'pc-
|
|
172
|
+
class="pc-seg-opt"
|
|
173
|
+
:class="{ 'pc-seg-opt--active': form.status === o.k }"
|
|
171
174
|
@click="form.status = o.k"
|
|
172
175
|
>
|
|
173
176
|
<q-icon :name="o.icon" size="14px" />
|
|
@@ -265,7 +268,7 @@
|
|
|
265
268
|
|
|
266
269
|
<script setup lang="ts">
|
|
267
270
|
import { computed } from 'vue';
|
|
268
|
-
import { identityChipStyle } from '../../client/identity-accents.js';
|
|
271
|
+
import { IDENTITY_NEUTRAL, identityChipStyle } from '../../client/identity-accents.js';
|
|
269
272
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
270
273
|
import { useSaMessages } from '../../vue/use-super-admin-i18n.js';
|
|
271
274
|
import type { PromoCodeDurationType, PromoCodePlanOption, PromoCodeValueType } from './types.js';
|
|
@@ -595,8 +598,8 @@ textarea.pc-input {
|
|
|
595
598
|
|
|
596
599
|
.pc-plan-opt--on {
|
|
597
600
|
border-color: var(--sa-color-accent);
|
|
598
|
-
background: var(--sa-color-accent-surface);
|
|
599
|
-
color: var(--sa-color-accent);
|
|
601
|
+
background: var(--sa-color-accent-surface-strong);
|
|
602
|
+
color: var(--sa-color-accent-strong);
|
|
600
603
|
}
|
|
601
604
|
|
|
602
605
|
.pc-plan-opt__mark {
|
|
@@ -605,13 +608,25 @@ textarea.pc-input {
|
|
|
605
608
|
border-radius: 50%;
|
|
606
609
|
}
|
|
607
610
|
|
|
608
|
-
.
|
|
611
|
+
/* One segmented control for both the duration and the status row. They are the
|
|
612
|
+
* same thing — a row of buttons of which one is chosen — and the status row had
|
|
613
|
+
* NO rules at all, so it rendered as raw browser buttons: white boxes on a dark
|
|
614
|
+
* dialog. Sharing the recipe is what makes the two look alike without either
|
|
615
|
+
* restating it. */
|
|
616
|
+
.pc-seg {
|
|
609
617
|
display: flex;
|
|
610
618
|
gap: 4px;
|
|
611
619
|
}
|
|
612
|
-
|
|
613
|
-
.pc-
|
|
620
|
+
/* The duration options divide the row; the status options size to their label. */
|
|
621
|
+
.pc-seg--fill .pc-seg-opt {
|
|
614
622
|
flex: 1;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.pc-seg-opt {
|
|
626
|
+
display: inline-flex;
|
|
627
|
+
align-items: center;
|
|
628
|
+
justify-content: center;
|
|
629
|
+
gap: var(--sa-gap-inline);
|
|
615
630
|
border: 1px solid var(--sa-color-border);
|
|
616
631
|
background: var(--sa-color-bg-surface);
|
|
617
632
|
border-radius: 7px;
|
|
@@ -621,10 +636,19 @@ textarea.pc-input {
|
|
|
621
636
|
color: var(--sa-color-fg-secondary);
|
|
622
637
|
}
|
|
623
638
|
|
|
624
|
-
.pc-
|
|
639
|
+
.pc-seg-opt:hover {
|
|
640
|
+
border-color: var(--sa-color-border-strong);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* `-accent-strong` on `-surface-strong`, which is what the design guide
|
|
644
|
+
* prescribes for accent text on a tint. The chosen option used to pair plain
|
|
645
|
+
* `--sa-color-accent` with the 8 % wash of itself: in dark mode that is the
|
|
646
|
+
* brand blue on a near-black blue, and the selected state was the one you could
|
|
647
|
+
* read least — the opposite of what selecting something should do. */
|
|
648
|
+
.pc-seg-opt--active {
|
|
625
649
|
border-color: var(--sa-color-accent);
|
|
626
|
-
background: var(--sa-color-accent-surface);
|
|
627
|
-
color: var(--sa-color-accent);
|
|
650
|
+
background: var(--sa-color-accent-surface-strong);
|
|
651
|
+
color: var(--sa-color-accent-strong);
|
|
628
652
|
}
|
|
629
653
|
|
|
630
654
|
.pc-check {
|
|
@@ -107,7 +107,7 @@ function remove(): void {
|
|
|
107
107
|
border: 1px dashed var(--sa-color-accent-border);
|
|
108
108
|
background: var(--sa-color-accent-surface-soft);
|
|
109
109
|
border-radius: 10px;
|
|
110
|
-
color: var(--sa-color-accent);
|
|
110
|
+
color: var(--sa-color-accent-strong);
|
|
111
111
|
font-weight: 700;
|
|
112
112
|
font-size: var(--sa-text-sm);
|
|
113
113
|
cursor: pointer;
|
|
@@ -825,23 +825,61 @@ async function executeTerminate(): Promise<void> {
|
|
|
825
825
|
);
|
|
826
826
|
border-color: var(--sa-color-border);
|
|
827
827
|
}
|
|
828
|
+
/* The marker block at the head of a diff row.
|
|
829
|
+
*
|
|
830
|
+
* Every rule below carries BOTH halves of its pair, and that is deliberate:
|
|
831
|
+
* the geometry used to live here and the background one rule further down,
|
|
832
|
+
* which made neither rule judgeable on its own. `theme-role-contrast` needs a
|
|
833
|
+
* background and a colour in the same body to measure anything, so the split
|
|
834
|
+
* hid `--sa-color-fg-on-accent` on `--sa-color-positive-strong` — white on
|
|
835
|
+
* green-500, 2.54:1, in BOTH themes — for as long as the component existed.
|
|
836
|
+
*
|
|
837
|
+
* A tint plus the tone's `-fg`, not a solid plus white: `-strong` is the rung
|
|
838
|
+
* chosen to read as a COLOUR against its theme's surface, which puts it in the
|
|
839
|
+
* middle of the lightness range — and nothing reads on the middle. That is
|
|
840
|
+
* what `<tone>-fg` and `-surface-strong` are for. */
|
|
828
841
|
.pd-diff-icon {
|
|
829
842
|
width: 32px;
|
|
830
843
|
align-self: stretch;
|
|
831
844
|
display: grid;
|
|
832
845
|
place-items: center;
|
|
833
846
|
flex: 0 0 32px;
|
|
834
|
-
|
|
847
|
+
background: var(--sa-color-bg-sunken);
|
|
848
|
+
color: var(--sa-color-fg-body);
|
|
835
849
|
font: 700 var(--sa-text-lg) var(--sa-font-body);
|
|
836
850
|
}
|
|
837
851
|
.pd-diff-row.add .pd-diff-icon {
|
|
838
|
-
background: var(--sa-color-positive-strong);
|
|
852
|
+
background: var(--sa-color-positive-surface-strong);
|
|
853
|
+
color: var(--sa-color-positive-fg);
|
|
839
854
|
}
|
|
840
855
|
.pd-diff-row.rm .pd-diff-icon {
|
|
841
|
-
background: var(--sa-color-negative-strong);
|
|
856
|
+
background: var(--sa-color-negative-surface-strong);
|
|
857
|
+
color: var(--sa-color-negative-fg);
|
|
842
858
|
}
|
|
859
|
+
/* Unchanged, and deliberately not brought into line with the two above: it was
|
|
860
|
+
* already readable (14.6:1) and `--sa-color-bg-sunken` is what the `mod` row
|
|
861
|
+
* itself starts its gradient with, so a neutral tint here would leave the
|
|
862
|
+
* marker with almost nothing to stand against in the light theme. It only
|
|
863
|
+
* gains the foreground it was already inheriting, so the rule can be read. */
|
|
843
864
|
.pd-diff-row.mod .pd-diff-icon {
|
|
844
865
|
background: var(--sa-color-inverse-surface);
|
|
866
|
+
color: var(--sa-color-inverse-fg);
|
|
867
|
+
}
|
|
868
|
+
/* The single-version listing marks WHAT a row is rather than what changed, so
|
|
869
|
+
* it reads the catalogue-entity roles. These three were the last colour
|
|
870
|
+
* literals in a template: `style="background: #f59e0b"` and friends, painted
|
|
871
|
+
* white by the base rule above at 2.15:1, 2.77:1 and 4.23:1. */
|
|
872
|
+
.pd-diff-row.plain .pd-diff-icon.feature {
|
|
873
|
+
background: var(--sa-color-feature-surface);
|
|
874
|
+
color: var(--sa-color-feature-fg);
|
|
875
|
+
}
|
|
876
|
+
.pd-diff-row.plain .pd-diff-icon.quota {
|
|
877
|
+
background: var(--sa-color-quota-surface);
|
|
878
|
+
color: var(--sa-color-quota-fg);
|
|
879
|
+
}
|
|
880
|
+
.pd-diff-row.plain .pd-diff-icon.bundle {
|
|
881
|
+
background: var(--sa-color-bundle-surface);
|
|
882
|
+
color: var(--sa-color-bundle-fg);
|
|
845
883
|
}
|
|
846
884
|
.pd-diff-body {
|
|
847
885
|
flex: 1;
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
:key="'cf-' + f"
|
|
67
67
|
class="pd-diff-row plain"
|
|
68
68
|
>
|
|
69
|
-
<div class="pd-diff-icon
|
|
69
|
+
<div class="pd-diff-icon feature">·</div>
|
|
70
70
|
<div class="pd-diff-body">
|
|
71
71
|
<span class="pd-diff-label">{{ featureLabel(f) }}</span>
|
|
72
72
|
<code class="pd-diff-key">{{ f }}</code>
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
:key="'cq-' + k"
|
|
89
89
|
class="pd-diff-row plain"
|
|
90
90
|
>
|
|
91
|
-
<div class="pd-diff-icon
|
|
91
|
+
<div class="pd-diff-icon quota">·</div>
|
|
92
92
|
<div class="pd-diff-body">
|
|
93
93
|
<span class="pd-diff-label">{{ quotaLabel(k) }}</span>
|
|
94
94
|
<code class="pd-diff-key">{{ k }}</code>
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
:key="'cb-' + b"
|
|
112
112
|
class="pd-diff-row plain"
|
|
113
113
|
>
|
|
114
|
-
<div class="pd-diff-icon
|
|
114
|
+
<div class="pd-diff-icon bundle">·</div>
|
|
115
115
|
<div class="pd-diff-body">
|
|
116
116
|
<span class="pd-diff-label">{{ bundleLabel(b) }}</span>
|
|
117
117
|
<code class="pd-diff-key">{{ b }}</code>
|
|
@@ -811,7 +811,7 @@ function formatQuota(v: number | undefined): string {
|
|
|
811
811
|
height: 30px;
|
|
812
812
|
border-radius: 50%;
|
|
813
813
|
background: var(--sa-color-accent-surface-strong);
|
|
814
|
-
color: var(--sa-color-accent);
|
|
814
|
+
color: var(--sa-color-accent-strong);
|
|
815
815
|
margin: 0 auto 6px;
|
|
816
816
|
}
|
|
817
817
|
.pm-add-title {
|
|
@@ -25,9 +25,13 @@
|
|
|
25
25
|
|
|
26
26
|
<div class="pve-prev-window" :class="`pve-prev-window--${previewMode}`">
|
|
27
27
|
<div class="pve-prev-chrome">
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
<!-- The same window controls as MarketingCatalogPreview, drawn
|
|
29
|
+
a second time, and the same reason for the `-strong`
|
|
30
|
+
rungs. The chrome itself is duplicated markup and belongs
|
|
31
|
+
in one component; that is a Phase 4 job, not a colour one. -->
|
|
32
|
+
<span class="pve-prev-dot" style="background: var(--sa-color-negative-strong)" />
|
|
33
|
+
<span class="pve-prev-dot" style="background: var(--sa-color-warning-strong)" />
|
|
34
|
+
<span class="pve-prev-dot" style="background: var(--sa-color-positive-strong)" />
|
|
31
35
|
<div class="pve-prev-url">{{ catalogUrl }}</div>
|
|
32
36
|
</div>
|
|
33
37
|
<div class="pve-prev-body">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="sa-manifest-error">
|
|
2
|
+
<div class="sa-page sa-manifest-error">
|
|
3
3
|
<q-card class="sa-manifest-error__card">
|
|
4
4
|
<q-card-section class="sa-manifest-error__head">
|
|
5
5
|
<q-icon name="cloud_off" size="32px" color="negative" />
|
|
@@ -112,6 +112,9 @@ function logout(): void | Promise<void> {
|
|
|
112
112
|
padding: 32px;
|
|
113
113
|
display: flex;
|
|
114
114
|
justify-content: center;
|
|
115
|
+
/* The page frame is a full viewport tall, and a stretched flex item would
|
|
116
|
+
* make this card as tall as the screen for four lines of text. */
|
|
117
|
+
align-items: flex-start;
|
|
115
118
|
}
|
|
116
119
|
.sa-manifest-error__card {
|
|
117
120
|
max-width: 640px;
|
|
@@ -405,7 +405,7 @@ function barWidth(value: number, max?: number): number {
|
|
|
405
405
|
/* Header look comes from the global .sa-page-head — only a margin tweak here. */
|
|
406
406
|
|
|
407
407
|
.sa-kpi__icon :deep(.q-icon) {
|
|
408
|
-
color: var(--sa-color-accent);
|
|
408
|
+
color: var(--sa-color-accent-strong);
|
|
409
409
|
}
|
|
410
410
|
|
|
411
411
|
.sa-dashboard__rows {
|
|
@@ -423,7 +423,7 @@ function barWidth(value: number, max?: number): number {
|
|
|
423
423
|
|
|
424
424
|
.sa-dashboard__count {
|
|
425
425
|
background: var(--sa-color-accent-surface);
|
|
426
|
-
color: var(--sa-color-accent);
|
|
426
|
+
color: var(--sa-color-accent-strong);
|
|
427
427
|
font-size: var(--sa-text-xs);
|
|
428
428
|
font-weight: 700;
|
|
429
429
|
padding: 2px 7px;
|
|
@@ -492,7 +492,7 @@ function barWidth(value: number, max?: number): number {
|
|
|
492
492
|
border-color: var(--sa-color-accent-border);
|
|
493
493
|
}
|
|
494
494
|
.sa-dashboard__shortcut :deep(.q-icon) {
|
|
495
|
-
color: var(--sa-color-accent);
|
|
495
|
+
color: var(--sa-color-accent-strong);
|
|
496
496
|
flex-shrink: 0;
|
|
497
497
|
}
|
|
498
498
|
.sa-dashboard__shortcut-title {
|
|
@@ -228,25 +228,59 @@ const statusFilterOptions = computed<Array<{ label: string; value: DiscoveryStat
|
|
|
228
228
|
],
|
|
229
229
|
);
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
// `?.app?.` rather than `?.app.` — the optional chain has to survive the whole
|
|
232
|
+
// path, not just its first step. `useDiscovery` assigns the response body with
|
|
233
|
+
// an unchecked `as DiscoverySnapshot`, so a 200 whose body is not a snapshot
|
|
234
|
+
// (an older backend, a proxy's JSON error page, a partial response) reaches
|
|
235
|
+
// here as a non-null value without `app`. The guard then stopped one step short
|
|
236
|
+
// and the page threw during a computed — which white-screens the route rather
|
|
237
|
+
// than showing the dash these fallbacks exist for.
|
|
238
|
+
// Narrowed to a string, not merely to "present". The type says `key` is a
|
|
239
|
+
// string; the runtime value is whatever the server sent, because `useDiscovery`
|
|
240
|
+
// assigns the body with an unchecked `as DiscoverySnapshot`. A truthy non-string
|
|
241
|
+
// — `{"app":{"key":1}}` — passes an existence check and then throws on
|
|
242
|
+
// `.charAt`, which is the same white-screen one step further in. Guarding
|
|
243
|
+
// presence without guarding type only moves the crash.
|
|
244
|
+
const appKeyText = computed(() => {
|
|
245
|
+
const key = props.snapshot?.app?.key;
|
|
246
|
+
return typeof key === 'string' ? key : '';
|
|
247
|
+
});
|
|
248
|
+
const appKey = computed(() => appKeyText.value || '—');
|
|
232
249
|
const appLabel = computed(() => {
|
|
233
|
-
const k =
|
|
250
|
+
const k = appKeyText.value;
|
|
234
251
|
return k ? k.charAt(0).toUpperCase() + k.slice(1) : 'Discovery';
|
|
235
252
|
});
|
|
236
|
-
const appVersion = computed(() =>
|
|
253
|
+
const appVersion = computed(() => {
|
|
254
|
+
const version = props.snapshot?.app?.version;
|
|
255
|
+
return typeof version === 'string' && version ? version : '0.0.0';
|
|
256
|
+
});
|
|
237
257
|
const scanLabel = computed(() => {
|
|
238
|
-
|
|
258
|
+
const scannedAt = props.snapshot?.scannedAt;
|
|
259
|
+
// Same reasoning: the catch below covers a bad DATE, not a bad type — the
|
|
260
|
+
// fallback `return scannedAt` would hand the template a non-string.
|
|
261
|
+
if (typeof scannedAt !== 'string' || !scannedAt) return msg.value.notScannedYet;
|
|
239
262
|
try {
|
|
240
|
-
return new Date(
|
|
263
|
+
return new Date(scannedAt).toLocaleString(intlLocale.value);
|
|
241
264
|
} catch {
|
|
242
|
-
return
|
|
265
|
+
return scannedAt;
|
|
243
266
|
}
|
|
244
267
|
});
|
|
245
268
|
|
|
246
269
|
const declaredAtByKey = computed<Record<string, string>>(() => {
|
|
247
270
|
const map: Record<string, string> = {};
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
// `Array.isArray`, not `?? []`. The nullish fallback only covers `null` and
|
|
272
|
+
// `undefined`; anything else present goes straight into `for…of`, and a
|
|
273
|
+
// JSON object or a number is not iterable — `for (const c of {})` throws,
|
|
274
|
+
// which takes the route down exactly like the string methods above did.
|
|
275
|
+
//
|
|
276
|
+
// A string would have slipped through both: it IS iterable, so the loop
|
|
277
|
+
// walks its characters and silently builds nonsense. That is why the case
|
|
278
|
+
// covering this in the test suite had to be an object rather than a string —
|
|
279
|
+
// the first version used a string and passed without exercising anything.
|
|
280
|
+
const capabilities = props.snapshot?.capabilities;
|
|
281
|
+
if (!Array.isArray(capabilities)) return map;
|
|
282
|
+
for (const c of capabilities) {
|
|
283
|
+
if (c && typeof c.capabilityKey === 'string') map[c.capabilityKey] = c.declaredAt;
|
|
250
284
|
}
|
|
251
285
|
return map;
|
|
252
286
|
});
|
|
@@ -1529,14 +1529,22 @@ async function onLocaleChange(loc: string): Promise<void> {
|
|
|
1529
1529
|
transform: rotate(90deg);
|
|
1530
1530
|
}
|
|
1531
1531
|
|
|
1532
|
+
/* The open editor is a WELL under its row, not another row.
|
|
1533
|
+
*
|
|
1534
|
+
* It used to end on a gradient whose last stop was `--sa-color-bg-surface` —
|
|
1535
|
+
* exactly the colour of the next row — closed by the same soft hairline that
|
|
1536
|
+
* separates any two rows. Nothing then said where the editor stopped, so the
|
|
1537
|
+
* following plan read as part of the plan being edited.
|
|
1538
|
+
*
|
|
1539
|
+
* Three things say it now, and each carries a different half of the message:
|
|
1540
|
+
* the surface is recessed (`bg-sunken`, which is what a well is for), an accent
|
|
1541
|
+
* edge on the left ties the panel to the row it belongs to, and a real border
|
|
1542
|
+
* closes it at the bottom instead of a hairline that means "next row". */
|
|
1532
1543
|
.sa-marketing-admin-expand {
|
|
1533
1544
|
grid-column: 1 / -1;
|
|
1534
|
-
background:
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
var(--sa-color-bg-surface) 100%
|
|
1538
|
-
);
|
|
1539
|
-
border-bottom: 1px solid var(--sa-color-border-soft);
|
|
1545
|
+
background: var(--sa-color-bg-sunken);
|
|
1546
|
+
border-left: 2px solid var(--sa-color-accent);
|
|
1547
|
+
border-bottom: 2px solid var(--sa-color-border-strong);
|
|
1540
1548
|
padding: 18px 20px 22px;
|
|
1541
1549
|
}
|
|
1542
1550
|
.sa-marketing-expand-grid {
|
|
@@ -1716,8 +1724,13 @@ async function onLocaleChange(loc: string): Promise<void> {
|
|
|
1716
1724
|
color 0.12s;
|
|
1717
1725
|
}
|
|
1718
1726
|
.sa-marketing-tf-chip em {
|
|
1727
|
+
/* No colour of its own — the mono face and the smaller step already set it
|
|
1728
|
+
* apart from the chip's label, and a colour here does not follow the chip.
|
|
1729
|
+
* `--sa-color-fg-subtle` stayed put when :hover moved the chip's surface to
|
|
1730
|
+
* a 22 % accent tint, which measured 2.92:1 in the dark theme: a foreground
|
|
1731
|
+
* declared without the background it will end up on is a pair nothing can
|
|
1732
|
+
* check, and this one was wrong in exactly the state nobody screenshots. */
|
|
1719
1733
|
font-style: normal;
|
|
1720
|
-
color: var(--sa-color-fg-subtle);
|
|
1721
1734
|
font: 500 var(--sa-text-xs) var(--sa-font-mono);
|
|
1722
1735
|
}
|
|
1723
1736
|
.sa-marketing-tf-chip:hover:not(:disabled) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
:icon="iconText"
|
|
6
6
|
@done="needsSetup = false"
|
|
7
7
|
/>
|
|
8
|
-
<div v-else class="sa-login-wrap">
|
|
8
|
+
<div v-else class="sa-page sa-login-wrap">
|
|
9
9
|
<div class="sa-login-card">
|
|
10
10
|
<div class="sa-login-brand">
|
|
11
11
|
<div v-if="logoUrl" class="sa-login-logo sa-login-logo--img">
|
|
@@ -285,7 +285,12 @@ async function handleSubmit(): Promise<void> {
|
|
|
285
285
|
line-height: 1.5;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
/* The heading block is one unit, and the form sits the same distance below it
|
|
289
|
+
* whether or not the optional subtitle is there. Without this the gap was the
|
|
290
|
+
* title's 6px on a login that passes no subtitle — which nobody saw, because
|
|
291
|
+
* Quasar's element-level `h1` line-height was padding it out to 96px. */
|
|
288
292
|
.sa-login-form {
|
|
293
|
+
margin-top: var(--sa-space-6);
|
|
289
294
|
display: flex;
|
|
290
295
|
flex-direction: column;
|
|
291
296
|
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="sa-marketing-window">
|
|
3
3
|
<div class="sa-marketing-chrome">
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
<!-- The window controls of the browser this mock-up draws. Their hue
|
|
5
|
+
is fixed by what they depict rather than by what they mean, and
|
|
6
|
+
the `-strong` rungs are exactly that hue — the three replaced
|
|
7
|
+
the same three literals byte for byte. Only the amber differs
|
|
8
|
+
between themes, and it lifts one rung in the dark one. -->
|
|
9
|
+
<span
|
|
10
|
+
class="sa-marketing-chrome-dot"
|
|
11
|
+
style="background: var(--sa-color-negative-strong)"
|
|
12
|
+
/>
|
|
13
|
+
<span
|
|
14
|
+
class="sa-marketing-chrome-dot"
|
|
15
|
+
style="background: var(--sa-color-warning-strong)"
|
|
16
|
+
/>
|
|
17
|
+
<span
|
|
18
|
+
class="sa-marketing-chrome-dot"
|
|
19
|
+
style="background: var(--sa-color-positive-strong)"
|
|
20
|
+
/>
|
|
7
21
|
<div class="sa-marketing-chrome-url">{{ previewUrl }}</div>
|
|
8
22
|
</div>
|
|
9
23
|
<div class="sa-marketing-canvas">
|
|
@@ -305,7 +305,7 @@ defineExpose({ draft });
|
|
|
305
305
|
font-size: var(--sa-text-xs);
|
|
306
306
|
letter-spacing: 0.1em;
|
|
307
307
|
text-transform: uppercase;
|
|
308
|
-
color: var(--sa-color-accent);
|
|
308
|
+
color: var(--sa-color-accent-strong);
|
|
309
309
|
background: var(--sa-color-accent-surface);
|
|
310
310
|
padding: 6px 11px;
|
|
311
311
|
border-radius: 999px;
|
package/src/ui/theme/base.css
CHANGED
|
@@ -22,7 +22,19 @@
|
|
|
22
22
|
* The font and text colour belong here because the hosting app's stylesheet
|
|
23
23
|
* loads after this one: without a rule on the page itself, admin pages would
|
|
24
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.
|
|
25
|
+
* their own root, which is exactly the divergence this prevents.
|
|
26
|
+
*
|
|
27
|
+
* `.sa-page` is also the theme's REACH MARKER, and that is the larger job: it
|
|
28
|
+
* and `.sa-portal` are the only two prefixes through which the component layer
|
|
29
|
+
* corrects Quasar's own DOM. A screen without one of them gets Quasar's look —
|
|
30
|
+
* its 4px radii, its transparent outlined control, its neutral #1d1d1d card in
|
|
31
|
+
* dark mode — and nothing announces it. The login and setup screens rendered
|
|
32
|
+
* that way for their whole existence, and they are the first thing a user sees.
|
|
33
|
+
*
|
|
34
|
+
* Which is why the screens OUTSIDE the shell carry it too, even though they
|
|
35
|
+
* bring their own frame: their own root class is more specific than this one,
|
|
36
|
+
* so the min-height and padding below stay theirs and only the reach is shared.
|
|
37
|
+
* `tests/theme-reaches-every-page.test.js` holds every page to it. */
|
|
26
38
|
.sa-page {
|
|
27
39
|
min-height: calc(100vh - 56px);
|
|
28
40
|
padding: 20px 28px 28px;
|
|
@@ -36,3 +36,47 @@
|
|
|
36
36
|
flex: 1;
|
|
37
37
|
min-width: 200px;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
/* Quasar's own accent-coloured text, painted from the role that is safe on a
|
|
41
|
+
* dark surface.
|
|
42
|
+
*
|
|
43
|
+
* Quasar colours the focused field's floating label and the selected menu item
|
|
44
|
+
* with `--q-primary` — the brand itself, which does not change between themes.
|
|
45
|
+
* On a light card that is fine; on the dark theme's slate surfaces the brand
|
|
46
|
+
* blue lands around 3.4:1 and the shrunk label is small text, so it reads as
|
|
47
|
+
* decoration rather than as the name of the field you are typing in.
|
|
48
|
+
*
|
|
49
|
+
* `--sa-color-accent-strong` is the same brand mixed toward the theme's own
|
|
50
|
+
* extreme — darker in light, lighter in dark — which is exactly what the guide
|
|
51
|
+
* prescribes for accent text. The colour still follows the host's `$primary`,
|
|
52
|
+
* so nothing about branding changes.
|
|
53
|
+
*
|
|
54
|
+
* `q-field--highlighted` is the whole story and `q-field--focused` would be
|
|
55
|
+
* noise: Quasar's only label-colour rule is
|
|
56
|
+
* `.q-field--highlighted .q-field__label { color: currentColor }`, and it sets
|
|
57
|
+
* that class whenever the field is focused OR invalid.
|
|
58
|
+
*
|
|
59
|
+
* Which is why the invalid half has to be carved back out. Quasar paints an
|
|
60
|
+
* invalid field's red by putting `text-negative` on the CONTROL and letting the
|
|
61
|
+
* label inherit it through that `currentColor`. Naming any colour on the label
|
|
62
|
+
* cuts the inheritance, and `!important` on `.text-negative` cannot defend it,
|
|
63
|
+
* because that declaration sits on a different element. Without the `:not()` a
|
|
64
|
+
* field with `rules` or `:error` — the login, setup and confirm forms — would
|
|
65
|
+
* announce its failure in every indicator except the one naming the field.
|
|
66
|
+
*
|
|
67
|
+
* `.sa-portal` alongside `.sa-page` for the usual reason: Quasar teleports every
|
|
68
|
+
* menu to `<body>`, so a `.sa-page`-prefixed rule never reaches a dropdown. */
|
|
69
|
+
.sa-page .q-field--highlighted:not(.q-field--error) .q-field__label,
|
|
70
|
+
.sa-portal .q-field--highlighted:not(.q-field--error) .q-field__label {
|
|
71
|
+
color: var(--sa-color-accent-strong);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The chosen entry in an open select. Quasar marks it `q-item--active` and
|
|
75
|
+
* colours it with the brand; on the dark menu surface that is the same 3.4:1,
|
|
76
|
+
* and it is the one line in the list that has to be legible. */
|
|
77
|
+
.sa-page .q-item--active,
|
|
78
|
+
.sa-portal .q-item--active,
|
|
79
|
+
.sa-page .q-item.q-manual-focusable--focused,
|
|
80
|
+
.sa-portal .q-item.q-manual-focusable--focused {
|
|
81
|
+
color: var(--sa-color-accent-strong);
|
|
82
|
+
}
|
|
@@ -120,9 +120,11 @@ body.body--dark {
|
|
|
120
120
|
--sa-color-feature-fg: var(--sa-violet-200);
|
|
121
121
|
--sa-color-feature-surface: color-mix(in srgb, var(--sa-violet-500) 16%, transparent);
|
|
122
122
|
--sa-color-quota: var(--sa-sky-500);
|
|
123
|
+
--sa-color-quota-fg: var(--sa-sky-200);
|
|
123
124
|
--sa-color-quota-surface: color-mix(in srgb, var(--sa-sky-500) 16%, transparent);
|
|
124
125
|
--sa-color-quota-border: color-mix(in srgb, var(--sa-sky-500) 36%, transparent);
|
|
125
126
|
--sa-color-bundle: var(--sa-amber-500);
|
|
127
|
+
--sa-color-bundle-fg: var(--sa-amber-200);
|
|
126
128
|
--sa-color-bundle-surface: color-mix(in srgb, var(--sa-amber-500) 16%, transparent);
|
|
127
129
|
|
|
128
130
|
/* The identity ramp, one rung lighter throughout — see the light file for
|
|
@@ -141,16 +141,25 @@
|
|
|
141
141
|
--sa-color-scheduled-surface-strong: var(--sa-indigo-100);
|
|
142
142
|
--sa-color-scheduled-border: var(--sa-indigo-200);
|
|
143
143
|
|
|
144
|
-
/* ── Catalogue entities. Not statuses: these
|
|
144
|
+
/* ── Catalogue entities. Not statuses: these three say WHAT a row is, and
|
|
145
145
|
* the plan editor, the matrix, the review and the discovery page all need
|
|
146
|
-
* to agree, or the same dot means two things on two screens.
|
|
146
|
+
* to agree, or the same dot means two things on two screens.
|
|
147
|
+
*
|
|
148
|
+
* Each one has the same three slots a status tone has: the colour itself,
|
|
149
|
+
* a tinted SURFACE, and an `-fg` that is readable on that surface. Only
|
|
150
|
+
* `feature` had the third, and the two that were missing are why a diff
|
|
151
|
+
* marker painted white on `--sa-sky-500` (2.77:1) and on `--sa-amber-500`
|
|
152
|
+
* (2.15:1): with no role to point at, the panel reached past the layer for
|
|
153
|
+
* the primitive and wrote it into the template. ───────────────────────── */
|
|
147
154
|
--sa-color-feature: var(--sa-violet-500);
|
|
148
155
|
--sa-color-feature-fg: var(--sa-violet-700);
|
|
149
156
|
--sa-color-feature-surface: var(--sa-violet-50);
|
|
150
157
|
--sa-color-quota: var(--sa-sky-500);
|
|
158
|
+
--sa-color-quota-fg: var(--sa-sky-700);
|
|
151
159
|
--sa-color-quota-surface: var(--sa-sky-50);
|
|
152
160
|
--sa-color-quota-border: var(--sa-sky-200);
|
|
153
161
|
--sa-color-bundle: var(--sa-amber-500);
|
|
162
|
+
--sa-color-bundle-fg: var(--sa-amber-700);
|
|
154
163
|
--sa-color-bundle-surface: var(--sa-amber-100);
|
|
155
164
|
|
|
156
165
|
/* ── Identity. A CATEGORICAL ramp: the only job of these seven is to be
|