@redseed/redseed-ui-vue3 8.69.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
|
@@ -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>
|