orn-ui 0.2.8 → 0.2.10
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/AGENTS.md +6 -2
- package/README.md +6 -6
- package/lib/commonjs/atoms/ShinyText.js +188 -0
- package/lib/commonjs/atoms/ShinyText.js.map +1 -0
- package/lib/commonjs/atoms/index.js +7 -0
- package/lib/commonjs/atoms/index.js.map +1 -1
- package/lib/commonjs/organisms/Chart.js +968 -0
- package/lib/commonjs/organisms/Chart.js.map +1 -0
- package/lib/commonjs/organisms/OptionWheel.js +408 -0
- package/lib/commonjs/organisms/OptionWheel.js.map +1 -0
- package/lib/commonjs/organisms/Timeline.js +420 -0
- package/lib/commonjs/organisms/Timeline.js.map +1 -0
- package/lib/commonjs/organisms/chart/frame.js +373 -0
- package/lib/commonjs/organisms/chart/frame.js.map +1 -0
- package/lib/commonjs/organisms/chart/marks.js +316 -0
- package/lib/commonjs/organisms/chart/marks.js.map +1 -0
- package/lib/commonjs/organisms/chart/math.js +468 -0
- package/lib/commonjs/organisms/chart/math.js.map +1 -0
- package/lib/commonjs/organisms/chart/palette.js +120 -0
- package/lib/commonjs/organisms/chart/palette.js.map +1 -0
- package/lib/commonjs/organisms/index.js +21 -0
- package/lib/commonjs/organisms/index.js.map +1 -1
- package/lib/module/atoms/ShinyText.js +183 -0
- package/lib/module/atoms/ShinyText.js.map +1 -0
- package/lib/module/atoms/index.js +1 -0
- package/lib/module/atoms/index.js.map +1 -1
- package/lib/module/organisms/Chart.js +963 -0
- package/lib/module/organisms/Chart.js.map +1 -0
- package/lib/module/organisms/OptionWheel.js +403 -0
- package/lib/module/organisms/OptionWheel.js.map +1 -0
- package/lib/module/organisms/Timeline.js +415 -0
- package/lib/module/organisms/Timeline.js.map +1 -0
- package/lib/module/organisms/chart/frame.js +367 -0
- package/lib/module/organisms/chart/frame.js.map +1 -0
- package/lib/module/organisms/chart/marks.js +310 -0
- package/lib/module/organisms/chart/marks.js.map +1 -0
- package/lib/module/organisms/chart/math.js +445 -0
- package/lib/module/organisms/chart/math.js.map +1 -0
- package/lib/module/organisms/chart/palette.js +114 -0
- package/lib/module/organisms/chart/palette.js.map +1 -0
- package/lib/module/organisms/index.js +3 -0
- package/lib/module/organisms/index.js.map +1 -1
- package/lib/typescript/atoms/ShinyText.d.ts +48 -0
- package/lib/typescript/atoms/ShinyText.d.ts.map +1 -0
- package/lib/typescript/atoms/index.d.ts +2 -0
- package/lib/typescript/atoms/index.d.ts.map +1 -1
- package/lib/typescript/organisms/Chart.d.ts +208 -0
- package/lib/typescript/organisms/Chart.d.ts.map +1 -0
- package/lib/typescript/organisms/OptionWheel.d.ts +78 -0
- package/lib/typescript/organisms/OptionWheel.d.ts.map +1 -0
- package/lib/typescript/organisms/Timeline.d.ts +61 -0
- package/lib/typescript/organisms/Timeline.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/frame.d.ts +85 -0
- package/lib/typescript/organisms/chart/frame.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/marks.d.ts +88 -0
- package/lib/typescript/organisms/chart/marks.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/math.d.ts +161 -0
- package/lib/typescript/organisms/chart/math.d.ts.map +1 -0
- package/lib/typescript/organisms/chart/palette.d.ts +6 -0
- package/lib/typescript/organisms/chart/palette.d.ts.map +1 -0
- package/lib/typescript/organisms/index.d.ts +6 -0
- package/lib/typescript/organisms/index.d.ts.map +1 -1
- package/llms.txt +3 -3
- package/package.json +23 -2
- package/registry/chart.json +35 -0
- package/registry/index.json +53 -0
- package/registry/manifest.json +28 -0
- package/registry/option-wheel.json +19 -0
- package/registry/shiny-text.json +19 -0
- package/registry/timeline.json +20 -0
- package/src/atoms/ShinyText.tsx +226 -0
- package/src/atoms/index.ts +2 -0
- package/src/organisms/Chart.tsx +1252 -0
- package/src/organisms/OptionWheel.tsx +476 -0
- package/src/organisms/Timeline.tsx +467 -0
- package/src/organisms/chart/frame.tsx +349 -0
- package/src/organisms/chart/marks.tsx +295 -0
- package/src/organisms/chart/math.ts +475 -0
- package/src/organisms/chart/palette.ts +112 -0
- package/src/organisms/index.ts +14 -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';
|