panelui-native 0.75.0 → 0.76.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 (155) hide show
  1. package/lib/module/components/ai-input/ai-input-growth.js +62 -0
  2. package/lib/module/components/ai-input/ai-input-growth.js.map +1 -0
  3. package/lib/module/components/ai-input/index.js +1568 -0
  4. package/lib/module/components/ai-input/index.js.map +1 -0
  5. package/lib/module/components/alert/index.js +1 -1
  6. package/lib/module/components/alert/index.js.map +1 -1
  7. package/lib/module/components/avatar/avatar-source.js +15 -0
  8. package/lib/module/components/avatar/avatar-source.js.map +1 -0
  9. package/lib/module/components/avatar/index.js +32 -31
  10. package/lib/module/components/avatar/index.js.map +1 -1
  11. package/lib/module/components/bottom-sheet/index.js +3 -2
  12. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  13. package/lib/module/components/button-group/index.js +2 -3
  14. package/lib/module/components/button-group/index.js.map +1 -1
  15. package/lib/module/components/carousel/index.js +10 -7
  16. package/lib/module/components/carousel/index.js.map +1 -1
  17. package/lib/module/components/collapsible/index.js +1 -1
  18. package/lib/module/components/collapsible/index.js.map +1 -1
  19. package/lib/module/components/drawer/index.js +18 -14
  20. package/lib/module/components/drawer/index.js.map +1 -1
  21. package/lib/module/components/field/index.js +7 -8
  22. package/lib/module/components/field/index.js.map +1 -1
  23. package/lib/module/components/label/index.js +1 -1
  24. package/lib/module/components/label/index.js.map +1 -1
  25. package/lib/module/components/marquee/index.js +36 -11
  26. package/lib/module/components/marquee/index.js.map +1 -1
  27. package/lib/module/components/marquee/marquee-math.js +22 -6
  28. package/lib/module/components/marquee/marquee-math.js.map +1 -1
  29. package/lib/module/components/plan/index.js +2 -1
  30. package/lib/module/components/plan/index.js.map +1 -1
  31. package/lib/module/components/plot/index.js +27 -9
  32. package/lib/module/components/plot/index.js.map +1 -1
  33. package/lib/module/components/search-bar/index.js +11 -3
  34. package/lib/module/components/search-bar/index.js.map +1 -1
  35. package/lib/module/components/split-view/index.js +26 -27
  36. package/lib/module/components/split-view/index.js.map +1 -1
  37. package/lib/module/components/split-view/split-view-lifecycle.js +32 -0
  38. package/lib/module/components/split-view/split-view-lifecycle.js.map +1 -0
  39. package/lib/module/components/splitter/index.js +7 -13
  40. package/lib/module/components/splitter/index.js.map +1 -1
  41. package/lib/module/components/splitter/splitter-math.js +14 -0
  42. package/lib/module/components/splitter/splitter-math.js.map +1 -1
  43. package/lib/module/components/tabs/index.js +1 -1
  44. package/lib/module/components/tabs/index.js.map +1 -1
  45. package/lib/module/components/timeline/index.js +156 -42
  46. package/lib/module/components/timeline/index.js.map +1 -1
  47. package/lib/module/components/timeline/timeline-geometry.js +11 -0
  48. package/lib/module/components/timeline/timeline-geometry.js.map +1 -1
  49. package/lib/module/components/toast/index.js +1 -1
  50. package/lib/module/components/toast/index.js.map +1 -1
  51. package/lib/module/components/tooltip/index.js +2 -2
  52. package/lib/module/components/tooltip/index.js.map +1 -1
  53. package/lib/module/components/tree/index.js +2 -2
  54. package/lib/module/components/tree/index.js.map +1 -1
  55. package/lib/module/components/typography/index.js +2 -1
  56. package/lib/module/components/typography/index.js.map +1 -1
  57. package/lib/module/components/waterfall-chart/index.js +22 -3
  58. package/lib/module/components/waterfall-chart/index.js.map +1 -1
  59. package/lib/module/hooks/use-disclosure.js +10 -8
  60. package/lib/module/hooks/use-disclosure.js.map +1 -1
  61. package/lib/module/icons/index.js +135 -0
  62. package/lib/module/icons/index.js.map +1 -1
  63. package/lib/module/index.js +5 -1
  64. package/lib/module/index.js.map +1 -1
  65. package/lib/module/primitives/controllable-state.js +61 -0
  66. package/lib/module/primitives/controllable-state.js.map +1 -0
  67. package/lib/module/primitives/finite-chart.js +15 -0
  68. package/lib/module/primitives/finite-chart.js.map +1 -0
  69. package/lib/module/primitives/glass.js +136 -0
  70. package/lib/module/primitives/glass.js.map +1 -0
  71. package/lib/module/primitives/scrim.js +8 -1
  72. package/lib/module/primitives/scrim.js.map +1 -1
  73. package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts +59 -0
  74. package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts.map +1 -0
  75. package/lib/typescript/src/components/ai-input/index.d.ts +423 -0
  76. package/lib/typescript/src/components/ai-input/index.d.ts.map +1 -0
  77. package/lib/typescript/src/components/avatar/avatar-source.d.ts +4 -0
  78. package/lib/typescript/src/components/avatar/avatar-source.d.ts.map +1 -0
  79. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
  80. package/lib/typescript/src/components/bottom-sheet/index.d.ts +10 -1
  81. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
  83. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/marquee/index.d.ts +15 -6
  85. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/marquee/marquee-math.d.ts +6 -0
  87. package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -1
  88. package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/plot/index.d.ts +2 -1
  90. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts +14 -0
  94. package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts.map +1 -0
  95. package/lib/typescript/src/components/splitter/index.d.ts +2 -2
  96. package/lib/typescript/src/components/splitter/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/splitter/splitter-math.d.ts +2 -0
  98. package/lib/typescript/src/components/splitter/splitter-math.d.ts.map +1 -1
  99. package/lib/typescript/src/components/timeline/index.d.ts +24 -2
  100. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  101. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts +2 -0
  102. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts.map +1 -1
  103. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  104. package/lib/typescript/src/components/waterfall-chart/index.d.ts +2 -1
  105. package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
  106. package/lib/typescript/src/hooks/use-disclosure.d.ts.map +1 -1
  107. package/lib/typescript/src/icons/index.d.ts +18 -0
  108. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  109. package/lib/typescript/src/index.d.ts +3 -1
  110. package/lib/typescript/src/index.d.ts.map +1 -1
  111. package/lib/typescript/src/primitives/controllable-state.d.ts +33 -0
  112. package/lib/typescript/src/primitives/controllable-state.d.ts.map +1 -0
  113. package/lib/typescript/src/primitives/finite-chart.d.ts +5 -0
  114. package/lib/typescript/src/primitives/finite-chart.d.ts.map +1 -0
  115. package/lib/typescript/src/primitives/glass.d.ts +43 -0
  116. package/lib/typescript/src/primitives/glass.d.ts.map +1 -0
  117. package/lib/typescript/src/primitives/scrim.d.ts +7 -0
  118. package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
  119. package/package.json +7 -2
  120. package/src/components/ai-input/ai-input-growth.ts +65 -0
  121. package/src/components/ai-input/index.tsx +1937 -0
  122. package/src/components/alert/index.tsx +1 -1
  123. package/src/components/avatar/avatar-source.ts +18 -0
  124. package/src/components/avatar/index.tsx +35 -27
  125. package/src/components/bottom-sheet/index.tsx +15 -3
  126. package/src/components/button-group/index.tsx +1 -1
  127. package/src/components/carousel/index.tsx +10 -11
  128. package/src/components/collapsible/index.tsx +1 -1
  129. package/src/components/drawer/index.tsx +24 -20
  130. package/src/components/field/index.tsx +4 -4
  131. package/src/components/label/index.tsx +1 -1
  132. package/src/components/marquee/index.tsx +56 -12
  133. package/src/components/marquee/marquee-math.ts +26 -3
  134. package/src/components/plan/index.tsx +6 -2
  135. package/src/components/plot/index.tsx +37 -11
  136. package/src/components/search-bar/index.tsx +9 -3
  137. package/src/components/split-view/index.tsx +24 -33
  138. package/src/components/split-view/split-view-lifecycle.ts +43 -0
  139. package/src/components/splitter/index.tsx +7 -11
  140. package/src/components/splitter/splitter-math.ts +28 -0
  141. package/src/components/tabs/index.tsx +1 -1
  142. package/src/components/timeline/index.tsx +189 -34
  143. package/src/components/timeline/timeline-geometry.ts +11 -0
  144. package/src/components/toast/index.tsx +6 -1
  145. package/src/components/tooltip/index.tsx +1 -1
  146. package/src/components/tree/index.tsx +2 -2
  147. package/src/components/typography/index.tsx +2 -2
  148. package/src/components/waterfall-chart/index.tsx +37 -4
  149. package/src/hooks/use-disclosure.ts +7 -12
  150. package/src/icons/index.tsx +106 -0
  151. package/src/index.ts +27 -0
  152. package/src/primitives/controllable-state.ts +112 -0
  153. package/src/primitives/finite-chart.ts +14 -0
  154. package/src/primitives/glass.tsx +171 -0
  155. package/src/primitives/scrim.tsx +7 -1
@@ -45,14 +45,23 @@
45
45
  */
46
46
  import {
47
47
  Children,
48
+ cloneElement,
48
49
  createContext,
49
50
  forwardRef,
50
51
  isValidElement,
51
52
  useContext,
52
53
  useMemo,
54
+ type ReactElement,
53
55
  type ReactNode,
54
56
  } from 'react';
55
- import { ScrollView, View, type Text as RNText, type ViewProps } from 'react-native';
57
+ import {
58
+ ScrollView,
59
+ View,
60
+ type FlatListProps,
61
+ type ListRenderItemInfo,
62
+ type Text as RNText,
63
+ type ViewProps,
64
+ } from 'react-native';
56
65
  import Animated, {
57
66
  interpolate,
58
67
  useAnimatedScrollHandler,
@@ -65,7 +74,7 @@ import { tv } from 'tailwind-variants';
65
74
  import { useCSSVariable } from 'uniwind';
66
75
  import { IconColorProvider } from '../../icons';
67
76
  import { Text, type TextProps, textChildren } from '../../primitives/text';
68
- import { timelineColumnWidth } from './timeline-geometry';
77
+ import { TIMELINE_WIDE_COLUMN, timelineColumnOffsets, timelineColumnWidth } from './timeline-geometry';
69
78
 
70
79
  export type TimelineVariant = 'dot' | 'icon' | 'numbered' | 'card' | 'compact';
71
80
  /** Semantic colour for a single event, independent of progress. */
@@ -74,6 +83,7 @@ export type TimelineTone = 'default' | 'info' | 'success' | 'warning' | 'danger'
74
83
  export type TimelineOrientation = 'vertical' | 'horizontal';
75
84
 
76
85
  const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
86
+ const TimelineColumnOffsetContext = createContext<number | null>(null);
77
87
 
78
88
  /*
79
89
  * Horizontal geometry.
@@ -252,6 +262,8 @@ interface TimelineContextValue {
252
262
  offsets: number[];
253
263
  /** Horizontal only: false when the reader has asked for less movement. */
254
264
  animate: boolean;
265
+ /** A virtualized list draws one rail segment per mounted cell. */
266
+ virtualized: boolean;
255
267
  }
256
268
 
257
269
  interface TimelineItemContextValue {
@@ -330,17 +342,28 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
330
342
  * themselves by. These are the snap points, and they are what an item reads
331
343
  * to know how far it is from the reading edge.
332
344
  */
333
- const offsets = useMemo(() => {
334
- if (!horizontal) return [];
335
- const result: number[] = [];
336
- let sum = 0;
345
+ const columns = useMemo(() => {
346
+ if (!horizontal) return { offsets: [], children };
347
+ const items: ReactElement<TimelineItemProps>[] = [];
337
348
  Children.forEach(children, (child) => {
338
- if (!isValidElement(child)) return;
339
- result.push(sum);
340
- sum += itemWidth(child.props as TimelineItemProps);
349
+ if (isValidElement(child) && child.type === TimelineItem) {
350
+ items.push(child as ReactElement<TimelineItemProps>);
351
+ }
341
352
  });
342
- return result;
353
+ const offsets = timelineColumnOffsets(items.map((item) => itemWidth(item.props)));
354
+ let position = 0;
355
+ const rendered = Children.map(children, (child) => {
356
+ if (!isValidElement(child) || child.type !== TimelineItem) return child;
357
+ const offset = offsets[position++] ?? 0;
358
+ return (
359
+ <TimelineColumnOffsetContext.Provider value={offset}>
360
+ {child}
361
+ </TimelineColumnOffsetContext.Provider>
362
+ );
363
+ });
364
+ return { offsets, children: rendered };
343
365
  }, [children, horizontal]);
366
+ const { offsets } = columns;
344
367
 
345
368
  const context = useMemo(
346
369
  () => ({
@@ -350,6 +373,7 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
350
373
  scrollX,
351
374
  offsets,
352
375
  animate: !reducedMotion,
376
+ virtualized: false,
353
377
  }),
354
378
  [value, variant, orientation, scrollX, offsets, reducedMotion]
355
379
  );
@@ -402,7 +426,7 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
402
426
  borderColor: typeof border === 'string' ? border : undefined,
403
427
  }}
404
428
  />
405
- {textChildren(children)}
429
+ {textChildren(columns.children)}
406
430
  </View>
407
431
  </AnimatedScrollView>
408
432
  </View>
@@ -412,6 +436,128 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
412
436
  );
413
437
  TimelineRoot.displayName = 'Timeline';
414
438
 
439
+ export interface TimelineListProps<T>
440
+ extends Omit<
441
+ FlatListProps<T>,
442
+ | 'data'
443
+ | 'renderItem'
444
+ | 'horizontal'
445
+ | 'getItemLayout'
446
+ | 'onScroll'
447
+ | 'snapToOffsets'
448
+ | 'CellRendererComponent'
449
+ > {
450
+ /** Complete event collection; rows outside the native window stay unmounted. */
451
+ data: readonly T[];
452
+ /** Render one `Timeline.Item`. Its step, width, and last marker are owned by the list. */
453
+ renderItem: (info: ListRenderItemInfo<T>) => ReactElement<TimelineItemProps>;
454
+ /** Width of each column, or a resolver for mixed-width histories. */
455
+ itemWidth?: number | ((item: T, index: number) => number);
456
+ /** Steps at or below this index render as completed. */
457
+ value?: number;
458
+ variant?: TimelineVariant;
459
+ snap?: boolean;
460
+ }
461
+
462
+ /**
463
+ * Opt-in bounded mount path for long horizontal histories. The compound
464
+ * `Timeline` API remains the simpler choice for short or mixed-content lists.
465
+ */
466
+ function TimelineList<T>({
467
+ data,
468
+ renderItem,
469
+ itemWidth: requestedWidth = TIMELINE_WIDE_COLUMN,
470
+ value = 0,
471
+ variant = 'dot',
472
+ snap = true,
473
+ initialNumToRender = 6,
474
+ maxToRenderPerBatch = 6,
475
+ windowSize = 5,
476
+ className,
477
+ ...props
478
+ }: TimelineListProps<T>) {
479
+ const reducedMotion = useReducedMotion();
480
+ const scrollX = useSharedValue(0);
481
+ const widths = useMemo(
482
+ () =>
483
+ data.map((item, index) =>
484
+ timelineColumnWidth(
485
+ typeof requestedWidth === 'function' ? requestedWidth(item, index) : requestedWidth,
486
+ true
487
+ )
488
+ ),
489
+ [data, requestedWidth]
490
+ );
491
+ const offsets = useMemo(() => {
492
+ let offset = 0;
493
+ return widths.map((width) => {
494
+ const current = offset;
495
+ offset += width;
496
+ return current;
497
+ });
498
+ }, [widths]);
499
+ const context = useMemo(
500
+ () => ({
501
+ activeStep: value,
502
+ variant,
503
+ orientation: 'horizontal' as const,
504
+ scrollX,
505
+ offsets,
506
+ animate: !reducedMotion,
507
+ virtualized: true,
508
+ }),
509
+ [value, variant, scrollX, offsets, reducedMotion]
510
+ );
511
+ const { root } = timelineVariants({ variant, orientation: 'horizontal' });
512
+ const onScroll = useAnimatedScrollHandler((event) => {
513
+ scrollX.value = event.contentOffset.x;
514
+ });
515
+
516
+ return (
517
+ <TimelineContext.Provider value={context}>
518
+ <Animated.FlatList
519
+ data={data}
520
+ horizontal
521
+ accessibilityRole="list"
522
+ showsHorizontalScrollIndicator={false}
523
+ decelerationRate="fast"
524
+ snapToAlignment="start"
525
+ initialNumToRender={initialNumToRender}
526
+ maxToRenderPerBatch={maxToRenderPerBatch}
527
+ windowSize={windowSize}
528
+ removeClippedSubviews
529
+ className={root({ className })}
530
+ {...props}
531
+ getItemLayout={(_, index) => ({
532
+ index,
533
+ length: widths[index] ?? TIMELINE_WIDE_COLUMN,
534
+ offset: offsets[index] ?? 0,
535
+ })}
536
+ snapToOffsets={snap ? offsets : undefined}
537
+ onScroll={onScroll}
538
+ scrollEventThrottle={16}
539
+ renderItem={(info) => (
540
+ /*
541
+ * The list owns the order, so it hands each column its own left edge
542
+ * the same way the compound root does. An item reads its offset from
543
+ * here rather than indexing by `step`, and a virtualized item that
544
+ * was given none would sit at zero and never recede.
545
+ */
546
+ <TimelineColumnOffsetContext.Provider value={offsets[info.index] ?? 0}>
547
+ {cloneElement(renderItem(info), {
548
+ step: info.index,
549
+ width: widths[info.index],
550
+ last: info.index === data.length - 1,
551
+ role: 'listitem',
552
+ })}
553
+ </TimelineColumnOffsetContext.Provider>
554
+ )}
555
+ />
556
+ </TimelineContext.Provider>
557
+ );
558
+ }
559
+ TimelineList.displayName = 'Timeline.List';
560
+
415
561
  export interface TimelineItemProps extends ViewProps {
416
562
  className?: string;
417
563
  /** Position in the sequence, zero-based. */
@@ -450,8 +596,16 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
450
596
  },
451
597
  ref
452
598
  ) => {
453
- const { activeStep, variant, orientation, scrollX, offsets, animate } =
599
+ const { activeStep, variant, orientation, scrollX, animate, virtualized } =
454
600
  useTimeline('Timeline.Item');
601
+ /*
602
+ * Where this column starts, from its position among the rendered items
603
+ * rather than from `step`. `step` is the semantic progress value: it may
604
+ * be sparse, repeated or reordered, and indexing the offsets by it lands a
605
+ * column on another column's snap point.
606
+ */
607
+ const columnOffset = useContext(TimelineColumnOffsetContext);
608
+ const border = useCSSVariable('--color-border');
455
609
  const isCompleted = completed ?? step <= activeStep;
456
610
  const { item } = timelineVariants({
457
611
  variant,
@@ -464,7 +618,7 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
464
618
  const columnWidth = horizontal
465
619
  ? itemWidth({ step, width, children } as TimelineItemProps)
466
620
  : undefined;
467
- const offset = offsets[step] ?? 0;
621
+ const offset = columnOffset ?? 0;
468
622
 
469
623
  const context = useMemo(
470
624
  () => ({
@@ -479,8 +633,8 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
479
633
  );
480
634
 
481
635
  /*
482
- * A column recedes as it leaves the reading edge, so the one being read is
483
- * the one that looks read.
636
+ * A column scales and drops as it leaves the reading edge, so the one being
637
+ * read is distinct without fading informative text below its contrast.
484
638
  *
485
639
  * One column's width either side, not two: a window wide enough to hold
486
640
  * three columns near full strength is a row where nothing is picked out,
@@ -494,12 +648,11 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
494
648
  * the rest.
495
649
  */
496
650
  const columnStyle = useAnimatedStyle(() => {
497
- if (!horizontal || !animate) return { opacity: 1, transform: [] };
651
+ if (!horizontal || !animate) return { transform: [] };
498
652
  const distance = Math.abs(scrollX.value - offset);
499
653
  const window = Math.max(columnWidth ?? timelineColumnWidth(undefined, true), 1);
500
654
  const away = interpolate(distance, [0, window], [0, 1], 'clamp');
501
655
  return {
502
- opacity: 1 - away * 0.4,
503
656
  transform: [{ scale: 1 - away * 0.04 }, { translateY: away * 4 }],
504
657
  };
505
658
  });
@@ -522,6 +675,22 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
522
675
  {...props}
523
676
  style={[style, { width: columnWidth }, columnStyle]}
524
677
  >
678
+ {virtualized && !last ? (
679
+ <View
680
+ accessibilityElementsHidden
681
+ importantForAccessibility="no-hide-descendants"
682
+ pointerEvents="none"
683
+ style={{
684
+ position: 'absolute',
685
+ left: 0,
686
+ right: 0,
687
+ top: HORIZONTAL_RAIL_TOP,
688
+ borderTopWidth: 1,
689
+ borderStyle: 'dashed',
690
+ borderColor: typeof border === 'string' ? border : undefined,
691
+ }}
692
+ />
693
+ ) : null}
525
694
  {textChildren(children)}
526
695
  </Animated.View>
527
696
  </TimelineItemContext.Provider>
@@ -678,22 +847,7 @@ const TimelineContent = forwardRef<View, ViewProps & { className?: string }>(
678
847
  const { body, panel } = timelineVariants({ variant, tone, completed, orientation });
679
848
  const horizontal = orientation === 'horizontal';
680
849
 
681
- /*
682
- * Horizontal: the body recedes further than the column around it.
683
- *
684
- * The rail is a continuous thing and has to stay readable across the whole
685
- * track — the years either side of the one being read are half of what a
686
- * timeline is for. The prose under them is not: several columns of it at
687
- * equal weight is a wall, and no amount of fading the column as a whole
688
- * fixes that without taking the years down with it. So the body takes a
689
- * second, steeper curve and the dates keep the first one.
690
- */
691
- const bodyStyle = useAnimatedStyle(() => {
692
- if (!horizontal || !animate) return { opacity: 1 };
693
- const distance = Math.abs(scrollX.value - offset);
694
- const window = Math.max(columnWidth, 1);
695
- return { opacity: interpolate(distance, [0, window], [1, 0.3], 'clamp') };
696
- });
850
+ /* Horizontal content stays fully opaque; scale and translation provide focus. */
697
851
 
698
852
  const inner =
699
853
  variant === 'card' ? (
@@ -715,7 +869,7 @@ const TimelineContent = forwardRef<View, ViewProps & { className?: string }>(
715
869
  ref={ref}
716
870
  className={body({ className })}
717
871
  {...props}
718
- style={[style, bodyStyle]}
872
+ style={style}
719
873
  >
720
874
  {inner}
721
875
  </Animated.View>
@@ -825,6 +979,7 @@ const TimelineStat = forwardRef<View, TimelineStatProps>(
825
979
  TimelineStat.displayName = 'Timeline.Stat';
826
980
 
827
981
  export const Timeline = Object.assign(TimelineRoot, {
982
+ List: TimelineList,
828
983
  Item: TimelineItem,
829
984
  Aside: TimelineAside,
830
985
  Indicator: TimelineIndicator,
@@ -7,3 +7,14 @@ export function timelineColumnWidth(requested: number | undefined, filled: boole
7
7
  }
8
8
  return filled ? TIMELINE_WIDE_COLUMN : TIMELINE_NARROW_COLUMN;
9
9
  }
10
+
11
+ /** Left edge of each column in render order. */
12
+ export function timelineColumnOffsets(widths: readonly number[]): number[] {
13
+ const offsets: number[] = [];
14
+ let sum = 0;
15
+ for (const width of widths) {
16
+ offsets.push(sum);
17
+ sum += width;
18
+ }
19
+ return offsets;
20
+ }
@@ -175,7 +175,12 @@ const ToastRoot = ({ className, variant = 'default', onHide, children, ...props
175
175
 
176
176
  return (
177
177
  <ToastContext.Provider value={context}>
178
- <View accessibilityRole="alert" accessibilityLiveRegion="polite" className={root({ className })} {...props}>
178
+ <View
179
+ {...props}
180
+ accessibilityRole="alert"
181
+ accessibilityLiveRegion="polite"
182
+ className={root({ className })}
183
+ >
179
184
  {textChildren(children)}
180
185
  </View>
181
186
  </ToastContext.Provider>
@@ -623,7 +623,7 @@ function TooltipTitle({ className, ...props }: TooltipTitleProps) {
623
623
  const { title } = tooltipVariants({ variant });
624
624
 
625
625
  return (
626
- <Text accessibilityRole="header" className={cn(title(), className)} {...props} />
626
+ <Text {...props} accessibilityRole="header" className={cn(title(), className)} />
627
627
  );
628
628
  }
629
629
 
@@ -286,10 +286,10 @@ const TreeRoot = forwardRef<View, TreeProps>(
286
286
  <TreeContext.Provider value={context}>
287
287
  <Animated.View
288
288
  ref={ref}
289
+ {...props}
289
290
  layout={LinearTransition.duration(TRANSITION_DURATION)}
290
291
  accessibilityRole="list"
291
292
  className={root({ className })}
292
- {...props}
293
293
  >
294
294
  {children}
295
295
  </Animated.View>
@@ -394,6 +394,7 @@ const TreeTrigger = forwardRef<View, TreeTriggerProps>(
394
394
  return (
395
395
  <Pressable
396
396
  ref={ref}
397
+ {...props}
397
398
  accessibilityRole="button"
398
399
  accessibilityState={{
399
400
  expanded: isBranch ? isExpanded : undefined,
@@ -403,7 +404,6 @@ const TreeTrigger = forwardRef<View, TreeTriggerProps>(
403
404
  aria-level={level + 1}
404
405
  disabled={triggerDisabled}
405
406
  className={trigger({ className })}
406
- {...props}
407
407
  onPress={handlePress}
408
408
  >
409
409
  <IconColorProvider color={isSelected ? selectedColor : undefined}>
@@ -290,9 +290,9 @@ export interface TypographyListProps extends ViewProps {
290
290
  */
291
291
  const TypographyList = forwardRef<View, TypographyListProps>(
292
292
  ({ className, ordered = false, children, ...props }, ref) => (
293
- <View ref={ref} accessibilityRole="list" className={cn('gap-2', className)} {...props}>
293
+ <View ref={ref} {...props} accessibilityRole="list" className={cn('gap-2', className)}>
294
294
  {Children.map(children, (child, index) => (
295
- <View className="w-full flex-row gap-2">
295
+ <View role="listitem" className="w-full flex-row gap-2">
296
296
  {ordered ? (
297
297
  <Text className="text-base text-muted-foreground">{index + 1}.</Text>
298
298
  ) : (
@@ -65,7 +65,12 @@ import Animated, {
65
65
  } from 'react-native-reanimated';
66
66
  import Svg, { Defs, G, Line as SvgLine, LinearGradient, Path, Stop } from 'react-native-svg';
67
67
  import { useCSSVariable } from 'uniwind';
68
+ import { ChartAccessibilityData, type ChartAccessibilityProps } from '../../primitives/chart-accessibility';
68
69
  import { Text } from '../../primitives/text';
70
+ import {
71
+ finiteChartDomain,
72
+ finiteChartNumber,
73
+ } from '../../primitives/finite-chart';
69
74
  import { barPath, compactNumber, type Plot } from '../../utils/chart';
70
75
  import { cn } from '../../utils/cn';
71
76
 
@@ -196,7 +201,7 @@ export function waterfallSteps(data: WaterfallDatum[]): WaterfallStep[] {
196
201
  let running = 0;
197
202
 
198
203
  for (const datum of data) {
199
- const value = typeof datum.value === 'number' && !Number.isNaN(datum.value) ? datum.value : 0;
204
+ const value = finiteChartNumber(datum.value) ?? 0;
200
205
 
201
206
  if (datum.total) {
202
207
  // A total is a reading, so it is measured from the baseline and the
@@ -228,7 +233,7 @@ export function waterfallSteps(data: WaterfallDatum[]): WaterfallStep[] {
228
233
  return steps;
229
234
  }
230
235
 
231
- export interface WaterfallChartProps extends ViewProps {
236
+ export interface WaterfallChartProps extends ViewProps, ChartAccessibilityProps<WaterfallStep> {
232
237
  className?: string;
233
238
  /** The steps, in the order they happen. */
234
239
  data: WaterfallDatum[];
@@ -327,6 +332,11 @@ const WaterfallChartRoot = forwardRef<WaterfallChartHandle, WaterfallChartProps>
327
332
  fallColor,
328
333
  totalColor,
329
334
  onActiveIndexChange,
335
+ accessible,
336
+ accessibilityLabel,
337
+ accessibilityHint,
338
+ accessibilityLabelForDatum,
339
+ onAccessibilityDatumPress,
330
340
  compact = false,
331
341
  children,
332
342
  ...props
@@ -385,7 +395,8 @@ const WaterfallChartRoot = forwardRef<WaterfallChartHandle, WaterfallChartProps>
385
395
  };
386
396
 
387
397
  const extent = useMemo<[number, number]>(() => {
388
- if (yDomain) return yDomain;
398
+ const explicit = finiteChartDomain(yDomain);
399
+ if (explicit) return explicit;
389
400
 
390
401
  /*
391
402
  * Both ends of every bar, not just the values. A step's bar occupies the
@@ -535,7 +546,29 @@ const WaterfallChartRoot = forwardRef<WaterfallChartHandle, WaterfallChartProps>
535
546
  <WaterfallChartContext.Provider value={context}>
536
547
  <View {...props} style={props.style} className={cn('w-full', className)}>
537
548
  {header}
538
- <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
549
+ <ChartAccessibilityData
550
+ chart="Waterfall chart"
551
+ data={steps}
552
+ disabled={accessible === false || loading}
553
+ accessibilityLabel={accessibilityLabel}
554
+ accessibilityHint={accessibilityHint}
555
+ accessibilityLabelForDatum={accessibilityLabelForDatum}
556
+ onAccessibilityDatumPress={onAccessibilityDatumPress}
557
+ valueOf={(step) => [
558
+ ['label', step.label],
559
+ ['change', step.value],
560
+ ['kind', step.kind],
561
+ ['starts at', step.start],
562
+ ['ends at', step.end],
563
+ ]}
564
+ />
565
+ <View
566
+ onLayout={onLayout}
567
+ style={{ aspectRatio }}
568
+ className="w-full"
569
+ accessibilityElementsHidden
570
+ importantForAccessibility="no-hide-descendants"
571
+ >
539
572
  {plot.width > 0 ? (
540
573
  <>
541
574
  <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
@@ -1,4 +1,5 @@
1
- import { useCallback, useMemo, useState } from 'react';
1
+ import { useCallback, useMemo } from 'react';
2
+ import { useControllableState } from '../primitives/controllable-state';
2
3
 
3
4
  export interface UseDisclosureOptions {
4
5
  defaultOpen?: boolean;
@@ -34,17 +35,11 @@ export function useDisclosure({
34
35
  open: controlledOpen,
35
36
  onOpenChange,
36
37
  }: UseDisclosureOptions = {}): UseDisclosureResult {
37
- const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
38
- const isControlled = controlledOpen !== undefined;
39
- const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
40
-
41
- const setOpen = useCallback(
42
- (next: boolean) => {
43
- if (!isControlled) setUncontrolledOpen(next);
44
- onOpenChange?.(next);
45
- },
46
- [isControlled, onOpenChange]
47
- );
38
+ const { value: isOpen, setValue: setOpen } = useControllableState({
39
+ value: controlledOpen,
40
+ defaultValue: defaultOpen,
41
+ onChange: onOpenChange,
42
+ });
48
43
 
49
44
  const open = useCallback(() => setOpen(true), [setOpen]);
50
45
  const close = useCallback(() => setOpen(false), [setOpen]);
@@ -852,6 +852,22 @@ export function PaperclipIcon({ size = 16, color, ...props }: IconProps) {
852
852
  }
853
853
 
854
854
  /** A capsule on a stand — a microphone, for anything voice. */
855
+ /** A voice waveform — dictation, voice mode, a recording that has a shape. */
856
+ export function AudioLinesIcon({ size = 16, color, ...props }: IconProps) {
857
+ const resolved = useResolvedColor(color, '#737373');
858
+ return (
859
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
860
+ <Path
861
+ d="M2 10v3M6 6v11M10 3v18M14 8v7M18 5v13M22 10v3"
862
+ stroke={resolved}
863
+ strokeWidth={2}
864
+ strokeLinecap="round"
865
+ strokeLinejoin="round"
866
+ />
867
+ </Svg>
868
+ );
869
+ }
870
+
855
871
  export function MicIcon({ size = 16, color, ...props }: IconProps) {
856
872
  const resolved = useResolvedColor(color, '#737373');
857
873
  return (
@@ -1078,6 +1094,73 @@ export function CrosshairIcon({ size = 16, color, ...props }: IconProps) {
1078
1094
  }
1079
1095
 
1080
1096
  /** A compass needle — which way is north. */
1097
+ export function GlobeIcon({ size = 16, color, ...props }: IconProps) {
1098
+ const resolved = useResolvedColor(color, '#737373');
1099
+ return (
1100
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1101
+ <Circle
1102
+ cx={12}
1103
+ cy={12}
1104
+ r={10}
1105
+ stroke={resolved}
1106
+ strokeWidth={2}
1107
+ strokeLinecap="round"
1108
+ strokeLinejoin="round"
1109
+ />
1110
+ <Path
1111
+ d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20M2 12h20"
1112
+ stroke={resolved}
1113
+ strokeWidth={2}
1114
+ strokeLinecap="round"
1115
+ strokeLinejoin="round"
1116
+ />
1117
+ </Svg>
1118
+ );
1119
+ }
1120
+
1121
+ export function CameraIcon({ size = 16, color, ...props }: IconProps) {
1122
+ const resolved = useResolvedColor(color, '#737373');
1123
+ return (
1124
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1125
+ <Path
1126
+ d="M13.997 4a2 2 0 0 1 1.76 1.05l.486.9A2 2 0 0 0 18.003 7H20a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h1.997a2 2 0 0 0 1.759-1.048l.489-.904A2 2 0 0 1 10.004 4z"
1127
+ stroke={resolved}
1128
+ strokeWidth={2}
1129
+ strokeLinecap="round"
1130
+ strokeLinejoin="round"
1131
+ />
1132
+ <Circle
1133
+ cx={12}
1134
+ cy={13}
1135
+ r={3}
1136
+ stroke={resolved}
1137
+ strokeWidth={2}
1138
+ strokeLinecap="round"
1139
+ strokeLinejoin="round"
1140
+ />
1141
+ </Svg>
1142
+ );
1143
+ }
1144
+
1145
+ /**
1146
+ * The glyph on a control that opens a list of values — a stacked pair, because
1147
+ * one chevron promises a direction the control does not travel in.
1148
+ */
1149
+ export function ChevronsUpDownIcon({ size = 16, color, ...props }: IconProps) {
1150
+ const resolved = useResolvedColor(color, '#737373');
1151
+ return (
1152
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1153
+ <Path
1154
+ d="m7 15 5 5 5-5M7 9l5-5 5 5"
1155
+ stroke={resolved}
1156
+ strokeWidth={2}
1157
+ strokeLinecap="round"
1158
+ strokeLinejoin="round"
1159
+ />
1160
+ </Svg>
1161
+ );
1162
+ }
1163
+
1081
1164
  export function CompassIcon({ size = 16, color, ...props }: IconProps) {
1082
1165
  const resolved = useResolvedColor(color, '#737373');
1083
1166
  return (
@@ -1256,6 +1339,29 @@ export interface ToggleIconProps extends IconProps {
1256
1339
  }
1257
1340
 
1258
1341
  /** Upvote. Filled once the vote is cast. */
1342
+ /**
1343
+ * A plain up arrow, drawn symmetrically about the centre of its box so it sits
1344
+ * dead centre in a circular button.
1345
+ *
1346
+ * Distinct from `ArrowUpIcon`, which is the heavy solid arrow a vote control
1347
+ * wants, and from `SendIcon`, which is a paper plane. This is the one a
1348
+ * composer's send button uses.
1349
+ */
1350
+ export function SendArrowIcon({ size = 16, color, ...props }: IconProps) {
1351
+ const resolved = useResolvedColor(color, '#737373');
1352
+ return (
1353
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
1354
+ <Path
1355
+ d="m5 12 7-7 7 7M12 19V5"
1356
+ stroke={resolved}
1357
+ strokeWidth={2.5}
1358
+ strokeLinecap="round"
1359
+ strokeLinejoin="round"
1360
+ />
1361
+ </Svg>
1362
+ );
1363
+ }
1364
+
1259
1365
  export function ArrowUpIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
1260
1366
  const resolved = useResolvedColor(color, '#737373');
1261
1367
  return (