@inkra/react 0.10.0 → 0.11.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 +27 -3
- package/dist/log/index.js +7 -6
- package/dist/number-ticker/MorphValue.d.ts +7 -0
- package/dist/number-ticker/MorphValue.js +10 -0
- package/dist/number-ticker/index.d.ts +1 -1
- package/dist/number-ticker/index.js +65 -11
- package/dist/number-ticker/morph-motion.d.ts +46 -0
- package/dist/number-ticker/morph-motion.js +121 -0
- package/dist/number-ticker/morph.d.ts +7 -0
- package/dist/number-ticker/morph.js +78 -0
- package/dist/styles/log.css +5 -18
- package/dist/styles/number-ticker.css +41 -0
- package/dist/styles.css +46 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -487,16 +487,37 @@ shrinking values and sign changes expand or collapse the affected columns.
|
|
|
487
487
|
Grouping, signs, currency and compact units remain formatted text, not digit
|
|
488
488
|
reels. `rollDirection="auto"` rolls up for increasing values and down for
|
|
489
489
|
decreasing values; `up` or `down` fixes the direction. `duration` and `easing`
|
|
490
|
-
apply to
|
|
490
|
+
apply to all modes. The default `animation="count"` retains value interpolation.
|
|
491
491
|
`renderValue` takes precedence over the built-in reels. Changing the formatting
|
|
492
492
|
or animation mode settles immediately, as do disabled/reduced motion, invalid
|
|
493
493
|
durations, and transitions with more than 128 formatted tokens. Exact strings
|
|
494
|
-
and bigint values retain their original precision in
|
|
494
|
+
and bigint values retain their original precision in every mode.
|
|
495
495
|
|
|
496
496
|
```tsx
|
|
497
497
|
<NumberTicker value={amount} animation="roll" rollDirection="auto" duration={650} />
|
|
498
498
|
```
|
|
499
499
|
|
|
500
|
+
Use `animation="morph"` to preserve digit identities during exact nonzero,
|
|
501
|
+
same-sign multiplication or division by a power of ten. For example, `1 → 100`
|
|
502
|
+
moves the existing `1` left and reveals `00` from the right; `12.34 → 123.4`
|
|
503
|
+
preserves all four digits while the decimal moves. The displayed significand and
|
|
504
|
+
decimal shift must agree with the exact values after formatting. Other changes,
|
|
505
|
+
including carries, sign changes, unit changes and exponent notation, use
|
|
506
|
+
place-aligned vertical rolling. This mode does not search for arbitrary matching
|
|
507
|
+
digits or reorder repeated glyphs.
|
|
508
|
+
|
|
509
|
+
Glyph widths stay fixed: motion uses position, clipping and a smooth opacity
|
|
510
|
+
fade rather than stretching text. Rapid updates continue from visible positions
|
|
511
|
+
and opacity. Keep the caller's container right-aligned to hold the numeric right
|
|
512
|
+
edge fixed as its width changes, and allow enough space for the formatted value;
|
|
513
|
+
the animation is clipped within its number area. The default duration is 420ms.
|
|
514
|
+
|
|
515
|
+
```tsx
|
|
516
|
+
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
|
517
|
+
<NumberTicker value={amount} animation="morph" duration={420} />
|
|
518
|
+
</div>
|
|
519
|
+
```
|
|
520
|
+
|
|
500
521
|
Set `compact` to follow `locale` (Chinese units for `zh`, English units otherwise),
|
|
501
522
|
or use `compact="en"` / `compact="zh"` to select a unit table explicitly.
|
|
502
523
|
English uses this library's short-scale suffix convention: K, M, B, T, Qa, Qi,
|
|
@@ -570,7 +591,10 @@ technology; decorative affixes are not announced twice.
|
|
|
570
591
|
|
|
571
592
|
`Log` renders caller-ordered records with stable ids, optional level filters and
|
|
572
593
|
pause/follow controls. It follows only append updates while the viewport remains
|
|
573
|
-
near the bottom.
|
|
594
|
+
near the bottom. When scrolled away or paused, a localized icon button floats
|
|
595
|
+
at the viewport's bottom end corner without adding a footer row. Clicking it
|
|
596
|
+
returns focus to the scroll region and resumes following or jumps to the latest
|
|
597
|
+
record. The named scroll region supports Home/End and intentionally is
|
|
574
598
|
not a live region, so a long stream is not repeatedly announced. Log does not
|
|
575
599
|
provide persistence, virtualization or transport.
|
|
576
600
|
|
package/dist/log/index.js
CHANGED
|
@@ -159,10 +159,11 @@ export const Log = forwardRef(function Log({ 'aria-label': ariaLabel, className,
|
|
|
159
159
|
return _jsxs("li", { className: "inkra-log__item", "data-has-icon": item.icon != null || undefined, "data-level": item.level, "data-tone": tone, children: [index > 0 ? _jsx(PencilLine, { className: "inkra-log__item-divider", seed: `inkra-log-item-${item.id}-divider` }) : null, item.icon == null ? null : _jsx("span", { "aria-hidden": "true", className: "inkra-log__icon", children: item.icon }), _jsxs("div", { className: "inkra-log__record", children: [item.time == null && item.level == null ? null : _jsxs("div", { className: "inkra-log__meta", children: [item.time == null ? null : item.dateTime
|
|
160
160
|
? _jsx("time", { dateTime: item.dateTime, children: renderText(item.time) })
|
|
161
161
|
: _jsx("span", { children: renderText(item.time) }), item.level == null ? null : _jsx("span", { className: "inkra-log__level", children: renderText(definition?.label ?? item.level) })] }), _jsx("div", { className: "inkra-log__content", children: renderText(item.content) })] })] }, item.id);
|
|
162
|
-
}) }) : _jsx("div", { className: "inkra-log__empty", children: renderText(isEmpty ? (empty ?? labels.empty) : (noMatches ?? labels.noMatches)) }) })
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
}) }) : _jsx("div", { className: "inkra-log__empty", children: renderText(isEmpty ? (empty ?? labels.empty) : (noMatches ?? labels.noMatches)) }) }), showLatest ? _jsx("div", { className: "inkra-log__latest", children: _jsx(Button, { "aria-label": resolvedPaused ? labels.resume : labels.latest, "data-tooltip": resolvedPaused ? labels.resume : labels.latest, title: resolvedPaused ? labels.resume : labels.latest, size: "icon-sm", variant: "secondary", onClick: () => {
|
|
163
|
+
viewportRef.current?.focus({ preventScroll: true });
|
|
164
|
+
if (resolvedPaused)
|
|
165
|
+
changePaused(false);
|
|
166
|
+
else
|
|
167
|
+
scrollToLatest();
|
|
168
|
+
}, children: _jsx(PencilIcon, { paths: followPaths, size: 20, sourceSize: 24 }) }) }) : null] })] }));
|
|
168
169
|
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MorphMotion } from './morph-motion.js';
|
|
2
|
+
import type { RollingToken } from './rolling.js';
|
|
3
|
+
export declare function MorphValue({ tokens, motion, progress }: {
|
|
4
|
+
tokens: RollingToken[];
|
|
5
|
+
motion: MorphMotion | null;
|
|
6
|
+
progress: number;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PencilText } from '../PencilText.js';
|
|
3
|
+
import { sampleMorphGlyph } from './morph-motion.js';
|
|
4
|
+
export function MorphValue({ tokens, motion, progress }) {
|
|
5
|
+
return _jsx("span", { className: "inkra-number-ticker__morph", dir: "ltr", "data-transition": motion?.kind ?? 'idle', style: motion ? { width: `${progress >= 1 ? motion.targetWidth : motion.width}ch` } : undefined, children: motion ? motion.glyphs.filter(glyph => progress < 1 || glyph.target).map(glyph => {
|
|
6
|
+
const state = sampleMorphGlyph(glyph, progress);
|
|
7
|
+
return _jsx("span", { className: "inkra-number-ticker__morph-glyph", "data-token-id": glyph.id, "data-place": glyph.referenceKey, "data-digit": glyph.digit || undefined, "data-face": glyph.target ? 'to' : 'from', style: { position: 'absolute', left: '100%', width: `${glyph.width}ch`,
|
|
8
|
+
transform: `translate(${state.x / glyph.width * 100}%, ${state.y * 100}%)`, opacity: state.opacity }, children: _jsx(PencilText, { seed: `ticker-${glyph.id}-${glyph.text}`, children: glyph.text }) }, glyph.id);
|
|
9
|
+
}) : tokens.map(token => _jsx("span", { className: "inkra-number-ticker__morph-glyph", "data-token-id": `settled-${token.key}`, "data-place": token.key, "data-digit": token.digit || undefined, "data-face": "to", style: token.digit ? { width: '1ch' } : undefined, children: _jsx(PencilText, { seed: `ticker-settled-${token.key}-${token.text}`, children: token.text }) }, `settled-${token.key}`)) });
|
|
10
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
|
2
2
|
import type { NumberTickerCompact, NumberTickerCompactOptions, NumberTickerFormatOptions, NumberTickerLocale, NumberTickerValue } from './model.js';
|
|
3
|
-
export type NumberTickerAnimation = 'count' | 'roll';
|
|
3
|
+
export type NumberTickerAnimation = 'count' | 'roll' | 'morph';
|
|
4
4
|
export type NumberTickerRollDirection = 'auto' | 'up' | 'down';
|
|
5
5
|
export interface NumberTickerRenderDetails {
|
|
6
6
|
animating: boolean;
|
|
@@ -7,6 +7,9 @@ import { canAnimateTickerValue, createNumberFormatter, createTickerFormatter, ea
|
|
|
7
7
|
import { ExactDecimal } from './precision.js';
|
|
8
8
|
import { RollingValue } from './RollingValue.js';
|
|
9
9
|
import { createRollingMotion, rollingTokens, sameRollingTokens } from './rolling.js';
|
|
10
|
+
import { MorphValue } from './MorphValue.js';
|
|
11
|
+
import { morphTokenMapping } from './morph.js';
|
|
12
|
+
import { createMorphMotion, measureMorphWidths } from './morph-motion.js';
|
|
10
13
|
function usePrefersReducedMotion() {
|
|
11
14
|
const [reduced, setReduced] = useState(false);
|
|
12
15
|
useBrowserLayoutEffect(() => {
|
|
@@ -41,6 +44,8 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
41
44
|
const [roll, setRoll] = useState(null);
|
|
42
45
|
const [rollProgress, setRollProgress] = useState(1);
|
|
43
46
|
const rollFrameRef = useRef(null);
|
|
47
|
+
const [morph, setMorph] = useState(null);
|
|
48
|
+
const morphFrameRef = useRef(null);
|
|
44
49
|
const reducedMotion = usePrefersReducedMotion();
|
|
45
50
|
const localeKey = Array.isArray(locale) ? locale.join('\u0000') : locale;
|
|
46
51
|
const formatOptionsKey = JSON.stringify(formatOptions ?? {});
|
|
@@ -51,12 +56,18 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
51
56
|
const announcementFormatter = useMemo(() => createNumberFormatter(locale, compact ? { ...formatOptions, notation: 'standard' } : formatOptions), [localeKey, formatOptionsKey, compact]);
|
|
52
57
|
useImperativeHandle(forwardedRef, () => rootRef.current);
|
|
53
58
|
const rollRequested = animation === 'roll' && !renderValue;
|
|
54
|
-
const
|
|
59
|
+
const morphRequested = animation === 'morph' && !renderValue;
|
|
60
|
+
const finalParts = useMemo(() => rollRequested || morphRequested ? formatter.formatToParts(value) : [], [formatter, value, rollRequested, morphRequested]);
|
|
61
|
+
const finalTokens = useMemo(() => rollingTokens(finalParts), [finalParts]);
|
|
55
62
|
// Keep exact large values readable without creating thousands of painted reels.
|
|
56
63
|
const useRoll = rollRequested && finalTokens.length <= 128;
|
|
64
|
+
const useMorph = morphRequested && finalTokens.length <= 128;
|
|
65
|
+
const useTokens = useRoll || useMorph;
|
|
57
66
|
const settledTokensRef = useRef(finalTokens);
|
|
67
|
+
const morphPartsRef = useRef(finalParts);
|
|
58
68
|
const previousFormatterRef = useRef(formatter);
|
|
59
69
|
const previousUseRollRef = useRef(useRoll);
|
|
70
|
+
const previousAnimationRef = useRef(animation);
|
|
60
71
|
const columns = useMemo(() => roll?.columns ?? createRollingMotion(finalTokens, finalTokens, 'up'), [roll, finalTokens]);
|
|
61
72
|
useBrowserLayoutEffect(() => {
|
|
62
73
|
const previousTarget = targetRef.current;
|
|
@@ -87,12 +98,13 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
87
98
|
const from = displayedRef.current;
|
|
88
99
|
const formatChanged = previousFormatterRef.current !== formatter;
|
|
89
100
|
previousFormatterRef.current = formatter;
|
|
90
|
-
const modeChanged = previousUseRollRef.current !== useRoll;
|
|
101
|
+
const modeChanged = previousUseRollRef.current !== useRoll || previousAnimationRef.current !== animation;
|
|
91
102
|
previousUseRollRef.current = useRoll;
|
|
103
|
+
previousAnimationRef.current = animation;
|
|
92
104
|
if (!animate || reduceImmediately || formatChanged || modeChanged
|
|
93
|
-
|| (
|
|
94
|
-
|| (rollRequested && (!
|
|
95
|
-
|| !canAnimateTickerValue(from, value, duration)) {
|
|
105
|
+
|| (useTokens && !rollFrameRef.current && (!morphFrameRef.current || morphFrameRef.current.progress >= 1) && sameRollingTokens(settledTokensRef.current, finalTokens))
|
|
106
|
+
|| ((rollRequested || morphRequested) && (!useTokens || settledTokensRef.current.length > 128))
|
|
107
|
+
|| (!canAnimateTickerValue(from, value, duration) && !(useMorph && canAnimateTickerValue(previousTarget, value, duration)))) {
|
|
96
108
|
displayedRef.current = value;
|
|
97
109
|
setDisplayedValue(value);
|
|
98
110
|
setAnnouncedValue(value);
|
|
@@ -101,9 +113,44 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
101
113
|
setRollProgress(1);
|
|
102
114
|
settledTokensRef.current = finalTokens;
|
|
103
115
|
rollFrameRef.current = null;
|
|
116
|
+
setMorph(null);
|
|
117
|
+
morphFrameRef.current = null;
|
|
118
|
+
morphPartsRef.current = finalParts;
|
|
104
119
|
return undefined;
|
|
105
120
|
}
|
|
106
|
-
if (
|
|
121
|
+
if (useMorph) {
|
|
122
|
+
const direction = rollDirection === 'auto'
|
|
123
|
+
? (new ExactDecimal(String(value)).gt(String(previousTarget)) ? 'up' : 'down') : rollDirection;
|
|
124
|
+
const previousTokens = morphFrameRef.current?.motion.tokens ?? settledTokensRef.current;
|
|
125
|
+
const zeroPart = formatter.formatToParts(0).find(part => part.type === 'integer')?.value ?? '0';
|
|
126
|
+
const zero = Array.from(zeroPart).slice(-1)[0] ?? '0';
|
|
127
|
+
const mapping = morphTokenMapping(morphPartsRef.current, finalParts, previousTarget, value, zero);
|
|
128
|
+
const motion = createMorphMotion(previousTokens, finalTokens, mapping, direction, measureMorphWidths(rootRef.current, [...previousTokens, ...finalTokens]), morphFrameRef.current ?? undefined);
|
|
129
|
+
// Bound interrupted layers as well as formatted target token counts.
|
|
130
|
+
if (motion.glyphs.length > 512) {
|
|
131
|
+
displayedRef.current = value;
|
|
132
|
+
setDisplayedValue(value);
|
|
133
|
+
setAnnouncedValue(value);
|
|
134
|
+
setAnimating(false);
|
|
135
|
+
setMorph(null);
|
|
136
|
+
morphFrameRef.current = null;
|
|
137
|
+
morphPartsRef.current = finalParts;
|
|
138
|
+
settledTokensRef.current = finalTokens;
|
|
139
|
+
setRoll(null);
|
|
140
|
+
rollFrameRef.current = null;
|
|
141
|
+
setRollProgress(1);
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
setMorph(motion);
|
|
145
|
+
morphFrameRef.current = { motion, progress: 0 };
|
|
146
|
+
morphPartsRef.current = finalParts;
|
|
147
|
+
setRoll(null);
|
|
148
|
+
rollFrameRef.current = null;
|
|
149
|
+
setRollProgress(0);
|
|
150
|
+
}
|
|
151
|
+
else if (useRoll) {
|
|
152
|
+
setMorph(null);
|
|
153
|
+
morphFrameRef.current = null;
|
|
107
154
|
const direction = rollDirection === 'auto'
|
|
108
155
|
? (new ExactDecimal(String(value)).gt(String(from)) ? 'up' : 'down') : rollDirection;
|
|
109
156
|
const nextColumns = createRollingMotion(settledTokensRef.current, finalTokens, direction, rollFrameRef.current ?? undefined);
|
|
@@ -114,6 +161,8 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
114
161
|
else {
|
|
115
162
|
setRoll(null);
|
|
116
163
|
rollFrameRef.current = null;
|
|
164
|
+
setMorph(null);
|
|
165
|
+
morphFrameRef.current = null;
|
|
117
166
|
}
|
|
118
167
|
let startTime = null;
|
|
119
168
|
setAnimating(true);
|
|
@@ -125,10 +174,12 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
125
174
|
const bounded = Number.isFinite(eased) ? Math.min(1, Math.max(0, eased)) : progress;
|
|
126
175
|
const next = interpolateTickerValue(from, value, bounded);
|
|
127
176
|
displayedRef.current = next;
|
|
128
|
-
if (
|
|
177
|
+
if (useTokens) {
|
|
129
178
|
setRollProgress(bounded);
|
|
130
179
|
if (rollFrameRef.current)
|
|
131
180
|
rollFrameRef.current.progress = bounded;
|
|
181
|
+
if (morphFrameRef.current)
|
|
182
|
+
morphFrameRef.current.progress = bounded;
|
|
132
183
|
}
|
|
133
184
|
else
|
|
134
185
|
setDisplayedValue(next);
|
|
@@ -145,6 +196,8 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
145
196
|
setRollProgress(1);
|
|
146
197
|
settledTokensRef.current = finalTokens;
|
|
147
198
|
rollFrameRef.current = null;
|
|
199
|
+
if (morphFrameRef.current)
|
|
200
|
+
morphFrameRef.current.progress = 1;
|
|
148
201
|
}
|
|
149
202
|
};
|
|
150
203
|
frameRef.current = requestAnimationFrame(tick);
|
|
@@ -154,7 +207,7 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
154
207
|
frameRef.current = null;
|
|
155
208
|
}
|
|
156
209
|
};
|
|
157
|
-
}, [animate, deltaDuration, duration, easing, reducedMotion, showDelta, value, useRoll, rollRequested, rollDirection, formatter]);
|
|
210
|
+
}, [animate, deltaDuration, duration, easing, reducedMotion, showDelta, value, useRoll, useMorph, useTokens, rollRequested, morphRequested, rollDirection, formatter, animation]);
|
|
158
211
|
useEffect(() => () => {
|
|
159
212
|
if (frameRef.current !== null)
|
|
160
213
|
cancelAnimationFrame(frameRef.current);
|
|
@@ -168,9 +221,10 @@ export const NumberTicker = forwardRef(function NumberTicker({ animate = true, a
|
|
|
168
221
|
.filter(Boolean).join(' ');
|
|
169
222
|
const accessibleLabel = getValueLabel?.(announcedValue, announcedFormattedValue) ?? defaultAccessibleLabel;
|
|
170
223
|
const valueContent = renderValue?.(displayedValue, { animating, formattedValue })
|
|
171
|
-
?? (
|
|
172
|
-
? _jsx(
|
|
173
|
-
|
|
224
|
+
?? (useMorph ? _jsx(MorphValue, { tokens: finalTokens, motion: morph, progress: morph ? rollProgress : 1 })
|
|
225
|
+
: useRoll ? _jsx(RollingValue, { columns: columns, progress: roll ? rollProgress : 1, direction: roll?.direction ?? 'up' }) : animating
|
|
226
|
+
? _jsx("span", { className: "inkra-number-ticker__native-value", children: formattedValue })
|
|
227
|
+
: _jsx(PencilText, { seed: `number-ticker-${finalFormattedValue}`, children: finalFormattedValue }));
|
|
174
228
|
const deltaContent = delta === null ? null : (() => {
|
|
175
229
|
const formattedDelta = formatDerivedTickerDelta(deltaFormatter, delta);
|
|
176
230
|
const details = {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { RollingToken } from './rolling.js';
|
|
2
|
+
export interface MorphGlyph {
|
|
3
|
+
id: string;
|
|
4
|
+
referenceKey: string;
|
|
5
|
+
text: string;
|
|
6
|
+
digit: boolean;
|
|
7
|
+
width: number;
|
|
8
|
+
target: boolean;
|
|
9
|
+
fromX: number;
|
|
10
|
+
toX: number;
|
|
11
|
+
fromY: number;
|
|
12
|
+
toY: number;
|
|
13
|
+
fromOpacity: number;
|
|
14
|
+
toOpacity: number;
|
|
15
|
+
}
|
|
16
|
+
export interface MorphMotion {
|
|
17
|
+
glyphs: MorphGlyph[];
|
|
18
|
+
tokens: RollingToken[];
|
|
19
|
+
width: number;
|
|
20
|
+
targetWidth: number;
|
|
21
|
+
kind: 'shift' | 'roll';
|
|
22
|
+
}
|
|
23
|
+
export interface MorphFrame {
|
|
24
|
+
motion: MorphMotion;
|
|
25
|
+
progress: number;
|
|
26
|
+
}
|
|
27
|
+
export type MorphWidths = (token: RollingToken) => number;
|
|
28
|
+
export declare function measureMorphWidths(root: HTMLElement | null, tokens: RollingToken[]): MorphWidths;
|
|
29
|
+
export declare function sampleMorphGlyph(glyph: MorphGlyph, progress: number): {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
opacity: number;
|
|
33
|
+
id: string;
|
|
34
|
+
referenceKey: string;
|
|
35
|
+
text: string;
|
|
36
|
+
digit: boolean;
|
|
37
|
+
width: number;
|
|
38
|
+
target: boolean;
|
|
39
|
+
fromX: number;
|
|
40
|
+
toX: number;
|
|
41
|
+
fromY: number;
|
|
42
|
+
toY: number;
|
|
43
|
+
fromOpacity: number;
|
|
44
|
+
toOpacity: number;
|
|
45
|
+
};
|
|
46
|
+
export declare function createMorphMotion(from: RollingToken[], to: RollingToken[], mapping: Array<[number, number]> | null, direction: 'up' | 'down', widths: MorphWidths, previous?: MorphFrame): MorphMotion;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { rollMix } from './rolling.js';
|
|
2
|
+
let nextGlyph = 0;
|
|
3
|
+
export function measureMorphWidths(root, tokens) {
|
|
4
|
+
if (!root || typeof document === 'undefined')
|
|
5
|
+
return token => token.digit ? 1 : Math.max(.25, Array.from(token.text).length);
|
|
6
|
+
const style = getComputedStyle(root);
|
|
7
|
+
const probe = document.createElement('span');
|
|
8
|
+
Object.assign(probe.style, { position: 'fixed', visibility: 'hidden', pointerEvents: 'none',
|
|
9
|
+
font: style.font, fontVariantNumeric: 'tabular-nums lining-nums', fontKerning: 'none',
|
|
10
|
+
fontVariantLigatures: 'none', letterSpacing: '0', whiteSpace: 'pre', width: '1ch', display: 'inline-block' });
|
|
11
|
+
document.body.append(probe);
|
|
12
|
+
const ch = probe.getBoundingClientRect().width || 1;
|
|
13
|
+
probe.style.width = 'auto';
|
|
14
|
+
const widths = new Map();
|
|
15
|
+
// Measurement happens once when a target changes, never in the frame loop.
|
|
16
|
+
const measure = (token) => {
|
|
17
|
+
if (token.digit)
|
|
18
|
+
return 1;
|
|
19
|
+
if (!widths.has(token.text)) {
|
|
20
|
+
probe.textContent = token.text;
|
|
21
|
+
widths.set(token.text, Math.max(.05, probe.getBoundingClientRect().width / ch));
|
|
22
|
+
}
|
|
23
|
+
return widths.get(token.text);
|
|
24
|
+
};
|
|
25
|
+
// The returned callback cannot retain a live measuring node.
|
|
26
|
+
tokens.forEach(measure);
|
|
27
|
+
const result = token => token.digit ? 1 : widths.get(token.text) ?? Math.max(.25, Array.from(token.text).length);
|
|
28
|
+
probe.remove();
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function layout(tokens, widths) {
|
|
32
|
+
const total = tokens.reduce((sum, token) => sum + widths(token), 0);
|
|
33
|
+
let x = -total;
|
|
34
|
+
return { width: total, items: tokens.map(token => {
|
|
35
|
+
const width = widths(token);
|
|
36
|
+
const item = { ...token, x, width };
|
|
37
|
+
x += width;
|
|
38
|
+
return item;
|
|
39
|
+
}) };
|
|
40
|
+
}
|
|
41
|
+
export function sampleMorphGlyph(glyph, progress) {
|
|
42
|
+
const fade = progress * progress * (3 - 2 * progress);
|
|
43
|
+
return { ...glyph, x: rollMix(glyph.fromX, glyph.toX, progress), y: rollMix(glyph.fromY, glyph.toY, progress),
|
|
44
|
+
opacity: rollMix(glyph.fromOpacity, glyph.toOpacity, fade) };
|
|
45
|
+
}
|
|
46
|
+
export function createMorphMotion(from, to, mapping, direction, widths, previous) {
|
|
47
|
+
const before = layout(from, widths), after = layout(to, widths);
|
|
48
|
+
const source = previous ? previous.motion.glyphs.map(glyph => sampleMorphGlyph(glyph, previous.progress))
|
|
49
|
+
.filter(glyph => glyph.opacity > .001 && Math.abs(glyph.y) < 1.001)
|
|
50
|
+
: before.items.map(token => ({ id: `settled-${token.key}`, referenceKey: token.key, text: token.text,
|
|
51
|
+
digit: token.digit, width: token.width, target: true, x: token.x, y: 0, opacity: 1 }));
|
|
52
|
+
const used = new Set();
|
|
53
|
+
const glyphs = [];
|
|
54
|
+
const sign = direction === 'up' ? -1 : 1;
|
|
55
|
+
const pairs = mapping ?? from.flatMap((token, index) => {
|
|
56
|
+
const next = to.findIndex(item => item.key === token.key);
|
|
57
|
+
return next < 0 ? [] : [[index, next]];
|
|
58
|
+
});
|
|
59
|
+
const oldIndexes = new Map(pairs.map(([left, right]) => [right, left]));
|
|
60
|
+
const findSource = (index) => source.filter(item => item.referenceKey === from[index]?.key)
|
|
61
|
+
.sort((left, right) => right.opacity - left.opacity);
|
|
62
|
+
const decimalEnd = (items) => {
|
|
63
|
+
const integers = items.filter(item => item.key.startsWith('integer-'));
|
|
64
|
+
const integer = integers[integers.length - 1];
|
|
65
|
+
return integer ? integer.x + integer.width : 0;
|
|
66
|
+
};
|
|
67
|
+
for (let index = 0; index < to.length; index++) {
|
|
68
|
+
const token = to[index], target = after.items[index];
|
|
69
|
+
const oldIndex = oldIndexes.get(index);
|
|
70
|
+
const candidates = oldIndex === undefined ? [] : findSource(oldIndex);
|
|
71
|
+
const existing = candidates.find(item => item.text === token.text && !used.has(item.id));
|
|
72
|
+
if (existing) {
|
|
73
|
+
used.add(existing.id);
|
|
74
|
+
glyphs.push({ id: existing.id, referenceKey: token.key, text: token.text, digit: token.digit,
|
|
75
|
+
width: existing.width, target: true, fromX: existing.x, toX: target.x,
|
|
76
|
+
fromY: existing.y, toY: 0, fromOpacity: existing.opacity, toOpacity: 1 });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
let x = candidates[0]?.x ?? before.items[oldIndex ?? -1]?.x ?? target.x;
|
|
80
|
+
let y = token.digit && !mapping ? -sign : 0;
|
|
81
|
+
if (mapping) {
|
|
82
|
+
if (token.key.startsWith('decimal-'))
|
|
83
|
+
x = decimalEnd(before.items);
|
|
84
|
+
else if (token.digit) {
|
|
85
|
+
const anchor = pairs.filter(([left, right]) => right < index && from[left].digit)
|
|
86
|
+
.sort((left, right) => right[1] - left[1])[0];
|
|
87
|
+
if (anchor) {
|
|
88
|
+
const visible = findSource(anchor[0])[0];
|
|
89
|
+
x = (visible?.x ?? before.items[anchor[0]].x) + target.x - after.items[anchor[1]].x;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
glyphs.push({ id: `morph-${nextGlyph++}`, referenceKey: token.key, text: token.text,
|
|
94
|
+
digit: token.digit, width: target.width, target: true, fromX: x, toX: target.x,
|
|
95
|
+
fromY: y, toY: 0, fromOpacity: mapping || !token.digit ? 0 : 1, toOpacity: 1 });
|
|
96
|
+
}
|
|
97
|
+
for (const item of source) {
|
|
98
|
+
if (used.has(item.id))
|
|
99
|
+
continue;
|
|
100
|
+
let x = item.x;
|
|
101
|
+
let y = item.digit && !mapping ? sign : 0;
|
|
102
|
+
if (mapping) {
|
|
103
|
+
if (item.referenceKey.startsWith('decimal-'))
|
|
104
|
+
x = decimalEnd(after.items);
|
|
105
|
+
else {
|
|
106
|
+
const oldIndex = from.findIndex(token => token.key === item.referenceKey);
|
|
107
|
+
const anchor = pairs.filter(([left]) => left < oldIndex && from[left].digit)
|
|
108
|
+
.sort((left, right) => right[0] - left[0])[0];
|
|
109
|
+
if (anchor) {
|
|
110
|
+
const visible = findSource(anchor[0])[0];
|
|
111
|
+
x += after.items[anchor[1]].x - (visible?.x ?? before.items[anchor[0]].x);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
glyphs.push({ id: item.id, referenceKey: item.referenceKey, text: item.text, digit: item.digit,
|
|
116
|
+
width: item.width, target: false, fromX: item.x, toX: x, fromY: item.y, toY: y,
|
|
117
|
+
fromOpacity: item.opacity, toOpacity: mapping || !item.digit ? 0 : 1 });
|
|
118
|
+
}
|
|
119
|
+
return { glyphs, tokens: to, width: Math.max(before.width, after.width, previous && previous.progress < 1 ? previous.motion.width : 0),
|
|
120
|
+
targetWidth: after.width, kind: mapping ? 'shift' : 'roll' };
|
|
121
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NumberTickerValue } from './precision.js';
|
|
2
|
+
/**
|
|
3
|
+
* Match an exact decimal shift, rather than guessing identity from equal glyphs.
|
|
4
|
+
* Pair indexes address rollingTokens(parts), including unchanged typed symbols.
|
|
5
|
+
* A null result leaves the transition to the existing place-aligned reels.
|
|
6
|
+
*/
|
|
7
|
+
export declare function morphTokenMapping(fromParts: Intl.NumberFormatPart[], toParts: Intl.NumberFormatPart[], fromValue: NumberTickerValue, toValue: NumberTickerValue, zeroGlyph?: string): Array<[number, number]> | null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ExactDecimal, tickerDecimal } from './precision.js';
|
|
2
|
+
import { rollingTokens } from './rolling.js';
|
|
3
|
+
const numericParts = new Set(['integer', 'fraction', 'decimal', 'group']);
|
|
4
|
+
const exponentParts = new Set(['exponentSeparator', 'exponentMinusSign', 'exponentInteger']);
|
|
5
|
+
function displayDigits(parts, zeroGlyph) {
|
|
6
|
+
const tokens = rollingTokens(parts);
|
|
7
|
+
const indexes = tokens.flatMap((token, index) => token.digit ? [index] : []);
|
|
8
|
+
const text = indexes.map(index => tokens[index].text);
|
|
9
|
+
const first = text.findIndex(digit => digit !== zeroGlyph);
|
|
10
|
+
if (first < 0)
|
|
11
|
+
return null;
|
|
12
|
+
let last = text.length - 1;
|
|
13
|
+
while (text[last] === zeroGlyph)
|
|
14
|
+
last--;
|
|
15
|
+
const integerLength = parts.filter(part => part.type === 'integer')
|
|
16
|
+
.reduce((length, part) => length + Array.from(part.value).length, 0);
|
|
17
|
+
return { indexes, text, first, last, exponent: integerLength - first - 1 };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Match an exact decimal shift, rather than guessing identity from equal glyphs.
|
|
21
|
+
* Pair indexes address rollingTokens(parts), including unchanged typed symbols.
|
|
22
|
+
* A null result leaves the transition to the existing place-aligned reels.
|
|
23
|
+
*/
|
|
24
|
+
export function morphTokenMapping(fromParts, toParts, fromValue, toValue, zeroGlyph = '0') {
|
|
25
|
+
const from = tickerDecimal(fromValue);
|
|
26
|
+
const to = tickerDecimal(toValue);
|
|
27
|
+
if (!from.isFinite() || !to.isFinite() || from.isZero() || to.isZero()
|
|
28
|
+
|| from.isNegative() !== to.isNegative())
|
|
29
|
+
return null;
|
|
30
|
+
const shift = to.e - from.e;
|
|
31
|
+
if (shift === 0 || !from.times(new ExactDecimal(10).pow(shift)).eq(to))
|
|
32
|
+
return null;
|
|
33
|
+
// Unit/sign/literal changes are separate semantic transitions. Exponents have
|
|
34
|
+
// their own digit domain and must never be mistaken for mantissa digits.
|
|
35
|
+
if ([...fromParts, ...toParts].some(part => exponentParts.has(part.type)))
|
|
36
|
+
return null;
|
|
37
|
+
const symbols = (parts) => parts.filter(part => !numericParts.has(part.type));
|
|
38
|
+
const fromSymbols = symbols(fromParts);
|
|
39
|
+
const toSymbols = symbols(toParts);
|
|
40
|
+
if (fromSymbols.length !== toSymbols.length || fromSymbols.some((part, index) => {
|
|
41
|
+
const next = toSymbols[index];
|
|
42
|
+
return part.type !== next.type || part.value !== next.value;
|
|
43
|
+
}))
|
|
44
|
+
return null;
|
|
45
|
+
const before = displayDigits(fromParts, zeroGlyph);
|
|
46
|
+
const after = displayDigits(toParts, zeroGlyph);
|
|
47
|
+
if (!before || !after || after.exponent - before.exponent !== shift)
|
|
48
|
+
return null;
|
|
49
|
+
const core = before.text.slice(before.first, before.last + 1);
|
|
50
|
+
const nextCore = after.text.slice(after.first, after.last + 1);
|
|
51
|
+
// Rounding may change the visible significand even for an exact raw shift.
|
|
52
|
+
if (core.length !== nextCore.length || core.some((digit, index) => digit !== nextCore[index]))
|
|
53
|
+
return null;
|
|
54
|
+
const mapping = [];
|
|
55
|
+
for (let index = 0; index < core.length; index++) {
|
|
56
|
+
mapping.push([before.indexes[before.first + index], after.indexes[after.first + index]]);
|
|
57
|
+
}
|
|
58
|
+
// Edge zeros retain their ordinal distance from the significant core. This
|
|
59
|
+
// preserves 10 as the first 10 in 1000 without matching arbitrary repeats.
|
|
60
|
+
for (let offset = 1; offset <= Math.min(before.first, after.first); offset++) {
|
|
61
|
+
mapping.push([before.indexes[before.first - offset], after.indexes[after.first - offset]]);
|
|
62
|
+
}
|
|
63
|
+
const trailing = Math.min(before.text.length - before.last - 1, after.text.length - after.last - 1);
|
|
64
|
+
for (let offset = 1; offset <= trailing; offset++) {
|
|
65
|
+
mapping.push([before.indexes[before.last + offset], after.indexes[after.last + offset]]);
|
|
66
|
+
}
|
|
67
|
+
const fromTokens = rollingTokens(fromParts);
|
|
68
|
+
const toTokens = rollingTokens(toParts);
|
|
69
|
+
const targetSymbols = new Map(toTokens.flatMap((token, index) => token.digit ? [] : [[token.key, { token, index }]]));
|
|
70
|
+
fromTokens.forEach((token, index) => {
|
|
71
|
+
if (token.digit)
|
|
72
|
+
return;
|
|
73
|
+
const next = targetSymbols.get(token.key);
|
|
74
|
+
if (next && next.token.text === token.text)
|
|
75
|
+
mapping.push([index, next.index]);
|
|
76
|
+
});
|
|
77
|
+
return mapping.sort((left, right) => left[0] - right[0]);
|
|
78
|
+
}
|
package/dist/styles/log.css
CHANGED
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.inkra-log[data-toolbar="true"] { grid-template-rows: auto minmax(var(--inkra-touch-target), 1fr); }
|
|
16
|
-
.inkra-log:has(> .inkra-log__latest) { grid-template-rows: minmax(var(--inkra-touch-target), 1fr) auto; }
|
|
17
|
-
.inkra-log[data-toolbar="true"]:has(> .inkra-log__latest) { grid-template-rows: auto minmax(var(--inkra-touch-target), 1fr) auto; }
|
|
18
16
|
|
|
19
17
|
.inkra-log__toolbar {
|
|
20
18
|
align-items: center;
|
|
@@ -158,21 +156,15 @@
|
|
|
158
156
|
}
|
|
159
157
|
|
|
160
158
|
.inkra-log__latest {
|
|
161
|
-
align-items: center;
|
|
162
159
|
display: flex;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
padding: var(--inkra-space-2) var(--inkra-space-3);
|
|
167
|
-
position: relative;
|
|
160
|
+
inset-block-end: clamp(0px, calc(100% - var(--inkra-touch-target)), var(--inkra-space-2));
|
|
161
|
+
inset-inline-end: var(--inkra-space-3);
|
|
162
|
+
position: absolute;
|
|
168
163
|
z-index: 2;
|
|
169
164
|
}
|
|
170
165
|
|
|
171
|
-
.inkra-log__latest-
|
|
172
|
-
|
|
173
|
-
inset-block-start: -4px;
|
|
174
|
-
inset-inline: 0;
|
|
175
|
-
position: absolute;
|
|
166
|
+
.inkra-log__latest .inkra-button__surface {
|
|
167
|
+
background: var(--inkra-paper);
|
|
176
168
|
}
|
|
177
169
|
|
|
178
170
|
@container (max-width: 36rem) {
|
|
@@ -188,11 +180,6 @@
|
|
|
188
180
|
.inkra-log__follow-control .inkra-button__content,
|
|
189
181
|
.inkra-log__follow-control .inkra-button__label,
|
|
190
182
|
.inkra-log__follow-control .inkra-pencil-text__source { min-inline-size: 0; white-space: normal; overflow-wrap: anywhere; }
|
|
191
|
-
.inkra-log__latest .inkra-button { inline-size: 100%; }
|
|
192
|
-
.inkra-log__latest .inkra-button__surface { height: auto; inline-size: 100%; max-inline-size: 100%; min-height: var(--inkra-button-visual-height); padding-block: var(--inkra-space-2); }
|
|
193
|
-
.inkra-log__latest .inkra-button__content,
|
|
194
|
-
.inkra-log__latest .inkra-button__label,
|
|
195
|
-
.inkra-log__latest .inkra-pencil-text__source { min-inline-size: 0; white-space: normal; overflow-wrap: anywhere; }
|
|
196
183
|
}
|
|
197
184
|
|
|
198
185
|
@media (forced-colors: active) {
|
|
@@ -66,7 +66,48 @@
|
|
|
66
66
|
flex: none;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
.inkra-number-ticker[data-animation="morph"] { min-inline-size: 0; }
|
|
70
|
+
|
|
71
|
+
.inkra-number-ticker[data-animation="morph"] .inkra-number-ticker__value {
|
|
72
|
+
display: block;
|
|
73
|
+
max-inline-size: 100%;
|
|
74
|
+
overflow: hidden;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.inkra-number-ticker__morph {
|
|
78
|
+
position: relative;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
flex: none;
|
|
81
|
+
align-items: baseline;
|
|
82
|
+
justify-content: flex-end;
|
|
83
|
+
max-inline-size: 100%;
|
|
84
|
+
height: 1lh;
|
|
85
|
+
line-height: var(--inkra-line-ui, 1.45);
|
|
86
|
+
overflow: hidden;
|
|
87
|
+
contain: paint;
|
|
88
|
+
unicode-bidi: isolate;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.inkra-number-ticker__morph-glyph {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
flex: none;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
align-items: center;
|
|
96
|
+
height: 1lh;
|
|
97
|
+
white-space: pre;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.inkra-number-ticker__morph-glyph > .inkra-pencil-text { flex: none; }
|
|
101
|
+
|
|
69
102
|
@media (prefers-reduced-motion: reduce), print {
|
|
103
|
+
.inkra-number-ticker__morph { width: auto !important; }
|
|
104
|
+
.inkra-number-ticker__morph-glyph[data-face="from"] { display: none; }
|
|
105
|
+
.inkra-number-ticker__morph-glyph[data-face="to"] {
|
|
106
|
+
position: relative !important;
|
|
107
|
+
left: auto !important;
|
|
108
|
+
transform: none !important;
|
|
109
|
+
opacity: 1 !important;
|
|
110
|
+
}
|
|
70
111
|
.inkra-number-ticker__column { grid-template-columns: 1fr !important; }
|
|
71
112
|
.inkra-number-ticker__column[data-digit] { width: 1ch !important; }
|
|
72
113
|
.inkra-number-ticker__column[data-departing] { display: none; }
|
package/dist/styles.css
CHANGED
|
@@ -4445,8 +4445,6 @@
|
|
|
4445
4445
|
}
|
|
4446
4446
|
|
|
4447
4447
|
.inkra-log[data-toolbar="true"] { grid-template-rows: auto minmax(var(--inkra-touch-target), 1fr); }
|
|
4448
|
-
.inkra-log:has(> .inkra-log__latest) { grid-template-rows: minmax(var(--inkra-touch-target), 1fr) auto; }
|
|
4449
|
-
.inkra-log[data-toolbar="true"]:has(> .inkra-log__latest) { grid-template-rows: auto minmax(var(--inkra-touch-target), 1fr) auto; }
|
|
4450
4448
|
|
|
4451
4449
|
.inkra-log__toolbar {
|
|
4452
4450
|
align-items: center;
|
|
@@ -4590,21 +4588,15 @@
|
|
|
4590
4588
|
}
|
|
4591
4589
|
|
|
4592
4590
|
.inkra-log__latest {
|
|
4593
|
-
align-items: center;
|
|
4594
4591
|
display: flex;
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
padding: var(--inkra-space-2) var(--inkra-space-3);
|
|
4599
|
-
position: relative;
|
|
4592
|
+
inset-block-end: clamp(0px, calc(100% - var(--inkra-touch-target)), var(--inkra-space-2));
|
|
4593
|
+
inset-inline-end: var(--inkra-space-3);
|
|
4594
|
+
position: absolute;
|
|
4600
4595
|
z-index: 2;
|
|
4601
4596
|
}
|
|
4602
4597
|
|
|
4603
|
-
.inkra-log__latest-
|
|
4604
|
-
|
|
4605
|
-
inset-block-start: -4px;
|
|
4606
|
-
inset-inline: 0;
|
|
4607
|
-
position: absolute;
|
|
4598
|
+
.inkra-log__latest .inkra-button__surface {
|
|
4599
|
+
background: var(--inkra-paper);
|
|
4608
4600
|
}
|
|
4609
4601
|
|
|
4610
4602
|
@container (max-width: 36rem) {
|
|
@@ -4620,11 +4612,6 @@
|
|
|
4620
4612
|
.inkra-log__follow-control .inkra-button__content,
|
|
4621
4613
|
.inkra-log__follow-control .inkra-button__label,
|
|
4622
4614
|
.inkra-log__follow-control .inkra-pencil-text__source { min-inline-size: 0; white-space: normal; overflow-wrap: anywhere; }
|
|
4623
|
-
.inkra-log__latest .inkra-button { inline-size: 100%; }
|
|
4624
|
-
.inkra-log__latest .inkra-button__surface { height: auto; inline-size: 100%; max-inline-size: 100%; min-height: var(--inkra-button-visual-height); padding-block: var(--inkra-space-2); }
|
|
4625
|
-
.inkra-log__latest .inkra-button__content,
|
|
4626
|
-
.inkra-log__latest .inkra-button__label,
|
|
4627
|
-
.inkra-log__latest .inkra-pencil-text__source { min-inline-size: 0; white-space: normal; overflow-wrap: anywhere; }
|
|
4628
4615
|
}
|
|
4629
4616
|
|
|
4630
4617
|
@media (forced-colors: active) {
|
|
@@ -4892,7 +4879,48 @@
|
|
|
4892
4879
|
flex: none;
|
|
4893
4880
|
}
|
|
4894
4881
|
|
|
4882
|
+
.inkra-number-ticker[data-animation="morph"] { min-inline-size: 0; }
|
|
4883
|
+
|
|
4884
|
+
.inkra-number-ticker[data-animation="morph"] .inkra-number-ticker__value {
|
|
4885
|
+
display: block;
|
|
4886
|
+
max-inline-size: 100%;
|
|
4887
|
+
overflow: hidden;
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
.inkra-number-ticker__morph {
|
|
4891
|
+
position: relative;
|
|
4892
|
+
display: inline-flex;
|
|
4893
|
+
flex: none;
|
|
4894
|
+
align-items: baseline;
|
|
4895
|
+
justify-content: flex-end;
|
|
4896
|
+
max-inline-size: 100%;
|
|
4897
|
+
height: 1lh;
|
|
4898
|
+
line-height: var(--inkra-line-ui, 1.45);
|
|
4899
|
+
overflow: hidden;
|
|
4900
|
+
contain: paint;
|
|
4901
|
+
unicode-bidi: isolate;
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
.inkra-number-ticker__morph-glyph {
|
|
4905
|
+
display: inline-flex;
|
|
4906
|
+
flex: none;
|
|
4907
|
+
justify-content: center;
|
|
4908
|
+
align-items: center;
|
|
4909
|
+
height: 1lh;
|
|
4910
|
+
white-space: pre;
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
.inkra-number-ticker__morph-glyph > .inkra-pencil-text { flex: none; }
|
|
4914
|
+
|
|
4895
4915
|
@media (prefers-reduced-motion: reduce), print {
|
|
4916
|
+
.inkra-number-ticker__morph { width: auto !important; }
|
|
4917
|
+
.inkra-number-ticker__morph-glyph[data-face="from"] { display: none; }
|
|
4918
|
+
.inkra-number-ticker__morph-glyph[data-face="to"] {
|
|
4919
|
+
position: relative !important;
|
|
4920
|
+
left: auto !important;
|
|
4921
|
+
transform: none !important;
|
|
4922
|
+
opacity: 1 !important;
|
|
4923
|
+
}
|
|
4896
4924
|
.inkra-number-ticker__column { grid-template-columns: 1fr !important; }
|
|
4897
4925
|
.inkra-number-ticker__column[data-digit] { width: 1ch !important; }
|
|
4898
4926
|
.inkra-number-ticker__column[data-departing] { display: none; }
|