@vvedantb/native-ui 0.13.0 → 0.17.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/README.md +40 -19
- package/package.json +36 -3
- package/src/BottomSheetContainer.tsx +2 -2
- package/src/BulkActionBar.tsx +240 -105
- package/src/Button.tsx +11 -7
- package/src/ConfirmDialog.tsx +25 -35
- package/src/Dialog.tsx +47 -35
- package/src/DividedList.tsx +11 -7
- package/src/DropdownMenu.tsx +22 -8
- package/src/FloatingSurface.tsx +5 -2
- package/src/FloatingTabBar.tsx +13 -22
- package/src/HeaderIconButton.tsx +4 -3
- package/src/Input.tsx +2 -2
- package/src/LoadingState.tsx +132 -0
- package/src/PageTitle.tsx +9 -2
- package/src/Pager.tsx +62 -0
- package/src/PasswordInput.tsx +1 -1
- package/src/PillTabs.tsx +144 -34
- package/src/Progress.tsx +9 -7
- package/src/Select.tsx +4 -4
- package/src/Sheet.tsx +29 -22
- package/src/SheetRow.tsx +27 -9
- package/src/Skeleton.tsx +91 -9
- package/src/SurfaceRow.tsx +111 -0
- package/src/SwipeableRow.tsx +43 -0
- package/src/Switch.tsx +6 -4
- package/src/Tabs.tsx +68 -13
- package/src/Toast.tsx +107 -65
- package/src/ai/activity-step-detail.tsx +1 -3
- package/src/ai/activity-step-label.ts +981 -0
- package/src/ai/activity-step-types.ts +107 -0
- package/src/ai/activity-tasks-utils.ts +199 -0
- package/src/ai/activity-timeline.ts +45 -0
- package/src/ai/attachments.tsx +3 -1
- package/src/ai/chat-text.ts +13 -7
- package/src/ai/chat-thread.tsx +34 -27
- package/src/ai/choice-question.tsx +154 -35
- package/src/ai/code-block.tsx +189 -49
- package/src/ai/collapsible.tsx +19 -13
- package/src/ai/conversation.tsx +390 -57
- package/src/ai/icon-button.tsx +41 -6
- package/src/ai/index.ts +38 -1
- package/src/ai/message-action-sheet.tsx +4 -1
- package/src/ai/message.tsx +60 -31
- package/src/ai/prompt-input-menu.tsx +197 -0
- package/src/ai/prompt-input.tsx +301 -75
- package/src/ai/shimmer.tsx +4 -6
- package/src/ai/trace.tsx +54 -17
- package/src/ai/unified-diff.tsx +182 -0
- package/src/auth/GoogleSignInButton.tsx +6 -3
- package/src/auth/SignInScreen.tsx +11 -2
- package/src/brand/ProviderIcon.tsx +136 -0
- package/src/brand/index.ts +2 -0
- package/src/configureTheme.ts +39 -11
- package/src/controlStyles.ts +46 -30
- package/src/entries/gesture-handler.ts +4 -0
- package/src/entries/keyboard-controller.ts +4 -0
- package/src/entries/markdown.ts +1 -1
- package/src/entries/primitives.ts +24 -3
- package/src/entries/theme.ts +2 -1
- package/src/entries/video.ts +4 -0
- package/src/entries/webview.ts +6 -0
- package/src/floating.ts +25 -4
- package/src/haptics.ts +30 -4
- package/src/highlight.ts +302 -1
- package/src/index.ts +27 -5
- package/src/loadingGrid.ts +75 -0
- package/src/markdown/Markdown.tsx +234 -32
- package/src/markdown/MarkdownTable.tsx +188 -0
- package/src/markdown/tableText.ts +58 -0
- package/src/marketing/UpgradeToProCard.tsx +4 -3
- package/src/media/ImageGallery.tsx +224 -0
- package/src/media/MediaCard.tsx +100 -0
- package/src/media/imageGrid.ts +51 -0
- package/src/motion.ts +33 -0
- package/src/pagerSwipe.ts +102 -0
- package/src/palette.ts +8 -1
- package/src/primitives/button.tsx +4 -1
- package/src/primitives/keyboard-controller.tsx +106 -0
- package/src/primitives/keyboard-parts.tsx +104 -0
- package/src/primitives/overlay.tsx +133 -45
- package/src/primitives/pager-gesture.tsx +131 -0
- package/src/primitives/sheet-drag.tsx +305 -0
- package/src/primitives/sheet-gesture.tsx +181 -0
- package/src/primitives/swipe-row-gesture.tsx +290 -0
- package/src/primitives/toast-drag-gesture.tsx +92 -0
- package/src/primitives/web-click-guard.ts +50 -0
- package/src/relativeTime.ts +94 -0
- package/src/router/RouterStackPageHeader.tsx +2 -2
- package/src/settings/SettingsEmptyState.tsx +20 -10
- package/src/settings/SettingsField.tsx +11 -5
- package/src/settings/SettingsSection.tsx +8 -5
- package/src/settings/SettingsToggleRow.tsx +55 -22
- package/src/settings/relaxedSizeStyle.ts +11 -0
- package/src/sheetDrag.ts +67 -0
- package/src/swipeRow.ts +169 -0
- package/src/tabBarRoutes.ts +18 -0
- package/src/tabIndicator.ts +71 -0
- package/src/tailwind-preset.cjs +13 -6
- package/src/tailwind-preset.d.cts +5 -0
- package/src/theme.tsx +6 -2
- package/src/toastDrag.ts +58 -0
- package/src/typography.ts +20 -3
- package/src/useScheme.ts +10 -1
- package/src/video/InlineVideo.tsx +91 -0
- package/src/webview/HtmlView.tsx +91 -0
- package/src/webview/HtmlView.web.tsx +21 -0
- package/src/webview/htmlView.ts +45 -0
package/src/ai/prompt-input.tsx
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { useState, type ReactNode } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useContext, useState, type ReactNode } from "react";
|
|
2
|
+
import { Platform, Pressable, Text, View, useWindowDimensions } from "react-native";
|
|
3
3
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
-
import { IconArrowUp, IconPlayerStopFilled } from "@tabler/icons-react-native";
|
|
4
|
+
import { IconArrowUp, IconChevronDown, IconPlayerStopFilled } from "@tabler/icons-react-native";
|
|
5
5
|
import { cn } from "../cn";
|
|
6
|
-
import { fieldStyle, fieldTextStyle, typeStyle } from "../controlStyles";
|
|
6
|
+
import { MIN_TOUCH_TARGET, fieldStyle, fieldTextStyle, sizeStyle, touchSlop, typeStyle } from "../controlStyles";
|
|
7
|
+
import { withAlpha } from "../palette";
|
|
8
|
+
import { tokens } from "../tokens";
|
|
7
9
|
import { Input as FieldInput } from "../primitives/input";
|
|
10
|
+
import { COMPOSER_INPUT_ID, KeyboardAvoidingView, KeyboardLiftContext } from "../primitives/keyboard-parts";
|
|
11
|
+
import { usePressFeedback } from "../motion";
|
|
8
12
|
import { useControllableState } from "../primitives/use-controllable-state";
|
|
9
13
|
import { useLargerText } from "../theme";
|
|
10
14
|
import { useScheme } from "../useScheme";
|
|
11
|
-
import { ChatIconButton, type ChatIconButtonProps } from "./icon-button";
|
|
15
|
+
import { ChatIconButton, type ChatIconButtonProps, type ChatIconButtonSize } from "./icon-button";
|
|
12
16
|
|
|
13
17
|
/** The `ai` SDK chat status the web `PromptInputSubmit` takes. */
|
|
14
18
|
export type ChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
@@ -24,6 +28,8 @@ export type PromptInputSubmitProps = {
|
|
|
24
28
|
onStop?: () => void;
|
|
25
29
|
/** Whether there is something to send. Default `true`. */
|
|
26
30
|
canSubmit?: boolean;
|
|
31
|
+
/** Default `sm` (32). `md` is 36, the `pill` composer's send button. */
|
|
32
|
+
size?: ChatIconButtonSize;
|
|
27
33
|
testID?: string;
|
|
28
34
|
};
|
|
29
35
|
|
|
@@ -34,16 +40,17 @@ export type PromptInputSubmitProps = {
|
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Send, or stop while the AI answers: the web `default` `icon-sm` button
|
|
37
|
-
* (32, round, slop to 44). Send is an up arrow on the primary fill,
|
|
38
|
-
* `
|
|
39
|
-
* `submitted` or `streaming` it is a filled square labelled
|
|
40
|
-
* `onStop` (disabled without one).
|
|
43
|
+
* (32, round, slop to 44). Send is an up arrow on the primary fill, faded by
|
|
44
|
+
* the disabled `Button` with nothing to send, as the web send button. While
|
|
45
|
+
* `status` is `submitted` or `streaming` it is a filled square labelled
|
|
46
|
+
* "Stop" that calls `onStop` (disabled without one).
|
|
41
47
|
*/
|
|
42
48
|
export function PromptInputSubmit({
|
|
43
49
|
status,
|
|
44
50
|
onSubmit,
|
|
45
51
|
onStop,
|
|
46
52
|
canSubmit = true,
|
|
53
|
+
size,
|
|
47
54
|
testID,
|
|
48
55
|
}: PromptInputSubmitProps) {
|
|
49
56
|
if (isGenerating(status)) {
|
|
@@ -52,6 +59,7 @@ export function PromptInputSubmit({
|
|
|
52
59
|
icon={IconPlayerStopFilled}
|
|
53
60
|
label="Stop"
|
|
54
61
|
variant="primary"
|
|
62
|
+
size={size}
|
|
55
63
|
onPress={onStop}
|
|
56
64
|
disabled={!onStop}
|
|
57
65
|
testID={testID}
|
|
@@ -62,7 +70,8 @@ export function PromptInputSubmit({
|
|
|
62
70
|
<ChatIconButton
|
|
63
71
|
icon={IconArrowUp}
|
|
64
72
|
label="Send"
|
|
65
|
-
variant=
|
|
73
|
+
variant="primary"
|
|
74
|
+
size={size}
|
|
66
75
|
onPress={onSubmit}
|
|
67
76
|
disabled={!canSubmit}
|
|
68
77
|
testID={testID}
|
|
@@ -77,6 +86,141 @@ export function PromptInputButton(props: PromptInputButtonProps) {
|
|
|
77
86
|
return <ChatIconButton variant="ghost" {...props} />;
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
/**
|
|
90
|
+
* The web `ModelPicker` trigger (`model-picker.tsx` `TRIGGER_CLASS`), the
|
|
91
|
+
* chip a composer footer holds: `h-7 gap-1.5 rounded-md px-2`, a 14 icon, the
|
|
92
|
+
* suffix at `/70` after " · ", and a 12 chevron at `opacity-60`. The label is
|
|
93
|
+
* `text-sm` (14), the phone floor for tappable text (the web is `text-xs`).
|
|
94
|
+
*/
|
|
95
|
+
export const PROMPT_INPUT_CHIP = {
|
|
96
|
+
height: 28,
|
|
97
|
+
paddingX: 8,
|
|
98
|
+
gap: 6,
|
|
99
|
+
radius: tokens.radius.md,
|
|
100
|
+
iconSize: 14,
|
|
101
|
+
chevronSize: 12,
|
|
102
|
+
chevronOpacity: 0.6,
|
|
103
|
+
suffixOpacity: 0.7,
|
|
104
|
+
/** The narrowest label worth drawing: about two characters and the ellipsis at `text-sm` (2em). */
|
|
105
|
+
labelMinWidth: 28,
|
|
106
|
+
/** `size="touch"`: 36 on screen, 4 slop above and below, so a 44 bar holds its whole target. */
|
|
107
|
+
touchHeight: 36,
|
|
108
|
+
} as const;
|
|
109
|
+
|
|
110
|
+
export type PromptInputChipProps = {
|
|
111
|
+
/** Before the label: a `PROMPT_INPUT_CHIP.iconSize` (14) mark, such as `ProviderIcon` or a Tabler icon. */
|
|
112
|
+
icon?: ReactNode;
|
|
113
|
+
label: string;
|
|
114
|
+
/** After the label, " · " between, at 70% of `color`: the web trigger suffix (the reasoning level). */
|
|
115
|
+
suffix?: string;
|
|
116
|
+
/** After the label in medium weight with tabular figures, such as a saved count. */
|
|
117
|
+
count?: number;
|
|
118
|
+
/** The chevron after the label. Default `true`. */
|
|
119
|
+
chevron?: boolean;
|
|
120
|
+
/** Label weight. Default `medium`, the web trigger; a quieter footer passes `normal`. */
|
|
121
|
+
weight?: "normal" | "medium";
|
|
122
|
+
/** Text, chevron and Tabler icon colour. Default `tokens.color.muted`, the web `text-muted-foreground`. */
|
|
123
|
+
color?: string;
|
|
124
|
+
/** Default: the label, then the suffix and count. */
|
|
125
|
+
accessibilityLabel?: string;
|
|
126
|
+
/** Whether the menu it opens is open, for VoiceOver and TalkBack. */
|
|
127
|
+
expanded?: boolean;
|
|
128
|
+
disabled?: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Default `sm`, the web trigger: 28 high, slop to 44. `touch` has no web
|
|
131
|
+
* twin: 36 high, for a phone bar that is 44 itself, so the slop stays
|
|
132
|
+
* inside the bar and never reaches the field above.
|
|
133
|
+
*/
|
|
134
|
+
size?: "sm" | "touch";
|
|
135
|
+
onPress?: () => void;
|
|
136
|
+
testID?: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A chip for the composer `footer`, the web `ModelPicker` trigger: 28 high on
|
|
141
|
+
* screen (36 with `size="touch"`), slop to 44 above and below. The label shrinks and truncates, so a
|
|
142
|
+
* long model name gives up width to the chips beside it, as the web
|
|
143
|
+
* `max-sm:truncate`; the icon and chevron keep their size. Below
|
|
144
|
+
* `labelMinWidth` (scaled by the system font size) the label hides, so a
|
|
145
|
+
* squeezed chip is its icon and chevron, as the web at 390, not a sliver of a
|
|
146
|
+
* letter. Disabled dims to 50%, as the web `disabled:opacity-50`.
|
|
147
|
+
*/
|
|
148
|
+
export function PromptInputChip({
|
|
149
|
+
icon,
|
|
150
|
+
label,
|
|
151
|
+
suffix,
|
|
152
|
+
count,
|
|
153
|
+
chevron = true,
|
|
154
|
+
weight = "medium",
|
|
155
|
+
color,
|
|
156
|
+
accessibilityLabel,
|
|
157
|
+
expanded,
|
|
158
|
+
disabled = false,
|
|
159
|
+
size = "sm",
|
|
160
|
+
onPress,
|
|
161
|
+
testID,
|
|
162
|
+
}: PromptInputChipProps) {
|
|
163
|
+
const height = size === "touch" ? PROMPT_INPUT_CHIP.touchHeight : PROMPT_INPUT_CHIP.height;
|
|
164
|
+
const scheme = useScheme();
|
|
165
|
+
const pressed = usePressFeedback();
|
|
166
|
+
const { fontScale } = useWindowDimensions();
|
|
167
|
+
// Whether the label slot is wide enough to read. Hiding it only sets the
|
|
168
|
+
// opacity, so the layout and the next `onLayout` width stay the same: the
|
|
169
|
+
// state changes once per threshold crossing, never in a layout loop, and a
|
|
170
|
+
// wider slot later shows the label again.
|
|
171
|
+
const [readable, setReadable] = useState(true);
|
|
172
|
+
const tint = color ?? tokens.color.muted[scheme];
|
|
173
|
+
const text = [sizeStyle("text-sm"), { color: tint }];
|
|
174
|
+
const name = accessibilityLabel ?? [label, suffix, count].filter((part) => part !== undefined).join(", ");
|
|
175
|
+
return (
|
|
176
|
+
<Pressable
|
|
177
|
+
testID={testID}
|
|
178
|
+
accessibilityRole="button"
|
|
179
|
+
accessibilityLabel={name}
|
|
180
|
+
accessibilityState={{ disabled, expanded }}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
onPress={onPress}
|
|
183
|
+
// Slop only above and below: chips sit side by side, so the width stays theirs.
|
|
184
|
+
hitSlop={touchSlop(MIN_TOUCH_TARGET, height)}
|
|
185
|
+
className={cn("min-w-0 shrink flex-row items-center", pressed, disabled && "opacity-50")}
|
|
186
|
+
// `minHeight`, so a larger system font size can still grow it.
|
|
187
|
+
style={{
|
|
188
|
+
minHeight: height,
|
|
189
|
+
paddingHorizontal: PROMPT_INPUT_CHIP.paddingX,
|
|
190
|
+
gap: PROMPT_INPUT_CHIP.gap,
|
|
191
|
+
borderRadius: PROMPT_INPUT_CHIP.radius,
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
{/* Held whole: in a squeezed chip only the label gives up width. */}
|
|
195
|
+
{icon ? <View className="shrink-0">{icon}</View> : null}
|
|
196
|
+
<Text
|
|
197
|
+
numberOfLines={1}
|
|
198
|
+
onLayout={(event) =>
|
|
199
|
+
setReadable(event.nativeEvent.layout.width >= PROMPT_INPUT_CHIP.labelMinWidth * Math.max(1, fontScale))
|
|
200
|
+
}
|
|
201
|
+
className={cn("min-w-0 shrink text-sm", weight === "medium" ? "font-instrument-sans-medium" : "font-instrument-sans")}
|
|
202
|
+
style={[...text, weight === "medium" ? typeStyle("medium") : null, readable ? null : { opacity: 0 }]}
|
|
203
|
+
>
|
|
204
|
+
{label}
|
|
205
|
+
{suffix ? <Text style={{ color: withAlpha(tint, PROMPT_INPUT_CHIP.suffixOpacity) }}>{` · ${suffix}`}</Text> : null}
|
|
206
|
+
</Text>
|
|
207
|
+
{count !== undefined ? (
|
|
208
|
+
<Text
|
|
209
|
+
className="font-instrument-sans-medium text-sm"
|
|
210
|
+
style={[...text, typeStyle("medium"), { fontVariant: ["tabular-nums"] }]}
|
|
211
|
+
>
|
|
212
|
+
{count}
|
|
213
|
+
</Text>
|
|
214
|
+
) : null}
|
|
215
|
+
{chevron ? (
|
|
216
|
+
<View style={{ opacity: PROMPT_INPUT_CHIP.chevronOpacity }}>
|
|
217
|
+
<IconChevronDown size={PROMPT_INPUT_CHIP.chevronSize} color={tint} />
|
|
218
|
+
</View>
|
|
219
|
+
) : null}
|
|
220
|
+
</Pressable>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
80
224
|
export type PromptInputProps = {
|
|
81
225
|
value?: string;
|
|
82
226
|
defaultValue?: string;
|
|
@@ -99,6 +243,17 @@ export type PromptInputProps = {
|
|
|
99
243
|
header?: ReactNode;
|
|
100
244
|
/** Before the send button, such as `PromptInputButton`s. */
|
|
101
245
|
toolbar?: ReactNode;
|
|
246
|
+
/**
|
|
247
|
+
* Under the field, inside the keyboard lift and above the bottom inset: a
|
|
248
|
+
* row of `PromptInputChip`s, as the bar under Eva's web composer.
|
|
249
|
+
*/
|
|
250
|
+
footer?: ReactNode;
|
|
251
|
+
/**
|
|
252
|
+
* Right above the field, the field's width and 6 clear of it, inside the
|
|
253
|
+
* keyboard lift: a `PromptInputMenu`, the web `@` / `/` picker. Pass it only
|
|
254
|
+
* while the picker is open.
|
|
255
|
+
*/
|
|
256
|
+
menu?: ReactNode;
|
|
102
257
|
/**
|
|
103
258
|
* `stacked` (default): the field over a footer of tools and send, the web
|
|
104
259
|
* `PromptInput`. `inline`: one row, as the web `Composer layout="inline"`
|
|
@@ -106,12 +261,19 @@ export type PromptInputProps = {
|
|
|
106
261
|
* field with `p-1.5`, 44 high on one line.
|
|
107
262
|
*/
|
|
108
263
|
layout?: "stacked" | "inline";
|
|
264
|
+
/**
|
|
265
|
+
* `inline` only. `rounded` (default): the web `Composer` field, radius 20.
|
|
266
|
+
* `pill`: Eva's web chat composer (`h-12 rounded-full py-1`): 48 high on one
|
|
267
|
+
* line, a 36 send button and a `text-sm` (14 / 20) field 36 high; it keeps
|
|
268
|
+
* radius 24 as it grows. Give the + a filled `variant="secondary"`, as Eva.
|
|
269
|
+
*/
|
|
270
|
+
shape?: "rounded" | "pill";
|
|
109
271
|
/** `inline` only: before the field, such as a + (attach) `PromptInputButton`. */
|
|
110
272
|
leading?: ReactNode;
|
|
111
273
|
disabled?: boolean;
|
|
112
274
|
/** Pad the bottom safe-area inset (home indicator). Default `true`; turn it off when a tab bar below already does. */
|
|
113
275
|
safeBottom?: boolean;
|
|
114
|
-
/** Height of anything above the screen's own top, such as a navigation header, for the
|
|
276
|
+
/** Height of anything above the screen's own top, such as a navigation header, for the keyboard lift. Default 0. */
|
|
115
277
|
keyboardVerticalOffset?: number;
|
|
116
278
|
className?: string;
|
|
117
279
|
testID?: string;
|
|
@@ -120,16 +282,33 @@ export type PromptInputProps = {
|
|
|
120
282
|
/** The web `Composer` inline radius, `rounded-[1.25rem]`. */
|
|
121
283
|
const INLINE_RADIUS = 20;
|
|
122
284
|
|
|
123
|
-
/**
|
|
285
|
+
/**
|
|
286
|
+
* Eva's `pill` composer: 48 high on one line (border 1, then 5 around a 36
|
|
287
|
+
* field: the web `h-12 py-1` centres the 36 field and send button in 38),
|
|
288
|
+
* `pl-1.5` / `pr-1.5`, and radius 24 (`rounded-full` at 48). Its placeholder
|
|
289
|
+
* is the muted colour at 90% (`data-empty:before:text-muted-foreground/90`).
|
|
290
|
+
*/
|
|
291
|
+
const PILL = { radius: 24, paddingY: 5, paddingX: 6, lineHeight: 36, placeholderAlpha: 0.9 } as const;
|
|
292
|
+
|
|
293
|
+
/** The field grows to this height, then scrolls: about 6 lines of 24 (with `py-2.5`), and about 8 of `text-xl` with larger text. */
|
|
124
294
|
const MAX_FIELD_HEIGHT = { regular: 160, larger: 240 } as const;
|
|
125
295
|
|
|
296
|
+
/**
|
|
297
|
+
* React Native Web draws a multiline field as a `textarea` with the HTML
|
|
298
|
+
* default of 2 rows, and the auto-grow keeps that height. One row there; on
|
|
299
|
+
* native, `numberOfLines` would lock the line count on Android, so none.
|
|
300
|
+
*/
|
|
301
|
+
const ONE_ROW = Platform.OS === "web" ? 1 : undefined;
|
|
302
|
+
|
|
126
303
|
/**
|
|
127
304
|
* The chat composer: the web `PromptInput` field (`rounded-field border
|
|
128
305
|
* bg-field-background`, `fieldStyle`) with an optional `header` row, a
|
|
129
306
|
* multiline field that grows from one line, and a footer of `toolbar` tools
|
|
130
307
|
* and the send / stop button. It sits in a `KeyboardAvoidingView` (padding,
|
|
131
|
-
* on iOS and Android
|
|
132
|
-
* full-height column, under the
|
|
308
|
+
* on iOS and Android; frame by frame with `KeyboardControllerProvider`) and
|
|
309
|
+
* pads the safe-area insets, so put it last in a full-height column, under the
|
|
310
|
+
* `Conversation`. Inside a part that lifts it already (`ConversationDock`
|
|
311
|
+
* with `KeyboardControllerProvider`, `ChatThread`) it adds no lift of its own.
|
|
133
312
|
*/
|
|
134
313
|
export function PromptInput({
|
|
135
314
|
value,
|
|
@@ -143,7 +322,10 @@ export function PromptInput({
|
|
|
143
322
|
replyTo,
|
|
144
323
|
header,
|
|
145
324
|
toolbar,
|
|
325
|
+
footer,
|
|
326
|
+
menu,
|
|
146
327
|
layout = "stacked",
|
|
328
|
+
shape = "rounded",
|
|
147
329
|
leading,
|
|
148
330
|
disabled = false,
|
|
149
331
|
safeBottom = true,
|
|
@@ -154,6 +336,7 @@ export function PromptInput({
|
|
|
154
336
|
const scheme = useScheme();
|
|
155
337
|
const largerText = useLargerText();
|
|
156
338
|
const insets = useSafeAreaInsets();
|
|
339
|
+
const lifted = useContext(KeyboardLiftContext);
|
|
157
340
|
const [text, setText] = useControllableState({ value, defaultValue, onChange: onChangeText });
|
|
158
341
|
const message = text.trim();
|
|
159
342
|
const canSubmit = message.length > 0 && !disabled && !isGenerating(status);
|
|
@@ -164,6 +347,21 @@ export function PromptInput({
|
|
|
164
347
|
const maxHeight = largerText ? MAX_FIELD_HEIGHT.larger : MAX_FIELD_HEIGHT.regular;
|
|
165
348
|
const growStyle = contentHeight > 0 ? { height: Math.min(contentHeight, maxHeight) } : null;
|
|
166
349
|
const scrollEnabled = contentHeight > maxHeight;
|
|
350
|
+
const pill = layout === "inline" && shape === "pill";
|
|
351
|
+
// Inline: the web `Composer` `text-sm leading-6` (Eva's pill: `leading-5`).
|
|
352
|
+
// Stacked: the web `PromptInput` `Textarea`, `text-base` below `sm`. Larger text is `text-xl`.
|
|
353
|
+
const fieldText = largerText
|
|
354
|
+
? sizeStyle("text-xl")
|
|
355
|
+
: layout === "inline"
|
|
356
|
+
? { fontSize: 14, lineHeight: pill ? 20 : 24 }
|
|
357
|
+
: sizeStyle("text-base");
|
|
358
|
+
|
|
359
|
+
// An empty field drops its measured height and is one line again: on the web
|
|
360
|
+
// a `textarea` only reports a taller height, never a shorter one.
|
|
361
|
+
const changeText = (next: string) => {
|
|
362
|
+
setText(next);
|
|
363
|
+
if (next === "") setContentHeight(0);
|
|
364
|
+
};
|
|
167
365
|
|
|
168
366
|
const submit = () => {
|
|
169
367
|
if (!canSubmit) return;
|
|
@@ -172,76 +370,50 @@ export function PromptInput({
|
|
|
172
370
|
setContentHeight(0);
|
|
173
371
|
};
|
|
174
372
|
|
|
175
|
-
|
|
176
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
keyboardVerticalOffset={keyboardVerticalOffset - bottom}
|
|
373
|
+
const composer = (
|
|
374
|
+
<View
|
|
375
|
+
testID={testID}
|
|
376
|
+
className={cn("pt-2", className)}
|
|
377
|
+
style={{ paddingBottom: bottom + 8, paddingLeft: insets.left + 12, paddingRight: insets.right + 12 }}
|
|
181
378
|
>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<View
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
onChangeText={setText}
|
|
198
|
-
placeholder={placeholder}
|
|
199
|
-
accessibilityLabel={accessibilityLabel}
|
|
200
|
-
editable={!disabled}
|
|
201
|
-
textAlignVertical="center"
|
|
202
|
-
scrollEnabled={scrollEnabled}
|
|
203
|
-
onContentSizeChange={(e) => setContentHeight(e.nativeEvent.contentSize.height)}
|
|
204
|
-
className={cn(
|
|
205
|
-
"min-h-8 flex-1 border-0 px-2 py-1 text-base font-instrument-sans",
|
|
206
|
-
largerText && "min-h-11 py-2 text-xl font-instrument-sans-medium",
|
|
207
|
-
)}
|
|
208
|
-
style={[
|
|
209
|
-
fieldTextStyle(scheme),
|
|
210
|
-
largerText ? typeStyle("medium") : null,
|
|
211
|
-
{
|
|
212
|
-
backgroundColor: "transparent",
|
|
213
|
-
maxHeight,
|
|
214
|
-
...growStyle,
|
|
215
|
-
},
|
|
216
|
-
]}
|
|
217
|
-
/>
|
|
218
|
-
<View className="shrink-0 flex-row items-center gap-1">
|
|
219
|
-
{toolbar}
|
|
220
|
-
<PromptInputSubmit status={status} onSubmit={submit} onStop={onStop} canSubmit={canSubmit} />
|
|
379
|
+
{/* The web panel picker's 6 gap (`POPUP_GAP`) above the field. */}
|
|
380
|
+
{menu ? <View className="mb-1.5">{menu}</View> : null}
|
|
381
|
+
{layout === "inline" ? (
|
|
382
|
+
// Web `Composer` inline: `rounded-[1.25rem]` and a `p-1.5` row, items at the bottom as the field grows.
|
|
383
|
+
<View className="border" style={[fieldStyle(scheme), { borderRadius: pill ? PILL.radius : INLINE_RADIUS }]}>
|
|
384
|
+
{replyTo ? <View className="px-1.5 pt-1.5">{replyTo}</View> : null}
|
|
385
|
+
{header ? <View className="flex-row flex-wrap gap-1 px-1.5 pt-1.5">{header}</View> : null}
|
|
386
|
+
<View
|
|
387
|
+
className={cn("flex-row items-end gap-1", !pill && "p-1.5")}
|
|
388
|
+
style={pill ? { paddingVertical: PILL.paddingY, paddingHorizontal: PILL.paddingX } : null}
|
|
389
|
+
>
|
|
390
|
+
{leading ? (
|
|
391
|
+
// In the pill, centred on the 36 line, as the web `items-center` row.
|
|
392
|
+
<View className="shrink-0 flex-row items-center gap-1" style={pill ? { minHeight: PILL.lineHeight } : null}>
|
|
393
|
+
{leading}
|
|
221
394
|
</View>
|
|
222
|
-
|
|
223
|
-
</View>
|
|
224
|
-
) : (
|
|
225
|
-
<View className="border" style={fieldStyle(scheme)}>
|
|
226
|
-
{replyTo ? <View className="px-2 pt-2">{replyTo}</View> : null}
|
|
227
|
-
{header ? <View className="flex-row flex-wrap gap-1 px-2 pt-2">{header}</View> : null}
|
|
228
|
-
{/* The `Textarea` field without its `numberOfLines` (default 4), which on Android sets the line count: this one starts at one line and grows. */}
|
|
395
|
+
) : null}
|
|
229
396
|
<FieldInput
|
|
397
|
+
nativeID={COMPOSER_INPUT_ID}
|
|
230
398
|
multiline
|
|
399
|
+
numberOfLines={ONE_ROW}
|
|
231
400
|
value={text}
|
|
232
|
-
onChangeText={
|
|
401
|
+
onChangeText={changeText}
|
|
233
402
|
placeholder={placeholder}
|
|
403
|
+
placeholderTextColor={pill ? withAlpha(tokens.color.muted[scheme], PILL.placeholderAlpha) : undefined}
|
|
234
404
|
accessibilityLabel={accessibilityLabel}
|
|
235
405
|
editable={!disabled}
|
|
236
|
-
textAlignVertical="
|
|
406
|
+
textAlignVertical="center"
|
|
237
407
|
scrollEnabled={scrollEnabled}
|
|
238
408
|
onContentSizeChange={(e) => setContentHeight(e.nativeEvent.contentSize.height)}
|
|
239
409
|
className={cn(
|
|
240
|
-
"
|
|
241
|
-
|
|
410
|
+
"flex-1 border-0 text-sm font-instrument-sans",
|
|
411
|
+
pill ? "min-h-9 px-1 py-2" : "min-h-8 px-2 py-1",
|
|
412
|
+
largerText && "min-h-11 py-2 text-xl font-instrument-sans-medium",
|
|
242
413
|
)}
|
|
243
414
|
style={[
|
|
244
415
|
fieldTextStyle(scheme),
|
|
416
|
+
fieldText,
|
|
245
417
|
largerText ? typeStyle("medium") : null,
|
|
246
418
|
{
|
|
247
419
|
backgroundColor: "transparent",
|
|
@@ -250,13 +422,67 @@ export function PromptInput({
|
|
|
250
422
|
},
|
|
251
423
|
]}
|
|
252
424
|
/>
|
|
253
|
-
<View className="flex-row items-center
|
|
254
|
-
|
|
255
|
-
<PromptInputSubmit
|
|
425
|
+
<View className="shrink-0 flex-row items-center gap-1" style={pill ? { minHeight: PILL.lineHeight } : null}>
|
|
426
|
+
{toolbar}
|
|
427
|
+
<PromptInputSubmit
|
|
428
|
+
status={status}
|
|
429
|
+
onSubmit={submit}
|
|
430
|
+
onStop={onStop}
|
|
431
|
+
canSubmit={canSubmit}
|
|
432
|
+
size={pill ? "md" : undefined}
|
|
433
|
+
/>
|
|
256
434
|
</View>
|
|
257
435
|
</View>
|
|
258
|
-
|
|
259
|
-
|
|
436
|
+
</View>
|
|
437
|
+
) : (
|
|
438
|
+
<View className="border" style={fieldStyle(scheme)}>
|
|
439
|
+
{replyTo ? <View className="px-2 pt-2">{replyTo}</View> : null}
|
|
440
|
+
{header ? <View className="flex-row flex-wrap gap-1 px-2 pt-2">{header}</View> : null}
|
|
441
|
+
{/* The `Textarea` field without its `numberOfLines` (default 4), which on Android sets the line count: this one starts at one line and grows. */}
|
|
442
|
+
<FieldInput
|
|
443
|
+
nativeID={COMPOSER_INPUT_ID}
|
|
444
|
+
multiline
|
|
445
|
+
numberOfLines={ONE_ROW}
|
|
446
|
+
value={text}
|
|
447
|
+
onChangeText={changeText}
|
|
448
|
+
placeholder={placeholder}
|
|
449
|
+
accessibilityLabel={accessibilityLabel}
|
|
450
|
+
editable={!disabled}
|
|
451
|
+
textAlignVertical="top"
|
|
452
|
+
scrollEnabled={scrollEnabled}
|
|
453
|
+
onContentSizeChange={(e) => setContentHeight(e.nativeEvent.contentSize.height)}
|
|
454
|
+
className={cn(
|
|
455
|
+
"min-h-11 border-0 px-3.5 py-2.5 text-base font-instrument-sans",
|
|
456
|
+
largerText && "min-h-14 px-4 py-3 text-xl font-instrument-sans-medium",
|
|
457
|
+
)}
|
|
458
|
+
style={[
|
|
459
|
+
fieldTextStyle(scheme),
|
|
460
|
+
fieldText,
|
|
461
|
+
largerText ? typeStyle("medium") : null,
|
|
462
|
+
{
|
|
463
|
+
backgroundColor: "transparent",
|
|
464
|
+
maxHeight,
|
|
465
|
+
...growStyle,
|
|
466
|
+
},
|
|
467
|
+
]}
|
|
468
|
+
/>
|
|
469
|
+
<View className="flex-row items-center justify-between gap-1 px-2 pb-2">
|
|
470
|
+
<View className="min-w-0 shrink flex-row items-center gap-1">{toolbar}</View>
|
|
471
|
+
<PromptInputSubmit status={status} onSubmit={submit} onStop={onStop} canSubmit={canSubmit} />
|
|
472
|
+
</View>
|
|
473
|
+
</View>
|
|
474
|
+
)}
|
|
475
|
+
{footer}
|
|
476
|
+
</View>
|
|
477
|
+
);
|
|
478
|
+
if (lifted) return composer;
|
|
479
|
+
return (
|
|
480
|
+
<KeyboardAvoidingView
|
|
481
|
+
// The keyboard covers the home indicator, so the inset padded below is
|
|
482
|
+
// taken off the lift: the composer sits right on the keyboard.
|
|
483
|
+
keyboardVerticalOffset={keyboardVerticalOffset - bottom}
|
|
484
|
+
>
|
|
485
|
+
{composer}
|
|
260
486
|
</KeyboardAvoidingView>
|
|
261
487
|
);
|
|
262
488
|
}
|
package/src/ai/shimmer.tsx
CHANGED
|
@@ -54,18 +54,16 @@ export function Shimmer({
|
|
|
54
54
|
useEffect(() => {
|
|
55
55
|
if (reduceMotion) {
|
|
56
56
|
cancelAnimation(opacity);
|
|
57
|
-
opacity.
|
|
57
|
+
opacity.set(1);
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
|
-
opacity.
|
|
61
|
-
withTiming(SHIMMER_LOW_OPACITY, { duration: (duration * 1000) / 2 }),
|
|
62
|
-
-1,
|
|
63
|
-
true,
|
|
60
|
+
opacity.set(
|
|
61
|
+
withRepeat(withTiming(SHIMMER_LOW_OPACITY, { duration: (duration * 1000) / 2 }), -1, true),
|
|
64
62
|
);
|
|
65
63
|
return () => cancelAnimation(opacity);
|
|
66
64
|
}, [opacity, reduceMotion, duration]);
|
|
67
65
|
|
|
68
|
-
const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.
|
|
66
|
+
const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.get() }));
|
|
69
67
|
const textSize = chatTextSize(size, largerText);
|
|
70
68
|
|
|
71
69
|
return (
|