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.
- package/lib/module/components/ai-input/ai-input-growth.js +62 -0
- package/lib/module/components/ai-input/ai-input-growth.js.map +1 -0
- package/lib/module/components/ai-input/index.js +1568 -0
- package/lib/module/components/ai-input/index.js.map +1 -0
- package/lib/module/components/alert/index.js +1 -1
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/avatar/avatar-source.js +15 -0
- package/lib/module/components/avatar/avatar-source.js.map +1 -0
- package/lib/module/components/avatar/index.js +32 -31
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +3 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button-group/index.js +2 -3
- package/lib/module/components/button-group/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +10 -7
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/collapsible/index.js +1 -1
- package/lib/module/components/collapsible/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +18 -14
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/field/index.js +7 -8
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/label/index.js +1 -1
- package/lib/module/components/label/index.js.map +1 -1
- package/lib/module/components/marquee/index.js +36 -11
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/marquee/marquee-math.js +22 -6
- package/lib/module/components/marquee/marquee-math.js.map +1 -1
- package/lib/module/components/plan/index.js +2 -1
- package/lib/module/components/plan/index.js.map +1 -1
- package/lib/module/components/plot/index.js +27 -9
- package/lib/module/components/plot/index.js.map +1 -1
- package/lib/module/components/search-bar/index.js +11 -3
- package/lib/module/components/search-bar/index.js.map +1 -1
- package/lib/module/components/split-view/index.js +26 -27
- package/lib/module/components/split-view/index.js.map +1 -1
- package/lib/module/components/split-view/split-view-lifecycle.js +32 -0
- package/lib/module/components/split-view/split-view-lifecycle.js.map +1 -0
- package/lib/module/components/splitter/index.js +7 -13
- package/lib/module/components/splitter/index.js.map +1 -1
- package/lib/module/components/splitter/splitter-math.js +14 -0
- package/lib/module/components/splitter/splitter-math.js.map +1 -1
- package/lib/module/components/tabs/index.js +1 -1
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +156 -42
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/timeline/timeline-geometry.js +11 -0
- package/lib/module/components/timeline/timeline-geometry.js.map +1 -1
- package/lib/module/components/toast/index.js +1 -1
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/tooltip/index.js +2 -2
- package/lib/module/components/tooltip/index.js.map +1 -1
- package/lib/module/components/tree/index.js +2 -2
- package/lib/module/components/tree/index.js.map +1 -1
- package/lib/module/components/typography/index.js +2 -1
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +22 -3
- package/lib/module/components/waterfall-chart/index.js.map +1 -1
- package/lib/module/hooks/use-disclosure.js +10 -8
- package/lib/module/hooks/use-disclosure.js.map +1 -1
- package/lib/module/icons/index.js +135 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +5 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/controllable-state.js +61 -0
- package/lib/module/primitives/controllable-state.js.map +1 -0
- package/lib/module/primitives/finite-chart.js +15 -0
- package/lib/module/primitives/finite-chart.js.map +1 -0
- package/lib/module/primitives/glass.js +136 -0
- package/lib/module/primitives/glass.js.map +1 -0
- package/lib/module/primitives/scrim.js +8 -1
- package/lib/module/primitives/scrim.js.map +1 -1
- package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts +59 -0
- package/lib/typescript/src/components/ai-input/ai-input-growth.d.ts.map +1 -0
- package/lib/typescript/src/components/ai-input/index.d.ts +423 -0
- package/lib/typescript/src/components/ai-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/avatar-source.d.ts +4 -0
- package/lib/typescript/src/components/avatar/avatar-source.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +10 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts +15 -6
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/marquee-math.d.ts +6 -0
- package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -1
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
- package/lib/typescript/src/components/plot/index.d.ts +2 -1
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
- package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/index.d.ts.map +1 -1
- package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts +14 -0
- package/lib/typescript/src/components/split-view/split-view-lifecycle.d.ts.map +1 -0
- package/lib/typescript/src/components/splitter/index.d.ts +2 -2
- package/lib/typescript/src/components/splitter/index.d.ts.map +1 -1
- package/lib/typescript/src/components/splitter/splitter-math.d.ts +2 -0
- package/lib/typescript/src/components/splitter/splitter-math.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +24 -2
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/timeline-geometry.d.ts +2 -0
- package/lib/typescript/src/components/timeline/timeline-geometry.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +2 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-disclosure.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +18 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +3 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/controllable-state.d.ts +33 -0
- package/lib/typescript/src/primitives/controllable-state.d.ts.map +1 -0
- package/lib/typescript/src/primitives/finite-chart.d.ts +5 -0
- package/lib/typescript/src/primitives/finite-chart.d.ts.map +1 -0
- package/lib/typescript/src/primitives/glass.d.ts +43 -0
- package/lib/typescript/src/primitives/glass.d.ts.map +1 -0
- package/lib/typescript/src/primitives/scrim.d.ts +7 -0
- package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/components/ai-input/ai-input-growth.ts +65 -0
- package/src/components/ai-input/index.tsx +1937 -0
- package/src/components/alert/index.tsx +1 -1
- package/src/components/avatar/avatar-source.ts +18 -0
- package/src/components/avatar/index.tsx +35 -27
- package/src/components/bottom-sheet/index.tsx +15 -3
- package/src/components/button-group/index.tsx +1 -1
- package/src/components/carousel/index.tsx +10 -11
- package/src/components/collapsible/index.tsx +1 -1
- package/src/components/drawer/index.tsx +24 -20
- package/src/components/field/index.tsx +4 -4
- package/src/components/label/index.tsx +1 -1
- package/src/components/marquee/index.tsx +56 -12
- package/src/components/marquee/marquee-math.ts +26 -3
- package/src/components/plan/index.tsx +6 -2
- package/src/components/plot/index.tsx +37 -11
- package/src/components/search-bar/index.tsx +9 -3
- package/src/components/split-view/index.tsx +24 -33
- package/src/components/split-view/split-view-lifecycle.ts +43 -0
- package/src/components/splitter/index.tsx +7 -11
- package/src/components/splitter/splitter-math.ts +28 -0
- package/src/components/tabs/index.tsx +1 -1
- package/src/components/timeline/index.tsx +189 -34
- package/src/components/timeline/timeline-geometry.ts +11 -0
- package/src/components/toast/index.tsx +6 -1
- package/src/components/tooltip/index.tsx +1 -1
- package/src/components/tree/index.tsx +2 -2
- package/src/components/typography/index.tsx +2 -2
- package/src/components/waterfall-chart/index.tsx +37 -4
- package/src/hooks/use-disclosure.ts +7 -12
- package/src/icons/index.tsx +106 -0
- package/src/index.ts +27 -0
- package/src/primitives/controllable-state.ts +112 -0
- package/src/primitives/finite-chart.ts +14 -0
- package/src/primitives/glass.tsx +171 -0
- 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
|