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
@@ -0,0 +1,1568 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * AIInput — the composer a person types a prompt into.
5
+ *
6
+ * ```tsx
7
+ * <AIInput value={value} onValueChange={setValue} onSubmit={send}>
8
+ * <AIInput.Field placeholder="Chat with the model" />
9
+ * <AIInput.Toolbar>
10
+ * <AIInput.Action label="Add" icon={<PlusIcon />} onPress={openSheet} />
11
+ * <AIInput.Pill label="Sonnet 4.6" detail="High" onPress={openModels} />
12
+ * <AIInput.Action label="Dictate" icon={<MicIcon />} onPress={record} />
13
+ * <AIInput.Submit />
14
+ * </AIInput.Toolbar>
15
+ * </AIInput>
16
+ * ```
17
+ *
18
+ * ## The field grows, and then it stops
19
+ *
20
+ * It opens one line tall and follows what is typed into it up to `maxRows`,
21
+ * after which it holds that height and scrolls its own content. Five rows is
22
+ * the default because a composer that keeps growing eventually pushes its own
23
+ * send button off the bottom of the screen, and the row that carries the
24
+ * actions stays pinned to the bottom of the box either way.
25
+ *
26
+ * ## It does not record anything
27
+ *
28
+ * There is a recording state, and it draws a live meter, but nothing here
29
+ * touches the microphone. The app owns the recorder — the permission prompt,
30
+ * the session, the platform quirks — and passes back a `level` between 0 and 1
31
+ * along with a `status`. Pass a Reanimated shared value and a recorder
32
+ * reporting every 30ms never re-renders anything above this component.
33
+ *
34
+ * That division is what keeps the composer free of an audio dependency.
35
+ *
36
+ * ## Glass is iOS 26 and above
37
+ *
38
+ * The surfaces ask for the system material and get it where it exists. Below
39
+ * iOS 26, on Android, and for anyone with Reduce Transparency switched on,
40
+ * they are drawn as solid token surfaces instead. Both are finished looks; see
41
+ * `Glass` for why nothing is faked on the platforms without it.
42
+ *
43
+ * ## Where the props come from
44
+ *
45
+ * The root takes `ViewProps`. `Field` takes `TextInputProps`, so
46
+ * `keyboardType`, `maxLength`, `autoFocus` and the rest work as they always
47
+ * do. Every part takes `className`.
48
+ */
49
+ import { Children, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
50
+ import { AccessibilityInfo, findNodeHandle, TextInput, View, useWindowDimensions } from 'react-native';
51
+ import Animated, { Easing, FadeOut, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
52
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
53
+ import { tv } from 'tailwind-variants';
54
+ import { useCSSVariable } from 'uniwind';
55
+ import { useDirection } from "../../hooks/use-direction.js";
56
+ import { AudioLinesIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, IconColorProvider, MicIcon, SendArrowIcon, XIcon } from "../../icons/index.js";
57
+ import { hasNativeUI } from "../../native/index.js";
58
+ import { AnimatedPressable } from "../../primitives/animated-pressable.js";
59
+ import { Glass } from "../../primitives/glass.js";
60
+ import { KeyboardAvoider } from "../../primitives/keyboard-avoider.js";
61
+ import { Text } from "../../primitives/text.js";
62
+ import { cn } from "../../utils/cn.js";
63
+ import { BottomSheet } from "../bottom-sheet/index.js";
64
+ import { Button } from "../button/index.js";
65
+ import { Soundwave } from "../soundwave/index.js";
66
+ import { Switch } from "../switch/index.js";
67
+ import { AI_INPUT_METRICS, growthBounds } from "./ai-input-growth.js";
68
+
69
+ /** What the app is doing, which is what the trailing button offers to do next. */
70
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
71
+ /** Corner radius per size, in points. The material rounds itself to these. */
72
+ const RADIUS = {
73
+ sm: 22,
74
+ md: 26,
75
+ lg: 30
76
+ };
77
+
78
+ /**
79
+ * Circular control diameter per size.
80
+ *
81
+ * Deliberately under the 44pt touch minimum: these sit in a row inside a card
82
+ * with padding around it, and the card's own padding carries the target the
83
+ * finger actually gets. A 44pt circle here makes a toolbar that is taller than
84
+ * the field above it.
85
+ */
86
+ const CONTROL = {
87
+ sm: 26,
88
+ md: 30,
89
+ lg: 34
90
+ };
91
+
92
+ /**
93
+ * The row a platform-drawn control needs, and the space kept under it.
94
+ *
95
+ * `Button` frames a native icon button at 44pt — the number that ends the
96
+ * measurement chain rather than one that sets a look. A row shorter than that
97
+ * is a row its controls hang out of, over whatever is above them, and a row
98
+ * with no definite height at all is one they never lay out against: a hosted
99
+ * view only measures where something above it is fixed on both axes.
100
+ *
101
+ * The drawn controls are smaller and are laid out by the row itself, so this
102
+ * applies only when the controls have been handed over.
103
+ */
104
+ const NATIVE_ROW_HEIGHT = 48;
105
+ const NATIVE_ROW_FOOT = 4;
106
+ const EASE = Easing.out(Easing.cubic);
107
+ const HEIGHT_DURATION = 240;
108
+ const ENTER_DURATION = 220;
109
+ const EXIT_DURATION = 140;
110
+
111
+ /**
112
+ * How far a pushed screen travels in, as a fraction of the sheet's width.
113
+ *
114
+ * A third rather than the whole width: the sheet is not a screen, and a slide
115
+ * the full width of it reads as the app navigating rather than as this surface
116
+ * going one level deeper.
117
+ */
118
+ const SLIDE_FRACTION = 0.33;
119
+ const aiInputVariants = tv({
120
+ slots: {
121
+ root: 'w-full',
122
+ field: 'w-full bg-transparent px-4 font-normal text-foreground',
123
+ // On one line the field is what takes up the slack between the controls,
124
+ // and the row's own padding already stands it off the edge.
125
+ fieldInline: 'flex-1 bg-transparent px-2 font-normal text-foreground',
126
+ /*
127
+ * Centred, not bottom-aligned.
128
+ *
129
+ * The stacked toolbar sits under the field, so it aligns to the bottom.
130
+ * Here the field is *between* the controls, and its box is not the height
131
+ * this component computes: a multiline TextInput carries a vertical inset
132
+ * of the platform's own on top of any padding given to it. Bottom-aligning
133
+ * a box whose real height is unknown puts its text below the controls
134
+ * beside it by however much that inset is — which is what kept happening.
135
+ *
136
+ * Centring does not need to know the height. Whatever the field's box
137
+ * turns out to be, its middle lines up with theirs, and the text sits in
138
+ * the middle of it.
139
+ */
140
+ row: 'w-full flex-row items-center gap-1.5 p-2',
141
+ toolbar: 'w-full flex-row items-center gap-2 px-2 pb-2',
142
+ spacer: 'flex-1',
143
+ pill: 'flex-row items-center gap-1.5 px-3',
144
+ recording: 'w-full flex-row items-center gap-3 px-2 pb-2',
145
+ meter: 'flex-1'
146
+ },
147
+ variants: {
148
+ size: {
149
+ sm: {
150
+ toolbar: 'gap-1.5'
151
+ },
152
+ md: {},
153
+ lg: {
154
+ toolbar: 'gap-2.5'
155
+ }
156
+ },
157
+ disabled: {
158
+ true: {
159
+ root: 'opacity-60'
160
+ },
161
+ false: {}
162
+ }
163
+ },
164
+ defaultVariants: {
165
+ size: 'md',
166
+ disabled: false
167
+ }
168
+ });
169
+
170
+ /* -------------------------------------------------------------------------- */
171
+ /* Root */
172
+ /* -------------------------------------------------------------------------- */
173
+
174
+ const AIInputContext = /*#__PURE__*/createContext(null);
175
+ function useAIInput(component) {
176
+ const context = useContext(AIInputContext);
177
+ if (!context) throw new Error(`${component} must be used within an <AIInput>`);
178
+ return context;
179
+ }
180
+
181
+ /**
182
+ * The composer's context if there is one, and nothing if there is not.
183
+ *
184
+ * The controls that only want a size and an enabled state work anywhere — a
185
+ * sheet header, a voice-mode toolbar, a row of their own. Those surfaces
186
+ * render through a portal or as their own screen, so a root wrapped around the
187
+ * composer is not above them, and demanding one would make a button throw for
188
+ * being used where it was designed to be used.
189
+ */
190
+ function useAIInputOptional() {
191
+ return useContext(AIInputContext);
192
+ }
193
+ function AIInputRoot({
194
+ children,
195
+ className,
196
+ value: valueProp,
197
+ onValueChange,
198
+ defaultValue = '',
199
+ status = 'ready',
200
+ onSubmit,
201
+ onStop,
202
+ onVoice,
203
+ onRecordCancel,
204
+ onRecordConfirm,
205
+ level,
206
+ size = 'md',
207
+ disabled = false,
208
+ native = false,
209
+ minRows = 1,
210
+ maxRows = 5,
211
+ avoidKeyboard = true,
212
+ keyboardBottomInset = 0,
213
+ keyboardGap = 8,
214
+ style,
215
+ ...props
216
+ }) {
217
+ const [internalValue, setInternalValue] = useState(defaultValue);
218
+ const isControlled = valueProp !== undefined;
219
+ const value = isControlled ? valueProp : internalValue;
220
+ const [focused, setFocused] = useState(false);
221
+ const setValue = useCallback(next => {
222
+ if (!isControlled) setInternalValue(next);
223
+ onValueChange?.(next);
224
+ }, [isControlled, onValueChange]);
225
+ const submit = useCallback(() => {
226
+ const trimmed = value.trim();
227
+ if (!trimmed) return;
228
+ onSubmit?.(value);
229
+ }, [onSubmit, value]);
230
+
231
+ /*
232
+ * Whether the controls can actually be handed over.
233
+ *
234
+ * `native` is a request, not a fact: the toolkit is an optional peer, and
235
+ * without it `Button` quietly draws its own control instead — at the size
236
+ * *it* uses, which is not the size this row is built around. Asking for a
237
+ * platform control and getting a 44pt drawn one in a row shaped for a 30pt
238
+ * one is how the buttons ended up over the field.
239
+ *
240
+ * So the request is resolved against what is installed, once, and every part
241
+ * reads the answer rather than the ask.
242
+ */
243
+ const platformControls = native && hasNativeUI();
244
+ const context = useMemo(() => ({
245
+ value,
246
+ setValue,
247
+ status,
248
+ size,
249
+ level,
250
+ disabled,
251
+ native: platformControls,
252
+ minRows,
253
+ maxRows,
254
+ focused,
255
+ setFocused,
256
+ submit,
257
+ canVoice: onVoice !== undefined,
258
+ canStop: onStop !== undefined,
259
+ stop: () => onStop?.(),
260
+ recordCancel: () => onRecordCancel?.(),
261
+ recordConfirm: () => onRecordConfirm?.(),
262
+ voice: () => onVoice?.()
263
+ }), [value, setValue, status, size, level, disabled, platformControls, minRows, maxRows, focused, submit, onStop, onVoice, onRecordCancel, onRecordConfirm, onVoice]);
264
+ const slots = aiInputVariants({
265
+ size,
266
+ disabled
267
+ });
268
+
269
+ /*
270
+ * Whether the card has to keep room at its foot for a row it does not lay
271
+ * out.
272
+ *
273
+ * A handed-over control is a hosted native view, and a hosted view settles
274
+ * where the platform puts it rather than where the row it is in ends up —
275
+ * which, in a card that is also sizing itself to the field above, is over
276
+ * that field. Reserving the row's height here and pinning the row into the
277
+ * reservation takes the question away: the field's box stops where the
278
+ * reservation starts, so a control that draws itself somewhere unexpected
279
+ * has nothing above it left to cover.
280
+ *
281
+ * Read from the children rather than tracked in state, so the card and the
282
+ * row agree on the first render instead of the second.
283
+ */
284
+ const hasNativeRow = platformControls && Children.toArray(children).some(child => /*#__PURE__*/isValidElement(child) && (child.type === AIInputToolbar || child.type === AIInputRecording));
285
+ const surface = /*#__PURE__*/_jsx(Glass, {
286
+ radius: RADIUS[size],
287
+ fallbackClassName: "border border-border bg-card",
288
+ ...props,
289
+ className: slots.root({
290
+ className
291
+ }),
292
+ style: [hasNativeRow ? {
293
+ paddingBottom: NATIVE_ROW_HEIGHT
294
+ } : null, style],
295
+ children: children
296
+ });
297
+
298
+ /*
299
+ * The keyboard hook stays behind a component boundary rather than an
300
+ * `enabled` flag. Calling it at all has global consequences — without the
301
+ * keyboard controller installed it falls back to Reanimated's
302
+ * useAnimatedKeyboard, which switches Android out of adjustResize for the
303
+ * whole app. A composer that was told not to avoid the keyboard must not do
304
+ * that to every other screen in the app.
305
+ */
306
+ return /*#__PURE__*/_jsx(AIInputContext.Provider, {
307
+ value: context,
308
+ children: avoidKeyboard ? /*#__PURE__*/_jsx(KeyboardAvoider, {
309
+ active: focused,
310
+ mode: "dock"
311
+ // Docking travels the composer up by the keyboard's height less the
312
+ // inset it already sits above. Claiming to sit a little lower than it
313
+ // does is what leaves a gap once it arrives.
314
+ ,
315
+ bottomInset: keyboardBottomInset - keyboardGap,
316
+ className: "w-full",
317
+ children: surface
318
+ }) : surface
319
+ });
320
+ }
321
+
322
+ /* -------------------------------------------------------------------------- */
323
+ /* Field */
324
+ /* -------------------------------------------------------------------------- */
325
+
326
+ function AIInputField({
327
+ className,
328
+ placeholder = 'Ask anything',
329
+ onFocus,
330
+ onBlur,
331
+ style,
332
+ ...props
333
+ }) {
334
+ const context = useAIInput('AIInput.Field');
335
+ const {
336
+ value,
337
+ setValue,
338
+ size,
339
+ disabled,
340
+ minRows,
341
+ maxRows,
342
+ setFocused
343
+ } = context;
344
+ const metrics = AI_INPUT_METRICS[size];
345
+ const placeholderColor = useCSSVariable('--color-muted-foreground');
346
+ const bounds = growthBounds(metrics, minRows, maxRows);
347
+ const inline = useContext(AIInputInlineContext);
348
+ const handleFocus = useCallback(event => {
349
+ setFocused(true);
350
+ onFocus?.(event);
351
+ }, [onFocus, setFocused]);
352
+ const handleBlur = useCallback(event => {
353
+ setFocused(false);
354
+ onBlur?.(event);
355
+ }, [onBlur, setFocused]);
356
+
357
+ /*
358
+ * Type metrics go through `style` rather than through classes, because the
359
+ * height is computed from them and a line height set in two places is a line
360
+ * height that will disagree with itself.
361
+ */
362
+ /*
363
+ * On one line the field's first row has to be exactly as tall as the buttons
364
+ * beside it.
365
+ *
366
+ * The row aligns to the bottom, so that a field which has grown keeps its
367
+ * controls at the foot of the box rather than floating them beside the
368
+ * middle of the text. That alignment is only correct if an *ungrown* field
369
+ * is the same height as the controls — otherwise its extra padding pushes
370
+ * the whole row down inside the card, which is a composer whose contents sit
371
+ * low in it. So the padding is whatever centres one line in a control.
372
+ */
373
+ /*
374
+ * On one line the field states no line height at all.
375
+ *
376
+ * A line height above the font's own exists to separate lines from each
377
+ * other, and iOS spends the surplus *above* the glyphs rather than splitting
378
+ * it — so a single line set to 22 over 16pt type sits several points below
379
+ * the buttons beside it, placeholder and value alike. With none set, the
380
+ * platform lays the text out in the font's own line box, which sits where a
381
+ * line of that font is supposed to.
382
+ *
383
+ * The box is then exactly one control tall with the natural line centred in
384
+ * it. `NATURAL_LEADING` is what a line of a given size actually occupies —
385
+ * it only has to be close, since it decides a padding either side rather
386
+ * than the height itself.
387
+ */
388
+ const lineHeight = inline ? undefined : metrics.lineHeight;
389
+ const padding = inline ? 0 : metrics.padding;
390
+ const boxStyle = useMemo(() => ({
391
+ /*
392
+ * A floor and a ceiling, and no height. The field sizes itself to its
393
+ * own content between them, which the layout engine does without anyone
394
+ * measuring anything — and once it reaches the ceiling, a multiline
395
+ * field scrolls its content instead of growing.
396
+ *
397
+ * Deriving a height from `onContentSizeChange` instead is a loop whose
398
+ * answer differs by platform: one reports the height of the text, the
399
+ * other the height of the box that was just set from it, and on that one
400
+ * the field can never grow past the height it opened at.
401
+ */
402
+ minHeight: inline ? undefined : bounds.minHeight,
403
+ maxHeight: bounds.maxHeight,
404
+ fontSize: metrics.fontSize,
405
+ lineHeight,
406
+ paddingTop: padding,
407
+ paddingBottom: padding,
408
+ /*
409
+ * Android puts the caret in the middle of a multiline box unless told
410
+ * otherwise, which is wrong for a field that grows downward — and right
411
+ * for one that is a single line between two buttons.
412
+ */
413
+ textAlignVertical: inline ? 'center' : 'top'
414
+ }), [bounds.minHeight, bounds.maxHeight, inline, lineHeight, metrics.fontSize, padding]);
415
+ const slots = aiInputVariants({
416
+ size
417
+ });
418
+ return /*#__PURE__*/_jsx(TextInput, {
419
+ ...props,
420
+ multiline: true,
421
+ value: value,
422
+ onChangeText: setValue,
423
+ placeholder: placeholder,
424
+ placeholderTextColor: typeof placeholderColor === 'string' ? placeholderColor : undefined,
425
+ editable: !disabled
426
+ /*
427
+ * `scrollEnabled` is deliberately left alone.
428
+ *
429
+ * Driving it from the measurement is a loop: switching it on changes
430
+ * what the platform reports as the content size, which changes the
431
+ * answer to whether it should be on, and the field resizes for ever. A
432
+ * multiline field scrolls once its height is fixed anyway, so there is
433
+ * nothing to drive — the clamp below is the whole mechanism.
434
+ */,
435
+ onFocus: handleFocus,
436
+ onBlur: handleBlur,
437
+ className: inline ? slots.fieldInline({
438
+ className
439
+ }) : slots.field({
440
+ className
441
+ }),
442
+ style: [boxStyle, style]
443
+ });
444
+ }
445
+
446
+ /* -------------------------------------------------------------------------- */
447
+ /* Toolbar and its controls */
448
+ /* -------------------------------------------------------------------------- */
449
+
450
+ /**
451
+ * True inside `AIInput.Row` — the parts that lay out differently on one line
452
+ * read it rather than taking a prop each, so a caller rearranges the composer
453
+ * by moving one component instead of setting a flag on three.
454
+ */
455
+ const AIInputInlineContext = /*#__PURE__*/createContext(false);
456
+ /**
457
+ * The field and its controls on a single line.
458
+ *
459
+ * The composer at its smallest: one pill above the keyboard with the field
460
+ * between the controls rather than above them. It suits a bar that is always
461
+ * on screen, where the stacked composer suits one that is the focus of it.
462
+ *
463
+ * The field still grows — it just grows the row.
464
+ *
465
+ * ```tsx
466
+ * <AIInput>
467
+ * <AIInput.Row>
468
+ * <AIInput.Action label="Add" icon={<PlusIcon />} />
469
+ * <AIInput.Field placeholder="Ask anything" />
470
+ * <AIInput.Action label="Dictate" icon={<MicIcon />} />
471
+ * <AIInput.Submit />
472
+ * </AIInput.Row>
473
+ * </AIInput>
474
+ * ```
475
+ */
476
+ function AIInputRow({
477
+ className,
478
+ children,
479
+ ...props
480
+ }) {
481
+ const {
482
+ size
483
+ } = useAIInput('AIInput.Row');
484
+ const slots = aiInputVariants({
485
+ size
486
+ });
487
+ return /*#__PURE__*/_jsx(AIInputInlineContext.Provider, {
488
+ value: true,
489
+ children: /*#__PURE__*/_jsx(View, {
490
+ ...props,
491
+ className: slots.row({
492
+ className
493
+ }),
494
+ children: children
495
+ })
496
+ });
497
+ }
498
+ function AIInputToolbar({
499
+ className,
500
+ children,
501
+ style,
502
+ ...props
503
+ }) {
504
+ const {
505
+ size,
506
+ native
507
+ } = useAIInput('AIInput.Toolbar');
508
+ const slots = aiInputVariants({
509
+ size
510
+ });
511
+ return /*#__PURE__*/_jsx(View, {
512
+ ...props,
513
+ accessibilityRole: "toolbar",
514
+ className: cn(slots.toolbar({
515
+ className
516
+ }), native && 'absolute inset-x-0 bottom-0'),
517
+ style: [native ? {
518
+ height: NATIVE_ROW_HEIGHT,
519
+ paddingBottom: NATIVE_ROW_FOOT
520
+ } : null, style],
521
+ children: children
522
+ });
523
+ }
524
+
525
+ /** Pushes everything after it to the trailing end of the toolbar. */
526
+ function AIInputSpacer({
527
+ className,
528
+ ...props
529
+ }) {
530
+ return /*#__PURE__*/_jsx(View, {
531
+ ...props,
532
+ className: cn('flex-1', className)
533
+ });
534
+ }
535
+ function AIInputAction({
536
+ className,
537
+ label,
538
+ icon,
539
+ onPress,
540
+ disabled,
541
+ size: sizeProp,
542
+ native,
543
+ ...props
544
+ }) {
545
+ const context = useAIInputOptional();
546
+ const scale = sizeProp ?? context?.size ?? 'md';
547
+ const size = CONTROL[scale];
548
+ const isDisabled = disabled ?? context?.disabled ?? false;
549
+ const isNative = (native ?? context?.native ?? false) && hasNativeUI();
550
+
551
+ /*
552
+ * The platform's own button, in its own material. `Button` already knows how
553
+ * to ask for that — the circular border shape, the glass style, and the
554
+ * hosting an icon inside a native control needs — so this is a handoff
555
+ * rather than a second implementation of it.
556
+ */
557
+ if (isNative) {
558
+ return /*#__PURE__*/_jsx(View, {
559
+ ...props,
560
+ className: className,
561
+ children: /*#__PURE__*/_jsx(Button, {
562
+ native: true,
563
+ glass: true,
564
+ variant: "secondary",
565
+ size: "icon",
566
+ accessibilityLabel: label,
567
+ disabled: isDisabled,
568
+ onPress: onPress,
569
+ children: icon
570
+ })
571
+ });
572
+ }
573
+ return /*#__PURE__*/_jsx(Glass, {
574
+ variant: "clear",
575
+ radius: size / 2,
576
+ fallbackClassName: "bg-muted",
577
+ ...props,
578
+ className: className,
579
+ style: {
580
+ width: size,
581
+ height: size
582
+ },
583
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
584
+ accessibilityRole: "button",
585
+ accessibilityLabel: label,
586
+ accessibilityState: {
587
+ disabled: isDisabled
588
+ },
589
+ disabled: isDisabled,
590
+ onPress: onPress
591
+ // The dim goes on the content rather than on the material. Fading a
592
+ // system material degrades it, and at zero it stops drawing entirely.
593
+ ,
594
+ className: cn('h-full w-full items-center justify-center', isDisabled && 'opacity-40'),
595
+ children: icon
596
+ })
597
+ });
598
+ }
599
+ function AIInputPill({
600
+ className,
601
+ label,
602
+ detail,
603
+ indicator,
604
+ onPress,
605
+ disabled,
606
+ accessibilityLabel,
607
+ size: sizeProp,
608
+ native,
609
+ ...props
610
+ }) {
611
+ const context = useAIInputOptional();
612
+ const size = sizeProp ?? context?.size ?? 'md';
613
+ const height = CONTROL[size];
614
+ const isDisabled = disabled ?? context?.disabled ?? false;
615
+ const slots = aiInputVariants({
616
+ size
617
+ });
618
+
619
+ /*
620
+ * Only a plain string goes to the platform. Passing elements makes it host
621
+ * them, and a hosted view inside a labelled button leaves the width
622
+ * unresolved — which is not an exception anything here could catch but a
623
+ * crash in native code. A pill with a `detail` or an `indicator` is drawn.
624
+ */
625
+ const isNative = (native ?? context?.native ?? false) && hasNativeUI() && typeof label === 'string' && detail === undefined && indicator === undefined;
626
+ if (isNative) {
627
+ return /*#__PURE__*/_jsx(View, {
628
+ ...props,
629
+ className: className,
630
+ children: /*#__PURE__*/_jsx(Button, {
631
+ native: true,
632
+ glass: true,
633
+ variant: "secondary",
634
+ size: "sm",
635
+ accessibilityLabel: accessibilityLabel,
636
+ disabled: isDisabled,
637
+ onPress: onPress,
638
+ children: label
639
+ })
640
+ });
641
+ }
642
+ return /*#__PURE__*/_jsx(Glass, {
643
+ variant: "clear",
644
+ radius: height / 2,
645
+ fallbackClassName: "bg-muted",
646
+ ...props,
647
+ className: className,
648
+ style: {
649
+ height
650
+ },
651
+ children: /*#__PURE__*/_jsxs(AnimatedPressable, {
652
+ accessibilityRole: "button",
653
+ accessibilityLabel: accessibilityLabel,
654
+ accessibilityState: {
655
+ disabled: isDisabled
656
+ },
657
+ disabled: isDisabled,
658
+ onPress: onPress,
659
+ className: slots.pill({
660
+ className: cn('h-full', isDisabled && 'opacity-40')
661
+ }),
662
+ children: [typeof label === 'string' ? /*#__PURE__*/_jsx(Text, {
663
+ size: "sm",
664
+ weight: "medium",
665
+ children: label
666
+ }) : label, typeof detail === 'string' ? /*#__PURE__*/_jsx(Text, {
667
+ size: "sm",
668
+ muted: true,
669
+ children: detail
670
+ }) : detail, indicator]
671
+ })
672
+ });
673
+ }
674
+ /**
675
+ * The trailing button, which is three buttons wearing one shape.
676
+ *
677
+ * Empty and ready, it opens voice mode; with something typed, it sends; while
678
+ * the model is answering, it stops. One control rather than three because only
679
+ * one of them is ever the thing to do, and a row of dimmed siblings is a row
680
+ * of things that look broken.
681
+ */
682
+ function AIInputSubmit({
683
+ className,
684
+ sendLabel = 'Send',
685
+ voiceLabel = 'Voice mode',
686
+ stopLabel = 'Stop',
687
+ native,
688
+ ...props
689
+ }) {
690
+ const context = useAIInput('AIInput.Submit');
691
+ const {
692
+ value,
693
+ status,
694
+ size,
695
+ disabled,
696
+ submit,
697
+ stop,
698
+ voice,
699
+ canVoice,
700
+ canStop
701
+ } = context;
702
+ const diameter = CONTROL[size] + 2;
703
+ const streaming = status === 'streaming' || status === 'submitted';
704
+ const hasText = value.trim().length > 0;
705
+ const onSurface = useCSSVariable('--color-primary-foreground');
706
+ const onSolid = useCSSVariable('--color-background');
707
+ const mode = streaming ? 'stop' : hasText ? 'send' : 'voice';
708
+ const label = mode === 'stop' ? stopLabel : mode === 'send' ? sendLabel : voiceLabel;
709
+ const onPress = mode === 'stop' ? stop : mode === 'send' ? submit : voice;
710
+ const isNative = (native ?? context.native) && hasNativeUI();
711
+
712
+ /*
713
+ * Nothing to send is not something to press.
714
+ *
715
+ * Empty, the button offers voice mode — but only if the app took `onVoice`;
716
+ * with nothing wired to it, it is a live-looking control that does nothing.
717
+ * Same for stop. So the button is disabled whenever the thing it is offering
718
+ * cannot happen, which for an empty composer with no voice mode means it
719
+ * stays inert until something is typed.
720
+ */
721
+ const inert = disabled || (mode === 'send' ? !hasText : mode === 'stop' ? !canStop : !canVoice);
722
+ const glyph = mode === 'send' ? /*#__PURE__*/_jsx(SendArrowIcon, {
723
+ size: 18
724
+ }) : mode === 'stop' ? /*#__PURE__*/_jsx(View, {
725
+ className: "rounded-sm bg-primary-foreground",
726
+ style: {
727
+ width: 11,
728
+ height: 11
729
+ }
730
+ }) : /*#__PURE__*/_jsx(AudioLinesIcon, {
731
+ size: 18
732
+ });
733
+
734
+ /*
735
+ * The platform's own button, tinted. `Button` maps a primary variant onto
736
+ * `glassProminent`, which is the one that keeps an accent fill — drawing the
737
+ * material by hand over a plain button throws that fill away, and this is
738
+ * the one control in the row that is supposed to be filled.
739
+ */
740
+ if (isNative) {
741
+ return /*#__PURE__*/_jsx(View, {
742
+ ...props,
743
+ className: className,
744
+ children: /*#__PURE__*/_jsx(Button, {
745
+ native: true,
746
+ glass: true,
747
+ variant: "primary",
748
+ size: "icon",
749
+ accessibilityLabel: label,
750
+ disabled: inert,
751
+ onPress: onPress,
752
+ children: glyph
753
+ })
754
+ });
755
+ }
756
+ return /*#__PURE__*/_jsx(View, {
757
+ ...props,
758
+ className: cn('overflow-hidden rounded-full', mode === 'voice' ? 'bg-foreground' : 'bg-primary', className),
759
+ style: {
760
+ width: diameter,
761
+ height: diameter
762
+ },
763
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
764
+ accessibilityRole: "button",
765
+ accessibilityLabel: label,
766
+ accessibilityState: {
767
+ disabled: inert
768
+ },
769
+ disabled: inert,
770
+ onPress: onPress,
771
+ className: cn('h-full w-full items-center justify-center', inert && 'opacity-40'),
772
+ children: /*#__PURE__*/_jsx(IconColorProvider, {
773
+ color: mode === 'voice' ? typeof onSolid === 'string' ? onSolid : undefined : typeof onSurface === 'string' ? onSurface : undefined,
774
+ children: glyph
775
+ })
776
+ })
777
+ });
778
+ }
779
+ /**
780
+ * What the toolbar becomes while a recording is running: throw it away, watch
781
+ * it, or keep it. Render it in place of `AIInput.Toolbar` when `status` is
782
+ * `recording`.
783
+ */
784
+ function AIInputRecording({
785
+ className,
786
+ cancelLabel = 'Discard recording',
787
+ confirmLabel = 'Use recording',
788
+ native,
789
+ ...props
790
+ }) {
791
+ const context = useAIInput('AIInput.Recording');
792
+ const {
793
+ size,
794
+ level,
795
+ recordCancel,
796
+ recordConfirm,
797
+ native: contextNative
798
+ } = context;
799
+ const control = CONTROL[size];
800
+ const slots = aiInputVariants({
801
+ size
802
+ });
803
+ const onPrimary = useCSSVariable('--color-primary-foreground');
804
+ const isNative = (native ?? contextNative) && hasNativeUI();
805
+ const meter = /*#__PURE__*/_jsx(View, {
806
+ className: slots.meter(),
807
+ children: /*#__PURE__*/_jsx(Soundwave, {
808
+ variant: "bars",
809
+ mode: "scrolling",
810
+ state: "listening",
811
+ level: level,
812
+ centered: true,
813
+ barWidth: 2.5,
814
+ height: control
815
+ })
816
+ });
817
+ if (isNative) {
818
+ return /*#__PURE__*/_jsxs(View, {
819
+ ...props,
820
+ className: cn(slots.recording({
821
+ className
822
+ }), 'absolute inset-x-0 bottom-0'),
823
+ style: {
824
+ height: NATIVE_ROW_HEIGHT,
825
+ paddingBottom: NATIVE_ROW_FOOT
826
+ },
827
+ children: [/*#__PURE__*/_jsx(Button, {
828
+ native: true,
829
+ glass: true,
830
+ variant: "secondary",
831
+ size: "icon",
832
+ accessibilityLabel: cancelLabel,
833
+ onPress: recordCancel,
834
+ children: /*#__PURE__*/_jsx(XIcon, {
835
+ size: 16
836
+ })
837
+ }), meter, /*#__PURE__*/_jsx(Button, {
838
+ native: true,
839
+ glass: true,
840
+ variant: "primary",
841
+ size: "icon",
842
+ accessibilityLabel: confirmLabel,
843
+ onPress: recordConfirm,
844
+ children: /*#__PURE__*/_jsx(CheckIcon, {
845
+ size: 16
846
+ })
847
+ })]
848
+ });
849
+ }
850
+ return /*#__PURE__*/_jsxs(View, {
851
+ ...props,
852
+ className: slots.recording({
853
+ className
854
+ }),
855
+ children: [/*#__PURE__*/_jsx(Glass, {
856
+ variant: "clear",
857
+ radius: control / 2,
858
+ fallbackClassName: "bg-muted",
859
+ style: {
860
+ width: control,
861
+ height: control
862
+ },
863
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
864
+ accessibilityRole: "button",
865
+ accessibilityLabel: cancelLabel,
866
+ onPress: recordCancel,
867
+ className: "h-full w-full items-center justify-center",
868
+ children: /*#__PURE__*/_jsx(XIcon, {
869
+ size: 16
870
+ })
871
+ })
872
+ }), meter, /*#__PURE__*/_jsx(View, {
873
+ className: "overflow-hidden rounded-full bg-primary",
874
+ style: {
875
+ width: control + 4,
876
+ height: control + 4
877
+ },
878
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
879
+ accessibilityRole: "button",
880
+ accessibilityLabel: confirmLabel,
881
+ onPress: recordConfirm,
882
+ className: "h-full w-full items-center justify-center",
883
+ children: /*#__PURE__*/_jsx(CheckIcon, {
884
+ size: 18,
885
+ color: typeof onPrimary === 'string' ? onPrimary : undefined
886
+ })
887
+ })
888
+ })]
889
+ });
890
+ }
891
+
892
+ /* -------------------------------------------------------------------------- */
893
+ /* Sheet */
894
+ /* -------------------------------------------------------------------------- */
895
+
896
+ const AIInputSheetContext = /*#__PURE__*/createContext(null);
897
+ function useAIInputSheet(component) {
898
+ const context = useContext(AIInputSheetContext);
899
+ if (!context) throw new Error(`${component} must be used within an <AIInput.Sheet>`);
900
+ return context;
901
+ }
902
+ function AIInputSheetScreen({
903
+ className,
904
+ id,
905
+ title,
906
+ trailing,
907
+ children,
908
+ ...props
909
+ }) {
910
+ void id;
911
+ void title;
912
+ void trailing;
913
+ return /*#__PURE__*/_jsx(View, {
914
+ ...props,
915
+ className: cn('gap-4 pb-2', className),
916
+ children: children
917
+ });
918
+ }
919
+ function screensOf(children) {
920
+ return Children.toArray(children).filter(child => /*#__PURE__*/isValidElement(child) && child.type === AIInputSheetScreen);
921
+ }
922
+ /**
923
+ * The sheet the composer's controls open, and the screens it goes on to.
924
+ *
925
+ * A row that leads somewhere pushes a screen onto this sheet rather than
926
+ * opening a second one over it. The sheet stays where it is, the header's
927
+ * close button becomes a back button, and the body slides — which is what
928
+ * makes going one level deeper feel like the same surface rather than another
929
+ * one landing on top of it.
930
+ *
931
+ * ```tsx
932
+ * <AIInput.Sheet open={open} onOpenChange={setOpen}>
933
+ * <AIInput.Sheet.Screen id="root" title="Add to chat">
934
+ * <AIInput.Sheet.Group>
935
+ * <AIInput.Sheet.Row label="Add to project" value="None" to="project" />
936
+ * <AIInput.Sheet.Toggle label="Web search" value={web} onValueChange={setWeb} />
937
+ * </AIInput.Sheet.Group>
938
+ * </AIInput.Sheet.Screen>
939
+ * <AIInput.Sheet.Screen id="project" title="Add to project">…</AIInput.Sheet.Screen>
940
+ * </AIInput.Sheet>
941
+ * ```
942
+ */
943
+ function AIInputSheet({
944
+ children,
945
+ className,
946
+ open: openProp,
947
+ onOpenChange,
948
+ defaultOpen = false,
949
+ initialScreen,
950
+ onScreenChange,
951
+ blur = false,
952
+ size = 'auto',
953
+ detached = true
954
+ }) {
955
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
956
+ const isControlled = openProp !== undefined;
957
+ const open = isControlled ? openProp : internalOpen;
958
+ const screens = screensOf(children);
959
+ const rootId = initialScreen ?? screens[0]?.props.id ?? '';
960
+ const [stack, setStack] = useState(rootId ? [rootId] : []);
961
+ const setOpen = useCallback(next => {
962
+ if (!isControlled) setInternalOpen(next);
963
+ onOpenChange?.(next);
964
+ }, [isControlled, onOpenChange]);
965
+
966
+ /*
967
+ * A closed sheet forgets where it was. Reopening onto a screen somebody
968
+ * pushed and then dismissed is a sheet that answers a question nobody asked
969
+ * again. Done on the way out rather than the way in, where the reset would
970
+ * be visible for a frame before the sheet arrived.
971
+ */
972
+ useEffect(() => {
973
+ if (!open && rootId) setStack([rootId]);
974
+ }, [open, rootId]);
975
+ const push = useCallback(id => {
976
+ setStack(current => current[current.length - 1] === id ? current : [...current, id]);
977
+ onScreenChange?.(id);
978
+ }, [onScreenChange]);
979
+ const pop = useCallback(() => {
980
+ setStack(current => {
981
+ if (current.length <= 1) return current;
982
+ const next = current.slice(0, -1);
983
+ const top = next[next.length - 1];
984
+ if (top) onScreenChange?.(top);
985
+ return next;
986
+ });
987
+ }, [onScreenChange]);
988
+ const close = useCallback(() => setOpen(false), [setOpen]);
989
+ const context = useMemo(() => ({
990
+ stack,
991
+ push,
992
+ pop,
993
+ close
994
+ }), [stack, push, pop, close]);
995
+ const activeId = stack[stack.length - 1];
996
+ const active = screens.find(screen => screen.props.id === activeId) ?? screens[0];
997
+ const depth = stack.length;
998
+ const insets = useSafeAreaInsets();
999
+
1000
+ // A sheet with no screens has nothing to show and no title to head it with.
1001
+ // Below every hook, so the count does not change with the children.
1002
+ if (!active) return null;
1003
+ return /*#__PURE__*/_jsx(BottomSheet, {
1004
+ open: open,
1005
+ onOpenChange: setOpen,
1006
+ children: /*#__PURE__*/_jsx(BottomSheet.Content, {
1007
+ blur: blur,
1008
+ size: size,
1009
+ detached: detached,
1010
+ showClose: false,
1011
+ showGrabber: false,
1012
+ className: "border-0 bg-transparent px-0 pt-0 shadow-none"
1013
+ // The sheet's own bottom padding would sit outside the material and
1014
+ // leave a band of backdrop under it. The glass takes the inset instead.
1015
+ ,
1016
+ style: {
1017
+ paddingBottom: 0
1018
+ },
1019
+ children: /*#__PURE__*/_jsx(AIInputSheetContext.Provider, {
1020
+ value: context,
1021
+ children: /*#__PURE__*/_jsxs(View, {
1022
+ className: cn('overflow-hidden border border-border bg-popover px-4 pt-2', detached ? 'rounded-3xl' : 'rounded-t-3xl border-b-0')
1023
+ // A floating sheet's own bottom margin already clears the home
1024
+ // indicator, so it takes plain padding rather than stacking the
1025
+ // inset on top of the gap.
1026
+ ,
1027
+ style: {
1028
+ paddingBottom: detached ? 16 : Math.max(insets.bottom, 16)
1029
+ },
1030
+ children: [/*#__PURE__*/_jsx(View, {
1031
+ className: "mb-2 self-center",
1032
+ children: /*#__PURE__*/_jsx(View, {
1033
+ className: "h-1 w-10 rounded-full bg-muted-foreground/40"
1034
+ })
1035
+ }), /*#__PURE__*/_jsx(AIInputSheetHeader, {
1036
+ depth: depth,
1037
+ title: active.props.title,
1038
+ trailing: active.props.trailing
1039
+ }), /*#__PURE__*/_jsx(AIInputSheetBody, {
1040
+ activeId: activeId,
1041
+ depth: depth,
1042
+ className: className,
1043
+ children: active
1044
+ })]
1045
+ })
1046
+ })
1047
+ })
1048
+ });
1049
+ }
1050
+ function AIInputSheetHeader({
1051
+ depth,
1052
+ title,
1053
+ trailing
1054
+ }) {
1055
+ const {
1056
+ pop,
1057
+ close
1058
+ } = useAIInputSheet('AIInput.Sheet');
1059
+ const rtl = useDirection() === 'rtl';
1060
+ const nested = depth > 1;
1061
+ const Back = rtl ? ChevronRightIcon : ChevronLeftIcon;
1062
+ return /*#__PURE__*/_jsxs(View, {
1063
+ className: "h-11 flex-row items-center",
1064
+ children: [/*#__PURE__*/_jsx(Glass, {
1065
+ variant: "clear",
1066
+ radius: 17,
1067
+ fallbackClassName: "bg-muted",
1068
+ style: {
1069
+ width: 34,
1070
+ height: 34
1071
+ },
1072
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
1073
+ accessibilityRole: "button",
1074
+ accessibilityLabel: nested ? 'Back' : 'Close',
1075
+ onPress: nested ? pop : close,
1076
+ className: "h-full w-full items-center justify-center",
1077
+ children: nested ? /*#__PURE__*/_jsx(Back, {
1078
+ size: 20
1079
+ }) : /*#__PURE__*/_jsx(XIcon, {
1080
+ size: 18
1081
+ })
1082
+ })
1083
+ }), /*#__PURE__*/_jsx(View, {
1084
+ pointerEvents: "none",
1085
+ className: "absolute inset-x-0 items-center",
1086
+ children: typeof title === 'string' ? /*#__PURE__*/_jsx(Text, {
1087
+ size: "xl",
1088
+ weight: "semibold",
1089
+ numberOfLines: 1,
1090
+ children: title
1091
+ }) : title
1092
+ }), /*#__PURE__*/_jsx(View, {
1093
+ className: "flex-1"
1094
+ }), trailing]
1095
+ });
1096
+ }
1097
+ function AIInputSheetBody({
1098
+ activeId,
1099
+ depth,
1100
+ className,
1101
+ children
1102
+ }) {
1103
+ const {
1104
+ width
1105
+ } = useWindowDimensions();
1106
+ const height = useSharedValue(-1);
1107
+ const paneRef = useRef(null);
1108
+
1109
+ /*
1110
+ * Whether the screen is lifted out of the flow onto its own layer.
1111
+ *
1112
+ * Not on the first layout, which is what a wizard inside a fixed frame can
1113
+ * afford. A sheet sized to its content is still sliding up while its first
1114
+ * screen measures, and switching the screen to absolute mid-flight takes the
1115
+ * height out from under the sheet and puts it back a frame later — which is
1116
+ * the sheet arriving with a stutter in it.
1117
+ *
1118
+ * So the first screen stays in the flow and the sheet opens around a real
1119
+ * height. The layer is only needed once two screens have to overlap, which
1120
+ * is the moment somebody navigates, and by then the height below is already
1121
+ * recorded to travel from.
1122
+ */
1123
+ const [layered, setLayered] = useState(false);
1124
+
1125
+ /*
1126
+ * Which way the screen slides, worked out from the move rather than from
1127
+ * whatever caused it — a row, a back button and a caller pushing directly
1128
+ * are the same move to the reader, and only the change in depth says which
1129
+ * way it went.
1130
+ *
1131
+ * Read during render, not in an effect: the arriving screen's animation is
1132
+ * fixed when it mounts, and an effect runs after that, which would leave
1133
+ * every transition playing the direction of the one before it.
1134
+ */
1135
+ const previousDepth = useRef(depth);
1136
+ const direction = depth >= previousDepth.current ? 1 : -1;
1137
+ const navigated = useRef(false);
1138
+ useEffect(() => {
1139
+ if (depth !== previousDepth.current) {
1140
+ previousDepth.current = depth;
1141
+ navigated.current = true;
1142
+ }
1143
+ }, [depth]);
1144
+
1145
+ // The first navigation is what needs two screens on top of each other.
1146
+ useEffect(() => {
1147
+ if (navigated.current) setLayered(true);
1148
+ }, [activeId]);
1149
+
1150
+ // Move the reader onto the screen that just arrived, rather than leaving
1151
+ // focus on the row that is no longer there.
1152
+ useEffect(() => {
1153
+ if (!navigated.current) return;
1154
+ const node = paneRef.current;
1155
+ if (!node) return;
1156
+ const tag = findNodeHandle(node);
1157
+ if (tag != null) AccessibilityInfo.setAccessibilityFocus(tag);
1158
+ }, [activeId]);
1159
+
1160
+ /*
1161
+ * Until the first screen has been measured the pane stays in the flow, so a
1162
+ * sheet sized to its content has a real height on the first pass. Absolute
1163
+ * from the start would measure zero — the container's only child would
1164
+ * contribute nothing — and the sheet would open around a screen nobody can
1165
+ * see.
1166
+ */
1167
+ const onPaneLayout = useCallback(event => {
1168
+ const next = event.nativeEvent.layout.height;
1169
+ if (next <= 0) return;
1170
+ // Recorded whether or not it is being used yet, so the first push has a
1171
+ // height to travel from rather than growing out of nothing.
1172
+ if (layered) {
1173
+ height.value = withTiming(next, {
1174
+ duration: HEIGHT_DURATION,
1175
+ easing: EASE
1176
+ });
1177
+ return;
1178
+ }
1179
+ height.value = next;
1180
+ }, [layered, height]);
1181
+ const heightStyle = useAnimatedStyle(() => height.value < 0 ? {} : {
1182
+ height: height.value
1183
+ });
1184
+
1185
+ /*
1186
+ * Written out rather than taken from the stock builders, which carry an
1187
+ * initial opacity and nothing else — the distance is the part worth
1188
+ * controlling here, and none of them lets it be set.
1189
+ */
1190
+ const offset = width > 0 ? width * SLIDE_FRACTION : 60;
1191
+ const entering = useCallback(() => {
1192
+ 'worklet';
1193
+
1194
+ return {
1195
+ initialValues: {
1196
+ opacity: 0,
1197
+ transform: [{
1198
+ translateX: direction * offset
1199
+ }]
1200
+ },
1201
+ animations: {
1202
+ opacity: withTiming(1, {
1203
+ duration: ENTER_DURATION,
1204
+ easing: EASE
1205
+ }),
1206
+ transform: [{
1207
+ translateX: withTiming(0, {
1208
+ duration: ENTER_DURATION,
1209
+ easing: EASE
1210
+ })
1211
+ }]
1212
+ }
1213
+ };
1214
+ }, [direction, offset]);
1215
+ return /*#__PURE__*/_jsx(Animated.View, {
1216
+ style: layered ? heightStyle : undefined,
1217
+ className: cn('w-full overflow-hidden', className),
1218
+ children: /*#__PURE__*/_jsx(Animated.View, {
1219
+ entering: layered ? entering : undefined,
1220
+ exiting: FadeOut.duration(EXIT_DURATION),
1221
+ className: layered ? 'absolute inset-x-0 top-0' : 'w-full',
1222
+ children: /*#__PURE__*/_jsx(View, {
1223
+ ref: paneRef,
1224
+ onLayout: onPaneLayout,
1225
+ className: "w-full pt-2",
1226
+ children: children
1227
+ })
1228
+ }, activeId ?? '__empty__')
1229
+ });
1230
+ }
1231
+
1232
+ /* -------------------------------------------------------------------------- */
1233
+ /* Sheet content */
1234
+ /* -------------------------------------------------------------------------- */
1235
+
1236
+ /** A card of related rows, with a hairline between each pair. */
1237
+ function AIInputSheetGroup({
1238
+ className,
1239
+ footnote,
1240
+ children,
1241
+ ...props
1242
+ }) {
1243
+ const rows = Children.toArray(children);
1244
+ return /*#__PURE__*/_jsxs(View, {
1245
+ className: "gap-2",
1246
+ children: [/*#__PURE__*/_jsx(View, {
1247
+ ...props,
1248
+ className: cn('overflow-hidden rounded-[20px] bg-muted/50', className),
1249
+ children: rows.map((row, index) => /*#__PURE__*/_jsxs(View, {
1250
+ children: [index > 0 ? /*#__PURE__*/_jsx(View, {
1251
+ className: "ms-4 h-px bg-border"
1252
+ }) : null, row]
1253
+ }, index))
1254
+ }), typeof footnote === 'string' ? /*#__PURE__*/_jsx(Text, {
1255
+ size: "sm",
1256
+ muted: true,
1257
+ className: "px-2",
1258
+ children: footnote
1259
+ }) : footnote]
1260
+ });
1261
+ }
1262
+ function AIInputSheetRow({
1263
+ className,
1264
+ label,
1265
+ description,
1266
+ icon,
1267
+ value,
1268
+ to,
1269
+ onPress,
1270
+ disabled = false,
1271
+ ...props
1272
+ }) {
1273
+ const {
1274
+ push
1275
+ } = useAIInputSheet('AIInput.Sheet.Row');
1276
+ const rtl = useDirection() === 'rtl';
1277
+ const Chevron = rtl ? ChevronLeftIcon : ChevronRightIcon;
1278
+ const handlePress = useCallback(() => {
1279
+ onPress?.();
1280
+ if (to) push(to);
1281
+ }, [onPress, push, to]);
1282
+ const body = /*#__PURE__*/_jsxs(_Fragment, {
1283
+ children: [icon, /*#__PURE__*/_jsxs(View, {
1284
+ className: "flex-1 gap-0.5",
1285
+ children: [typeof label === 'string' ? /*#__PURE__*/_jsx(Text, {
1286
+ size: "base",
1287
+ children: label
1288
+ }) : label, typeof description === 'string' ? /*#__PURE__*/_jsx(Text, {
1289
+ size: "sm",
1290
+ muted: true,
1291
+ children: description
1292
+ }) : description]
1293
+ }), typeof value === 'string' ? /*#__PURE__*/_jsx(Text, {
1294
+ size: "base",
1295
+ muted: true,
1296
+ children: value
1297
+ }) : value, to ? /*#__PURE__*/_jsx(Chevron, {
1298
+ size: 18
1299
+ }) : null]
1300
+ });
1301
+ const interactive = Boolean(to || onPress);
1302
+ if (!interactive) {
1303
+ return /*#__PURE__*/_jsx(View, {
1304
+ ...props,
1305
+ className: cn('flex-row items-center gap-3 px-4 py-4', className),
1306
+ children: body
1307
+ });
1308
+ }
1309
+ return /*#__PURE__*/_jsx(AnimatedPressable, {
1310
+ pressScale: 1,
1311
+ pressOpacity: 0.6,
1312
+ ...props,
1313
+ accessibilityRole: "button",
1314
+ accessibilityState: {
1315
+ disabled
1316
+ },
1317
+ disabled: disabled,
1318
+ onPress: handlePress,
1319
+ className: cn('flex-row items-center gap-3 px-4 py-4', disabled && 'opacity-40', className),
1320
+ children: body
1321
+ });
1322
+ }
1323
+ function AIInputSheetToggle({
1324
+ className,
1325
+ label,
1326
+ description,
1327
+ icon,
1328
+ value,
1329
+ onValueChange,
1330
+ disabled = false,
1331
+ ...props
1332
+ }) {
1333
+ return /*#__PURE__*/_jsxs(View, {
1334
+ ...props,
1335
+ className: cn('flex-row items-center gap-3 px-4 py-2.5', className),
1336
+ children: [icon, /*#__PURE__*/_jsxs(View, {
1337
+ className: "flex-1 gap-0.5",
1338
+ children: [typeof label === 'string' ? /*#__PURE__*/_jsx(Text, {
1339
+ size: "base",
1340
+ children: label
1341
+ }) : label, typeof description === 'string' ? /*#__PURE__*/_jsx(Text, {
1342
+ size: "sm",
1343
+ muted: true,
1344
+ children: description
1345
+ }) : description]
1346
+ }), /*#__PURE__*/_jsx(Switch, {
1347
+ value: value,
1348
+ onValueChange: onValueChange,
1349
+ disabled: disabled,
1350
+ label: typeof label === 'string' ? label : undefined
1351
+ })]
1352
+ });
1353
+ }
1354
+ /** One of a set, with a tick on the one in force. */
1355
+ function AIInputSheetChoice({
1356
+ className,
1357
+ label,
1358
+ description,
1359
+ badge,
1360
+ selected = false,
1361
+ onPress,
1362
+ disabled = false,
1363
+ ...props
1364
+ }) {
1365
+ const tint = useCSSVariable('--color-primary');
1366
+ return /*#__PURE__*/_jsxs(AnimatedPressable, {
1367
+ pressScale: 1,
1368
+ pressOpacity: 0.6,
1369
+ ...props,
1370
+ accessibilityRole: "radio",
1371
+ accessibilityState: {
1372
+ checked: selected,
1373
+ disabled
1374
+ },
1375
+ disabled: disabled,
1376
+ onPress: onPress,
1377
+ className: cn('flex-row items-start gap-3 px-4 py-4', disabled && 'opacity-40', className),
1378
+ children: [/*#__PURE__*/_jsxs(View, {
1379
+ className: "flex-1 gap-0.5",
1380
+ children: [/*#__PURE__*/_jsxs(View, {
1381
+ className: "flex-row items-center gap-2",
1382
+ children: [typeof label === 'string' ? /*#__PURE__*/_jsx(Text, {
1383
+ size: "lg",
1384
+ weight: "medium",
1385
+ children: label
1386
+ }) : label, badge]
1387
+ }), typeof description === 'string' ? /*#__PURE__*/_jsx(Text, {
1388
+ size: "sm",
1389
+ muted: true,
1390
+ children: description
1391
+ }) : description]
1392
+ }), selected ? /*#__PURE__*/_jsx(CheckIcon, {
1393
+ size: 20,
1394
+ color: typeof tint === 'string' ? tint : undefined
1395
+ }) : null]
1396
+ });
1397
+ }
1398
+
1399
+ /* -------------------------------------------------------------------------- */
1400
+ /* Voice mode */
1401
+ /* -------------------------------------------------------------------------- */
1402
+
1403
+ /**
1404
+ * The screen a voice conversation happens on: no field, no keyboard, one
1405
+ * microphone and a way out.
1406
+ *
1407
+ * It is a surface rather than an overlay — render it as its own route or
1408
+ * inside a modal of your own. It provides the composer's context, so
1409
+ * `AIInput.Action` and `AIInput.Pill` work in the bottom row without an
1410
+ * `AIInput` around them.
1411
+ */
1412
+ function AIInputVoiceMode({
1413
+ className,
1414
+ children,
1415
+ state = 'listening',
1416
+ level,
1417
+ title,
1418
+ onMicPress,
1419
+ micLabel = 'Mute',
1420
+ onClose,
1421
+ closeLabel = 'End voice mode',
1422
+ size = 'md',
1423
+ native = false,
1424
+ ...props
1425
+ }) {
1426
+ const insets = useSafeAreaInsets();
1427
+ const onSolid = useCSSVariable('--color-background');
1428
+ const control = CONTROL[size] + 4;
1429
+ const context = useMemo(() => ({
1430
+ value: '',
1431
+ setValue: () => {},
1432
+ status: 'ready',
1433
+ size,
1434
+ level,
1435
+ disabled: false,
1436
+ native: native && hasNativeUI(),
1437
+ minRows: 1,
1438
+ maxRows: 5,
1439
+ focused: false,
1440
+ setFocused: () => {},
1441
+ submit: () => {},
1442
+ stop: () => {},
1443
+ canVoice: false,
1444
+ canStop: false,
1445
+ recordCancel: () => {},
1446
+ recordConfirm: () => {},
1447
+ voice: () => {}
1448
+ }), [level, native, size]);
1449
+ return /*#__PURE__*/_jsx(AIInputContext.Provider, {
1450
+ value: context,
1451
+ children: /*#__PURE__*/_jsxs(View, {
1452
+ ...props,
1453
+ className: cn('flex-1 bg-background', className),
1454
+ children: [/*#__PURE__*/_jsx(Soundwave, {
1455
+ variant: "ambient",
1456
+ state: state,
1457
+ level: level,
1458
+ pointerEvents: "none",
1459
+ className: "absolute inset-0"
1460
+ }), /*#__PURE__*/_jsxs(View, {
1461
+ className: "flex-1 items-center justify-center gap-8 px-6",
1462
+ children: [typeof title === 'string' ? /*#__PURE__*/_jsx(Text, {
1463
+ size: "xl",
1464
+ className: "text-center",
1465
+ children: title
1466
+ }) : title, /*#__PURE__*/_jsxs(View, {
1467
+ className: "items-center gap-5",
1468
+ children: [/*#__PURE__*/_jsx(Soundwave, {
1469
+ variant: "pills",
1470
+ state: state,
1471
+ level: level,
1472
+ height: 28
1473
+ }), /*#__PURE__*/_jsx(Glass, {
1474
+ variant: "clear",
1475
+ radius: 44,
1476
+ fallbackClassName: "border border-border bg-muted",
1477
+ style: {
1478
+ width: 88,
1479
+ height: 88
1480
+ },
1481
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
1482
+ accessibilityRole: "button",
1483
+ accessibilityLabel: micLabel,
1484
+ onPress: onMicPress,
1485
+ className: "h-full w-full items-center justify-center",
1486
+ children: /*#__PURE__*/_jsx(MicIcon, {
1487
+ size: 32
1488
+ })
1489
+ })
1490
+ })]
1491
+ })]
1492
+ }), /*#__PURE__*/_jsxs(View, {
1493
+ className: "flex-row items-center gap-3 px-5",
1494
+ style: {
1495
+ paddingBottom: Math.max(insets.bottom, 20)
1496
+ },
1497
+ children: [children, /*#__PURE__*/_jsx(View, {
1498
+ className: "flex-1"
1499
+ }), native && hasNativeUI() ? /*#__PURE__*/_jsx(Button, {
1500
+ native: true,
1501
+ glass: true,
1502
+ variant: "primary",
1503
+ size: "icon",
1504
+ accessibilityLabel: closeLabel,
1505
+ onPress: onClose,
1506
+ children: /*#__PURE__*/_jsx(XIcon, {
1507
+ size: 16
1508
+ })
1509
+ }) : /*#__PURE__*/_jsx(View, {
1510
+ className: "overflow-hidden rounded-full bg-foreground",
1511
+ style: {
1512
+ width: control,
1513
+ height: control
1514
+ },
1515
+ children: /*#__PURE__*/_jsx(AnimatedPressable, {
1516
+ accessibilityRole: "button",
1517
+ accessibilityLabel: closeLabel,
1518
+ onPress: onClose,
1519
+ className: "h-full w-full items-center justify-center",
1520
+ children: /*#__PURE__*/_jsx(XIcon, {
1521
+ size: 18,
1522
+ color: typeof onSolid === 'string' ? onSolid : undefined
1523
+ })
1524
+ })
1525
+ })]
1526
+ })]
1527
+ })
1528
+ });
1529
+ }
1530
+
1531
+ /* -------------------------------------------------------------------------- */
1532
+
1533
+ AIInputRoot.displayName = 'AIInput';
1534
+ AIInputField.displayName = 'AIInput.Field';
1535
+ AIInputRow.displayName = 'AIInput.Row';
1536
+ AIInputToolbar.displayName = 'AIInput.Toolbar';
1537
+ AIInputSpacer.displayName = 'AIInput.Spacer';
1538
+ AIInputAction.displayName = 'AIInput.Action';
1539
+ AIInputPill.displayName = 'AIInput.Pill';
1540
+ AIInputSubmit.displayName = 'AIInput.Submit';
1541
+ AIInputRecording.displayName = 'AIInput.Recording';
1542
+ AIInputSheetScreen.displayName = 'AIInput.Sheet.Screen';
1543
+ AIInputSheetGroup.displayName = 'AIInput.Sheet.Group';
1544
+ AIInputSheetRow.displayName = 'AIInput.Sheet.Row';
1545
+ AIInputSheetToggle.displayName = 'AIInput.Sheet.Toggle';
1546
+ AIInputSheetChoice.displayName = 'AIInput.Sheet.Choice';
1547
+ AIInputSheet.displayName = 'AIInput.Sheet';
1548
+ AIInputVoiceMode.displayName = 'AIInput.VoiceMode';
1549
+ const Sheet = Object.assign(AIInputSheet, {
1550
+ Screen: AIInputSheetScreen,
1551
+ Group: AIInputSheetGroup,
1552
+ Row: AIInputSheetRow,
1553
+ Toggle: AIInputSheetToggle,
1554
+ Choice: AIInputSheetChoice
1555
+ });
1556
+ export const AIInput = Object.assign(AIInputRoot, {
1557
+ Field: AIInputField,
1558
+ Row: AIInputRow,
1559
+ Toolbar: AIInputToolbar,
1560
+ Spacer: AIInputSpacer,
1561
+ Action: AIInputAction,
1562
+ Pill: AIInputPill,
1563
+ Submit: AIInputSubmit,
1564
+ Recording: AIInputRecording,
1565
+ Sheet,
1566
+ VoiceMode: AIInputVoiceMode
1567
+ });
1568
+ //# sourceMappingURL=index.js.map