@juspay/svelte-ui-components 4.30.0 → 4.31.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/AnimatedNumber/AnimatedNumber.svelte +75 -1
- package/dist/AnimatedNumber/properties.d.ts +16 -0
- package/dist/StatCard/StatCard.svelte +114 -8
- package/dist/StatCard/metric.d.ts +22 -0
- package/dist/StatCard/metric.js +74 -0
- package/dist/StatCard/properties.d.ts +32 -0
- package/dist-wc/index.js +340 -223
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
locale = 'en-US',
|
|
9
9
|
format,
|
|
10
10
|
live = 'off',
|
|
11
|
+
animateOnMount = false,
|
|
11
12
|
ariaLabel,
|
|
12
13
|
testId,
|
|
13
14
|
classes
|
|
@@ -397,7 +398,7 @@
|
|
|
397
398
|
<span
|
|
398
399
|
class="animated-number-digit"
|
|
399
400
|
style="--_animated-number-digit: {column.digit};"
|
|
400
|
-
{@attach roll(column.digit, mounted, numeric)}
|
|
401
|
+
{@attach roll(column.digit, mounted || animateOnMount, numeric)}
|
|
401
402
|
>
|
|
402
403
|
{#each glyphs as glyph, index (glyph)}
|
|
403
404
|
<!--
|
|
@@ -449,6 +450,64 @@
|
|
|
449
450
|
color: var(--animated-number-color, inherit);
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/*
|
|
454
|
+
* The number renders as the text it replaced, even against a host stylesheet
|
|
455
|
+
* that styles bare elements.
|
|
456
|
+
*
|
|
457
|
+
* Every part of this component is a `<span>`: the root, the columns, the
|
|
458
|
+
* glyphs and the selection-copy node. A consumer carrying a bare `span` rule
|
|
459
|
+
* therefore hits all of them, and a type selector beats plain inheritance, so
|
|
460
|
+
* the number silently stopped matching its own label -- which is a text node
|
|
461
|
+
* in the parent and never affected.
|
|
462
|
+
*
|
|
463
|
+
* Measured rather than imagined: Lighthouse ships a bare `span` rule in
|
|
464
|
+
* static/style/text.css that sets colour, font-size and font-weight from its
|
|
465
|
+
* own tokens, and adopting the odometer there took every headline dashboard
|
|
466
|
+
* metric from 24px/600 to 12px/400 in the tertiary text colour, across nine
|
|
467
|
+
* routes. Nothing in this repo could have shown it: our own pages carry no
|
|
468
|
+
* such rule.
|
|
469
|
+
*
|
|
470
|
+
* Declared on the classes rather than left to inheritance so a bare type
|
|
471
|
+
* selector cannot win. `line-height` is deliberately NOT inherited -- the
|
|
472
|
+
* column box is this component's own geometry and is set below.
|
|
473
|
+
*/
|
|
474
|
+
.animated-number,
|
|
475
|
+
.animated-number-plain,
|
|
476
|
+
.animated-number-visual,
|
|
477
|
+
.animated-number-digit,
|
|
478
|
+
.animated-number-digit-glyph,
|
|
479
|
+
.animated-number-literal {
|
|
480
|
+
font-family: inherit;
|
|
481
|
+
font-size: inherit;
|
|
482
|
+
font-weight: inherit;
|
|
483
|
+
font-style: inherit;
|
|
484
|
+
letter-spacing: inherit;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/*
|
|
488
|
+
* Colour is deliberately NOT in the group above, and the root is deliberately
|
|
489
|
+
* not in this one.
|
|
490
|
+
*
|
|
491
|
+
* The root already resolves its colour through the token, and that
|
|
492
|
+
* declaration sits EARLIER in this stylesheet. A grouped rule that also
|
|
493
|
+
* matched `.animated-number` would set `color: inherit` at equal specificity
|
|
494
|
+
* and later source order, so the token lost every time and the root silently
|
|
495
|
+
* took the parent's colour instead -- which is indistinguishable from the
|
|
496
|
+
* token working, because the token's own fallback IS `inherit`. Only a caller
|
|
497
|
+
* that actually set `--animated-number-color` would have seen it ignored.
|
|
498
|
+
*
|
|
499
|
+
* The descendants still need the explicit declaration: a bare `span` selector
|
|
500
|
+
* in a host stylesheet outranks plain inheritance, which is the whole reason
|
|
501
|
+
* this block exists.
|
|
502
|
+
*/
|
|
503
|
+
.animated-number-plain,
|
|
504
|
+
.animated-number-visual,
|
|
505
|
+
.animated-number-digit,
|
|
506
|
+
.animated-number-digit-glyph,
|
|
507
|
+
.animated-number-literal {
|
|
508
|
+
color: inherit;
|
|
509
|
+
}
|
|
510
|
+
|
|
452
511
|
.animated-number-visual {
|
|
453
512
|
/* Decorative glyph content, same treatment Avatar, KeyboardInput, Tabs,
|
|
454
513
|
Calendar and Draggable already give theirs. */
|
|
@@ -495,6 +554,21 @@
|
|
|
495
554
|
margin-block: calc(-1 * var(--animated-number-fade-height, 0.28em));
|
|
496
555
|
}
|
|
497
556
|
|
|
557
|
+
/*
|
|
558
|
+
* `inline-block` above makes every column its own inline formatting context,
|
|
559
|
+
* and collapsible whitespace at the edges of one of those is dropped -- so a
|
|
560
|
+
* literal column holding exactly one space measured zero wide and "12m 15s"
|
|
561
|
+
* painted as "12m15s". `pre` is the narrowest fix that keeps the space: it
|
|
562
|
+
* changes nothing for a literal that is not whitespace, and a column is a
|
|
563
|
+
* single character, so there is no wrapping behaviour to lose.
|
|
564
|
+
*
|
|
565
|
+
* Digits are exempt because they are never whitespace and their column has
|
|
566
|
+
* its own width.
|
|
567
|
+
*/
|
|
568
|
+
.animated-number-literal {
|
|
569
|
+
white-space: pre;
|
|
570
|
+
}
|
|
571
|
+
|
|
498
572
|
.animated-number-digit {
|
|
499
573
|
position: relative;
|
|
500
574
|
width: var(--animated-number-digit-width, 1ch);
|
|
@@ -33,6 +33,22 @@ export type OptionalAnimatedNumberProperties = {
|
|
|
33
33
|
* @default 'off'
|
|
34
34
|
*/
|
|
35
35
|
live?: AnimatedNumberLive;
|
|
36
|
+
/**
|
|
37
|
+
* Rolls every column up from zero when the number first appears, instead of
|
|
38
|
+
* painting it outright.
|
|
39
|
+
*
|
|
40
|
+
* Off by default, because a number that was always going to be on screen
|
|
41
|
+
* should not make the reader wait for it. Turn it on where the figure ARRIVES
|
|
42
|
+
* -- a dashboard whose metrics land after a fetch, a total that appears at the
|
|
43
|
+
* end of a flow -- and especially where the value then never changes again:
|
|
44
|
+
* without this, such a number is static for its whole life and the odometer
|
|
45
|
+
* does nothing at all.
|
|
46
|
+
*
|
|
47
|
+
* Independent of the rolls that follow. A later change animates either way,
|
|
48
|
+
* and this only governs the first paint.
|
|
49
|
+
* @default false
|
|
50
|
+
*/
|
|
51
|
+
animateOnMount?: boolean;
|
|
36
52
|
/** Overrides the accessible name, which otherwise mirrors the visible text. */
|
|
37
53
|
ariaLabel?: string;
|
|
38
54
|
/** Value for the `data-pw` attribute on the root element for E2E selection. */
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import Tooltip from '../Tooltip/Tooltip.svelte';
|
|
5
5
|
import CheckListItem from '../CheckListItem/CheckListItem.svelte';
|
|
6
6
|
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.svelte';
|
|
7
|
+
import { isAnimatableMetric } from './metric';
|
|
7
8
|
|
|
8
9
|
let {
|
|
9
10
|
title,
|
|
@@ -14,6 +15,9 @@
|
|
|
14
15
|
footer,
|
|
15
16
|
valueSnippet,
|
|
16
17
|
animateValue = false,
|
|
18
|
+
animatePrimary = false,
|
|
19
|
+
primary = 'auto',
|
|
20
|
+
animateOnMount = false,
|
|
17
21
|
rows,
|
|
18
22
|
rowsDirection = 'column',
|
|
19
23
|
tooltip,
|
|
@@ -28,6 +32,99 @@
|
|
|
28
32
|
|
|
29
33
|
const isInteractive = $derived(typeof onclick === 'function');
|
|
30
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Which ONE value on this card rolls.
|
|
37
|
+
*
|
|
38
|
+
* A card has several numeric slots and they are not equally important. The
|
|
39
|
+
* business variant renders `subtitle` at 44px as the headline and its rows at
|
|
40
|
+
* 18-20px underneath, so animating "the values" animated the small ones and
|
|
41
|
+
* left the number the card is actually about sitting still -- backwards, and
|
|
42
|
+
* visible on every dashboard.
|
|
43
|
+
*
|
|
44
|
+
* `primary` resolves three ways so a caller can be as specific as it needs:
|
|
45
|
+
* 'auto' measure the rendered slots and pick the largest metric
|
|
46
|
+
* 'subtitle' | 'value' | <row index> name it outright
|
|
47
|
+
* 'auto' is the default because it follows the card's own type scale rather
|
|
48
|
+
* than a second copy of it kept in a mapper, so a card that restyles keeps
|
|
49
|
+
* animating the right thing. A caller that disagrees names the slot instead.
|
|
50
|
+
*
|
|
51
|
+
* Measured after mount rather than guessed from props: the sizes come from
|
|
52
|
+
* tokens the consuming app can override, so only the resolved style knows.
|
|
53
|
+
*/
|
|
54
|
+
let autoKey = $state<string | null>(null);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Re-measured whenever the card's values change, not once at mount.
|
|
58
|
+
*
|
|
59
|
+
* A card whose figures arrive from a fetch renders empty first: the balance
|
|
60
|
+
* card mounted with no balance, so the measurement found no metric, and
|
|
61
|
+
* because an attachment only re-runs when its argument changes it never looked
|
|
62
|
+
* again -- the number arrived and sat there unanimated. Keying the attachment
|
|
63
|
+
* on the values themselves fixes that, and also re-picks the primary if the
|
|
64
|
+
* card's content later changes shape.
|
|
65
|
+
*/
|
|
66
|
+
const valueSignature = $derived(
|
|
67
|
+
JSON.stringify([
|
|
68
|
+
value,
|
|
69
|
+
subtitle,
|
|
70
|
+
(rows ?? []).map((row) => [
|
|
71
|
+
row.value,
|
|
72
|
+
row.comparisonValue,
|
|
73
|
+
(row.breakdown ?? []).map((item) => item.value)
|
|
74
|
+
])
|
|
75
|
+
])
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const measurePrimary =
|
|
79
|
+
(_signature: string) =>
|
|
80
|
+
(node: HTMLElement): void => {
|
|
81
|
+
if (!animatePrimary || primary !== 'auto') {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
let best: string | null = null;
|
|
85
|
+
let bestSize = -1;
|
|
86
|
+
for (const slot of node.querySelectorAll('[data-sc-slot]')) {
|
|
87
|
+
if (!(slot instanceof HTMLElement)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!isAnimatableMetric(slot.textContent ?? '')) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const size = Number.parseFloat(getComputedStyle(slot).fontSize);
|
|
94
|
+
if (Number.isFinite(size) && size > bestSize) {
|
|
95
|
+
bestSize = size;
|
|
96
|
+
best = slot.dataset.scSlot ?? null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
autoKey = best;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A numeric `primary` names a row by index, and rows carry `data-sc-slot`
|
|
104
|
+
* values of `row-<index>`. Passing the number through `String` alone yielded
|
|
105
|
+
* `'0'`, which matches no slot at all, so `primary={0}` resolved to nothing
|
|
106
|
+
* and the card animated none of its values. The custom element declares the
|
|
107
|
+
* prop as a String, so `primary="0"` arrives the same way and needs the same
|
|
108
|
+
* normalisation -- hence matching the digits rather than only `typeof`.
|
|
109
|
+
*/
|
|
110
|
+
const slotKeyFor = (value: 'auto' | 'subtitle' | 'value' | number | string): string =>
|
|
111
|
+
typeof value === 'number' || /^\d+$/.test(String(value)) ? `row-${value}` : String(value);
|
|
112
|
+
|
|
113
|
+
const primaryKey = $derived(primary === 'auto' ? autoKey : slotKeyFor(primary));
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* `animateValue` keeps its old meaning -- every value on the card -- but is
|
|
117
|
+
* now gated on the value being a metric at all, so an identifier that merely
|
|
118
|
+
* contains digits is never rolled. See metric.ts: this is what stopped a store
|
|
119
|
+
* domain and a GA4 account id spinning on the anomaly cards.
|
|
120
|
+
*/
|
|
121
|
+
const animates = (key: string, text: unknown): boolean => {
|
|
122
|
+
if (typeof text !== 'string' || !isAnimatableMetric(text)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return animatePrimary ? key === primaryKey : animateValue;
|
|
126
|
+
};
|
|
127
|
+
|
|
31
128
|
/**
|
|
32
129
|
* When `deltaPositive` is explicitly provided use it; otherwise infer from
|
|
33
130
|
* the leading character of `delta` — '+' → positive, '-' → negative,
|
|
@@ -84,6 +181,7 @@
|
|
|
84
181
|
|
|
85
182
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
86
183
|
<div
|
|
184
|
+
{@attach measurePrimary(valueSignature)}
|
|
87
185
|
class="statcard {classes ?? ''}"
|
|
88
186
|
class:statcard-interactive={isInteractive}
|
|
89
187
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
@@ -189,11 +287,12 @@
|
|
|
189
287
|
class="statcard-value"
|
|
190
288
|
class:statcard-value-success={row.valueVariant === 'success'}
|
|
191
289
|
class:statcard-value-warning={row.valueVariant === 'warning'}
|
|
290
|
+
data-sc-slot={`row-${rowIndex}`}
|
|
192
291
|
data-pw={typeof testId === 'string' ? `${testId}-value-${rowIndex}` : null}
|
|
193
292
|
testID={typeof testId === 'string' ? `${testId}-value-${rowIndex}` : null}
|
|
194
293
|
>
|
|
195
|
-
{#if
|
|
196
|
-
<AnimatedNumber value={row.value} />
|
|
294
|
+
{#if animates(`row-${rowIndex}`, row.value)}
|
|
295
|
+
<AnimatedNumber value={row.value} {animateOnMount} />
|
|
197
296
|
{:else}
|
|
198
297
|
{row.value}
|
|
199
298
|
{/if}
|
|
@@ -204,7 +303,8 @@
|
|
|
204
303
|
data-pw={typeof testId === 'string' ? `${testId}-comparison-${rowIndex}` : null}
|
|
205
304
|
testID={typeof testId === 'string' ? `${testId}-comparison-${rowIndex}` : null}
|
|
206
305
|
>
|
|
207
|
-
/ {#if
|
|
306
|
+
/ {#if animates(`row-${rowIndex}-comparison`, row.comparisonValue)}<AnimatedNumber
|
|
307
|
+
{animateOnMount}
|
|
208
308
|
value={row.comparisonValue}
|
|
209
309
|
/>{:else}{row.comparisonValue}{/if}
|
|
210
310
|
</div>
|
|
@@ -264,8 +364,8 @@
|
|
|
264
364
|
<div class="statcard-breakdown-label">{breakdownItem.label}</div>
|
|
265
365
|
<div class="statcard-breakdown-value-line">
|
|
266
366
|
<div class="statcard-breakdown-value">
|
|
267
|
-
{#if
|
|
268
|
-
<AnimatedNumber value={breakdownItem.value} />
|
|
367
|
+
{#if animates(`breakdown-${rowIndex}-${breakdownIndex}`, breakdownItem.value)}
|
|
368
|
+
<AnimatedNumber value={breakdownItem.value} {animateOnMount} />
|
|
269
369
|
{:else}
|
|
270
370
|
{breakdownItem.value}
|
|
271
371
|
{/if}
|
|
@@ -297,11 +397,12 @@
|
|
|
297
397
|
{:else if typeof value === 'string' && value.length > 0}
|
|
298
398
|
<div
|
|
299
399
|
class="statcard-value"
|
|
400
|
+
data-sc-slot="value"
|
|
300
401
|
data-pw={typeof testId === 'string' ? `${testId}-value` : null}
|
|
301
402
|
testID={typeof testId === 'string' ? `${testId}-value` : null}
|
|
302
403
|
>
|
|
303
|
-
{#if
|
|
304
|
-
<AnimatedNumber {value} />
|
|
404
|
+
{#if animates('value', value)}
|
|
405
|
+
<AnimatedNumber {value} {animateOnMount} />
|
|
305
406
|
{:else}
|
|
306
407
|
{value}
|
|
307
408
|
{/if}
|
|
@@ -323,10 +424,15 @@
|
|
|
323
424
|
{#if typeof subtitle === 'string' && subtitle.length > 0}
|
|
324
425
|
<div
|
|
325
426
|
class="statcard-subtitle"
|
|
427
|
+
data-sc-slot="subtitle"
|
|
326
428
|
data-pw={typeof testId === 'string' ? `${testId}-subtitle` : null}
|
|
327
429
|
testID={typeof testId === 'string' ? `${testId}-subtitle` : null}
|
|
328
430
|
>
|
|
329
|
-
{subtitle}
|
|
431
|
+
{#if animates('subtitle', subtitle)}
|
|
432
|
+
<AnimatedNumber value={subtitle} {animateOnMount} />
|
|
433
|
+
{:else}
|
|
434
|
+
{subtitle}
|
|
435
|
+
{/if}
|
|
330
436
|
</div>
|
|
331
437
|
{/if}
|
|
332
438
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a displayed value is a METRIC worth rolling, or an identifier that
|
|
3
|
+
* merely contains digits.
|
|
4
|
+
*
|
|
5
|
+
* The distinction is not cosmetic. Adopting the odometer across a dashboard
|
|
6
|
+
* mapper animated `demo-store.myshopify.com` and `Shinchan Store GA4 (987654321)`
|
|
7
|
+
* on the anomaly surface: a store domain and a GA4 account id, rolled digit by
|
|
8
|
+
* digit as though they were measurements. Nothing was broken -- they went
|
|
9
|
+
* through the same `statcard-value` slot as `₹60k` -- which is exactly why a
|
|
10
|
+
* blanket "animate this card's values" is the wrong instruction.
|
|
11
|
+
*
|
|
12
|
+
* The rule is digits plus decoration. A metric is allowed to carry a currency
|
|
13
|
+
* symbol, a sign, a percent, group and decimal separators, and SHORT unit words
|
|
14
|
+
* -- `₹60k`, `90%`, `₹1.23Cr`, `12m 15s`, `-8%`. Anything else with a long
|
|
15
|
+
* alphabetic run, a dotted hostname or a parenthesised id is an identifier and
|
|
16
|
+
* is left alone.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately conservative in one direction only: a metric wrongly refused
|
|
19
|
+
* renders exactly as it does today, while an identifier wrongly accepted is a
|
|
20
|
+
* number spinning in a domain name.
|
|
21
|
+
*/
|
|
22
|
+
export declare const isAnimatableMetric: (text: string) => boolean;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a displayed value is a METRIC worth rolling, or an identifier that
|
|
3
|
+
* merely contains digits.
|
|
4
|
+
*
|
|
5
|
+
* The distinction is not cosmetic. Adopting the odometer across a dashboard
|
|
6
|
+
* mapper animated `demo-store.myshopify.com` and `Shinchan Store GA4 (987654321)`
|
|
7
|
+
* on the anomaly surface: a store domain and a GA4 account id, rolled digit by
|
|
8
|
+
* digit as though they were measurements. Nothing was broken -- they went
|
|
9
|
+
* through the same `statcard-value` slot as `₹60k` -- which is exactly why a
|
|
10
|
+
* blanket "animate this card's values" is the wrong instruction.
|
|
11
|
+
*
|
|
12
|
+
* The rule is digits plus decoration. A metric is allowed to carry a currency
|
|
13
|
+
* symbol, a sign, a percent, group and decimal separators, and SHORT unit words
|
|
14
|
+
* -- `₹60k`, `90%`, `₹1.23Cr`, `12m 15s`, `-8%`. Anything else with a long
|
|
15
|
+
* alphabetic run, a dotted hostname or a parenthesised id is an identifier and
|
|
16
|
+
* is left alone.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately conservative in one direction only: a metric wrongly refused
|
|
19
|
+
* renders exactly as it does today, while an identifier wrongly accepted is a
|
|
20
|
+
* number spinning in a domain name.
|
|
21
|
+
*/
|
|
22
|
+
/** Unit fragments a metric may carry. Matched case-insensitively, whole-run. */
|
|
23
|
+
const UNITS = new Set([
|
|
24
|
+
'k',
|
|
25
|
+
'l',
|
|
26
|
+
'm',
|
|
27
|
+
'b',
|
|
28
|
+
't',
|
|
29
|
+
'cr',
|
|
30
|
+
'lac',
|
|
31
|
+
'lakh',
|
|
32
|
+
'mn',
|
|
33
|
+
'bn',
|
|
34
|
+
's',
|
|
35
|
+
'ms',
|
|
36
|
+
'h',
|
|
37
|
+
'hr',
|
|
38
|
+
'hrs',
|
|
39
|
+
'd',
|
|
40
|
+
'day',
|
|
41
|
+
'days',
|
|
42
|
+
'min',
|
|
43
|
+
'mins',
|
|
44
|
+
'sec',
|
|
45
|
+
'secs',
|
|
46
|
+
'x',
|
|
47
|
+
'pt',
|
|
48
|
+
'pts',
|
|
49
|
+
'px'
|
|
50
|
+
]);
|
|
51
|
+
/** Decoration that carries no meaning for this decision. */
|
|
52
|
+
const DECORATION = /[\p{Nd}\s,.:+%()[\]]|\p{Sc}|\p{Pd}|\p{Sm}/gu;
|
|
53
|
+
export const isAnimatableMetric = (text) => {
|
|
54
|
+
if (typeof text !== 'string') {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const value = text.trim();
|
|
58
|
+
if (value.length === 0 || !/\p{Nd}/u.test(value)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// A dotted hostname or an email is an identifier however many digits it has.
|
|
62
|
+
if (/\.[a-z]{2,}(?:\/|$)|@|\/\//i.test(value)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// A parenthesised run of digits is an id in a label, not a measurement:
|
|
66
|
+
// "Shinchan Store GA4 (987654321)".
|
|
67
|
+
if (/\([^)]*\p{Nd}[^)]*\)/u.test(value)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
// What remains once digits and decoration are removed must be unit words only.
|
|
71
|
+
const remainder = value.replace(DECORATION, ' ');
|
|
72
|
+
const words = remainder.split(/\s+/).filter((word) => word.length > 0);
|
|
73
|
+
return words.every((word) => UNITS.has(word.toLowerCase()));
|
|
74
|
+
};
|
|
@@ -123,6 +123,38 @@ export type OptionalStatCardProperties = {
|
|
|
123
123
|
* @default false
|
|
124
124
|
*/
|
|
125
125
|
animateValue?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Rolls the animated values up from zero as the card appears, rather than
|
|
128
|
+
* painting them outright. Requires an animation mode to be active -- either
|
|
129
|
+
* `animateValue`, or `animatePrimary` having resolved a slot to animate.
|
|
130
|
+
* Ignored when neither applies, because there is then nothing to roll.
|
|
131
|
+
*
|
|
132
|
+
* Worth turning on for a card whose figures arrive once with a fetch and then
|
|
133
|
+
* do not change, which is the ordinary dashboard case -- otherwise the
|
|
134
|
+
* odometer has nothing to react to for the life of the page.
|
|
135
|
+
* @default false
|
|
136
|
+
*/
|
|
137
|
+
animateOnMount?: boolean;
|
|
138
|
+
/**
|
|
139
|
+
* Animate exactly ONE value on the card instead of all of them.
|
|
140
|
+
*
|
|
141
|
+
* Prefer this to `animateValue` on a dashboard. A card carries several numeric
|
|
142
|
+
* slots at different sizes, and animating all of them puts motion on the small
|
|
143
|
+
* supporting figures as loudly as on the headline -- which reads as noise, and
|
|
144
|
+
* on the business variant actually animated the 18px rows while the 44px
|
|
145
|
+
* subtitle above them sat still.
|
|
146
|
+
* @default false
|
|
147
|
+
*/
|
|
148
|
+
animatePrimary?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Which slot `animatePrimary` animates.
|
|
151
|
+
*
|
|
152
|
+
* `'auto'` measures the rendered slots and picks the largest one holding a
|
|
153
|
+
* metric, so the card follows its own type scale. Name a slot to override:
|
|
154
|
+
* `'subtitle'`, `'value'`, or a row index. Ignored unless `animatePrimary`.
|
|
155
|
+
* @default 'auto'
|
|
156
|
+
*/
|
|
157
|
+
primary?: 'auto' | 'subtitle' | 'value' | number;
|
|
126
158
|
/**
|
|
127
159
|
* Multiple metric rows. When provided, replaces the single value/delta row with
|
|
128
160
|
* a column of rows separated by dividers.
|