panelui-native 0.65.0 → 0.68.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 (209) hide show
  1. package/README.md +21 -1
  2. package/lib/module/components/area-chart/index.js +18 -1
  3. package/lib/module/components/area-chart/index.js.map +1 -1
  4. package/lib/module/components/bar-chart/index.js +18 -1
  5. package/lib/module/components/bar-chart/index.js.map +1 -1
  6. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  7. package/lib/module/components/candlestick-chart/index.js +18 -1
  8. package/lib/module/components/candlestick-chart/index.js.map +1 -1
  9. package/lib/module/components/carousel/carousel-lifecycle.js +51 -0
  10. package/lib/module/components/carousel/carousel-lifecycle.js.map +1 -0
  11. package/lib/module/components/carousel/index.js +61 -24
  12. package/lib/module/components/carousel/index.js.map +1 -1
  13. package/lib/module/components/context-menu/context-menu-invocation.js +23 -0
  14. package/lib/module/components/context-menu/context-menu-invocation.js.map +1 -0
  15. package/lib/module/components/context-menu/index.js +78 -9
  16. package/lib/module/components/context-menu/index.js.map +1 -1
  17. package/lib/module/components/dialog/index.js.map +1 -1
  18. package/lib/module/components/drawer/index.js.map +1 -1
  19. package/lib/module/components/flow/flow-accessibility.js +18 -2
  20. package/lib/module/components/flow/flow-accessibility.js.map +1 -1
  21. package/lib/module/components/flow/flow-identifiers.js +81 -0
  22. package/lib/module/components/flow/flow-identifiers.js.map +1 -0
  23. package/lib/module/components/flow/index.js +12 -18
  24. package/lib/module/components/flow/index.js.map +1 -1
  25. package/lib/module/components/form/index.js +7 -5
  26. package/lib/module/components/form/index.js.map +1 -1
  27. package/lib/module/components/form/typed-form.js +11 -0
  28. package/lib/module/components/form/typed-form.js.map +1 -0
  29. package/lib/module/components/form/use-form.js +4 -0
  30. package/lib/module/components/form/use-form.js.map +1 -1
  31. package/lib/module/components/heatmap-chart/index.js +27 -1
  32. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  33. package/lib/module/components/line-chart/index.js +18 -1
  34. package/lib/module/components/line-chart/index.js.map +1 -1
  35. package/lib/module/components/message-scroller/index.js +187 -17
  36. package/lib/module/components/message-scroller/index.js.map +1 -1
  37. package/lib/module/components/message-scroller/message-scroller-math.js +43 -0
  38. package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -0
  39. package/lib/module/components/meter/index.js +250 -0
  40. package/lib/module/components/meter/index.js.map +1 -0
  41. package/lib/module/components/meter/meter-scale.js +88 -0
  42. package/lib/module/components/meter/meter-scale.js.map +1 -0
  43. package/lib/module/components/planner/index.js +714 -0
  44. package/lib/module/components/planner/index.js.map +1 -0
  45. package/lib/module/components/planner/planner-entries.js +124 -0
  46. package/lib/module/components/planner/planner-entries.js.map +1 -0
  47. package/lib/module/components/popover/index.js.map +1 -1
  48. package/lib/module/components/radar-chart/index.js +18 -1
  49. package/lib/module/components/radar-chart/index.js.map +1 -1
  50. package/lib/module/components/scatter-chart/index.js +18 -1
  51. package/lib/module/components/scatter-chart/index.js.map +1 -1
  52. package/lib/module/components/selection-mode/index.js +26 -31
  53. package/lib/module/components/selection-mode/index.js.map +1 -1
  54. package/lib/module/components/selection-mode/selection-mode-contracts.js +48 -0
  55. package/lib/module/components/selection-mode/selection-mode-contracts.js.map +1 -0
  56. package/lib/module/components/signature/index.js +62 -20
  57. package/lib/module/components/signature/index.js.map +1 -1
  58. package/lib/module/components/signature/signature-accessibility.js +58 -0
  59. package/lib/module/components/signature/signature-accessibility.js.map +1 -0
  60. package/lib/module/components/skeleton/index.js +45 -7
  61. package/lib/module/components/skeleton/index.js.map +1 -1
  62. package/lib/module/components/slider/index.js +22 -18
  63. package/lib/module/components/slider/index.js.map +1 -1
  64. package/lib/module/components/spinner/index.js +63 -14
  65. package/lib/module/components/spinner/index.js.map +1 -1
  66. package/lib/module/components/time-picker/index.js +46 -21
  67. package/lib/module/components/time-picker/index.js.map +1 -1
  68. package/lib/module/components/time-picker/ruler-window.js +25 -0
  69. package/lib/module/components/time-picker/ruler-window.js.map +1 -0
  70. package/lib/module/hooks/breakpoint-contract.js +29 -0
  71. package/lib/module/hooks/breakpoint-contract.js.map +1 -0
  72. package/lib/module/hooks/index.js +1 -1
  73. package/lib/module/hooks/index.js.map +1 -1
  74. package/lib/module/hooks/use-breakpoint.js +46 -1
  75. package/lib/module/hooks/use-breakpoint.js.map +1 -1
  76. package/lib/module/index.js +3 -1
  77. package/lib/module/index.js.map +1 -1
  78. package/lib/module/primitives/chart-accessibility.js +68 -0
  79. package/lib/module/primitives/chart-accessibility.js.map +1 -0
  80. package/lib/module/primitives/scrim.js +94 -3
  81. package/lib/module/primitives/scrim.js.map +1 -1
  82. package/lib/typescript/src/components/area-chart/index.d.ts +2 -1
  83. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  84. package/lib/typescript/src/components/bar-chart/index.d.ts +2 -1
  85. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/bottom-sheet/index.d.ts +3 -0
  87. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  88. package/lib/typescript/src/components/candlestick-chart/index.d.ts +2 -1
  89. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
  90. package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts +23 -0
  91. package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts.map +1 -0
  92. package/lib/typescript/src/components/carousel/index.d.ts +5 -2
  93. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts +20 -0
  95. package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts.map +1 -0
  96. package/lib/typescript/src/components/context-menu/index.d.ts +18 -8
  97. package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/dialog/index.d.ts +3 -0
  99. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  100. package/lib/typescript/src/components/drawer/index.d.ts +3 -0
  101. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/flow/flow-accessibility.d.ts +8 -0
  103. package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -1
  104. package/lib/typescript/src/components/flow/flow-identifiers.d.ts +24 -0
  105. package/lib/typescript/src/components/flow/flow-identifiers.d.ts.map +1 -0
  106. package/lib/typescript/src/components/flow/index.d.ts +6 -5
  107. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  108. package/lib/typescript/src/components/form/index.d.ts +18 -5
  109. package/lib/typescript/src/components/form/index.d.ts.map +1 -1
  110. package/lib/typescript/src/components/form/typed-form.d.ts +7 -0
  111. package/lib/typescript/src/components/form/typed-form.d.ts.map +1 -0
  112. package/lib/typescript/src/components/form/use-form.d.ts +4 -0
  113. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  114. package/lib/typescript/src/components/heatmap-chart/index.d.ts +2 -1
  115. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/line-chart/index.d.ts +2 -1
  117. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  118. package/lib/typescript/src/components/message-scroller/index.d.ts +30 -2
  119. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  120. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +18 -0
  121. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -0
  122. package/lib/typescript/src/components/meter/index.d.ts +270 -0
  123. package/lib/typescript/src/components/meter/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/meter/meter-scale.d.ts +57 -0
  125. package/lib/typescript/src/components/meter/meter-scale.d.ts.map +1 -0
  126. package/lib/typescript/src/components/planner/index.d.ts +274 -0
  127. package/lib/typescript/src/components/planner/index.d.ts.map +1 -0
  128. package/lib/typescript/src/components/planner/planner-entries.d.ts +75 -0
  129. package/lib/typescript/src/components/planner/planner-entries.d.ts.map +1 -0
  130. package/lib/typescript/src/components/popover/index.d.ts +3 -0
  131. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  132. package/lib/typescript/src/components/radar-chart/index.d.ts +4 -0
  133. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/scatter-chart/index.d.ts +2 -1
  135. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/selection-mode/index.d.ts +6 -4
  137. package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
  138. package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts +18 -0
  139. package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts.map +1 -0
  140. package/lib/typescript/src/components/signature/index.d.ts +6 -0
  141. package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
  142. package/lib/typescript/src/components/signature/signature-accessibility.d.ts +27 -0
  143. package/lib/typescript/src/components/signature/signature-accessibility.d.ts.map +1 -0
  144. package/lib/typescript/src/components/skeleton/index.d.ts +25 -2
  145. package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -1
  146. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/spinner/index.d.ts +25 -2
  148. package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/time-picker/index.d.ts +5 -6
  150. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  151. package/lib/typescript/src/components/time-picker/ruler-window.d.ts +12 -0
  152. package/lib/typescript/src/components/time-picker/ruler-window.d.ts.map +1 -0
  153. package/lib/typescript/src/hooks/breakpoint-contract.d.ts +6 -0
  154. package/lib/typescript/src/hooks/breakpoint-contract.d.ts.map +1 -0
  155. package/lib/typescript/src/hooks/index.d.ts +1 -1
  156. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  157. package/lib/typescript/src/hooks/use-breakpoint.d.ts +22 -0
  158. package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -1
  159. package/lib/typescript/src/index.d.ts +4 -2
  160. package/lib/typescript/src/index.d.ts.map +1 -1
  161. package/lib/typescript/src/primitives/chart-accessibility.d.ts +33 -0
  162. package/lib/typescript/src/primitives/chart-accessibility.d.ts.map +1 -0
  163. package/lib/typescript/src/primitives/scrim.d.ts +5 -3
  164. package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
  165. package/lib/typescript/types/form-api.type-test.d.ts +2 -0
  166. package/lib/typescript/types/form-api.type-test.d.ts.map +1 -0
  167. package/package.json +34 -5
  168. package/src/components/area-chart/index.tsx +27 -2
  169. package/src/components/bar-chart/index.tsx +27 -2
  170. package/src/components/bottom-sheet/index.tsx +3 -0
  171. package/src/components/candlestick-chart/index.tsx +30 -2
  172. package/src/components/carousel/carousel-lifecycle.ts +74 -0
  173. package/src/components/carousel/index.tsx +80 -25
  174. package/src/components/context-menu/context-menu-invocation.ts +44 -0
  175. package/src/components/context-menu/index.tsx +113 -13
  176. package/src/components/dialog/index.tsx +3 -0
  177. package/src/components/drawer/index.tsx +3 -0
  178. package/src/components/flow/flow-accessibility.ts +16 -2
  179. package/src/components/flow/flow-identifiers.ts +98 -0
  180. package/src/components/flow/index.tsx +29 -19
  181. package/src/components/form/index.tsx +32 -5
  182. package/src/components/form/typed-form.ts +13 -0
  183. package/src/components/form/use-form.ts +9 -0
  184. package/src/components/heatmap-chart/index.tsx +49 -3
  185. package/src/components/line-chart/index.tsx +27 -2
  186. package/src/components/message-scroller/index.tsx +273 -13
  187. package/src/components/message-scroller/message-scroller-math.ts +62 -0
  188. package/src/components/meter/index.tsx +318 -0
  189. package/src/components/meter/meter-scale.ts +109 -0
  190. package/src/components/planner/index.tsx +886 -0
  191. package/src/components/planner/planner-entries.ts +146 -0
  192. package/src/components/popover/index.tsx +3 -0
  193. package/src/components/radar-chart/index.tsx +24 -0
  194. package/src/components/scatter-chart/index.tsx +27 -2
  195. package/src/components/selection-mode/index.tsx +37 -30
  196. package/src/components/selection-mode/selection-mode-contracts.ts +53 -0
  197. package/src/components/signature/index.tsx +107 -20
  198. package/src/components/signature/signature-accessibility.ts +67 -0
  199. package/src/components/skeleton/index.tsx +59 -5
  200. package/src/components/slider/index.tsx +28 -18
  201. package/src/components/spinner/index.tsx +80 -13
  202. package/src/components/time-picker/index.tsx +57 -29
  203. package/src/components/time-picker/ruler-window.ts +24 -0
  204. package/src/hooks/breakpoint-contract.ts +31 -0
  205. package/src/hooks/index.ts +4 -0
  206. package/src/hooks/use-breakpoint.ts +61 -10
  207. package/src/index.ts +32 -0
  208. package/src/primitives/chart-accessibility.ts +102 -0
  209. package/src/primitives/scrim.tsx +127 -7
@@ -39,9 +39,18 @@ import {
39
39
  useMemo,
40
40
  useRef,
41
41
  useState,
42
+ type ReactElement,
42
43
  type ReactNode,
43
44
  } from 'react';
44
- import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
45
+ import {
46
+ FlatList,
47
+ View,
48
+ type FlatListProps,
49
+ type LayoutChangeEvent,
50
+ type ListRenderItemInfo,
51
+ type ViewProps,
52
+ type ViewToken,
53
+ } from 'react-native';
45
54
  import Animated, {
46
55
  runOnJS,
47
56
  useAnimatedRef,
@@ -57,9 +66,15 @@ import { ChevronDownIcon } from '../../icons';
57
66
  import { AnimatedPressable } from '../../primitives/animated-pressable';
58
67
  import { cn } from '../../utils/cn';
59
68
  import { textChildren } from '../../primitives/text';
69
+ import {
70
+ distanceFromMessageScrollerTarget,
71
+ initialMessageScrollerIndex,
72
+ isMessageScrollerTargetVisible,
73
+ messageScrollerAnchorAt,
74
+ messageScrollerIndex,
75
+ MESSAGE_SCROLLER_EDGE_THRESHOLD,
76
+ } from './message-scroller-math';
60
77
 
61
- /** Within this many pixels of the bottom still counts as being at the bottom. */
62
- const END_THRESHOLD = 24;
63
78
  /**
64
79
  * How much of the previous turn is left showing above an anchored one. Scrolling
65
80
  * a turn flush to the top reads as content having been cut off; a sliver of the
@@ -69,10 +84,20 @@ const ANCHOR_PEEK = 24;
69
84
 
70
85
  export type MessageScrollerPosition = 'start' | 'end' | 'last-anchor';
71
86
 
87
+ interface MessageScrollerDriver {
88
+ scrollToEnd: (animated: boolean) => void;
89
+ scrollToStart: (animated: boolean) => void;
90
+ scrollToMessage: (id: string, animated: boolean) => boolean;
91
+ }
92
+
72
93
  interface MessageScrollerContextValue {
73
94
  scrollRef: ReturnType<typeof useAnimatedRef<Animated.ScrollView>>;
74
- /** 1 while the reader is at the live edge, 0 otherwise. Drives the button. */
95
+ registerDriver: (driver: MessageScrollerDriver | null) => void;
96
+ /** 1 while the reader is at the live edge, 0 otherwise. Drives follow state. */
75
97
  atEnd: ReturnType<typeof useSharedValue<number>>;
98
+ /** Live distance from each edge, used by target-specific jump controls. */
99
+ distanceFromStart: ReturnType<typeof useSharedValue<number>>;
100
+ distanceFromEnd: ReturnType<typeof useSharedValue<number>>;
76
101
  atEndJS: boolean;
77
102
  setAtEndJS: (atEnd: boolean) => void;
78
103
  autoScroll: boolean;
@@ -150,12 +175,21 @@ function MessageScrollerRoot({
150
175
  }: MessageScrollerProps) {
151
176
  const scrollRef = useAnimatedRef<Animated.ScrollView>();
152
177
  const atEnd = useSharedValue(1);
178
+ const awayFromEdge = MESSAGE_SCROLLER_EDGE_THRESHOLD + 1;
179
+ const distanceFromStart = useSharedValue(
180
+ defaultScrollPosition === 'start' ? 0 : awayFromEdge
181
+ );
182
+ const distanceFromEnd = useSharedValue(defaultScrollPosition === 'end' ? 0 : awayFromEdge);
153
183
  const [atEndJS, setAtEndJS] = useState(true);
154
184
  const [currentAnchorId, setCurrentAnchorId] = useState<string | null>(null);
155
185
 
156
186
  const itemY = useRef(new Map<string, number>());
157
187
  const anchors = useRef<string[]>([]);
158
188
  const following = useRef(autoScroll);
189
+ const driver = useRef<MessageScrollerDriver | null>(null);
190
+ const registerDriver = useCallback((next: MessageScrollerDriver | null) => {
191
+ driver.current = next;
192
+ }, []);
159
193
 
160
194
  const registerItem = useCallback((id: string, y: number, anchor: boolean) => {
161
195
  itemY.current.set(id, y);
@@ -172,7 +206,8 @@ function MessageScrollerRoot({
172
206
  const scrollToEnd = useCallback(
173
207
  (animated = true) => {
174
208
  following.current = true;
175
- scrollRef.current?.scrollToEnd({ animated });
209
+ if (driver.current) driver.current.scrollToEnd(animated);
210
+ else scrollRef.current?.scrollToEnd({ animated });
176
211
  },
177
212
  [scrollRef]
178
213
  );
@@ -180,17 +215,23 @@ function MessageScrollerRoot({
180
215
  const scrollToStart = useCallback(
181
216
  (animated = true) => {
182
217
  following.current = false;
183
- scrollRef.current?.scrollTo({ y: 0, animated });
218
+ if (driver.current) driver.current.scrollToStart(animated);
219
+ else scrollRef.current?.scrollTo({ y: 0, animated });
184
220
  },
185
221
  [scrollRef]
186
222
  );
187
223
 
188
224
  const scrollToMessage = useCallback(
189
225
  (id: string, animated = true) => {
226
+ if (driver.current) {
227
+ if (driver.current.scrollToMessage(id, animated)) following.current = false;
228
+ return;
229
+ }
190
230
  const y = itemY.current.get(id);
191
- if (y === undefined) return;
192
- following.current = false;
193
- scrollRef.current?.scrollTo({ y: Math.max(0, y - ANCHOR_PEEK), animated });
231
+ if (y !== undefined) {
232
+ following.current = false;
233
+ scrollRef.current?.scrollTo({ y: Math.max(0, y - ANCHOR_PEEK), animated });
234
+ }
194
235
  },
195
236
  [scrollRef]
196
237
  );
@@ -198,7 +239,10 @@ function MessageScrollerRoot({
198
239
  const context = useMemo<MessageScrollerContextValue>(
199
240
  () => ({
200
241
  scrollRef,
242
+ registerDriver,
201
243
  atEnd,
244
+ distanceFromStart,
245
+ distanceFromEnd,
202
246
  atEndJS,
203
247
  setAtEndJS,
204
248
  autoScroll,
@@ -217,7 +261,10 @@ function MessageScrollerRoot({
217
261
  }),
218
262
  [
219
263
  scrollRef,
264
+ registerDriver,
220
265
  atEnd,
266
+ distanceFromStart,
267
+ distanceFromEnd,
221
268
  atEndJS,
222
269
  autoScroll,
223
270
  preserveScrollOnPrepend,
@@ -268,6 +315,8 @@ function MessageScrollerViewport({
268
315
  const {
269
316
  scrollRef,
270
317
  atEnd,
318
+ distanceFromStart,
319
+ distanceFromEnd,
271
320
  setAtEndJS,
272
321
  autoScroll,
273
322
  preserveScrollOnPrepend,
@@ -310,8 +359,19 @@ function MessageScrollerViewport({
310
359
  const { contentOffset, contentSize, layoutMeasurement } = event;
311
360
  offsetY.value = contentOffset.y;
312
361
 
313
- const distance = contentSize.height - layoutMeasurement.height - contentOffset.y;
314
- const next = distance <= END_THRESHOLD ? 1 : 0;
362
+ distanceFromStart.value = distanceFromMessageScrollerTarget(
363
+ 'start',
364
+ contentOffset.y,
365
+ contentSize.height,
366
+ layoutMeasurement.height
367
+ );
368
+ distanceFromEnd.value = distanceFromMessageScrollerTarget(
369
+ 'end',
370
+ contentOffset.y,
371
+ contentSize.height,
372
+ layoutMeasurement.height
373
+ );
374
+ const next = isMessageScrollerTargetVisible(distanceFromEnd.value) ? 0 : 1;
315
375
  if (next !== atEnd.value) {
316
376
  atEnd.value = next;
317
377
  runOnJS(publishAtEnd)(next === 1);
@@ -411,6 +471,201 @@ function MessageScrollerViewport({
411
471
  }
412
472
  MessageScrollerViewport.displayName = 'MessageScroller.Viewport';
413
473
 
474
+ export interface MessageScrollerListItem {
475
+ /** Stable id used by `scrollToMessage` and as the default React key. */
476
+ messageId: string;
477
+ /** Marks the start of a turn for `last-anchor` and visibility reporting. */
478
+ scrollAnchor?: boolean;
479
+ }
480
+
481
+ export interface MessageScrollerListProps<T extends MessageScrollerListItem = MessageScrollerListItem>
482
+ extends Omit<
483
+ FlatListProps<T>,
484
+ | 'data'
485
+ | 'renderItem'
486
+ | 'keyExtractor'
487
+ | 'onScroll'
488
+ | 'onContentSizeChange'
489
+ | 'onViewableItemsChanged'
490
+ | 'maintainVisibleContentPosition'
491
+ | 'onScrollToIndexFailed'
492
+ | 'onScrollEndDrag'
493
+ | 'onMomentumScrollEnd'
494
+ | 'CellRendererComponent'
495
+ > {
496
+ className?: string;
497
+ /** Classes on the virtualized list's padded transcript column. */
498
+ contentContainerClassName?: string;
499
+ /**
500
+ * The complete transcript. Rows outside the native render window stay
501
+ * unmounted; each item therefore carries its stable navigation metadata.
502
+ */
503
+ data: readonly T[];
504
+ /** Draw one turn from the native list window. */
505
+ renderItem: (info: ListRenderItemInfo<T>) => ReactElement | null;
506
+ }
507
+
508
+ /**
509
+ * The real virtualized transcript path. Only its render window mounts; native
510
+ * visible-content maintenance keeps prepends still without measuring offscreen
511
+ * rows in JavaScript.
512
+ */
513
+ function MessageScrollerList<T extends MessageScrollerListItem>({
514
+ className,
515
+ contentContainerClassName,
516
+ data,
517
+ renderItem,
518
+ initialNumToRender = 12,
519
+ maxToRenderPerBatch = 8,
520
+ windowSize = 7,
521
+ ...props
522
+ }: MessageScrollerListProps<T>) {
523
+ const {
524
+ atEnd,
525
+ distanceFromStart,
526
+ distanceFromEnd,
527
+ setAtEndJS,
528
+ autoScroll,
529
+ preserveScrollOnPrepend,
530
+ defaultScrollPosition,
531
+ following,
532
+ registerDriver,
533
+ setCurrentAnchorId,
534
+ } = useScroller('MessageScroller.List');
535
+ const listRef = useRef<FlatList<T>>(null);
536
+ const items = useRef(data);
537
+ items.current = data;
538
+ const opened = useRef(false);
539
+ const firstVisibleIndex = useRef<number | null>(null);
540
+
541
+ const publishAtEnd = useCallback(
542
+ (next: boolean) => {
543
+ following.current = autoScroll && next;
544
+ setAtEndJS(next);
545
+ },
546
+ [autoScroll, following, setAtEndJS]
547
+ );
548
+
549
+ const scrollHandler = useAnimatedScrollHandler({
550
+ onScroll: (event) => {
551
+ const { contentOffset, contentSize, layoutMeasurement } = event;
552
+ distanceFromStart.value = distanceFromMessageScrollerTarget(
553
+ 'start',
554
+ contentOffset.y,
555
+ contentSize.height,
556
+ layoutMeasurement.height
557
+ );
558
+ distanceFromEnd.value = distanceFromMessageScrollerTarget(
559
+ 'end',
560
+ contentOffset.y,
561
+ contentSize.height,
562
+ layoutMeasurement.height
563
+ );
564
+ const next = isMessageScrollerTargetVisible(distanceFromEnd.value) ? 0 : 1;
565
+ if (next !== atEnd.value) {
566
+ atEnd.value = next;
567
+ runOnJS(publishAtEnd)(next === 1);
568
+ }
569
+ },
570
+ });
571
+
572
+ const scrollToIndex = useCallback((index: number, animated: boolean) => {
573
+ listRef.current?.scrollToIndex({ index, viewOffset: ANCHOR_PEEK, animated });
574
+ }, []);
575
+
576
+ const setAnchorAtIndex = useCallback(
577
+ (index: number) => setCurrentAnchorId(messageScrollerAnchorAt(items.current, index)),
578
+ [setCurrentAnchorId]
579
+ );
580
+
581
+ useEffect(() => {
582
+ registerDriver({
583
+ scrollToEnd: (animated) => {
584
+ listRef.current?.scrollToEnd({ animated });
585
+ setAnchorAtIndex(items.current.length - 1);
586
+ },
587
+ scrollToStart: (animated) => {
588
+ listRef.current?.scrollToOffset({ offset: 0, animated });
589
+ setAnchorAtIndex(0);
590
+ },
591
+ scrollToMessage: (id, animated) => {
592
+ const index = messageScrollerIndex(items.current, id);
593
+ if (index === undefined) return false;
594
+ scrollToIndex(index, animated);
595
+ setAnchorAtIndex(index);
596
+ return true;
597
+ },
598
+ });
599
+ return () => registerDriver(null);
600
+ }, [registerDriver, scrollToIndex, setAnchorAtIndex]);
601
+
602
+ useEffect(() => {
603
+ following.current = autoScroll && atEnd.value === 1;
604
+ }, [atEnd, autoScroll, following]);
605
+
606
+ const onContentSizeChange = () => {
607
+ if (!opened.current) {
608
+ opened.current = true;
609
+ const index = initialMessageScrollerIndex(data, defaultScrollPosition);
610
+ const target = index ?? data.length - 1;
611
+ if (index === undefined) listRef.current?.scrollToEnd({ animated: false });
612
+ else if (index > 0) scrollToIndex(index, false);
613
+ setAnchorAtIndex(target);
614
+ return;
615
+ }
616
+ if (following.current) listRef.current?.scrollToEnd({ animated: true });
617
+ };
618
+
619
+ const onViewableItemsChanged = useRef(
620
+ ({ viewableItems }: { viewableItems: ViewToken<T>[] }) => {
621
+ const first = viewableItems.reduce(
622
+ (best, token) => (token.index !== null && token.index < best ? token.index : best),
623
+ Infinity
624
+ );
625
+ firstVisibleIndex.current = Number.isFinite(first) ? first : null;
626
+ }
627
+ ).current;
628
+
629
+ const settleAnchor = () => {
630
+ if (firstVisibleIndex.current !== null) {
631
+ setCurrentAnchorId(messageScrollerAnchorAt(items.current, firstVisibleIndex.current));
632
+ }
633
+ };
634
+
635
+ return (
636
+ <Animated.FlatList
637
+ ref={listRef}
638
+ data={data}
639
+ renderItem={renderItem}
640
+ keyExtractor={(item) => item.messageId}
641
+ accessibilityRole="list"
642
+ accessibilityLabel="Messages"
643
+ accessibilityLiveRegion="polite"
644
+ scrollEventThrottle={16}
645
+ showsVerticalScrollIndicator={false}
646
+ initialNumToRender={initialNumToRender}
647
+ maxToRenderPerBatch={maxToRenderPerBatch}
648
+ windowSize={windowSize}
649
+ maintainVisibleContentPosition={
650
+ preserveScrollOnPrepend ? { minIndexForVisible: 0 } : undefined
651
+ }
652
+ className={cn('flex-1', className)}
653
+ contentContainerClassName={cn('gap-3 p-4 pb-16', contentContainerClassName)}
654
+ {...props}
655
+ onScroll={scrollHandler}
656
+ onContentSizeChange={onContentSizeChange}
657
+ onViewableItemsChanged={onViewableItemsChanged}
658
+ onScrollEndDrag={settleAnchor}
659
+ onMomentumScrollEnd={settleAnchor}
660
+ onScrollToIndexFailed={({ index, averageItemLength }) => {
661
+ listRef.current?.scrollToOffset({ offset: index * averageItemLength, animated: false });
662
+ setTimeout(() => scrollToIndex(index, false), 0);
663
+ }}
664
+ />
665
+ );
666
+ }
667
+ MessageScrollerList.displayName = 'MessageScroller.List';
668
+
414
669
  /**
415
670
  * How far the transcript shifted, measured on the message closest to the top
416
671
  * that was there before and is still there.
@@ -515,12 +770,16 @@ function MessageScrollerButton({
515
770
  children,
516
771
  ...props
517
772
  }: MessageScrollerButtonProps) {
518
- const { atEnd, scrollToEnd, scrollToStart } = useScroller('MessageScroller.Button');
773
+ const { distanceFromStart, distanceFromEnd, scrollToEnd, scrollToStart } =
774
+ useScroller('MessageScroller.Button');
519
775
  const tint = useCSSVariable('--color-foreground');
520
776
 
777
+ const targetDistance = target === 'end' ? distanceFromEnd : distanceFromStart;
521
778
  // Shown when away from the edge the button points at.
522
779
  const shown = useDerivedValue(() =>
523
- withTiming(target === 'end' ? 1 - atEnd.value : atEnd.value, { duration: 160 })
780
+ withTiming(isMessageScrollerTargetVisible(targetDistance.value) ? 1 : 0, {
781
+ duration: 160,
782
+ })
524
783
  );
525
784
 
526
785
  const style = useAnimatedStyle(() => ({
@@ -569,6 +828,7 @@ MessageScrollerButton.displayName = 'MessageScroller.Button';
569
828
 
570
829
  export const MessageScroller = Object.assign(MessageScrollerRoot, {
571
830
  Viewport: MessageScrollerViewport,
831
+ List: MessageScrollerList,
572
832
  Content: MessageScrollerContent,
573
833
  Item: MessageScrollerItem,
574
834
  Button: MessageScrollerButton,
@@ -0,0 +1,62 @@
1
+ type MessageScrollerTarget = 'start' | 'end';
2
+
3
+ export const MESSAGE_SCROLLER_EDGE_THRESHOLD = 24;
4
+
5
+ /** Distance from the viewport to the edge a jump button targets. */
6
+ export function distanceFromMessageScrollerTarget(
7
+ target: MessageScrollerTarget,
8
+ contentOffsetY: number,
9
+ contentHeight: number,
10
+ viewportHeight: number
11
+ ): number {
12
+ 'worklet';
13
+
14
+ const maximumOffset = Math.max(0, contentHeight - viewportHeight);
15
+ const boundedOffset = Math.min(maximumOffset, Math.max(0, contentOffsetY));
16
+ return target === 'start' ? boundedOffset : maximumOffset - boundedOffset;
17
+ }
18
+
19
+ /** Whether the control has enough distance to its target edge to be useful. */
20
+ export function isMessageScrollerTargetVisible(distance: number): boolean {
21
+ 'worklet';
22
+
23
+ return distance > MESSAGE_SCROLLER_EDGE_THRESHOLD;
24
+ }
25
+
26
+ export interface MessageScrollerIndexedItem {
27
+ messageId: string;
28
+ scrollAnchor?: boolean;
29
+ }
30
+
31
+ /** Index used by initial positioning and id-addressed virtualized jumps. */
32
+ export function messageScrollerIndex(
33
+ items: readonly MessageScrollerIndexedItem[],
34
+ id: string
35
+ ): number | undefined {
36
+ const index = items.findIndex((item) => item.messageId === id);
37
+ return index < 0 ? undefined : index;
38
+ }
39
+
40
+ /** Initial target for a list; `undefined` means use the physical end. */
41
+ export function initialMessageScrollerIndex(
42
+ items: readonly MessageScrollerIndexedItem[],
43
+ position: 'start' | 'end' | 'last-anchor'
44
+ ): number | undefined {
45
+ if (position === 'end') return undefined;
46
+ if (position === 'start') return items.length ? 0 : undefined;
47
+ for (let index = items.length - 1; index >= 0; index -= 1) {
48
+ if (items[index]!.scrollAnchor) return index;
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ /** Anchor at or before the first visible virtualized row. */
54
+ export function messageScrollerAnchorAt(
55
+ items: readonly MessageScrollerIndexedItem[],
56
+ firstVisibleIndex: number
57
+ ): string | null {
58
+ for (let index = Math.min(firstVisibleIndex, items.length - 1); index >= 0; index -= 1) {
59
+ if (items[index]!.scrollAnchor) return items[index]!.messageId;
60
+ }
61
+ return null;
62
+ }