@vvedantb/native-ui 0.17.2 → 0.18.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 (220) hide show
  1. package/dist/AlertDialog.jsx +123 -0
  2. package/dist/AmbientBackground.jsx +73 -0
  3. package/dist/ArtifactList.jsx +59 -0
  4. package/dist/Avatar.jsx +285 -0
  5. package/dist/Badge.jsx +154 -0
  6. package/dist/BottomSheetContainer.jsx +208 -0
  7. package/dist/BulkActionBar.jsx +717 -0
  8. package/dist/Button.jsx +344 -0
  9. package/dist/CaptureButton.jsx +104 -0
  10. package/dist/Card.jsx +371 -0
  11. package/dist/Checkbox.jsx +177 -0
  12. package/dist/Chip.jsx +79 -0
  13. package/dist/ColorSwatches.jsx +159 -0
  14. package/dist/ConfirmDialog.jsx +157 -0
  15. package/dist/DateTimeField.jsx +204 -0
  16. package/dist/Dialog.jsx +614 -0
  17. package/dist/DividedList.jsx +120 -0
  18. package/dist/DropdownMenu.jsx +849 -0
  19. package/dist/EmptyState.jsx +434 -0
  20. package/dist/ExpoWebRoot.jsx +134 -0
  21. package/dist/FloatingFAB.jsx +95 -0
  22. package/dist/FloatingSurface.jsx +226 -0
  23. package/dist/FloatingTabBar.jsx +403 -0
  24. package/dist/HeaderIconButton.jsx +53 -0
  25. package/dist/IconButton.jsx +157 -0
  26. package/dist/Input.jsx +96 -0
  27. package/dist/Label.jsx +96 -0
  28. package/dist/LabeledSwitchRow.jsx +170 -0
  29. package/dist/LiquidGlass.jsx +2 -0
  30. package/dist/ListRow.jsx +281 -0
  31. package/dist/LoadingState.jsx +205 -0
  32. package/dist/PageIndicator.jsx +128 -0
  33. package/dist/PageTitle.jsx +205 -0
  34. package/dist/Pager.jsx +105 -0
  35. package/dist/PasswordInput.jsx +120 -0
  36. package/dist/PillTabs.jsx +300 -0
  37. package/dist/PinnedBotTile.jsx +212 -0
  38. package/dist/Progress.jsx +147 -0
  39. package/dist/SafeAreaContainer.jsx +38 -0
  40. package/dist/ScreenContainer.jsx +117 -0
  41. package/dist/SearchInput.jsx +150 -0
  42. package/dist/SectionLabel.jsx +128 -0
  43. package/dist/Select.jsx +223 -0
  44. package/dist/Separator.jsx +95 -0
  45. package/dist/Sheet.jsx +343 -0
  46. package/dist/SheetRow.jsx +198 -0
  47. package/dist/Skeleton.jsx +311 -0
  48. package/dist/Spinner.jsx +195 -0
  49. package/dist/StackPageHeader.jsx +160 -0
  50. package/dist/StatGrid.jsx +74 -0
  51. package/dist/StatusDot.jsx +129 -0
  52. package/dist/SurfaceRow.jsx +143 -0
  53. package/dist/SwipeableRow.jsx +50 -0
  54. package/dist/Switch.jsx +130 -0
  55. package/dist/Tabs.jsx +385 -0
  56. package/dist/Textarea.jsx +103 -0
  57. package/dist/ThemeToggle.jsx +78 -0
  58. package/dist/Toast.jsx +396 -0
  59. package/dist/ai/activity-step-detail.jsx +179 -0
  60. package/dist/ai/activity-step-label.js +816 -0
  61. package/dist/ai/activity-step-types.js +14 -0
  62. package/dist/ai/activity-step.jsx +98 -0
  63. package/dist/ai/activity-tasks-utils.js +172 -0
  64. package/dist/ai/activity-timeline.js +47 -0
  65. package/dist/ai/attachments.jsx +258 -0
  66. package/dist/ai/card-button.jsx +188 -0
  67. package/dist/ai/chain-of-thought.jsx +459 -0
  68. package/dist/ai/chat-jump-rail.jsx +57 -0
  69. package/dist/ai/chat-text.js +44 -0
  70. package/dist/ai/chat-thread.jsx +135 -0
  71. package/dist/ai/choice-question.jsx +925 -0
  72. package/dist/ai/code-block.jsx +574 -0
  73. package/dist/ai/collapsible.jsx +275 -0
  74. package/dist/ai/conversation.jsx +723 -0
  75. package/dist/ai/file-chip.jsx +198 -0
  76. package/dist/ai/icon-button.jsx +179 -0
  77. package/dist/ai/index.js +37 -0
  78. package/dist/ai/message-action-sheet.jsx +408 -0
  79. package/dist/ai/message-quote.jsx +241 -0
  80. package/dist/ai/message.jsx +739 -0
  81. package/dist/ai/prompt-input-menu.jsx +365 -0
  82. package/dist/ai/prompt-input.jsx +626 -0
  83. package/dist/ai/reasoning.jsx +446 -0
  84. package/dist/ai/reply-quote-chip.jsx +278 -0
  85. package/dist/ai/response-text.jsx +130 -0
  86. package/dist/ai/shimmer.jsx +149 -0
  87. package/dist/ai/sources.jsx +260 -0
  88. package/dist/ai/subagent-cards.jsx +645 -0
  89. package/dist/ai/suggestion.jsx +186 -0
  90. package/dist/ai/tool-approval-card.jsx +359 -0
  91. package/dist/ai/trace.jsx +728 -0
  92. package/dist/ai/unified-diff.jsx +280 -0
  93. package/dist/ai/use-streaming-disclosure.js +70 -0
  94. package/dist/auth/AuthProviders.jsx +27 -0
  95. package/dist/auth/AuthScreen.jsx +292 -0
  96. package/dist/auth/GoogleSignInButton.jsx +188 -0
  97. package/dist/auth/SecondFactorScreen.jsx +133 -0
  98. package/dist/auth/SignInScreen.jsx +482 -0
  99. package/dist/auth/authErrors.js +131 -0
  100. package/dist/auth/index.js +11 -0
  101. package/dist/auth/totp.js +378 -0
  102. package/dist/blur.js +36 -0
  103. package/dist/brand/AppIcons.jsx +118 -0
  104. package/dist/brand/AppMark.jsx +128 -0
  105. package/dist/brand/AppWordmark.jsx +138 -0
  106. package/dist/brand/GoogleMark.jsx +71 -0
  107. package/dist/brand/ProviderIcon.jsx +192 -0
  108. package/dist/brand/familyApps.js +33 -0
  109. package/dist/brand/index.js +11 -0
  110. package/dist/brand/svg.js +252 -0
  111. package/dist/brand/vProAppBenefits.js +19 -0
  112. package/dist/cn.js +5 -0
  113. package/dist/colors.js +15 -0
  114. package/dist/configureTheme.js +72 -0
  115. package/dist/controlStyles.js +520 -0
  116. package/dist/definiteSize.js +12 -0
  117. package/dist/dialogLayout.js +18 -0
  118. package/dist/entries/ai.js +4 -0
  119. package/dist/entries/ambient.js +3 -0
  120. package/dist/entries/auth.js +5 -0
  121. package/dist/entries/bottom-sheet.js +2 -0
  122. package/dist/entries/brand.js +2 -0
  123. package/dist/entries/button.js +2 -0
  124. package/dist/entries/date-time.js +3 -0
  125. package/dist/entries/expo-router.js +4 -0
  126. package/dist/entries/expo-web-root.js +3 -0
  127. package/dist/entries/floating.js +7 -0
  128. package/dist/entries/gesture-handler.js +4 -0
  129. package/dist/entries/keyboard-controller.js +4 -0
  130. package/dist/entries/layout.js +16 -0
  131. package/dist/entries/markdown.js +4 -0
  132. package/dist/entries/marketing.js +3 -0
  133. package/dist/entries/page-title.js +7 -0
  134. package/dist/entries/password-input.js +3 -0
  135. package/dist/entries/primitives.js +57 -0
  136. package/dist/entries/search-input.js +3 -0
  137. package/dist/entries/tab-bar.js +4 -0
  138. package/dist/entries/theme.js +7 -0
  139. package/dist/entries/toast.js +5 -0
  140. package/dist/entries/video.js +3 -0
  141. package/dist/entries/webview.js +4 -0
  142. package/dist/examples/AiChatGallery.jsx +509 -0
  143. package/dist/examples/ParityGallery.jsx +1307 -0
  144. package/dist/floating.js +62 -0
  145. package/dist/glassBlur.jsx +145 -0
  146. package/dist/gradient.js +17 -0
  147. package/dist/haptics.js +44 -0
  148. package/dist/highlight.js +382 -0
  149. package/dist/index.js +85 -0
  150. package/dist/initials.js +4 -0
  151. package/dist/loadingGrid.js +81 -0
  152. package/dist/markdown/Markdown.jsx +515 -0
  153. package/dist/markdown/MarkdownTable.jsx +443 -0
  154. package/dist/markdown/tableText.js +68 -0
  155. package/dist/marketing/PricingModal.jsx +608 -0
  156. package/dist/marketing/UpgradeToProCard.jsx +164 -0
  157. package/dist/marketing/pricing.js +34 -0
  158. package/dist/media/ImageGallery.jsx +580 -0
  159. package/dist/media/MediaCard.jsx +162 -0
  160. package/dist/media/imageGrid.js +54 -0
  161. package/dist/motion.js +33 -0
  162. package/dist/pagerSwipe.js +104 -0
  163. package/dist/palette.js +104 -0
  164. package/dist/primitives/button.jsx +127 -0
  165. package/dist/primitives/card.jsx +249 -0
  166. package/dist/primitives/icon.jsx +37 -0
  167. package/dist/primitives/input.jsx +56 -0
  168. package/dist/primitives/keyboard-controller.jsx +207 -0
  169. package/dist/primitives/keyboard-parts.jsx +75 -0
  170. package/dist/primitives/native-only-animated-view.jsx +68 -0
  171. package/dist/primitives/overlay.jsx +391 -0
  172. package/dist/primitives/pager-gesture.jsx +131 -0
  173. package/dist/primitives/select-host.jsx +103 -0
  174. package/dist/primitives/select.jsx +555 -0
  175. package/dist/primitives/sheet-drag.jsx +420 -0
  176. package/dist/primitives/sheet-gesture.jsx +219 -0
  177. package/dist/primitives/swipe-row-gesture.jsx +341 -0
  178. package/dist/primitives/switch.jsx +105 -0
  179. package/dist/primitives/text.jsx +145 -0
  180. package/dist/primitives/toast-drag-gesture.jsx +94 -0
  181. package/dist/primitives/use-controllable-state.js +40 -0
  182. package/dist/primitives/web-click-guard.js +69 -0
  183. package/dist/push.js +166 -0
  184. package/dist/relativeTime.js +70 -0
  185. package/dist/router/RouterStackPageHeader.jsx +53 -0
  186. package/dist/selectA11y.js +32 -0
  187. package/dist/settings/MoreFromProjectV.jsx +168 -0
  188. package/dist/settings/SettingsEmptyState.jsx +102 -0
  189. package/dist/settings/SettingsField.jsx +49 -0
  190. package/dist/settings/SettingsSection.jsx +101 -0
  191. package/dist/settings/SettingsStack.jsx +17 -0
  192. package/dist/settings/SettingsToggleRow.jsx +163 -0
  193. package/dist/settings/projectVMobileApps.js +126 -0
  194. package/dist/settings/relaxedSizeStyle.js +11 -0
  195. package/dist/sheetDrag.js +76 -0
  196. package/dist/svgA11y.js +46 -0
  197. package/dist/swipeRow.js +161 -0
  198. package/dist/tabBarRoutes.js +41 -0
  199. package/dist/tabIndicator.js +139 -0
  200. package/dist/tailwind-preset.cjs +255 -0
  201. package/dist/tailwind-preset.d.cts +55 -0
  202. package/dist/theme.jsx +62 -0
  203. package/dist/toastChrome.js +110 -0
  204. package/dist/toastDrag.js +51 -0
  205. package/dist/toastStore.js +54 -0
  206. package/dist/tokens.js +70 -0
  207. package/dist/typography.js +40 -0
  208. package/dist/useReduceMotion.js +32 -0
  209. package/dist/useReduceTransparency.js +27 -0
  210. package/dist/useScheme.js +15 -0
  211. package/dist/video/InlineVideo.jsx +115 -0
  212. package/dist/webview/HtmlView.jsx +169 -0
  213. package/dist/webview/HtmlView.web.jsx +63 -0
  214. package/dist/webview/htmlView.js +43 -0
  215. package/package.json +70 -33
  216. package/src/DropdownMenu.tsx +28 -16
  217. package/src/FloatingTabBar.tsx +78 -41
  218. package/src/ai/conversation.tsx +12 -8
  219. package/src/glassBlur.tsx +47 -22
  220. package/src/primitives/swipe-row-gesture.tsx +11 -2
@@ -0,0 +1,723 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { createContext, isValidElement, useContext, useRef, useState } from "react";
3
+ import { FlatList, Platform, Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
4
+ import { IconArrowDown } from "@tabler/icons-react-native";
5
+ import { FadeIn, FadeOut } from "react-native-reanimated";
6
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
7
+ import { EASE_OUT } from "../motion";
8
+ import { Button } from "../Button";
9
+ import { cn } from "../cn";
10
+ import { MIN_TOUCH_TARGET, sizeStyle, textStyle, touchSlop, typeStyle } from "../controlStyles";
11
+ import { withAlpha } from "../palette";
12
+ import { KeyboardLiftContext, useKeyboardParts } from "../primitives/keyboard-parts";
13
+ import { NativeOnlyAnimatedView } from "../primitives/native-only-animated-view";
14
+ import { useLargerText } from "../theme";
15
+ import { tokens } from "../tokens";
16
+ import { useReduceMotion } from "../useReduceMotion";
17
+ import { useScheme } from "../useScheme";
18
+ import { chatTextSize } from "./chat-text";
19
+
20
+ /** Within this many points of the end, the thread counts as at the bottom and keeps following new content. */
21
+ export const AT_BOTTOM_THRESHOLD = 70;
22
+
23
+ /**
24
+ * Whether a scroll position is within `AT_BOTTOM_THRESHOLD` of the end.
25
+ * Content shorter than the view is at the bottom. `bottomInset` is space
26
+ * added past the content (the keyboard's, with `KeyboardControllerProvider`).
27
+ * An `inverted` list (newest first) has its end at offset 0, and there
28
+ * `bottomInset` is space added before offset 0 (the keyboard's on iOS), so
29
+ * the end is at minus `bottomInset`.
30
+ */
31
+ export function isScrolledToBottom({
32
+ contentOffset,
33
+ contentSize,
34
+ layoutMeasurement
35
+ }, bottomInset = 0, inverted = false) {
36
+ if (inverted) return contentOffset.y + bottomInset <= AT_BOTTOM_THRESHOLD;
37
+ return contentSize.height + bottomInset - layoutMeasurement.height - contentOffset.y <= AT_BOTTOM_THRESHOLD;
38
+ }
39
+
40
+ /**
41
+ * What the thread scrolls with: the `ScrollView` of `ConversationContent` or
42
+ * the `FlatList` of `ConversationList`. `scrollToEnd` goes to the newest
43
+ * message, which is offset 0 in an inverted list. `scrollTo` goes to an
44
+ * offset; only the `ScrollView` has it, so call it as `scrollTo?.(...)`.
45
+ */
46
+
47
+ const ConversationContext = createContext(null);
48
+
49
+ /** `isAtBottom` and `scrollToBottom` of the enclosing `Conversation`, for a custom scroll button. */
50
+ export function useConversation() {
51
+ const context = useContext(ConversationContext);
52
+ if (!context) {
53
+ throw new Error("Conversation parts must be used within Conversation");
54
+ }
55
+ return context;
56
+ }
57
+ /**
58
+ * The thread frame (web `relative flex-1`). It sticks to the bottom: while the
59
+ * reader is at the end, new and growing messages stay in view; once they
60
+ * scroll up, it stops following and `ConversationScrollButton` shows.
61
+ */
62
+ export function Conversation(t0) {
63
+ const $ = _c(21);
64
+ let className;
65
+ let props;
66
+ if ($[0] !== t0) {
67
+ ({
68
+ className,
69
+ ...props
70
+ } = t0);
71
+ $[0] = t0;
72
+ $[1] = className;
73
+ $[2] = props;
74
+ } else {
75
+ className = $[1];
76
+ props = $[2];
77
+ }
78
+ const reduceMotion = useReduceMotion();
79
+ const scrollRef = useRef(null);
80
+ const atBottom = useRef(true);
81
+ const autoScrolling = useRef(false);
82
+ const [isAtBottom, setIsAtBottom] = useState(true);
83
+ const [dockHeight, setDockHeight] = useState(0);
84
+ let t1;
85
+ if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
86
+ t1 = next => {
87
+ atBottom.current = next;
88
+ setIsAtBottom(next);
89
+ };
90
+ $[3] = t1;
91
+ } else {
92
+ t1 = $[3];
93
+ }
94
+ const update = t1;
95
+ let t2;
96
+ if ($[4] !== reduceMotion) {
97
+ t2 = () => {
98
+ autoScrolling.current = true;
99
+ update(true);
100
+ scrollRef.current?.scrollToEnd({
101
+ animated: !reduceMotion
102
+ });
103
+ };
104
+ $[4] = reduceMotion;
105
+ $[5] = t2;
106
+ } else {
107
+ t2 = $[5];
108
+ }
109
+ let t3;
110
+ let t4;
111
+ let t5;
112
+ if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
113
+ t3 = (event, bottomInset, inverted) => {
114
+ const bottom = isScrolledToBottom(event, bottomInset, inverted);
115
+ if (bottom) {
116
+ autoScrolling.current = false;
117
+ } else {
118
+ if (autoScrolling.current) {
119
+ return;
120
+ }
121
+ }
122
+ update(bottom);
123
+ };
124
+ t4 = () => {
125
+ autoScrolling.current = false;
126
+ };
127
+ t5 = () => {
128
+ if (atBottom.current) {
129
+ scrollRef.current?.scrollToEnd({
130
+ animated: false
131
+ });
132
+ }
133
+ };
134
+ $[6] = t3;
135
+ $[7] = t4;
136
+ $[8] = t5;
137
+ } else {
138
+ t3 = $[6];
139
+ t4 = $[7];
140
+ t5 = $[8];
141
+ }
142
+ let t6;
143
+ if ($[9] !== dockHeight || $[10] !== isAtBottom || $[11] !== t2) {
144
+ t6 = {
145
+ isAtBottom,
146
+ scrollRef,
147
+ scrollToBottom: t2,
148
+ onScroll: t3,
149
+ onUserScroll: t4,
150
+ followContent: t5,
151
+ dockHeight,
152
+ setDockHeight
153
+ };
154
+ $[9] = dockHeight;
155
+ $[10] = isAtBottom;
156
+ $[11] = t2;
157
+ $[12] = t6;
158
+ } else {
159
+ t6 = $[12];
160
+ }
161
+ const value = t6;
162
+ let t7;
163
+ if ($[13] !== className) {
164
+ t7 = cn("relative flex-1", className);
165
+ $[13] = className;
166
+ $[14] = t7;
167
+ } else {
168
+ t7 = $[14];
169
+ }
170
+ let t8;
171
+ if ($[15] !== props || $[16] !== t7) {
172
+ t8 = <View className={t7} {...props} />;
173
+ $[15] = props;
174
+ $[16] = t7;
175
+ $[17] = t8;
176
+ } else {
177
+ t8 = $[17];
178
+ }
179
+ let t9;
180
+ if ($[18] !== t8 || $[19] !== value) {
181
+ t9 = <ConversationContext.Provider value={value}>{t8}</ConversationContext.Provider>;
182
+ $[18] = t8;
183
+ $[19] = value;
184
+ $[20] = t9;
185
+ } else {
186
+ t9 = $[20];
187
+ }
188
+ return t9;
189
+ }
190
+
191
+ /** The scroll handlers a thread's scroller passes on, as `ScrollView` and `FlatList` both take them. */
192
+
193
+ /**
194
+ * Taps reach rows while the keyboard is up; a drag lowers it (interactively
195
+ * on iOS, and on Android 11+ with `KeyboardControllerProvider`, in
196
+ * `ConversationList`); scroll events at 60 Hz.
197
+ */
198
+ const SCROLL_DEFAULTS = {
199
+ keyboardShouldPersistTaps: "handled",
200
+ keyboardDismissMode: Platform.OS === "ios" ? "interactive" : "on-drag",
201
+ scrollEventThrottle: 16
202
+ };
203
+
204
+ /**
205
+ * The wiring `ConversationContent` and `ConversationList` share: the ref the
206
+ * scroll button and the follow use, taps that reach rows while the keyboard is
207
+ * up, and the handlers that track the bottom and follow new content. The
208
+ * defaults come first so `props` can override them; the handlers come last
209
+ * and call the app's own after the thread's. An inverted list needs no
210
+ * follow: its newest message is at offset 0, so it stays in view as the
211
+ * content and the view change. While `paused` is true the bottom is not
212
+ * tracked: something other than the reader scrolls the list.
213
+ */
214
+ function useConversationScroll({
215
+ onScroll,
216
+ onScrollBeginDrag,
217
+ onContentSizeChange,
218
+ onLayout,
219
+ ...props
220
+ }, {
221
+ bottomInset,
222
+ inverted = false,
223
+ paused
224
+ } = {}) {
225
+ const conversation = useConversation();
226
+ return {
227
+ ref: scroller => {
228
+ conversation.scrollRef.current = scroller;
229
+ },
230
+ ...SCROLL_DEFAULTS,
231
+ ...props,
232
+ onScroll: event => {
233
+ if (!paused?.()) conversation.onScroll(event.nativeEvent, bottomInset?.(), inverted);
234
+ onScroll?.(event);
235
+ },
236
+ onScrollBeginDrag: event_0 => {
237
+ conversation.onUserScroll();
238
+ onScrollBeginDrag?.(event_0);
239
+ },
240
+ // The app's handler first, so a scroller that measures itself (`ConversationList`) has the new size to follow.
241
+ onContentSizeChange: (width, height) => {
242
+ onContentSizeChange?.(width, height);
243
+ if (!inverted) conversation.followContent();
244
+ },
245
+ // The keyboard or composer resizing the view keeps the end in view too.
246
+ onLayout: event_1 => {
247
+ onLayout?.(event_1);
248
+ if (!inverted) conversation.followContent();
249
+ }
250
+ };
251
+ }
252
+ /**
253
+ * The scrolling message column: the web `flex flex-col gap-4 px-3 py-3`. A
254
+ * `ScrollView`, which draws every message; a thread that can grow long
255
+ * should use `ConversationList`.
256
+ */
257
+ export function ConversationContent(t0) {
258
+ const $ = _c(12);
259
+ let className;
260
+ let contentContainerClassName;
261
+ let props;
262
+ if ($[0] !== t0) {
263
+ ({
264
+ className,
265
+ contentContainerClassName,
266
+ ...props
267
+ } = t0);
268
+ $[0] = t0;
269
+ $[1] = className;
270
+ $[2] = contentContainerClassName;
271
+ $[3] = props;
272
+ } else {
273
+ className = $[1];
274
+ contentContainerClassName = $[2];
275
+ props = $[3];
276
+ }
277
+ let t1;
278
+ if ($[4] !== className) {
279
+ t1 = cn("flex-1", className);
280
+ $[4] = className;
281
+ $[5] = t1;
282
+ } else {
283
+ t1 = $[5];
284
+ }
285
+ let t2;
286
+ if ($[6] !== contentContainerClassName) {
287
+ t2 = cn("gap-4 px-3 py-3", contentContainerClassName);
288
+ $[6] = contentContainerClassName;
289
+ $[7] = t2;
290
+ } else {
291
+ t2 = $[7];
292
+ }
293
+ const t3 = useConversationScroll(props);
294
+ let t4;
295
+ if ($[8] !== t1 || $[9] !== t2 || $[10] !== t3) {
296
+ t4 = <ScrollView className={t1} contentContainerClassName={t2} {...t3} />;
297
+ $[8] = t1;
298
+ $[9] = t2;
299
+ $[10] = t3;
300
+ $[11] = t4;
301
+ } else {
302
+ t4 = $[11];
303
+ }
304
+ return t4;
305
+ }
306
+ /**
307
+ * `ConversationContent` as a virtualised `FlatList`, for a thread that can
308
+ * grow long: only the messages near the view are drawn. The same sticking to
309
+ * the bottom, scroll button and keyboard behaviour. The column has the
310
+ * `px-3 py-3` padding but no gap, as a `FlatList` gap would also space its
311
+ * hidden-row placeholders: give `ItemSeparatorComponent` for the space
312
+ * between messages, and a stable `keyExtractor`.
313
+ *
314
+ * With `inverted` (give `data` newest first) the newest message is the first
315
+ * row, drawn at the bottom, so a thread opens at its end in its first frame,
316
+ * with no scroll and no measuring. A streamed reply (row 0) grows upwards and
317
+ * stays in view. Scrolled up, new content keeps what the reader sees in place
318
+ * (`maintainVisibleContentPosition`, iOS and Android). A thread shorter than
319
+ * the view starts at its top, as without `inverted`. `ListHeaderComponent`
320
+ * draws at the bottom, under the newest message, and `ListFooterComponent` at
321
+ * the top. An empty list draws `ListEmptyComponent` in place of the list, the
322
+ * right way up. On Android React Native flips an inverted list on both axes,
323
+ * so the column's left and right padding swap: keep it even, and put uneven
324
+ * side padding in the rows.
325
+ *
326
+ * Without `inverted`, it scrolls to the end it has measured, not with
327
+ * `FlatList.scrollToEnd`, which places the end from estimated row heights. A
328
+ * FlatList draws from its start, so a thread opened with messages would show
329
+ * its first rows and then jump to the end: it stays hidden until its first
330
+ * jump to the end (the next frame, once both sizes are known). Rows that are
331
+ * drawn or measured later can still move it once it shows; prefer `inverted`.
332
+ *
333
+ * With `KeyboardControllerProvider` the list keeps its size as the keyboard
334
+ * opens: its content gains the keyboard's height at the end and rises with
335
+ * the keyboard frame by frame, so the newest message stays on the composer.
336
+ * A drag down on it moves the keyboard with the finger once the finger
337
+ * reaches the composer, and lets it go at the finger's speed (iOS, Android
338
+ * 11+; Android 10 and below lower it at once, as without the provider).
339
+ */
340
+ export function ConversationList({
341
+ className,
342
+ contentContainerClassName,
343
+ onContentSizeChange,
344
+ onLayout,
345
+ ...props
346
+ }) {
347
+ const conversation = useConversation();
348
+ const keyboard = useKeyboardParts();
349
+ const insets = useSafeAreaInsets();
350
+ const inverted = props.inverted === true;
351
+ // Sizes from the list's own events, for the scroll offset; no render reads
352
+ // them. `inset` is the keyboard's space past the content (before offset 0
353
+ // when inverted); `keyboardMoving` is true while the keyboard scrolls the
354
+ // list, which is not the reader leaving the bottom.
355
+ const size = useRef({
356
+ content: 0,
357
+ view: 0,
358
+ inset: 0,
359
+ keyboardMoving: false
360
+ });
361
+ // Not inverted: hidden until the first jump to the end (see above). An
362
+ // empty list (a spinner, an empty state) shows at once.
363
+ const hasRows = (props.data?.length ?? 0) > 0;
364
+ const [placed, setPlaced] = useState(false);
365
+ const placeOnce = () => {
366
+ if (!inverted && !placed && hasRows && size.current.content > 0 && size.current.view > 0) {
367
+ requestAnimationFrame(() => setPlaced(true));
368
+ }
369
+ };
370
+ const wired = useConversationScroll({
371
+ ...props,
372
+ onContentSizeChange: (width, height) => {
373
+ size.current.content = height;
374
+ onContentSizeChange?.(width, height);
375
+ placeOnce();
376
+ },
377
+ onLayout: event => {
378
+ size.current.view = event.nativeEvent.layout.height;
379
+ onLayout?.(event);
380
+ placeOnce();
381
+ }
382
+ }, {
383
+ bottomInset: () => size.current.inset,
384
+ inverted,
385
+ paused: () => size.current.keyboardMoving
386
+ });
387
+ // An inverted list would draw its empty state at the bottom, flipped back
388
+ // only if the element takes a `style`: draw it the right way up instead.
389
+ if (inverted && !hasRows) {
390
+ const Empty = props.ListEmptyComponent;
391
+ return <View style={styles.fill}>{isValidElement(Empty) ? Empty : Empty ? <Empty /> : null}</View>;
392
+ }
393
+ // The newest message's offset: inverted, 0 less the space before it.
394
+ const end = () => inverted ? size.current.inset > 0 ? -size.current.inset : 0 : Math.max(0, size.current.content + size.current.inset - size.current.view);
395
+ const list_0 = <FlatList className={cn("flex-1", className)}
396
+ // Inverted, the column's end is the top: a short thread sits there.
397
+ contentContainerClassName={cn("px-3 py-3", inverted && "grow justify-end", contentContainerClassName)} {...wired} style={[wired.style, !inverted && hasRows && !placed ? styles.hidden : null]}
398
+ // At the bottom, a new message shows at offset 0; scrolled up, it must not move what the reader sees.
399
+ maintainVisibleContentPosition={props.maintainVisibleContentPosition ?? (inverted && !conversation.isAtBottom ? KEEP_VISIBLE : undefined)} keyboardDismissMode={props.keyboardDismissMode ?? (keyboard?.interactive ? "interactive" : SCROLL_DEFAULTS.keyboardDismissMode)} renderScrollComponent={keyboard ? scrollProps => <keyboard.ChatScrollView {...scrollProps} inverted={inverted}
400
+ // The composer pads the bottom inset, which the keyboard covers: the content rises by the rest.
401
+ bottomOffset={insets.bottom} onInsetChange={inset => {
402
+ size.current.inset = inset;
403
+ }} onKeyboardMove={moving => {
404
+ size.current.keyboardMoving = moving;
405
+ }} /> : props.renderScrollComponent} ref={list => {
406
+ conversation.scrollRef.current = list && {
407
+ scrollToEnd: options => list.scrollToOffset({
408
+ offset: end(),
409
+ animated: options?.animated
410
+ })
411
+ };
412
+ }} />;
413
+ if (!keyboard) return list_0;
414
+ // The keyboard follows the finger from the composer's top: the dock less the inset it pads.
415
+ return <keyboard.GestureArea offset={Math.max(0, conversation.dockHeight - insets.bottom)} style={styles.fill}>
416
+ {list_0}
417
+ </keyboard.GestureArea>;
418
+ }
419
+
420
+ /** Scrolled up in an inverted list: the first drawn row in view keeps its place as rows are added or grow. */
421
+ const KEEP_VISIBLE = {
422
+ minIndexForVisible: 0
423
+ };
424
+ /** Centred placeholder for a thread with no messages: the web title (medium) and muted description. */
425
+ export function ConversationEmptyState(t0) {
426
+ const $ = _c(26);
427
+ let Icon;
428
+ let children;
429
+ let className;
430
+ let props;
431
+ let t1;
432
+ let t2;
433
+ if ($[0] !== t0) {
434
+ ({
435
+ title: t1,
436
+ description: t2,
437
+ icon: Icon,
438
+ className,
439
+ children,
440
+ ...props
441
+ } = t0);
442
+ $[0] = t0;
443
+ $[1] = Icon;
444
+ $[2] = children;
445
+ $[3] = className;
446
+ $[4] = props;
447
+ $[5] = t1;
448
+ $[6] = t2;
449
+ } else {
450
+ Icon = $[1];
451
+ children = $[2];
452
+ className = $[3];
453
+ props = $[4];
454
+ t1 = $[5];
455
+ t2 = $[6];
456
+ }
457
+ const title = t1 === undefined ? "No messages yet" : t1;
458
+ const description = t2 === undefined ? "Start a conversation to see messages here" : t2;
459
+ const scheme = useScheme();
460
+ const largerText = useLargerText();
461
+ let T0;
462
+ let t3;
463
+ let t4;
464
+ let t5;
465
+ if ($[7] !== Icon || $[8] !== children || $[9] !== className || $[10] !== description || $[11] !== largerText || $[12] !== props || $[13] !== scheme || $[14] !== title) {
466
+ const size = chatTextSize("small", largerText);
467
+ T0 = View;
468
+ if ($[19] !== className) {
469
+ t3 = cn("flex-1 items-center justify-center gap-3 p-8", className);
470
+ $[19] = className;
471
+ $[20] = t3;
472
+ } else {
473
+ t3 = $[20];
474
+ }
475
+ t4 = props;
476
+ t5 = children ?? <>{Icon ? <Icon size={largerText ? 28 : 24} color={tokens.color.muted[scheme]} /> : null}<View className="items-center gap-1"><Text accessibilityRole="header" className={cn(size, "text-center font-instrument-sans-medium")} style={[sizeStyle(size), textStyle("foreground", scheme), typeStyle("medium")]}>{title}</Text>{description ? <Text className={cn(size, "text-center font-instrument-sans")} style={[sizeStyle(size), textStyle("muted", scheme)]}>{description}</Text> : null}</View></>;
477
+ $[7] = Icon;
478
+ $[8] = children;
479
+ $[9] = className;
480
+ $[10] = description;
481
+ $[11] = largerText;
482
+ $[12] = props;
483
+ $[13] = scheme;
484
+ $[14] = title;
485
+ $[15] = T0;
486
+ $[16] = t3;
487
+ $[17] = t4;
488
+ $[18] = t5;
489
+ } else {
490
+ T0 = $[15];
491
+ t3 = $[16];
492
+ t4 = $[17];
493
+ t5 = $[18];
494
+ }
495
+ let t6;
496
+ if ($[21] !== T0 || $[22] !== t3 || $[23] !== t4 || $[24] !== t5) {
497
+ t6 = <T0 className={t3} {...t4}>{t5}</T0>;
498
+ $[21] = T0;
499
+ $[22] = t3;
500
+ $[23] = t4;
501
+ $[24] = t5;
502
+ $[25] = t6;
503
+ } else {
504
+ t6 = $[25];
505
+ }
506
+ return t6;
507
+ }
508
+ /**
509
+ * The group under the thread, inside `Conversation` after the list: the
510
+ * composer, or a docked question card in its place. `ConversationScrollButton`
511
+ * sits centred 16 above its top edge (the web button's `bottom-4` over the
512
+ * dock), so the button stays with the dock as it grows (a reply quote, a
513
+ * taller field) and as the keyboard lifts it, instead of a fixed height over
514
+ * the messages.
515
+ *
516
+ * With `KeyboardControllerProvider` the dock moves with the keyboard frame by
517
+ * frame (`translateY`, no layout), less the bottom inset: its content pads
518
+ * that inset, as `PromptInput` does, and the keyboard covers it. A
519
+ * `PromptInput` in it then adds no lift of its own.
520
+ */
521
+ export function ConversationDock(t0) {
522
+ const $ = _c(15);
523
+ let onLayout;
524
+ let props;
525
+ if ($[0] !== t0) {
526
+ ({
527
+ onLayout,
528
+ ...props
529
+ } = t0);
530
+ $[0] = t0;
531
+ $[1] = onLayout;
532
+ $[2] = props;
533
+ } else {
534
+ onLayout = $[1];
535
+ props = $[2];
536
+ }
537
+ const {
538
+ setDockHeight
539
+ } = useConversation();
540
+ const keyboard = useKeyboardParts();
541
+ const insets = useSafeAreaInsets();
542
+ let t1;
543
+ if ($[3] !== onLayout || $[4] !== setDockHeight) {
544
+ t1 = event => {
545
+ setDockHeight(event.nativeEvent.layout.height);
546
+ onLayout?.(event);
547
+ };
548
+ $[3] = onLayout;
549
+ $[4] = setDockHeight;
550
+ $[5] = t1;
551
+ } else {
552
+ t1 = $[5];
553
+ }
554
+ let t2;
555
+ if ($[6] !== props || $[7] !== t1) {
556
+ t2 = <View {...props} onLayout={t1} />;
557
+ $[6] = props;
558
+ $[7] = t1;
559
+ $[8] = t2;
560
+ } else {
561
+ t2 = $[8];
562
+ }
563
+ const dock = t2;
564
+ if (!keyboard) {
565
+ return dock;
566
+ }
567
+ let t3;
568
+ if ($[9] !== dock) {
569
+ t3 = <KeyboardLiftContext.Provider value={true}>{dock}</KeyboardLiftContext.Provider>;
570
+ $[9] = dock;
571
+ $[10] = t3;
572
+ } else {
573
+ t3 = $[10];
574
+ }
575
+ let t4;
576
+ if ($[11] !== insets.bottom || $[12] !== keyboard.StickyView || $[13] !== t3) {
577
+ t4 = <keyboard.StickyView bottomOffset={insets.bottom}>{t3}</keyboard.StickyView>;
578
+ $[11] = insets.bottom;
579
+ $[12] = keyboard.StickyView;
580
+ $[13] = t3;
581
+ $[14] = t4;
582
+ } else {
583
+ t4 = $[14];
584
+ }
585
+ return t4;
586
+ }
587
+ /** The scroll button fades in and out on the strong ease-out. */
588
+ const SCROLL_BUTTON_ENTER = FadeIn.duration(150).easing(EASE_OUT);
589
+ const SCROLL_BUTTON_EXIT = FadeOut.duration(120).easing(EASE_OUT);
590
+
591
+ /** The labelled pill's height (web `h-7`). */
592
+ const SCROLL_PILL_HEIGHT = 28;
593
+
594
+ /**
595
+ * Round outline arrow, centred 16 above the bottom of the `Conversation`, or
596
+ * above its `ConversationDock` when it has one, shown while the reader is
597
+ * away from the end (the web `absolute bottom-4`).
598
+ * The web `Button` `icon` size (40, slop to 48). It has an opaque surface
599
+ * fill and no shadow, so it reads over the messages under it. With
600
+ * `showLabel`, a compact pill with the icon and `label`.
601
+ */
602
+ export function ConversationScrollButton(t0) {
603
+ const $ = _c(22);
604
+ const {
605
+ label: t1,
606
+ icon: t2,
607
+ showLabel: t3,
608
+ insets,
609
+ testID
610
+ } = t0;
611
+ const label = t1 === undefined ? "Scroll to bottom" : t1;
612
+ const Icon = t2 === undefined ? IconArrowDown : t2;
613
+ const showLabel = t3 === undefined ? false : t3;
614
+ const {
615
+ isAtBottom,
616
+ scrollToBottom,
617
+ dockHeight
618
+ } = useConversation();
619
+ const keyboard = useKeyboardParts();
620
+ const safeArea = useSafeAreaInsets();
621
+ const scheme = useScheme();
622
+ const largerText = useLargerText();
623
+ if (isAtBottom) {
624
+ return null;
625
+ }
626
+ let t4;
627
+ if ($[0] !== largerText) {
628
+ t4 = chatTextSize("small", largerText);
629
+ $[0] = largerText;
630
+ $[1] = t4;
631
+ } else {
632
+ t4 = $[1];
633
+ }
634
+ const labelSize = t4;
635
+ const t5 = insets?.left ?? 0;
636
+ const t6 = insets?.right ?? 0;
637
+ const t7 = dockHeight + 16;
638
+ let t8;
639
+ if ($[2] !== t5 || $[3] !== t6 || $[4] !== t7) {
640
+ t8 = {
641
+ position: "absolute",
642
+ left: t5,
643
+ right: t6,
644
+ bottom: t7,
645
+ alignItems: "center"
646
+ };
647
+ $[2] = t5;
648
+ $[3] = t6;
649
+ $[4] = t7;
650
+ $[5] = t8;
651
+ } else {
652
+ t8 = $[5];
653
+ }
654
+ let t9;
655
+ if ($[6] !== Icon || $[7] !== label || $[8] !== labelSize || $[9] !== largerText || $[10] !== scheme || $[11] !== scrollToBottom || $[12] !== showLabel || $[13] !== testID) {
656
+ t9 = showLabel ? <Pressable accessibilityRole="button" accessibilityLabel={label} onPress={scrollToBottom} testID={testID} hitSlop={touchSlop(MIN_TOUCH_TARGET, SCROLL_PILL_HEIGHT)} style={{
657
+ minHeight: SCROLL_PILL_HEIGHT,
658
+ flexDirection: "row",
659
+ alignItems: "center",
660
+ paddingHorizontal: 12,
661
+ borderRadius: tokens.radius.pill,
662
+ borderWidth: 1,
663
+ borderColor: withAlpha(tokens.color.border[scheme], 0.6),
664
+ backgroundColor: tokens.color.surface[scheme]
665
+ }}>{t10 => {
666
+ const {
667
+ pressed
668
+ } = t10;
669
+ return <View style={{
670
+ flexDirection: "row",
671
+ alignItems: "center",
672
+ gap: 6,
673
+ opacity: pressed ? 0.7 : 1
674
+ }}><Icon size={largerText ? 20 : 16} color={tokens.color.muted[scheme]} /><Text className={cn(labelSize, "font-instrument-sans")} style={[sizeStyle(labelSize), textStyle("muted", scheme)]}>{label}</Text></View>;
675
+ }}</Pressable> : <Button variant="outline" size="icon" accessibilityLabel={label} onPress={scrollToBottom} testID={testID} style={{
676
+ borderRadius: tokens.radius.pill,
677
+ backgroundColor: tokens.color.surface[scheme]
678
+ }}><Icon size={largerText ? 20 : 16} color={tokens.color.foreground[scheme]} /></Button>;
679
+ $[6] = Icon;
680
+ $[7] = label;
681
+ $[8] = labelSize;
682
+ $[9] = largerText;
683
+ $[10] = scheme;
684
+ $[11] = scrollToBottom;
685
+ $[12] = showLabel;
686
+ $[13] = testID;
687
+ $[14] = t9;
688
+ } else {
689
+ t9 = $[14];
690
+ }
691
+ let t10;
692
+ if ($[15] !== t8 || $[16] !== t9) {
693
+ t10 = <NativeOnlyAnimatedView entering={SCROLL_BUTTON_ENTER} exiting={SCROLL_BUTTON_EXIT} pointerEvents="box-none" style={t8}>{t9}</NativeOnlyAnimatedView>;
694
+ $[15] = t8;
695
+ $[16] = t9;
696
+ $[17] = t10;
697
+ } else {
698
+ t10 = $[17];
699
+ }
700
+ const button = t10;
701
+ if (!keyboard) {
702
+ return button;
703
+ }
704
+ let t11;
705
+ if ($[18] !== button || $[19] !== keyboard.StickyView || $[20] !== safeArea.bottom) {
706
+ t11 = <keyboard.StickyView bottomOffset={safeArea.bottom} pointerEvents="box-none" style={StyleSheet.absoluteFill}>{button}</keyboard.StickyView>;
707
+ $[18] = button;
708
+ $[19] = keyboard.StickyView;
709
+ $[20] = safeArea.bottom;
710
+ $[21] = t11;
711
+ } else {
712
+ t11 = $[21];
713
+ }
714
+ return t11;
715
+ }
716
+ const styles = StyleSheet.create({
717
+ fill: {
718
+ flex: 1
719
+ },
720
+ hidden: {
721
+ opacity: 0
722
+ }
723
+ });