panelui-native 0.38.0 → 0.41.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.
Files changed (67) hide show
  1. package/README.md +3 -1
  2. package/lib/module/components/card/index.js +172 -3
  3. package/lib/module/components/card/index.js.map +1 -1
  4. package/lib/module/components/drawer/index.js +13 -4
  5. package/lib/module/components/drawer/index.js.map +1 -1
  6. package/lib/module/components/heatmap-chart/index.js +113 -6
  7. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  8. package/lib/module/components/map/index.js +72 -12
  9. package/lib/module/components/map/index.js.map +1 -1
  10. package/lib/module/components/pagination/index.js +529 -0
  11. package/lib/module/components/pagination/index.js.map +1 -0
  12. package/lib/module/components/panelside/index.js +68 -18
  13. package/lib/module/components/panelside/index.js.map +1 -1
  14. package/lib/module/components/ring-chart/index.js +183 -45
  15. package/lib/module/components/ring-chart/index.js.map +1 -1
  16. package/lib/module/components/steps/index.js +8 -6
  17. package/lib/module/components/steps/index.js.map +1 -1
  18. package/lib/module/components/table/index.js +100 -26
  19. package/lib/module/components/table/index.js.map +1 -1
  20. package/lib/module/components/text-animation/index.js +857 -0
  21. package/lib/module/components/text-animation/index.js.map +1 -0
  22. package/lib/module/components/timeline/index.js +8 -7
  23. package/lib/module/components/timeline/index.js.map +1 -1
  24. package/lib/module/icons/index.js +25 -7
  25. package/lib/module/icons/index.js.map +1 -1
  26. package/lib/module/index.js +2 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/typescript/src/components/card/index.d.ts +20 -0
  29. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  30. package/lib/typescript/src/components/drawer/index.d.ts +13 -1
  31. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  32. package/lib/typescript/src/components/heatmap-chart/index.d.ts +51 -3
  33. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  34. package/lib/typescript/src/components/map/index.d.ts +37 -2
  35. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/pagination/index.d.ts +285 -0
  37. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -0
  38. package/lib/typescript/src/components/panelside/index.d.ts +50 -4
  39. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  40. package/lib/typescript/src/components/ring-chart/index.d.ts +75 -8
  41. package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -1
  42. package/lib/typescript/src/components/steps/index.d.ts +8 -6
  43. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/table/index.d.ts +59 -17
  45. package/lib/typescript/src/components/table/index.d.ts.map +1 -1
  46. package/lib/typescript/src/components/text-animation/index.d.ts +268 -0
  47. package/lib/typescript/src/components/text-animation/index.d.ts.map +1 -0
  48. package/lib/typescript/src/components/timeline/index.d.ts +8 -7
  49. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  50. package/lib/typescript/src/icons/index.d.ts +6 -0
  51. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  52. package/lib/typescript/src/index.d.ts +6 -4
  53. package/lib/typescript/src/index.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/src/components/card/index.tsx +227 -2
  56. package/src/components/drawer/index.tsx +31 -9
  57. package/src/components/heatmap-chart/index.tsx +147 -5
  58. package/src/components/map/index.tsx +158 -14
  59. package/src/components/pagination/index.tsx +603 -0
  60. package/src/components/panelside/index.tsx +134 -19
  61. package/src/components/ring-chart/index.tsx +269 -47
  62. package/src/components/steps/index.tsx +8 -6
  63. package/src/components/table/index.tsx +146 -30
  64. package/src/components/text-animation/index.tsx +1093 -0
  65. package/src/components/timeline/index.tsx +8 -7
  66. package/src/icons/index.tsx +23 -4
  67. package/src/index.ts +28 -1
@@ -0,0 +1,857 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * TextAnimation — the five ways a piece of text arrives.
5
+ *
6
+ * ```tsx
7
+ * <TextAnimation.Typing text="Everything ships with its accessibility done" />
8
+ * <TextAnimation.Rotating text={['fast', 'native', 'yours']} />
9
+ * <TextAnimation.Counting value={2048} />
10
+ * <TextAnimation.Sliding value={price} decimals={2} />
11
+ * <TextAnimation.Scrolling value={120} step={10} />
12
+ * ```
13
+ *
14
+ * ## Why one component with five parts
15
+ *
16
+ * They are one idea — a value that changes and wants to be seen changing —
17
+ * and they share every prop that says *how*: `duration`, `delay`, `loop`,
18
+ * `enabled`. Put on the `TextAnimation` root, those become the defaults for
19
+ * everything inside it, so a hero with three of these in it is configured once
20
+ * rather than three times.
21
+ *
22
+ * ## What React Native forces
23
+ *
24
+ * Nested `Text` is the only thing that gets real line-breaking, and it cannot
25
+ * be transformed — a `translateY` on it is ignored. So the two parts that
26
+ * animate a whole string, `Typing` and `Counting`, stay real text and keep
27
+ * wrapping; the three that slide glyphs past each other lay out as rows of
28
+ * views, which buys the transform and costs line-breaking. None of the three
29
+ * is ever a paragraph, so the trade only ever falls the right way.
30
+ *
31
+ * ## Why the digits are drawn rather than measured
32
+ *
33
+ * A number animated as React state is a re-render per frame. `Counting` runs
34
+ * the value on the UI thread and only crosses back when the *rounded* number
35
+ * changes, which for a whole number is a couple of dozen times rather than
36
+ * sixty a second. The sliding and scrolling parts do not cross back at all:
37
+ * every digit is already rendered, and the animation is a transform on a
38
+ * column of them.
39
+ */
40
+ import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
41
+ import { Text as RNText, View } from 'react-native';
42
+ import Animated, { Easing, cancelAnimation, runOnJS, useAnimatedReaction, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withSequence, withSpring, withTiming } from 'react-native-reanimated';
43
+ import { LinearGradient } from 'expo-linear-gradient';
44
+ import { tv } from 'tailwind-variants';
45
+ import { useCSSVariable } from 'uniwind';
46
+ import { cn } from "../../utils/cn.js";
47
+ import { Text } from "../../primitives/text.js";
48
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
49
+ const AnimatedText = Animated.createAnimatedComponent(RNText);
50
+
51
+ /** How long a caret spends lit, and then dark. */
52
+ const CARET_PERIOD = 520;
53
+ /** The spring every number rides. Slow enough to read the digits going past. */
54
+ const NUMBER_SPRING = {
55
+ damping: 26,
56
+ stiffness: 90,
57
+ mass: 1
58
+ };
59
+ /** How far a rotating phrase travels, in points. */
60
+ const ROTATE_DISTANCE = 22;
61
+ const textAnimationVariants = tv({
62
+ slots: {
63
+ /*
64
+ * `tabular-nums` on everything numeric, without exception. Proportional
65
+ * digits are different widths, so a rolling number changes width as it
66
+ * rolls and shoves whatever is beside it back and forth — which reads as
67
+ * the layout being broken rather than as the number being alive.
68
+ */
69
+ digits: 'flex-row items-center tabular-nums',
70
+ column: 'items-center',
71
+ caret: 'text-foreground',
72
+ highlight: 'rounded-lg bg-secondary'
73
+ }
74
+ });
75
+
76
+ /* -------------------------------------------------------------------------- */
77
+ /* Root */
78
+ /* -------------------------------------------------------------------------- */
79
+
80
+ const TextAnimationContext = /*#__PURE__*/createContext({});
81
+ /**
82
+ * Shared configuration, and a row to lay the parts out in.
83
+ *
84
+ * Optional in every case — each part works standalone with its own props, and
85
+ * this is only here so a line with three of them in it says `duration` once.
86
+ */
87
+ function TextAnimationRoot({
88
+ className,
89
+ duration,
90
+ delay,
91
+ loop,
92
+ enabled,
93
+ children,
94
+ ...props
95
+ }) {
96
+ const value = useMemo(() => ({
97
+ duration,
98
+ delay,
99
+ loop,
100
+ enabled
101
+ }), [delay, duration, enabled, loop]);
102
+ return /*#__PURE__*/_jsx(TextAnimationContext.Provider, {
103
+ value: value,
104
+ children: /*#__PURE__*/_jsx(View, {
105
+ className: cn('flex-row items-center', className),
106
+ ...props,
107
+ children: children
108
+ })
109
+ });
110
+ }
111
+ TextAnimationRoot.displayName = 'TextAnimation';
112
+
113
+ /**
114
+ * The part's own prop, then the root's, then the part's own default.
115
+ *
116
+ * Written out because `??` alone would take the root's value over an explicit
117
+ * `false` on the part, and "the root says loop, this one does not" is the
118
+ * whole reason the root's values are defaults rather than settings.
119
+ */
120
+ function useShared(key, own, fallback) {
121
+ const inherited = useContext(TextAnimationContext);
122
+ return own ?? inherited[key] ?? fallback;
123
+ }
124
+
125
+ /**
126
+ * The same colour at a given alpha.
127
+ *
128
+ * A gradient stop of `transparent` is black at zero alpha on Android, so a
129
+ * fade to it arrives through a grey smear on the way — it has to be the same
130
+ * colour, faded.
131
+ */
132
+ function withAlpha(color, alpha) {
133
+ if (color.startsWith('#')) {
134
+ const hex = color.slice(1);
135
+ const full = hex.length === 3 ? hex.split('').map(c => c + c).join('') : hex.slice(0, 6);
136
+ const r = parseInt(full.slice(0, 2), 16);
137
+ const g = parseInt(full.slice(2, 4), 16);
138
+ const b = parseInt(full.slice(4, 6), 16);
139
+ if (Number.isNaN(r + g + b)) return color;
140
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
141
+ }
142
+ const channels = color.match(/rgba?\(([^)]+)\)/)?.[1];
143
+ if (channels) {
144
+ const [r, g, b] = channels.split(',').map(part => part.trim());
145
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
146
+ }
147
+ return color;
148
+ }
149
+
150
+ /** True when nothing should move: asked for, or asked for by the system. */
151
+ function useStill(enabled) {
152
+ return useReducedMotion() || !enabled;
153
+ }
154
+
155
+ /* -------------------------------------------------------------------------- */
156
+ /* Typing */
157
+ /* -------------------------------------------------------------------------- */
158
+
159
+ /**
160
+ * A string arriving one character at a time.
161
+ *
162
+ * The text is React state rather than anything on the UI thread, and it has
163
+ * to be: a character is a different string, and a string is a re-render
164
+ * whichever thread decided on it. That is fine at a keystroke every 50-odd
165
+ * milliseconds, which is three orders of magnitude slower than a frame.
166
+ *
167
+ * It reserves no space. A line that grows as it types pushes whatever is under
168
+ * it down the screen on every keystroke, so give the container a height, or
169
+ * type into a block that has one already.
170
+ */
171
+ function TextAnimationTyping({
172
+ className,
173
+ text,
174
+ duration,
175
+ delay,
176
+ hold = 1400,
177
+ loop,
178
+ caret = false,
179
+ caretClassName,
180
+ onDone,
181
+ enabled,
182
+ ...props
183
+ }) {
184
+ const step = useShared('duration', duration, 55);
185
+ const wait = useShared('delay', delay, 0);
186
+ const repeat = useShared('loop', loop, false);
187
+ const still = useStill(useShared('enabled', enabled, true));
188
+ const strings = useMemo(() => typeof text === 'string' ? [text] : text, [text]);
189
+ const [shown, setShown] = useState(() => still ? strings[0] ?? '' : '');
190
+ const [typing, setTyping] = useState(false);
191
+
192
+ // Held in a ref so the effect below does not restart the whole run every
193
+ // time the caller passes a new inline arrow function.
194
+ const done = useRef(onDone);
195
+ done.current = onDone;
196
+ useEffect(() => {
197
+ if (still) {
198
+ setShown(strings[0] ?? '');
199
+ setTyping(false);
200
+ return;
201
+ }
202
+ const timers = [];
203
+ let cancelled = false;
204
+ const run = index => {
205
+ const value = strings[index] ?? '';
206
+ let at = 0;
207
+ setTyping(true);
208
+ const type = () => {
209
+ if (cancelled) return;
210
+ setShown(value.slice(0, at));
211
+ if (at < value.length) {
212
+ at += 1;
213
+ timers.push(setTimeout(type, step));
214
+ return;
215
+ }
216
+ setTyping(false);
217
+ const last = index === strings.length - 1;
218
+ if (last && !repeat) {
219
+ done.current?.();
220
+ return;
221
+ }
222
+ timers.push(setTimeout(() => erase(value, index), hold));
223
+ };
224
+ type();
225
+ };
226
+ const erase = (value, index) => {
227
+ if (cancelled) return;
228
+ let at = value.length;
229
+ setTyping(true);
230
+ const back = () => {
231
+ if (cancelled) return;
232
+ setShown(value.slice(0, at));
233
+ if (at > 0) {
234
+ at -= 1;
235
+ timers.push(setTimeout(back, step));
236
+ return;
237
+ }
238
+ setTyping(false);
239
+ run(index === strings.length - 1 ? 0 : index + 1);
240
+ };
241
+ back();
242
+ };
243
+ timers.push(setTimeout(() => run(0), wait));
244
+ return () => {
245
+ cancelled = true;
246
+ timers.forEach(clearTimeout);
247
+ };
248
+ }, [hold, repeat, step, still, strings, wait]);
249
+ return (
250
+ /*#__PURE__*/
251
+ /*
252
+ * The caret is nested *inside* the text rather than laid out beside it. As
253
+ * a sibling in a row it sits against the right of the whole block, so the
254
+ * moment a line wraps it stops being after the last character and starts
255
+ * floating at the end of the paragraph — which is exactly where a caret
256
+ * should never be. Nested, it is one more glyph in the flow and lands
257
+ * wherever typing has got to, on whichever line that is.
258
+ */
259
+ _jsxs(Text, {
260
+ className: className,
261
+ ...props,
262
+ children: [shown, caret ? /*#__PURE__*/_jsx(TypingCaret, {
263
+ blinking: !typing,
264
+ still: still,
265
+ className: caretClassName
266
+ }) : null]
267
+ })
268
+ );
269
+ }
270
+ TextAnimationTyping.displayName = 'TextAnimation.Typing';
271
+
272
+ /**
273
+ * The caret. Solid while characters are arriving, blinking while they are not
274
+ * — which is what a real one does, and what says "waiting" rather than
275
+ * "finished".
276
+ *
277
+ * A glyph rather than a view, because only a glyph takes part in line-breaking
278
+ * — and because a character is sized by the font it is in, so it is the right
279
+ * height at every text size without being told any of them. Nested text can be
280
+ * faded but not transformed, which the opacity here is well inside.
281
+ */
282
+ function TypingCaret({
283
+ blinking,
284
+ still,
285
+ className
286
+ }) {
287
+ const {
288
+ caret
289
+ } = textAnimationVariants();
290
+ const opacity = useSharedValue(1);
291
+ useEffect(() => {
292
+ if (still || !blinking) {
293
+ cancelAnimation(opacity);
294
+ opacity.value = 1;
295
+ return;
296
+ }
297
+ opacity.value = withRepeat(withSequence(withTiming(1, {
298
+ duration: CARET_PERIOD
299
+ }), withTiming(0, {
300
+ duration: CARET_PERIOD
301
+ })), -1, false);
302
+ return () => cancelAnimation(opacity);
303
+ }, [blinking, opacity, still]);
304
+ const style = useAnimatedStyle(() => ({
305
+ opacity: opacity.value
306
+ }));
307
+ return /*#__PURE__*/_jsx(AnimatedText
308
+ // Decorative: the text it follows is what a screen reader should read.
309
+ , {
310
+ accessibilityElementsHidden: true,
311
+ importantForAccessibility: "no-hide-descendants",
312
+ style: style,
313
+ className: caret({
314
+ className
315
+ }),
316
+ children: '\u258F'
317
+ });
318
+ }
319
+
320
+ /* -------------------------------------------------------------------------- */
321
+ /* Rotating */
322
+ /* -------------------------------------------------------------------------- */
323
+
324
+ /**
325
+ * One phrase replaced by the next, the outgoing one leaving upward and the
326
+ * incoming one arriving from below.
327
+ *
328
+ * The box is sized by the longest phrase and not by the current one. A box
329
+ * that resizes as the words change makes the line around it jump, which is
330
+ * more distracting than the effect is interesting — and a box sized by the
331
+ * *first* phrase clips every longer one that follows, which is worse than
332
+ * either.
333
+ *
334
+ * So the width comes from a sizer: every phrase, laid out invisibly in a view
335
+ * with no height. Yoga still takes a column's width from its children when
336
+ * their height is fixed at zero, so the box ends up as wide as the longest
337
+ * phrase and as tall as one line of them.
338
+ */
339
+ function TextAnimationRotating({
340
+ className,
341
+ text,
342
+ duration,
343
+ delay,
344
+ enabled,
345
+ ...props
346
+ }) {
347
+ const period = useShared('duration', duration, 2200);
348
+ const wait = useShared('delay', delay, 0);
349
+ const still = useStill(useShared('enabled', enabled, true));
350
+ const phrases = useMemo(() => typeof text === 'string' ? [text] : text, [text]);
351
+ const [index, setIndex] = useState(0);
352
+ useEffect(() => {
353
+ if (still || phrases.length < 2) return;
354
+ let interval;
355
+ const start = setTimeout(() => {
356
+ setIndex(current => (current + 1) % phrases.length);
357
+ interval = setInterval(() => setIndex(current => (current + 1) % phrases.length), period);
358
+ }, wait + period);
359
+ return () => {
360
+ clearTimeout(start);
361
+ if (interval) clearInterval(interval);
362
+ };
363
+ }, [period, phrases.length, still, wait]);
364
+ return /*#__PURE__*/_jsxs(View, {
365
+ className: cn('overflow-hidden', className),
366
+ children: [/*#__PURE__*/_jsx(View, {
367
+ style: {
368
+ height: 0
369
+ },
370
+ pointerEvents: "none",
371
+ accessibilityElementsHidden: true,
372
+ children: phrases.map(phrase => /*#__PURE__*/_jsx(Text, {
373
+ ...props,
374
+ className: "opacity-0",
375
+ children: phrase
376
+ }, phrase))
377
+ }), phrases.map((phrase, at) => /*#__PURE__*/_jsx(RotatingPhrase, {
378
+ phrase: phrase
379
+ // The first is laid out in flow and gives the box its height; the
380
+ // rest are stacked over it, so one line is the height whichever
381
+ // phrase is showing.
382
+ ,
383
+ measured: at === 0,
384
+ active: at === index,
385
+ still: still,
386
+ textProps: props
387
+ }, phrase))]
388
+ });
389
+ }
390
+ TextAnimationRotating.displayName = 'TextAnimation.Rotating';
391
+ function RotatingPhrase({
392
+ phrase,
393
+ measured,
394
+ active,
395
+ still,
396
+ textProps
397
+ }) {
398
+ const travel = useSharedValue(active ? 0 : ROTATE_DISTANCE);
399
+ const opacity = useSharedValue(active ? 1 : 0);
400
+ useEffect(() => {
401
+ if (still) {
402
+ travel.value = active ? 0 : ROTATE_DISTANCE;
403
+ opacity.value = active ? 1 : 0;
404
+ return;
405
+ }
406
+ if (active) {
407
+ // From below, not from wherever it was left: a phrase coming back after
408
+ // a full cycle should arrive the same way it did the first time.
409
+ travel.value = ROTATE_DISTANCE;
410
+ travel.value = withTiming(0, {
411
+ duration: 300,
412
+ easing: Easing.out(Easing.cubic)
413
+ });
414
+ opacity.value = withTiming(1, {
415
+ duration: 240
416
+ });
417
+ } else {
418
+ travel.value = withTiming(-ROTATE_DISTANCE, {
419
+ duration: 300,
420
+ easing: Easing.in(Easing.cubic)
421
+ });
422
+ opacity.value = withTiming(0, {
423
+ duration: 200
424
+ });
425
+ }
426
+ }, [active, opacity, still, travel]);
427
+ const style = useAnimatedStyle(() => ({
428
+ opacity: opacity.value,
429
+ transform: [{
430
+ translateY: travel.value
431
+ }]
432
+ }));
433
+ return /*#__PURE__*/_jsx(Animated.View, {
434
+ style: [style, measured ? undefined : {
435
+ position: 'absolute',
436
+ left: 0,
437
+ right: 0
438
+ }]
439
+ // Only the phrase on screen is announced. All of them are laid out, and
440
+ // a screen reader reading the whole list is the cost of that.
441
+ ,
442
+ accessibilityElementsHidden: !active,
443
+ importantForAccessibility: active ? 'auto' : 'no-hide-descendants',
444
+ children: /*#__PURE__*/_jsx(Text, {
445
+ ...textProps,
446
+ children: phrase
447
+ })
448
+ });
449
+ }
450
+
451
+ /* -------------------------------------------------------------------------- */
452
+ /* Counting */
453
+ /* -------------------------------------------------------------------------- */
454
+
455
+ /** Formats a number the way both counting parts agree to. */
456
+ function useFormatter(decimals, formatOptions) {
457
+ return useMemo(() => {
458
+ if (formatOptions) {
459
+ try {
460
+ const format = new Intl.NumberFormat(undefined, formatOptions);
461
+ return value => format.format(value);
462
+ } catch {
463
+ // Some engines ship a partial Intl; fall through to the plain form.
464
+ }
465
+ }
466
+ return value => value.toFixed(decimals);
467
+ }, [decimals, formatOptions]);
468
+ }
469
+ /**
470
+ * A number counting up to itself.
471
+ *
472
+ * The text is rewritten from a Reanimated reaction rather than from state:
473
+ * sixty re-renders a second for a number that is only ever a string is the
474
+ * kind of thing that makes an otherwise still screen drop frames. Only the
475
+ * rounded value crosses back to JavaScript, so the work per frame is one
476
+ * comparison and, at most, one `setState` on a value that actually changed.
477
+ */
478
+ function TextAnimationCounting({
479
+ className,
480
+ value,
481
+ from = 0,
482
+ duration,
483
+ delay,
484
+ decimals = 0,
485
+ formatOptions,
486
+ enabled,
487
+ ...props
488
+ }) {
489
+ const span = useShared('duration', duration, 1200);
490
+ const wait = useShared('delay', delay, 0);
491
+ const still = useStill(useShared('enabled', enabled, true));
492
+ const format = useFormatter(decimals, formatOptions);
493
+ const progress = useSharedValue(still ? value : from);
494
+ const [shown, setShown] = useState(() => still ? value : from);
495
+ useEffect(() => {
496
+ if (still) {
497
+ cancelAnimation(progress);
498
+ progress.value = value;
499
+ setShown(value);
500
+ return;
501
+ }
502
+ progress.value = from;
503
+ const timer = setTimeout(() => {
504
+ progress.value = withTiming(value, {
505
+ duration: span,
506
+ // Fast at first and settling at the end, which is what makes the last
507
+ // few digits readable instead of a blur that stops.
508
+ easing: Easing.out(Easing.cubic)
509
+ });
510
+ }, wait);
511
+ return () => {
512
+ clearTimeout(timer);
513
+ cancelAnimation(progress);
514
+ };
515
+ }, [from, progress, span, still, value, wait]);
516
+ const factor = 10 ** decimals;
517
+ useAnimatedReaction(() => Math.round(progress.value * factor) / factor, (current, previous) => {
518
+ if (current !== previous) runOnJS(setShown)(current);
519
+ }, [factor]);
520
+ return /*#__PURE__*/_jsx(Text
521
+ // The final value, always: a screen reader should be told the number,
522
+ // not read a slot machine.
523
+ , {
524
+ accessibilityLabel: format(value),
525
+ className: cn('tabular-nums', className),
526
+ ...props,
527
+ children: format(shown)
528
+ });
529
+ }
530
+ TextAnimationCounting.displayName = 'TextAnimation.Counting';
531
+
532
+ /* -------------------------------------------------------------------------- */
533
+ /* Sliding */
534
+ /* -------------------------------------------------------------------------- */
535
+
536
+ /**
537
+ * An odometer: every digit is a column of ten, and each column rolls to the
538
+ * one it should be showing.
539
+ *
540
+ * Nothing here is measured at runtime. A column is ten stacked digits and the
541
+ * whole thing is moved by a fraction of its own height, so the roll needs no
542
+ * `onLayout`, no first frame at the wrong offset, and no re-render per digit
543
+ * per frame. The height comes from one hidden `0` in flow, which is what makes
544
+ * the column as tall as the font is rather than as tall as a number happens to
545
+ * be.
546
+ */
547
+ function TextAnimationSliding({
548
+ className,
549
+ value,
550
+ decimals = 0,
551
+ padStart = 1,
552
+ thousandSeparator,
553
+ decimalSeparator = '.',
554
+ textClassName,
555
+ size,
556
+ weight,
557
+ delay,
558
+ enabled,
559
+ ...props
560
+ }) {
561
+ const wait = useShared('delay', delay, 0);
562
+ const still = useStill(useShared('enabled', enabled, true));
563
+ const {
564
+ digits
565
+ } = textAnimationVariants();
566
+ const {
567
+ whole,
568
+ fraction,
569
+ negative
570
+ } = useMemo(() => {
571
+ const absolute = Math.abs(value);
572
+ const fixed = absolute.toFixed(decimals);
573
+ const [left = '0', right = ''] = fixed.split('.');
574
+ return {
575
+ whole: left.padStart(padStart, '0'),
576
+ fraction: right,
577
+ negative: value < 0
578
+ };
579
+ }, [decimals, padStart, value]);
580
+ const columns = [];
581
+ whole.split('').forEach((digit, index) => {
582
+ const fromEnd = whole.length - index - 1;
583
+ columns.push(/*#__PURE__*/_jsx(SlidingColumn, {
584
+ digit: Number(digit),
585
+ delay: wait,
586
+ still: still,
587
+ className: textClassName,
588
+ size: size,
589
+ weight: weight
590
+ }, `w${index}`));
591
+ if (thousandSeparator && fromEnd > 0 && fromEnd % 3 === 0) {
592
+ columns.push(/*#__PURE__*/_jsx(Text, {
593
+ size: size,
594
+ weight: weight,
595
+ className: textClassName,
596
+ children: thousandSeparator
597
+ }, `s${index}`));
598
+ }
599
+ });
600
+ if (fraction) {
601
+ columns.push(/*#__PURE__*/_jsx(Text, {
602
+ size: size,
603
+ weight: weight,
604
+ className: textClassName,
605
+ children: decimalSeparator
606
+ }, "point"));
607
+ fraction.split('').forEach((digit, index) => {
608
+ columns.push(/*#__PURE__*/_jsx(SlidingColumn, {
609
+ digit: Number(digit),
610
+ delay: wait,
611
+ still: still,
612
+ className: textClassName,
613
+ size: size,
614
+ weight: weight
615
+ }, `f${index}`));
616
+ });
617
+ }
618
+ return /*#__PURE__*/_jsxs(View
619
+ // The number as one thing, so a screen reader says "1,024" rather than
620
+ // reading four columns of ten digits each.
621
+ , {
622
+ accessibilityRole: "text",
623
+ accessibilityLabel: `${negative ? '-' : ''}${whole}${fraction ? decimalSeparator + fraction : ''}`,
624
+ className: digits({
625
+ className
626
+ }),
627
+ ...props,
628
+ children: [negative ? /*#__PURE__*/_jsx(Text, {
629
+ size: size,
630
+ weight: weight,
631
+ className: textClassName,
632
+ children: "\u2212"
633
+ }) : null, columns]
634
+ });
635
+ }
636
+ TextAnimationSliding.displayName = 'TextAnimation.Sliding';
637
+ const TEN = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
638
+ function SlidingColumn({
639
+ digit,
640
+ delay,
641
+ still,
642
+ className,
643
+ size,
644
+ weight
645
+ }) {
646
+ const {
647
+ column
648
+ } = textAnimationVariants();
649
+ const offset = useSharedValue(still ? digit : 0);
650
+ useEffect(() => {
651
+ if (still) {
652
+ cancelAnimation(offset);
653
+ offset.value = digit;
654
+ return;
655
+ }
656
+ const timer = setTimeout(() => {
657
+ offset.value = withSpring(digit, NUMBER_SPRING);
658
+ }, delay);
659
+ return () => {
660
+ clearTimeout(timer);
661
+ cancelAnimation(offset);
662
+ };
663
+ }, [delay, digit, offset, still]);
664
+
665
+ /*
666
+ * A percentage of the column's own height, so nothing has to be measured.
667
+ * The column is ten digits tall; moving it by `-n/10` of that puts the nth
668
+ * one in the window, whatever the font size turns out to be.
669
+ */
670
+ const style = useAnimatedStyle(() => ({
671
+ transform: [{
672
+ translateY: `${-offset.value / TEN.length * 100}%`
673
+ }]
674
+ }));
675
+ return /*#__PURE__*/_jsxs(View, {
676
+ className: "overflow-hidden",
677
+ children: [/*#__PURE__*/_jsx(Text, {
678
+ size: size,
679
+ weight: weight,
680
+ className: cn('opacity-0', className),
681
+ children: "0"
682
+ }), /*#__PURE__*/_jsx(Animated.View, {
683
+ style: [style, {
684
+ position: 'absolute',
685
+ top: 0
686
+ }],
687
+ className: column(),
688
+ children: TEN.map(n => /*#__PURE__*/_jsx(Text, {
689
+ size: size,
690
+ weight: weight,
691
+ className: cn('tabular-nums', className),
692
+ children: n
693
+ }, n))
694
+ })]
695
+ });
696
+ }
697
+
698
+ /* -------------------------------------------------------------------------- */
699
+ /* Scrolling */
700
+ /* -------------------------------------------------------------------------- */
701
+
702
+ /**
703
+ * A column of values scrolling past a window, coming to rest on one.
704
+ *
705
+ * The difference from `Sliding` is what the reader is being told. An odometer
706
+ * says *this number changed*; a column that scrolls past its neighbours says
707
+ * *this number was chosen from a scale*, and the values either side of it are
708
+ * the scale. Reach for it for a target, a threshold, a picked quantity — and
709
+ * for a plain change of value, reach for the other one.
710
+ */
711
+ function TextAnimationScrolling({
712
+ className,
713
+ value,
714
+ step = 1,
715
+ around = 2,
716
+ duration,
717
+ delay,
718
+ formatOptions,
719
+ textClassName,
720
+ size,
721
+ weight,
722
+ highlight = false,
723
+ highlightClassName,
724
+ fadeColor = '--color-background',
725
+ enabled,
726
+ ...props
727
+ }) {
728
+ const span = useShared('duration', duration, 1400);
729
+ const wait = useShared('delay', delay, 0);
730
+ const still = useStill(useShared('enabled', enabled, true));
731
+ const {
732
+ column,
733
+ highlight: band
734
+ } = textAnimationVariants();
735
+ const token = useCSSVariable(typeof fadeColor === 'string' && fadeColor.startsWith('--') ? fadeColor : '--color-background');
736
+ const resolvedToken = typeof token === 'string' ? token : undefined;
737
+ const fade = fadeColor === false ? undefined : (fadeColor.startsWith('--') ? resolvedToken : fadeColor) ?? resolvedToken;
738
+ const format = useFormatter(0, formatOptions);
739
+
740
+ // The window shows `around` either side of the resting value, so the run
741
+ // starts that many steps below zero and ends that many above the target.
742
+ const values = useMemo(() => {
743
+ const out = [];
744
+ for (let i = -around; value + around * step >= i * step; i += 1) {
745
+ out.push(i * step);
746
+ if (out.length > 400) break;
747
+ }
748
+ return out;
749
+ }, [around, step, value]);
750
+ const rows = around * 2 + 1;
751
+ /*
752
+ * The window shows `rows` values and the chosen one belongs in the middle of
753
+ * them, so the column comes to rest `around` short of the target's own index.
754
+ * Landing on the index itself would put the answer against the top edge with
755
+ * the whole scale below it, which reads as having overshot.
756
+ */
757
+ const found = values.indexOf(value);
758
+ const target = Math.max(0, (found >= 0 ? found : values.length - 1) - around);
759
+ const offset = useSharedValue(still ? target : 0);
760
+ useEffect(() => {
761
+ if (still) {
762
+ cancelAnimation(offset);
763
+ offset.value = target;
764
+ return;
765
+ }
766
+ offset.value = 0;
767
+ const timer = setTimeout(() => {
768
+ offset.value = withTiming(target, {
769
+ duration: span,
770
+ easing: Easing.out(Easing.cubic)
771
+ });
772
+ }, wait);
773
+ return () => {
774
+ clearTimeout(timer);
775
+ cancelAnimation(offset);
776
+ };
777
+ }, [offset, span, still, target, wait]);
778
+ const style = useAnimatedStyle(() => ({
779
+ transform: [{
780
+ translateY: `${-offset.value / values.length * 100}%`
781
+ }]
782
+ }));
783
+ return /*#__PURE__*/_jsxs(View, {
784
+ accessibilityRole: "text",
785
+ accessibilityLabel: format(value),
786
+ className: cn('overflow-hidden', className),
787
+ ...props,
788
+ children: [/*#__PURE__*/_jsx(View, {
789
+ className: "opacity-0",
790
+ children: Array.from({
791
+ length: rows
792
+ }, (_, i) => /*#__PURE__*/_jsx(Text, {
793
+ size: size,
794
+ weight: weight,
795
+ className: textClassName,
796
+ children: "0"
797
+ }, i))
798
+ }), highlight ? /*#__PURE__*/_jsx(View, {
799
+ pointerEvents: "none",
800
+ className: band({
801
+ className: highlightClassName
802
+ }),
803
+ style: {
804
+ position: 'absolute',
805
+ left: 0,
806
+ right: 0,
807
+ top: `${around / rows * 100}%`,
808
+ height: `${1 / rows * 100}%`
809
+ }
810
+ }) : null, /*#__PURE__*/_jsx(Animated.View, {
811
+ style: [style, {
812
+ position: 'absolute',
813
+ top: 0,
814
+ left: 0,
815
+ right: 0
816
+ }],
817
+ className: column(),
818
+ children: values.map(entry => /*#__PURE__*/_jsx(Text, {
819
+ size: size,
820
+ weight: weight,
821
+ className: cn('tabular-nums', textClassName),
822
+ children: format(entry)
823
+ }, entry))
824
+ }), fade ? /*#__PURE__*/_jsxs(_Fragment, {
825
+ children: [/*#__PURE__*/_jsx(LinearGradient, {
826
+ pointerEvents: "none",
827
+ colors: [fade, withAlpha(fade, 0)],
828
+ style: {
829
+ position: 'absolute',
830
+ top: 0,
831
+ left: 0,
832
+ right: 0,
833
+ height: `${around / rows * 100}%`
834
+ }
835
+ }), /*#__PURE__*/_jsx(LinearGradient, {
836
+ pointerEvents: "none",
837
+ colors: [withAlpha(fade, 0), fade],
838
+ style: {
839
+ position: 'absolute',
840
+ bottom: 0,
841
+ left: 0,
842
+ right: 0,
843
+ height: `${around / rows * 100}%`
844
+ }
845
+ })]
846
+ }) : null]
847
+ });
848
+ }
849
+ TextAnimationScrolling.displayName = 'TextAnimation.Scrolling';
850
+ export const TextAnimation = Object.assign(TextAnimationRoot, {
851
+ Typing: TextAnimationTyping,
852
+ Rotating: TextAnimationRotating,
853
+ Counting: TextAnimationCounting,
854
+ Sliding: TextAnimationSliding,
855
+ Scrolling: TextAnimationScrolling
856
+ });
857
+ //# sourceMappingURL=index.js.map