@saasicat/ui-vue 0.24.0 → 0.24.1
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/dialogs/PromoCodeDialogFields.vue +35 -14
- package/src/components/plan/PromoCodeInput.vue +1 -1
- package/src/components/plan-matrix/PlanMatrix.vue +1 -1
- 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 +14 -6
- package/src/pages-standard/SuperAdminLoginPage.vue +6 -1
- package/src/pages-standard/SuperAdminSetupWizard.vue +1 -1
- 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
|
@@ -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.1",
|
|
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.1"
|
|
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
|
}
|
|
@@ -101,13 +101,13 @@
|
|
|
101
101
|
<div class="pc-grid pc-grid--2">
|
|
102
102
|
<div class="pc-field">
|
|
103
103
|
<div class="pc-field__label">{{ msg.form.durationLabel }}</div>
|
|
104
|
-
<div class="pc-
|
|
104
|
+
<div class="pc-seg pc-seg--fill">
|
|
105
105
|
<button
|
|
106
106
|
v-for="o in durationOptions"
|
|
107
107
|
:key="o.k"
|
|
108
108
|
type="button"
|
|
109
|
-
class="pc-
|
|
110
|
-
:class="{ 'pc-
|
|
109
|
+
class="pc-seg-opt"
|
|
110
|
+
:class="{ 'pc-seg-opt--active': form.durationType === o.k }"
|
|
111
111
|
@click="form.durationType = o.k"
|
|
112
112
|
>
|
|
113
113
|
{{ o.label }}
|
|
@@ -161,13 +161,13 @@
|
|
|
161
161
|
<div v-if="mode === 'edit'" class="pc-grid pc-grid--2">
|
|
162
162
|
<div class="pc-field">
|
|
163
163
|
<div class="pc-field__label">{{ common.status }}</div>
|
|
164
|
-
<div class="pc-status">
|
|
164
|
+
<div class="pc-seg pc-status">
|
|
165
165
|
<button
|
|
166
166
|
v-for="o in statusOptions"
|
|
167
167
|
:key="o.k"
|
|
168
168
|
type="button"
|
|
169
|
-
class="pc-
|
|
170
|
-
:class="{ 'pc-
|
|
169
|
+
class="pc-seg-opt"
|
|
170
|
+
:class="{ 'pc-seg-opt--active': form.status === o.k }"
|
|
171
171
|
@click="form.status = o.k"
|
|
172
172
|
>
|
|
173
173
|
<q-icon :name="o.icon" size="14px" />
|
|
@@ -595,8 +595,8 @@ textarea.pc-input {
|
|
|
595
595
|
|
|
596
596
|
.pc-plan-opt--on {
|
|
597
597
|
border-color: var(--sa-color-accent);
|
|
598
|
-
background: var(--sa-color-accent-surface);
|
|
599
|
-
color: var(--sa-color-accent);
|
|
598
|
+
background: var(--sa-color-accent-surface-strong);
|
|
599
|
+
color: var(--sa-color-accent-strong);
|
|
600
600
|
}
|
|
601
601
|
|
|
602
602
|
.pc-plan-opt__mark {
|
|
@@ -605,13 +605,25 @@ textarea.pc-input {
|
|
|
605
605
|
border-radius: 50%;
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
.
|
|
608
|
+
/* One segmented control for both the duration and the status row. They are the
|
|
609
|
+
* same thing — a row of buttons of which one is chosen — and the status row had
|
|
610
|
+
* NO rules at all, so it rendered as raw browser buttons: white boxes on a dark
|
|
611
|
+
* dialog. Sharing the recipe is what makes the two look alike without either
|
|
612
|
+
* restating it. */
|
|
613
|
+
.pc-seg {
|
|
609
614
|
display: flex;
|
|
610
615
|
gap: 4px;
|
|
611
616
|
}
|
|
612
|
-
|
|
613
|
-
.pc-
|
|
617
|
+
/* The duration options divide the row; the status options size to their label. */
|
|
618
|
+
.pc-seg--fill .pc-seg-opt {
|
|
614
619
|
flex: 1;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.pc-seg-opt {
|
|
623
|
+
display: inline-flex;
|
|
624
|
+
align-items: center;
|
|
625
|
+
justify-content: center;
|
|
626
|
+
gap: var(--sa-gap-inline);
|
|
615
627
|
border: 1px solid var(--sa-color-border);
|
|
616
628
|
background: var(--sa-color-bg-surface);
|
|
617
629
|
border-radius: 7px;
|
|
@@ -621,10 +633,19 @@ textarea.pc-input {
|
|
|
621
633
|
color: var(--sa-color-fg-secondary);
|
|
622
634
|
}
|
|
623
635
|
|
|
624
|
-
.pc-
|
|
636
|
+
.pc-seg-opt:hover {
|
|
637
|
+
border-color: var(--sa-color-border-strong);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/* `-accent-strong` on `-surface-strong`, which is what the design guide
|
|
641
|
+
* prescribes for accent text on a tint. The chosen option used to pair plain
|
|
642
|
+
* `--sa-color-accent` with the 8 % wash of itself: in dark mode that is the
|
|
643
|
+
* brand blue on a near-black blue, and the selected state was the one you could
|
|
644
|
+
* read least — the opposite of what selecting something should do. */
|
|
645
|
+
.pc-seg-opt--active {
|
|
625
646
|
border-color: var(--sa-color-accent);
|
|
626
|
-
background: var(--sa-color-accent-surface);
|
|
627
|
-
color: var(--sa-color-accent);
|
|
647
|
+
background: var(--sa-color-accent-surface-strong);
|
|
648
|
+
color: var(--sa-color-accent-strong);
|
|
628
649
|
}
|
|
629
650
|
|
|
630
651
|
.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;
|
|
@@ -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 {
|
|
@@ -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 {
|
|
@@ -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
|
}
|
|
@@ -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
|
+
}
|