@vvedantb/native-ui 0.3.17 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +146 -13
  2. package/package.json +10 -2
  3. package/src/CaptureButton.tsx +62 -0
  4. package/src/Card.tsx +37 -13
  5. package/src/ColorSwatches.tsx +133 -0
  6. package/src/DateTimeField.tsx +75 -19
  7. package/src/HeaderIconButton.tsx +18 -1
  8. package/src/IconButton.tsx +87 -0
  9. package/src/ListRow.tsx +104 -30
  10. package/src/PageIndicator.tsx +87 -0
  11. package/src/SectionLabel.tsx +48 -0
  12. package/src/SheetRow.tsx +89 -0
  13. package/src/StackPageHeader.tsx +100 -0
  14. package/src/StatGrid.tsx +80 -0
  15. package/src/StatusDot.tsx +14 -1
  16. package/src/ThemeToggle.tsx +17 -5
  17. package/src/ai/activity-step.tsx +57 -0
  18. package/src/ai/attachments.tsx +153 -0
  19. package/src/ai/chain-of-thought.tsx +143 -0
  20. package/src/ai/chat-text.ts +35 -0
  21. package/src/ai/code-block.tsx +104 -0
  22. package/src/ai/collapsible.tsx +111 -0
  23. package/src/ai/conversation.tsx +245 -0
  24. package/src/ai/file-chip.tsx +66 -0
  25. package/src/ai/icon-button.tsx +51 -0
  26. package/src/ai/index.ts +123 -0
  27. package/src/ai/message.tsx +146 -0
  28. package/src/ai/prompt-input.tsx +186 -0
  29. package/src/ai/reasoning.tsx +200 -0
  30. package/src/ai/response-text.tsx +56 -0
  31. package/src/ai/shimmer.tsx +86 -0
  32. package/src/ai/sources.tsx +100 -0
  33. package/src/ai/suggestion.tsx +93 -0
  34. package/src/ai/trace.tsx +234 -0
  35. package/src/ai/use-streaming-disclosure.ts +50 -0
  36. package/src/entries/ai.ts +4 -0
  37. package/src/entries/date-time.ts +1 -0
  38. package/src/entries/layout.ts +6 -2
  39. package/src/entries/page-title.ts +7 -2
  40. package/src/entries/primitives.ts +19 -1
  41. package/src/examples/AiChatGallery.tsx +233 -0
  42. package/src/examples/ParityGallery.tsx +205 -3
  43. package/src/index.ts +19 -0
package/README.md CHANGED
@@ -61,20 +61,24 @@ The root entry exports everything. It still imports `BottomSheetContainer` and `
61
61
  | `@vvedantb/native-ui/tab-bar` | `FloatingTabBar`, `FloatingTabBarProps`, `FLOATING_INSET`, `FLOATING_TAB_BAR_HEIGHT`, `FLOATING_TAB_BAR_HEIGHT_LARGE`, `tabBarReserve` | `expo-blur`, `expo-haptics` (loaders) |
62
62
  | `@vvedantb/native-ui/brand` | `AppMark`, `AppWordmark`, `GoogleMark` (from 0.3.15), `APP_ICONS`, `IconVibot` and the other `Icon*`, the SVG helpers (`APP_MARKS`, `appMarkSvg`, `appTileSvg` and the rest), `FAMILY_APPS`, `familyApp`, `V_PRO_APP_BENEFITS` | none |
63
63
  | `@vvedantb/native-ui/button` | `Button`, `ButtonProps`, `ButtonSize`, `ButtonVariant` | none |
64
- | `@vvedantb/native-ui/primitives` | `Button`, `Input`, `SearchInput`, `Textarea`, `Label`, `Select`, `Switch`, `LabeledSwitchRow`, `Checkbox`, `Card` and its parts, `Badge`, `StatusDot`, `Avatar`, `Separator`, `Progress`, `Spinner`, `Skeleton`, `EmptyState`, `ListRow`, `Chip`, `HeaderIconButton`, `ThemeToggle` (from 0.3.16), `Tabs`, `PillTabs`, `Dialog`, `AlertDialog`, `ConfirmDialog`, `Sheet`, `toast`, `Toaster` | `expo-blur`, `expo-haptics` (loaders) |
64
+ | `@vvedantb/native-ui/primitives` | `Button`, `Input`, `SearchInput`, `Textarea`, `Label`, `Select`, `Switch`, `LabeledSwitchRow`, `Checkbox`, `Card` and its parts, `Badge`, `StatusDot`, `Avatar`, `Separator`, `Progress`, `Spinner`, `Skeleton`, `EmptyState`, `ListRow`, `Chip`, `HeaderIconButton`, `ThemeToggle` (from 0.3.16), `Tabs`, `PillTabs`, `Dialog`, `AlertDialog`, `ConfirmDialog`, `Sheet`, `toast`, `Toaster`. From 0.5.0: `Text` and `TextClassContext` (only here, not on the root), `ColorSwatches`, `StatGrid`, `SectionLabel`, `SheetRow`, `PageIndicator`, `IconButton`, `CaptureButton`, and the `ListRowProps` and `CardProps` types | `expo-blur`, `expo-haptics` (loaders) |
65
65
  | `@vvedantb/native-ui/search-input` | `SearchInput`, `SearchInputProps` | none |
66
- | `@vvedantb/native-ui/page-title` | `PageTitle`, `PageHeader`, `PageTitleProps`, `PageHeaderProps` (from 0.3.13) | none |
66
+ | `@vvedantb/native-ui/page-title` | `PageTitle`, `PageHeader`, `PageTitleProps`, `PageHeaderProps` (from 0.3.13), `StackPageHeader`, `StackPageHeaderProps` (from 0.5.0; it brings the Tabler package for its chevron) | none |
67
67
  | `@vvedantb/native-ui/toast` | `toast`, `Toaster`, `ToastOptions`, `ToastVariant` | none (from 0.3.12) |
68
68
  | `@vvedantb/native-ui/theme` | `NativeUiProvider`, `useNativeUi`, `useLargerText`, `useAccentColor`, `DEFAULT_ACCENT`, `tokens`, `palette`, `withAlpha`, `fontClass`, `cn`, `Colors` and the colour constants | none |
69
69
  | `@vvedantb/native-ui/floating` | `FloatingSurface`, `GlassSheetBackground`, `LiquidGlass`, `GlassBlurProvider`, `GlassBlurTarget`, `FloatingFAB`, `useReduceTransparency`, the floating metrics and styles | `expo-blur` (loader) |
70
- | `@vvedantb/native-ui/layout` | `ScreenContainer`, `SafeAreaContainer`, `PageTitle`, `PageHeader` and their props types, `SettingsSection`, `SettingsField`, `SettingsToggleRow`, `SettingsStack` | `expo-blur` (loader) |
70
+ | `@vvedantb/native-ui/layout` | `ScreenContainer`, `SafeAreaContainer`, `PageTitle`, `PageHeader`, `StackPageHeader` (0.5.0) and their props types, `SectionLabel` (0.5.0), `SettingsSection`, `SettingsField`, `SettingsToggleRow`, `SettingsStack` | `expo-blur` (loader) |
71
71
  | `@vvedantb/native-ui/marketing` | `UpgradeToProCard`, `PricingModal` and the pricing types | none |
72
72
  | `@vvedantb/native-ui/ambient` | `AmbientBackground` | `expo-linear-gradient` (loader) |
73
73
  | `@vvedantb/native-ui/blur`, `/haptics` | the loaders | `expo-blur`, `expo-haptics` (loaders) |
74
74
  | `@vvedantb/native-ui/bottom-sheet` | `BottomSheetContainer` | `@gorhom/bottom-sheet` (import), `expo-blur` (loader) |
75
- | `@vvedantb/native-ui/date-time` | `DateTimeField` | `@react-native-community/datetimepicker`, `dayjs` (import) |
75
+ | `@vvedantb/native-ui/date-time` | `DateTimeField`, `DateTimeFieldProps` (0.5.0) | `@react-native-community/datetimepicker`, `dayjs` (import) |
76
+ | `@vvedantb/native-ui/ai` | The AI chat parts (from 0.4.0): `Conversation`, `Message`, `Reasoning`, `AgentTrace`, `ChainOfThought`, `ActivityStep`, `PromptInput`, `Suggestions`, `Sources`, `FileChip`, `Attachments`, `CodeBlock`, `ResponseText`, `Shimmer`, `Collapsible`, `useStreamingDisclosure` and their parts. See [AI chat](#ai-chat-040). Not on the root entry. | none |
77
+ | `@vvedantb/native-ui/ai-gallery` | `AiChatGallery`, `AiChatSamples` (from 0.4.0) | none |
76
78
 
77
- "Loader" means the `require` inside `try` described above, so the entry bundles and runs without that peer. `@vvedantb/native-ui/gallery` imports the root entry.
79
+ "Loader" means the `require` inside `try` described above, so the entry bundles and runs without that peer. `@vvedantb/native-ui/gallery` imports the root entry; `@vvedantb/native-ui/ai-gallery` imports `/ai` only.
80
+
81
+ The AI chat parts are only on `/ai`. Metro does not tree-shake by default, so on the root entry they would bundle into every app, with or without a chat.
78
82
 
79
83
  ```tsx
80
84
  import { FloatingTabBar } from "@vvedantb/native-ui/tab-bar";
@@ -89,10 +93,11 @@ The entries re-export the same source files, so they can be mixed: `NativeUiProv
89
93
 
90
94
  ## Expo / NativeWind
91
95
 
92
- Source-exported. Point Tailwind at the package so class names are generated:
96
+ Source-exported. Point Tailwind at the package so class names are generated, and set `darkMode: "class"` (see [Theme](#theme)):
93
97
 
94
98
  ```js
95
99
  // tailwind.config.js
100
+ darkMode: "class",
96
101
  content: [
97
102
  "./app/**/*.{js,ts,tsx}",
98
103
  "./components/**/*.{js,ts,tsx}",
@@ -111,10 +116,26 @@ config.resolver.nodeModulesPaths = [
111
116
  ];
112
117
  ```
113
118
 
114
- The app must define NativeWind tokens (`bg-surface`, `bg-secondary`, pink utilities) and Instrument Sans font classes (`font-instrument-sans-*`). Accent pink stays in the app stylesheet. `Button`, `Input`, `Textarea`, the `Select` trigger, `Switch`, `Checkbox`, `Badge`, `Tabs` and `PillTabs` take their colours from `tokens` and `palette`, so they do not need `primary`, `input` or other theme keys. From 0.3.3 so does component text (titles, descriptions, labels, rows, toasts): it no longer uses `text-foreground` or `text-muted-foreground`, which stay light-mode colours in dark mode when an app defines `foreground` with a nested `dark` key. Override those colours with `style`, not `className`. App text needs the same care: `tokens.color.foreground[scheme]` reads in both schemes.
119
+ The app must define NativeWind tokens (`bg-surface`, `bg-secondary`, pink utilities) and Instrument Sans font classes (`font-instrument-sans-*`). Accent pink stays in the app stylesheet. `Button`, `Input`, `Textarea`, the `Select` trigger, `Switch`, `Checkbox`, `Badge`, `Tabs` and `PillTabs` take their colours from `tokens` and `palette`, so they do not need `primary`, `input` or other theme keys. From 0.3.3 so does component text (titles, descriptions, labels, rows, toasts): it no longer uses `text-foreground` or `text-muted-foreground`, which stay light-mode colours in dark mode when an app defines `foreground` with a nested `dark` key. Override those colours with `style`, not `className`. App text needs the same care: `tokens.color.foreground[scheme]` reads in both schemes. From 0.5.0 the `ListRow`, `DateTimeField` and `HeaderIconButton` fills are styles too, so `ThemeToggle` and the `StackPageHeader` back button follow the scheme.
115
120
 
116
121
  From 0.3.5 text that uses `font-instrument-sans-medium` or `-semibold` also sets `fontWeight` as a style (`typeStyle` in `src/controlStyles.ts`). Where an app maps every `font-instrument-sans-*` class to one family with no weight, as Expo web does, the class alone draws Regular. A weight without the face class does not pick the Instrument Sans Medium or Semibold file, so set both. From 0.3.13 every title in the package pairs the two, the internal primitives included, and `tests/fontWeight.test.ts` fails on a face class without its weight, a weight without its face class, or a bare Tailwind `font-medium`, `font-semibold` or `font-bold`.
117
122
 
123
+ The app loads the font; the package does not. On Expo web, one variable family covers every weight. `@vvedantb/ui` ships the woff2 files (400–700, latin and latin-ext) in `fonts/`. Expo web CSS does not bundle relative `url()`s, so copy the files to the app's `public/fonts/` and copy the `@font-face` rules from `@vvedantb/ui/fonts/instrument-sans.css` with `url("/fonts/…")`. Then map all four classes to that family with a sans-serif fallback, as the components set the weight:
124
+
125
+ ```js
126
+ // tailwind.config.js (Expo web)
127
+ const instrumentSans = ["Instrument Sans", "system-ui", "sans-serif"];
128
+ // theme.extend.fontFamily:
129
+ fontFamily: {
130
+ "instrument-sans": instrumentSans,
131
+ "instrument-sans-medium": instrumentSans,
132
+ "instrument-sans-semibold": instrumentSans,
133
+ "instrument-sans-bold": instrumentSans,
134
+ },
135
+ ```
136
+
137
+ A class mapped to a family the browser does not have, such as `["System"]`, with no generic fallback draws in the browser default, often a serif.
138
+
118
139
  ## Theme
119
140
 
120
141
  Wrap the tree (inside your colour-scheme provider):
@@ -129,16 +150,20 @@ import { NativeUiProvider, Button, Card } from "@vvedantb/native-ui";
129
150
  </NativeUiProvider>
130
151
  ```
131
152
 
153
+ **Dark mode needs `darkMode: "class"`.** Every component reads the scheme with NativeWind's `useColorScheme()` (`useScheme` in `src/useScheme.ts`) and takes its colours from `tokens` and `palette` for it; the AI composer and code block do too. `ThemeToggle`, and any `setColorScheme` call in the app, need `darkMode: "class"` in `tailwind.config.js`. Under Tailwind's default, `media`, NativeWind's `setColorScheme` throws ("Unable to manually set color scheme without using darkMode: class"), on web and native. The scheme then stays on the system setting, and the components keep the light colours, even where the app paints its own page dark.
154
+
155
+ **Switch the scheme through NativeWind only.** Call `useColorScheme().setColorScheme("dark"|"light")` (or use `ThemeToggle`). That updates `useScheme` and, with `darkMode: "class"`, the `<html>` class together. Do **not** toggle `document.documentElement.classList` (or a page-level CSS `.dark` rule) by hand while leaving NativeWind on light: the page background can go dark while every token-styled part stays on the light set. That dual-owner fault is an app bug, not a native-ui colour bug.
156
+
132
157
  `useLargerText()` steps type and tap targets up. Without it, `Button`, `Input`, `Textarea` and the `Select` trigger have the web sizes (`Button` 32 / 40 / 44 high, fields 40); with it they keep the larger 0.3.2 sizes. `accentColor` tints FABs, card icons and progress bars (checked checkboxes use the palette primary, as on the web). Surfaces use the tokens in `src/tokens.ts` for fill, radius (card 24, button 16, field 10, pill 999; rows 20) and spacing (16).
133
158
 
134
159
  ## Components (0.3.0)
135
160
 
136
161
  | Area | Exports |
137
162
  | --- | --- |
138
- | Actions and fields | `Button` (`default`/`primary`, `secondary`, `outline`, `destructive`, `ghost`, `link`, `tinted`; `sm`/`small`, `default`/`medium`, `lg`/`large`, `icon`), `Input`, `SearchInput` (0.3.9), `Textarea`, `Label`, `Select`, `Switch`, `Checkbox`, `LabeledSwitchRow`, `DateTimeField` |
139
- | Display | `Card` (+ `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`), `Badge`, `StatusDot`, `Avatar` (+ `AvatarImage`, `AvatarFallback`, `initials()`), `Separator`, `Progress`, `Spinner`, `CenteredSpinner`, `Skeleton`, `EmptyState` (+ `EmptyState.Root` and parts), `ListRow`, `Chip` |
140
- | Navigation | `Tabs` (+ `TabsList`, `TabsTrigger`, `TabsContent`), `PillTabs`, `FloatingTabBar`, `FloatingFAB`, `HeaderIconButton`, `ThemeToggle` (0.3.16) |
141
- | Overlays | `Dialog`, `AlertDialog`, `ConfirmDialog`, `Sheet` (RN `Modal`, each with the web part names), `BottomSheetContainer` (`@gorhom/bottom-sheet`) |
163
+ | Actions and fields | `Button` (`default`/`primary`, `secondary`, `outline`, `destructive`, `ghost`, `link`, `tinted`; `sm`/`small`, `default`/`medium`, `lg`/`large`, `icon`), `IconButton` (0.5.0), `CaptureButton` (0.5.0), `Input`, `SearchInput` (0.3.9), `Textarea`, `Label`, `Select`, `Switch`, `Checkbox`, `LabeledSwitchRow`, `ColorSwatches` (0.5.0), `DateTimeField` (date-only, empty and clearable from 0.5.0) |
164
+ | Display | `Card` (+ `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`; `onPress` from 0.5.0), `Badge`, `StatusDot` (`info` from 0.5.0), `Avatar` (+ `AvatarImage`, `AvatarFallback`, `initials()`), `Separator`, `Progress`, `Spinner`, `CenteredSpinner`, `Skeleton`, `EmptyState` (+ `EmptyState.Root` and parts), `ListRow` (`leading`, `titleEnd`, `control`, `onLongPress` from 0.5.0), `StatGrid` (0.5.0), `SectionLabel` (0.5.0), `Chip`, `Text` (0.5.0, `/primitives` only) |
165
+ | Navigation | `Tabs` (+ `TabsList`, `TabsTrigger`, `TabsContent`), `PillTabs`, `PageIndicator` (0.5.0), `FloatingTabBar`, `FloatingFAB`, `HeaderIconButton`, `ThemeToggle` (0.3.16), `StackPageHeader` (0.5.0) |
166
+ | Overlays | `Dialog`, `AlertDialog`, `ConfirmDialog`, `Sheet` (RN `Modal`, each with the web part names), `SheetRow` (0.5.0), `BottomSheetContainer` (`@gorhom/bottom-sheet`) |
142
167
  | Feedback | `toast()` and `<Toaster />` |
143
168
  | Brand and upgrade | `APP_ICONS`, `IconVibot` and the other `Icon*`, `AppMark`, `AppWordmark`, `GoogleMark` (0.3.15), `FAMILY_APPS`, `V_PRO_APP_BENEFITS`, `UpgradeToProCard`, `PricingModal` |
144
169
  | Settings and layout | `PageTitle`, `PageHeader` (0.3.13), `SettingsSection`, `SettingsField`, `SettingsToggleRow`, `SettingsStack`, `ScreenContainer`, `SafeAreaContainer`, `FloatingSurface`, `AmbientBackground` |
@@ -193,15 +218,123 @@ From 0.3.12 a toast looks like the web `SonnerToaster` at its default density wi
193
218
 
194
219
  `@vvedantb/native-ui/gallery` exports `ParityGallery`, a screen that renders these components. Put it in a dev-only route to check them on a device.
195
220
 
221
+ ## App pieces (0.5.0)
222
+
223
+ Components and props that the Verve, vibot and Velth apps need. They follow the rest of the package: Instrument Sans with its weight as a style, `tokens` and `palette` colours as styles in light and dark, a step up with larger text, 44 pt targets, and roles, names and states for VoiceOver and TalkBack.
224
+
225
+ | Part | Notes |
226
+ | --- | --- |
227
+ | `ListRow` `leading`, `titleEnd`, `control`, `onLongPress`, `accessibilityLabel` | `leading` (any node, such as an avatar with an unread dot) replaces `icon`. `titleEnd` sits at the end of the title line. `control` sits outside the pressable area, so a `Switch` there does not fire `onPress`; `right` stays inside it. With a `control`, `testID` is on the pressable body. |
228
+ | `Card` `onPress` | The whole card is a button that fades while pressed, named by `title` or `accessibilityLabel`. It is one element for assistive tech, so put no other control in it. |
229
+ | `DateTimeField` | `mode="date"` for the date button only. `value` may be `null`: the buttons show `placeholder`, and the picker opens on today (or an earlier `maximumDate`). `onClear` adds a 44 clear button while there is a value. |
230
+ | `ColorSwatches` | A radio group of round swatches (`sm` 24, `md` 32, `lg` 40) with a foreground ring and a black or white check on the selected one. `labels` names each swatch. |
231
+ | `StatGrid` | Label and value tiles on the flat card, in rows of 2 or 3. |
232
+ | `SectionLabel` | The muted `text-xs` medium label above a group of rows; `uppercase` for capitals. Add `className="px-4"` to line it up with rows. |
233
+ | `SheetRow` | An action row for a `Sheet`: icon, title, detail; `active` adds a check, `danger` uses the danger colour, `disabled` dims it. |
234
+ | `PageIndicator` | The web carousel dots. With `onChange` each dot is a 44 button named "Page 2 of 5", so the dots sit 44 apart. |
235
+ | `StatusDot` `info` | A blue dot (`#3b82f6`, `#60a5fa` in dark), native only. |
236
+ | `StackPageHeader` | A see-through stack header for screens over `AmbientBackground`: back chevron, centred `PageTitle` with an optional description, and `headerRight`. Pads the top inset unless `safeTop={false}`. |
237
+ | `IconButton`, `CaptureButton` | A round icon button (`default` bordered surface, or `overlay` translucent black for camera chrome) and a 76 white camera shutter. `IconButton` is not the AI chat button. |
238
+
239
+ ```tsx
240
+ <ListRow
241
+ title="Ada Lovelace"
242
+ meta="See you at nine"
243
+ leading={<Avatar><AvatarFallback>AL</AvatarFallback></Avatar>}
244
+ titleEnd={<Badge variant="secondary">09:41</Badge>}
245
+ onPress={openThread}
246
+ onLongPress={showActions}
247
+ control={<Switch checked={pinned} onCheckedChange={setPinned} accessibilityLabel="Pin" />}
248
+ />
249
+
250
+ <DateTimeField mode="date" value={due} onChange={setDue} placeholder="No due date" onClear={() => setDue(null)} />
251
+
252
+ <AmbientBackground>
253
+ <StackPageHeader title="Details" onBack={router.back} headerRight={<ThemeToggle />} />
254
+ </AmbientBackground>
255
+ ```
256
+
257
+ `Text` from `@vvedantb/native-ui/primitives` is the variant text (`h1`–`h4`, `p`, `lead`, `large`, `small`, `muted`, `code`, `blockquote`). It is not on the root entry. Unlike the components, it takes its colour from NativeWind classes (`text-foreground`, `text-muted-foreground`); where the app defines `foreground` with a nested `dark` key, give it `style={{ color: tokens.color.foreground[scheme] }}` in dark mode.
258
+
259
+ ## AI chat (0.4.0)
260
+
261
+ `@vvedantb/native-ui/ai` ports the `@vvedantb/ui` AI elements for touch: the thread, messages, reasoning and agent trace folds, the composer, suggestions, sources, attachments and code. They are generic (no model names, no app logic) and say "the AI", "Thinking…", "Thought for 12s" and "Worked for 12s". They add no package.
262
+
263
+ ```tsx
264
+ import {
265
+ Conversation,
266
+ ConversationContent,
267
+ ConversationScrollButton,
268
+ Message,
269
+ MessageAction,
270
+ MessageActionsBar,
271
+ MessageContent,
272
+ PromptInput,
273
+ Reasoning,
274
+ ReasoningContent,
275
+ ReasoningTrigger,
276
+ ResponseText,
277
+ } from "@vvedantb/native-ui/ai";
278
+
279
+ <View className="flex-1">
280
+ <Conversation>
281
+ <ConversationContent>
282
+ <Message from="user">
283
+ <MessageContent>{question}</MessageContent>
284
+ </Message>
285
+ <Message from="assistant">
286
+ <MessageContent>
287
+ <Reasoning isStreaming={status === "streaming"}>
288
+ <ReasoningTrigger />
289
+ <ReasoningContent>{reasoning}</ReasoningContent>
290
+ </Reasoning>
291
+ <ResponseText>{answer}</ResponseText>
292
+ </MessageContent>
293
+ <MessageActionsBar>
294
+ <MessageAction kind="copy" onPress={() => copy(answer)} />
295
+ <MessageAction kind="retry" onPress={retry} />
296
+ </MessageActionsBar>
297
+ </Message>
298
+ </ConversationContent>
299
+ <ConversationScrollButton />
300
+ </Conversation>
301
+ <PromptInput onSubmit={send} status={status} onStop={stop} />
302
+ </View>
303
+ ```
304
+
305
+ | Part | Notes |
306
+ | --- | --- |
307
+ | `Conversation`, `ConversationContent`, `ConversationEmptyState`, `ConversationScrollButton`, `useConversation` | A `ScrollView` that sticks to the bottom: while the reader is within 70 of the end, new and growing messages stay in view. Once they drag up it stops following, and the scroll button shows: a round outline arrow (40, slop to 48), 16 above the bottom, with an opaque fill and no shadow. Pressing it scrolls down (a jump with Reduce Motion). For very long threads, drive a `FlatList` with `useConversation`. |
308
+ | `Message`, `MessageContent`, `MessageActionsBar`, `MessageAction` | A user message is the web primary bubble (radius 16, bottom-right 14, `px-4 py-3`) at the end of the row; an assistant message has no fill. `MessageAction` is a ghost icon button at the web `icon-sm` size (32, slop to 44). `kind` gives the icon and label: "Copy", "Retry", "Good response", "Bad response". `selected` marks chosen feedback. |
309
+ | `Reasoning`, `ReasoningTrigger`, `ReasoningContent`, `useReasoning` | Opens when streaming starts, times the stream and closes once, a second after it stops. The trigger shows a brain, the shimmering "Thinking…", then "Thought for 12s", and a chevron. The content is muted monospace text on the web `bg-surface-secondary/40` panel. Unlike the web, a reader can close it while it streams. |
310
+ | `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. |
311
+ | `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)}` ``. |
312
+ | `ChainOfThought` and its parts | Simplified: header, steps with a rail, search-result chips. |
313
+ | `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}`. |
314
+ | `Suggestions`, `Suggestion` | A horizontal row of round chips (or `orientation="vertical"`). `onPress` gets the suggestion text. |
315
+ | `Sources`, `SourcesTrigger`, `SourcesContent`, `Source` | A fold of link rows. A row opens its address with `Linking.openURL`, or hands it to `onPress`. |
316
+ | `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. |
317
+ | `CodeBlock`, `CodeBlockCopyButton` | Plain monospace code on the flat panel, scrolling sideways, with the language and a copy button in a header. No highlighting. |
318
+ | `ResponseText` | Plain, selectable reply text with newlines kept. It takes the colour of the `MessageContent` it is in. |
319
+ | `Shimmer` | "Live" text: the foreground fades to 0.45 and back on the UI thread. With Reduce Motion it holds still, muted. |
320
+
321
+ **Markdown.** There is no markdown renderer: `ResponseText` draws the text as it is. To render markdown, an app can add one of these and use it in place of `ResponseText`: `react-native-markdown-display`, `@ronradtke/react-native-markdown-display` (a maintained fork) or `react-native-marked`.
322
+
323
+ **Clipboard.** React Native's own `Clipboard` is deprecated (moved out of core), and the package adds no clipboard package. `MessageAction kind="copy"` calls your `onPress`, and `CodeBlock` shows its copy button when given `onCopy(code)`. Pass your own clipboard call, for example `expo-clipboard`'s `setStringAsync`.
324
+
325
+ Every control has a role, an accessible name and a 44 pt target. Folds report `expanded`, feedback actions `selected`, and the send button `disabled`. Type and icons step up with larger text. Colours come from `palette` and `tokens` as styles, in light and dark.
326
+
327
+ `@vvedantb/native-ui/ai-gallery` exports `AiChatGallery`, a full chat screen with a sample thread: a user message, a finished assistant message with folded reasoning and trace, code, sources and actions, and a second answer that streams with its reasoning open until you press Stop. It also has suggestions and the composer with sample attachments. Put it in a dev-only route with a `SafeAreaProvider` above it. `ParityGallery` shows the same thread (`AiChatSamples`) in its "AI chat" section.
328
+
196
329
  ## Phones and accessibility
197
330
 
198
- The components keep the web sizes on screen and reach a 44 pt touch target with `hitSlop` (`touchSlop` in `src/controlStyles.ts`): `Checkbox` (16), `Switch` (36 × 20), tab triggers (30 high), the `Select` trigger (40 high), the `SearchInput` clear button (24), the `UpgradeToProCard` pill (32 high, slop 8) and `Button` (the slop brings each size to at least 48). Controls set their role and state for VoiceOver and TalkBack: `Button` (`disabled`, `busy` while loading), `Checkbox` (`checked`, `mixed`), `Switch` (`checked`), `Tabs` and `PillTabs` (`tablist`, `tab`, `selected`) and the `Select` trigger (`combobox`, with an `accessibilityLabel` prop). Give a `Button` with no text child an `accessibilityLabel`.
331
+ The components keep the web sizes on screen and reach a 44 pt touch target with `hitSlop` (`touchSlop` in `src/controlStyles.ts`): `Checkbox` (16), `Switch` (36 × 20), tab triggers (30 high), the `Select` trigger (40 high), the `SearchInput` clear button (24), the `UpgradeToProCard` pill (32 high, slop 8) and `Button` (the slop brings each size to at least 48). From 0.5.0 so do `ColorSwatches` swatches and `IconButton` (32 and 40); the `DateTimeField` clear button and the pressable `PageIndicator` dots are 44 squares. Controls set their role and state for VoiceOver and TalkBack: `Button` (`disabled`, `busy` while loading), `Checkbox` (`checked`, `mixed`), `Switch` (`checked`), `Tabs` and `PillTabs` (`tablist`, `tab`, `selected`) and the `Select` trigger (`combobox`, with an `accessibilityLabel` prop). Give a `Button` with no text child an `accessibilityLabel`.
199
332
 
200
333
  Text scales with the system font size. Labelled controls have minimum heights, so a large Dynamic Type size grows them instead of clipping the label (the `SearchInput` lock grows with the font scale); only the `FloatingTabBar` labels are capped (1.5×), to fit the fixed bar. With Reduce Motion on, dialogs, sheets, toasts, the `Select` popover, `PricingModal`, `Progress`, `Skeleton` and the `FloatingTabBar` pill do not animate. Dialogs, sheets, toasts, `FloatingTabBar` and `FloatingFAB` stay inside the safe-area insets, and dialog and sheet fields stay above the keyboard on iOS and Android.
201
334
 
202
335
  The brand marks (`APP_ICONS`, the `Icon*` exports, `AppMark` and `GoogleMark`) are images named by `title`, and hidden from assistive tech without one. From 0.3.14 they set the props each platform reads: `accessibilityRole="image"` and `accessibilityLabel` on iOS and Android, `role="img"` and `aria-label` on web. Without a title they set `accessibilityElementsHidden` and `importantForAccessibility="no-hide-descendants"` on native and `aria-hidden` on web. `testID` is `data-testid` on web. On web `react-native-svg` passes its props straight to the DOM `<svg>`, and React Native Web does not map `accessible`, the `accessibility*` props or `testID` there. So do not put them on an `Svg` or on a Tabler icon: put them on the parent `View` or `Pressable`. The package draws no Lucide icon, as a Lucide icon always sets `testID` on its `Svg`. On web the `Select` options are Radix DOM nodes, so from 0.3.14 they take their name and test id as `aria-label` and `data-testid` there, and as `accessibilityLabel` and `testID` on iOS and Android.
203
336
 
204
- `ThemeToggle` (from 0.3.16) is a `HeaderIconButton` that switches between light and dark with NativeWind's `setColorScheme`, the scheme every component reads through `useColorScheme`. It shows and names the scheme it switches to: a moon and "Switch to dark mode" in light, a sun and "Switch to light mode" in dark. It stores nothing: to restore the choice on the next launch, store `useColorScheme().colorScheme` in the app. It takes `testID`.
337
+ `ThemeToggle` (from 0.3.16) is a `HeaderIconButton` that switches between light and dark with NativeWind's `setColorScheme`, the scheme every component reads through `useColorScheme`. It shows and names the scheme it switches to: a moon and "Switch to dark mode" in light, a sun and "Switch to light mode" in dark. It stores nothing: to restore the choice on the next launch, store `useColorScheme().colorScheme` in the app. It takes `testID`. It needs `darkMode: "class"` (see [Theme](#theme)). Without it, from 0.5.0, a press logs that fix with `console.error` and leaves the scheme as it is, instead of throwing from the press handler.
205
338
 
206
339
  `GoogleMark` (from 0.3.15) is the four-colour Google "G", 18 by default, for a "Continue with Google" button: `<Button variant="outline" leftIcon={<GoogleMark />}>Continue with Google</Button>`. Its colours are fixed, so it takes no `color`. Leave it without a `title` there, as the label names the button.
207
340
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vvedantb/native-ui",
3
- "version": "0.3.17",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
- "description": "Shared React Native / Expo UI primitives. Floating surfaces, forms, overlays, toasts, settings chrome, tab bar and the family app marks.",
5
+ "description": "Shared React Native / Expo UI primitives. Floating surfaces, forms, overlays, toasts, settings chrome, tab bar, AI chat parts and the family app marks.",
6
6
  "type": "module",
7
7
  "publishConfig": {
8
8
  "access": "public"
@@ -73,10 +73,18 @@
73
73
  "types": "./src/entries/date-time.ts",
74
74
  "default": "./src/entries/date-time.ts"
75
75
  },
76
+ "./ai": {
77
+ "types": "./src/entries/ai.ts",
78
+ "default": "./src/entries/ai.ts"
79
+ },
76
80
  "./gallery": {
77
81
  "types": "./src/examples/ParityGallery.tsx",
78
82
  "default": "./src/examples/ParityGallery.tsx"
79
83
  },
84
+ "./ai-gallery": {
85
+ "types": "./src/examples/AiChatGallery.tsx",
86
+ "default": "./src/examples/AiChatGallery.tsx"
87
+ },
80
88
  "./blur": {
81
89
  "types": "./src/blur.ts",
82
90
  "default": "./src/blur.ts"
@@ -0,0 +1,62 @@
1
+ import { Pressable, View } from "react-native";
2
+ import { cn } from "./cn";
3
+ import { withAlpha } from "./palette";
4
+ import { tokens } from "./tokens";
5
+
6
+ export type CaptureButtonProps = {
7
+ onPress?: () => void;
8
+ disabled?: boolean;
9
+ testID?: string;
10
+ /** Default "Take photo". */
11
+ accessibilityLabel?: string;
12
+ };
13
+
14
+ /** Shutter metrics: outer diameter, ring width and the gap inside the ring. */
15
+ export const CAPTURE_BUTTON = { size: 76, ring: 4, gap: 4 } as const;
16
+
17
+ /**
18
+ * A camera shutter: a 76 white ring around a white disc, for a live preview in
19
+ * light and dark alike. The disc shrinks to 90% while pressed. Larger than the
20
+ * 44 target, so it needs no hit slop. A labelled button, dimmed and ignoring
21
+ * presses when `disabled`.
22
+ */
23
+ export function CaptureButton({
24
+ onPress,
25
+ disabled = false,
26
+ testID,
27
+ accessibilityLabel = "Take photo",
28
+ }: CaptureButtonProps) {
29
+ const { size, ring, gap } = CAPTURE_BUTTON;
30
+ const inner = size - 2 * (ring + gap);
31
+ return (
32
+ <Pressable
33
+ testID={testID}
34
+ accessibilityRole="button"
35
+ accessibilityLabel={accessibilityLabel}
36
+ accessibilityState={{ disabled }}
37
+ disabled={disabled}
38
+ onPress={onPress}
39
+ className={cn("items-center justify-center", disabled && "opacity-50")}
40
+ style={{
41
+ width: size,
42
+ height: size,
43
+ borderRadius: size / 2,
44
+ borderWidth: ring,
45
+ borderColor: tokens.color.white,
46
+ backgroundColor: withAlpha(tokens.color.black, 0.15),
47
+ }}
48
+ >
49
+ {({ pressed }) => (
50
+ <View
51
+ style={{
52
+ width: inner,
53
+ height: inner,
54
+ borderRadius: inner / 2,
55
+ backgroundColor: tokens.color.white,
56
+ transform: [{ scale: pressed && !disabled ? 0.9 : 1 }],
57
+ }}
58
+ />
59
+ )}
60
+ </Pressable>
61
+ );
62
+ }
package/src/Card.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ComponentType, ReactNode } from "react";
2
2
  import type { IconProps } from "@tabler/icons-react-native";
3
- import { Text, View, type TextProps, type ViewProps } from "react-native";
3
+ import { Pressable, Text, View, type TextProps, type ViewProps } from "react-native";
4
4
  import {
5
5
  Card as PrimitiveCard,
6
6
  CardContent as PrimitiveCardContent,
@@ -12,15 +12,7 @@ import { cardStyle, sizeStyle, textStyle, typeStyle } from "./controlStyles";
12
12
  import { useAccentColor, useLargerText } from "./theme";
13
13
  import { useScheme } from "./useScheme";
14
14
 
15
- export function Card({
16
- children,
17
- title,
18
- description,
19
- action,
20
- leftIcon: Icon,
21
- className,
22
- testID,
23
- }: {
15
+ export type CardProps = {
24
16
  children: ReactNode;
25
17
  title?: string;
26
18
  /** Muted line under the title (the web `CardDescription`). */
@@ -29,8 +21,28 @@ export function Card({
29
21
  leftIcon?: ComponentType<IconProps>;
30
22
  /** Classes on the card surface. */
31
23
  className?: string;
24
+ /**
25
+ * Makes the whole card a button that fades while pressed (from 0.5.0). It is
26
+ * then one element for VoiceOver and TalkBack, so put no other control in it.
27
+ */
28
+ onPress?: () => void;
29
+ /** Names the pressable card. Default `title`; pass one when there is no title. */
30
+ accessibilityLabel?: string;
31
+ /** On the button when there is `onPress`, otherwise on the card surface. */
32
32
  testID?: string;
33
- }) {
33
+ };
34
+
35
+ export function Card({
36
+ children,
37
+ title,
38
+ description,
39
+ action,
40
+ leftIcon: Icon,
41
+ className,
42
+ onPress,
43
+ accessibilityLabel,
44
+ testID,
45
+ }: CardProps) {
34
46
  const largerText = useLargerText();
35
47
  const accentColor = useAccentColor();
36
48
  const scheme = useScheme();
@@ -38,9 +50,9 @@ export function Card({
38
50
  const titleSize = largerText ? "text-2xl" : "text-base";
39
51
  // The web card: a flat `surfaceCard` fill at radius 16, with no border, no
40
52
  // shadow and no blur. Dialogs, sheets and toasts float, as on the web.
41
- return (
53
+ const card = (
42
54
  <PrimitiveCard
43
- testID={testID}
55
+ testID={onPress ? undefined : testID}
44
56
  className={cn("gap-4 overflow-hidden border-0 py-4", className)}
45
57
  style={cardStyle(scheme)}
46
58
  >
@@ -74,6 +86,18 @@ export function Card({
74
86
  </PrimitiveCardContent>
75
87
  </PrimitiveCard>
76
88
  );
89
+ if (!onPress) return card;
90
+ return (
91
+ <Pressable
92
+ testID={testID}
93
+ accessibilityRole="button"
94
+ accessibilityLabel={accessibilityLabel ?? title}
95
+ onPress={onPress}
96
+ className="active:opacity-70"
97
+ >
98
+ {card}
99
+ </Pressable>
100
+ );
77
101
  }
78
102
 
79
103
  // Parts for composing a card body with the web part names. They sit inside
@@ -0,0 +1,133 @@
1
+ import { Pressable, View } from "react-native";
2
+ import { IconCheck } from "@tabler/icons-react-native";
3
+ import { cn } from "./cn";
4
+ import { touchSlop } from "./controlStyles";
5
+ import { selectionHaptic } from "./haptics";
6
+ import { withAlpha } from "./palette";
7
+ import { useLargerText } from "./theme";
8
+ import { tokens } from "./tokens";
9
+ import { useScheme } from "./useScheme";
10
+
11
+ export type ColorSwatchesSize = "sm" | "md" | "lg";
12
+
13
+ export type ColorSwatchesProps = {
14
+ /** The selected colour: one of `colors`, compared without case. */
15
+ value: string;
16
+ onChange: (color: string) => void;
17
+ /** `#rgb` or `#rrggbb` colours. */
18
+ colors: string[];
19
+ /** Swatch diameter: `sm` 24, `md` 32, `lg` 40. Default `md`. */
20
+ size?: ColorSwatchesSize;
21
+ /** Per-swatch accessibility labels; falls back to the colour string */
22
+ labels?: string[];
23
+ /** Names the group, such as "Accent colour". */
24
+ accessibilityLabel?: string;
25
+ testID?: string;
26
+ disabled?: boolean;
27
+ };
28
+
29
+ /** Swatch diameters, one step up with larger text. */
30
+ const DIAMETER = {
31
+ regular: { sm: 24, md: 32, lg: 40 },
32
+ larger: { sm: 32, md: 40, lg: 48 },
33
+ } as const;
34
+
35
+ /** Selection ring width and the gap between the ring and the swatch. */
36
+ const RING = 2;
37
+ const RING_GAP = 2;
38
+
39
+ /**
40
+ * The check colour on a swatch: black or white, whichever has the higher WCAG
41
+ * contrast with it. `null` when the colour is not `#rgb` or `#rrggbb`, and the
42
+ * ring alone marks the selection.
43
+ */
44
+ export function swatchCheckColor(color: string): string | null {
45
+ const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color);
46
+ const hex = short ? `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}` : color;
47
+ if (!/^#[0-9a-f]{6}$/i.test(hex)) return null;
48
+ const n = Number.parseInt(hex.slice(1), 16);
49
+ const linear = (channel: number) => {
50
+ const c = channel / 255;
51
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
52
+ };
53
+ const luminance =
54
+ 0.2126 * linear((n >> 16) & 255) + 0.7152 * linear((n >> 8) & 255) + 0.0722 * linear(n & 255);
55
+ // Contrast with white is 1.05 / (L + 0.05); with black, (L + 0.05) / 0.05.
56
+ return 1.05 / (luminance + 0.05) >= (luminance + 0.05) / 0.05 ? tokens.color.white : tokens.color.black;
57
+ }
58
+
59
+ /**
60
+ * Pick one colour from a row of round swatches that wraps. The selected swatch
61
+ * has a ring in the foreground colour and a check in black or white, whichever
62
+ * reads on it. Each swatch has a faint outline, so white shows on a white
63
+ * surface. A radio group for VoiceOver and TalkBack: each swatch is a radio
64
+ * named by `labels` (or its colour string). A `md` swatch is 32 across and 40
65
+ * with its ring, and reaches 44 with its hit slop; the 12 gap keeps the slops
66
+ * of neighbouring swatches apart.
67
+ */
68
+ export function ColorSwatches({
69
+ value,
70
+ onChange,
71
+ colors,
72
+ size = "md",
73
+ labels,
74
+ accessibilityLabel,
75
+ testID,
76
+ disabled = false,
77
+ }: ColorSwatchesProps) {
78
+ const scheme = useScheme();
79
+ const largerText = useLargerText();
80
+ const diameter = DIAMETER[largerText ? "larger" : "regular"][size];
81
+ const outer = diameter + 2 * (RING + RING_GAP);
82
+ const selected = value.toLowerCase();
83
+ return (
84
+ <View
85
+ testID={testID}
86
+ accessibilityRole="radiogroup"
87
+ accessibilityLabel={accessibilityLabel}
88
+ className={cn("flex-row flex-wrap gap-3", disabled && "opacity-50")}
89
+ >
90
+ {colors.map((color, index) => {
91
+ const isSelected = color.toLowerCase() === selected;
92
+ const check = isSelected ? swatchCheckColor(color) : null;
93
+ return (
94
+ <Pressable
95
+ key={`${color}-${index}`}
96
+ testID={testID ? `${testID}-${index}` : undefined}
97
+ accessibilityRole="radio"
98
+ accessibilityLabel={labels?.[index] ?? color}
99
+ accessibilityState={{ checked: isSelected, disabled }}
100
+ hitSlop={touchSlop(outer)}
101
+ disabled={disabled}
102
+ onPress={() => {
103
+ selectionHaptic();
104
+ onChange(color);
105
+ }}
106
+ className="items-center justify-center active:opacity-70"
107
+ style={{
108
+ width: outer,
109
+ height: outer,
110
+ borderRadius: outer / 2,
111
+ borderWidth: RING,
112
+ borderColor: isSelected ? tokens.color.foreground[scheme] : "transparent",
113
+ }}
114
+ >
115
+ <View
116
+ className="items-center justify-center"
117
+ style={{
118
+ width: diameter,
119
+ height: diameter,
120
+ borderRadius: diameter / 2,
121
+ backgroundColor: color,
122
+ borderWidth: 1,
123
+ borderColor: withAlpha(tokens.color.foreground[scheme], 0.12),
124
+ }}
125
+ >
126
+ {check ? <IconCheck size={Math.round(diameter / 2)} color={check} strokeWidth={2} /> : null}
127
+ </View>
128
+ </Pressable>
129
+ );
130
+ })}
131
+ </View>
132
+ );
133
+ }