@inkra/react 0.2.0 → 0.3.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 +86 -4
- package/dist/alert-dialog/index.d.ts +1 -1
- package/dist/alert-dialog/index.js +4 -2
- package/dist/command/index.d.ts +24 -2
- package/dist/command/index.js +2 -2
- package/dist/context-menu/index.d.ts +2 -2
- package/dist/context-menu/index.js +3 -2
- package/dist/drawer/index.d.ts +1 -1
- package/dist/drawer/index.js +6 -4
- package/dist/dropdown-menu/index.d.ts +1 -1
- package/dist/dropdown-menu/index.js +3 -2
- package/dist/form/EditablePencilText.js +34 -0
- package/dist/form/Input.js +1 -1
- package/dist/input-number/index.d.ts +6 -0
- package/dist/input-number/index.js +126 -15
- package/dist/input-number/model.d.ts +13 -0
- package/dist/input-number/model.js +50 -0
- package/dist/number-ticker/index.d.ts +11 -9
- package/dist/number-ticker/index.js +14 -11
- package/dist/number-ticker/model.d.ts +38 -5
- package/dist/number-ticker/model.js +139 -10
- package/dist/number-ticker/precision.d.ts +7 -0
- package/dist/number-ticker/precision.js +134 -0
- package/dist/overlay/Overlay.d.ts +1 -1
- package/dist/overlay/Overlay.js +4 -2
- package/dist/overlay/Popover.d.ts +1 -1
- package/dist/overlay/Popover.js +3 -2
- package/dist/overlay/themeBridge.d.ts +12 -1
- package/dist/overlay/themeBridge.js +27 -1
- package/dist/radar/index.d.ts +2 -0
- package/dist/radar/index.js +2 -2
- package/dist/sortable/index.js +9 -4
- package/dist/styles.css +147 -22
- package/dist/tooltip/Tooltip.d.ts +1 -1
- package/dist/tooltip/Tooltip.js +3 -2
- package/dist/virtual-list/index.js +3 -2
- package/package.json +4 -3
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
3
3
|
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
4
4
|
import { Button } from '../Button.js';
|
|
5
5
|
import { PencilIcon } from '../PencilIcon.js';
|
|
6
6
|
import { Input } from '../form/Input.js';
|
|
7
7
|
import { useResolvedControlProps } from '../form/controlProps.js';
|
|
8
8
|
import { Tooltip } from '../tooltip/Tooltip.js';
|
|
9
|
+
import { normalizeInputNumberRepeat, resolveInputNumberStep, stepInputNumber, } from './model.js';
|
|
9
10
|
// Lucide Plus / Minus ISC geometry uses the shared pencil renderer.
|
|
10
11
|
const plusPaths = [{ d: 'M5 12h14' }, { d: 'M12 5v14' }];
|
|
11
12
|
const minusPaths = [{ d: 'M5 12h14' }];
|
|
12
13
|
const finiteValue = (value) => typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
13
14
|
const format = (value) => String(finiteValue(value) ?? '');
|
|
14
15
|
const numberFromDraft = (draft) => draft === '' ? null : finiteValue(Number(draft));
|
|
15
|
-
export const InputNumber = forwardRef(function InputNumber({ classNames, controls = true, decrementButtonLabel = 'Decrease value', defaultValue, disabled, incrementButtonLabel = 'Increase value', inputMode = 'decimal', max, min, onChange, onValueChange, readOnly, size = 'md', step = 1, textAlign = 'center', value, ...props }, forwardedRef) {
|
|
16
|
+
export const InputNumber = forwardRef(function InputNumber({ classNames, controls = true, decrementButtonLabel = 'Decrease value', defaultValue, disabled, incrementButtonLabel = 'Increase value', inputMode = 'decimal', max, min, onChange, onBlur, onKeyDown, onValueChange, pressAndHold = true, readOnly, repeatDelay = 400, repeatInterval = 80, size = 'md', step = 1, stepModifiers, textAlign = 'center', value, ...props }, forwardedRef) {
|
|
16
17
|
const inputRef = useRef(null);
|
|
17
18
|
const isControlled = value !== undefined;
|
|
18
19
|
const [draft, setDraft] = useState(() => format(isControlled ? value : defaultValue));
|
|
@@ -21,6 +22,50 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
21
22
|
const latest = useRef({ isControlled, value, defaultValue });
|
|
22
23
|
latest.current = { isControlled, value, defaultValue };
|
|
23
24
|
useImperativeHandle(forwardedRef, () => inputRef.current);
|
|
25
|
+
const holdRef = useRef(null);
|
|
26
|
+
const timerRef = useRef(undefined);
|
|
27
|
+
const suppressClickRef = useRef(false);
|
|
28
|
+
const stopHolding = useCallback(() => {
|
|
29
|
+
clearTimeout(timerRef.current);
|
|
30
|
+
timerRef.current = undefined;
|
|
31
|
+
const hold = holdRef.current;
|
|
32
|
+
holdRef.current = null;
|
|
33
|
+
if (hold?.button.hasPointerCapture?.(hold.pointerId)) {
|
|
34
|
+
try {
|
|
35
|
+
hold.button.releasePointerCapture(hold.pointerId);
|
|
36
|
+
}
|
|
37
|
+
catch { /* The pointer may already be inactive. */ }
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const onVisibilityChange = () => { if (document.hidden)
|
|
42
|
+
stopHolding(); };
|
|
43
|
+
const onPointerEnd = (event) => {
|
|
44
|
+
if (event.pointerId === holdRef.current?.pointerId)
|
|
45
|
+
stopHolding();
|
|
46
|
+
};
|
|
47
|
+
const updateModifiers = (event) => {
|
|
48
|
+
if (holdRef.current)
|
|
49
|
+
holdRef.current.modifiers = {
|
|
50
|
+
shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
window.addEventListener('blur', stopHolding);
|
|
54
|
+
window.addEventListener('pointerup', onPointerEnd);
|
|
55
|
+
window.addEventListener('pointercancel', onPointerEnd);
|
|
56
|
+
window.addEventListener('keydown', updateModifiers);
|
|
57
|
+
window.addEventListener('keyup', updateModifiers);
|
|
58
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
59
|
+
return () => {
|
|
60
|
+
stopHolding();
|
|
61
|
+
window.removeEventListener('blur', stopHolding);
|
|
62
|
+
window.removeEventListener('pointerup', onPointerEnd);
|
|
63
|
+
window.removeEventListener('pointercancel', onPointerEnd);
|
|
64
|
+
window.removeEventListener('keydown', updateModifiers);
|
|
65
|
+
window.removeEventListener('keyup', updateModifiers);
|
|
66
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
67
|
+
};
|
|
68
|
+
}, [stopHolding]);
|
|
24
69
|
useBrowserLayoutEffect(() => {
|
|
25
70
|
if (isControlled && currentValue !== finiteValue(value))
|
|
26
71
|
setDraft(format(value));
|
|
@@ -32,6 +77,7 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
32
77
|
return;
|
|
33
78
|
let resetTimer;
|
|
34
79
|
const reset = (event) => {
|
|
80
|
+
stopHolding();
|
|
35
81
|
clearTimeout(resetTimer);
|
|
36
82
|
// Native reset runs after the event; restore the public default, not the latest draft.
|
|
37
83
|
resetTimer = setTimeout(() => {
|
|
@@ -45,33 +91,98 @@ export const InputNumber = forwardRef(function InputNumber({ classNames, control
|
|
|
45
91
|
};
|
|
46
92
|
form.addEventListener('reset', reset);
|
|
47
93
|
return () => { clearTimeout(resetTimer); form.removeEventListener('reset', reset); };
|
|
48
|
-
}, [props.form]);
|
|
94
|
+
}, [props.form, stopHolding]);
|
|
49
95
|
const cannotStep = state.disabled || state.readOnly || step === 'any' || (min !== undefined && max !== undefined && min > max);
|
|
50
96
|
const decreaseDisabled = cannotStep || (currentValue !== null && min !== undefined && currentValue <= min);
|
|
51
97
|
const increaseDisabled = cannotStep || (currentValue !== null && max !== undefined && currentValue >= max);
|
|
52
|
-
|
|
98
|
+
useBrowserLayoutEffect(() => {
|
|
99
|
+
const direction = holdRef.current?.direction;
|
|
100
|
+
if (cannotStep || !controls || !pressAndHold
|
|
101
|
+
|| (direction === 'increment' && increaseDisabled)
|
|
102
|
+
|| (direction === 'decrement' && decreaseDisabled))
|
|
103
|
+
stopHolding();
|
|
104
|
+
}, [cannotStep, controls, decreaseDisabled, increaseDisabled, pressAndHold, stopHolding]);
|
|
105
|
+
function changeStep(direction, modifiers) {
|
|
53
106
|
const input = inputRef.current;
|
|
54
107
|
if (!input || input.matches(':disabled') || input.readOnly || cannotStep)
|
|
55
|
-
return;
|
|
56
|
-
const
|
|
57
|
-
// Native stepping applies the step base and decimal rounding without a custom parser.
|
|
58
|
-
if (direction === 'increment')
|
|
59
|
-
input.stepUp();
|
|
60
|
-
else
|
|
61
|
-
input.stepDown();
|
|
108
|
+
return false;
|
|
109
|
+
const changed = stepInputNumber(input, direction, resolveInputNumberStep(modifiers, stepModifiers));
|
|
62
110
|
input.focus({ preventScroll: true });
|
|
63
|
-
if (
|
|
64
|
-
return;
|
|
111
|
+
if (!changed)
|
|
112
|
+
return false;
|
|
65
113
|
setDraft(input.value);
|
|
66
114
|
onValueChange?.(Number.isFinite(input.valueAsNumber) ? input.valueAsNumber : null, { reason: direction });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const stepRef = useRef(changeStep);
|
|
118
|
+
stepRef.current = changeStep;
|
|
119
|
+
const intervalRef = useRef(repeatInterval);
|
|
120
|
+
intervalRef.current = repeatInterval;
|
|
121
|
+
function startHolding(event, direction) {
|
|
122
|
+
if (event.button !== 0 || !event.isPrimary)
|
|
123
|
+
return;
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
stopHolding();
|
|
126
|
+
suppressClickRef.current = false;
|
|
127
|
+
if (!pressAndHold)
|
|
128
|
+
return;
|
|
129
|
+
suppressClickRef.current = true;
|
|
130
|
+
const modifiers = {
|
|
131
|
+
shiftKey: event.shiftKey, altKey: event.altKey, ctrlKey: event.ctrlKey, metaKey: event.metaKey,
|
|
132
|
+
};
|
|
133
|
+
if (!changeStep(direction, modifiers))
|
|
134
|
+
return;
|
|
135
|
+
const button = event.currentTarget;
|
|
136
|
+
if (!button.isConnected || button.disabled)
|
|
137
|
+
return;
|
|
138
|
+
holdRef.current = { button, pointerId: event.pointerId, direction, modifiers };
|
|
139
|
+
try {
|
|
140
|
+
button.setPointerCapture(event.pointerId);
|
|
141
|
+
}
|
|
142
|
+
catch { /* Window listeners retain release handling. */ }
|
|
143
|
+
const repeat = () => {
|
|
144
|
+
const hold = holdRef.current;
|
|
145
|
+
if (!hold || document.hidden || !stepRef.current(hold.direction, hold.modifiers)) {
|
|
146
|
+
stopHolding();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (holdRef.current)
|
|
150
|
+
timerRef.current = setTimeout(repeat, normalizeInputNumberRepeat(intervalRef.current, 80, 32));
|
|
151
|
+
};
|
|
152
|
+
timerRef.current = setTimeout(repeat, normalizeInputNumberRepeat(repeatDelay, 400, 150));
|
|
153
|
+
}
|
|
154
|
+
function buttonEvents(direction) {
|
|
155
|
+
return {
|
|
156
|
+
onPointerDown: (event) => startHolding(event, direction),
|
|
157
|
+
onPointerUp: stopHolding,
|
|
158
|
+
onPointerCancel: stopHolding,
|
|
159
|
+
onLostPointerCapture: stopHolding,
|
|
160
|
+
onBlur: stopHolding,
|
|
161
|
+
onClick: (event) => {
|
|
162
|
+
if (event.detail > 0 && suppressClickRef.current) {
|
|
163
|
+
suppressClickRef.current = false;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
suppressClickRef.current = false;
|
|
167
|
+
changeStep(direction, event);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
67
170
|
}
|
|
68
171
|
const iconSize = size === 'sm' ? 18 : size === 'lg' ? 26 : 22;
|
|
69
172
|
const buttonSize = size === 'sm' ? 'icon-sm' : size === 'lg' ? 'icon-lg' : 'icon';
|
|
70
|
-
return _jsx(Input, { ...props, ref: inputRef, type: "number", inputMode: inputMode, textAlign: textAlign, clearable: false, size: size, min: min, max: max, step: step, disabled: state.disabled, readOnly: state.readOnly, value: draft, classNames: { ...classNames, root: ['inkra-input-number', classNames?.root].filter(Boolean).join(' ') },
|
|
173
|
+
return _jsx(Input, { ...props, ref: inputRef, type: "number", inputMode: inputMode, textAlign: textAlign, clearable: false, size: size, min: min, max: max, step: step, disabled: state.disabled, readOnly: state.readOnly, value: draft, classNames: { ...classNames, root: ['inkra-input-number', classNames?.root].filter(Boolean).join(' ') }, onBlur: event => { stopHolding(); onBlur?.(event); }, onKeyDown: event => {
|
|
174
|
+
onKeyDown?.(event);
|
|
175
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown'))
|
|
176
|
+
return;
|
|
177
|
+
if (resolveInputNumberStep(event, stepModifiers) === undefined)
|
|
178
|
+
return;
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
changeStep(event.key === 'ArrowUp' ? 'increment' : 'decrement', event);
|
|
181
|
+
}, onChange: event => {
|
|
71
182
|
const input = event.currentTarget;
|
|
72
183
|
const nextValue = Number.isFinite(input.valueAsNumber) ? input.valueAsNumber : null;
|
|
73
184
|
setDraft(input.value);
|
|
74
185
|
onChange?.(event);
|
|
75
186
|
onValueChange?.(nextValue, { reason: 'input' });
|
|
76
|
-
}, startAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: decreaseDisabled, "aria-label": decrementButtonLabel,
|
|
187
|
+
}, startAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: decreaseDisabled, "aria-label": decrementButtonLabel, ...buttonEvents('decrement'), children: _jsx(PencilIcon, { paths: minusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: decrementButtonLabel }) })] }) }) }) : undefined, endAction: controls ? _jsx(Tooltip.Provider, { children: _jsx("span", { className: "inkra-input-number__controls", children: _jsxs(Tooltip.Root, { children: [_jsx(Tooltip.Trigger, { asChild: true, children: _jsx(Button, { type: "button", size: buttonSize, variant: "ghost", disabled: increaseDisabled, "aria-label": incrementButtonLabel, ...buttonEvents('increment'), children: _jsx(PencilIcon, { paths: plusPaths, size: iconSize, sourceSize: 24 }) }) }), _jsx(Tooltip.Portal, { children: _jsx(Tooltip.Content, { children: incrementButtonLabel }) })] }) }) }) : undefined });
|
|
77
188
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface InputNumberStepModifier {
|
|
2
|
+
shiftKey?: boolean;
|
|
3
|
+
altKey?: boolean;
|
|
4
|
+
ctrlKey?: boolean;
|
|
5
|
+
metaKey?: boolean;
|
|
6
|
+
step: number;
|
|
7
|
+
}
|
|
8
|
+
export type InputNumberModifierState = Pick<KeyboardEvent, 'shiftKey' | 'altKey' | 'ctrlKey' | 'metaKey'>;
|
|
9
|
+
export type InputNumberStepDirection = 'increment' | 'decrement';
|
|
10
|
+
export declare const defaultInputNumberStepModifiers: readonly InputNumberStepModifier[];
|
|
11
|
+
export declare function resolveInputNumberStep(modifiers: InputNumberModifierState, rules?: readonly InputNumberStepModifier[]): number | undefined;
|
|
12
|
+
export declare function normalizeInputNumberRepeat(value: number | undefined, fallback: number, minimum: number): number;
|
|
13
|
+
export declare function stepInputNumber(input: HTMLInputElement, direction: InputNumberStepDirection, modifierStep?: number): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const defaultInputNumberStepModifiers = [
|
|
2
|
+
{ shiftKey: true, step: 10 },
|
|
3
|
+
];
|
|
4
|
+
export function resolveInputNumberStep(modifiers, rules = defaultInputNumberStepModifiers) {
|
|
5
|
+
return rules.find(rule => Number.isFinite(rule.step) && rule.step > 0
|
|
6
|
+
&& Boolean(rule.shiftKey) === modifiers.shiftKey
|
|
7
|
+
&& Boolean(rule.altKey) === modifiers.altKey
|
|
8
|
+
&& Boolean(rule.ctrlKey) === modifiers.ctrlKey
|
|
9
|
+
&& Boolean(rule.metaKey) === modifiers.metaKey)?.step;
|
|
10
|
+
}
|
|
11
|
+
export function normalizeInputNumberRepeat(value, fallback, minimum) {
|
|
12
|
+
return value === undefined || !Number.isFinite(value)
|
|
13
|
+
? fallback
|
|
14
|
+
: Math.min(60000, Math.max(minimum, value));
|
|
15
|
+
}
|
|
16
|
+
export function stepInputNumber(input, direction, modifierStep) {
|
|
17
|
+
const before = input.value;
|
|
18
|
+
const min = input.min === '' ? -Infinity : Number(input.min);
|
|
19
|
+
const max = input.max === '' ? Infinity : Number(input.max);
|
|
20
|
+
if (input.disabled || input.readOnly || input.step === 'any' || min > max)
|
|
21
|
+
return false;
|
|
22
|
+
if ((direction === 'increment' && input.valueAsNumber >= max)
|
|
23
|
+
|| (direction === 'decrement' && input.valueAsNumber <= min))
|
|
24
|
+
return false;
|
|
25
|
+
if (modifierStep === undefined) {
|
|
26
|
+
if (direction === 'increment')
|
|
27
|
+
input.stepUp();
|
|
28
|
+
else
|
|
29
|
+
input.stepDown();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// A detached native input anchors the step base at the current value, so
|
|
33
|
+
// modifier steps are absolute deltas without floating-point arithmetic.
|
|
34
|
+
const probe = input.cloneNode(false);
|
|
35
|
+
probe.removeAttribute('min');
|
|
36
|
+
probe.removeAttribute('max');
|
|
37
|
+
probe.step = String(modifierStep);
|
|
38
|
+
probe.defaultValue = before || '0';
|
|
39
|
+
probe.value = before;
|
|
40
|
+
if (direction === 'increment')
|
|
41
|
+
probe.stepUp();
|
|
42
|
+
else
|
|
43
|
+
probe.stepDown();
|
|
44
|
+
const next = probe.valueAsNumber;
|
|
45
|
+
if (!Number.isFinite(next))
|
|
46
|
+
return false;
|
|
47
|
+
input.value = String(Math.min(Number.isFinite(max) ? max : Infinity, Math.max(Number.isFinite(min) ? min : -Infinity, next)));
|
|
48
|
+
}
|
|
49
|
+
return before !== input.value;
|
|
50
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
2
|
-
import type { NumberTickerLocale } from './model.js';
|
|
2
|
+
import type { NumberTickerCompact, NumberTickerCompactOptions, NumberTickerFormatOptions, NumberTickerLocale, NumberTickerValue } from './model.js';
|
|
3
3
|
export interface NumberTickerRenderDetails {
|
|
4
4
|
animating: boolean;
|
|
5
5
|
formattedValue: string;
|
|
@@ -9,21 +9,23 @@ export interface NumberTickerDeltaDetails {
|
|
|
9
9
|
formattedDelta: string;
|
|
10
10
|
}
|
|
11
11
|
export interface NumberTickerProps extends Omit<ComponentPropsWithoutRef<'span'>, 'children' | 'prefix'> {
|
|
12
|
-
value:
|
|
12
|
+
value: NumberTickerValue;
|
|
13
13
|
locale?: NumberTickerLocale;
|
|
14
|
-
formatOptions?:
|
|
14
|
+
formatOptions?: NumberTickerFormatOptions;
|
|
15
|
+
compact?: NumberTickerCompact;
|
|
16
|
+
compactOptions?: NumberTickerCompactOptions;
|
|
15
17
|
prefix?: ReactNode;
|
|
16
18
|
suffix?: ReactNode;
|
|
17
|
-
renderValue?: (value:
|
|
18
|
-
getValueLabel?: (value:
|
|
19
|
+
renderValue?: (value: NumberTickerValue, details: NumberTickerRenderDetails) => ReactNode;
|
|
20
|
+
getValueLabel?: (value: NumberTickerValue, formattedValue: string) => string;
|
|
19
21
|
animate?: boolean;
|
|
20
22
|
duration?: number;
|
|
21
23
|
easing?: (progress: number) => number;
|
|
22
24
|
showDelta?: boolean;
|
|
23
25
|
deltaDuration?: number;
|
|
24
|
-
deltaFormatOptions?:
|
|
25
|
-
renderDelta?: (delta:
|
|
26
|
+
deltaFormatOptions?: NumberTickerFormatOptions;
|
|
27
|
+
renderDelta?: (delta: NumberTickerValue, details: NumberTickerDeltaDetails) => ReactNode;
|
|
26
28
|
}
|
|
27
29
|
export declare const NumberTicker: import("react").ForwardRefExoticComponent<NumberTickerProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
28
|
-
export { createNumberFormatter, easeOutCubic, formatTickerDelta, formatTickerValue, interpolateTickerValue, } from './model.js';
|
|
29
|
-
export type { NumberTickerLocale } from './model.js';
|
|
30
|
+
export { createNumberFormatter, easeOutCubic, formatTickerDelta, formatTickerValue, interpolateTickerValue, numberTickerUnits, } from './model.js';
|
|
31
|
+
export type { NumberTickerCompact, NumberTickerCompactOptions, NumberTickerFormatOptions, NumberTickerLocale, NumberTickerUnit, NumberTickerValue } from './model.js';
|
|
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { PencilText } from '../PencilText.js';
|
|
4
4
|
import { useBrowserLayoutEffect } from '../internal/useBrowserLayoutEffect.js';
|
|
5
|
-
import { canAnimateTickerValue, createNumberFormatter, easeOutCubic,
|
|
5
|
+
import { canAnimateTickerValue, createNumberFormatter, createTickerFormatter, easeOutCubic, formatDerivedTickerDelta, formatTickerValue, interpolateTickerValue, tickerValueChanged, subtractTickerValues, tickerValueKey, } from './model.js';
|
|
6
|
+
import { ExactDecimal } from './precision.js';
|
|
6
7
|
function usePrefersReducedMotion() {
|
|
7
8
|
const [reduced, setReduced] = useState(false);
|
|
8
9
|
useBrowserLayoutEffect(() => {
|
|
@@ -22,7 +23,7 @@ function lettering(content, seed) {
|
|
|
22
23
|
function accessibleAffix(content) {
|
|
23
24
|
return typeof content === 'string' || typeof content === 'number' ? String(content) : '';
|
|
24
25
|
}
|
|
25
|
-
export const NumberTicker = forwardRef(function NumberTicker({ animate = true, className, deltaDuration = 1600, deltaFormatOptions, duration = 420, easing = easeOutCubic, formatOptions, getValueLabel, locale, prefix, renderDelta, renderValue, showDelta = false, suffix, value, ...props }, forwardedRef) {
|
|
26
|
+
export const NumberTicker = forwardRef(function NumberTicker({ animate = true, className, compact = false, compactOptions, deltaDuration = 1600, deltaFormatOptions, duration = 420, easing = easeOutCubic, formatOptions, getValueLabel, locale, prefix, renderDelta, renderValue, showDelta = false, suffix, value, ...props }, forwardedRef) {
|
|
26
27
|
const rootRef = useRef(null);
|
|
27
28
|
const frameRef = useRef(null);
|
|
28
29
|
const deltaTimerRef = useRef(null);
|
|
@@ -35,15 +36,17 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
|
|
|
35
36
|
const reducedMotion = usePrefersReducedMotion();
|
|
36
37
|
const localeKey = Array.isArray(locale) ? locale.join('\u0000') : locale;
|
|
37
38
|
const formatOptionsKey = JSON.stringify(formatOptions ?? {});
|
|
39
|
+
const compactOptionsKey = tickerValueKey(compactOptions ?? {});
|
|
38
40
|
const deltaFormatOptionsKey = JSON.stringify(deltaFormatOptions ?? formatOptions ?? {});
|
|
39
|
-
const formatter = useMemo(() =>
|
|
40
|
-
const deltaFormatter = useMemo(() =>
|
|
41
|
+
const formatter = useMemo(() => createTickerFormatter(locale, formatOptions, compact, compactOptions), [localeKey, formatOptionsKey, compact, compactOptionsKey]);
|
|
42
|
+
const deltaFormatter = useMemo(() => createTickerFormatter(locale, deltaFormatOptions ?? formatOptions, compact, compactOptions), [localeKey, deltaFormatOptionsKey, compact, compactOptionsKey]);
|
|
43
|
+
const announcementFormatter = useMemo(() => createNumberFormatter(locale, compact ? { ...formatOptions, notation: 'standard' } : formatOptions), [localeKey, formatOptionsKey, compact]);
|
|
41
44
|
useImperativeHandle(forwardedRef, () => rootRef.current);
|
|
42
45
|
useBrowserLayoutEffect(() => {
|
|
43
46
|
const previousTarget = targetRef.current;
|
|
44
47
|
targetRef.current = value;
|
|
45
|
-
if (showDelta && tickerValueChanged(previousTarget, value)
|
|
46
|
-
const nextDelta = value
|
|
48
|
+
if (showDelta && tickerValueChanged(previousTarget, value)) {
|
|
49
|
+
const nextDelta = subtractTickerValues(value, previousTarget);
|
|
47
50
|
setDelta(nextDelta);
|
|
48
51
|
if (deltaTimerRef.current !== null)
|
|
49
52
|
clearTimeout(deltaTimerRef.current);
|
|
@@ -109,7 +112,7 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
|
|
|
109
112
|
}, []);
|
|
110
113
|
const formattedValue = formatTickerValue(formatter, displayedValue);
|
|
111
114
|
const finalFormattedValue = formatTickerValue(formatter, value);
|
|
112
|
-
const announcedFormattedValue = formatTickerValue(
|
|
115
|
+
const announcedFormattedValue = formatTickerValue(announcementFormatter, announcedValue);
|
|
113
116
|
const defaultAccessibleLabel = [accessibleAffix(prefix), announcedFormattedValue, accessibleAffix(suffix)]
|
|
114
117
|
.filter(Boolean).join(' ');
|
|
115
118
|
const accessibleLabel = getValueLabel?.(announcedValue, announcedFormattedValue) ?? defaultAccessibleLabel;
|
|
@@ -118,15 +121,15 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, c
|
|
|
118
121
|
? _jsx("span", { className: "inkra-number-ticker__native-value", children: formattedValue })
|
|
119
122
|
: _jsx(PencilText, { seed: `number-ticker-${finalFormattedValue}`, children: finalFormattedValue }));
|
|
120
123
|
const deltaContent = delta === null ? null : (() => {
|
|
121
|
-
const formattedDelta =
|
|
124
|
+
const formattedDelta = formatDerivedTickerDelta(deltaFormatter, delta);
|
|
122
125
|
const details = {
|
|
123
|
-
direction: delta
|
|
126
|
+
direction: new ExactDecimal(String(delta)).lt(0) ? 'decrease' : 'increase',
|
|
124
127
|
formattedDelta,
|
|
125
128
|
};
|
|
126
129
|
return renderDelta?.(delta, details) ?? _jsx(PencilText, { seed: `number-ticker-delta-${formattedDelta}`, children: formattedDelta });
|
|
127
130
|
})();
|
|
128
131
|
const classes = ['inkra-number-ticker', animating && 'inkra-number-ticker--animating', className]
|
|
129
132
|
.filter(Boolean).join(' ');
|
|
130
|
-
return _jsxs("span", { ...props, className: classes, ref: rootRef, "data-animating": animating || undefined, children: [_jsxs("span", { "aria-hidden": "true", className: "inkra-number-ticker__visual", children: [prefix !== undefined && _jsx("span", { className: "inkra-number-ticker__prefix", children: lettering(prefix, 'number-ticker-prefix') }), _jsx("span", { className: "inkra-number-ticker__value", children: valueContent }), suffix !== undefined && _jsx("span", { className: "inkra-number-ticker__suffix", children: lettering(suffix, 'number-ticker-suffix') }), deltaContent !== null && _jsx("span", { className: "inkra-number-ticker__delta", "data-direction": delta
|
|
133
|
+
return _jsxs("span", { ...props, className: classes, ref: rootRef, "data-animating": animating || undefined, children: [_jsxs("span", { "aria-hidden": "true", className: "inkra-number-ticker__visual", children: [prefix !== undefined && _jsx("span", { className: "inkra-number-ticker__prefix", children: lettering(prefix, 'number-ticker-prefix') }), _jsx("span", { className: "inkra-number-ticker__value", children: valueContent }), suffix !== undefined && _jsx("span", { className: "inkra-number-ticker__suffix", children: lettering(suffix, 'number-ticker-suffix') }), deltaContent !== null && _jsx("span", { className: "inkra-number-ticker__delta", "data-direction": new ExactDecimal(String(delta)).lt(0) ? 'decrease' : 'increase', children: deltaContent })] }), _jsx("span", { className: "inkra-number-ticker__announcement", "aria-atomic": "true", "aria-live": "polite", children: accessibleLabel })] });
|
|
131
134
|
});
|
|
132
|
-
export { createNumberFormatter, easeOutCubic, formatTickerDelta, formatTickerValue, interpolateTickerValue, } from './model.js';
|
|
135
|
+
export { createNumberFormatter, easeOutCubic, formatTickerDelta, formatTickerValue, interpolateTickerValue, numberTickerUnits, } from './model.js';
|
|
@@ -1,8 +1,41 @@
|
|
|
1
|
+
import { tickerDecimal } from './precision.js';
|
|
2
|
+
import type { NumberTickerValue } from './precision.js';
|
|
3
|
+
export { subtractTickerValues, tickerValueKey } from './precision.js';
|
|
4
|
+
export type { NumberTickerValue } from './precision.js';
|
|
1
5
|
export type NumberTickerLocale = string | readonly string[];
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
|
|
6
|
+
export type NumberTickerCompact = boolean | 'en' | 'zh';
|
|
7
|
+
export interface NumberTickerFormatOptions extends Intl.NumberFormatOptions {
|
|
8
|
+
roundingMode?: 'ceil' | 'floor' | 'expand' | 'trunc' | 'halfCeil' | 'halfFloor' | 'halfExpand' | 'halfTrunc' | 'halfEven';
|
|
9
|
+
}
|
|
10
|
+
export interface NumberTickerUnit {
|
|
11
|
+
factor: NumberTickerValue;
|
|
12
|
+
label: string;
|
|
13
|
+
/** Absolute display magnitude required to enter this unit. */
|
|
14
|
+
threshold?: NumberTickerValue;
|
|
15
|
+
}
|
|
16
|
+
export interface NumberTickerCompactOptions {
|
|
17
|
+
units?: readonly NumberTickerUnit[];
|
|
18
|
+
minUnit?: string;
|
|
19
|
+
maxUnit?: string;
|
|
20
|
+
separator?: string;
|
|
21
|
+
promotionThreshold?: NumberTickerValue;
|
|
22
|
+
promoteAfterRounding?: boolean;
|
|
23
|
+
overflow?: 'unit' | 'scientific';
|
|
24
|
+
}
|
|
25
|
+
export declare const numberTickerUnits: Readonly<Record<'en' | 'zh', readonly NumberTickerUnit[]>>;
|
|
26
|
+
type TickerFormatter = {
|
|
27
|
+
format(value: NumberTickerValue): string;
|
|
28
|
+
formatToParts(value: NumberTickerValue): Intl.NumberFormatPart[];
|
|
29
|
+
formatDecimal(value: ReturnType<typeof tickerDecimal>): Intl.NumberFormatPart[];
|
|
30
|
+
};
|
|
31
|
+
export declare function createNumberFormatter(locale?: NumberTickerLocale, options?: NumberTickerFormatOptions): Intl.NumberFormat;
|
|
32
|
+
export declare function createTickerFormatter(locale?: NumberTickerLocale, options?: NumberTickerFormatOptions, compact?: NumberTickerCompact, compactOptions?: NumberTickerCompactOptions): TickerFormatter;
|
|
33
|
+
export declare function formatTickerValue(formatter: TickerFormatter | Intl.NumberFormat, value: NumberTickerValue): string;
|
|
34
|
+
export declare function formatTickerDelta(formatter: TickerFormatter | Intl.NumberFormat, delta: NumberTickerValue): string;
|
|
35
|
+
export declare function formatDerivedTickerDelta(formatter: TickerFormatter, delta: NumberTickerValue): string;
|
|
5
36
|
export declare function interpolateTickerValue(from: number, to: number, progress: number): number;
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
37
|
+
export declare function interpolateTickerValue(from: bigint, to: bigint, progress: number): bigint;
|
|
38
|
+
export declare function interpolateTickerValue(from: NumberTickerValue, to: NumberTickerValue, progress: number): NumberTickerValue;
|
|
39
|
+
export declare function tickerValueChanged(previous: NumberTickerValue, next: NumberTickerValue): boolean;
|
|
40
|
+
export declare function canAnimateTickerValue(from: NumberTickerValue, to: NumberTickerValue, duration: number): boolean;
|
|
8
41
|
export declare function easeOutCubic(progress: number): number;
|
|
@@ -1,29 +1,158 @@
|
|
|
1
|
+
import { ExactDecimal, formatExactParts, tickerDecimal } from './precision.js';
|
|
2
|
+
export { subtractTickerValues, tickerValueKey } from './precision.js';
|
|
3
|
+
export const numberTickerUnits = {
|
|
4
|
+
en: ['K', 'M', 'B', 'T', 'Qa', 'Qi', 'Sx', 'Sp', 'Oc', 'No', 'Dc']
|
|
5
|
+
.map((label, index) => ({ label, factor: Number(`1e${(index + 1) * 3}`) })),
|
|
6
|
+
zh: [{ label: '千', factor: 1e3 }, ...['万', '亿', '兆', '京', '垓', '秭', '穰', '沟', '涧', '正', '载', '极', '恒河沙', '阿僧祇', '那由他', '不可思议', '无量大数']
|
|
7
|
+
.map((label, index) => ({ label, factor: Number(`1e${(index + 1) * 4}`) }))],
|
|
8
|
+
};
|
|
1
9
|
export function createNumberFormatter(locale = 'en-US', options) {
|
|
2
|
-
|
|
10
|
+
const formatter = new Intl.NumberFormat(locale, options);
|
|
11
|
+
if (options?.roundingMode && options.roundingMode !== 'halfExpand' && !('roundingMode' in formatter.resolvedOptions())) {
|
|
12
|
+
throw new RangeError('NumberTicker roundingMode requires Intl.NumberFormat rounding support');
|
|
13
|
+
}
|
|
14
|
+
return formatter;
|
|
15
|
+
}
|
|
16
|
+
export function createTickerFormatter(locale = 'en-US', options, compact = false, compactOptions = {}) {
|
|
17
|
+
if (!compact || options?.notation === 'scientific' || options?.notation === 'engineering') {
|
|
18
|
+
const native = createNumberFormatter(locale, options);
|
|
19
|
+
const formatToParts = (value) => formatExactParts(native, value);
|
|
20
|
+
return { formatToParts, formatDecimal: value => formatExactParts(native, value), format: value => formatToParts(value).map(part => part.value).join('') };
|
|
21
|
+
}
|
|
22
|
+
const precision = {
|
|
23
|
+
minimumFractionDigits: 0,
|
|
24
|
+
maximumFractionDigits: Math.max(1, options?.minimumFractionDigits ?? 0),
|
|
25
|
+
...options,
|
|
26
|
+
notation: 'standard',
|
|
27
|
+
};
|
|
28
|
+
const formatter = createNumberFormatter(locale, precision);
|
|
29
|
+
const language = compact === true
|
|
30
|
+
? (formatter.resolvedOptions().locale.startsWith('zh') ? 'zh' : 'en')
|
|
31
|
+
: compact;
|
|
32
|
+
const multiplier = tickerDecimal(compactOptions.promotionThreshold ?? 1);
|
|
33
|
+
if (!multiplier.gt(0) || !multiplier.isFinite())
|
|
34
|
+
throw new RangeError('promotionThreshold must be positive and finite');
|
|
35
|
+
const supplied = [...(compactOptions.units ?? numberTickerUnits[language])]
|
|
36
|
+
.map(unit => ({ ...unit, factor: tickerDecimal(unit.factor), threshold: unit.threshold === undefined ? undefined : tickerDecimal(unit.threshold) }))
|
|
37
|
+
.sort((a, b) => a.factor.cmp(b.factor));
|
|
38
|
+
const labels = new Set();
|
|
39
|
+
let previousFactor = tickerDecimal(1);
|
|
40
|
+
let previousThreshold = tickerDecimal(0);
|
|
41
|
+
for (const unit of supplied) {
|
|
42
|
+
const threshold = unit.threshold ?? unit.factor.times(multiplier);
|
|
43
|
+
if (!unit.label.trim() || labels.has(unit.label) || !unit.factor.isFinite() || unit.factor.lte(previousFactor)
|
|
44
|
+
|| !threshold.isFinite() || threshold.lte(previousThreshold)) {
|
|
45
|
+
throw new RangeError('Compact units need unique nonempty labels, increasing finite factors > 1 and positive increasing thresholds');
|
|
46
|
+
}
|
|
47
|
+
labels.add(unit.label);
|
|
48
|
+
previousFactor = unit.factor;
|
|
49
|
+
previousThreshold = threshold;
|
|
50
|
+
}
|
|
51
|
+
const units = [{ factor: tickerDecimal(1), label: '', threshold: undefined }, ...supplied];
|
|
52
|
+
const indexFor = (label, fallback) => {
|
|
53
|
+
if (label === undefined)
|
|
54
|
+
return fallback;
|
|
55
|
+
const index = units.findIndex(unit => unit.label === label);
|
|
56
|
+
if (index < 0)
|
|
57
|
+
throw new RangeError(`Unknown compact unit: ${label}`);
|
|
58
|
+
return index;
|
|
59
|
+
};
|
|
60
|
+
const minIndex = indexFor(compactOptions.minUnit, 0);
|
|
61
|
+
const maxIndex = indexFor(compactOptions.maxUnit, units.length - 1);
|
|
62
|
+
if (minIndex > maxIndex)
|
|
63
|
+
throw new RangeError('minUnit must not exceed maxUnit');
|
|
64
|
+
const thresholdFor = (index) => units[index].threshold ?? units[index].factor.times(multiplier);
|
|
65
|
+
const last = units[maxIndex];
|
|
66
|
+
const overflowThreshold = maxIndex + 1 < units.length ? thresholdFor(maxIndex + 1)
|
|
67
|
+
: thresholdFor(maxIndex).times(maxIndex > 0 ? last.factor.div(units[maxIndex - 1].factor) : 1000);
|
|
68
|
+
const overflow = compactOptions.overflow ?? (compactOptions.maxUnit !== undefined ? 'unit' : 'scientific');
|
|
69
|
+
const scientific = createNumberFormatter(locale, { ...precision, notation: 'scientific' });
|
|
70
|
+
const rounding = createNumberFormatter('en-US', {
|
|
71
|
+
...precision, style: 'decimal', numberingSystem: 'latn', useGrouping: false,
|
|
72
|
+
});
|
|
73
|
+
const percentScale = options?.style === 'percent' ? 100 : 1;
|
|
74
|
+
const formatDecimal = (exact) => {
|
|
75
|
+
if (!exact.isFinite())
|
|
76
|
+
return formatExactParts(formatter, exact);
|
|
77
|
+
let index = minIndex;
|
|
78
|
+
const reaches = (threshold) => {
|
|
79
|
+
if (compactOptions.promoteAfterRounding === false)
|
|
80
|
+
return exact.abs().times(percentScale).gte(threshold);
|
|
81
|
+
const scaled = exact.div(units[index].factor).times(percentScale);
|
|
82
|
+
const rounded = formatExactParts(rounding, scaled).map(part => part.value).join('');
|
|
83
|
+
return new ExactDecimal(rounded).abs().times(units[index].factor).gte(threshold);
|
|
84
|
+
};
|
|
85
|
+
while (index < maxIndex) {
|
|
86
|
+
if (!reaches(thresholdFor(index + 1)))
|
|
87
|
+
break;
|
|
88
|
+
index++;
|
|
89
|
+
}
|
|
90
|
+
if (index === maxIndex && overflow === 'scientific' && reaches(overflowThreshold))
|
|
91
|
+
return formatExactParts(scientific, exact);
|
|
92
|
+
const { factor, label } = units[index];
|
|
93
|
+
const parts = formatExactParts(formatter, exact.div(factor));
|
|
94
|
+
if (label) {
|
|
95
|
+
let insertion = parts.length;
|
|
96
|
+
while (insertion > 0 && !['integer', 'fraction'].includes(parts[insertion - 1].type))
|
|
97
|
+
insertion--;
|
|
98
|
+
const separator = compactOptions.separator ?? ' ';
|
|
99
|
+
parts.splice(insertion, 0, ...(separator ? [{ type: 'literal', value: separator }] : []), { type: 'compact', value: label });
|
|
100
|
+
}
|
|
101
|
+
return parts;
|
|
102
|
+
};
|
|
103
|
+
const formatToParts = (value) => formatDecimal(tickerDecimal(value));
|
|
104
|
+
return { formatToParts, formatDecimal, format: value => formatToParts(value).map(part => part.value).join('') };
|
|
3
105
|
}
|
|
4
106
|
export function formatTickerValue(formatter, value) {
|
|
5
|
-
return formatter.format(value);
|
|
107
|
+
return formatter instanceof Intl.NumberFormat ? formatExactParts(formatter, value).map(part => part.value).join('') : formatter.format(value);
|
|
6
108
|
}
|
|
7
109
|
export function formatTickerDelta(formatter, delta) {
|
|
8
|
-
const
|
|
9
|
-
|
|
110
|
+
const exact = tickerDecimal(delta);
|
|
111
|
+
return formatDecimalDelta(formatter, exact);
|
|
112
|
+
}
|
|
113
|
+
// Deltas are derived from validated inputs and can legitimately exceed input bounds.
|
|
114
|
+
export function formatDerivedTickerDelta(formatter, delta) {
|
|
115
|
+
return formatDecimalDelta(formatter, new ExactDecimal(String(delta)));
|
|
116
|
+
}
|
|
117
|
+
function formatDecimalDelta(formatter, exact) {
|
|
118
|
+
const sign = exact.gt(0) ? '+' : exact.lt(0) ? '\u2212' : '';
|
|
119
|
+
const magnitude = (formatter instanceof Intl.NumberFormat ? formatExactParts(formatter, exact) : formatter.formatDecimal(exact))
|
|
10
120
|
.filter(part => part.type !== 'plusSign' && part.type !== 'minusSign')
|
|
121
|
+
.filter(part => part.type !== 'literal' || !/^[()]$/.test(part.value))
|
|
11
122
|
.map(part => part.value)
|
|
12
123
|
.join('');
|
|
13
124
|
return `${sign}${magnitude}`;
|
|
14
125
|
}
|
|
15
126
|
export function interpolateTickerValue(from, to, progress) {
|
|
16
|
-
if (!
|
|
127
|
+
if (!tickerDecimal(from).isFinite() || !tickerDecimal(to).isFinite())
|
|
17
128
|
return to;
|
|
18
|
-
const boundedProgress = Math.min(1, Math.max(0, progress));
|
|
19
|
-
|
|
129
|
+
const boundedProgress = Number.isFinite(progress) ? Math.min(1, Math.max(0, progress)) : 1;
|
|
130
|
+
if (boundedProgress === 0)
|
|
131
|
+
return from;
|
|
132
|
+
if (boundedProgress === 1)
|
|
133
|
+
return to;
|
|
134
|
+
if (typeof from === 'number' && typeof to === 'number') {
|
|
135
|
+
const distance = to - from;
|
|
136
|
+
return Number.isFinite(distance) ? from + distance * boundedProgress
|
|
137
|
+
: from * (1 - boundedProgress) + to * boundedProgress;
|
|
138
|
+
}
|
|
139
|
+
const start = tickerDecimal(from);
|
|
140
|
+
const next = start.plus(tickerDecimal(to).minus(start).times(String(boundedProgress)));
|
|
141
|
+
return typeof from === 'bigint' && typeof to === 'bigint' ? BigInt(next.trunc().toFixed(0)) : next.toString();
|
|
20
142
|
}
|
|
21
143
|
export function tickerValueChanged(previous, next) {
|
|
22
|
-
|
|
144
|
+
if (Object.is(previous, next))
|
|
145
|
+
return false;
|
|
146
|
+
const before = tickerDecimal(previous);
|
|
147
|
+
const after = tickerDecimal(next);
|
|
148
|
+
return !before.eq(after) || (before.isZero() && before.isNegative() !== after.isNegative());
|
|
23
149
|
}
|
|
24
150
|
export function canAnimateTickerValue(from, to, duration) {
|
|
25
|
-
|
|
26
|
-
|
|
151
|
+
const start = tickerDecimal(from);
|
|
152
|
+
const end = tickerDecimal(to);
|
|
153
|
+
return start.isFinite() && end.isFinite()
|
|
154
|
+
&& typeof from === typeof to
|
|
155
|
+
&& (typeof to === 'number' || (Math.max(start.sd(), end.sd(), Math.abs(start.e - end.e)) <= 512))
|
|
27
156
|
&& Number.isFinite(duration)
|
|
28
157
|
&& duration > 0
|
|
29
158
|
&& tickerValueChanged(from, to);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Decimal } from 'decimal.js';
|
|
2
|
+
export type NumberTickerValue = number | bigint | string;
|
|
3
|
+
export declare const ExactDecimal: typeof Decimal;
|
|
4
|
+
export declare function tickerDecimal(value: NumberTickerValue): Decimal;
|
|
5
|
+
export declare function tickerValueKey(value: unknown): string;
|
|
6
|
+
export declare function formatExactParts(formatter: Intl.NumberFormat, value: NumberTickerValue | Decimal): Intl.NumberFormatPart[];
|
|
7
|
+
export declare function subtractTickerValues(next: NumberTickerValue, previous: NumberTickerValue): NumberTickerValue | null;
|