@vvedantb/native-ui 0.12.0 → 0.12.1
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 +4 -3
- package/package.json +1 -1
- package/src/DropdownMenu.tsx +35 -30
- package/src/FloatingSurface.tsx +2 -1
- package/src/ai/prompt-input.tsx +16 -5
- package/src/definiteSize.ts +12 -0
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ Peers the app must already have (Expo SDK 57 / RN 0.76+):
|
|
|
21
21
|
|
|
22
22
|
Optional, only if you import those components:
|
|
23
23
|
|
|
24
|
+
- `@rn-primitives/portal` — `Select`. Mount one `<PortalHost />` from this package at the app root (for example in the root layout), outside any `Modal`. The package does not re-export it, so install it with `@rn-primitives/select`.
|
|
24
25
|
- `@gorhom/bottom-sheet` — `BottomSheetContainer`
|
|
25
26
|
- `@react-native-community/datetimepicker` and `dayjs` — `DateTimeField`
|
|
26
27
|
- `@clerk/clerk-expo`, `convex`, `expo-auth-session`, `expo-web-browser` and `expo-router` — `@vvedantb/native-ui/auth` (0.7.0). `expo-router` also for `@vvedantb/native-ui/expo-web-root`.
|
|
@@ -518,8 +519,8 @@ import {
|
|
|
518
519
|
| `AgentTrace`, `AgentTraceLine`, `AgentTraceIcon`, `AgentTraceTitle`, `AgentTraceDescription`, `AgentTraceCollapsible*` | One step per line, with an optional fold behind a rail. `AgentTraceTitle shimmer` pulses a running step. Spread `useStreamingDisclosure(isStreaming)` on `AgentTraceCollapsible` to open it while a step streams. |
|
|
519
520
|
| `ActivityStep` | One tool step as a trace line: icon, label, a spinner while `running`, a check when `complete`, a warning on `error`, and a description such as `` `Worked for ${formatElapsed(12)}` ``. |
|
|
520
521
|
| `ChainOfThought` and its parts | Simplified: header, steps with a rail, search-result chips. |
|
|
521
|
-
| `PromptInput`, `PromptInputButton`, `PromptInputSubmit` | The web field surface with a `header` slot (attachments), a field, a `toolbar` slot and the send button. The field grows from one line to 160 high (about 6 lines), then scrolls. Send is primary with text and disabled without. While `status` is `submitted` or `streaming` it is a "Stop" square that calls `onStop`. It sits in a `KeyboardAvoidingView` (padding, on iOS and Android) and pads the safe-area insets: put it last in a full-height column. Under a navigation header, pass the header height as `keyboardVerticalOffset`; above a tab bar, pass `safeBottom={false}`. |
|
|
522
|
-
| `PromptInput layout="inline"` | One row, as the web `Composer layout="inline"` (Eva's chat pill): `leading` (such as a + button), the field (one line to start, then it grows), `toolbar`, then send, in a 20-radius field with `p-1.5`. 44 high on one line. |
|
|
522
|
+
| `PromptInput`, `PromptInputButton`, `PromptInputSubmit` | The web field surface with a `header` slot (attachments), a field, a `toolbar` slot and the send button. The field grows from one line to 160 high (about 6 lines), then scrolls (auto-grow in both layouts from 0.12.1; send resets the height). Send is primary with text and disabled without. While `status` is `submitted` or `streaming` it is a "Stop" square that calls `onStop`. It sits in a `KeyboardAvoidingView` (padding, on iOS and Android) and pads the safe-area insets: put it last in a full-height column. Under a navigation header, pass the header height as `keyboardVerticalOffset`; above a tab bar, pass `safeBottom={false}`. |
|
|
523
|
+
| `PromptInput layout="inline"` | One row, as the web `Composer layout="inline"` (Eva's chat pill): `leading` (such as a + button), the field (one line to start, then it grows from 0.12.1), `toolbar`, then send, in a 20-radius field with `p-1.5`. 44 high on one line. |
|
|
523
524
|
| `Suggestions`, `Suggestion` | A horizontal row of round chips (or `orientation="vertical"`). `onPress` gets the suggestion text. |
|
|
524
525
|
| `Sources`, `SourcesTrigger`, `SourcesContent`, `Source` | A fold of link rows. A row opens its address with `Linking.openURL`, or hands it to `onPress`. |
|
|
525
526
|
| `FileChip`, `Attachments`, `Attachment` | A monospace file chip (a button with `onOpenFile`), and attachment chips with a media icon or image thumbnail and a labelled remove button. No hover cards and no grid previews. |
|
|
@@ -543,7 +544,7 @@ Every control has a role, an accessible name and a 44 pt target. Folds report `e
|
|
|
543
544
|
|
|
544
545
|
## DropdownMenu (Unreleased)
|
|
545
546
|
|
|
546
|
-
A small menu anchored to its trigger, the native counterpart of the web `DropdownMenu`. On `/primitives` and the root.
|
|
547
|
+
A small menu anchored to its trigger, the native counterpart of the web `DropdownMenu`. On `/primitives` and the root. From 0.12.1 the backdrop and menu sit in a full-size `View` inside the `Modal`, so the menu stays above tab bars and glass layers.
|
|
547
548
|
|
|
548
549
|
```tsx
|
|
549
550
|
<DropdownMenu>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/native-ui",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared React Native / Expo UI primitives. Floating surfaces, forms, overlays, toasts, settings chrome, tab bar, AI chat parts, the family app marks, a NativeWind preset, the Expo web root and Clerk + Convex auth screens.",
|
|
6
6
|
"type": "module",
|
package/src/DropdownMenu.tsx
CHANGED
|
@@ -217,36 +217,41 @@ export function DropdownMenuContent({
|
|
|
217
217
|
statusBarTranslucent
|
|
218
218
|
onRequestClose={close}
|
|
219
219
|
>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
220
|
+
{/* A full-size host, as `OverlayModal` has: without it the backdrop and the
|
|
221
|
+
menu are bare Modal children with no box to size or hit-test against,
|
|
222
|
+
and the menu can stack under tab bars and glass layers. */}
|
|
223
|
+
<View style={{ flex: 1 }} pointerEvents="box-none">
|
|
224
|
+
<Pressable
|
|
225
|
+
testID="DropdownMenu-backdrop"
|
|
226
|
+
accessibilityRole="button"
|
|
227
|
+
accessibilityLabel="Close menu"
|
|
228
|
+
onPress={close}
|
|
229
|
+
style={StyleSheet.absoluteFill}
|
|
230
|
+
/>
|
|
231
|
+
<View
|
|
232
|
+
accessibilityRole="menu"
|
|
233
|
+
accessibilityViewIsModal
|
|
234
|
+
onAccessibilityEscape={close}
|
|
235
|
+
style={{
|
|
236
|
+
position: "absolute",
|
|
237
|
+
...position,
|
|
238
|
+
minWidth: DROPDOWN_MENU.minWidth,
|
|
239
|
+
maxWidth: window.width - insets.left - insets.right - 2 * DROPDOWN_MENU.edge,
|
|
240
|
+
}}
|
|
241
|
+
>
|
|
242
|
+
<FloatingSurface blur={false} radius={DROPDOWN_MENU.radius} style={{ maxHeight: position.maxHeight }}>
|
|
243
|
+
<ScrollView
|
|
244
|
+
bounces={false}
|
|
245
|
+
showsVerticalScrollIndicator={false}
|
|
246
|
+
onContentSizeChange={(_width, height) => setContentHeight(height)}
|
|
247
|
+
contentContainerStyle={{ padding: DROPDOWN_MENU.padding }}
|
|
248
|
+
>
|
|
249
|
+
<View className={cn(className)} style={style} {...props}>
|
|
250
|
+
{children}
|
|
251
|
+
</View>
|
|
252
|
+
</ScrollView>
|
|
253
|
+
</FloatingSurface>
|
|
254
|
+
</View>
|
|
250
255
|
</View>
|
|
251
256
|
</Modal>
|
|
252
257
|
);
|
package/src/FloatingSurface.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { Platform, StyleSheet, View, type ViewProps } from "react-native";
|
|
|
2
2
|
import { useColorScheme } from "nativewind";
|
|
3
3
|
import { glassMode, loadBlur } from "./blur";
|
|
4
4
|
import { cn } from "./cn";
|
|
5
|
+
import { hasDefiniteHeight } from "./definiteSize";
|
|
5
6
|
import {
|
|
6
7
|
floatingFill,
|
|
7
8
|
floatingHairline,
|
|
@@ -65,7 +66,7 @@ export function FloatingSurface({
|
|
|
65
66
|
backgroundColor: mode === "blur" ? "transparent" : fill,
|
|
66
67
|
},
|
|
67
68
|
Platform.OS === "ios" ? styles.continuous : null,
|
|
68
|
-
style
|
|
69
|
+
hasDefiniteHeight(style) ? styles.fill : null,
|
|
69
70
|
]}
|
|
70
71
|
>
|
|
71
72
|
{mode === "blur" && BlurView ? (
|
package/src/ai/prompt-input.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
2
|
import { KeyboardAvoidingView, View } from "react-native";
|
|
3
3
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
4
|
import { IconArrowUp, IconPlayerStopFilled } from "@tabler/icons-react-native";
|
|
@@ -151,11 +151,18 @@ export function PromptInput({
|
|
|
151
151
|
const message = text.trim();
|
|
152
152
|
const canSubmit = message.length > 0 && !disabled && !isGenerating(status);
|
|
153
153
|
const bottom = safeBottom ? insets.bottom : 0;
|
|
154
|
+
// The field grows with its content up to `maxHeight`, then scrolls. A fixed
|
|
155
|
+
// `numberOfLines` would lock the line count on Android, so neither layout sets it.
|
|
156
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
157
|
+
const maxHeight = largerText ? MAX_FIELD_HEIGHT.larger : MAX_FIELD_HEIGHT.regular;
|
|
158
|
+
const growStyle = contentHeight > 0 ? { height: Math.min(contentHeight, maxHeight) } : null;
|
|
159
|
+
const scrollEnabled = contentHeight > maxHeight;
|
|
154
160
|
|
|
155
161
|
const submit = () => {
|
|
156
162
|
if (!canSubmit) return;
|
|
157
163
|
onSubmit(message);
|
|
158
164
|
setText("");
|
|
165
|
+
setContentHeight(0);
|
|
159
166
|
};
|
|
160
167
|
|
|
161
168
|
return (
|
|
@@ -184,8 +191,8 @@ export function PromptInput({
|
|
|
184
191
|
accessibilityLabel={accessibilityLabel}
|
|
185
192
|
editable={!disabled}
|
|
186
193
|
textAlignVertical="center"
|
|
187
|
-
|
|
188
|
-
|
|
194
|
+
scrollEnabled={scrollEnabled}
|
|
195
|
+
onContentSizeChange={(e) => setContentHeight(e.nativeEvent.contentSize.height)}
|
|
189
196
|
className={cn(
|
|
190
197
|
"min-h-8 flex-1 border-0 px-2 py-1 text-base font-instrument-sans",
|
|
191
198
|
largerText && "min-h-11 py-2 text-xl font-instrument-sans-medium",
|
|
@@ -195,7 +202,8 @@ export function PromptInput({
|
|
|
195
202
|
largerText ? typeStyle("medium") : null,
|
|
196
203
|
{
|
|
197
204
|
backgroundColor: "transparent",
|
|
198
|
-
maxHeight
|
|
205
|
+
maxHeight,
|
|
206
|
+
...growStyle,
|
|
199
207
|
},
|
|
200
208
|
]}
|
|
201
209
|
/>
|
|
@@ -217,6 +225,8 @@ export function PromptInput({
|
|
|
217
225
|
accessibilityLabel={accessibilityLabel}
|
|
218
226
|
editable={!disabled}
|
|
219
227
|
textAlignVertical="top"
|
|
228
|
+
scrollEnabled={scrollEnabled}
|
|
229
|
+
onContentSizeChange={(e) => setContentHeight(e.nativeEvent.contentSize.height)}
|
|
220
230
|
className={cn(
|
|
221
231
|
"min-h-11 border-0 px-3.5 py-2.5 text-base font-instrument-sans",
|
|
222
232
|
largerText && "min-h-14 px-4 py-3 text-xl font-instrument-sans-medium",
|
|
@@ -226,7 +236,8 @@ export function PromptInput({
|
|
|
226
236
|
largerText ? typeStyle("medium") : null,
|
|
227
237
|
{
|
|
228
238
|
backgroundColor: "transparent",
|
|
229
|
-
maxHeight
|
|
239
|
+
maxHeight,
|
|
240
|
+
...growStyle,
|
|
230
241
|
},
|
|
231
242
|
]}
|
|
232
243
|
/>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StyleSheet, type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True when `style` gives a view a definite height or a flex grow. A style with
|
|
5
|
+
* only `maxHeight`, `maxWidth` or `minHeight` does not: a `flex: 1` child of
|
|
6
|
+
* such a parent collapses to height 0 on Android.
|
|
7
|
+
*/
|
|
8
|
+
export function hasDefiniteHeight(style: StyleProp<ViewStyle>): boolean {
|
|
9
|
+
const flat = StyleSheet.flatten(style);
|
|
10
|
+
if (flat == null) return false;
|
|
11
|
+
return flat.height != null || flat.flex != null || flat.flexGrow != null;
|
|
12
|
+
}
|