@juspay/svelte-ui-components 4.29.0 → 4.30.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/README.md +15 -14
- package/dist/Accordion/Accordion.svelte +18 -0
- package/dist/AnimatedNumber/AnimatedNumber.svelte +702 -0
- package/dist/AnimatedNumber/AnimatedNumber.svelte.d.ts +4 -0
- package/dist/AnimatedNumber/motion.d.ts +47 -0
- package/dist/AnimatedNumber/motion.js +190 -0
- package/dist/AnimatedNumber/properties.d.ts +42 -0
- package/dist/AnimatedNumber/properties.js +1 -0
- package/dist/Badge/Badge.svelte +20 -3
- package/dist/Badge/properties.d.ts +9 -0
- package/dist/Book/Book.svelte +9 -0
- package/dist/Calendar/Calendar.svelte +34 -2
- package/dist/Carousel/Carousel.svelte +17 -2
- package/dist/ChatBubble/ChatBubble.svelte +12 -0
- package/dist/ChatComposer/ChatComposer.svelte +9 -0
- package/dist/CommandMenu/CommandMenu.svelte +13 -0
- package/dist/DateRangePicker/DateRangePicker.svelte +77 -20
- package/dist/DeltaIndicator/DeltaIndicator.svelte +12 -1
- package/dist/DeltaIndicator/properties.d.ts +9 -0
- package/dist/Gallery/Gallery.svelte +16 -0
- package/dist/Gauge/Gauge.svelte +21 -2
- package/dist/Gauge/properties.d.ts +13 -0
- package/dist/Input/Input.svelte +34 -26
- package/dist/KeyboardInput/KeyboardInput.svelte +1 -1
- package/dist/ListItem/ListItem.svelte +27 -2
- package/dist/ListItem/label.d.ts +11 -0
- package/dist/ListItem/label.js +40 -0
- package/dist/ListItem/properties.d.ts +52 -0
- package/dist/MediaUpload/MediaUpload.svelte +12 -1
- package/dist/MediaUpload/properties.d.ts +9 -0
- package/dist/Menu/Menu.svelte +11 -2
- package/dist/Modal/Modal.svelte +14 -4
- package/dist/PieChart/PieChart.svelte +16 -1
- package/dist/PieChart/properties.d.ts +13 -0
- package/dist/Progress/Progress.svelte +21 -1
- package/dist/Progress/properties.d.ts +12 -0
- package/dist/ProportionBar/ProportionBar.svelte +7 -1
- package/dist/ProportionBar/ProportionBar.svelte.d.ts +0 -19
- package/dist/ProportionBar/properties.d.ts +10 -0
- package/dist/Select/Select.svelte +23 -8
- package/dist/SplitButton/SplitButton.svelte +4 -1
- package/dist/SplitInput/SplitInput.svelte +1 -1
- package/dist/StatCard/StatCard.svelte +22 -4
- package/dist/StatCard/properties.d.ts +12 -0
- package/dist/Table/BuiltinCell.svelte +19 -2
- package/dist/Table/cellData.js +3 -0
- package/dist/Table/properties.d.ts +15 -0
- package/dist/Tabs/Tabs.svelte +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/styles/theme-dark.css +27 -3
- package/dist-codemod/codemod/migration-assets/legacy-display.css +2 -1
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +4 -1
- package/dist-codemod/codemod/migration-assets/wc-display.json +5 -0
- package/dist-wc/index.js +1334 -754
- package/package.json +9 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.svelte';
|
|
2
3
|
import Button from '../Button/Button.svelte';
|
|
3
4
|
import Img from '../Img/Img.svelte';
|
|
4
5
|
import Shimmer from '../Shimmer/Shimmer.svelte';
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
fileIcon,
|
|
27
28
|
errorMessages,
|
|
28
29
|
files = $bindable([]),
|
|
30
|
+
animateCounter = false,
|
|
29
31
|
onchange,
|
|
30
32
|
onremove,
|
|
31
33
|
onerror,
|
|
@@ -191,7 +193,16 @@
|
|
|
191
193
|
<div class="label">{label}</div>
|
|
192
194
|
{/if}
|
|
193
195
|
{#if showCounter}
|
|
194
|
-
|
|
196
|
+
{#if animateCounter}
|
|
197
|
+
<!-- The whole "N / max" reads as one value to assistive technology.
|
|
198
|
+
Only the count changes, so only its digits roll; the separator and
|
|
199
|
+
the maximum sit still as literal columns. -->
|
|
200
|
+
<div class="counter">
|
|
201
|
+
<AnimatedNumber value={`${items.length} / ${maxLength}`} />
|
|
202
|
+
</div>
|
|
203
|
+
{:else}
|
|
204
|
+
<div class="counter">{items.length} / {maxLength}</div>
|
|
205
|
+
{/if}
|
|
195
206
|
{/if}
|
|
196
207
|
</div>
|
|
197
208
|
{/if}
|
|
@@ -36,6 +36,15 @@ export type OptionalMediaUploadProperties = {
|
|
|
36
36
|
fileIcon?: Snippet;
|
|
37
37
|
errorMessages?: MediaUploadErrorMessages;
|
|
38
38
|
files?: File[];
|
|
39
|
+
/**
|
|
40
|
+
* Routes the header's `N / maxLength` counter through `AnimatedNumber` so an
|
|
41
|
+
* add/remove rolls the count instead of swapping it as plain text. Only
|
|
42
|
+
* `items.length` animates -- the `/` separator and `maxLength` stay static,
|
|
43
|
+
* since neither of those actually changes. Off by default so an unset prop
|
|
44
|
+
* renders the exact static text node it always has.
|
|
45
|
+
* @default false
|
|
46
|
+
*/
|
|
47
|
+
animateCounter?: boolean;
|
|
39
48
|
testId?: string;
|
|
40
49
|
classes?: string;
|
|
41
50
|
};
|
package/dist/Menu/Menu.svelte
CHANGED
|
@@ -527,8 +527,17 @@
|
|
|
527
527
|
-webkit-tap-highlight-color: transparent;
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
|
|
531
|
-
|
|
530
|
+
/* :focus-visible, not :focus: the trigger is opened by mouse click as often
|
|
531
|
+
as by keyboard (onclick={toggle} above), and a ring on every mouse-opened
|
|
532
|
+
menu would be noise. The default used to be `none` with nothing standing
|
|
533
|
+
in for it, so a keyboard user tabbing to (or landing back on, after
|
|
534
|
+
Escape closes the dropdown) the trigger had no visible sign it was
|
|
535
|
+
focused. SplitButton's own trigger wraps this same element and used to
|
|
536
|
+
set --menu-trigger-focus-outline to `none` for the identical reason --
|
|
537
|
+
see its .split-button-trigger, now fixed to inherit this default instead. */
|
|
538
|
+
.menu-trigger:focus-visible {
|
|
539
|
+
outline: var(--menu-trigger-focus-outline, 2px solid #2563eb);
|
|
540
|
+
outline-offset: var(--menu-trigger-focus-outline-offset, 2px);
|
|
532
541
|
}
|
|
533
542
|
|
|
534
543
|
.menu-dropdown {
|
package/dist/Modal/Modal.svelte
CHANGED
|
@@ -445,10 +445,6 @@
|
|
|
445
445
|
border-radius: var(--modal-border-radius, var(--radius, 4px));
|
|
446
446
|
overflow: var(--modal-content-overflow, auto);
|
|
447
447
|
border-top: var(--modal-content-border-top);
|
|
448
|
-
/* tabindex="-1" makes this the focus-trap's fallback target when the modal
|
|
449
|
-
has no focusable content of its own (see _interaction/focus.ts); it is
|
|
450
|
-
never in the Tab order itself, so it needs no visible focus ring the way
|
|
451
|
-
a real focusable element would. Matches Sheet's sheet-panel. */
|
|
452
448
|
outline: none;
|
|
453
449
|
/* Viewport containment for every size class: only .fit-content used to carry
|
|
454
450
|
a max-height, so a size whose height var is overridden to fit-content (or
|
|
@@ -459,6 +455,20 @@
|
|
|
459
455
|
max-height: var(--modal-max-height, calc(100dvh - 32px));
|
|
460
456
|
}
|
|
461
457
|
|
|
458
|
+
/* The comment this replaced claimed tabindex="-1" meant this never needed a
|
|
459
|
+
ring because it's "never in the Tab order itself" -- true, but beside the
|
|
460
|
+
point: onMount calls focusEntryPoint(modalContent), which focuses THIS
|
|
461
|
+
element directly (real DOM focus, not merely reachable via Tab) whenever
|
|
462
|
+
the modal has no focusable content of its own. That is exactly Sheet's
|
|
463
|
+
sheet-panel problem, and sheet-panel got the fix this comment said it
|
|
464
|
+
"matches" -- this one didn't. :focus-visible so the trigger click that
|
|
465
|
+
opens the modal shows nothing new; offset is negative like Sheet's,
|
|
466
|
+
since a modal can be sized to fill nearly the whole viewport. */
|
|
467
|
+
.modal-content:focus-visible {
|
|
468
|
+
outline: var(--modal-content-focus-outline, 2px solid #2563eb);
|
|
469
|
+
outline-offset: var(--modal-content-focus-outline-offset, -2px);
|
|
470
|
+
}
|
|
471
|
+
|
|
462
472
|
.slot-content {
|
|
463
473
|
display: var(--modal-display, flex);
|
|
464
474
|
/* Flex children default to min-height: auto and refuse to shrink below their
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import ChartTooltip from '../_chart/ChartTooltip.svelte';
|
|
7
7
|
import Legend from '../_chart/Legend.svelte';
|
|
8
8
|
import DeltaIndicator from '../DeltaIndicator/DeltaIndicator.svelte';
|
|
9
|
+
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.svelte';
|
|
9
10
|
import { arcPath } from '../_chart/paths';
|
|
10
11
|
import { computePieLayout, pieSliceValue } from '../_chart/geometry';
|
|
11
12
|
import { getColor } from '../_chart/colors';
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
legendPosition = 'bottom',
|
|
46
47
|
legendMaxItems,
|
|
47
48
|
onlegendmore,
|
|
49
|
+
animateLegendValues = false,
|
|
48
50
|
percentDecimals = 0,
|
|
49
51
|
onchartready,
|
|
50
52
|
highlightedIndex = null,
|
|
@@ -123,6 +125,15 @@
|
|
|
123
125
|
(v: number): string =>
|
|
124
126
|
total === 0 ? '0%' : ((v / total) * 100).toFixed(percentDecimals) + '%'
|
|
125
127
|
);
|
|
128
|
+
// Builds the exact text the static legend branch below prints -- value,
|
|
129
|
+
// then a non-breaking space, then the percentage -- as one string, so
|
|
130
|
+
// the animateLegendValues branch feeds AnimatedNumber literally what a
|
|
131
|
+
// reader already sees today rather than a re-derived approximation of it.
|
|
132
|
+
let legendValueText = $derived.by(
|
|
133
|
+
() =>
|
|
134
|
+
(v: number): string =>
|
|
135
|
+
`${format(v)}\u00A0${pctFormat(v)}`
|
|
136
|
+
);
|
|
126
137
|
let isEmpty = $derived(data.length === 0 || total === 0);
|
|
127
138
|
|
|
128
139
|
// When semiCircle is true the effective aspect ratio is driven by:
|
|
@@ -590,7 +601,11 @@
|
|
|
590
601
|
<span class="pie-legend-swatch" style="background: {d.color ?? getColor(i)}"></span>
|
|
591
602
|
<span class="pie-legend-label">{d.label}</span>
|
|
592
603
|
<span class="pie-legend-value">
|
|
593
|
-
{
|
|
604
|
+
{#if animateLegendValues}
|
|
605
|
+
<AnimatedNumber value={legendValueText(pieSliceValue(d.value))} />
|
|
606
|
+
{:else}
|
|
607
|
+
{format(pieSliceValue(d.value))} {pctFormat(pieSliceValue(d.value))}
|
|
608
|
+
{/if}
|
|
594
609
|
</span>
|
|
595
610
|
</li>
|
|
596
611
|
{/each}
|
|
@@ -66,6 +66,19 @@ export type OptionalPieChartProperties = {
|
|
|
66
66
|
* the control toggles the hidden rows in place.
|
|
67
67
|
*/
|
|
68
68
|
onlegendmore?: () => void;
|
|
69
|
+
/**
|
|
70
|
+
* Routes `.pie-legend-value` (the `legendShowValues` list) through
|
|
71
|
+
* `AnimatedNumber` so a changed slice value rolls instead of jumping. Off
|
|
72
|
+
* by default because it is new: with it left unset, the legend renders the
|
|
73
|
+
* same static text node it always has. The exact string the static branch
|
|
74
|
+
* already prints -- `valueFormat` when supplied, else the built-in
|
|
75
|
+
* default, plus the percentage -- is passed straight through, so a
|
|
76
|
+
* consumer's own formatter keeps deciding what the digits say. The on-arc
|
|
77
|
+
* `<text>` labels can't host per-digit spans and are unaffected either
|
|
78
|
+
* way.
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
animateLegendValues?: boolean;
|
|
69
82
|
percentDecimals?: number;
|
|
70
83
|
/**
|
|
71
84
|
* Called once on mount with the imperative highlight API. Pass the returned
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { ProgressProperties } from './properties';
|
|
3
|
+
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.svelte';
|
|
3
4
|
|
|
4
5
|
let {
|
|
5
6
|
value,
|
|
6
7
|
max = 100,
|
|
7
8
|
showLabel = false,
|
|
9
|
+
animateValue = false,
|
|
8
10
|
ariaLabel,
|
|
9
11
|
testId,
|
|
10
12
|
classes
|
|
@@ -58,7 +60,25 @@
|
|
|
58
60
|
></div>
|
|
59
61
|
</div>
|
|
60
62
|
{#if showLabel && !isIndeterminate}
|
|
61
|
-
<div class="label">
|
|
63
|
+
<div class="label">
|
|
64
|
+
{#if animateValue}
|
|
65
|
+
<!-- The rounded percentage is already the display value labelText
|
|
66
|
+
computes, so it is passed as a plain number rather than through
|
|
67
|
+
`format`: `AnimatedNumber`'s Intl percent style expects a 0-1
|
|
68
|
+
fraction and would re-derive a different number from this
|
|
69
|
+
already-0-100 one. The % is a literal beside it instead, exactly
|
|
70
|
+
mirroring what labelText already spells out below. -->
|
|
71
|
+
<!-- The unit goes INSIDE AnimatedNumber, not beside it. The odometer
|
|
72
|
+
carries role="img" with its own aria-label, so a unit left outside is
|
|
73
|
+
invisible to anyone navigating by graphic -- the rotor reads "75" with
|
|
74
|
+
no sign of what it measures. Passing the formatted string keeps the
|
|
75
|
+
unit in the accessible name; only the digits move, since the string
|
|
76
|
+
path rolls the characters that changed and leaves the rest. -->
|
|
77
|
+
<AnimatedNumber value={`${Math.round(percentage)}%`} />
|
|
78
|
+
{:else}
|
|
79
|
+
{labelText}
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
62
82
|
{/if}
|
|
63
83
|
</div>
|
|
64
84
|
|
|
@@ -12,6 +12,18 @@ export type OptionalProgressProperties = {
|
|
|
12
12
|
*/
|
|
13
13
|
max?: number;
|
|
14
14
|
showLabel?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Routes the completion percentage through `AnimatedNumber` instead of
|
|
17
|
+
* static text, so a consumer that already renders `showLabel` gets the
|
|
18
|
+
* label visibly counting to its new value for free. Kept opt-in rather
|
|
19
|
+
* than folded into `showLabel` itself, since `AnimatedNumber` is a new
|
|
20
|
+
* dependency this component didn't previously have and existing markup
|
|
21
|
+
* must stay untouched by default. An indeterminate bar has no percentage
|
|
22
|
+
* to animate towards, so this is silently inert there rather than a state
|
|
23
|
+
* that would need its own documentation.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
animateValue?: boolean;
|
|
15
27
|
/**
|
|
16
28
|
* Accessible label for the progress element (`role="progressbar"`). Falls
|
|
17
29
|
* back to the computed percentage text (e.g. `"75%"`) when determinate, or
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* @see docs/ProportionBar.md
|
|
20
20
|
*/
|
|
21
|
+
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.svelte';
|
|
21
22
|
import type { ProportionBarProperties, ProportionBarSegment } from './properties';
|
|
22
23
|
|
|
23
24
|
const DEFAULT_PALETTE = ['#8F49DE', '#FFC533', '#62D5C0', '#FF74CD', '#D1E7FF'];
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
showLegend = true,
|
|
28
29
|
valueFormat,
|
|
29
30
|
trackHeight,
|
|
31
|
+
animateValue = false,
|
|
30
32
|
testId,
|
|
31
33
|
classes
|
|
32
34
|
}: ProportionBarProperties = $props();
|
|
@@ -152,7 +154,11 @@
|
|
|
152
154
|
></span>
|
|
153
155
|
<span class="proportion-bar-legend-label">{computedSegment.label}</span>
|
|
154
156
|
<span class="proportion-bar-legend-value">
|
|
155
|
-
{
|
|
157
|
+
{#if animateValue}
|
|
158
|
+
<AnimatedNumber value={formatValue(computedSegment.value, computedSegment.percent)} />
|
|
159
|
+
{:else}
|
|
160
|
+
{formatValue(computedSegment.value, computedSegment.percent)}
|
|
161
|
+
{/if}
|
|
156
162
|
</span>
|
|
157
163
|
</li>
|
|
158
164
|
{/each}
|
|
@@ -1,22 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ProportionBar — a horizontal stacked bar that visualises how a total is
|
|
3
|
-
* distributed across labelled segments. Renders proportional coloured bands in
|
|
4
|
-
* an SVG track with an optional legend. Each band's width is derived from its
|
|
5
|
-
* `value` relative to the sum of all segments; non-finite or negative values
|
|
6
|
-
* are treated as zero so the rendered widths always stay within 0–100%.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```svelte
|
|
10
|
-
* <ProportionBar
|
|
11
|
-
* segments={[
|
|
12
|
-
* { label: 'UPI', value: 480 },
|
|
13
|
-
* { label: 'Cards', value: 220 }
|
|
14
|
-
* ]}
|
|
15
|
-
* />
|
|
16
|
-
* ```
|
|
17
|
-
*
|
|
18
|
-
* @see docs/ProportionBar.md
|
|
19
|
-
*/
|
|
20
1
|
import type { ProportionBarProperties } from './properties';
|
|
21
2
|
declare const ProportionBar: import("svelte").Component<ProportionBarProperties, {}, "">;
|
|
22
3
|
type ProportionBar = ReturnType<typeof ProportionBar>;
|
|
@@ -27,6 +27,16 @@ export type OptionalProportionBarProperties = {
|
|
|
27
27
|
* `--proportion-bar-track-height` CSS variable.
|
|
28
28
|
*/
|
|
29
29
|
trackHeight?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Routes the legend value column through `AnimatedNumber` so a changed
|
|
32
|
+
* value rolls instead of jumping. Off by default because it is new: with it
|
|
33
|
+
* left unset, the legend renders the same static text node it always has.
|
|
34
|
+
* The formatted string from `valueFormat` (or the built-in default) is
|
|
35
|
+
* passed straight through, never the raw `value` number, so a consumer's
|
|
36
|
+
* own formatter keeps deciding what the digits say.
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
animateValue?: boolean;
|
|
30
40
|
/** Test selector applied as the `data-pw` attribute on the root element. */
|
|
31
41
|
testId?: string;
|
|
32
42
|
/** Extra CSS class names appended to the root element. */
|
|
@@ -451,10 +451,18 @@
|
|
|
451
451
|
|
|
452
452
|
async function moveHighlight(delta: number): Promise<void> {
|
|
453
453
|
const next = highlightedIndex + delta;
|
|
454
|
-
|
|
454
|
+
const inBounds = next >= 0 && next < optionRows.length;
|
|
455
|
+
// Off either end, wrap to the opposite edge instead of stopping -- landing
|
|
456
|
+
// on the nearest selectable row there the same way moveHighlightToEdge
|
|
457
|
+
// does for Home/End, rather than the raw first/last row which may be
|
|
458
|
+
// disabled. Wrapping only makes sense once the panel is open and has rows
|
|
459
|
+
// to land on; ArrowUp (unlike ArrowDown) reaches here even while closed,
|
|
460
|
+
// and closed this must stay the plain out-of-bounds no-op it always was.
|
|
461
|
+
const target = inBounds ? next : open ? nearestSelectableEdge(next < 0 ? 'last' : 'first') : -1;
|
|
462
|
+
if (target < 0) {
|
|
455
463
|
return;
|
|
456
464
|
}
|
|
457
|
-
highlightedIndex =
|
|
465
|
+
highlightedIndex = target;
|
|
458
466
|
await tick();
|
|
459
467
|
// Query the dropdown node itself, not containerEl, so highlight-scrolling
|
|
460
468
|
// keeps working once the panel is portaled out to <body>.
|
|
@@ -476,17 +484,24 @@
|
|
|
476
484
|
return event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
|
|
477
485
|
}
|
|
478
486
|
|
|
487
|
+
// The nearest selectable row from an edge, or -1 when nothing in that
|
|
488
|
+
// direction is selectable (including an empty list). Shared by Home/End
|
|
489
|
+
// (moveHighlightToEdge) and arrow-key wraparound (moveHighlight), which both
|
|
490
|
+
// need to land on a real option rather than a disabled row sitting at the
|
|
491
|
+
// very edge of the list.
|
|
492
|
+
function nearestSelectableEdge(edge: 'first' | 'last'): number {
|
|
493
|
+
return edge === 'first'
|
|
494
|
+
? optionRows.findIndex(isRowSelectable)
|
|
495
|
+
: optionRows.findLastIndex(isRowSelectable);
|
|
496
|
+
}
|
|
497
|
+
|
|
479
498
|
/**
|
|
480
499
|
* Home/End: jump the highlight to the first/last selectable row, skipping
|
|
481
|
-
* disabled options the same way
|
|
482
|
-
* targets a different index and reuses the same scroll-into-view step. A
|
|
500
|
+
* disabled options the same way arrow-key wraparound does at the ends. A
|
|
483
501
|
* no-op when no row in that direction is selectable, including an empty list.
|
|
484
502
|
*/
|
|
485
503
|
async function moveHighlightToEdge(edge: 'first' | 'last'): Promise<void> {
|
|
486
|
-
const target =
|
|
487
|
-
edge === 'first'
|
|
488
|
-
? optionRows.findIndex(isRowSelectable)
|
|
489
|
-
: optionRows.findLastIndex(isRowSelectable);
|
|
504
|
+
const target = nearestSelectableEdge(edge);
|
|
490
505
|
if (target < 0) {
|
|
491
506
|
return;
|
|
492
507
|
}
|
|
@@ -114,7 +114,10 @@
|
|
|
114
114
|
.split-button-trigger {
|
|
115
115
|
--menu-container-position: static;
|
|
116
116
|
--menu-container-display: flex;
|
|
117
|
-
--menu-trigger-focus-outline
|
|
117
|
+
/* Used to force --menu-trigger-focus-outline to `none` with nothing
|
|
118
|
+
standing in for it (WCAG 2.4.7) -- removed so the trigger falls
|
|
119
|
+
through to Menu's own :focus-visible default instead of suppressing
|
|
120
|
+
it. See Menu.svelte's .menu-trigger:focus-visible for the fix. */
|
|
118
121
|
--menu-dropdown-left: 0;
|
|
119
122
|
--menu-min-width: 100%;
|
|
120
123
|
border-left: var(--split-button-trigger-separator, 1px solid rgba(255, 255, 255, 0.3));
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
align-items: center;
|
|
368
368
|
height: var(--field-group-input-height, 36px);
|
|
369
369
|
font-size: var(--field-group-separator-font-size, 16px);
|
|
370
|
-
color: var(--field-group-separator-color, #
|
|
370
|
+
color: var(--field-group-separator-color, #6b7280);
|
|
371
371
|
flex-shrink: 0;
|
|
372
372
|
}
|
|
373
373
|
</style>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import DeltaIndicator from '../DeltaIndicator/DeltaIndicator.svelte';
|
|
4
4
|
import Tooltip from '../Tooltip/Tooltip.svelte';
|
|
5
5
|
import CheckListItem from '../CheckListItem/CheckListItem.svelte';
|
|
6
|
+
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.svelte';
|
|
6
7
|
|
|
7
8
|
let {
|
|
8
9
|
title,
|
|
@@ -12,6 +13,7 @@
|
|
|
12
13
|
subtitle,
|
|
13
14
|
footer,
|
|
14
15
|
valueSnippet,
|
|
16
|
+
animateValue = false,
|
|
15
17
|
rows,
|
|
16
18
|
rowsDirection = 'column',
|
|
17
19
|
tooltip,
|
|
@@ -190,7 +192,11 @@
|
|
|
190
192
|
data-pw={typeof testId === 'string' ? `${testId}-value-${rowIndex}` : null}
|
|
191
193
|
testID={typeof testId === 'string' ? `${testId}-value-${rowIndex}` : null}
|
|
192
194
|
>
|
|
193
|
-
{
|
|
195
|
+
{#if animateValue}
|
|
196
|
+
<AnimatedNumber value={row.value} />
|
|
197
|
+
{:else}
|
|
198
|
+
{row.value}
|
|
199
|
+
{/if}
|
|
194
200
|
</div>
|
|
195
201
|
{#if typeof row.comparisonValue === 'string' && row.comparisonValue.length > 0}
|
|
196
202
|
<div
|
|
@@ -198,7 +204,9 @@
|
|
|
198
204
|
data-pw={typeof testId === 'string' ? `${testId}-comparison-${rowIndex}` : null}
|
|
199
205
|
testID={typeof testId === 'string' ? `${testId}-comparison-${rowIndex}` : null}
|
|
200
206
|
>
|
|
201
|
-
/ {
|
|
207
|
+
/ {#if animateValue}<AnimatedNumber
|
|
208
|
+
value={row.comparisonValue}
|
|
209
|
+
/>{:else}{row.comparisonValue}{/if}
|
|
202
210
|
</div>
|
|
203
211
|
{/if}
|
|
204
212
|
{#if typeof row.change === 'number'}
|
|
@@ -255,7 +263,13 @@
|
|
|
255
263
|
>
|
|
256
264
|
<div class="statcard-breakdown-label">{breakdownItem.label}</div>
|
|
257
265
|
<div class="statcard-breakdown-value-line">
|
|
258
|
-
<div class="statcard-breakdown-value">
|
|
266
|
+
<div class="statcard-breakdown-value">
|
|
267
|
+
{#if animateValue}
|
|
268
|
+
<AnimatedNumber value={breakdownItem.value} />
|
|
269
|
+
{:else}
|
|
270
|
+
{breakdownItem.value}
|
|
271
|
+
{/if}
|
|
272
|
+
</div>
|
|
259
273
|
{#if typeof breakdownItem.change === 'number'}
|
|
260
274
|
<DeltaIndicator
|
|
261
275
|
value={breakdownItem.change}
|
|
@@ -286,7 +300,11 @@
|
|
|
286
300
|
data-pw={typeof testId === 'string' ? `${testId}-value` : null}
|
|
287
301
|
testID={typeof testId === 'string' ? `${testId}-value` : null}
|
|
288
302
|
>
|
|
289
|
-
{
|
|
303
|
+
{#if animateValue}
|
|
304
|
+
<AnimatedNumber {value} />
|
|
305
|
+
{:else}
|
|
306
|
+
{value}
|
|
307
|
+
{/if}
|
|
290
308
|
</div>
|
|
291
309
|
{/if}
|
|
292
310
|
|
|
@@ -111,6 +111,18 @@ export type OptionalStatCardProperties = {
|
|
|
111
111
|
footer?: Snippet;
|
|
112
112
|
/** Replaces the string `value` with a custom snippet for advanced value rendering. */
|
|
113
113
|
valueSnippet?: Snippet;
|
|
114
|
+
/**
|
|
115
|
+
* Rolls `value`, each row's `value`/`comparisonValue`, and each breakdown
|
|
116
|
+
* item's `value` through AnimatedNumber's per-digit odometer instead of
|
|
117
|
+
* swapping the text outright. Every one of those is already a
|
|
118
|
+
* consumer-preformatted string, not a raw number -- AnimatedNumber diffs a
|
|
119
|
+
* string by character position, so `'₹1.23Cr'` -> `'₹1.45Cr'` rolls only the
|
|
120
|
+
* two digits that changed and leaves `₹`, the decimal point and `Cr` alone,
|
|
121
|
+
* with nothing here ever surfacing the number underneath. `valueSnippet`
|
|
122
|
+
* still wins outright when set, exactly as it does today.
|
|
123
|
+
* @default false
|
|
124
|
+
*/
|
|
125
|
+
animateValue?: boolean;
|
|
114
126
|
/**
|
|
115
127
|
* Multiple metric rows. When provided, replaces the single value/delta row with
|
|
116
128
|
* a column of rows separated by dividers.
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import Select from '../Select/Select.svelte';
|
|
26
26
|
import Input from '../Input/Input.svelte';
|
|
27
27
|
import Menu from '../Menu/Menu.svelte';
|
|
28
|
+
import AnimatedNumber from '../AnimatedNumber/AnimatedNumber.svelte';
|
|
28
29
|
import trendUpSvg from '../assets/trend-up.svg?raw';
|
|
29
30
|
import trendDownSvg from '../assets/trend-down.svg?raw';
|
|
30
31
|
import copySvg from '../assets/copy.svg?raw';
|
|
@@ -258,7 +259,15 @@
|
|
|
258
259
|
>
|
|
259
260
|
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
260
261
|
<span class="builtin-trend-icon">{@html trendUpSvg}</span>
|
|
261
|
-
{compare.
|
|
262
|
+
{#if compare.animateTrend}
|
|
263
|
+
<!-- The string form, not the raw number: it keeps the % inside the
|
|
264
|
+
odometer's accessible name, and avoids Intl grouping a raw
|
|
265
|
+
trendPercent into "1,234.5" where the plain branch shows
|
|
266
|
+
"1234.5". -->
|
|
267
|
+
<AnimatedNumber value={`${compare.trendPercent}%`} />
|
|
268
|
+
{:else}
|
|
269
|
+
{compare.trendPercent}%
|
|
270
|
+
{/if}
|
|
262
271
|
</span>
|
|
263
272
|
{:else if compare.trendPercent < 0}
|
|
264
273
|
<span
|
|
@@ -268,7 +277,15 @@
|
|
|
268
277
|
>
|
|
269
278
|
<!-- eslint-disable svelte/no-at-html-tags -->
|
|
270
279
|
<span class="builtin-trend-icon">{@html trendDownSvg}</span>
|
|
271
|
-
{compare.
|
|
280
|
+
{#if compare.animateTrend}
|
|
281
|
+
<!-- The string form, not the raw number: it keeps the % inside the
|
|
282
|
+
odometer's accessible name, and avoids Intl grouping a raw
|
|
283
|
+
trendPercent into "1,234.5" where the plain branch shows
|
|
284
|
+
"1234.5". -->
|
|
285
|
+
<AnimatedNumber value={`${compare.trendPercent}%`} />
|
|
286
|
+
{:else}
|
|
287
|
+
{compare.trendPercent}%
|
|
288
|
+
{/if}
|
|
272
289
|
</span>
|
|
273
290
|
{:else}
|
|
274
291
|
<span class="builtin-trend builtin-trend-flat">↔ 0%</span>
|
package/dist/Table/cellData.js
CHANGED
|
@@ -116,6 +116,9 @@ export const asCompareCellData = (value) => {
|
|
|
116
116
|
if (typeof record.trendLabel === 'string') {
|
|
117
117
|
compareData.trendLabel = record.trendLabel;
|
|
118
118
|
}
|
|
119
|
+
if (typeof record.animateTrend === 'boolean') {
|
|
120
|
+
compareData.animateTrend = record.animateTrend;
|
|
121
|
+
}
|
|
119
122
|
return compareData;
|
|
120
123
|
};
|
|
121
124
|
export const asLinkCellData = (value) => {
|
|
@@ -89,6 +89,21 @@ export type TableCompareCellData = {
|
|
|
89
89
|
comparison?: string;
|
|
90
90
|
trendPercent?: number;
|
|
91
91
|
trendLabel?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Rolls `trendPercent` through `AnimatedNumber` instead of the plain text
|
|
94
|
+
* it renders as today, so a comparison row that updates in place (a live
|
|
95
|
+
* dashboard, a polling total) visibly counts to its new value rather than
|
|
96
|
+
* jumping. Per-cell rather than a Table-level prop, since a single column
|
|
97
|
+
* can mix trend rows a consumer wants live with historical ones that don't
|
|
98
|
+
* need to move. `AnimatedNumber` is handed the complete string
|
|
99
|
+
* `` `${trendPercent}%` ``, so the unit sits inside its accessible name
|
|
100
|
+
* rather than beside it, where a reader navigating by graphic would never
|
|
101
|
+
* reach it. The `%` stays a literal character rather than an
|
|
102
|
+
* `Intl.NumberFormat` percent style, which would divide the value by 100
|
|
103
|
+
* and render the wrong figure, and it is a literal column that never rolls.
|
|
104
|
+
* @default false
|
|
105
|
+
*/
|
|
106
|
+
animateTrend?: boolean;
|
|
92
107
|
};
|
|
93
108
|
/** Cell shape for `type: 'toggle'` — the handler lives on the column. */
|
|
94
109
|
export type TableToggleCellData = {
|
package/dist/Tabs/Tabs.svelte
CHANGED
|
@@ -638,6 +638,22 @@
|
|
|
638
638
|
transition: var(--tabs-transition, color 0.2s ease, background 0.2s ease);
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
/* Roving focus really does move (see focusedKey/focusTabAt above) -- this
|
|
642
|
+
just replaces the INDICATOR that outline:none above strips, which is what
|
|
643
|
+
was actually missing (WCAG 2.4.7). :focus-visible only, so a mouse click
|
|
644
|
+
that activates a tab still shows nothing new, matching the rest of this
|
|
645
|
+
file's pattern (Sheet's .sheet-panel, Select's .select-clear, ...).
|
|
646
|
+
currentColor tracks --tabs-item-color / --tabs-active-color, which are
|
|
647
|
+
already themed per-mode and per-state, so the ring stays visible against
|
|
648
|
+
--tabs-bar-background in both themes with no extra token needed here. The
|
|
649
|
+
offset is inset, not the usual outward one: .tabs-bar is a same-axis
|
|
650
|
+
overflow:auto scroller with 0px padding/gap by default, so an outward ring
|
|
651
|
+
on an edge tab would be clipped by the scroll container's own edge. */
|
|
652
|
+
.tabs-item:focus-visible {
|
|
653
|
+
outline: var(--tabs-item-focus-outline, 2px solid currentColor);
|
|
654
|
+
outline-offset: var(--tabs-item-focus-outline-offset, -2px);
|
|
655
|
+
}
|
|
656
|
+
|
|
641
657
|
.tabs-item :global(.tabs-item-icon) {
|
|
642
658
|
--image-width: var(--tabs-item-icon-size, 16px);
|
|
643
659
|
--image-height: var(--tabs-item-icon-size, 16px);
|
package/dist/index.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export { default as FileDropzoneTrigger } from './FileDropzoneTrigger/FileDropzo
|
|
|
86
86
|
export { default as Separator } from './Separator/Separator.svelte';
|
|
87
87
|
export { default as Label } from './Label/Label.svelte';
|
|
88
88
|
export { default as AspectRatio } from './AspectRatio/AspectRatio.svelte';
|
|
89
|
+
export { default as AnimatedNumber } from './AnimatedNumber/AnimatedNumber.svelte';
|
|
89
90
|
export { default as RatingGroup } from './RatingGroup/RatingGroup.svelte';
|
|
90
91
|
export { default as Chat } from './Chat/Chat.svelte';
|
|
91
92
|
export { default as ChatHeader } from './ChatHeader/ChatHeader.svelte';
|
|
@@ -154,6 +155,7 @@ export type * from './LoadingDots/properties';
|
|
|
154
155
|
export type * from './Separator/properties';
|
|
155
156
|
export type * from './Label/properties';
|
|
156
157
|
export type * from './AspectRatio/properties';
|
|
158
|
+
export type * from './AnimatedNumber/properties';
|
|
157
159
|
export type * from './RatingGroup/properties';
|
|
158
160
|
export type * from './Menu/properties';
|
|
159
161
|
export type * from './Sheet/properties';
|
package/dist/index.js
CHANGED
|
@@ -86,6 +86,7 @@ export { default as FileDropzoneTrigger } from './FileDropzoneTrigger/FileDropzo
|
|
|
86
86
|
export { default as Separator } from './Separator/Separator.svelte';
|
|
87
87
|
export { default as Label } from './Label/Label.svelte';
|
|
88
88
|
export { default as AspectRatio } from './AspectRatio/AspectRatio.svelte';
|
|
89
|
+
export { default as AnimatedNumber } from './AnimatedNumber/AnimatedNumber.svelte';
|
|
89
90
|
export { default as RatingGroup } from './RatingGroup/RatingGroup.svelte';
|
|
90
91
|
export { default as Chat } from './Chat/Chat.svelte';
|
|
91
92
|
export { default as ChatHeader } from './ChatHeader/ChatHeader.svelte';
|