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
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * ```tsx
5
5
  * <RingChart data={goals}>
6
+ * <RingChart.Header title="Today" value="486 kcal" legend />
6
7
  * <RingChart.Ring index={0} />
7
8
  * <RingChart.Ring index={1} />
8
9
  * <RingChart.Center />
9
- * <RingChart.Legend />
10
10
  * </RingChart>
11
11
  * ```
12
12
  *
@@ -25,11 +25,15 @@
25
25
  *
26
26
  * ## Drawing
27
27
  *
28
- * Each ring is two arcs — a full-circle track and the progress over it — with
29
- * only the progress animated, through `strokeDasharray`. Sweeping the arc by
30
- * rebuilding its path would work, but a dash offset is two numbers moving on
31
- * an unchanged path, and it keeps the rounded cap pinned to the moving end for
32
- * free.
28
+ * Each ring is two arcs — a track and the progress over it — with only the
29
+ * progress animated, through `strokeDasharray`. Sweeping the arc by rebuilding
30
+ * its path would work, but a dash offset is two numbers moving on an unchanged
31
+ * path, and it keeps the rounded cap pinned to the moving end for free.
32
+ *
33
+ * The same dash pattern is what makes the other two shapes possible without any
34
+ * more geometry. An open gauge is the pattern cut short of the circumference
35
+ * and the whole circle turned to put the gap where it is wanted; a segmented
36
+ * ring is the pattern repeated, one pair per tick. Both stay one `Circle`.
33
37
  *
34
38
  * Touch, not hover: a ring is selected by pressing it, and pressing the same
35
39
  * one again clears the selection. There is no equivalent of a pointer resting
@@ -65,6 +69,12 @@ import { cn } from '../../utils/cn';
65
69
 
66
70
  const AnimatedCircle = Animated.createAnimatedComponent(Circle);
67
71
 
72
+ /**
73
+ * Where a child is drawn. Rings go inside the SVG, the header above it, and
74
+ * everything else over it as ordinary views.
75
+ */
76
+ type Slot = 'ring' | 'overlay' | 'header';
77
+
68
78
  /** One ring per datum, and the datum is the whole of its contract. */
69
79
  export interface RingDatum {
70
80
  /** Name for the legend and the centre readout. */
@@ -88,6 +98,10 @@ interface RingChartContextValue {
88
98
  activeIndex: number;
89
99
  setActiveIndex: (index: number) => void;
90
100
  radiusOf: (index: number) => number;
101
+ /** Where the arcs begin, in degrees clockwise from twelve o'clock. */
102
+ startAngle: number;
103
+ /** How far they run, as a fraction of a full turn. 1 is a closed ring. */
104
+ arc: number;
91
105
  }
92
106
 
93
107
  const RingChartContext = createContext<RingChartContextValue | null>(null);
@@ -119,6 +133,17 @@ export interface RingChartProps extends ViewProps {
119
133
  strokeWidth?: number;
120
134
  /** Gap between one ring and the next. */
121
135
  ringGap?: number;
136
+ /**
137
+ * Where the arcs begin, in degrees clockwise from twelve o'clock. `0` is the
138
+ * top, `90` the right-hand side.
139
+ */
140
+ startAngle?: number;
141
+ /**
142
+ * Where they end, on the same clock. Leaving a turn's worth between the two
143
+ * gives a closed ring; anything less leaves a gap and reads as a gauge —
144
+ * `startAngle={-90} endAngle={90}` is the half circle over the top.
145
+ */
146
+ endAngle?: number;
122
147
  /** Milliseconds for the arcs to sweep in. */
123
148
  animationDuration?: number;
124
149
  /** Selected ring. Leave unset to let the chart track it. */
@@ -140,6 +165,8 @@ const RingChartRoot = forwardRef<RingChartHandle, RingChartProps>(function RingC
140
165
  size,
141
166
  strokeWidth = 12,
142
167
  ringGap = 6,
168
+ startAngle = 0,
169
+ endAngle = 360,
143
170
  animationDuration = 1100,
144
171
  activeIndex: activeIndexProp,
145
172
  onActiveIndexChange,
@@ -166,6 +193,13 @@ const RingChartRoot = forwardRef<RingChartHandle, RingChartProps>(function RingC
166
193
 
167
194
  const box = size ?? measured;
168
195
 
196
+ /*
197
+ * The sweep as a fraction of a turn, which is the form every arc length here
198
+ * is wanted in. Clamped to one turn because a ring drawn past 360° laps
199
+ * itself, and clamped above zero because a ring of no length is not a chart.
200
+ */
201
+ const arc = Math.min(Math.max(endAngle - startAngle, 0), 360) / 360;
202
+
169
203
  /*
170
204
  * Outermost ring first, so `data[0]` is the one the eye lands on. Each ring
171
205
  * inside it steps in by its own thickness plus the gap.
@@ -231,47 +265,76 @@ const RingChartRoot = forwardRef<RingChartHandle, RingChartProps>(function RingC
231
265
  activeIndex,
232
266
  setActiveIndex,
233
267
  radiusOf,
268
+ startAngle,
269
+ arc,
234
270
  }),
235
- [data, box, strokeWidth, ringGap, colors, reveal, activeIndex, setActiveIndex, radiusOf]
271
+ [
272
+ data,
273
+ box,
274
+ strokeWidth,
275
+ ringGap,
276
+ colors,
277
+ reveal,
278
+ activeIndex,
279
+ setActiveIndex,
280
+ radiusOf,
281
+ startAngle,
282
+ arc,
283
+ ]
236
284
  );
237
285
 
238
286
  const rings: ReactNode[] = [];
239
287
  const overlay: ReactNode[] = [];
288
+ const header: ReactNode[] = [];
240
289
  Children.forEach(children, (child, index) => {
241
290
  if (!isValidElement(child)) return;
242
- const slot = (child.type as { slot?: 'ring' | 'overlay' }).slot ?? 'overlay';
243
- (slot === 'ring' ? rings : overlay).push(
291
+ const slot = (child.type as { slot?: Slot }).slot ?? 'overlay';
292
+ (slot === 'ring' ? rings : slot === 'header' ? header : overlay).push(
244
293
  <ChildSlot key={index}>{child}</ChildSlot>
245
294
  );
246
295
  });
247
296
 
248
297
  return (
249
298
  <RingChartContext.Provider value={context}>
250
- <View
251
- {...props}
252
- onLayout={onLayout}
253
- style={[size ? { width: size, height: size } : { aspectRatio: 1 }, props.style]}
254
- className={cn('w-full items-center justify-center', className)}
255
- >
256
- {box > 0 ? (
257
- <>
258
- <Svg width={box} height={box}>
259
- {rings}
260
- </Svg>
261
- {/*
262
- * The centre and the legend sit over the SVG rather than inside
263
- * it: both are text, and SVG text ignores the platform's text
264
- * scaling and the theme's font.
265
- */}
266
- <View
267
- pointerEvents="box-none"
268
- style={{ position: 'absolute', width: box, height: box }}
269
- className="items-center justify-center"
270
- >
271
- {overlay}
272
- </View>
273
- </>
274
- ) : null}
299
+ {/*
300
+ * Two views, because the header is not part of the plot. The square and
301
+ * the layout measurement belong to the drawing area alone — measured on
302
+ * the outer view they would take in the header too, and the rings would
303
+ * be laid out inside a box taller than the one they are drawn in.
304
+ */}
305
+ <View {...props} style={props.style} className={cn('w-full', className)}>
306
+ {header}
307
+ {/*
308
+ * Given a size the plot is that square and sits in the middle of
309
+ * whatever it was handed; left to measure, it takes the width and is
310
+ * as tall as it is wide. `size` is the usual answer inside a card —
311
+ * a full-width square is twice the height the other charts take.
312
+ */}
313
+ <View
314
+ onLayout={onLayout}
315
+ style={size ? { width: size, height: size } : { aspectRatio: 1 }}
316
+ className={cn('items-center justify-center', size ? 'self-center' : 'w-full')}
317
+ >
318
+ {box > 0 ? (
319
+ <>
320
+ <Svg width={box} height={box}>
321
+ {rings}
322
+ </Svg>
323
+ {/*
324
+ * The centre and the legend sit over the SVG rather than inside
325
+ * it: both are text, and SVG text ignores the platform's text
326
+ * scaling and the theme's font.
327
+ */}
328
+ <View
329
+ pointerEvents="box-none"
330
+ style={{ position: 'absolute', width: box, height: box }}
331
+ className="items-center justify-center"
332
+ >
333
+ {overlay}
334
+ </View>
335
+ </>
336
+ ) : null}
337
+ </View>
275
338
  </View>
276
339
  </RingChartContext.Provider>
277
340
  );
@@ -289,10 +352,23 @@ export interface RingChartRingProps {
289
352
  color?: string;
290
353
  /** Which of the five chart tokens to take, when the datum names no colour. */
291
354
  colorIndex?: SeriesColorIndex;
292
- /** Rounded ends, or square ones. */
355
+ /**
356
+ * Rounded ends, or square ones. Defaults to round, and to square when the
357
+ * ring is segmented — a rounded cap on a tick as long as it is wide draws a
358
+ * lozenge rather than a tick.
359
+ */
293
360
  lineCap?: 'round' | 'butt';
294
361
  /** Opacity of the track behind the arc. */
295
362
  trackOpacity?: number;
363
+ /**
364
+ * Break the ring into this many ticks, lit one at a time as the value
365
+ * climbs. For a target made of countable things — eight of twelve sessions
366
+ * reads off ticks you can count, and off a smooth arc only as "about two
367
+ * thirds".
368
+ */
369
+ segments?: number;
370
+ /** Gap between one tick and the next, in points. */
371
+ segmentGap?: number;
296
372
  }
297
373
 
298
374
  /**
@@ -308,11 +384,23 @@ function RingChartRing({
308
384
  index,
309
385
  color,
310
386
  colorIndex,
311
- lineCap = 'round',
387
+ lineCap,
312
388
  trackOpacity = 0.15,
389
+ segments,
390
+ segmentGap = 3,
313
391
  }: RingChartRingProps) {
314
- const { data, size, strokeWidth, colors, reveal, activeIndex, setActiveIndex, radiusOf } =
315
- useChart('RingChart.Ring');
392
+ const {
393
+ data,
394
+ size,
395
+ strokeWidth,
396
+ colors,
397
+ reveal,
398
+ activeIndex,
399
+ setActiveIndex,
400
+ radiusOf,
401
+ startAngle,
402
+ arc,
403
+ } = useChart('RingChart.Ring');
316
404
 
317
405
  const datum = data[index];
318
406
  const tokenColor = useSeriesColor(undefined, colorIndex ?? 1);
@@ -321,6 +409,14 @@ function RingChartRing({
321
409
  const radius = radiusOf(index);
322
410
  const circumference = 2 * Math.PI * radius;
323
411
  const centre = size / 2;
412
+ /* The drawn part of the circle. The whole of it for a closed ring. */
413
+ const arcLength = circumference * arc;
414
+ const ticks = segments && segments > 0 ? Math.floor(segments) : 0;
415
+ /* One tick and the gap after it. The gap is taken out of the tick, not added
416
+ to it, so N ticks still span exactly the arc asked for. */
417
+ const slot = ticks ? arcLength / ticks : 0;
418
+ const dash = ticks ? Math.max(slot - segmentGap, 0.5) : 0;
419
+ const cap = lineCap ?? (ticks ? 'butt' : 'round');
324
420
 
325
421
  const fraction = datum && datum.maxValue > 0 ? datum.value / datum.maxValue : 0;
326
422
  // A value past its target fills the ring and stops. Going round twice would
@@ -337,16 +433,38 @@ function RingChartRing({
337
433
 
338
434
  const props = useAnimatedProps(() => {
339
435
  const progress = Math.max(0, Math.min(1, (reveal.value - start) / 0.6));
340
- const drawn = circumference * clamped * progress;
341
- return {
342
- strokeDasharray: [drawn, circumference],
343
- };
436
+ if (!ticks) {
437
+ return { strokeDasharray: [arcLength * clamped * progress, circumference] };
438
+ }
439
+ /*
440
+ * A tick is lit or it is not, so the sweep rounds to whole ones. The array
441
+ * stays the same length whatever is lit — an unlit tick is a dash of no
442
+ * length rather than a missing pair — because a dash pattern that changes
443
+ * length between frames is a new pattern, not an animated one.
444
+ */
445
+ const lit = Math.round(ticks * clamped * progress);
446
+ const pattern: number[] = [];
447
+ for (let tick = 0; tick < ticks; tick += 1) {
448
+ if (tick < lit) pattern.push(dash, slot - dash);
449
+ else pattern.push(0, slot);
450
+ }
451
+ return { strokeDasharray: pattern };
344
452
  });
345
453
 
346
454
  const dimmed = activeIndex >= 0 && activeIndex !== index;
347
455
 
348
456
  if (!datum || radius <= 0) return null;
349
457
 
458
+ /*
459
+ * A circle's stroke starts at three o'clock, so everything is turned back a
460
+ * quarter to put the start at twelve — an arc starting at three reads as a
461
+ * gauge that has already been running. `startAngle` turns it on from there.
462
+ */
463
+ const rotate = `rotate(${startAngle - 90} ${centre} ${centre})`;
464
+ /* The unlit part of a segmented ring is the gaps between its ticks, so the
465
+ track is ticked too; an open gauge's track stops where its arc stops. */
466
+ const trackDash = ticks ? [dash, slot - dash] : [arcLength, circumference];
467
+
350
468
  return (
351
469
  <G opacity={dimmed ? 0.35 : 1}>
352
470
  <Circle
@@ -356,7 +474,10 @@ function RingChartRing({
356
474
  stroke={stroke}
357
475
  strokeOpacity={trackOpacity}
358
476
  strokeWidth={strokeWidth}
477
+ strokeLinecap={cap}
478
+ strokeDasharray={trackDash}
359
479
  fill="none"
480
+ transform={rotate}
360
481
  />
361
482
  <AnimatedCircle
362
483
  animatedProps={props}
@@ -365,16 +486,15 @@ function RingChartRing({
365
486
  r={radius}
366
487
  stroke={stroke}
367
488
  strokeWidth={strokeWidth}
368
- strokeLinecap={lineCap}
489
+ strokeLinecap={cap}
369
490
  fill="none"
370
- // Rotated so the arc starts at twelve o'clock. An arc starting at
371
- // three reads as a gauge that has already been running.
372
- transform={`rotate(-90 ${centre} ${centre})`}
491
+ transform={rotate}
373
492
  />
374
493
  {/*
375
494
  * The touch target is the ring itself, drawn as a transparent stroke
376
495
  * over it and made wider than the ring — a twelve-point band is below
377
- * the size a finger can reliably hit.
496
+ * the size a finger can reliably hit. It follows the sweep rather than
497
+ * closing the circle, so the dead half of a gauge stays dead.
378
498
  */}
379
499
  <Circle
380
500
  cx={centre}
@@ -382,7 +502,9 @@ function RingChartRing({
382
502
  r={radius}
383
503
  stroke="transparent"
384
504
  strokeWidth={Math.max(strokeWidth, 28)}
505
+ strokeDasharray={[arcLength, circumference]}
385
506
  fill="none"
507
+ transform={rotate}
386
508
  onPress={() => setActiveIndex(activeIndex === index ? -1 : index)}
387
509
  />
388
510
  </G>
@@ -536,7 +658,107 @@ function RingChartLegend({ className, showValue = true, ...props }: RingChartLeg
536
658
  RingChartLegend.displayName = 'RingChart.Legend';
537
659
  RingChartLegend.slot = 'overlay' as const;
538
660
 
661
+ export interface RingChartHeaderProps extends ViewProps {
662
+ className?: string;
663
+ /** Small line above the value — what the chart is of. */
664
+ title?: string;
665
+ /** The readout. The largest thing on the card, and the first thing read. */
666
+ value?: string;
667
+ /** One muted line under the value — a period, a comparison, a target. */
668
+ caption?: string;
669
+ /** Prettier names for the rings, keyed by their `label`. */
670
+ labels?: Record<string, string>;
671
+ /**
672
+ * Draw a swatch and a name per ring along the trailing edge. Prefer this to
673
+ * `RingChart.Legend` on a chart that has a header: that legend hangs off the
674
+ * bottom of the square, where it overlaps whatever is under the chart.
675
+ */
676
+ legend?: boolean;
677
+ /** Trailing slot — a control, a badge, a range picker. Wins over `legend`. */
678
+ children?: ReactNode;
679
+ }
680
+
681
+ /**
682
+ * The strip above the rings: what the chart is of, what it currently reads, and
683
+ * what the colours mean.
684
+ *
685
+ * It belongs to the chart rather than to the card around it because it is about
686
+ * the *rings* — the number changes as one is selected, and the legend is the
687
+ * list the chart itself is holding. The card's header is a caption on the tray
688
+ * the chart sits in; this is the chart introducing itself.
689
+ *
690
+ * The value is not derived here. There is no total to derive: the rings measure
691
+ * different things against different targets. Take it from `onActiveIndexChange`
692
+ * and pass the formatted string down, so one header can show the headline figure
693
+ * when nothing is selected and a ring's own when something is.
694
+ */
695
+ function RingChartHeader({
696
+ className,
697
+ title,
698
+ value,
699
+ caption,
700
+ labels,
701
+ legend = false,
702
+ children,
703
+ ...props
704
+ }: RingChartHeaderProps) {
705
+ const { data, colors } = useChart('RingChart.Header');
706
+ const trailing =
707
+ children ??
708
+ (legend && data.length ? (
709
+ <View className="flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1">
710
+ {data.map((ring, index) => (
711
+ <View key={ring.label} className="flex-row items-center gap-1.5">
712
+ <View
713
+ style={{
714
+ width: 8,
715
+ height: 8,
716
+ borderRadius: 4,
717
+ backgroundColor: colors[index],
718
+ }}
719
+ />
720
+ <Text size="xs" muted numberOfLines={1}>
721
+ {labels?.[ring.label] ?? ring.label}
722
+ </Text>
723
+ </View>
724
+ ))}
725
+ </View>
726
+ ) : null);
727
+
728
+ return (
729
+ <View
730
+ {...props}
731
+ className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
732
+ >
733
+ <View className="flex-1 gap-0.5">
734
+ {title ? (
735
+ <Text size="xs" muted>
736
+ {title}
737
+ </Text>
738
+ ) : null}
739
+ {value ? (
740
+ <Text size="xl" weight="bold">
741
+ {value}
742
+ </Text>
743
+ ) : null}
744
+ {caption ? (
745
+ <Text size="xs" muted>
746
+ {caption}
747
+ </Text>
748
+ ) : null}
749
+ </View>
750
+ {/* Shrinkable, unlike a view's default in React Native. Held rigid, a
751
+ three-ring key takes the width it wants and the caption underneath
752
+ the value wraps to two lines to make room for it. */}
753
+ {trailing ? <View className="shrink pt-1">{trailing}</View> : null}
754
+ </View>
755
+ );
756
+ }
757
+ RingChartHeader.displayName = 'RingChart.Header';
758
+ RingChartHeader.slot = 'header' as const;
759
+
539
760
  export const RingChart = Object.assign(RingChartRoot, {
761
+ Header: RingChartHeader,
540
762
  Ring: RingChartRing,
541
763
  Center: RingChartCenter,
542
764
  Legend: RingChartLegend,
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Steps — a stepper for multi-step flows.
3
3
  *
4
- * Adapted from: origin-space/originui apps/origin/registry/default/ui/stepper.tsx
5
- * (the Stepper / Item / Trigger / Indicator / Title / Description / Separator
6
- * anatomy, and the completed | active | inactive | loading state resolution).
4
+ * A row or column of numbered stops, each one completed, active, inactive or
5
+ * loading. An item works that state out from its own position against the
6
+ * root's `value` rather than being told it, so the four states stay mutually
7
+ * exclusive and in order — there is no way to write two active steps, or to
8
+ * mark step four done while step three is still pending.
7
9
  *
8
- * Upstream drives all styling through `data-*` attributes, which React Native
9
- * has no equivalent for. Here the state is resolved in JS and passed down
10
- * through context into `tv()` variants — the same approach Alert uses.
10
+ * The state is resolved in JS and passed down through context into `tv()`
11
+ * variants, because React Native has no attribute selectors a stylesheet could
12
+ * key off. Alert and Timeline resolve their states the same way.
11
13
  *
12
14
  * Steps does not own your flow: it reflects whatever step your app says is
13
15
  * active. Pass `value` to control it, or `defaultValue` to let it manage