@redseed/redseed-ui-vue3 8.68.0 → 8.70.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
CHANGED
|
@@ -66,6 +66,39 @@ const props = defineProps({
|
|
|
66
66
|
type: Boolean,
|
|
67
67
|
default: false,
|
|
68
68
|
},
|
|
69
|
+
// CATEGORY colour, which is a different job from the semantic variants above.
|
|
70
|
+
// `brand`/`success`/`error` and friends say what STATE a thing is in; they
|
|
71
|
+
// cannot say which taxonomy a thing BELONGS to (a capability domain, a team, a
|
|
72
|
+
// location, a pathway), and there are always more categories than the semantic
|
|
73
|
+
// set has slots.
|
|
74
|
+
//
|
|
75
|
+
// Takes the name of a RUNTIME CSS custom property, applied as
|
|
76
|
+
// `var(--<color>, <neutral>)` — the same convention and the same neutral
|
|
77
|
+
// degrade as Pill's `color` prop, so the two components stay consistent and
|
|
78
|
+
// RSUI stays theme-agnostic. Pass a real runtime theme variable, e.g. the
|
|
79
|
+
// consuming app's palette (`color7` -> `var(--color7)`). NOTE: RSUI's own
|
|
80
|
+
// Tailwind `@theme` tokens (e.g. `--color-brand-500`) are inlined into
|
|
81
|
+
// utilities, not exposed as runtime vars, so they will NOT resolve here. An
|
|
82
|
+
// UNRESOLVABLE (undefined) token degrades to a neutral grey rather than an
|
|
83
|
+
// invisible bar; a token that *resolves* to `transparent`/empty is not caught.
|
|
84
|
+
//
|
|
85
|
+
// On its own this prop paints nothing — it needs `accent` to say which edge
|
|
86
|
+
// carries it.
|
|
87
|
+
color: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: '',
|
|
90
|
+
},
|
|
91
|
+
// Which edge carries the category accent bar. Default 'none', so nothing
|
|
92
|
+
// changes for any existing consumer. Thickness is NOT a consumer decision —
|
|
93
|
+
// it comes from a token (see --rsui-card-accent-w in card.css). That is the
|
|
94
|
+
// whole point of the prop: the same concept was hand-rolled twice on one page
|
|
95
|
+
// at two different weights (4px and 3px) precisely because nothing told the
|
|
96
|
+
// author what the weight should be.
|
|
97
|
+
accent: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: 'none',
|
|
100
|
+
validator: (value) => ['left', 'top', 'none'].includes(value),
|
|
101
|
+
},
|
|
69
102
|
ariaLabel: {
|
|
70
103
|
type: String,
|
|
71
104
|
default: undefined,
|
|
@@ -110,6 +143,16 @@ const ariaCurrent = computed(() => {
|
|
|
110
143
|
// conveyed by colour alone (WCAG 1.4.1).
|
|
111
144
|
const isActive = computed(() => props.pressed === true || Boolean(props.current))
|
|
112
145
|
|
|
146
|
+
// The accent token is only set when a color is supplied; otherwise the property
|
|
147
|
+
// is left unset so the CSS neutral fallback applies. When it IS supplied it
|
|
148
|
+
// carries an inline fallback to the neutral grey, so an unresolvable token
|
|
149
|
+
// degrades to grey rather than to an invisible edge. Mirrors Pill exactly.
|
|
150
|
+
const accentStyle = computed(() =>
|
|
151
|
+
props.color
|
|
152
|
+
? { '--rsui-card-accent': `var(--${props.color}, var(--Colors-Grey-500))` }
|
|
153
|
+
: {},
|
|
154
|
+
)
|
|
155
|
+
|
|
113
156
|
const slots = useSlots()
|
|
114
157
|
|
|
115
158
|
const cardElement = ref(null)
|
|
@@ -130,6 +173,8 @@ const cardClass = computed(() => [
|
|
|
130
173
|
'rsui-card--warning': props.warning,
|
|
131
174
|
'rsui-card--error': props.error,
|
|
132
175
|
'rsui-card--draft': props.draft,
|
|
176
|
+
'rsui-card--accent-left': props.accent === 'left',
|
|
177
|
+
'rsui-card--accent-top': props.accent === 'top',
|
|
133
178
|
'rsui-card--2xs': responsiveWidth.value['2xs'],
|
|
134
179
|
'rsui-card--xs': responsiveWidth.value['xs'],
|
|
135
180
|
'rsui-card--sm': responsiveWidth.value['sm'],
|
|
@@ -170,7 +215,7 @@ function onClick() {
|
|
|
170
215
|
}
|
|
171
216
|
</script>
|
|
172
217
|
<template>
|
|
173
|
-
<div ref="cardElement" :class="cardClass" data-testid="card">
|
|
218
|
+
<div ref="cardElement" :class="cardClass" :style="accentStyle" data-testid="card">
|
|
174
219
|
<svg
|
|
175
220
|
v-if="draft"
|
|
176
221
|
class="rsui-card__draft-border"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { computed, useId } from 'vue'
|
|
3
|
+
import { ArrowUpRightIcon, ArrowDownRightIcon, ArrowRightIcon } from '@heroicons/vue/24/outline'
|
|
2
4
|
import Card from './Card.vue'
|
|
3
5
|
import FlexContainer from '../FlexContainer/FlexContainer.vue'
|
|
4
6
|
|
|
@@ -39,7 +41,70 @@ const props = defineProps({
|
|
|
39
41
|
default: null,
|
|
40
42
|
validator: value => ['green', 'teal', 'light'].includes(value),
|
|
41
43
|
},
|
|
44
|
+
// DIRECTION of travel — which arrow. Deliberately separate from `sentiment`,
|
|
45
|
+
// because an arrow direction and a colour are not the same decision:
|
|
46
|
+
// attrition up is bad, completions up is good. One prop driving both would
|
|
47
|
+
// render every metric where rising is bad in green.
|
|
48
|
+
trend: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: null,
|
|
51
|
+
validator: value => ['up', 'down', 'flat'].includes(value),
|
|
52
|
+
},
|
|
53
|
+
// Whether that direction is GOOD NEWS — colour only. Defaults to deriving
|
|
54
|
+
// from `trend` (up => positive) so the common case stays terse, but it is
|
|
55
|
+
// independently overridable, which is the entire point of the split.
|
|
56
|
+
sentiment: {
|
|
57
|
+
type: String,
|
|
58
|
+
default: null,
|
|
59
|
+
validator: value => ['positive', 'negative', 'neutral'].includes(value),
|
|
60
|
+
},
|
|
61
|
+
// WHERE the comparison sits relative to the figure.
|
|
62
|
+
//
|
|
63
|
+
// below the comparison is a caption under the number (the default, and
|
|
64
|
+
// what every existing tile already renders).
|
|
65
|
+
// inline the comparison sits on the number's own row, immediately beside
|
|
66
|
+
// it, centred against its line box.
|
|
67
|
+
//
|
|
68
|
+
// A string rather than an `inlineComparison` boolean because the reference
|
|
69
|
+
// carries a third shape too — the change as a bordered chip pushed to the far
|
|
70
|
+
// right of the number row — and a boolean would have to be retired to add it.
|
|
71
|
+
//
|
|
72
|
+
// Inline is the tighter tile: it buys back a line of height, which is what
|
|
73
|
+
// makes a dense dashboard row of these work. It costs horizontal room, so the
|
|
74
|
+
// row wraps back to two lines rather than overflowing when the tile is narrow.
|
|
75
|
+
comparisonPlacement: {
|
|
76
|
+
type: String,
|
|
77
|
+
default: 'below',
|
|
78
|
+
validator: value => ['below', 'inline'].includes(value),
|
|
79
|
+
},
|
|
42
80
|
})
|
|
81
|
+
|
|
82
|
+
// The terse default. `sentiment` is read first so an explicit value always wins,
|
|
83
|
+
// including 'neutral', which is why this tests for null rather than falsiness.
|
|
84
|
+
const TREND_SENTIMENT = {
|
|
85
|
+
up: 'positive',
|
|
86
|
+
down: 'negative',
|
|
87
|
+
flat: 'neutral',
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const resolvedSentiment = computed(() => {
|
|
91
|
+
if (props.sentiment !== null) return props.sentiment
|
|
92
|
+
|
|
93
|
+
return TREND_SENTIMENT[props.trend] ?? 'neutral'
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const TREND_ICON = {
|
|
97
|
+
up: ArrowUpRightIcon,
|
|
98
|
+
down: ArrowDownRightIcon,
|
|
99
|
+
flat: ArrowRightIcon,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const trendIcon = computed(() => TREND_ICON[props.trend] ?? null)
|
|
103
|
+
|
|
104
|
+
// Ties the comparison to the number for assistive tech, so the trend is not read
|
|
105
|
+
// as a loose fragment floating after the figure. DOM order already puts them
|
|
106
|
+
// adjacent; aria-describedby is what survives the number being queried alone.
|
|
107
|
+
const comparisonId = useId()
|
|
43
108
|
</script>
|
|
44
109
|
|
|
45
110
|
<template>
|
|
@@ -69,19 +134,82 @@ const props = defineProps({
|
|
|
69
134
|
<div v-if="$slots.icon" class="rsui-metric-card__icon" aria-hidden="true">
|
|
70
135
|
<slot name="icon"></slot>
|
|
71
136
|
</div>
|
|
72
|
-
<div class="rsui-metric-
|
|
73
|
-
<
|
|
137
|
+
<div class="rsui-metric-card__heading-text">
|
|
138
|
+
<div class="rsui-metric-card__label">
|
|
139
|
+
<slot name="label"></slot>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div v-if="$slots.description" class="rsui-metric-card__description">
|
|
143
|
+
<slot name="description"></slot>
|
|
144
|
+
</div>
|
|
74
145
|
</div>
|
|
75
146
|
</div>
|
|
76
147
|
|
|
148
|
+
<!-- The figure and the thing that qualifies it, as one unit. The wrapper
|
|
149
|
+
stacks them by default, which renders exactly as the two siblings did
|
|
150
|
+
before it existed, and turns into a row when comparisonPlacement is
|
|
151
|
+
inline. Keeping both placements on the same two elements is what lets
|
|
152
|
+
the a11y wiring, the sentiment modifiers and the tonal overrides stay
|
|
153
|
+
placement-agnostic — only the direction of this one box changes. -->
|
|
77
154
|
<div :class="[
|
|
78
|
-
'rsui-metric-
|
|
155
|
+
'rsui-metric-card__figure',
|
|
156
|
+
`rsui-metric-card__figure--${props.comparisonPlacement}`,
|
|
79
157
|
{ 'rsui-metric-card--center': alignment === 'center' },
|
|
80
158
|
{ 'rsui-metric-card--left': alignment === 'left' },
|
|
81
159
|
{ 'rsui-metric-card--right': alignment === 'right' },
|
|
82
160
|
]"
|
|
83
161
|
>
|
|
84
|
-
<
|
|
162
|
+
<div :class="[
|
|
163
|
+
'rsui-metric-card__number',
|
|
164
|
+
{ 'rsui-metric-card--center': alignment === 'center' },
|
|
165
|
+
{ 'rsui-metric-card--left': alignment === 'left' },
|
|
166
|
+
{ 'rsui-metric-card--right': alignment === 'right' },
|
|
167
|
+
]"
|
|
168
|
+
:aria-describedby="$slots.comparison ? comparisonId : undefined"
|
|
169
|
+
>
|
|
170
|
+
<slot name="number"></slot>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
<!-- With the number at every labelFirst setting, matching the reference
|
|
174
|
+
shape: the comparison qualifies the figure, not the label, so it
|
|
175
|
+
stays with the figure wherever the label goes. -->
|
|
176
|
+
<div v-if="$slots.comparison"
|
|
177
|
+
:class="[
|
|
178
|
+
'rsui-metric-card__comparison-row',
|
|
179
|
+
{ 'rsui-metric-card--center': alignment === 'center' },
|
|
180
|
+
{ 'rsui-metric-card--left': alignment === 'left' },
|
|
181
|
+
{ 'rsui-metric-card--right': alignment === 'right' },
|
|
182
|
+
]"
|
|
183
|
+
>
|
|
184
|
+
<span :id="comparisonId"
|
|
185
|
+
:class="[
|
|
186
|
+
'rsui-metric-card__comparison',
|
|
187
|
+
`rsui-metric-card__comparison--${resolvedSentiment}`,
|
|
188
|
+
]"
|
|
189
|
+
>
|
|
190
|
+
<span v-if="trendIcon" class="rsui-metric-card__comparison-icon" aria-hidden="true">
|
|
191
|
+
<component :is="trendIcon" />
|
|
192
|
+
</span>
|
|
193
|
+
|
|
194
|
+
<slot name="comparison"></slot>
|
|
195
|
+
|
|
196
|
+
<!-- The quieter half of the reference's "Change and text": the change
|
|
197
|
+
carries the sentiment colour, the phrase that qualifies it drops to
|
|
198
|
+
tertiary ink so the eye lands on the figure first.
|
|
199
|
+
|
|
200
|
+
A second slot rather than asking the consumer to wrap the phrase in
|
|
201
|
+
a class themselves. The split is a colour decision, which belongs to
|
|
202
|
+
the design system; the words and their translation stay with the
|
|
203
|
+
consumer, exactly as the comparison itself does.
|
|
204
|
+
|
|
205
|
+
Inside the comparison span, so it sits within the region the number's
|
|
206
|
+
aria-describedby points at — "175.95% Increase vs last month" is one
|
|
207
|
+
description of the figure, not a fragment plus an orphan. -->
|
|
208
|
+
<span v-if="$slots.comparisonContext" class="rsui-metric-card__comparison-context">
|
|
209
|
+
<slot name="comparisonContext"></slot>
|
|
210
|
+
</span>
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
85
213
|
</div>
|
|
86
214
|
|
|
87
215
|
<div v-if="!labelFirst"
|
|
@@ -95,8 +223,14 @@ const props = defineProps({
|
|
|
95
223
|
<div v-if="$slots.icon" class="rsui-metric-card__icon" aria-hidden="true">
|
|
96
224
|
<slot name="icon"></slot>
|
|
97
225
|
</div>
|
|
98
|
-
<div class="rsui-metric-
|
|
99
|
-
<
|
|
226
|
+
<div class="rsui-metric-card__heading-text">
|
|
227
|
+
<div class="rsui-metric-card__label">
|
|
228
|
+
<slot name="label"></slot>
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
<div v-if="$slots.description" class="rsui-metric-card__description">
|
|
232
|
+
<slot name="description"></slot>
|
|
233
|
+
</div>
|
|
100
234
|
</div>
|
|
101
235
|
</div>
|
|
102
236
|
</FlexContainer>
|