@thewhileloop/whileui 4.0.0 → 4.1.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 (44) hide show
  1. package/README.md +127 -15
  2. package/dist/blocks/chat/chat-conversation.d.ts +8 -0
  3. package/dist/blocks/chat/chat-conversation.js +36 -0
  4. package/dist/blocks/chat/chat.js +5 -11
  5. package/dist/blocks/chat/index.d.ts +1 -0
  6. package/dist/blocks/chat/index.js +1 -0
  7. package/dist/blocks/layout/smart-input.js +2 -1
  8. package/dist/code.d.ts +1 -0
  9. package/dist/code.js +1 -0
  10. package/dist/components/card/card.d.ts +1 -1
  11. package/dist/components/card/card.js +4 -25
  12. package/dist/components/code-block/code-block.d.ts +12 -0
  13. package/dist/components/code-block/code-block.js +13 -0
  14. package/dist/components/code-block/index.d.ts +1 -0
  15. package/dist/components/code-block/index.js +1 -0
  16. package/dist/components/form-field/form-field.d.ts +8 -0
  17. package/dist/components/form-field/form-field.js +3 -0
  18. package/dist/components/form-field/index.d.ts +1 -1
  19. package/dist/components/form-field/index.js +1 -1
  20. package/dist/components/input/input.d.ts +1 -0
  21. package/dist/components/input/input.js +11 -6
  22. package/dist/components/markdown/index.d.ts +1 -0
  23. package/dist/components/markdown/index.js +1 -0
  24. package/dist/components/markdown/markdown.d.ts +14 -0
  25. package/dist/components/markdown/markdown.js +94 -0
  26. package/dist/components/numeric-input/numeric-input.d.ts +1 -0
  27. package/dist/components/numeric-input/numeric-input.js +11 -6
  28. package/dist/components/select/select.d.ts +2 -1
  29. package/dist/components/select/select.js +18 -10
  30. package/dist/components/surface/index.d.ts +1 -0
  31. package/dist/components/surface/index.js +1 -0
  32. package/dist/components/surface/surface.d.ts +14 -0
  33. package/dist/components/surface/surface.js +35 -0
  34. package/dist/components/text/text.d.ts +42 -42
  35. package/dist/components/text/text.js +39 -17
  36. package/dist/components/textarea/textarea.d.ts +1 -0
  37. package/dist/components/textarea/textarea.js +7 -3
  38. package/dist/index.d.ts +2 -1
  39. package/dist/index.js +2 -1
  40. package/dist/lib/web-autofill.d.ts +9 -0
  41. package/dist/lib/web-autofill.js +9 -0
  42. package/dist/markdown.d.ts +1 -0
  43. package/dist/markdown.js +1 -0
  44. package/package.json +16 -2
package/README.md CHANGED
@@ -18,6 +18,8 @@ bun add @thewhileloop/whileui
18
18
  bun add react@^19.3.0 react-native@^0.88.0 uniwind@^1.12.2 tailwindcss@^4.0.0 react-native-reanimated react-native-safe-area-context react-native-screens
19
19
  # Only if you use Select, Popover, Tooltip, or HoverCard:
20
20
  bun add @rn-primitives/portal @rn-primitives/hooks @rn-primitives/slot @rn-primitives/select @rn-primitives/popover @rn-primitives/tooltip @rn-primitives/hover-card
21
+ # Only if you import @thewhileloop/whileui/markdown:
22
+ bun add markdown-it
21
23
  ```
22
24
 
23
25
  Use `npm install` instead of `bun add` if you prefer npm.
@@ -230,17 +232,33 @@ function MyScreen() {
230
232
  `@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.
231
233
  - **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
232
234
 
235
+ ### Composition grammar
236
+
237
+ - Start with one page canvas and flat sections. Add a `Surface` only when grouping or
238
+ elevation communicates something; do not wrap every section in a card.
239
+ - Use `level="base"` for grouped content, `raised` for selectable or floating regions,
240
+ and `overlay` for content above the page. Avoid nesting more than one surfaced level.
241
+ - Express hierarchy through `Text` variants before custom font-size classes. Keep one
242
+ primary action per region and constrain long-form content with `Container`.
243
+ - Forms use `FormField`; its disabled and invalid state flows into `Input`, `Textarea`,
244
+ `NumericInput`, and `Select`. Pass a control prop only to override that inherited state.
245
+ - Chat mechanics belong to `ChatConversation` + `SmartInput`. Message payloads, tools,
246
+ reasoning, approvals, sources, and action rows are application-owned compositions.
247
+ - Import Markdown and code rendering from their optional subpaths. Do not make the root
248
+ package pay for content rendering when the screen does not use it.
249
+
233
250
  ## Components
234
251
 
235
252
  ### Primitives
236
253
 
237
- | Component | Notes |
238
- | ------------- | -------------------------------- |
239
- | **Text** | Themed text with variant support |
240
- | **View** | Themed view wrapper |
241
- | **Pressable** | Themed pressable wrapper |
242
- | **Stack** | Vertical flex layout with gap |
243
- | **Row** | Horizontal flex layout with gap |
254
+ | Component | Notes |
255
+ | ------------- | ------------------------------------------------ |
256
+ | **Text** | Themed text with variant support |
257
+ | **Surface** | Token-backed base, raised, and overlay container |
258
+ | **View** | Themed view wrapper |
259
+ | **Pressable** | Themed pressable wrapper |
260
+ | **Stack** | Vertical flex layout with gap |
261
+ | **Row** | Horizontal flex layout with gap |
244
262
 
245
263
  ### Root Providers
246
264
 
@@ -273,6 +291,7 @@ function MyScreen() {
273
291
  | Component | Variants | Notes |
274
292
  | --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
275
293
  | **Card** | padding (none, sm, default, lg), unstyled | Compound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter |
294
+ | **Surface** | level (base, raised, overlay) | Shared radius, padding, border, shadow, and optional frosted treatment |
276
295
  | **Badge** | default, secondary, destructive, outline, success | BadgeText sub-component |
277
296
  | **FAB** | default, secondary, destructive, surface; sm/default/lg | Floating action button. `label` required; pass your own icon as children |
278
297
  | **Alert** | default, destructive, success, warning | AlertTitle & AlertDescription |
@@ -364,11 +383,12 @@ Copy from `apps/showcase/templates/auth/`:
364
383
 
365
384
  ### Chat
366
385
 
367
- | Block | Description |
368
- | --------------------- | ------------------------------------------------ |
369
- | **Chat** | AI-style chat: messages, suggestions, SmartInput |
370
- | **ChatMessageBubble** | Message bubble (user/assistant, big/small text) |
371
- | **ChatSuggestions** | Suggestion chips when empty |
386
+ | Block | Description |
387
+ | --------------------- | --------------------------------------------------------------- |
388
+ | **Chat** | Opinionated messages, suggestions, and SmartInput composition |
389
+ | **ChatConversation** | Constrained conversation viewport that follows streaming output |
390
+ | **ChatMessageBubble** | Message bubble (user/assistant, big/small text) |
391
+ | **ChatSuggestions** | Suggestion chips when empty |
372
392
 
373
393
  ### Profile & Settings (Copy from showcase templates)
374
394
 
@@ -447,6 +467,11 @@ soft variant, overlay, surface, interaction state and shadow. Those are exactly
447
467
  `deriveThemeColors` computes, so hand-writing them produces a second derivation that
448
468
  drifts from the library's. Override a derived value by assigning to the result.
449
469
 
470
+ `WHILEUI_PRESET_SOFT` is the airy default. `WHILEUI_PRESET_ATELIER` is the stronger
471
+ product preset: tighter geometry, opaque surfaces, firmer borders, real shadows, and no
472
+ blur. Use it as the acceptance check that a screen still has enough visual weight when
473
+ the soft treatment is removed.
474
+
450
475
  ### FAB
451
476
 
452
477
  One primary action, raised above the content. It owns no icon dependency — pass the
@@ -558,7 +583,7 @@ import { AppShell, Header, BottomNav, ScrollView } from '@thewhileloop/whileui';
558
583
  | Forms | FormField + Input/NumericInput/LabeledField + DatePickerModal + FormModalScreen |
559
584
  | Settings | ProfileHeader + SettingsSection + SettingsItem (+ FormModalScreen for edits) |
560
585
  | E-commerce | ProductCard list → CheckoutSummary + ActionBar |
561
- | Chat | Chat + ChatSuggestions + SmartInput (attach, send). Extensible for images/tags |
586
+ | Chat | Chat, or ChatConversation + SmartInput for custom message/tool/source compositions |
562
587
  | App shell | AppShell + Header + BottomNav + content |
563
588
  | Loading | AppShell (`loading`) + PageSkeleton/ScreenSkeleton (keep header mounted) |
564
589
 
@@ -1000,6 +1025,17 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
1000
1025
  | loading | `boolean` | `false` | Show a spinner, expose busy state, and block input |
1001
1026
  | className | `string` | — | Additional Tailwind classes |
1002
1027
 
1028
+ ## Text
1029
+
1030
+ `Text` maps semantic roles to the active visual typography tokens; custom font-size
1031
+ classes are unnecessary for normal hierarchy. `heading` also sets
1032
+ `accessibilityRole="header"` unless the caller supplies another role.
1033
+
1034
+ | Prop | Type | Default |
1035
+ | ------- | --------------------------------------------------------------------------------------------- | ----------- |
1036
+ | variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'code'` | `'default'` |
1037
+ | size | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | by variant |
1038
+
1003
1039
  ## Input
1004
1040
 
1005
1041
  ```tsx
@@ -1008,6 +1044,9 @@ import { Input } from '@thewhileloop/whileui';
1008
1044
  <Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
1009
1045
  ```
1010
1046
 
1047
+ On web, filled Input, Textarea, NumericInput, and SmartInput surfaces preserve their
1048
+ themed background, foreground, and caret when the browser autofills them.
1049
+
1011
1050
  | Prop | Type | Default | Description |
1012
1051
  | -------------------- | --------------------------------- | ----------- | --------------------------------------------------------------------- |
1013
1052
  | variant | `'default' \| 'error' \| 'plain'` | `'default'` | Input style; `plain` drops border and fill for a large display figure |
@@ -1088,6 +1127,30 @@ import {
1088
1127
  </FormField>;
1089
1128
  ```
1090
1129
 
1130
+ `invalid` and `disabled` are inherited by `Input`, `Textarea`, `NumericInput`, and
1131
+ `Select`. An explicit control prop wins. `useFormFieldState()` exposes the same state for
1132
+ custom controls; labels, hints, messages, density, and required indicators remain part
1133
+ of the compound field rather than each input.
1134
+
1135
+ ## Surface
1136
+
1137
+ ```tsx
1138
+ import { Surface } from '@thewhileloop/whileui';
1139
+
1140
+ <Surface level="raised" padding="default">
1141
+ {/* grouped content */}
1142
+ </Surface>;
1143
+ ```
1144
+
1145
+ | Prop | Type | Default | Description |
1146
+ | -------- | ------------------------------------- | ----------- | ----------------------------------- |
1147
+ | level | `'base' \| 'raised' \| 'overlay'` | `'base'` | Semantic background and depth tier |
1148
+ | padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Token-backed interior spacing |
1149
+ | radius | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'xl'` | Token-backed corner radius |
1150
+ | bordered | `boolean` | `true` | Draw the theme hairline border |
1151
+ | shadow | `'none' \| 'sm' \| 'md' \| 'lg'` | by level | Override the level's depth |
1152
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay |
1153
+
1091
1154
  ## LabeledField
1092
1155
 
1093
1156
  ```tsx
@@ -1380,6 +1443,10 @@ import {
1380
1443
  </Select>;
1381
1444
  ```
1382
1445
 
1446
+ Focused and pressed options use the subtle interaction-state tokens; the primary token
1447
+ marks the selected indicator without replacing the option's foreground colour. Long
1448
+ selected values stay on one line and truncate without displacing the trigger chevron.
1449
+
1383
1450
  `SelectContent` supports optional frosted props:
1384
1451
 
1385
1452
  | Prop | Type | Default |
@@ -1738,9 +1805,34 @@ import { SmartInput, Button, ButtonText } from '@thewhileloop/whileui';
1738
1805
  />;
1739
1806
  ```
1740
1807
 
1741
- ## Chat
1808
+ ## ChatConversation / Chat
1809
+
1810
+ `ChatConversation` is the low-level viewport for custom conversation screens. Its content stays constrained on wide windows, follows new or streaming output while the reader is near the bottom, and stops following after the reader scrolls upward. Compose it with `SmartInput`; the application owns message data, streaming, tools, approvals, and sources.
1742
1811
 
1743
- AI-style chat: message list, suggestion chips when empty, SmartInput with attach/send slots. Uses semantic tokens; theme via `global.css`. Copy-paste block — edit directly. `renderMessage` for markdown/code, `loadingIndicator` for typing state.
1812
+ It is a `ScrollView` for a bounded active transcript. Paginate or window historical messages; use a virtualized list when the full history can grow without a bound.
1813
+
1814
+ ```tsx
1815
+ import { ChatConversation, SmartInput, View } from '@thewhileloop/whileui';
1816
+
1817
+ <View className="flex-1">
1818
+ <ChatConversation contentContainerClassName="gap-4 px-4 py-6">
1819
+ {messages.map((message) => (
1820
+ <ProductMessage key={message.id} message={message} />
1821
+ ))}
1822
+ </ChatConversation>
1823
+ <SmartInput
1824
+ value={value}
1825
+ onChangeText={setValue}
1826
+ onSubmitEditing={send}
1827
+ submitBehavior="submit"
1828
+ leftSlot={<AttachButton />}
1829
+ centerSlot={<ModelSelect />}
1830
+ rightSlot={<SendButton />}
1831
+ />
1832
+ </View>;
1833
+ ```
1834
+
1835
+ `Chat` is the opinionated convenience composition: message bubbles, suggestion chips, and `SmartInput` with attach/send slots. It uses semantic tokens; theme via `global.css`. Use `renderMessage` for markdown, code, tool calls, reasoning, or sources, and `loadingIndicator` for streaming feedback.
1744
1836
 
1745
1837
  ```tsx
1746
1838
  import { Chat, type ChatMessage } from '@thewhileloop/whileui';
@@ -1777,6 +1869,26 @@ const [value, setValue] = useState('');
1777
1869
  | `inputSafeArea` | `boolean` | SmartInput safe-area (default `true`) |
1778
1870
  | `keyboardVerticalOffset` | `number` | For header offset when keyboard opens |
1779
1871
 
1872
+ `ChatConversation` extends React Native `ScrollViewProps`, forwards its ref, and adds `contentContainerClassName` to style the constrained inner column. Caller `onScroll` and `onContentSizeChange` callbacks are preserved.
1873
+
1874
+ ### Optional Markdown and code
1875
+
1876
+ These are explicit subpaths so the root package remains independent of a Markdown
1877
+ parser. Install `markdown-it` only in applications that render Markdown.
1878
+
1879
+ ```tsx
1880
+ import { Markdown } from '@thewhileloop/whileui/markdown';
1881
+ import { CodeBlock } from '@thewhileloop/whileui/code';
1882
+
1883
+ <Markdown onLinkPress={openLink}>{assistantText}</Markdown>
1884
+ <CodeBlock code={source} language="TypeScript" actions={<CopyButton />} />
1885
+ ```
1886
+
1887
+ `Markdown` supports headings, paragraphs, emphasis, links, lists, quotes, rules, inline
1888
+ code, and fenced code. It never opens links or writes to the clipboard itself:
1889
+ `onLinkPress` and `codeActions` keep those capabilities in the application. `CodeBlock`
1890
+ is dependency-free and accepts an application-owned `actions` slot.
1891
+
1780
1892
  ## DatePickerModal / DatePickerInline / DateRangePickerModal / TimePickerModal
1781
1893
 
1782
1894
  Date selection blocks using react-native-calendars. Theme-aware via `useCalendarTheme` (Uniwind light/dark). Optional `theme` prop for custom RN color strings (hex/rgb/hsl/named). `CalendarTheme` maps to react-native-calendars Theme and supports `arrowColor`, `disabledArrowColor`, and optional font keys.
@@ -0,0 +1,8 @@
1
+ import { type ScrollViewInstance, type ScrollViewProps } from 'react-native';
2
+ import type { ForwardRefComponent } from '../../lib/ref-types';
3
+ /** ScrollView viewport for a bounded active transcript; virtualize unbounded history. */
4
+ export interface ChatConversationProps extends ScrollViewProps {
5
+ contentContainerClassName?: string;
6
+ }
7
+ declare const ChatConversation: ForwardRefComponent<ScrollViewInstance, ChatConversationProps>;
8
+ export { ChatConversation };
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import React, { useCallback, useRef } from 'react';
3
+ import { ScrollView, View, } from 'react-native';
4
+ import { cn } from '../../lib/cn';
5
+ const BOTTOM_THRESHOLD = 48;
6
+ function assignRef(ref, value) {
7
+ if (typeof ref === 'function') {
8
+ ref(value);
9
+ }
10
+ else if (ref) {
11
+ ref.current = value;
12
+ }
13
+ }
14
+ const ChatConversation = React.forwardRef(({ className, contentContainerClassName, contentContainerStyle, keyboardShouldPersistTaps = 'handled', onContentSizeChange, onScroll, scrollEventThrottle = 64, children, ...props }, forwardedRef) => {
15
+ const scrollRef = useRef(null);
16
+ const followsOutputRef = useRef(true);
17
+ const setRef = useCallback((instance) => {
18
+ scrollRef.current = instance;
19
+ assignRef(forwardedRef, instance);
20
+ }, [forwardedRef]);
21
+ const handleScroll = useCallback((event) => {
22
+ const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
23
+ const distanceFromBottom = contentSize.height - layoutMeasurement.height - contentOffset.y;
24
+ followsOutputRef.current = distanceFromBottom <= BOTTOM_THRESHOLD;
25
+ onScroll?.(event);
26
+ }, [onScroll]);
27
+ const handleContentSizeChange = useCallback((width, height) => {
28
+ onContentSizeChange?.(width, height);
29
+ if (followsOutputRef.current) {
30
+ scrollRef.current?.scrollToEnd({ animated: true });
31
+ }
32
+ }, [onContentSizeChange]);
33
+ return (_jsx(ScrollView, { ref: setRef, className: cn('flex-1', className), contentContainerStyle: { flexGrow: 1 }, keyboardShouldPersistTaps: keyboardShouldPersistTaps, onContentSizeChange: handleContentSizeChange, onScroll: handleScroll, scrollEventThrottle: scrollEventThrottle, ...props, children: _jsx(View, { className: cn('w-full max-w-3xl flex-1 self-center', contentContainerClassName), style: contentContainerStyle, children: children }) }));
34
+ });
35
+ ChatConversation.displayName = 'ChatConversation';
36
+ export { ChatConversation };
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React, { useRef, useEffect } from 'react';
3
- import { View, ScrollView, Pressable, KeyboardAvoidingView, Platform, useWindowDimensions, } from 'react-native';
2
+ import React from 'react';
3
+ import { View, Pressable, KeyboardAvoidingView, Platform, useWindowDimensions } from 'react-native';
4
4
  import { Text } from '../../components/text';
5
5
  import { SmartInput } from '../layout/smart-input';
6
+ import { ChatConversation } from './chat-conversation';
6
7
  import { cn } from '../../lib/cn';
7
8
  import { useInteractionTokens, withInteractivePressableStyle } from '../../lib/interaction-tokens';
8
9
  import { useVisualTokens } from '../../lib/visual-tokens';
@@ -49,23 +50,16 @@ export function ChatSuggestions({ suggestions, onSelect, className }) {
49
50
  }
50
51
  const EMPTY_SUGGESTIONS = [];
51
52
  export function Chat({ messages, value, onChangeText, onSend, placeholder = 'Message...', suggestions = EMPTY_SUGGESTIONS, onSuggestionPress, leftSlot, rightSlot, loading = false, header, emptyTitle, emptyDescription, exampleMessage, renderMessage, loadingIndicator, inputSafeArea = true, keyboardVerticalOffset = 0, className, messagesClassName, }) {
52
- const scrollRef = useRef(null);
53
53
  const visual = useVisualTokens();
54
54
  const emptyTitleTypography = typographyStyle(visual, 'emphasis');
55
55
  const emptyDescriptionTypography = typographyStyle(visual, 'label');
56
56
  const isEmpty = messages.length === 0;
57
57
  const showSuggestions = isEmpty && suggestions.length > 0;
58
- useEffect(() => {
59
- if (!showSuggestions && messages.length > 0) {
60
- scrollRef.current?.scrollToEnd({ animated: true });
61
- }
62
- }, [messages.length, showSuggestions]);
63
58
  const defaultRightSlot = rightSlot ?? (_jsx(ChatSendButton, { onPress: onSend, disabled: loading || !value.trim() }));
64
- return (_jsxs(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, keyboardVerticalOffset: keyboardVerticalOffset, style: { flex: 1 }, className: className, children: [header, _jsx(ScrollView, { ref: scrollRef, className: cn('flex-1', messagesClassName), contentContainerStyle: {
65
- flexGrow: 1,
59
+ return (_jsxs(KeyboardAvoidingView, { behavior: Platform.OS === 'ios' ? 'padding' : undefined, keyboardVerticalOffset: keyboardVerticalOffset, style: { flex: 1 }, className: className, children: [header, _jsx(ChatConversation, { className: cn('flex-1', messagesClassName), contentContainerStyle: {
66
60
  paddingBottom: 8,
67
61
  justifyContent: showSuggestions ? 'center' : 'flex-end',
68
- }, keyboardShouldPersistTaps: "handled", children: showSuggestions ? (_jsxs(View, { className: "flex-1 justify-center", children: [exampleMessage && (_jsx(View, { className: "mb-6 px-4", children: renderMessage ? (renderMessage(exampleMessage)) : (_jsx(ChatMessageBubble, { message: exampleMessage })) })), emptyTitle && (_jsx(Text, { className: "mb-1 text-center font-medium text-foreground", style: emptyTitleTypography, children: emptyTitle })), emptyDescription && (_jsx(Text, { className: "mb-4 text-center text-muted-foreground", style: emptyDescriptionTypography, children: emptyDescription })), _jsx(ChatSuggestions, { suggestions: suggestions, onSelect: onSuggestionPress ?? (() => { }) })] })) : (_jsxs(View, { className: "gap-3 px-4 pt-4", children: [messages.map((msg) => renderMessage ? (_jsx(React.Fragment, { children: renderMessage(msg) }, msg.id)) : (_jsx(ChatMessageBubble, { message: msg }, msg.id))), loading && loadingIndicator && _jsx(View, { className: "px-4", children: loadingIndicator })] })) }), _jsx(SmartInput, { value: value, onChangeText: onChangeText, placeholder: placeholder, leftSlot: leftSlot, rightSlot: defaultRightSlot, editable: !loading, safeArea: inputSafeArea })] }));
62
+ }, children: showSuggestions ? (_jsxs(View, { className: "flex-1 justify-center", children: [exampleMessage && (_jsx(View, { className: "mb-6 px-4", children: renderMessage ? (renderMessage(exampleMessage)) : (_jsx(ChatMessageBubble, { message: exampleMessage })) })), emptyTitle && (_jsx(Text, { className: "mb-1 text-center font-medium text-foreground", style: emptyTitleTypography, children: emptyTitle })), emptyDescription && (_jsx(Text, { className: "mb-4 text-center text-muted-foreground", style: emptyDescriptionTypography, children: emptyDescription })), _jsx(ChatSuggestions, { suggestions: suggestions, onSelect: onSuggestionPress ?? (() => { }) })] })) : (_jsxs(View, { className: "gap-3 px-4 pt-4", children: [messages.map((msg) => renderMessage ? (_jsx(React.Fragment, { children: renderMessage(msg) }, msg.id)) : (_jsx(ChatMessageBubble, { message: msg }, msg.id))), loading && loadingIndicator && _jsx(View, { className: "px-4", children: loadingIndicator })] })) }), _jsx(SmartInput, { value: value, onChangeText: onChangeText, placeholder: placeholder, leftSlot: leftSlot, rightSlot: defaultRightSlot, editable: !loading, safeArea: inputSafeArea })] }));
69
63
  }
70
64
  function ChatSendButton({ onPress, disabled }) {
71
65
  const interaction = useInteractionTokens();
@@ -1 +1,2 @@
1
1
  export { Chat, ChatMessageBubble, ChatSuggestions, type ChatMessage, type ChatProps, type ChatSuggestionsProps, } from './chat';
2
+ export { ChatConversation, type ChatConversationProps } from './chat-conversation';
@@ -1 +1,2 @@
1
1
  export { Chat, ChatMessageBubble, ChatSuggestions, } from './chat';
2
+ export { ChatConversation } from './chat-conversation';
@@ -7,6 +7,7 @@ import { focusRing } from '../../lib/focus-ring';
7
7
  import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
+ import { webAutofillField } from '../../lib/web-autofill';
10
11
  import { controlRecipe, shadowStyle, surfaceRadius, typographyStyle } from '../../lib/recipes';
11
12
  // The examples list is joined into one string so it can be an effect dependency without
12
13
  // re-running on every render. A NUL byte cannot appear in a placeholder.
@@ -143,5 +144,5 @@ export const SmartInput = forwardRef(function SmartInput({ leftSlot, centerSlot,
143
144
  { maxHeight: visual.smartInputMaxHeight },
144
145
  !editable ? { opacity: interaction.disabledOpacity } : null,
145
146
  style,
146
- ], className: cn('flex-1 text-foreground placeholder:text-muted-foreground', focusRing, variant === 'bar' && 'border-border bg-muted', variant === 'card' && 'bg-transparent', inputClassName), multiline: true, submitBehavior: submitBehavior, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, placeholder: placeholderState.placeholder, value: value, defaultValue: defaultValue, onChangeText: placeholderState.onChangeText, editable: editable, ...props }), _jsx(SmartInputSlot, { children: rightSlot })] }));
147
+ ], className: cn('flex-1 text-foreground placeholder:text-muted-foreground', focusRing, variant === 'bar' && 'border-border bg-muted', variant === 'bar' && webAutofillField, variant === 'card' && 'bg-transparent', inputClassName), multiline: true, submitBehavior: submitBehavior, textAlignVertical: "top", placeholderTextColor: placeholderTextColor ?? colors.placeholder, placeholder: placeholderState.placeholder, value: value, defaultValue: defaultValue, onChangeText: placeholderState.onChangeText, editable: editable, ...props }), _jsx(SmartInputSlot, { children: rightSlot })] }));
147
148
  });
package/dist/code.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { CodeBlock, type CodeBlockProps } from './components/code-block';
package/dist/code.js ADDED
@@ -0,0 +1 @@
1
+ export { CodeBlock } from './components/code-block';
@@ -1,6 +1,6 @@
1
1
  import { type ViewInstance, type ViewProps, type TextInstance, type TextProps } from 'react-native';
2
2
  import { type VariantProps } from '../../lib/tv';
3
- import { type FrostedSurfaceProps } from '../../lib/frosted-surface';
3
+ import type { FrostedSurfaceProps } from '../../lib/frosted-surface';
4
4
  import { type ShadowTier } from '../../lib/recipes';
5
5
  import type { ForwardRefComponent } from '../../lib/ref-types';
6
6
  declare const cardVariants: import("tailwind-variants").TVReturnType<{
@@ -1,13 +1,12 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { View, } from 'react-native';
4
4
  import { Text } from '../text';
5
+ import { Surface } from '../surface';
5
6
  import { cn } from '../../lib/cn';
6
7
  import { tv } from '../../lib/tv';
7
- import { useFrostedSurface } from '../../lib/frosted-surface';
8
8
  import { useVisualTokens } from '../../lib/visual-tokens';
9
- import { useThemeColors } from '../../lib/theme-colors';
10
- import { shadowStyle, surfacePadding, typographyStyle } from '../../lib/recipes';
9
+ import { typographyStyle } from '../../lib/recipes';
11
10
  const cardVariants = tv({
12
11
  base: 'border border-border bg-card',
13
12
  variants: {
@@ -28,27 +27,7 @@ const cardVariants = tv({
28
27
  },
29
28
  });
30
29
  const Card = React.forwardRef(({ className, padding, unstyled, shadow = 'sm', frosted = false, blurIntensity, blurTintToken, style, children, ...props }, ref) => {
31
- const visual = useVisualTokens();
32
- const colors = useThemeColors();
33
- const frostedSurface = useFrostedSurface({
34
- frosted,
35
- blurIntensity,
36
- blurTintToken,
37
- defaultTintToken: 'card',
38
- defaultBlurPreset: 'subtle',
39
- });
40
- const effectivePadding = surfacePadding(visual, padding ?? 'default');
41
- const cardStyle = [
42
- {
43
- borderRadius: unstyled ? 0 : visual.radiusXl,
44
- padding: unstyled ? 0 : effectivePadding,
45
- borderWidth: unstyled ? 0 : visual.borderWidthHairline,
46
- },
47
- unstyled ? null : shadowStyle(visual, colors, shadow),
48
- frostedSurface.surfaceStyle,
49
- style,
50
- ];
51
- return (_jsxs(View, { ref: ref, className: cn(cardVariants({ padding, unstyled }), frosted && 'relative overflow-hidden bg-transparent', className), style: cardStyle, ...props, children: [frostedSurface.overlay, children] }));
30
+ return (_jsx(Surface, { ref: ref, level: "base", padding: unstyled ? 'none' : (padding ?? 'default'), bordered: !unstyled, shadow: unstyled ? 'none' : shadow, frosted: frosted, blurIntensity: blurIntensity, blurTintToken: blurTintToken, className: cn(cardVariants({ padding, unstyled }), className), style: [unstyled ? { borderRadius: 0 } : null, style], ...props, children: children }));
52
31
  });
53
32
  Card.displayName = 'Card';
54
33
  const CardHeader = React.forwardRef(({ className, ...props }, ref) => {
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { type ViewProps } from 'react-native';
3
+ export interface CodeBlockProps extends Omit<ViewProps, 'children'> {
4
+ code: string;
5
+ language?: string;
6
+ actions?: React.ReactNode;
7
+ }
8
+ declare function CodeBlock({ code, language, actions, className, ...props }: CodeBlockProps): React.JSX.Element;
9
+ declare namespace CodeBlock {
10
+ var displayName: string;
11
+ }
12
+ export { CodeBlock };
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ScrollView, View } from 'react-native';
3
+ import { Surface } from '../surface';
4
+ import { Text } from '../text';
5
+ import { cn } from '../../lib/cn';
6
+ import { useVisualTokens } from '../../lib/visual-tokens';
7
+ function CodeBlock({ code, language, actions, className, ...props }) {
8
+ const label = language ? `${language} code` : 'Code';
9
+ const visual = useVisualTokens();
10
+ return (_jsxs(Surface, { level: "base", padding: "none", radius: "lg", shadow: "none", className: cn('overflow-hidden bg-muted', className), ...props, children: [language || actions ? (_jsxs(View, { className: "flex-row items-center justify-between border-b border-border px-3 py-2", children: [_jsx(Text, { variant: "caption", children: language ?? 'Code' }), actions] })) : null, _jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false, contentContainerStyle: { padding: visual.surfacePaddingSm }, children: _jsx(Text, { variant: "code", className: "bg-transparent px-0 py-0 text-foreground", accessibilityLabel: label, selectable: true, children: code }) })] }));
11
+ }
12
+ CodeBlock.displayName = 'CodeBlock';
13
+ export { CodeBlock };
@@ -0,0 +1 @@
1
+ export { CodeBlock, type CodeBlockProps } from './code-block';
@@ -0,0 +1 @@
1
+ export { CodeBlock } from './code-block';
@@ -45,6 +45,14 @@ declare const formControlVariants: import("tailwind-variants").TVReturnType<{
45
45
  false: "";
46
46
  };
47
47
  }, undefined>>;
48
+ type FormFieldDensity = NonNullable<VariantProps<typeof formFieldVariants>['density']>;
49
+ export interface FormFieldState {
50
+ density: FormFieldDensity;
51
+ invalid: boolean;
52
+ disabled: boolean;
53
+ required: boolean;
54
+ }
55
+ export declare function useFormFieldState(): FormFieldState;
48
56
  export interface FormFieldProps extends ViewProps, VariantProps<typeof formFieldVariants> {
49
57
  invalid?: boolean;
50
58
  disabled?: boolean;
@@ -41,6 +41,9 @@ const FormFieldContext = createContext({
41
41
  disabled: false,
42
42
  required: false,
43
43
  });
44
+ export function useFormFieldState() {
45
+ return useContext(FormFieldContext);
46
+ }
44
47
  const FormField = React.forwardRef(({ className, density = 'default', invalid = false, disabled = false, required = false, ...props }, ref) => {
45
48
  const interaction = useInteractionTokens();
46
49
  const contextValue = useMemo(() => ({ density, invalid, disabled, required }), [density, disabled, invalid, required]);
@@ -1 +1 @@
1
- export { FormField, FormLabel, FormControl, FormHint, FormMessage, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, } from './form-field';
1
+ export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, type FormFieldState, } from './form-field';
@@ -1 +1 @@
1
- export { FormField, FormLabel, FormControl, FormHint, FormMessage, formFieldVariants, formControlVariants, } from './form-field';
1
+ export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, } from './form-field';
@@ -25,6 +25,7 @@ declare const inputVariants: import("tailwind-variants").TVReturnType<{
25
25
  };
26
26
  }, undefined>>;
27
27
  export interface InputProps extends TextInputProps, VariantProps<typeof inputVariants> {
28
+ 'aria-invalid'?: boolean;
28
29
  className?: string;
29
30
  prefix?: React.ReactNode;
30
31
  suffix?: React.ReactNode;
@@ -8,6 +8,8 @@ import { useThemeColors } from '../../lib/theme-colors';
8
8
  import { useInteractionTokens } from '../../lib/interaction-tokens';
9
9
  import { useVisualTokens } from '../../lib/visual-tokens';
10
10
  import { typographyStyle } from '../../lib/recipes';
11
+ import { webAutofillField } from '../../lib/web-autofill';
12
+ import { useFormFieldState } from '../form-field/form-field';
11
13
  const inputVariants = tv({
12
14
  base: 'w-full border-border bg-muted text-foreground',
13
15
  variants: {
@@ -22,27 +24,30 @@ const inputVariants = tv({
22
24
  variant: 'default',
23
25
  },
24
26
  });
25
- const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, variant, editable = true, placeholderTextColor, onFocus, onBlur, ...props }, ref) => {
27
+ const Input = React.forwardRef(({ className, prefix, suffix, inputClassName, variant, editable, 'aria-invalid': ariaInvalid, placeholderTextColor, onFocus, onBlur, ...props }, ref) => {
26
28
  const colors = useThemeColors();
27
29
  const interaction = useInteractionTokens();
28
30
  const visual = useVisualTokens();
31
+ const field = useFormFieldState();
32
+ const effectiveEditable = editable ?? !field.disabled;
33
+ const effectiveVariant = variant ?? (field.invalid ? 'error' : 'default');
29
34
  // The border is on this wrapper and the focus is on the TextInput inside it, so the
30
35
  // wrapper learns about focus from the events rather than a pseudo-class.
31
36
  const [focused, setFocused] = useState(false);
32
- const plain = variant === 'plain';
33
- return (_jsxs(View, { className: cn('flex-row items-center border-border bg-muted', inputVariants({ variant }), focused && !plain && focusFieldWrapper, className), style: [
37
+ const plain = effectiveVariant === 'plain';
38
+ return (_jsxs(View, { className: cn('flex-row items-center border-border bg-muted', inputVariants({ variant: effectiveVariant }), focused && !plain && focusFieldWrapper, className), style: [
34
39
  {
35
40
  minHeight: visual.controlHeightDefault,
36
41
  borderWidth: plain ? 0 : visual.borderWidthControl,
37
42
  borderRadius: visual.radiusLg,
38
43
  paddingHorizontal: plain ? 0 : visual.controlPaddingXDefault,
39
44
  },
40
- !editable ? { opacity: interaction.disabledOpacity } : undefined,
41
- ], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, className: cn('flex-1 text-foreground placeholder:text-muted-foreground web:outline-none', inputClassName), style: {
45
+ !effectiveEditable ? { opacity: interaction.disabledOpacity } : undefined,
46
+ ], children: [prefix && (_jsx(View, { style: { paddingRight: Math.max(8, Math.round(visual.controlPaddingXDefault * 0.5)) }, children: prefix })), _jsx(TextInput, { ref: ref, className: cn('flex-1 text-foreground placeholder:text-muted-foreground web:outline-none', !plain && webAutofillField, inputClassName), style: {
42
47
  ...typographyStyle(visual, 'label'),
43
48
  minHeight: visual.controlHeightDefault - 2,
44
49
  paddingVertical: visual.controlPaddingYDefault,
45
- }, editable: editable, placeholderTextColor: placeholderTextColor ?? colors.placeholder, onFocus: (event) => {
50
+ }, editable: effectiveEditable, "aria-invalid": ariaInvalid ?? field.invalid, placeholderTextColor: placeholderTextColor ?? colors.placeholder, onFocus: (event) => {
46
51
  setFocused(true);
47
52
  onFocus?.(event);
48
53
  }, onBlur: (event) => {
@@ -0,0 +1 @@
1
+ export { Markdown, type MarkdownProps } from './markdown';
@@ -0,0 +1 @@
1
+ export { Markdown } from './markdown';
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+ import { type StyleProp, type TextStyle, type ViewProps } from 'react-native';
3
+ export interface MarkdownProps extends Omit<ViewProps, 'children'> {
4
+ children: string;
5
+ onLinkPress?: (url: string) => void;
6
+ codeActions?: (code: string, language: string | undefined) => React.ReactNode;
7
+ textStyle?: StyleProp<TextStyle>;
8
+ selectable?: boolean;
9
+ }
10
+ declare function Markdown({ children, onLinkPress, codeActions, textStyle, selectable, className, ...props }: MarkdownProps): React.JSX.Element;
11
+ declare namespace Markdown {
12
+ var displayName: string;
13
+ }
14
+ export { Markdown };