@saasicat/ui-vue 0.24.1 → 0.25.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/package.json +2 -2
- package/src/components/admin-page/AdminAccordion.vue +125 -0
- 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 +5 -2
- package/src/components/plan-detail/PlanDetail.vue +41 -3
- package/src/components/plan-detail/PlanVersionDiffPanel.vue +3 -3
- package/src/components/plan-version-editor/PlanCatalogPreview.vue +7 -3
- package/src/pages-standard/BundlesPage.vue +5 -35
- package/src/pages-standard/MarketingCatalogPage.vue +6 -1
- package/src/pages-standard/bundles-page/BundleAccordionList.vue +30 -32
- package/src/pages-standard/discovery-page/DiscoveryFeatureCard.vue +73 -86
- package/src/pages-standard/discovery-page/DiscoveryQuotaCard.vue +73 -79
- package/src/pages-standard/marketing-catalog/MarketingCatalogPreview.vue +17 -3
- package/src/ui/theme/components/accordion.css +118 -0
- package/src/ui/theme/index.css +1 -0
- package/src/ui/theme/tokens.semantic.dark.css +2 -0
- package/src/ui/theme/tokens.semantic.light.css +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/ui-vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
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.
|
|
64
|
+
"@saasicat/types": "^0.25.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"pinia": "^2.0.0 || ^3.0.0",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sa-accordion" :class="{ 'sa-accordion--open': open }">
|
|
3
|
+
<div class="sa-accordion__head">
|
|
4
|
+
<button
|
|
5
|
+
:id="triggerId"
|
|
6
|
+
type="button"
|
|
7
|
+
class="sa-accordion__trigger"
|
|
8
|
+
:aria-expanded="open"
|
|
9
|
+
:aria-controls="bodyId"
|
|
10
|
+
:disabled="disabled"
|
|
11
|
+
@click="emit('update:open', !open)"
|
|
12
|
+
>
|
|
13
|
+
<span
|
|
14
|
+
v-if="$slots.mark"
|
|
15
|
+
class="sa-accordion__mark"
|
|
16
|
+
:class="`sa-accordion__mark--${markTone}`"
|
|
17
|
+
>
|
|
18
|
+
<slot name="mark" />
|
|
19
|
+
</span>
|
|
20
|
+
<span class="sa-accordion__label"><slot name="header" /></span>
|
|
21
|
+
<q-icon name="chevron_right" size="18px" class="sa-accordion__chev" />
|
|
22
|
+
</button>
|
|
23
|
+
<div v-if="$slots['header-actions']" class="sa-accordion__actions">
|
|
24
|
+
<slot name="header-actions" />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div
|
|
28
|
+
v-if="open"
|
|
29
|
+
:id="bodyId"
|
|
30
|
+
role="region"
|
|
31
|
+
:aria-labelledby="triggerId"
|
|
32
|
+
class="sa-accordion__body"
|
|
33
|
+
>
|
|
34
|
+
<slot />
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup lang="ts">
|
|
40
|
+
import { useId } from 'vue';
|
|
41
|
+
|
|
42
|
+
// One disclosure: a header that opens a body. Eight surfaces in this package
|
|
43
|
+
// built their own, in three incompatible idioms, and agreed on nothing —
|
|
44
|
+
// three header paddings, three body paddings, two radii, two colours for the
|
|
45
|
+
// open border, two transition durations, and hover feedback on exactly one of
|
|
46
|
+
// six despite five setting `cursor: pointer`.
|
|
47
|
+
//
|
|
48
|
+
// The reason is not that eight decisions were taken. It is that none was: there
|
|
49
|
+
// is an `AdminTable` and an `AdminSection`, and for "open this" there was
|
|
50
|
+
// nothing, so every page wrote it again. `admin-page-shell.test.ts` rule 14
|
|
51
|
+
// records the same story for tables — "nine pages used QTable directly and
|
|
52
|
+
// agreed on nothing".
|
|
53
|
+
//
|
|
54
|
+
// **The trigger is a `<button>`, and that is the point of the component.**
|
|
55
|
+
// Four of the eight were a `<div>` with a click handler: no `tabindex`, no
|
|
56
|
+
// keyboard, and a screen reader announcing text rather than a control. A grep
|
|
57
|
+
// for `aria-expanded` across the whole package returned nothing at all. A
|
|
58
|
+
// shared component has that argument once; eight surfaces have to win it eight
|
|
59
|
+
// times, and the score was 4:4.
|
|
60
|
+
//
|
|
61
|
+
// `header-actions` is a SIBLING of the button rather than a slot inside it, and
|
|
62
|
+
// both halves of that matter. A control inside the trigger would fire the
|
|
63
|
+
// toggle on its way past — the discovery cards put a live status control in
|
|
64
|
+
// their clickable header — and interactive content nested in a `<button>` is
|
|
65
|
+
// not valid HTML, so the browser's own behaviour there is not something to
|
|
66
|
+
// rely on.
|
|
67
|
+
//
|
|
68
|
+
// The state is NOT owned here. Five of the eight take it from the page
|
|
69
|
+
// (`openKey === row.id`), and in the bundle list opening one card while closing
|
|
70
|
+
// another is tied to loading that bundle's versions — a component that owned a
|
|
71
|
+
// boolean would break that. The other four simply hold the id, and lose nothing
|
|
72
|
+
// by the arrangement. `v-model:open` keeps the page in charge either way.
|
|
73
|
+
//
|
|
74
|
+
// The body is `v-if`, which is what all seven JS implementations already did.
|
|
75
|
+
// Uniform by accident until now; uniform on purpose from here.
|
|
76
|
+
const {
|
|
77
|
+
open,
|
|
78
|
+
disabled = false,
|
|
79
|
+
markTone = 'accent',
|
|
80
|
+
} = defineProps<{
|
|
81
|
+
/** Whether the body is shown. The page owns this — see above. */
|
|
82
|
+
open: boolean;
|
|
83
|
+
/** A locked row: the trigger is inert and announces itself as such. */
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* The `mark` badge's tone. `accent` unless the row is in a state the badge
|
|
87
|
+
* itself should report — the discovery quota with no usage provider is the
|
|
88
|
+
* one case, and it swaps its glyph as well.
|
|
89
|
+
*
|
|
90
|
+
* Two values because two exist. The frame, size, radius and position do not
|
|
91
|
+
* change with it: that is what makes the badge the same badge everywhere.
|
|
92
|
+
*/
|
|
93
|
+
markTone?: 'accent' | 'negative';
|
|
94
|
+
}>();
|
|
95
|
+
|
|
96
|
+
const emit = defineEmits<{ 'update:open': [value: boolean] }>();
|
|
97
|
+
|
|
98
|
+
defineSlots<{
|
|
99
|
+
/**
|
|
100
|
+
* The icon that identifies the row, in a badge this component draws.
|
|
101
|
+
*
|
|
102
|
+
* A slot rather than a prop, because the glyph is sometimes conditional and
|
|
103
|
+
* sometimes carries its own attributes. The BADGE is not the page's
|
|
104
|
+
* business: three surfaces had three answers — a 34px accent-tinted square,
|
|
105
|
+
* a bare 22px glyph, and a bare 20px glyph coloured by a Quasar palette
|
|
106
|
+
* prop — so the same row on two pages did not look like the same row.
|
|
107
|
+
*/
|
|
108
|
+
mark?(): unknown;
|
|
109
|
+
/** The header's content — a title, keys, chips, whatever the row needs. */
|
|
110
|
+
header(): unknown;
|
|
111
|
+
/**
|
|
112
|
+
* Controls on the right of the header that must NOT toggle it.
|
|
113
|
+
* Rendered outside the trigger, so a click here is only a click here.
|
|
114
|
+
*/
|
|
115
|
+
'header-actions'?(): unknown;
|
|
116
|
+
/** The body. Only rendered while open. */
|
|
117
|
+
default(): unknown;
|
|
118
|
+
}>();
|
|
119
|
+
|
|
120
|
+
// `useId`, as in `AdminSection`: `aria-controls` and `aria-labelledby` have to
|
|
121
|
+
// point at THIS instance, and an accordion is by nature rendered many times on
|
|
122
|
+
// one page.
|
|
123
|
+
const triggerId = useId();
|
|
124
|
+
const bodyId = useId();
|
|
125
|
+
</script>
|
|
@@ -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>
|
|
@@ -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';
|
|
@@ -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>
|
|
@@ -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">
|
|
@@ -687,32 +687,14 @@ const classifyDiff = computed(() => props.classifyDiff);
|
|
|
687
687
|
flex-direction: column;
|
|
688
688
|
gap: 8px;
|
|
689
689
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
overflow: hidden;
|
|
695
|
-
}
|
|
696
|
-
.sa-bd-card.open {
|
|
697
|
-
border-color: var(--sa-color-accent);
|
|
698
|
-
}
|
|
690
|
+
/* The card's surface, open border, head padding, chevron and body now come
|
|
691
|
+
* from `AdminAccordion` — twelve declarations that were this page's fourth
|
|
692
|
+
* opinion on what an accordion looks like. What is left below is what the
|
|
693
|
+
* BUNDLE row puts inside that header. */
|
|
699
694
|
.sa-bd-card__head {
|
|
700
695
|
display: flex;
|
|
701
|
-
gap:
|
|
696
|
+
gap: var(--sa-space-4);
|
|
702
697
|
align-items: center;
|
|
703
|
-
padding: 12px 14px;
|
|
704
|
-
cursor: pointer;
|
|
705
|
-
}
|
|
706
|
-
.sa-bd-card__mark {
|
|
707
|
-
width: 34px;
|
|
708
|
-
height: 34px;
|
|
709
|
-
border-radius: 8px;
|
|
710
|
-
background: var(--sa-color-accent-surface-strong);
|
|
711
|
-
color: var(--sa-color-accent-strong);
|
|
712
|
-
display: flex;
|
|
713
|
-
align-items: center;
|
|
714
|
-
justify-content: center;
|
|
715
|
-
flex-shrink: 0;
|
|
716
698
|
}
|
|
717
699
|
.sa-bd-card__titlewrap {
|
|
718
700
|
flex: 1;
|
|
@@ -737,18 +719,6 @@ const classifyDiff = computed(() => props.classifyDiff);
|
|
|
737
719
|
font-size: var(--sa-text-sm);
|
|
738
720
|
color: var(--sa-color-fg-muted);
|
|
739
721
|
}
|
|
740
|
-
.sa-bd-card__chev {
|
|
741
|
-
transition: transform 0.15s;
|
|
742
|
-
color: var(--sa-color-fg-subtle);
|
|
743
|
-
}
|
|
744
|
-
.sa-bd-card__chev.open {
|
|
745
|
-
transform: rotate(90deg);
|
|
746
|
-
}
|
|
747
|
-
.sa-bd-card__body {
|
|
748
|
-
border-top: 1px solid var(--sa-color-border);
|
|
749
|
-
padding: 14px;
|
|
750
|
-
background: var(--sa-color-bg-sunken);
|
|
751
|
-
}
|
|
752
722
|
.sa-bd-grid {
|
|
753
723
|
display: grid;
|
|
754
724
|
grid-template-columns: minmax(280px, 360px) 1fr;
|
|
@@ -1724,8 +1724,13 @@ async function onLocaleChange(loc: string): Promise<void> {
|
|
|
1724
1724
|
color 0.12s;
|
|
1725
1725
|
}
|
|
1726
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. */
|
|
1727
1733
|
font-style: normal;
|
|
1728
|
-
color: var(--sa-color-fg-subtle);
|
|
1729
1734
|
font: 500 var(--sa-text-xs) var(--sa-font-mono);
|
|
1730
1735
|
}
|
|
1731
1736
|
.sa-marketing-tf-chip:hover:not(:disabled) {
|
|
@@ -1,44 +1,41 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="sa-bundles__list">
|
|
3
|
-
<
|
|
3
|
+
<AdminAccordion
|
|
4
4
|
v-for="bundle in filteredBundles"
|
|
5
5
|
:key="bundle.id"
|
|
6
6
|
class="sa-bd-card"
|
|
7
|
-
:
|
|
7
|
+
:open="openKey === bundle.id"
|
|
8
|
+
@update:open="emit('toggle', bundle)"
|
|
8
9
|
>
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
class="sa-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
<template #mark><q-icon name="inventory_2" size="18px" /></template>
|
|
11
|
+
|
|
12
|
+
<template #header>
|
|
13
|
+
<div class="sa-bd-card__head">
|
|
14
|
+
<div class="sa-bd-card__titlewrap">
|
|
15
|
+
<div class="sa-bd-card__titlerow">
|
|
16
|
+
<span class="sa-bd-card__key">{{ bundle.bundleKey }}</span>
|
|
17
|
+
<span
|
|
18
|
+
class="sa-bundle-chip"
|
|
19
|
+
:class="`sa-bundle-chip--${statusClass(bundle)}`"
|
|
20
|
+
:title="statusTooltip(bundle)"
|
|
21
|
+
>
|
|
22
|
+
{{ statusLabel(bundle) }}
|
|
23
|
+
</span>
|
|
24
|
+
<span
|
|
25
|
+
v-if="i18nLocaleCount(bundle) > 0"
|
|
26
|
+
class="sa-bundle-chip sa-bundle-chip--info"
|
|
27
|
+
>
|
|
28
|
+
{{ translationCount(bundle) }}
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="sa-bd-card__name">{{ bundle.label }}</div>
|
|
32
|
+
<div class="sa-bd-card__desc">{{ bundle.description || '—' }}</div>
|
|
27
33
|
</div>
|
|
28
|
-
<div class="sa-bd-card__name">{{ bundle.label }}</div>
|
|
29
|
-
<div class="sa-bd-card__desc">{{ bundle.description || '—' }}</div>
|
|
30
34
|
</div>
|
|
31
|
-
|
|
32
|
-
name="chevron_right"
|
|
33
|
-
class="sa-bd-card__chev"
|
|
34
|
-
:class="{ open: openKey === bundle.id }"
|
|
35
|
-
/>
|
|
36
|
-
</div>
|
|
35
|
+
</template>
|
|
37
36
|
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
37
|
+
<slot name="detail" :bundle="bundle" />
|
|
38
|
+
</AdminAccordion>
|
|
42
39
|
|
|
43
40
|
<div v-if="bundlesTotal > 0 && filteredBundles.length === 0" class="sa-bd-empty-row">
|
|
44
41
|
{{ msg.list.emptyNoMatch }}
|
|
@@ -48,6 +45,7 @@
|
|
|
48
45
|
|
|
49
46
|
<script setup lang="ts">
|
|
50
47
|
import type { BundleRow } from '@saasicat/types';
|
|
48
|
+
import AdminAccordion from '../../components/admin-page/AdminAccordion.vue';
|
|
51
49
|
import {
|
|
52
50
|
bundleStatusMeta,
|
|
53
51
|
type BundleAggregateStatus,
|
|
@@ -1,71 +1,79 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
|
|
10
|
-
{{
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
2
|
+
<AdminAccordion class="sa-fc" :open="expanded" @update:open="emit('toggle')">
|
|
3
|
+
<template #mark><q-icon :name="iconValue || 'help_outline'" size="18px" /></template>
|
|
4
|
+
|
|
5
|
+
<template #header>
|
|
6
|
+
<div class="sa-fc__head">
|
|
7
|
+
<div class="sa-fc__main">
|
|
8
|
+
<div class="sa-fc__titlerow">
|
|
9
|
+
<code class="sa-fc__key">{{ feature.featureKey }}</code>
|
|
10
|
+
<span class="sa-fc__label">{{ labelValue || feature.featureKey }}</span>
|
|
11
|
+
<span v-if="newCapsCount > 0" class="sa-fc__flag sa-fc__flag--new">
|
|
12
|
+
{{ newCapsLabel }}
|
|
13
|
+
</span>
|
|
14
|
+
<span v-if="deprecatedCapsCount > 0" class="sa-fc__flag sa-fc__flag--dep">
|
|
15
|
+
{{ deprecatedCapsLabel }}
|
|
16
|
+
</span>
|
|
17
|
+
<span
|
|
18
|
+
v-if="feature.successorKey"
|
|
19
|
+
class="sa-fc__flag sa-fc__flag--succ"
|
|
20
|
+
:title="replacedByLabel"
|
|
21
|
+
>
|
|
22
|
+
{{ replacedByLabel }}
|
|
23
|
+
</span>
|
|
24
|
+
<span
|
|
25
|
+
v-if="feature.replaces.length"
|
|
26
|
+
class="sa-fc__flag sa-fc__flag--repl"
|
|
27
|
+
:title="replacesLabel"
|
|
28
|
+
>
|
|
29
|
+
{{ replacesLabel }}
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="sa-fc__sub">
|
|
33
|
+
<template v-if="owners.length">
|
|
34
|
+
<span class="sa-muted">{{ msg.owner }}</span>
|
|
35
|
+
{{ owners.join(', ') }}
|
|
36
|
+
<span class="sa-fc__dot">·</span>
|
|
37
|
+
</template>
|
|
38
|
+
{{ capabilities.length }}
|
|
39
|
+
{{
|
|
40
|
+
capabilities.length === 1
|
|
41
|
+
? msg.feature.capabilityOne
|
|
42
|
+
: msg.feature.capabilityMany
|
|
43
|
+
}}
|
|
44
|
+
<template v-if="tierValue">
|
|
45
|
+
<span class="sa-fc__dot">·</span>
|
|
46
|
+
<span class="sa-fc__tier">{{ tierValue }}</span>
|
|
47
|
+
</template>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="sa-fc__coverage">
|
|
22
52
|
<span
|
|
23
|
-
v-
|
|
24
|
-
|
|
25
|
-
|
|
53
|
+
v-for="lng in targetLocales"
|
|
54
|
+
:key="lng"
|
|
55
|
+
class="sa-cov-pill"
|
|
56
|
+
:class="coverageClass(coverage(lng))"
|
|
26
57
|
>
|
|
27
|
-
{{
|
|
58
|
+
<span>{{ localeShort(lng) }}</span>
|
|
59
|
+
<span>{{ coveragePct(coverage(lng)) }}%</span>
|
|
28
60
|
</span>
|
|
29
61
|
</div>
|
|
30
|
-
<div class="sa-fc__sub">
|
|
31
|
-
<template v-if="owners.length">
|
|
32
|
-
<span class="sa-muted">{{ msg.owner }}</span>
|
|
33
|
-
{{ owners.join(', ') }}
|
|
34
|
-
<span class="sa-fc__dot">·</span>
|
|
35
|
-
</template>
|
|
36
|
-
{{ capabilities.length }}
|
|
37
|
-
{{
|
|
38
|
-
capabilities.length === 1
|
|
39
|
-
? msg.feature.capabilityOne
|
|
40
|
-
: msg.feature.capabilityMany
|
|
41
|
-
}}
|
|
42
|
-
<template v-if="tierValue">
|
|
43
|
-
<span class="sa-fc__dot">·</span>
|
|
44
|
-
<span class="sa-fc__tier">{{ tierValue }}</span>
|
|
45
|
-
</template>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
<div class="sa-fc__coverage">
|
|
50
|
-
<span
|
|
51
|
-
v-for="lng in targetLocales"
|
|
52
|
-
:key="lng"
|
|
53
|
-
class="sa-cov-pill"
|
|
54
|
-
:class="coverageClass(coverage(lng))"
|
|
55
|
-
>
|
|
56
|
-
<span>{{ localeShort(lng) }}</span>
|
|
57
|
-
<span>{{ coveragePct(coverage(lng)) }}%</span>
|
|
58
|
-
</span>
|
|
59
62
|
</div>
|
|
63
|
+
</template>
|
|
60
64
|
|
|
65
|
+
<!-- Outside the trigger, not merely `@click.stop` inside it. The old
|
|
66
|
+
head was a `<div @click>` with this control nested in it, and the
|
|
67
|
+
only thing keeping a status change from also toggling the card was
|
|
68
|
+
a `.stop` somebody remembered. Structure beats memory. -->
|
|
69
|
+
<template #header-actions>
|
|
61
70
|
<DiscoveryStatusControl
|
|
62
71
|
:status="feature.discoveryStatus"
|
|
63
72
|
@set-status="(target) => emit('review', feature.featureKey, target)"
|
|
64
73
|
/>
|
|
65
|
-
|
|
66
|
-
</div>
|
|
74
|
+
</template>
|
|
67
75
|
|
|
68
|
-
<div
|
|
76
|
+
<div class="sa-fc__body">
|
|
69
77
|
<div v-if="feature.discoveryStatus === 'outdated'" class="sa-fc__banner warn">
|
|
70
78
|
<q-icon name="warning" size="16px" />
|
|
71
79
|
<span>
|
|
@@ -140,7 +148,7 @@
|
|
|
140
148
|
"
|
|
141
149
|
/>
|
|
142
150
|
</div>
|
|
143
|
-
</
|
|
151
|
+
</AdminAccordion>
|
|
144
152
|
</template>
|
|
145
153
|
|
|
146
154
|
<script setup lang="ts">
|
|
@@ -152,6 +160,7 @@ import type {
|
|
|
152
160
|
FeatureCatalogEntryRow,
|
|
153
161
|
UpdateCatalogEntryBaseData,
|
|
154
162
|
} from '@saasicat/types';
|
|
163
|
+
import AdminAccordion from '../../components/admin-page/AdminAccordion.vue';
|
|
155
164
|
import CatalogEntryTransPanel from './CatalogEntryTransPanel.vue';
|
|
156
165
|
import DiscoveryCapList from './DiscoveryCapList.vue';
|
|
157
166
|
import DiscoveryStatusControl from './DiscoveryStatusControl.vue';
|
|
@@ -268,26 +277,16 @@ function coverage(locale: string): number {
|
|
|
268
277
|
</script>
|
|
269
278
|
|
|
270
279
|
<style scoped>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.sa-fc.expanded {
|
|
278
|
-
border-color: var(--sa-color-scheduled-border);
|
|
279
|
-
box-shadow: 0 1px 3px var(--sa-shadow-tint-2);
|
|
280
|
-
}
|
|
280
|
+
/* Surface, radius, open border, head padding, chevron and body come from
|
|
281
|
+
* `AdminAccordion`. What is left is what a FEATURE row puts in that header.
|
|
282
|
+
*
|
|
283
|
+
* These seven rules were byte-identical to `DiscoveryQuotaCard`'s after a
|
|
284
|
+
* prefix rename — jscpd could not see it, because `minTokens: 50` is larger
|
|
285
|
+
* than any single rule here. The duplication was real and below the threshold. */
|
|
281
286
|
.sa-fc__head {
|
|
282
287
|
display: flex;
|
|
283
288
|
align-items: center;
|
|
284
|
-
gap:
|
|
285
|
-
padding: 10px 12px;
|
|
286
|
-
cursor: pointer;
|
|
287
|
-
}
|
|
288
|
-
.sa-fc__icon {
|
|
289
|
-
color: var(--sa-color-fg-secondary);
|
|
290
|
-
flex-shrink: 0;
|
|
289
|
+
gap: var(--sa-space-4);
|
|
291
290
|
}
|
|
292
291
|
.sa-fc__main {
|
|
293
292
|
flex: 1;
|
|
@@ -355,18 +354,6 @@ function coverage(locale: string): number {
|
|
|
355
354
|
gap: 6px;
|
|
356
355
|
flex-shrink: 0;
|
|
357
356
|
}
|
|
358
|
-
.sa-fc__chev {
|
|
359
|
-
color: var(--sa-color-fg-subtle);
|
|
360
|
-
transition: transform 0.15s;
|
|
361
|
-
}
|
|
362
|
-
.sa-fc__chev.open {
|
|
363
|
-
transform: rotate(90deg);
|
|
364
|
-
}
|
|
365
|
-
.sa-fc__body {
|
|
366
|
-
border-top: 1px solid var(--sa-color-border-soft);
|
|
367
|
-
padding: 12px;
|
|
368
|
-
background: var(--sa-color-bg-sunken);
|
|
369
|
-
}
|
|
370
357
|
.sa-fc__banner {
|
|
371
358
|
display: flex;
|
|
372
359
|
align-items: center;
|
|
@@ -1,62 +1,76 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
>
|
|
20
|
-
{{
|
|
21
|
-
|
|
2
|
+
<AdminAccordion
|
|
3
|
+
class="sa-qc"
|
|
4
|
+
:class="{ warn: !quota.usageProvider }"
|
|
5
|
+
:open="expanded"
|
|
6
|
+
:mark-tone="quota.usageProvider ? 'accent' : 'negative'"
|
|
7
|
+
@update:open="emit('toggle')"
|
|
8
|
+
>
|
|
9
|
+
<!-- The glyph already says it; the tone lets the badge say it too. The
|
|
10
|
+
`:color` this replaces was a Quasar PALETTE name, which reaches past
|
|
11
|
+
the role layer — `negative` there is Quasar's, not the theme's. -->
|
|
12
|
+
<template #mark>
|
|
13
|
+
<q-icon :name="quota.usageProvider ? 'inventory_2' : 'error'" size="18px" />
|
|
14
|
+
</template>
|
|
15
|
+
|
|
16
|
+
<template #header>
|
|
17
|
+
<div class="sa-qc__head">
|
|
18
|
+
<div class="sa-qc__main">
|
|
19
|
+
<div class="sa-qc__titlerow">
|
|
20
|
+
<span class="sa-qc__label">{{ labelValue || quota.quotaKey }}</span>
|
|
21
|
+
<code class="sa-qc__key">{{ quota.quotaKey }}</code>
|
|
22
|
+
<span class="sa-chip">{{ quota.enforcementMode }}</span>
|
|
23
|
+
<span
|
|
24
|
+
v-if="quota.successorKey"
|
|
25
|
+
class="sa-qc__flag sa-qc__flag--succ"
|
|
26
|
+
:title="replacedByLabel"
|
|
27
|
+
>
|
|
28
|
+
{{ replacedByLabel }}
|
|
29
|
+
</span>
|
|
30
|
+
<span
|
|
31
|
+
v-if="quota.replaces.length"
|
|
32
|
+
class="sa-qc__flag sa-qc__flag--repl"
|
|
33
|
+
:title="replacesLabel"
|
|
34
|
+
>
|
|
35
|
+
{{ replacesLabel }}
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="sa-qc__sub">
|
|
39
|
+
{{ msg.unit }} <code>{{ quota.unit }}</code> · {{ msg.quota.usageProvider }}
|
|
40
|
+
<code v-if="quota.usageProvider">{{ quota.usageProvider }}</code>
|
|
41
|
+
<span v-else class="sa-qc__missing">{{
|
|
42
|
+
msg.quota.usageProviderMissing
|
|
43
|
+
}}</span>
|
|
44
|
+
</div>
|
|
45
|
+
<div v-if="!quota.usageProvider" class="sa-qc__warning">
|
|
46
|
+
{{ msg.quota.noUsageProviderWarning }}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="sa-qc__coverage">
|
|
22
51
|
<span
|
|
23
|
-
v-
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
v-for="lng in targetLocales"
|
|
53
|
+
:key="lng"
|
|
54
|
+
class="sa-cov-pill"
|
|
55
|
+
:class="coverageClass(coverage(lng))"
|
|
26
56
|
>
|
|
27
|
-
{{
|
|
57
|
+
<span>{{ localeShort(lng) }}</span>
|
|
58
|
+
<span>{{ coveragePct(coverage(lng)) }}%</span>
|
|
28
59
|
</span>
|
|
29
60
|
</div>
|
|
30
|
-
<div class="sa-qc__sub">
|
|
31
|
-
{{ msg.unit }} <code>{{ quota.unit }}</code> · {{ msg.quota.usageProvider }}
|
|
32
|
-
<code v-if="quota.usageProvider">{{ quota.usageProvider }}</code>
|
|
33
|
-
<span v-else class="sa-qc__missing">{{ msg.quota.usageProviderMissing }}</span>
|
|
34
|
-
</div>
|
|
35
|
-
<div v-if="!quota.usageProvider" class="sa-qc__warning">
|
|
36
|
-
{{ msg.quota.noUsageProviderWarning }}
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div class="sa-qc__coverage">
|
|
41
|
-
<span
|
|
42
|
-
v-for="lng in targetLocales"
|
|
43
|
-
:key="lng"
|
|
44
|
-
class="sa-cov-pill"
|
|
45
|
-
:class="coverageClass(coverage(lng))"
|
|
46
|
-
>
|
|
47
|
-
<span>{{ localeShort(lng) }}</span>
|
|
48
|
-
<span>{{ coveragePct(coverage(lng)) }}%</span>
|
|
49
|
-
</span>
|
|
50
61
|
</div>
|
|
62
|
+
</template>
|
|
51
63
|
|
|
64
|
+
<!-- See DiscoveryFeatureCard: outside the trigger, not `@click.stop`
|
|
65
|
+
inside it. -->
|
|
66
|
+
<template #header-actions>
|
|
52
67
|
<DiscoveryStatusControl
|
|
53
68
|
:status="quota.discoveryStatus"
|
|
54
69
|
@set-status="(target) => emit('review', quota.quotaKey, target)"
|
|
55
70
|
/>
|
|
56
|
-
|
|
57
|
-
</div>
|
|
71
|
+
</template>
|
|
58
72
|
|
|
59
|
-
<div
|
|
73
|
+
<div class="sa-qc__body">
|
|
60
74
|
<div v-if="quota.discoveryStatus === 'outdated'" class="sa-qc__banner">
|
|
61
75
|
<q-icon name="warning" size="16px" />
|
|
62
76
|
<span>
|
|
@@ -74,7 +88,7 @@
|
|
|
74
88
|
"
|
|
75
89
|
/>
|
|
76
90
|
</div>
|
|
77
|
-
</
|
|
91
|
+
</AdminAccordion>
|
|
78
92
|
</template>
|
|
79
93
|
|
|
80
94
|
<script setup lang="ts">
|
|
@@ -85,6 +99,7 @@ import type {
|
|
|
85
99
|
QuotaCatalogEntryRow,
|
|
86
100
|
UpdateCatalogEntryBaseData,
|
|
87
101
|
} from '@saasicat/types';
|
|
102
|
+
import AdminAccordion from '../../components/admin-page/AdminAccordion.vue';
|
|
88
103
|
import CatalogEntryTransPanel from './CatalogEntryTransPanel.vue';
|
|
89
104
|
import DiscoveryStatusControl from './DiscoveryStatusControl.vue';
|
|
90
105
|
import { formatMessage } from '../../client/i18n/format.js';
|
|
@@ -153,31 +168,22 @@ function coverage(locale: string): number {
|
|
|
153
168
|
</script>
|
|
154
169
|
|
|
155
170
|
<style scoped>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
/* Surface, radius, open border, head padding, chevron and body come from
|
|
172
|
+
* `AdminAccordion`. What is left is what a QUOTA row puts in that header — and
|
|
173
|
+
* `warn`, which is this card's own idea and not the accordion's.
|
|
174
|
+
*
|
|
175
|
+
* Two values changed by moving: the open border was
|
|
176
|
+
* `--sa-color-scheduled-border` plus a shadow here and `--sa-color-accent`
|
|
177
|
+
* everywhere else, and the head padding was 10/12 against 12/14 next door.
|
|
178
|
+
* Those differences were the finding, not the design. */
|
|
162
179
|
.sa-qc.warn {
|
|
163
180
|
border-color: var(--sa-color-negative-border);
|
|
164
|
-
|
|
165
|
-
.sa-qc.expanded {
|
|
166
|
-
border-color: var(--sa-color-scheduled-border);
|
|
167
|
-
box-shadow: 0 1px 3px var(--sa-shadow-tint-2);
|
|
181
|
+
background: var(--sa-color-negative-surface);
|
|
168
182
|
}
|
|
169
183
|
.sa-qc__head {
|
|
170
184
|
display: flex;
|
|
171
185
|
align-items: center;
|
|
172
|
-
gap:
|
|
173
|
-
padding: 10px 12px;
|
|
174
|
-
cursor: pointer;
|
|
175
|
-
}
|
|
176
|
-
.sa-qc.warn .sa-qc__head {
|
|
177
|
-
background: var(--sa-color-negative-surface);
|
|
178
|
-
}
|
|
179
|
-
.sa-qc__icon {
|
|
180
|
-
flex-shrink: 0;
|
|
186
|
+
gap: var(--sa-space-4);
|
|
181
187
|
}
|
|
182
188
|
.sa-qc__main {
|
|
183
189
|
flex: 1;
|
|
@@ -230,18 +236,6 @@ function coverage(locale: string): number {
|
|
|
230
236
|
gap: 6px;
|
|
231
237
|
flex-shrink: 0;
|
|
232
238
|
}
|
|
233
|
-
.sa-qc__chev {
|
|
234
|
-
color: var(--sa-color-fg-subtle);
|
|
235
|
-
transition: transform 0.15s;
|
|
236
|
-
}
|
|
237
|
-
.sa-qc__chev.open {
|
|
238
|
-
transform: rotate(90deg);
|
|
239
|
-
}
|
|
240
|
-
.sa-qc__body {
|
|
241
|
-
border-top: 1px solid var(--sa-color-border-soft);
|
|
242
|
-
padding: 12px;
|
|
243
|
-
background: var(--sa-color-bg-sunken);
|
|
244
|
-
}
|
|
245
239
|
.sa-qc__banner {
|
|
246
240
|
display: flex;
|
|
247
241
|
align-items: center;
|
|
@@ -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">
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* Accordion — a header that opens a body (see AdminAccordion).
|
|
2
|
+
*
|
|
3
|
+
* Every value here was one of several. The survey that led to this file found
|
|
4
|
+
* three header paddings across the package (0, 10/12, 12/14), three body
|
|
5
|
+
* paddings (12, 14, 18/20/22), two radii (8 and 10), two colours for the open
|
|
6
|
+
* border (`accent` and `scheduled-border`), two transition durations, and hover
|
|
7
|
+
* feedback on one of six surfaces that all set `cursor: pointer`.
|
|
8
|
+
*
|
|
9
|
+
* The values below are the scale's, not the average of the six: a rung exists
|
|
10
|
+
* for each, so a literal here would be a fourth opinion rather than a decision.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.sa-accordion {
|
|
14
|
+
background: var(--sa-color-bg-surface);
|
|
15
|
+
border: 1px solid var(--sa-color-border);
|
|
16
|
+
border-radius: var(--sa-radius-head);
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* The open card names itself with the accent rather than with a tone: which
|
|
21
|
+
* row is open is a position, not a status, and a status colour here would
|
|
22
|
+
* compete with the chips the header carries. */
|
|
23
|
+
.sa-accordion--open {
|
|
24
|
+
border-color: var(--sa-color-accent);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sa-accordion__head {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: stretch;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The trigger fills the head so the whole row is the target — the affordance
|
|
33
|
+
* the `<div @click>` versions had, without giving up the button. */
|
|
34
|
+
.sa-accordion__trigger {
|
|
35
|
+
flex: 1;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
gap: var(--sa-space-4);
|
|
40
|
+
padding: var(--sa-space-4) var(--sa-space-5);
|
|
41
|
+
background: none;
|
|
42
|
+
border: 0;
|
|
43
|
+
font: inherit;
|
|
44
|
+
color: inherit;
|
|
45
|
+
text-align: left;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Hover feedback, which five of the six surfaces promised with `cursor` and
|
|
50
|
+
* only one delivered. */
|
|
51
|
+
.sa-accordion__trigger:hover:not(:disabled) {
|
|
52
|
+
background: var(--sa-color-bg-sunken);
|
|
53
|
+
}
|
|
54
|
+
.sa-accordion__trigger:disabled {
|
|
55
|
+
cursor: not-allowed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* The badge that identifies the row.
|
|
59
|
+
*
|
|
60
|
+
* Three surfaces drew this three ways — a 34px accent-tinted square, a bare
|
|
61
|
+
* 22px glyph, and a bare 20px glyph coloured through a Quasar palette prop — so
|
|
62
|
+
* the same kind of row did not look like the same kind of row from one page to
|
|
63
|
+
* the next. The frame is the component's; only the glyph is the page's.
|
|
64
|
+
*
|
|
65
|
+
* Square-ish rather than round: a circle here would read as an avatar, and none
|
|
66
|
+
* of these rows is about a person. */
|
|
67
|
+
.sa-accordion__mark {
|
|
68
|
+
flex: 0 0 auto;
|
|
69
|
+
width: 34px;
|
|
70
|
+
height: 34px;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
border-radius: var(--sa-radius-field);
|
|
75
|
+
background: var(--sa-color-accent-surface-strong);
|
|
76
|
+
color: var(--sa-color-accent-strong);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Only for a row whose state the badge itself should report. The size, radius
|
|
80
|
+
* and position do not move with it — that is what keeps it the same badge. */
|
|
81
|
+
.sa-accordion__mark--negative {
|
|
82
|
+
background: var(--sa-color-negative-surface-strong);
|
|
83
|
+
color: var(--sa-color-negative-fg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.sa-accordion__label {
|
|
87
|
+
flex: 1;
|
|
88
|
+
min-width: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Points right when closed, down when open. Not `transform: rotate(90deg)` on a
|
|
92
|
+
* `chevron_down`: the resting state is the one a reader sees most, and a glyph
|
|
93
|
+
* that means "there is more this way" is the honest one to rest on. */
|
|
94
|
+
.sa-accordion__chev {
|
|
95
|
+
flex: 0 0 auto;
|
|
96
|
+
color: var(--sa-color-fg-subtle);
|
|
97
|
+
transition: transform 0.15s;
|
|
98
|
+
}
|
|
99
|
+
.sa-accordion--open .sa-accordion__chev {
|
|
100
|
+
transform: rotate(90deg);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Outside the trigger, so a control here does not toggle the row. Padded on
|
|
104
|
+
* its own rather than by the trigger, or the two would share a hover surface
|
|
105
|
+
* and the row would look pressed while the pointer is on a button that does
|
|
106
|
+
* something else. */
|
|
107
|
+
.sa-accordion__actions {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
gap: var(--sa-space-3);
|
|
111
|
+
padding-right: var(--sa-space-5);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sa-accordion__body {
|
|
115
|
+
padding: var(--sa-space-5);
|
|
116
|
+
border-top: 1px solid var(--sa-color-border);
|
|
117
|
+
background: var(--sa-color-bg-sunken);
|
|
118
|
+
}
|
package/src/ui/theme/index.css
CHANGED
|
@@ -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
|