orn-ui 0.2.7 → 0.2.9

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 (103) hide show
  1. package/AGENTS.md +25 -3
  2. package/README.md +9 -8
  3. package/lib/commonjs/atoms/Gradient.js +111 -0
  4. package/lib/commonjs/atoms/Gradient.js.map +1 -0
  5. package/lib/commonjs/atoms/ShinyText.js +188 -0
  6. package/lib/commonjs/atoms/ShinyText.js.map +1 -0
  7. package/lib/commonjs/atoms/index.js +14 -0
  8. package/lib/commonjs/atoms/index.js.map +1 -1
  9. package/lib/commonjs/organisms/Chart.js +947 -0
  10. package/lib/commonjs/organisms/Chart.js.map +1 -0
  11. package/lib/commonjs/organisms/OptionWheel.js +408 -0
  12. package/lib/commonjs/organisms/OptionWheel.js.map +1 -0
  13. package/lib/commonjs/organisms/Slides.js +406 -0
  14. package/lib/commonjs/organisms/Slides.js.map +1 -0
  15. package/lib/commonjs/organisms/Timeline.js +420 -0
  16. package/lib/commonjs/organisms/Timeline.js.map +1 -0
  17. package/lib/commonjs/organisms/chart/frame.js +373 -0
  18. package/lib/commonjs/organisms/chart/frame.js.map +1 -0
  19. package/lib/commonjs/organisms/chart/marks.js +316 -0
  20. package/lib/commonjs/organisms/chart/marks.js.map +1 -0
  21. package/lib/commonjs/organisms/chart/math.js +468 -0
  22. package/lib/commonjs/organisms/chart/math.js.map +1 -0
  23. package/lib/commonjs/organisms/chart/palette.js +120 -0
  24. package/lib/commonjs/organisms/chart/palette.js.map +1 -0
  25. package/lib/commonjs/organisms/index.js +28 -0
  26. package/lib/commonjs/organisms/index.js.map +1 -1
  27. package/lib/commonjs/organisms/useSlides.js +186 -0
  28. package/lib/commonjs/organisms/useSlides.js.map +1 -0
  29. package/lib/module/atoms/Gradient.js +106 -0
  30. package/lib/module/atoms/Gradient.js.map +1 -0
  31. package/lib/module/atoms/ShinyText.js +183 -0
  32. package/lib/module/atoms/ShinyText.js.map +1 -0
  33. package/lib/module/atoms/index.js +2 -0
  34. package/lib/module/atoms/index.js.map +1 -1
  35. package/lib/module/organisms/Chart.js +942 -0
  36. package/lib/module/organisms/Chart.js.map +1 -0
  37. package/lib/module/organisms/OptionWheel.js +403 -0
  38. package/lib/module/organisms/OptionWheel.js.map +1 -0
  39. package/lib/module/organisms/Slides.js +401 -0
  40. package/lib/module/organisms/Slides.js.map +1 -0
  41. package/lib/module/organisms/Timeline.js +415 -0
  42. package/lib/module/organisms/Timeline.js.map +1 -0
  43. package/lib/module/organisms/chart/frame.js +367 -0
  44. package/lib/module/organisms/chart/frame.js.map +1 -0
  45. package/lib/module/organisms/chart/marks.js +310 -0
  46. package/lib/module/organisms/chart/marks.js.map +1 -0
  47. package/lib/module/organisms/chart/math.js +445 -0
  48. package/lib/module/organisms/chart/math.js.map +1 -0
  49. package/lib/module/organisms/chart/palette.js +114 -0
  50. package/lib/module/organisms/chart/palette.js.map +1 -0
  51. package/lib/module/organisms/index.js +4 -0
  52. package/lib/module/organisms/index.js.map +1 -1
  53. package/lib/module/organisms/useSlides.js +182 -0
  54. package/lib/module/organisms/useSlides.js.map +1 -0
  55. package/lib/typescript/atoms/Gradient.d.ts +37 -0
  56. package/lib/typescript/atoms/Gradient.d.ts.map +1 -0
  57. package/lib/typescript/atoms/ShinyText.d.ts +48 -0
  58. package/lib/typescript/atoms/ShinyText.d.ts.map +1 -0
  59. package/lib/typescript/atoms/index.d.ts +4 -0
  60. package/lib/typescript/atoms/index.d.ts.map +1 -1
  61. package/lib/typescript/organisms/Chart.d.ts +208 -0
  62. package/lib/typescript/organisms/Chart.d.ts.map +1 -0
  63. package/lib/typescript/organisms/OptionWheel.d.ts +78 -0
  64. package/lib/typescript/organisms/OptionWheel.d.ts.map +1 -0
  65. package/lib/typescript/organisms/Slides.d.ts +69 -0
  66. package/lib/typescript/organisms/Slides.d.ts.map +1 -0
  67. package/lib/typescript/organisms/Timeline.d.ts +61 -0
  68. package/lib/typescript/organisms/Timeline.d.ts.map +1 -0
  69. package/lib/typescript/organisms/chart/frame.d.ts +85 -0
  70. package/lib/typescript/organisms/chart/frame.d.ts.map +1 -0
  71. package/lib/typescript/organisms/chart/marks.d.ts +88 -0
  72. package/lib/typescript/organisms/chart/marks.d.ts.map +1 -0
  73. package/lib/typescript/organisms/chart/math.d.ts +161 -0
  74. package/lib/typescript/organisms/chart/math.d.ts.map +1 -0
  75. package/lib/typescript/organisms/chart/palette.d.ts +6 -0
  76. package/lib/typescript/organisms/chart/palette.d.ts.map +1 -0
  77. package/lib/typescript/organisms/index.d.ts +8 -0
  78. package/lib/typescript/organisms/index.d.ts.map +1 -1
  79. package/lib/typescript/organisms/useSlides.d.ts +52 -0
  80. package/lib/typescript/organisms/useSlides.d.ts.map +1 -0
  81. package/llms.txt +6 -5
  82. package/package.json +33 -2
  83. package/registry/chart.json +35 -0
  84. package/registry/gradient.json +18 -0
  85. package/registry/index.json +79 -0
  86. package/registry/manifest.json +42 -0
  87. package/registry/option-wheel.json +19 -0
  88. package/registry/shiny-text.json +19 -0
  89. package/registry/slides.json +24 -0
  90. package/registry/timeline.json +20 -0
  91. package/src/atoms/Gradient.tsx +99 -0
  92. package/src/atoms/ShinyText.tsx +226 -0
  93. package/src/atoms/index.ts +4 -0
  94. package/src/organisms/Chart.tsx +1223 -0
  95. package/src/organisms/OptionWheel.tsx +476 -0
  96. package/src/organisms/Slides.tsx +413 -0
  97. package/src/organisms/Timeline.tsx +467 -0
  98. package/src/organisms/chart/frame.tsx +349 -0
  99. package/src/organisms/chart/marks.tsx +295 -0
  100. package/src/organisms/chart/math.ts +475 -0
  101. package/src/organisms/chart/palette.ts +112 -0
  102. package/src/organisms/index.ts +22 -0
  103. package/src/organisms/useSlides.ts +220 -0
@@ -0,0 +1,467 @@
1
+ import React, { memo, useEffect, useMemo, useRef } from 'react';
2
+ import { Animated, Easing, Text, View, type StyleProp, type ViewStyle } from 'react-native';
3
+ import { createStyles } from '../theme/createStyles';
4
+ import { useAllowFontScaling, useColors } from '../theme/UIProvider';
5
+ import { PressableScale } from '../atoms/PressableScale';
6
+ import { useReduceMotion } from '../atoms/Transition';
7
+ import { Icon } from '../icons/Icon';
8
+ import type { IconName } from '../icons/types';
9
+
10
+ export type TimelineStatus = 'done' | 'pending';
11
+
12
+ /**
13
+ * Qué puede hacer un toque.
14
+ * - 'free': cualquier hito responde y la línea sigue a `items` para donde
15
+ * vaya, adelante o atrás. Es el que sirve cuando el recorrido no es un
16
+ * progreso, o cuando lo maneja otra cosa.
17
+ * - 'sequential': sólo responde el que sigue al alcanzado, y la línea no
18
+ * retrocede nunca. Avance estricto.
19
+ * - 'revisit': responden el que sigue y todos los ya alcanzados, y la línea
20
+ * tampoco retrocede. Es el caso de "puedo volver a mirar lo anterior sin
21
+ * perder lo que llevo".
22
+ */
23
+ export type TimelineAdvance = 'free' | 'sequential' | 'revisit';
24
+
25
+ export interface TimelineItem {
26
+ label: string;
27
+ /** Glyph from the library's set, to the left of the label. */
28
+ iconName?: IconName;
29
+ /** Emoji to the left of the label. Wins over `iconName`. */
30
+ emoji?: string;
31
+ /** @default 'done' */
32
+ status?: TimelineStatus;
33
+ }
34
+
35
+ export interface TimelineProps {
36
+ items: TimelineItem[];
37
+ /** Pixels the line bows away from the straight path. 0 draws it straight. @default 28 */
38
+ curve?: number;
39
+ /** Vertical distance between one node and the next. @default 84 */
40
+ spacing?: number;
41
+ /** Side the first pill goes to; the rest alternate. @default 'right' */
42
+ startSide?: 'left' | 'right';
43
+ /** Halo behind each pill. @default true */
44
+ glow?: boolean;
45
+ /** Makes each pill tappable and reports its index. */
46
+ onItemPress?: (index: number) => void;
47
+ /**
48
+ * Which taps are allowed, and whether the line may go back. @default 'free'
49
+ *
50
+ * In 'sequential' and 'revisit' the line keeps the furthest point it has
51
+ * been asked to draw, so a parent that hands back a shorter `items` does not
52
+ * undo the progress. Swapping in a different list keeps it too — reset it by
53
+ * remounting (a `key`).
54
+ */
55
+ advance?: TimelineAdvance;
56
+ /**
57
+ * The milestone being looked at right now, highlighted. It is not the
58
+ * progress: it can sit behind the line without pulling it back.
59
+ */
60
+ selectedIndex?: number;
61
+ /**
62
+ * Milliseconds the line takes to travel one gap when a milestone is
63
+ * reached. @default 420
64
+ */
65
+ duration?: number;
66
+ style?: StyleProp<ViewStyle>;
67
+ testID?: string;
68
+ }
69
+
70
+ const DEFAULT_CURVE = 28;
71
+ const DEFAULT_SPACING = 84;
72
+ const NODE = 14;
73
+ const LINE = 2;
74
+ /** Tramos rectos por cada vano: con menos, la curva se ve poligonal. */
75
+ const SEGMENTS = 14;
76
+ const DOT = 3;
77
+ /** Un punto cada cuántos tramos: el camino entero va punteado por debajo. */
78
+ const DOT_EVERY = 2;
79
+ const DEFAULT_DURATION = 420;
80
+ /**
81
+ * Cuánto tarda un tramo en encenderse, medido en hitos. Chico para que el
82
+ * frente de la línea se lea como un borde y no como un degradado largo.
83
+ */
84
+ const FADE = 0.06;
85
+ /**
86
+ * Capas del halo, de afuera hacia adentro. Son muchas y muy tenues a
87
+ * propósito: con tres capas se ven los anillos, que es justo lo contrario de
88
+ * un difuminado. Cuantos más escalones y más chico el salto de opacidad, más
89
+ * cerca queda de un degradado.
90
+ */
91
+ /** Lo que el halo sobresale de la píldora: es lo que hay que dejarle de aire. */
92
+ const GLOW_BLEED = 20;
93
+ const GLOW_LAYERS = [
94
+ { inset: -GLOW_BLEED, opacity: 0.03 },
95
+ { inset: -16, opacity: 0.035 },
96
+ { inset: -12, opacity: 0.04 },
97
+ { inset: -8, opacity: 0.045 },
98
+ { inset: -5, opacity: 0.05 },
99
+ { inset: -2, opacity: 0.055 },
100
+ ];
101
+
102
+ const useStyles = createStyles((theme) => ({
103
+ root: { alignSelf: 'stretch' },
104
+ row: { flexDirection: 'row', alignItems: 'center' },
105
+ side: { flex: 1, justifyContent: 'center' },
106
+ sideLeft: { alignItems: 'flex-end', paddingRight: theme.tokens.spacing.md },
107
+ sideRight: { alignItems: 'flex-start', paddingLeft: theme.tokens.spacing.md },
108
+ node: {
109
+ position: 'absolute',
110
+ width: NODE,
111
+ height: NODE,
112
+ borderRadius: NODE / 2,
113
+ borderWidth: LINE,
114
+ },
115
+ nodePending: { backgroundColor: 'transparent', borderColor: theme.colors.border },
116
+ // Ocupa el círculo entero menos el borde, así el relleno no lo tapa.
117
+ nodeFill: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: NODE / 2 },
118
+ pill: {
119
+ flexDirection: 'row',
120
+ alignItems: 'center',
121
+ gap: theme.tokens.spacing.sm,
122
+ paddingVertical: theme.tokens.spacing.sm,
123
+ paddingHorizontal: theme.tokens.spacing.lg,
124
+ borderRadius: theme.tokens.radius.full,
125
+ borderWidth: 1,
126
+ borderColor: theme.colors.border,
127
+ backgroundColor: theme.colors.surface,
128
+ },
129
+ // El foco se marca con el borde, no con el relleno: el relleno ya está
130
+ // diciendo otra cosa (si el hito está alcanzado o no).
131
+ pillSelected: { borderColor: theme.colors.primary, borderWidth: 2 },
132
+ label: { fontSize: theme.tokens.fontSize.md, color: theme.colors.text },
133
+ labelPending: { color: theme.colors.textLight },
134
+ emoji: { fontSize: theme.tokens.fontSize.md },
135
+ glow: {
136
+ position: 'absolute',
137
+ borderRadius: theme.tokens.radius.full,
138
+ backgroundColor: theme.colors.text,
139
+ },
140
+ // La línea y los nodos viven en la misma capa y se posicionan con las mismas
141
+ // coordenadas. Es lo único que garantiza que el círculo caiga sobre la
142
+ // curva: en el flujo de la fila el nodo lo corre el flex —y los paddings que
143
+ // cada lado necesita para no cortar su píldora—, así que terminaba a un
144
+ // costado de la línea en vez de encima.
145
+ lineLayer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, alignItems: 'center' },
146
+ // Hueco del ancho del nodo en la fila: la píldora se separa del centro lo
147
+ // mismo que antes, pero el círculo ya no se dibuja acá.
148
+ nodeSlot: { width: NODE },
149
+ segment: { position: 'absolute', width: LINE },
150
+ dot: { position: 'absolute', width: DOT, height: DOT, borderRadius: DOT / 2 },
151
+ }));
152
+
153
+ /**
154
+ * Desvío horizontal del nodo `index` respecto del eje. Una onda entera a lo
155
+ * largo de la lista: el primero y el último caen sobre el eje, y los del medio
156
+ * se van a un lado y al otro. Es lo que convierte una columna en un camino.
157
+ */
158
+ function offsetAt(index: number, count: number, curve: number): number {
159
+ if (count < 2) return 0;
160
+ return curve * Math.sin((2 * Math.PI * index) / (count - 1));
161
+ }
162
+
163
+ export const Timeline = memo(
164
+ ({
165
+ items,
166
+ curve = DEFAULT_CURVE,
167
+ spacing = DEFAULT_SPACING,
168
+ startSide = 'right',
169
+ glow = true,
170
+ onItemPress,
171
+ advance = 'free',
172
+ selectedIndex,
173
+ duration = DEFAULT_DURATION,
174
+ style,
175
+ testID,
176
+ }: TimelineProps) => {
177
+ const styles = useStyles();
178
+ const colors = useColors();
179
+ const allowFontScaling = useAllowFontScaling();
180
+ const reduceMotion = useReduceMotion();
181
+
182
+ const count = items.length;
183
+
184
+ /**
185
+ * Hasta dónde llegó el recorrido, en unidades de hito. Se cuenta desde el
186
+ * principio y se corta en el primer pendiente: un hito cumplido después de
187
+ * uno que no lo está no adelanta la línea, porque el camino es un camino.
188
+ */
189
+ const reached = useMemo(() => {
190
+ let last = -1;
191
+ for (let i = 0; i < items.length; i += 1) {
192
+ if (items[i]?.status === 'pending') break;
193
+ last = i;
194
+ }
195
+ return last;
196
+ }, [items]);
197
+
198
+ /**
199
+ * Marca de agua: salvo en 'free', la línea se queda en lo más lejos que
200
+ * llegó. El progreso es del usuario, no del render — que el padre mande
201
+ * una lista más corta no puede desandar lo que ya recorrió.
202
+ */
203
+ const furthest = useRef(reached);
204
+ if (advance === 'free') furthest.current = reached;
205
+ else furthest.current = Math.max(furthest.current, reached);
206
+ const lineTo = furthest.current;
207
+
208
+ const progress = useRef(new Animated.Value(lineTo)).current;
209
+
210
+ useEffect(() => {
211
+ const animation = Animated.timing(progress, {
212
+ toValue: lineTo,
213
+ // Un hito por `duration`: avanzar tres de una vez tarda tres veces
214
+ // más, así se ve el recorrido y no un salto.
215
+ duration: reduceMotion ? 0 : duration,
216
+ easing: Easing.inOut(Easing.cubic),
217
+ useNativeDriver: true,
218
+ });
219
+ animation.start();
220
+ return () => animation.stop();
221
+ }, [lineTo, duration, reduceMotion, progress]);
222
+
223
+ /**
224
+ * Cada vano partido en tramos rectos cortos, cada uno con su largo y su
225
+ * ángulo. Es la misma idea que `Gradient`: la plataforma no tiene curvas
226
+ * sin dependencia nativa, así que la curva se compone con pedazos rectos
227
+ * lo bastante chicos como para que el ojo no vea el quiebre.
228
+ */
229
+ const pieces = useMemo(() => {
230
+ const out: {
231
+ key: string;
232
+ x: number;
233
+ y: number;
234
+ length: number;
235
+ angle: string;
236
+ at: number;
237
+ dotted: boolean;
238
+ }[] = [];
239
+
240
+ for (let i = 0; i < count - 1; i += 1) {
241
+ for (let step = 0; step < SEGMENTS; step += 1) {
242
+ const from = i + step / SEGMENTS;
243
+ const to = i + (step + 1) / SEGMENTS;
244
+ const x1 = offsetAt(from, count, curve);
245
+ const x2 = offsetAt(to, count, curve);
246
+ const y1 = from * spacing;
247
+ const y2 = to * spacing;
248
+ const dx = x2 - x1;
249
+ const dy = y2 - y1;
250
+ const length = Math.sqrt(dx * dx + dy * dy);
251
+
252
+ out.push({
253
+ key: `${i}-${step}`,
254
+ x: (x1 + x2) / 2,
255
+ y: (y1 + y2) / 2,
256
+ length,
257
+ // El tramo se dibuja vertical y se gira hasta la pendiente: el
258
+ // ángulo se mide desde el eje Y, no desde el X.
259
+ angle: `${Math.atan2(dx, dy) * (180 / Math.PI) * -1}deg`,
260
+ // Posición del tramo medida en hitos: es la misma escala que
261
+ // `progress`, así que comparar una cosa con la otra es directo.
262
+ at: to,
263
+ dotted: step % DOT_EVERY === 0,
264
+ });
265
+ }
266
+ }
267
+
268
+ return out;
269
+ }, [count, curve, spacing]);
270
+
271
+ if (count === 0) return null;
272
+
273
+ return (
274
+ <View
275
+ style={[
276
+ styles.root,
277
+ { height: count * spacing },
278
+ style,
279
+ ]}
280
+ testID={testID}
281
+ >
282
+ <View style={styles.lineLayer} pointerEvents="none">
283
+ {/* El camino entero, punteado: es lo que falta recorrer. */}
284
+ {pieces.map(
285
+ (piece) =>
286
+ piece.dotted && (
287
+ <View
288
+ key={`dot-${piece.key}`}
289
+ style={[
290
+ styles.dot,
291
+ {
292
+ backgroundColor: colors.border,
293
+ top: spacing / 2 + piece.y - DOT / 2,
294
+ transform: [{ translateX: piece.x }],
295
+ },
296
+ ]}
297
+ />
298
+ )
299
+ )}
300
+
301
+ {/* Y encima la línea llena, que se revela tramo a tramo a medida que
302
+ `progress` avanza. Lo único que cambia por frame es `opacity`, o
303
+ sea que el recorrido se dibuja en el hilo nativo. */}
304
+ {pieces.map((piece) => (
305
+ <Animated.View
306
+ key={`line-${piece.key}`}
307
+ style={[
308
+ styles.segment,
309
+ {
310
+ backgroundColor: colors.textLight,
311
+ height: piece.length,
312
+ top: spacing / 2 + piece.y - piece.length / 2,
313
+ opacity: progress.interpolate({
314
+ inputRange: [piece.at - FADE, piece.at],
315
+ outputRange: [0, 1],
316
+ extrapolate: 'clamp',
317
+ }),
318
+ transform: [{ translateX: piece.x }, { rotate: piece.angle }],
319
+ },
320
+ ]}
321
+ />
322
+ ))}
323
+
324
+ {items.map((item, index) => (
325
+ <View
326
+ key={`node-${item.label}-${index}`}
327
+ style={[
328
+ styles.node,
329
+ styles.nodePending,
330
+ {
331
+ top: spacing / 2 + index * spacing - NODE / 2,
332
+ transform: [{ translateX: offsetAt(index, count, curve) }],
333
+ },
334
+ ]}
335
+ >
336
+ {/* El relleno es un nodo aparte encima del hueco: el color no se
337
+ puede animar en el hilo nativo, la opacidad sí. */}
338
+ <Animated.View
339
+ style={[
340
+ styles.nodeFill,
341
+ {
342
+ backgroundColor: colors.textLight,
343
+ opacity: progress.interpolate({
344
+ inputRange: [index - FADE, index],
345
+ outputRange: [0, 1],
346
+ extrapolate: 'clamp',
347
+ }),
348
+ transform: [
349
+ {
350
+ scale: progress.interpolate({
351
+ inputRange: [index - FADE, index],
352
+ outputRange: [0.4, 1],
353
+ extrapolate: 'clamp',
354
+ }),
355
+ },
356
+ ],
357
+ },
358
+ ]}
359
+ />
360
+ </View>
361
+ ))}
362
+ </View>
363
+
364
+ {items.map((item, index) => {
365
+ const pending = item.status === 'pending';
366
+ const onRight = (index % 2 === 0) === (startSide === 'right');
367
+ const offset = offsetAt(index, count, curve);
368
+ // La fila entera se corre con la curva, píldora incluida, así que el
369
+ // lado hacia el que se fue pierde ese espacio contra el borde. Se lo
370
+ // devolvemos como padding, más lo que sobresale el halo: sin esto la
371
+ // píldora de más afuera queda cortada al ras.
372
+ const bleed = glow ? GLOW_BLEED : 0;
373
+ const roomLeft = Math.max(0, -offset) + bleed;
374
+ const roomRight = Math.max(0, offset) + bleed;
375
+
376
+ const pill = (
377
+ <View>
378
+ {glow &&
379
+ GLOW_LAYERS.map((layer) => (
380
+ <View
381
+ key={layer.inset}
382
+ pointerEvents="none"
383
+ style={[
384
+ styles.glow,
385
+ {
386
+ top: layer.inset,
387
+ bottom: layer.inset,
388
+ left: layer.inset,
389
+ right: layer.inset,
390
+ opacity: pending ? layer.opacity / 2 : layer.opacity,
391
+ },
392
+ ]}
393
+ />
394
+ ))}
395
+ <View style={[styles.pill, index === selectedIndex && styles.pillSelected]}>
396
+ {item.emoji ? (
397
+ <Text allowFontScaling={allowFontScaling} style={styles.emoji}>
398
+ {item.emoji}
399
+ </Text>
400
+ ) : (
401
+ !!item.iconName && (
402
+ <Icon name={item.iconName} size={16} color={pending ? colors.textLight : colors.text} />
403
+ )
404
+ )}
405
+ <Text allowFontScaling={allowFontScaling} style={[styles.label, pending && styles.labelPending]}>
406
+ {item.label}
407
+ </Text>
408
+ </View>
409
+ </View>
410
+ );
411
+
412
+ // 'sequential' deja tocar sólo el que sigue; 'revisit' también los
413
+ // ya recorridos. El que no responde se marca como deshabilitado en
414
+ // vez de quedarse mudo: un botón que no hace nada y no lo dice es
415
+ // peor que un botón apagado.
416
+ const reachable =
417
+ advance === 'sequential'
418
+ ? index === lineTo + 1
419
+ : advance === 'revisit'
420
+ ? index <= lineTo + 1
421
+ : true;
422
+
423
+ const pressable = onItemPress ? (
424
+ <PressableScale
425
+ onPress={() => onItemPress(index)}
426
+ disabled={!reachable}
427
+ accessibilityRole="button"
428
+ accessibilityLabel={item.label}
429
+ accessibilityState={{ checked: !pending, disabled: !reachable, selected: index === selectedIndex }}
430
+ testID={testID && `${testID}-press-${index}`}
431
+ >
432
+ {pill}
433
+ </PressableScale>
434
+ ) : (
435
+ pill
436
+ );
437
+
438
+ return (
439
+ <View
440
+ key={`${item.label}-${index}`}
441
+ testID={testID && `${testID}-item-${index}`}
442
+ accessibilityRole={onItemPress ? undefined : 'text'}
443
+ accessibilityLabel={onItemPress ? undefined : item.label}
444
+ accessibilityState={onItemPress ? undefined : { checked: !pending }}
445
+ style={[styles.row, { height: spacing, transform: [{ translateX: offset }] }]}
446
+ >
447
+ <View
448
+ testID={testID && `${testID}-item-${index}-left`}
449
+ style={[styles.side, styles.sideLeft, { paddingLeft: roomLeft }]}
450
+ >
451
+ {!onRight && pressable}
452
+ </View>
453
+ <View style={styles.nodeSlot} />
454
+ <View
455
+ testID={testID && `${testID}-item-${index}-right`}
456
+ style={[styles.side, styles.sideRight, { paddingRight: roomRight }]}
457
+ >
458
+ {onRight && pressable}
459
+ </View>
460
+ </View>
461
+ );
462
+ })}
463
+ </View>
464
+ );
465
+ }
466
+ );
467
+ Timeline.displayName = 'Timeline';