@thewhileloop/whileui 4.0.0 → 4.1.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 +186 -20
- package/dist/blocks/chat/chat-conversation.d.ts +8 -0
- package/dist/blocks/chat/chat-conversation.js +36 -0
- package/dist/blocks/chat/chat.js +5 -11
- package/dist/blocks/chat/index.d.ts +1 -0
- package/dist/blocks/chat/index.js +1 -0
- package/dist/blocks/layout/smart-input.js +2 -1
- package/dist/blocks/media/smart-image.js +5 -7
- package/dist/code.d.ts +1 -0
- package/dist/code.js +1 -0
- package/dist/components/avatar/avatar.d.ts +3 -2
- package/dist/components/avatar/avatar.js +19 -13
- package/dist/components/avatar/index.d.ts +1 -1
- package/dist/components/card/card.d.ts +1 -1
- package/dist/components/card/card.js +4 -25
- package/dist/components/code-block/code-block.d.ts +12 -0
- package/dist/components/code-block/code-block.js +13 -0
- package/dist/components/code-block/index.d.ts +1 -0
- package/dist/components/code-block/index.js +1 -0
- package/dist/components/form-field/form-field.d.ts +8 -0
- package/dist/components/form-field/form-field.js +3 -0
- package/dist/components/form-field/index.d.ts +1 -1
- package/dist/components/form-field/index.js +1 -1
- package/dist/components/input/input.d.ts +1 -0
- package/dist/components/input/input.js +11 -6
- package/dist/components/markdown/index.d.ts +1 -0
- package/dist/components/markdown/index.js +1 -0
- package/dist/components/markdown/markdown.d.ts +14 -0
- package/dist/components/markdown/markdown.js +94 -0
- package/dist/components/numeric-input/numeric-input.d.ts +1 -0
- package/dist/components/numeric-input/numeric-input.js +11 -6
- package/dist/components/select/select.d.ts +2 -1
- package/dist/components/select/select.js +44 -24
- package/dist/components/surface/index.d.ts +1 -0
- package/dist/components/surface/index.js +1 -0
- package/dist/components/surface/surface.d.ts +14 -0
- package/dist/components/surface/surface.js +35 -0
- package/dist/components/text/text.d.ts +42 -42
- package/dist/components/text/text.js +39 -17
- package/dist/components/textarea/textarea.d.ts +1 -0
- package/dist/components/textarea/textarea.js +7 -3
- package/dist/components/toast/toast.js +5 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/lib/theme-bridge.d.ts +1 -1
- package/dist/lib/theme-bridge.js +52 -18
- package/dist/lib/web-autofill.d.ts +9 -0
- package/dist/lib/web-autofill.js +9 -0
- package/dist/markdown.d.ts +1 -0
- package/dist/markdown.js +1 -0
- package/package.json +16 -2
package/README.md
CHANGED
|
@@ -11,6 +11,18 @@ production targets — iOS and Android set the interaction baseline. See
|
|
|
11
11
|
[ROADMAP.md](./ROADMAP.md) for the product boundary and extraction rules, and
|
|
12
12
|
[PLAN.md](./PLAN.md) for the supported stack and production contracts.
|
|
13
13
|
|
|
14
|
+
## See it in action
|
|
15
|
+
|
|
16
|
+
One React Native component contract across native and web, with semantic typography,
|
|
17
|
+
token-driven light and dark themes, accessible controls, and application-owned chat
|
|
18
|
+
composition. The same component tree can feel airy and atmospheric or compact and
|
|
19
|
+
editorial by changing tokens alone.
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="./docs/assets/whileui-showcase.gif" width="300" alt="WhileUI Cosmic Aura mobile theme demonstrating light and dark modes, controls, Select, and composable chat" />
|
|
23
|
+
<img src="./docs/assets/whileui-showcase-atelier.gif" width="300" alt="WhileUI Atelier mobile theme demonstrating compact controls, authentication forms, Select, and composable chat" />
|
|
24
|
+
</p>
|
|
25
|
+
|
|
14
26
|
## Installation
|
|
15
27
|
|
|
16
28
|
```bash
|
|
@@ -18,6 +30,8 @@ bun add @thewhileloop/whileui
|
|
|
18
30
|
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
31
|
# Only if you use Select, Popover, Tooltip, or HoverCard:
|
|
20
32
|
bun add @rn-primitives/portal @rn-primitives/hooks @rn-primitives/slot @rn-primitives/select @rn-primitives/popover @rn-primitives/tooltip @rn-primitives/hover-card
|
|
33
|
+
# Only if you import @thewhileloop/whileui/markdown:
|
|
34
|
+
bun add markdown-it
|
|
21
35
|
```
|
|
22
36
|
|
|
23
37
|
Use `npm install` instead of `bun add` if you prefer npm.
|
|
@@ -230,17 +244,33 @@ function MyScreen() {
|
|
|
230
244
|
`@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.
|
|
231
245
|
- **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
|
|
232
246
|
|
|
247
|
+
### Composition grammar
|
|
248
|
+
|
|
249
|
+
- Start with one page canvas and flat sections. Add a `Surface` only when grouping or
|
|
250
|
+
elevation communicates something; do not wrap every section in a card.
|
|
251
|
+
- Use `level="base"` for grouped content, `raised` for selectable or floating regions,
|
|
252
|
+
and `overlay` for content above the page. Avoid nesting more than one surfaced level.
|
|
253
|
+
- Express hierarchy through `Text` variants before custom font-size classes. Keep one
|
|
254
|
+
primary action per region and constrain long-form content with `Container`.
|
|
255
|
+
- Forms use `FormField`; its disabled and invalid state flows into `Input`, `Textarea`,
|
|
256
|
+
`NumericInput`, and `Select`. Pass a control prop only to override that inherited state.
|
|
257
|
+
- Chat mechanics belong to `ChatConversation` + `SmartInput`. Message payloads, tools,
|
|
258
|
+
reasoning, approvals, sources, and action rows are application-owned compositions.
|
|
259
|
+
- Import Markdown and code rendering from their optional subpaths. Do not make the root
|
|
260
|
+
package pay for content rendering when the screen does not use it.
|
|
261
|
+
|
|
233
262
|
## Components
|
|
234
263
|
|
|
235
264
|
### Primitives
|
|
236
265
|
|
|
237
|
-
| Component | Notes
|
|
238
|
-
| ------------- |
|
|
239
|
-
| **Text** | Themed text with variant support
|
|
240
|
-
| **
|
|
241
|
-
| **
|
|
242
|
-
| **
|
|
243
|
-
| **
|
|
266
|
+
| Component | Notes |
|
|
267
|
+
| ------------- | ------------------------------------------------ |
|
|
268
|
+
| **Text** | Themed text with variant support |
|
|
269
|
+
| **Surface** | Token-backed base, raised, and overlay container |
|
|
270
|
+
| **View** | Themed view wrapper |
|
|
271
|
+
| **Pressable** | Themed pressable wrapper |
|
|
272
|
+
| **Stack** | Vertical flex layout with gap |
|
|
273
|
+
| **Row** | Horizontal flex layout with gap |
|
|
244
274
|
|
|
245
275
|
### Root Providers
|
|
246
276
|
|
|
@@ -273,6 +303,7 @@ function MyScreen() {
|
|
|
273
303
|
| Component | Variants | Notes |
|
|
274
304
|
| --------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
275
305
|
| **Card** | padding (none, sm, default, lg), unstyled | Compound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter |
|
|
306
|
+
| **Surface** | level (base, raised, overlay) | Shared radius, padding, border, shadow, and optional frosted treatment |
|
|
276
307
|
| **Badge** | default, secondary, destructive, outline, success | BadgeText sub-component |
|
|
277
308
|
| **FAB** | default, secondary, destructive, surface; sm/default/lg | Floating action button. `label` required; pass your own icon as children |
|
|
278
309
|
| **Alert** | default, destructive, success, warning | AlertTitle & AlertDescription |
|
|
@@ -364,11 +395,12 @@ Copy from `apps/showcase/templates/auth/`:
|
|
|
364
395
|
|
|
365
396
|
### Chat
|
|
366
397
|
|
|
367
|
-
| Block | Description
|
|
368
|
-
| --------------------- |
|
|
369
|
-
| **Chat** |
|
|
370
|
-
| **
|
|
371
|
-
| **
|
|
398
|
+
| Block | Description |
|
|
399
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
400
|
+
| **Chat** | Opinionated messages, suggestions, and SmartInput composition |
|
|
401
|
+
| **ChatConversation** | Constrained conversation viewport that follows streaming output |
|
|
402
|
+
| **ChatMessageBubble** | Message bubble (user/assistant, big/small text) |
|
|
403
|
+
| **ChatSuggestions** | Suggestion chips when empty |
|
|
372
404
|
|
|
373
405
|
### Profile & Settings (Copy from showcase templates)
|
|
374
406
|
|
|
@@ -405,9 +437,9 @@ Copy from `apps/showcase/templates/profile/`:
|
|
|
405
437
|
|
|
406
438
|
### Media
|
|
407
439
|
|
|
408
|
-
| Block | Description
|
|
409
|
-
| -------------- |
|
|
410
|
-
| **SmartImage** | Image with
|
|
440
|
+
| Block | Description |
|
|
441
|
+
| -------------- | ------------------------------------------------------------------ |
|
|
442
|
+
| **SmartImage** | Image with explicit loading, loaded, fallback, and error lifecycle |
|
|
411
443
|
|
|
412
444
|
### Date Picker
|
|
413
445
|
|
|
@@ -447,6 +479,11 @@ soft variant, overlay, surface, interaction state and shadow. Those are exactly
|
|
|
447
479
|
`deriveThemeColors` computes, so hand-writing them produces a second derivation that
|
|
448
480
|
drifts from the library's. Override a derived value by assigning to the result.
|
|
449
481
|
|
|
482
|
+
`WHILEUI_PRESET_SOFT` is the airy default. `WHILEUI_PRESET_ATELIER` is the stronger
|
|
483
|
+
product preset: tighter geometry, opaque surfaces, firmer borders, real shadows, and no
|
|
484
|
+
blur. Use it as the acceptance check that a screen still has enough visual weight when
|
|
485
|
+
the soft treatment is removed.
|
|
486
|
+
|
|
450
487
|
### FAB
|
|
451
488
|
|
|
452
489
|
One primary action, raised above the content. It owns no icon dependency — pass the
|
|
@@ -558,7 +595,7 @@ import { AppShell, Header, BottomNav, ScrollView } from '@thewhileloop/whileui';
|
|
|
558
595
|
| Forms | FormField + Input/NumericInput/LabeledField + DatePickerModal + FormModalScreen |
|
|
559
596
|
| Settings | ProfileHeader + SettingsSection + SettingsItem (+ FormModalScreen for edits) |
|
|
560
597
|
| E-commerce | ProductCard list → CheckoutSummary + ActionBar |
|
|
561
|
-
| Chat | Chat
|
|
598
|
+
| Chat | Chat, or ChatConversation + SmartInput for custom message/tool/source compositions |
|
|
562
599
|
| App shell | AppShell + Header + BottomNav + content |
|
|
563
600
|
| Loading | AppShell (`loading`) + PageSkeleton/ScreenSkeleton (keep header mounted) |
|
|
564
601
|
|
|
@@ -901,6 +938,9 @@ const adapter: ThemeBridgeAdapter = {
|
|
|
901
938
|
const { mode, resolvedTheme, setMode, cycleMode } = useThemeBridge({ adapter });
|
|
902
939
|
```
|
|
903
940
|
|
|
941
|
+
The bridge loads a new adapter before saving into it. If a user changes the mode while
|
|
942
|
+
storage is still loading, that explicit choice wins over the late persisted value.
|
|
943
|
+
|
|
904
944
|
When another store already owns the mode, mount the controlled bridge once near the
|
|
905
945
|
application root. Change `mode` in the owning event handler; the bridge applies and
|
|
906
946
|
persists it without mirroring state back through a callback effect.
|
|
@@ -1000,6 +1040,17 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
|
|
|
1000
1040
|
| loading | `boolean` | `false` | Show a spinner, expose busy state, and block input |
|
|
1001
1041
|
| className | `string` | — | Additional Tailwind classes |
|
|
1002
1042
|
|
|
1043
|
+
## Text
|
|
1044
|
+
|
|
1045
|
+
`Text` maps semantic roles to the active visual typography tokens; custom font-size
|
|
1046
|
+
classes are unnecessary for normal hierarchy. `heading` also sets
|
|
1047
|
+
`accessibilityRole="header"` unless the caller supplies another role.
|
|
1048
|
+
|
|
1049
|
+
| Prop | Type | Default |
|
|
1050
|
+
| ------- | --------------------------------------------------------------------------------------------- | ----------- |
|
|
1051
|
+
| variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'code'` | `'default'` |
|
|
1052
|
+
| size | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | by variant |
|
|
1053
|
+
|
|
1003
1054
|
## Input
|
|
1004
1055
|
|
|
1005
1056
|
```tsx
|
|
@@ -1008,6 +1059,9 @@ import { Input } from '@thewhileloop/whileui';
|
|
|
1008
1059
|
<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
|
|
1009
1060
|
```
|
|
1010
1061
|
|
|
1062
|
+
On web, filled Input, Textarea, NumericInput, and SmartInput surfaces preserve their
|
|
1063
|
+
themed background, foreground, and caret when the browser autofills them.
|
|
1064
|
+
|
|
1011
1065
|
| Prop | Type | Default | Description |
|
|
1012
1066
|
| -------------------- | --------------------------------- | ----------- | --------------------------------------------------------------------- |
|
|
1013
1067
|
| variant | `'default' \| 'error' \| 'plain'` | `'default'` | Input style; `plain` drops border and fill for a large display figure |
|
|
@@ -1088,6 +1142,30 @@ import {
|
|
|
1088
1142
|
</FormField>;
|
|
1089
1143
|
```
|
|
1090
1144
|
|
|
1145
|
+
`invalid` and `disabled` are inherited by `Input`, `Textarea`, `NumericInput`, and
|
|
1146
|
+
`Select`. An explicit control prop wins. `useFormFieldState()` exposes the same state for
|
|
1147
|
+
custom controls; labels, hints, messages, density, and required indicators remain part
|
|
1148
|
+
of the compound field rather than each input.
|
|
1149
|
+
|
|
1150
|
+
## Surface
|
|
1151
|
+
|
|
1152
|
+
```tsx
|
|
1153
|
+
import { Surface } from '@thewhileloop/whileui';
|
|
1154
|
+
|
|
1155
|
+
<Surface level="raised" padding="default">
|
|
1156
|
+
{/* grouped content */}
|
|
1157
|
+
</Surface>;
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
| Prop | Type | Default | Description |
|
|
1161
|
+
| -------- | ------------------------------------- | ----------- | ----------------------------------- |
|
|
1162
|
+
| level | `'base' \| 'raised' \| 'overlay'` | `'base'` | Semantic background and depth tier |
|
|
1163
|
+
| padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Token-backed interior spacing |
|
|
1164
|
+
| radius | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'xl'` | Token-backed corner radius |
|
|
1165
|
+
| bordered | `boolean` | `true` | Draw the theme hairline border |
|
|
1166
|
+
| shadow | `'none' \| 'sm' \| 'md' \| 'lg'` | by level | Override the level's depth |
|
|
1167
|
+
| frosted | `boolean` | `false` | Enable the optional frosted overlay |
|
|
1168
|
+
|
|
1091
1169
|
## LabeledField
|
|
1092
1170
|
|
|
1093
1171
|
```tsx
|
|
@@ -1380,6 +1458,12 @@ import {
|
|
|
1380
1458
|
</Select>;
|
|
1381
1459
|
```
|
|
1382
1460
|
|
|
1461
|
+
Focused and pressed options use the subtle interaction-state tokens; the primary token
|
|
1462
|
+
marks the selected indicator without replacing the option's foreground colour. Long
|
|
1463
|
+
selected values stay on one line and truncate without displacing the trigger chevron.
|
|
1464
|
+
For popover presentation on web, the options panel matches the trigger width, caps
|
|
1465
|
+
itself to the collision-available viewport, and wraps long option labels inside it.
|
|
1466
|
+
|
|
1383
1467
|
`SelectContent` supports optional frosted props:
|
|
1384
1468
|
|
|
1385
1469
|
| Prop | Type | Default |
|
|
@@ -1440,7 +1524,7 @@ import {
|
|
|
1440
1524
|
import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
|
|
1441
1525
|
|
|
1442
1526
|
<Avatar size="default">
|
|
1443
|
-
<AvatarImage
|
|
1527
|
+
<AvatarImage source={{ uri: 'https://...' }} />
|
|
1444
1528
|
<AvatarFallback>JD</AvatarFallback>
|
|
1445
1529
|
</Avatar>;
|
|
1446
1530
|
```
|
|
@@ -1449,6 +1533,10 @@ import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
|
|
|
1449
1533
|
| ---- | --------------------------- | ----------- |
|
|
1450
1534
|
| size | `'sm' \| 'default' \| 'lg'` | `'default'` |
|
|
1451
1535
|
|
|
1536
|
+
`AvatarImage` keeps `AvatarFallback` visible while loading or after an error. Its
|
|
1537
|
+
`onLoadingStatusChange` callback reports `'loading'`, `'loaded'`, and `'error'`, including
|
|
1538
|
+
when a changed source begins loading, while the standard image event callbacks still run.
|
|
1539
|
+
|
|
1452
1540
|
## Progress
|
|
1453
1541
|
|
|
1454
1542
|
```tsx
|
|
@@ -1485,6 +1573,9 @@ toast({ title: 'Success', description: 'Saved!', variant: 'success' });
|
|
|
1485
1573
|
| variant | `'default' \| 'success' \| 'destructive'` |
|
|
1486
1574
|
| duration | `number` (ms) |
|
|
1487
1575
|
|
|
1576
|
+
Each toast owns its duration from the moment it appears; adding or dismissing another
|
|
1577
|
+
toast does not restart timers already in progress.
|
|
1578
|
+
|
|
1488
1579
|
## Popover
|
|
1489
1580
|
|
|
1490
1581
|
```tsx
|
|
@@ -1738,9 +1829,34 @@ import { SmartInput, Button, ButtonText } from '@thewhileloop/whileui';
|
|
|
1738
1829
|
/>;
|
|
1739
1830
|
```
|
|
1740
1831
|
|
|
1741
|
-
## Chat
|
|
1832
|
+
## ChatConversation / Chat
|
|
1742
1833
|
|
|
1743
|
-
|
|
1834
|
+
`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.
|
|
1835
|
+
|
|
1836
|
+
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.
|
|
1837
|
+
|
|
1838
|
+
```tsx
|
|
1839
|
+
import { ChatConversation, SmartInput, View } from '@thewhileloop/whileui';
|
|
1840
|
+
|
|
1841
|
+
<View className="flex-1">
|
|
1842
|
+
<ChatConversation contentContainerClassName="gap-4 px-4 py-6">
|
|
1843
|
+
{messages.map((message) => (
|
|
1844
|
+
<ProductMessage key={message.id} message={message} />
|
|
1845
|
+
))}
|
|
1846
|
+
</ChatConversation>
|
|
1847
|
+
<SmartInput
|
|
1848
|
+
value={value}
|
|
1849
|
+
onChangeText={setValue}
|
|
1850
|
+
onSubmitEditing={send}
|
|
1851
|
+
submitBehavior="submit"
|
|
1852
|
+
leftSlot={<AttachButton />}
|
|
1853
|
+
centerSlot={<ModelSelect />}
|
|
1854
|
+
rightSlot={<SendButton />}
|
|
1855
|
+
/>
|
|
1856
|
+
</View>;
|
|
1857
|
+
```
|
|
1858
|
+
|
|
1859
|
+
`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
1860
|
|
|
1745
1861
|
```tsx
|
|
1746
1862
|
import { Chat, type ChatMessage } from '@thewhileloop/whileui';
|
|
@@ -1777,6 +1893,56 @@ const [value, setValue] = useState('');
|
|
|
1777
1893
|
| `inputSafeArea` | `boolean` | SmartInput safe-area (default `true`) |
|
|
1778
1894
|
| `keyboardVerticalOffset` | `number` | For header offset when keyboard opens |
|
|
1779
1895
|
|
|
1896
|
+
`ChatConversation` extends React Native `ScrollViewProps`, forwards its ref, and adds `contentContainerClassName` to style the constrained inner column. Caller `onScroll` and `onContentSizeChange` callbacks are preserved.
|
|
1897
|
+
|
|
1898
|
+
### Optional Markdown and code
|
|
1899
|
+
|
|
1900
|
+
These are explicit subpaths so the root package remains independent of a Markdown
|
|
1901
|
+
parser. Install `markdown-it` only in applications that render Markdown.
|
|
1902
|
+
|
|
1903
|
+
```tsx
|
|
1904
|
+
import { Markdown } from '@thewhileloop/whileui/markdown';
|
|
1905
|
+
import { CodeBlock } from '@thewhileloop/whileui/code';
|
|
1906
|
+
|
|
1907
|
+
<Markdown onLinkPress={openLink}>{assistantText}</Markdown>
|
|
1908
|
+
<CodeBlock code={source} language="TypeScript" actions={<CopyButton />} />
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
`Markdown` supports headings, paragraphs, emphasis, links, lists, quotes, rules, inline
|
|
1912
|
+
code, and fenced code. It never opens links or writes to the clipboard itself:
|
|
1913
|
+
`onLinkPress` and `codeActions` keep those capabilities in the application. `CodeBlock`
|
|
1914
|
+
is dependency-free and accepts an application-owned `actions` slot.
|
|
1915
|
+
|
|
1916
|
+
## SmartImage
|
|
1917
|
+
|
|
1918
|
+
```tsx
|
|
1919
|
+
import { SmartImage } from '@thewhileloop/whileui';
|
|
1920
|
+
|
|
1921
|
+
<SmartImage
|
|
1922
|
+
source={{ uri: imageUrl }}
|
|
1923
|
+
fallbackSource={{ uri: fallbackUrl }}
|
|
1924
|
+
alt="Product preview"
|
|
1925
|
+
className="h-48 w-full"
|
|
1926
|
+
/>;
|
|
1927
|
+
```
|
|
1928
|
+
|
|
1929
|
+
| Prop | Type | Default |
|
|
1930
|
+
| ------------------- | -------------------------------------------------- | --------- |
|
|
1931
|
+
| `source` | `ImageProps['source']` | required |
|
|
1932
|
+
| `alt` | `string` | required |
|
|
1933
|
+
| `fallbackSource` | `ImageProps['source']` | — |
|
|
1934
|
+
| `skeleton` | `boolean` | `true` |
|
|
1935
|
+
| `skeletonClassName` | `string` | — |
|
|
1936
|
+
| `transition` | `number` | `200` |
|
|
1937
|
+
| `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` |
|
|
1938
|
+
| `objectFit` | `'cover' \| 'contain' \| 'fill' \| 'scale-down'` | `'cover'` |
|
|
1939
|
+
| `onLoad` | `() => void` | — |
|
|
1940
|
+
| `onError` | `() => void` | — |
|
|
1941
|
+
|
|
1942
|
+
The skeleton returns whenever the primary image starts loading again. A failed primary
|
|
1943
|
+
source switches to `fallbackSource` when supplied without re-entering a primary/fallback
|
|
1944
|
+
loop.
|
|
1945
|
+
|
|
1780
1946
|
## DatePickerModal / DatePickerInline / DateRangePickerModal / TimePickerModal
|
|
1781
1947
|
|
|
1782
1948
|
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.
|
|
@@ -2298,7 +2464,7 @@ Tracked work items for future releases.
|
|
|
2298
2464
|
### Documentation Gaps
|
|
2299
2465
|
|
|
2300
2466
|
- [ ] Add `## API` sections for: Textarea, Toggle, ToggleGroup, Label, Separator, Skeleton, AspectRatio, Collapsible, Text, View
|
|
2301
|
-
- [ ] Add `## API` sections for blocks: FloatingBottomNav, TabBar, FormModalScreen, ErrorState, LoadingScreen, ListItem, NotificationItem, MetricCard,
|
|
2467
|
+
- [ ] Add `## API` sections for blocks: FloatingBottomNav, TabBar, FormModalScreen, ErrorState, LoadingScreen, ListItem, NotificationItem, MetricCard, TimelineFeed
|
|
2302
2468
|
|
|
2303
2469
|
### Infrastructure
|
|
2304
2470
|
|
|
@@ -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 };
|
package/dist/blocks/chat/chat.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React
|
|
3
|
-
import { View,
|
|
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(
|
|
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
|
-
},
|
|
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();
|
|
@@ -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
|
});
|
|
@@ -44,21 +44,19 @@ function resolveRadius(visual, radius) {
|
|
|
44
44
|
return visual.radiusMd;
|
|
45
45
|
}
|
|
46
46
|
const SmartImage = React.forwardRef(({ className, source, alt, skeleton = true, skeletonClassName, transition = 200, radius, objectFit, onLoad, onError, fallbackSource, style, ...props }, ref) => {
|
|
47
|
-
const [
|
|
48
|
-
const [error, setError] = useState(false);
|
|
47
|
+
const [phase, setPhase] = useState('loading');
|
|
49
48
|
const visual = useVisualTokens();
|
|
50
49
|
const borderRadius = resolveRadius(visual, radius ?? 'md');
|
|
51
50
|
const handleLoad = () => {
|
|
52
|
-
|
|
51
|
+
setPhase((current) => (current === 'fallback' ? 'fallback' : 'loaded'));
|
|
53
52
|
onLoad?.();
|
|
54
53
|
};
|
|
55
54
|
const handleError = () => {
|
|
56
|
-
|
|
57
|
-
setError(true);
|
|
55
|
+
setPhase((current) => current === 'fallback' ? 'fallback' : fallbackSource ? 'fallback' : 'error');
|
|
58
56
|
onError?.();
|
|
59
57
|
};
|
|
60
|
-
const imageSource =
|
|
61
|
-
return (_jsxs(View, { ref: ref, className: cn(smartImageVariants({ radius, objectFit }), className), style: [{ borderRadius }, style], ...props, children: [skeleton && loading && (_jsx(Skeleton, { className: cn('absolute inset-0', skeletonClassName) })), _jsx(Image, { source: imageSource, alt: alt, transition: transition, onLoad: handleLoad, onError: handleError, className: cn('w-full h-full', objectFit === 'cover' && 'object-cover', objectFit === 'contain' && 'object-contain', objectFit === 'fill' && 'object-fill', objectFit === 'scale-down' && 'object-scale-down'), style: { opacity:
|
|
58
|
+
const imageSource = phase === 'fallback' ? fallbackSource : source;
|
|
59
|
+
return (_jsxs(View, { ref: ref, className: cn(smartImageVariants({ radius, objectFit }), className), style: [{ borderRadius }, style], ...props, children: [skeleton && phase === 'loading' && (_jsx(Skeleton, { className: cn('absolute inset-0', skeletonClassName) })), _jsx(Image, { source: imageSource, alt: alt, transition: transition, onLoadStart: () => setPhase((current) => (current === 'fallback' ? current : 'loading')), onLoad: handleLoad, onError: handleError, className: cn('w-full h-full', objectFit === 'cover' && 'object-cover', objectFit === 'contain' && 'object-contain', objectFit === 'fill' && 'object-fill', objectFit === 'scale-down' && 'object-scale-down'), style: { opacity: phase === 'loading' ? 0 : 1 } })] }));
|
|
62
60
|
});
|
|
63
61
|
SmartImage.displayName = 'SmartImage';
|
|
64
62
|
export { SmartImage, smartImageVariants };
|
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';
|
|
@@ -27,15 +27,16 @@ declare const avatarVariants: import("tailwind-variants").TVReturnType<{
|
|
|
27
27
|
export interface AvatarProps extends ViewProps, VariantProps<typeof avatarVariants> {
|
|
28
28
|
className?: string;
|
|
29
29
|
}
|
|
30
|
+
export type AvatarImageStatus = 'loading' | 'loaded' | 'error';
|
|
30
31
|
export interface AvatarImageProps extends Omit<ImageProps, 'style'> {
|
|
31
32
|
className?: string;
|
|
32
|
-
onLoadingStatusChange?: (status:
|
|
33
|
+
onLoadingStatusChange?: (status: AvatarImageStatus) => void;
|
|
33
34
|
}
|
|
34
35
|
export interface AvatarFallbackProps extends ViewProps {
|
|
35
36
|
className?: string;
|
|
36
37
|
}
|
|
37
38
|
declare const Avatar: ForwardRefComponent<ViewInstance, AvatarProps>;
|
|
38
|
-
declare function AvatarImage({ className, onLoadingStatusChange, ...props }: AvatarImageProps): React.JSX.Element;
|
|
39
|
+
declare function AvatarImage({ className, onLoadingStatusChange, onLoadStart, onLoad, onError, ...props }: AvatarImageProps): React.JSX.Element;
|
|
39
40
|
declare namespace AvatarImage {
|
|
40
41
|
var displayName: string;
|
|
41
42
|
}
|
|
@@ -20,30 +20,36 @@ const avatarVariants = tv({
|
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
const AvatarContext = React.createContext({
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
imageStatus: 'loading',
|
|
24
|
+
setImageStatus: () => { },
|
|
25
25
|
});
|
|
26
26
|
const Avatar = React.forwardRef(({ className, size, ...props }, ref) => {
|
|
27
|
-
const [
|
|
28
|
-
const contextValue = useMemo(() => ({
|
|
27
|
+
const [imageStatus, setImageStatus] = useState('loading');
|
|
28
|
+
const contextValue = useMemo(() => ({ imageStatus, setImageStatus }), [imageStatus]);
|
|
29
29
|
return (_jsx(AvatarContext.Provider, { value: contextValue, children: _jsx(View, { ref: ref, className: cn(avatarVariants({ size }), className), ...props }) }));
|
|
30
30
|
});
|
|
31
31
|
Avatar.displayName = 'Avatar';
|
|
32
|
-
function AvatarImage({ className, onLoadingStatusChange, ...props }) {
|
|
33
|
-
const {
|
|
34
|
-
return (_jsx(Image, { className: cn('aspect-square h-full w-full rounded-full', className),
|
|
35
|
-
|
|
32
|
+
function AvatarImage({ className, onLoadingStatusChange, onLoadStart, onLoad, onError, ...props }) {
|
|
33
|
+
const { setImageStatus } = React.useContext(AvatarContext);
|
|
34
|
+
return (_jsx(Image, { ...props, className: cn('aspect-square h-full w-full rounded-full', className), onLoadStart: () => {
|
|
35
|
+
setImageStatus('loading');
|
|
36
|
+
onLoadingStatusChange?.('loading');
|
|
37
|
+
onLoadStart?.();
|
|
38
|
+
}, onLoad: (event) => {
|
|
39
|
+
setImageStatus('loaded');
|
|
36
40
|
onLoadingStatusChange?.('loaded');
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
onLoad?.(event);
|
|
42
|
+
}, onError: (event) => {
|
|
43
|
+
setImageStatus('error');
|
|
39
44
|
onLoadingStatusChange?.('error');
|
|
40
|
-
|
|
45
|
+
onError?.(event);
|
|
46
|
+
} }));
|
|
41
47
|
}
|
|
42
48
|
AvatarImage.displayName = 'AvatarImage';
|
|
43
49
|
function AvatarFallback({ className, children, ...props }) {
|
|
44
|
-
const {
|
|
50
|
+
const { imageStatus } = React.useContext(AvatarContext);
|
|
45
51
|
const visual = useVisualTokens();
|
|
46
|
-
if (
|
|
52
|
+
if (imageStatus === 'loaded')
|
|
47
53
|
return null;
|
|
48
54
|
return (_jsx(View, { className: cn('flex h-full w-full items-center justify-center rounded-full bg-muted', className), ...props, children: typeof children === 'string' ? (_jsx(Text, { className: "font-medium text-muted-foreground", style: typographyStyle(visual, 'label'), children: children })) : (children) }));
|
|
49
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Avatar, AvatarImage, AvatarFallback, avatarVariants, type AvatarProps, type AvatarImageProps, type AvatarFallbackProps, } from './avatar';
|
|
1
|
+
export { Avatar, AvatarImage, AvatarFallback, avatarVariants, type AvatarProps, type AvatarImageProps, type AvatarImageStatus, type AvatarFallbackProps, } from './avatar';
|
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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) => {
|