@thewhileloop/whileui 4.2.3 → 4.4.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 (67) hide show
  1. package/README.md +1271 -663
  2. package/dist/agent/catalog.json +1737 -0
  3. package/dist/agent/llms-components.txt +2808 -0
  4. package/dist/agent/llms-full.txt +2998 -0
  5. package/dist/agent/llms.txt +152 -0
  6. package/dist/blocks/chat/chat.d.ts +10 -2
  7. package/dist/blocks/chat/chat.js +1 -1
  8. package/dist/blocks/chat/index.d.ts +1 -1
  9. package/dist/blocks/commerce/feature-gate.js +2 -2
  10. package/dist/blocks/commerce/metric-card.js +1 -1
  11. package/dist/blocks/commerce/subscription-card.d.ts +7 -0
  12. package/dist/blocks/commerce/usage-bar.d.ts +4 -0
  13. package/dist/blocks/layout/app-shell.d.ts +4 -0
  14. package/dist/blocks/layout/empty-state.js +1 -1
  15. package/dist/blocks/layout/error-state.js +1 -1
  16. package/dist/blocks/layout/page-skeleton.d.ts +1 -0
  17. package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
  18. package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
  19. package/dist/blocks/layout/smart-input.js +2 -2
  20. package/dist/blocks/lists/list-item.js +1 -1
  21. package/dist/blocks/lists/notification-item.js +1 -1
  22. package/dist/blocks/media/smart-image.js +4 -2
  23. package/dist/blocks/navigation/bottom-nav.js +1 -1
  24. package/dist/blocks/navigation/drawer-menu.js +1 -1
  25. package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
  26. package/dist/blocks/navigation/tab-bar.js +1 -1
  27. package/dist/components/alert/alert.d.ts +7 -0
  28. package/dist/components/alert/alert.js +23 -4
  29. package/dist/components/button/button.d.ts +3 -0
  30. package/dist/components/button/button.js +2 -1
  31. package/dist/components/fab/fab.d.ts +12 -0
  32. package/dist/components/fab/fab.js +14 -2
  33. package/dist/components/fab/index.d.ts +1 -1
  34. package/dist/components/fab/index.js +1 -1
  35. package/dist/components/form-field/form-field.d.ts +18 -0
  36. package/dist/components/form-field/form-field.js +25 -4
  37. package/dist/components/input/input.d.ts +5 -0
  38. package/dist/components/input/input.js +4 -4
  39. package/dist/components/numeric-input/numeric-input.d.ts +3 -0
  40. package/dist/components/numeric-input/numeric-input.js +4 -4
  41. package/dist/components/otp-input/otp-input.d.ts +8 -0
  42. package/dist/components/popover/popover.js +9 -1
  43. package/dist/components/pressable/pressable.d.ts +1 -0
  44. package/dist/components/segmented-control/segmented-control.d.ts +39 -1
  45. package/dist/components/segmented-control/segmented-control.js +29 -5
  46. package/dist/components/select/select.js +7 -1
  47. package/dist/components/surface/surface.d.ts +5 -0
  48. package/dist/components/textarea/textarea.js +3 -3
  49. package/dist/components/toast/toast.d.ts +10 -10
  50. package/dist/components/toast/toast.js +53 -35
  51. package/dist/index.d.ts +2 -1
  52. package/dist/index.js +2 -1
  53. package/dist/lib/dismiss-keyboard-on-open.d.ts +16 -0
  54. package/dist/lib/dismiss-keyboard-on-open.js +22 -0
  55. package/dist/lib/frosted-surface.d.ts +3 -0
  56. package/dist/lib/recipes.d.ts +20 -0
  57. package/dist/lib/recipes.js +19 -2
  58. package/dist/lib/theme-bridge.d.ts +9 -0
  59. package/dist/lib/theme-bridge.js +3 -1
  60. package/dist/lib/theme-contract.js +12 -12
  61. package/dist/lib/theme-presets.js +9 -9
  62. package/dist/lib/theme-seeds.d.ts +40 -0
  63. package/dist/lib/theme-seeds.js +143 -0
  64. package/dist/metro.cjs +5 -4
  65. package/dist/tokens.d.ts +1 -0
  66. package/dist/tokens.js +1 -0
  67. package/package.json +3 -2
@@ -0,0 +1,2808 @@
1
+ # WhileUI 4.4.0
2
+
3
+ > React Native components and blocks for iOS, Android and web, styled with Uniwind and
4
+ > themed through a typed token contract. Package: `@thewhileloop/whileui`.
5
+
6
+ - **Text** (Primitives) — Themed text with variant support
7
+
8
+ ## Text
9
+
10
+ `Text` maps semantic roles to the active visual typography tokens; custom font-size
11
+ classes are unnecessary for normal hierarchy. `heading` also sets
12
+ `accessibilityRole="header"` unless the caller supplies another role.
13
+
14
+
15
+ | Prop | Type | Default | Description |
16
+ | --------- | ---------------------------------------------------------------------------------------------------------- | ----------- | ----------- |
17
+ | className | `string` | — | |
18
+ | size | `'base' \| 'xs' \| 'sm' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | — | |
19
+ | variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'eyebrow' \| 'code'` | `'default'` | |
20
+
21
+ Also accepts the props of React Native's `TextProps`.
22
+
23
+ ---
24
+
25
+ - **Surface** (Primitives) — Token-backed base, raised, and overlay container
26
+
27
+ ## Surface
28
+
29
+ ```tsx
30
+ import { Surface } from '@thewhileloop/whileui';
31
+
32
+ <Surface level="raised" padding="default">
33
+ {/* grouped content */}
34
+ </Surface>;
35
+ ```
36
+
37
+
38
+ | Prop | Type | Default | Description |
39
+ | ------------- | ------------------------------ | ------------------- | ------------------------------------ |
40
+ | level | `SurfaceLevel` | `'base'` | Semantic background and depth tier. |
41
+ | padding | `SurfacePaddingSize` | `'default'` | Token-backed interior spacing. |
42
+ | radius | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'xl'` | Token-backed corner radius. |
43
+ | bordered | `boolean` | `true` | Draw the theme hairline border. |
44
+ | shadow | `ShadowTier` | — | Override the level's depth. |
45
+ | className | `string` | — | |
46
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
47
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
48
+ | blurTintToken | `FrostedTintToken` | `LEVEL_TINT[level]` | Tint token used for frosted mode. |
49
+
50
+ Also accepts the props of `ViewProps`.
51
+
52
+ ---
53
+
54
+ - **View** (Primitives) — Themed view wrapper
55
+
56
+ ### View
57
+
58
+ Themed view wrapper
59
+
60
+ ```tsx
61
+ import { View } from '@thewhileloop/whileui';
62
+ ```
63
+
64
+
65
+ | Prop | Type | Default | Description |
66
+ | --------- | -------- | ------- | ----------- |
67
+ | className | `string` | — | |
68
+
69
+ Also accepts the props of React Native's `ViewProps`.
70
+
71
+ ---
72
+
73
+ - **Pressable** (Primitives) — Themed pressable wrapper
74
+
75
+ ## Pressable
76
+
77
+ ```tsx
78
+ import { Pressable, Text } from '@thewhileloop/whileui';
79
+
80
+ <Pressable
81
+ accessibilityRole="button"
82
+ feedback="strong"
83
+ disabledFeedback="default"
84
+ loading={false}
85
+ onPress={() => {}}
86
+ >
87
+ <Text>Open</Text>
88
+ </Pressable>;
89
+ ```
90
+
91
+ `Pressable` preserves React Native's event, ref, accessibility, and function-style
92
+ `style` contracts. Feedback changes opacity without changing layout. Native press state
93
+ comes directly from React Native; web adds a motion-policy-aware opacity transition.
94
+
95
+
96
+ | Prop | Type | Default | Description |
97
+ | ---------------- | ----------------- | ----------- | --------------------------------------------------------------------------- |
98
+ | className | `string` | — | Additional Tailwind classes. |
99
+ | feedback | `PressedVariant` | `'none'` | Semantic press feedback. `none` preserves a plain React Native Pressable. |
100
+ | disabledFeedback | `DisabledVariant` | `'default'` | Opacity used while the control is unavailable. |
101
+ | loading | `boolean` | `false` | Loading controls are busy and non-interactive, just like disabled controls. |
102
+
103
+ Also accepts the props of React Native's `PressableProps`.
104
+
105
+ ---
106
+
107
+ - **Stack** (Primitives) — Vertical flex layout with gap
108
+
109
+ ### Stack
110
+
111
+ Vertical flex layout with gap
112
+
113
+ ```tsx
114
+ import { Stack } from '@thewhileloop/whileui';
115
+ ```
116
+
117
+
118
+ | Prop | Type | Default | Description |
119
+ | --------- | ------------------------------------------------------------------- | ----------- | ----------- |
120
+ | className | `string` | — | |
121
+ | gap | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | `'md'` | |
122
+ | align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | `'stretch'` | |
123
+ | justify | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | `'start'` | |
124
+
125
+ Also accepts the props of `ViewProps`.
126
+
127
+ ---
128
+
129
+ - **Row** (Primitives) — Horizontal flex layout with gap
130
+
131
+ ## Layout Primitives (Stack, Row)
132
+
133
+ Use `Stack` for vertical layouts, `Row` for horizontal layouts. Both support `gap`, `align`, and `justify` variants.
134
+
135
+ ```tsx
136
+ import { Stack, Row } from '@thewhileloop/whileui';
137
+
138
+ <Stack gap="lg">
139
+ <Text>Title</Text>
140
+ <Row gap="md" justify="between">
141
+ <Button>
142
+ <ButtonText>Cancel</ButtonText>
143
+ </Button>
144
+ <Button>
145
+ <ButtonText>Save</ButtonText>
146
+ </Button>
147
+ </Row>
148
+ </Stack>;
149
+ ```
150
+
151
+
152
+ | Prop | Type | Default | Description |
153
+ | --------- | ------------------------------------------------------------------- | ---------- | ----------- |
154
+ | className | `string` | — | |
155
+ | gap | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | `'md'` | |
156
+ | align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | `'center'` | |
157
+ | justify | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | `'start'` | |
158
+
159
+ Also accepts the props of `ViewProps`.
160
+
161
+ ### Container
162
+
163
+ Caps content width and centres it. Phone-first layouts stretch to the viewport, which
164
+ is right at 390px and wrong at 1600px — line lengths become unreadable and related
165
+ controls drift apart.
166
+
167
+ ```tsx
168
+ <Container size="lg">{children}</Container>
169
+ ```
170
+
171
+
172
+ | Prop | Type | Default | Description |
173
+ | --------- | ---------------------------------------- | ------- | ----------- |
174
+ | className | `string` | — | |
175
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | |
176
+ | fill | `boolean` | `false` | |
177
+
178
+ Also accepts the props of `ViewProps`.
179
+
180
+ No platform branch, and none is needed: every size is wider than any phone, so on iOS
181
+ and Android the cap never engages and it renders as a plain full-width `View`. The same
182
+ component is a no-op on native and the whole fix on desktop.
183
+
184
+ For a scroll view, put the column on the content container so the scrollbar stays at
185
+ the window edge:
186
+
187
+ ```tsx
188
+ <ScrollView contentContainerClassName="w-full max-w-5xl self-center" />
189
+ ```
190
+
191
+ ---
192
+
193
+ - **MotionProvider** (Root Providers) — System reduced motion, explicit preview modes, and deterministic testing
194
+
195
+ ### MotionProvider
196
+
197
+ System reduced motion, explicit preview modes, and deterministic testing
198
+
199
+ ```tsx
200
+ import { MotionProvider } from '@thewhileloop/whileui';
201
+ ```
202
+
203
+
204
+ | Prop | Type | Default | Description |
205
+ | ------------------- | ----------------- | ---------- | ----------- |
206
+ | children (required) | `React.ReactNode` | — | |
207
+ | mode | `MotionMode` | `'system'` | |
208
+
209
+ ---
210
+
211
+ - **Button** (Form Controls) — 4 sizes, ButtonText & ButtonIcon sub-components
212
+
213
+ ## Button
214
+
215
+ ```tsx
216
+ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
217
+
218
+ <Button variant="default" size="default" disabled={false} loading={false} onPress={() => {}}>
219
+ <ButtonIcon>
220
+ <Icon />
221
+ </ButtonIcon>
222
+ <ButtonText>Click me</ButtonText>
223
+ {/* Placement is child order — a trailing ButtonIcon renders after the label. */}
224
+ <ButtonIcon>
225
+ <Icon />
226
+ </ButtonIcon>
227
+ </Button>;
228
+ ```
229
+
230
+
231
+ | Prop | Type | Default | Description |
232
+ | ---------------- | ----------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- |
233
+ | children | `React.ReactNode` | — | |
234
+ | className | `string` | — | Additional Tailwind classes. |
235
+ | disabled | `boolean` | `false` | Disable the button. |
236
+ | loading | `boolean` | `false` | Shows a centered spinner, exposes busy state, and prevents interaction. |
237
+ | disabledFeedback | `DisabledVariant` | — | Opacity used while the control is unavailable. |
238
+ | size | `'sm' \| 'lg' \| 'default' \| 'icon'` | `'default'` | |
239
+ | variant | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'` | |
240
+
241
+ Also accepts the props of `PressableProps`.
242
+
243
+ ---
244
+
245
+ - **Input** (Form Controls) — TextInput wrapper with themed styling
246
+
247
+ ## Input
248
+
249
+ ```tsx
250
+ import { Input, useIconColors } from '@thewhileloop/whileui';
251
+ import { Feather } from '@expo/vector-icons';
252
+
253
+ const icons = useIconColors();
254
+
255
+ <Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
256
+
257
+ <Input
258
+ placeholder="Search"
259
+ prefix={<Feather name="search" size={16} color={icons.muted} aria-hidden />}
260
+ />;
261
+ ```
262
+
263
+ On iOS and Android, Input, Textarea, NumericInput and SmartInput draw the caret and
264
+ selection in the theme's primary colour; pass `selectionColor` (and `cursorColor` on
265
+ Android) to override it. On web, filled surfaces keep their themed background,
266
+ foreground and caret when the browser autofills them.
267
+
268
+
269
+ | Prop | Type | Default | Description |
270
+ | -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
271
+ | aria-invalid | `boolean` | — | |
272
+ | className | `string` | — | |
273
+ | prefix | `React.ReactNode` | — | Shown before the text. A decorative icon here needs `aria-hidden`; screen readers otherwise stop on it as a separate element and read out its glyph. |
274
+ | suffix | `React.ReactNode` | — | Shown after the text: a unit, or a control such as a clear button. |
275
+ | inputClassName | `string` | — | |
276
+ | variant | `'default' \| 'error' \| 'plain'` | `'default'` | |
277
+
278
+ Also accepts the props of `TextInputProps`.
279
+
280
+ ---
281
+
282
+ - **NumericInput** (Form Controls) — Numeric input with prefix/suffix slots, optional steppers, and compact size
283
+
284
+ ## NumericInput
285
+
286
+ ```tsx
287
+ import { NumericInput } from '@thewhileloop/whileui';
288
+
289
+ <NumericInput
290
+ value={amount}
291
+ onValueChange={setAmount}
292
+ prefix={<Text className="text-muted-foreground">$</Text>}
293
+ suffix={<Text className="text-muted-foreground">USD</Text>}
294
+ min={0}
295
+ step={0.5}
296
+ showSteppers
297
+ size="compact"
298
+ />;
299
+ ```
300
+
301
+
302
+ | Prop | Type | Default | Description |
303
+ | -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
304
+ | aria-invalid | `boolean` | — | |
305
+ | className | `string` | — | |
306
+ | inputClassName | `string` | — | |
307
+ | prefix | `React.ReactNode` | — | |
308
+ | suffix | `React.ReactNode` | — | |
309
+ | value | `number \| null` | — | Controlled numeric value. |
310
+ | defaultValue | `number` | — | |
311
+ | onValueChange | `(value: number \| null) => void` | — | Numeric value change callback. |
312
+ | min | `number` | — | |
313
+ | max | `number` | — | |
314
+ | step | `number` | `1` | |
315
+ | showSteppers | `boolean` | `false` | Show decrement/increment controls. |
316
+ | unstyled | `boolean` | `false` | Drop the field shell (border, background, radius, padding) for embedding inside another field shell like LabeledField. |
317
+ | size | `'default' \| 'compact'` | `'default'` | |
318
+ | variant | `'default' \| 'error'` | `'default'` | |
319
+
320
+ Also accepts the props of `TextInputProps`.
321
+
322
+ ---
323
+
324
+ - **OTPInput** (Form Controls) — Verification code input with auto-focus, paste, secure mask, loading skeleton, error shake
325
+
326
+ ## OTPInput
327
+
328
+ ```tsx
329
+ import { OTPInput } from '@thewhileloop/whileui';
330
+
331
+ <OTPInput
332
+ value={otp}
333
+ onValueChange={setOtp}
334
+ onComplete={(code) => verify(code)}
335
+ length={6}
336
+ variant="default"
337
+ />;
338
+ ```
339
+
340
+
341
+ | Prop | Type | Default | Description |
342
+ | ------------- | ------------------------- | ----------- | ---------------------------------- |
343
+ | length | `number` | `6` | Number of digit cells. |
344
+ | value | `string` | — | Controlled value. |
345
+ | onValueChange | `(value: string) => void` | — | Called on each digit change. |
346
+ | onComplete | `(code: string) => void` | — | Called when all digits are filled. |
347
+ | variant | `'default' \| 'error'` | `'default'` | Error variant triggers shake. |
348
+ | size | `'default' \| 'compact'` | `'default'` | Cell size. |
349
+ | secure | `boolean` | `false` | Mask digits with dots. |
350
+ | disabled | `boolean` | `false` | |
351
+ | autoFocus | `boolean` | `false` | Focus first cell on mount. |
352
+ | className | `string` | — | |
353
+
354
+ Also accepts the props of `ViewProps`.
355
+
356
+ ---
357
+
358
+ - **FormField** (Form Controls) — Compound API: FormField, FormLabel, FormControl, FormHint, FormMessage
359
+
360
+ ## FormField
361
+
362
+ ```tsx
363
+ import {
364
+ FormField,
365
+ FormLabel,
366
+ FormControl,
367
+ FormHint,
368
+ FormMessage,
369
+ Input,
370
+ } from '@thewhileloop/whileui';
371
+
372
+ <FormField required invalid={Boolean(error)}>
373
+ <FormLabel>Email</FormLabel>
374
+ <FormControl>
375
+ <Input placeholder="you@example.com" />
376
+ </FormControl>
377
+ {error ? <FormMessage>{error}</FormMessage> : <FormHint>We'll never share your email.</FormHint>}
378
+ </FormField>;
379
+ ```
380
+
381
+ `invalid` and `disabled` are inherited by `Input`, `Textarea`, `NumericInput`, and
382
+ `Select`. An explicit control prop wins. `useFormFieldState()` exposes the same state for
383
+ custom controls; labels, hints, messages, density, and required indicators remain part
384
+ of the compound field rather than each input.
385
+
386
+ `FormLabel` names the `Input`, `Textarea` or `NumericInput` beside it for assistive
387
+ technology (`aria-labelledby` on web and Android; `useFormFieldState().labelId` gives a
388
+ custom control the same id). A label passed to the control itself wins.
389
+
390
+ ---
391
+
392
+ - **LabeledField** (Form Controls) — Field wrapper with label/helper/error plus left/right slots
393
+
394
+ ## LabeledField
395
+
396
+ ```tsx
397
+ import { LabeledField, LabeledFieldControl, Input } from '@thewhileloop/whileui';
398
+
399
+ <LabeledField
400
+ label="Username"
401
+ hint="3-20 characters"
402
+ leftSlot={<Icon />}
403
+ rightSlot={
404
+ <Button size="sm">
405
+ <ButtonText>Check</ButtonText>
406
+ </Button>
407
+ }
408
+ >
409
+ <LabeledFieldControl>
410
+ <Input className="border-0 bg-transparent px-0" />
411
+ </LabeledFieldControl>
412
+ </LabeledField>;
413
+ ```
414
+
415
+ ---
416
+
417
+ - **Textarea** (Form Controls) — Multi-line text input
418
+
419
+ ### Textarea
420
+
421
+ Multi-line text input
422
+
423
+ ```tsx
424
+ import { Textarea } from '@thewhileloop/whileui';
425
+ ```
426
+
427
+
428
+ | Prop | Type | Default | Description |
429
+ | ------------ | --------------------------- | ----------- | ----------- |
430
+ | aria-invalid | `boolean` | — | |
431
+ | className | `string` | — | |
432
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
433
+
434
+ Also accepts the props of `TextInputProps`.
435
+
436
+ ---
437
+
438
+ - **Checkbox** (Form Controls) — Controlled/uncontrolled, accessibility roles
439
+
440
+ ## Checkbox
441
+
442
+ ```tsx
443
+ import { Checkbox } from '@thewhileloop/whileui';
444
+
445
+ <Checkbox checked={checked} onCheckedChange={setChecked} />;
446
+ ```
447
+
448
+
449
+ | Prop | Type | Default | Description |
450
+ | --------------- | ---------------------------- | ------- | ----------- |
451
+ | checked | `boolean` | — | |
452
+ | defaultChecked | `boolean` | `false` | |
453
+ | onCheckedChange | `(checked: boolean) => void` | — | |
454
+ | disabled | `boolean` | `false` | |
455
+ | className | `string` | — | |
456
+
457
+ Also accepts the props of `ViewProps`.
458
+
459
+ ---
460
+
461
+ - **Switch** (Form Controls) — Controlled/uncontrolled, accessibility roles
462
+
463
+ ## Switch
464
+
465
+ ```tsx
466
+ import { Switch } from '@thewhileloop/whileui';
467
+
468
+ <Switch checked={checked} onCheckedChange={setChecked} />;
469
+ ```
470
+
471
+
472
+ | Prop | Type | Default | Description |
473
+ | --------------- | ---------------------------- | ------- | ----------- |
474
+ | checked | `boolean` | — | |
475
+ | defaultChecked | `boolean` | `false` | |
476
+ | onCheckedChange | `(checked: boolean) => void` | — | |
477
+ | disabled | `boolean` | `false` | |
478
+ | className | `string` | — | |
479
+
480
+ Also accepts the props of `ViewProps`.
481
+
482
+ ---
483
+
484
+ - **RadioGroup** (Form Controls) — RadioGroup + RadioGroupItem
485
+
486
+ ## RadioGroup
487
+
488
+ ```tsx
489
+ import { RadioGroup, RadioGroupItem } from '@thewhileloop/whileui';
490
+
491
+ <RadioGroup value={value} onValueChange={setValue}>
492
+ <RadioGroupItem value="option1" />
493
+ <RadioGroupItem value="option2" />
494
+ </RadioGroup>;
495
+ ```
496
+
497
+ ---
498
+
499
+ - **Select** (Form Controls) — Uses `SelectOption` type `{value, label}`. Includes SelectGroup, SelectLabel, SelectSeparator
500
+
501
+ ## Select
502
+
503
+ ```tsx
504
+ import {
505
+ Select,
506
+ SelectTrigger,
507
+ SelectValue,
508
+ SelectContent,
509
+ SelectItem,
510
+ } from '@thewhileloop/whileui';
511
+
512
+ <Select value={value} onValueChange={setValue}>
513
+ <SelectTrigger>
514
+ <SelectValue placeholder="Select..." />
515
+ </SelectTrigger>
516
+ <SelectContent frosted blurIntensity={24} blurTintToken="surfaceTranslucent">
517
+ <SelectItem label="Option 1" value="1" />
518
+ <SelectItem label="Option 2" value="2" />
519
+ </SelectContent>
520
+ </Select>;
521
+ ```
522
+
523
+ Focused and pressed options use the subtle interaction-state tokens; the primary token
524
+ marks the selected indicator without replacing the option's foreground colour. Long
525
+ selected values stay on one line and truncate without displacing the trigger chevron.
526
+ For popover presentation on web, the options panel matches the trigger width, caps
527
+ itself to the collision-available viewport, and wraps long option labels inside it.
528
+
529
+ Opening a Select, or a Popover, closes the on-screen keyboard first, so options opened
530
+ from a form are never hidden behind it. Both draw in a portal, where the platform would
531
+ otherwise leave the keyboard up; the Modal-hosted overlays close it on their own.
532
+
533
+ `SelectContent` supports optional frosted props:
534
+
535
+
536
+ | Prop | Type | Default | Description |
537
+ | ------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------ |
538
+ | className | `string` | — | |
539
+ | portalHost | `string` | — | |
540
+ | insets | `{ top?: number; bottom?: number; left?: number; right?: number }` | — | |
541
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
542
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
543
+ | blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |
544
+
545
+ Also accepts the props of the underlying primitive.
546
+
547
+ `SelectContent` keeps its listbox inside the safe area by default; pass `insets` only to
548
+ change that margin. A list taller than the screen scrolls inside the listbox on every
549
+ platform. Inside a Dialog, Sheet or other WhileUI overlay it renders in that
550
+ overlay's layer automatically. Option values must be non-empty strings: the web
551
+ primitive rejects `""`, so model "none" as an explicit value such as `'none'`.
552
+
553
+ ---
554
+
555
+ - **Label** (Form Controls) — Form field label
556
+
557
+ ### Label
558
+
559
+ Form field label
560
+
561
+ ```tsx
562
+ import { Label } from '@thewhileloop/whileui';
563
+ ```
564
+
565
+
566
+ | Prop | Type | Default | Description |
567
+ | --------- | -------- | ------- | ----------- |
568
+ | className | `string` | — | |
569
+
570
+ Also accepts the props of `TextProps`.
571
+
572
+ ---
573
+
574
+ - **SegmentedControl** (Form Controls) — SegmentedControl, SegmentedControlItem, SegmentedControlItemText with wrapping layout support
575
+
576
+ ## SegmentedControl
577
+
578
+ ```tsx
579
+ import {
580
+ SegmentedControl,
581
+ SegmentedControlItem,
582
+ SegmentedControlItemText,
583
+ } from '@thewhileloop/whileui';
584
+
585
+ <SegmentedControl value={unit} onValueChange={setUnit} variant="pill" wrap>
586
+ <SegmentedControlItem value="metric">
587
+ <SegmentedControlItemText>Metric</SegmentedControlItemText>
588
+ </SegmentedControlItem>
589
+ <SegmentedControlItem value="imperial">
590
+ <SegmentedControlItemText>Imperial</SegmentedControlItemText>
591
+ </SegmentedControlItem>
592
+ </SegmentedControl>;
593
+ ```
594
+
595
+ When the labels do not fit a phone, `wrap="scroll"` keeps them on one row at full width
596
+ and scrolls sideways, instead of wrapping or truncating them.
597
+
598
+
599
+ | Prop | Type | Default | Description |
600
+ | ------------- | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
601
+ | value | `string` | — | |
602
+ | defaultValue | `string` | `''` | |
603
+ | onValueChange | `(value: string) => void` | — | |
604
+ | variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items. |
605
+ | wrap | `boolean \| 'auto' \| 'scroll'` | `'auto'` | `true` wraps segments onto more rows; `false` keeps one row and shares its width. `'scroll'` keeps one row with each segment as wide as its label and scrolls sideways when they do not fit — for labels a phone cannot show in full. `'auto'` wraps above three segments. |
606
+ | size | `'default' \| 'compact'` | `'default'` | |
607
+ | disabled | `boolean` | `false` | |
608
+ | className | `string` | — | |
609
+
610
+ Also accepts the props of `ViewProps`.
611
+
612
+ ---
613
+
614
+ - **Toggle** (Form Controls) — ToggleText sub-component
615
+
616
+ ### Toggle
617
+
618
+ ToggleText sub-component
619
+
620
+ ```tsx
621
+ import { Toggle } from '@thewhileloop/whileui';
622
+ ```
623
+
624
+
625
+ | Prop | Type | Default | Description |
626
+ | --------------- | ------------------------------------------------ | ----------- | ----------- |
627
+ | pressed | `boolean` | `false` | |
628
+ | defaultPressed | `boolean` | `false` | |
629
+ | onPressedChange | `(pressed: boolean) => void` | — | |
630
+ | variant | `VariantProps<typeof toggleVariants>['variant']` | `'default'` | |
631
+ | size | `VariantProps<typeof toggleVariants>['size']` | `'default'` | |
632
+ | className | `string` | — | |
633
+ | children | `React.ReactNode` | — | |
634
+
635
+ Also accepts the props of `PressableProps`.
636
+
637
+ ---
638
+
639
+ - **ToggleGroup** (Form Controls) — Group of toggle items
640
+
641
+ ### ToggleGroup
642
+
643
+ Group of toggle items
644
+
645
+ ```tsx
646
+ import { ToggleGroup } from '@thewhileloop/whileui';
647
+ ```
648
+
649
+
650
+ | Prop | Type | Default | Description |
651
+ | ------------- | ------------------------------------- | ----------- | ----------- |
652
+ | type | `ToggleGroupType` | `'single'` | |
653
+ | defaultValue | `string \| string[]` | — | |
654
+ | value | `string \| string[]` | — | |
655
+ | onValueChange | `(value: string \| string[]) => void` | — | |
656
+ | variant | `'default' \| 'outline'` | `'default'` | |
657
+ | size | `'default' \| 'sm' \| 'lg'` | `'default'` | |
658
+ | className | `string` | — | |
659
+
660
+ Also accepts the props of `ViewProps`.
661
+
662
+ ---
663
+
664
+ - **Card** (Display) — Compound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
665
+
666
+ ## Card
667
+
668
+ ```tsx
669
+ import {
670
+ Card,
671
+ CardHeader,
672
+ CardTitle,
673
+ CardDescription,
674
+ CardContent,
675
+ CardFooter,
676
+ } from '@thewhileloop/whileui';
677
+
678
+ <Card padding="default">
679
+ <CardHeader>
680
+ <CardTitle>Title</CardTitle>
681
+ <CardDescription>Description</CardDescription>
682
+ </CardHeader>
683
+ <CardContent>{/* Content */}</CardContent>
684
+ <CardFooter>{/* Footer */}</CardFooter>
685
+ </Card>;
686
+
687
+ <Card unstyled padding="none" className="rounded-xl border border-border bg-card">
688
+ {/* advanced custom layouts */}
689
+ </Card>;
690
+ ```
691
+
692
+
693
+ | Prop | Type | Default | Description |
694
+ | ------------- | ------------------------------------- | ----------- | ------------------------------------------------------- |
695
+ | shadow | `ShadowTier` | `'sm'` | Elevation tier; 'sm' by default, 'none' for flat cards. |
696
+ | className | `string` | — | |
697
+ | padding | `'none' \| 'sm' \| 'lg' \| 'default'` | `'default'` | |
698
+ | unstyled | `boolean` | `false` | |
699
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
700
+ | blurIntensity | `number` | — | Override blur amount directly. |
701
+ | blurTintToken | `FrostedTintToken` | — | Tint token used for frosted mode. |
702
+
703
+ Also accepts the props of `ViewProps`.
704
+
705
+ ---
706
+
707
+ - **Surface** (Display) — Shared radius, padding, border, shadow, and optional frosted treatment
708
+
709
+ Documented under Surface.
710
+
711
+ ---
712
+
713
+ - **Badge** (Display) — BadgeText sub-component
714
+
715
+ ## Badge
716
+
717
+ ```tsx
718
+ import { Badge, BadgeText } from '@thewhileloop/whileui';
719
+
720
+ <Badge variant="default">
721
+ <BadgeText>New</BadgeText>
722
+ </Badge>;
723
+ ```
724
+
725
+
726
+ | Prop | Type | Default | Description |
727
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ----------- |
728
+ | className | `string` | — | |
729
+ | variant | `'default' \| 'soft' \| 'destructive' \| 'outline' \| 'secondary' \| 'success' \| 'warning' \| 'info' \| 'soft-secondary' \| 'soft-destructive' \| 'soft-success' \| 'soft-warning' \| 'soft-info'` | `'default'` | |
730
+
731
+ Also accepts the props of `ViewProps`.
732
+
733
+ ---
734
+
735
+ - **FAB** (Display) — Floating action button. `label` required; pass your own icon as children
736
+
737
+ ### FAB
738
+
739
+ One primary action, raised above the content. It owns no icon dependency — pass the
740
+ glyph as children and colour it from `useIconColors()`, because icon libraries need a
741
+ concrete colour rather than a class.
742
+
743
+ ```tsx
744
+ import { FAB, useIconColors } from '@thewhileloop/whileui';
745
+ import { Feather } from '@expo/vector-icons';
746
+
747
+ const icons = useIconColors();
748
+
749
+ <FAB label="New group" onPress={create}>
750
+ <Feather name="plus" size={24} color={icons.primaryForeground} />
751
+ </FAB>;
752
+ ```
753
+
754
+ | Prop | Type | Default | Notes |
755
+ | ----------- | ------------------------------------------------ | ---------- | --------------------------------------- |
756
+ | `label` | `string` | — | Required. The accessible name |
757
+ | `variant` | `default \| secondary \| destructive \| surface` | `default` | |
758
+ | `size` | `sm \| default \| lg` | `default` | 48 / 56 / 64 — all clear touch minimums |
759
+ | `placement` | `floating \| floating-left \| inline` | `floating` | `inline` lets the caller position it |
760
+
761
+ `FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never
762
+ needs `hitSlop`.
763
+
764
+ A floating FAB covers whatever scrolls under it, so a list behind one ends with room for
765
+ it: `contentContainerStyle={{ paddingBottom: fabClearance() }}`, passing the FAB's `size`
766
+ when it is not the default.
767
+
768
+ ---
769
+
770
+ - **Alert** (Display) — AlertTitle & AlertDescription
771
+
772
+ ## Alert
773
+
774
+ ```tsx
775
+ import { Alert, AlertTitle, AlertDescription } from '@thewhileloop/whileui';
776
+
777
+ <Alert variant="default">
778
+ <AlertTitle>Heads up!</AlertTitle>
779
+ <AlertDescription>Something happened.</AlertDescription>
780
+ </Alert>;
781
+
782
+ // Shown after a failed submit: also spoken on iOS when it appears.
783
+ <Alert variant="destructive" announce>
784
+ <AlertTitle>Could not save</AlertTitle>
785
+ <AlertDescription>Check your connection and try again.</AlertDescription>
786
+ </Alert>;
787
+ ```
788
+
789
+
790
+ | Prop | Type | Default | Description |
791
+ | --------- | ---------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
792
+ | announce | `boolean` | `false` | Speak the alert when it appears, for alerts shown in response to an action such as a failed submit. Android and the web already read it through the live region; iOS has none, so this announces its text once. Leave it off for alerts that are part of a screen's content, or every visit would read them aloud. |
793
+ | className | `string` | — | |
794
+ | variant | `'default' \| 'destructive' \| 'success' \| 'warning' \| 'info'` | `'default'` | |
795
+
796
+ Also accepts the props of `ViewProps`.
797
+
798
+ ---
799
+
800
+ - **Avatar** (Display) — AvatarImage + AvatarFallback
801
+
802
+ ## Avatar
803
+
804
+ ```tsx
805
+ import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
806
+
807
+ <Avatar size="default">
808
+ <AvatarImage source={{ uri: 'https://...' }} />
809
+ <AvatarFallback>JD</AvatarFallback>
810
+ </Avatar>;
811
+ ```
812
+
813
+
814
+ | Prop | Type | Default | Description |
815
+ | --------- | ----------------------------------- | ----------- | ----------- |
816
+ | className | `string` | — | |
817
+ | size | `'sm' \| 'lg' \| 'xl' \| 'default'` | `'default'` | |
818
+
819
+ Also accepts the props of `ViewProps`.
820
+
821
+ `AvatarImage` keeps `AvatarFallback` visible while loading or after an error. Its
822
+ `onLoadingStatusChange` callback reports `'loading'`, `'loaded'`, and `'error'`, including
823
+ when a changed source begins loading, while the standard image event callbacks still run.
824
+
825
+ ---
826
+
827
+ - **DataRow** (Display) — DataRow, DataRowLeft/Center/Right, DataRowLabel/Description/Value
828
+
829
+ ## DataRow
830
+
831
+ ```tsx
832
+ import {
833
+ DataRow,
834
+ DataRowLeft,
835
+ DataRowCenter,
836
+ DataRowRight,
837
+ DataRowLabel,
838
+ DataRowDescription,
839
+ DataRowValue,
840
+ Avatar,
841
+ AvatarFallback,
842
+ } from '@thewhileloop/whileui';
843
+
844
+ <DataRow>
845
+ <DataRowLeft>
846
+ <Avatar size="sm">
847
+ <AvatarFallback>JD</AvatarFallback>
848
+ </Avatar>
849
+ </DataRowLeft>
850
+ <DataRowCenter>
851
+ <DataRowLabel>Jane Doe</DataRowLabel>
852
+ <DataRowDescription>Product Designer</DataRowDescription>
853
+ </DataRowCenter>
854
+ <DataRowRight>
855
+ <DataRowValue>Owner</DataRowValue>
856
+ </DataRowRight>
857
+ </DataRow>;
858
+ ```
859
+
860
+ ---
861
+
862
+ - **Separator** (Display) — Themed divider
863
+
864
+ ### Separator
865
+
866
+ Themed divider
867
+
868
+ ```tsx
869
+ import { Separator } from '@thewhileloop/whileui';
870
+ ```
871
+
872
+
873
+ | Prop | Type | Default | Description |
874
+ | ----------- | ---------------------------- | -------------- | ----------- |
875
+ | orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | |
876
+ | className | `string` | — | |
877
+
878
+ Also accepts the props of `ViewProps`.
879
+
880
+ ---
881
+
882
+ - **Progress** (Display) — Value-based progress bar with accessibility
883
+
884
+ ## Progress
885
+
886
+ ```tsx
887
+ import { Progress } from '@thewhileloop/whileui';
888
+
889
+ <Progress value={50} size="default" />;
890
+ ```
891
+
892
+
893
+ | Prop | Type | Default | Description |
894
+ | ------------------ | --------------------------- | ----------- | ----------- |
895
+ | value | `number` | `0` | |
896
+ | indicatorClassName | `string` | — | |
897
+ | className | `string` | — | |
898
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
899
+
900
+ Also accepts the props of `ViewProps`.
901
+
902
+ ---
903
+
904
+ - **Spinner** (Display) — ActivityIndicator wrapper
905
+
906
+ ### Spinner
907
+
908
+ ActivityIndicator wrapper
909
+
910
+ ```tsx
911
+ import { Spinner } from '@thewhileloop/whileui';
912
+ ```
913
+
914
+
915
+ | Prop | Type | Default | Description |
916
+ | --------- | --------------------------- | ----------- | ----------- |
917
+ | className | `string` | — | |
918
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
919
+
920
+ Also accepts the props of `ActivityIndicatorProps`.
921
+
922
+ ---
923
+
924
+ - **Skeleton** (Display) — Loading placeholder (pulse = opacity fade, shimmer = sweep)
925
+
926
+ ### Skeleton
927
+
928
+ Loading placeholder (pulse = opacity fade, shimmer = sweep)
929
+
930
+ ```tsx
931
+ import { Skeleton } from '@thewhileloop/whileui';
932
+ ```
933
+
934
+
935
+ | Prop | Type | Default | Description |
936
+ | --------- | ---------------------- | --------- | ----------- |
937
+ | className | `string` | — | |
938
+ | variant | `'pulse' \| 'shimmer'` | `'pulse'` | |
939
+
940
+ Also accepts the props of `ViewProps`.
941
+
942
+ ---
943
+
944
+ - **AspectRatio** (Display) — Maintain aspect ratio container
945
+
946
+ ### AspectRatio
947
+
948
+ Maintain aspect ratio container
949
+
950
+ ```tsx
951
+ import { AspectRatio } from '@thewhileloop/whileui';
952
+ ```
953
+
954
+
955
+ | Prop | Type | Default | Description |
956
+ | --------- | -------- | ------- | ----------- |
957
+ | ratio | `number` | `1` | |
958
+ | className | `string` | — | |
959
+
960
+ Also accepts the props of `ViewProps`.
961
+
962
+ ---
963
+
964
+ - **Tabs** (Layout) — TabsList, TabsTrigger, TabsContent
965
+
966
+ ## Tabs
967
+
968
+ ```tsx
969
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@thewhileloop/whileui';
970
+
971
+ <Tabs defaultValue="tab1">
972
+ <TabsList>
973
+ <TabsTrigger value="tab1">
974
+ <Text>Tab 1</Text>
975
+ </TabsTrigger>
976
+ <TabsTrigger value="tab2">
977
+ <Text>Tab 2</Text>
978
+ </TabsTrigger>
979
+ </TabsList>
980
+ <TabsContent value="tab1">{/* Content 1 */}</TabsContent>
981
+ <TabsContent value="tab2">{/* Content 2 */}</TabsContent>
982
+ </Tabs>;
983
+ ```
984
+
985
+ ---
986
+
987
+ - **Accordion** (Layout) — AccordionItem, AccordionTrigger, Content
988
+
989
+ ## Accordion
990
+
991
+ ```tsx
992
+ import {
993
+ Accordion,
994
+ AccordionItem,
995
+ AccordionTrigger,
996
+ AccordionContent,
997
+ } from '@thewhileloop/whileui';
998
+
999
+ <Accordion type="single" collapsible>
1000
+ <AccordionItem value="item1">
1001
+ <AccordionTrigger>
1002
+ <Text>Section 1</Text>
1003
+ </AccordionTrigger>
1004
+ <AccordionContent>
1005
+ <Text>Content 1</Text>
1006
+ </AccordionContent>
1007
+ </AccordionItem>
1008
+ </Accordion>;
1009
+ ```
1010
+
1011
+
1012
+ | Prop | Type | Default | Description |
1013
+ | ------------ | -------------------- | ---------- | ----------- |
1014
+ | type | `AccordionType` | `'single'` | |
1015
+ | defaultValue | `string \| string[]` | — | |
1016
+ | collapsible | `boolean` | `true` | |
1017
+ | className | `string` | — | |
1018
+
1019
+ Also accepts the props of `ViewProps`.
1020
+
1021
+ ---
1022
+
1023
+ - **Collapsible** (Layout) — CollapsibleTrigger, CollapsibleContent
1024
+
1025
+ ### Collapsible
1026
+
1027
+ CollapsibleTrigger, CollapsibleContent
1028
+
1029
+ ```tsx
1030
+ import { Collapsible } from '@thewhileloop/whileui';
1031
+ ```
1032
+
1033
+
1034
+ | Prop | Type | Default | Description |
1035
+ | ------------ | ------------------------- | ------- | ----------- |
1036
+ | defaultOpen | `boolean` | `false` | |
1037
+ | open | `boolean` | — | |
1038
+ | onOpenChange | `(open: boolean) => void` | — | |
1039
+ | disabled | `boolean` | `false` | |
1040
+ | className | `string` | — | |
1041
+
1042
+ Also accepts the props of `ViewProps`.
1043
+
1044
+ ---
1045
+
1046
+ - **Container** (Layout) — Centred content column with a max width
1047
+
1048
+ ### Container
1049
+
1050
+ Caps content width and centres it. Phone-first layouts stretch to the viewport, which
1051
+ is right at 390px and wrong at 1600px — line lengths become unreadable and related
1052
+ controls drift apart.
1053
+
1054
+ ```tsx
1055
+ <Container size="lg">{children}</Container>
1056
+ ```
1057
+
1058
+
1059
+ | Prop | Type | Default | Description |
1060
+ | --------- | ---------------------------------------- | ------- | ----------- |
1061
+ | className | `string` | — | |
1062
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | |
1063
+ | fill | `boolean` | `false` | |
1064
+
1065
+ Also accepts the props of `ViewProps`.
1066
+
1067
+ No platform branch, and none is needed: every size is wider than any phone, so on iOS
1068
+ and Android the cap never engages and it renders as a plain full-width `View`. The same
1069
+ component is a no-op on native and the whole fix on desktop.
1070
+
1071
+ For a scroll view, put the column on the content container so the scrollbar stays at
1072
+ the window edge:
1073
+
1074
+ ```tsx
1075
+ <ScrollView contentContainerClassName="w-full max-w-5xl self-center" />
1076
+ ```
1077
+
1078
+ ---
1079
+
1080
+ - **Dialog** (Overlays & Menus) — Modal dialog with Header, Footer, Title, Description
1081
+
1082
+ ## Dialog
1083
+
1084
+ ```tsx
1085
+ import {
1086
+ Dialog,
1087
+ DialogTrigger,
1088
+ DialogContent,
1089
+ DialogHeader,
1090
+ DialogTitle,
1091
+ DialogDescription,
1092
+ DialogFooter,
1093
+ DialogClose,
1094
+ } from '@thewhileloop/whileui';
1095
+
1096
+ <Dialog>
1097
+ <DialogTrigger asChild>
1098
+ <Button>
1099
+ <ButtonText>Open</ButtonText>
1100
+ </Button>
1101
+ </DialogTrigger>
1102
+ <DialogContent>
1103
+ <DialogHeader>
1104
+ <DialogTitle>Title</DialogTitle>
1105
+ <DialogDescription>Description</DialogDescription>
1106
+ </DialogHeader>
1107
+ {/* Content */}
1108
+ <DialogFooter>
1109
+ <DialogClose asChild>
1110
+ <Button>
1111
+ <ButtonText>Close</ButtonText>
1112
+ </Button>
1113
+ </DialogClose>
1114
+ </DialogFooter>
1115
+ </DialogContent>
1116
+ </Dialog>;
1117
+ ```
1118
+
1119
+ `DialogContent` supports frosted props:
1120
+
1121
+
1122
+ | Prop | Type | Default | Description |
1123
+ | ------------- | ------------------ | ---------------------- | ------------------------------------ |
1124
+ | className | `string` | — | |
1125
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1126
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1127
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1128
+
1129
+ Also accepts the props of `ViewProps`.
1130
+
1131
+ ---
1132
+
1133
+ - **AlertDialog** (Overlays & Menus) — Confirmation dialog with Action/Cancel buttons
1134
+
1135
+ ## AlertDialog
1136
+
1137
+ ```tsx
1138
+ import {
1139
+ AlertDialog,
1140
+ AlertDialogTrigger,
1141
+ AlertDialogContent,
1142
+ AlertDialogHeader,
1143
+ AlertDialogTitle,
1144
+ AlertDialogDescription,
1145
+ AlertDialogFooter,
1146
+ AlertDialogAction,
1147
+ AlertDialogCancel,
1148
+ } from '@thewhileloop/whileui';
1149
+
1150
+ <AlertDialog>
1151
+ <AlertDialogTrigger asChild>
1152
+ <Button variant="destructive">
1153
+ <ButtonText>Delete</ButtonText>
1154
+ </Button>
1155
+ </AlertDialogTrigger>
1156
+ <AlertDialogContent>
1157
+ <AlertDialogHeader>
1158
+ <AlertDialogTitle>Are you sure?</AlertDialogTitle>
1159
+ <AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
1160
+ </AlertDialogHeader>
1161
+ <AlertDialogFooter>
1162
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
1163
+ <AlertDialogAction>Delete</AlertDialogAction>
1164
+ </AlertDialogFooter>
1165
+ </AlertDialogContent>
1166
+ </AlertDialog>;
1167
+ ```
1168
+
1169
+ `AlertDialogContent` supports the same frosted props as `DialogContent`.
1170
+
1171
+
1172
+ | Prop | Type | Default | Description |
1173
+ | ------------- | ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1174
+ | className | `string` | — | |
1175
+ | presentation | `AlertDialogPresentation` | `'modal'` | `modal` — RN `Modal` + themed card (default). `native` — `Alert.alert` on iOS/Android (system chrome); ignored on web. `overlay` — same themed card as `modal`, but absolute fill inside the parent view (nest inside your `Modal`). |
1176
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1177
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1178
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1179
+
1180
+ Also accepts the props of `ViewProps`.
1181
+
1182
+ ### React Native: stacked modals
1183
+
1184
+ Render a confirmation **inside** the overlay it belongs to. An `AlertDialog` or `Dialog`
1185
+ nested in a `Sheet` opens above it, and Android back or Escape closes the confirmation
1186
+ before the sheet. If `AlertDialog` must be a **sibling** of another fullscreen `Modal`, iOS
1187
+ often fails to show a second `Modal` on top; use **`presentation="overlay"`** inside that
1188
+ `Modal`, or **`presentation="native"`** for a system `Alert.alert` when themed UI is not
1189
+ required.
1190
+
1191
+ ---
1192
+
1193
+ - **Popover** (Overlays & Menus) — Position-aware popover (requires PortalHost)
1194
+
1195
+ ## Popover
1196
+
1197
+ ```tsx
1198
+ import { Popover, PopoverTrigger, PopoverContent } from '@thewhileloop/whileui';
1199
+
1200
+ <Popover>
1201
+ <PopoverTrigger asChild>
1202
+ <Button>
1203
+ <ButtonText>Open</ButtonText>
1204
+ </Button>
1205
+ </PopoverTrigger>
1206
+ <PopoverContent>
1207
+ <Text>Popover content</Text>
1208
+ </PopoverContent>
1209
+ </Popover>;
1210
+ ```
1211
+
1212
+ `PopoverContent` supports frosted props:
1213
+
1214
+
1215
+ | Prop | Type | Default | Description |
1216
+ | ------------- | ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------- |
1217
+ | className | `string` | — | |
1218
+ | portalHost | `string` | — | |
1219
+ | insets | `Partial<OverlayInsets>` | — | Collision padding. Defaults to safe area plus a margin so the panel never sits flush against an edge. |
1220
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1221
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
1222
+ | blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |
1223
+
1224
+ Also accepts the props of the underlying primitive.
1225
+
1226
+ ---
1227
+
1228
+ - **Tooltip** (Overlays & Menus) — Position-aware tooltip (requires PortalHost)
1229
+
1230
+ ### Tooltip
1231
+
1232
+ Position-aware tooltip (requires PortalHost)
1233
+
1234
+ ```tsx
1235
+ import { Tooltip } from '@thewhileloop/whileui';
1236
+ ```
1237
+
1238
+
1239
+ | Prop | Type | Default | Description |
1240
+ | ----------------------- | ------------------------- | ------- | ----------- |
1241
+ | onOpenChange | `(open: boolean) => void` | — | |
1242
+ | delayDuration | `number` | — | |
1243
+ | skipDelayDuration | `number` | — | |
1244
+ | disableHoverableContent | `boolean` | — | |
1245
+ | children (required) | `React.ReactNode` | — | |
1246
+
1247
+ ---
1248
+
1249
+ - **DropdownMenu** (Overlays & Menus) — DropdownMenuTrigger, Content, Item, Label, Separator
1250
+
1251
+ ## DropdownMenu
1252
+
1253
+ ```tsx
1254
+ import {
1255
+ DropdownMenu,
1256
+ DropdownMenuTrigger,
1257
+ DropdownMenuContent,
1258
+ DropdownMenuItem,
1259
+ DropdownMenuLabel,
1260
+ DropdownMenuSeparator,
1261
+ } from '@thewhileloop/whileui';
1262
+
1263
+ <DropdownMenu>
1264
+ <DropdownMenuTrigger asChild>
1265
+ <Button>
1266
+ <ButtonText>Menu</ButtonText>
1267
+ </Button>
1268
+ </DropdownMenuTrigger>
1269
+ <DropdownMenuContent>
1270
+ <DropdownMenuLabel>Actions</DropdownMenuLabel>
1271
+ <DropdownMenuSeparator />
1272
+ <DropdownMenuItem>
1273
+ <Text>Edit</Text>
1274
+ </DropdownMenuItem>
1275
+ <DropdownMenuItem>
1276
+ <Text>Delete</Text>
1277
+ </DropdownMenuItem>
1278
+ </DropdownMenuContent>
1279
+ </DropdownMenu>;
1280
+ ```
1281
+
1282
+ `DropdownMenuContent`, `ContextMenuContent`, and `MenubarContent` support the same frosted props as `PopoverContent` (default blur preset: medium).
1283
+
1284
+ ---
1285
+
1286
+ - **ContextMenu** (Overlays & Menus) — Long-press context menu
1287
+
1288
+ ## ContextMenu
1289
+
1290
+ `ContextMenuContent` supports the same frosted props as `PopoverContent` (default blur preset: medium).
1291
+
1292
+ ---
1293
+
1294
+ - **HoverCard** (Overlays & Menus) — Position-aware hover card (requires PortalHost)
1295
+
1296
+ ## HoverCard
1297
+
1298
+ `HoverCardContent` supports the same frosted props as `PopoverContent` (default blur preset: subtle).
1299
+
1300
+ ---
1301
+
1302
+ - **Menubar** (Overlays & Menus) — Horizontal menu bar
1303
+
1304
+ ## Menubar
1305
+
1306
+ ```tsx
1307
+ import {
1308
+ Menubar,
1309
+ MenubarMenu,
1310
+ MenubarTrigger,
1311
+ MenubarContent,
1312
+ MenubarLabel,
1313
+ MenubarItem,
1314
+ MenubarSeparator,
1315
+ } from '@thewhileloop/whileui';
1316
+
1317
+ <Menubar>
1318
+ <MenubarMenu>
1319
+ <MenubarTrigger>
1320
+ <Text className="text-foreground text-sm">File</Text>
1321
+ </MenubarTrigger>
1322
+ <MenubarContent>
1323
+ <MenubarLabel>Project</MenubarLabel>
1324
+ <MenubarItem>
1325
+ <Text className="text-foreground">New Project</Text>
1326
+ </MenubarItem>
1327
+ <MenubarSeparator />
1328
+ <MenubarItem>
1329
+ <Text className="text-foreground">Export</Text>
1330
+ </MenubarItem>
1331
+ </MenubarContent>
1332
+ </MenubarMenu>
1333
+ </Menubar>;
1334
+ ```
1335
+
1336
+ `MenubarLabel` is a non-interactive section heading. It takes a string or elements, and
1337
+ announces as a heading to assistive technology — as do `DropdownMenuLabel` and
1338
+ `ContextMenuLabel`.
1339
+
1340
+ `MenubarContent` supports the same frosted props as `PopoverContent` (default blur preset: medium).
1341
+
1342
+ ---
1343
+
1344
+ # Blocks API
1345
+
1346
+ ---
1347
+
1348
+ - **Toast** (Feedback) — ToastProvider, ToastContainer, useToast() hook
1349
+
1350
+ ## Toast
1351
+
1352
+ ```tsx
1353
+ import { ToastProvider, ToastContainer, useToast } from '@thewhileloop/whileui';
1354
+
1355
+ // Wrap app
1356
+ <ToastProvider>
1357
+ <App />
1358
+ <ToastContainer position="top" />
1359
+ </ToastProvider>;
1360
+
1361
+ // Use in component
1362
+ const { toast } = useToast();
1363
+ toast({ title: 'Success', description: 'Saved!', variant: 'success' });
1364
+ ```
1365
+
1366
+ | Toast Options | Type |
1367
+ | ------------- | ----------------------------------------- |
1368
+ | title | `string` |
1369
+ | description | `string` |
1370
+ | variant | `'default' \| 'success' \| 'destructive'` |
1371
+ | duration | `number` (ms) |
1372
+
1373
+ Each toast owns its duration from the moment it appears; adding or dismissing another
1374
+ toast does not restart timers already in progress.
1375
+
1376
+ Every variant sits on the theme's `popover` surface; `success` and `destructive` mark the
1377
+ toast with a status dot and tinted border rather than a solid fill. The stack is capped
1378
+ at `max-w-md` and centred, which never engages on a phone. A toast is announced politely
1379
+ to screen readers without taking focus, and its slide-in follows the motion policy.
1380
+
1381
+ ---
1382
+
1383
+ - **SignInForm** (Auth, copy-paste template) — Email/password sign in with callbacks
1384
+
1385
+ ## SignInForm
1386
+
1387
+ Copy `sign-in-form.tsx` and its sibling `auth-form-footer.tsx` from
1388
+ `apps/showcase/catalog/templates/auth/`, then:
1389
+
1390
+ ```tsx
1391
+ import { SignInForm } from './templates/auth'; // or your path
1392
+
1393
+ <SignInForm
1394
+ onSubmit={({ email, password }) => signIn(email, password)}
1395
+ onForgotPassword={() => navigate('ForgotPassword')}
1396
+ onSignUp={() => navigate('SignUp')}
1397
+ onGooglePress={() => signInWithGoogle()}
1398
+ onApplePress={() => signInWithApple()}
1399
+ />;
1400
+ ```
1401
+
1402
+ | Prop | Type | Description |
1403
+ | ---------------- | ------------------------------------- | ---------------------------- |
1404
+ | onSubmit | `(data: { email, password }) => void` | Called on sign-in submit |
1405
+ | onForgotPassword | `() => void` | Called when "Forgot?" tapped |
1406
+ | onSignUp | `() => void` | Called when "Sign Up" tapped |
1407
+ | onGooglePress | `() => void` | Called when Google tapped |
1408
+ | onApplePress | `() => void` | Called when Apple tapped |
1409
+
1410
+ ---
1411
+
1412
+ - **SignUpForm** (Auth, copy-paste template) — Registration form with callbacks
1413
+
1414
+ ## SignUpForm
1415
+
1416
+ Copy `sign-up-form.tsx` and its sibling `auth-form-footer.tsx` from
1417
+ `apps/showcase/catalog/templates/auth/`, then:
1418
+
1419
+ ```tsx
1420
+ import { SignUpForm } from './templates/auth'; // or your path
1421
+
1422
+ <SignUpForm
1423
+ onSubmit={({ firstName, lastName, email, password }) =>
1424
+ signUp(firstName, lastName, email, password)
1425
+ }
1426
+ onSignIn={() => navigate('SignIn')}
1427
+ onGooglePress={() => signInWithGoogle()}
1428
+ onApplePress={() => signInWithApple()}
1429
+ />;
1430
+ ```
1431
+
1432
+ | Prop | Type | Description |
1433
+ | ------------- | ---------------------------------------------------------- | ---------------------------- |
1434
+ | onSubmit | `(data: { firstName, lastName, email, password }) => void` | Called on registration |
1435
+ | onSignIn | `() => void` | Called when "Sign In" tapped |
1436
+ | onGooglePress | `() => void` | Called when Google tapped |
1437
+ | onApplePress | `() => void` | Called when Apple tapped |
1438
+
1439
+ ---
1440
+
1441
+ - **ForgotPasswordForm** (Auth, copy-paste template) — Password reset request
1442
+
1443
+ No API section yet; the summary above is the full description.
1444
+
1445
+ ---
1446
+
1447
+ - **ResetPasswordForm** (Auth, copy-paste template) — Set new password
1448
+
1449
+ No API section yet; the summary above is the full description.
1450
+
1451
+ ---
1452
+
1453
+ - **VerifyEmailForm** (Auth, copy-paste template) — Email verification code input
1454
+
1455
+ No API section yet; the summary above is the full description.
1456
+
1457
+ ---
1458
+
1459
+ - **SocialConnections** (Auth, copy-paste template) — OAuth provider buttons
1460
+
1461
+ No API section yet; the summary above is the full description.
1462
+
1463
+ ---
1464
+
1465
+ - **UserMenu** (Auth, copy-paste template) — Profile dropdown for auth flows
1466
+
1467
+ No API section yet; the summary above is the full description.
1468
+
1469
+ ---
1470
+
1471
+ - **AppShell** (Navigation) — Layout shell with header/footer/bottomNav slots
1472
+
1473
+ ## AppShell
1474
+
1475
+ Layout shell for full-screen pages. Keep shell chrome mounted and swap only content via `loading` + `skeleton`.
1476
+
1477
+ ```tsx
1478
+ import { AppShell, PageSkeleton } from '@thewhileloop/whileui';
1479
+
1480
+ <AppShell
1481
+ header={<Header title="Settings" />}
1482
+ bottomNav={<BottomNav items={[...]} activeKey="settings" onSelect={setTab} />}
1483
+ loading={loading}
1484
+ skeleton={<PageSkeleton variant="settings" headerPlaceholder />}
1485
+ >
1486
+ <ScrollView className="flex-1 p-4">{/* Content */}</ScrollView>
1487
+ </AppShell>;
1488
+ ```
1489
+
1490
+
1491
+ | Prop | Type | Default | Description |
1492
+ | --------- | ----------------- | ------- | --------------------------------------------------------- |
1493
+ | header | `React.ReactNode` | — | Header slot. |
1494
+ | footer | `React.ReactNode` | — | Footer slot. |
1495
+ | bottomNav | `React.ReactNode` | — | Bottom navigation slot. |
1496
+ | safeArea | `boolean` | `true` | Wrap in SafeAreaView. |
1497
+ | loading | `boolean` | `false` | Keep shell mounted while swapping content with skeleton. |
1498
+ | skeleton | `React.ReactNode` | — | Optional custom loading placeholder for the content area. |
1499
+ | className | `string` | — | |
1500
+
1501
+ Also accepts the props of `ViewProps`.
1502
+
1503
+ ---
1504
+
1505
+ - **Header** (Navigation) — Top app bar with back/actions
1506
+
1507
+ ## Header
1508
+
1509
+ ```tsx
1510
+ import { Header, HeaderBackButton } from '@thewhileloop/whileui';
1511
+
1512
+ <Header
1513
+ title="Settings"
1514
+ subtitle="Manage preferences"
1515
+ leftAction={<HeaderBackButton onPress={() => {}} />}
1516
+ rightActions={[
1517
+ { key: 'search', icon: <Icon />, onPress: () => {}, accessibilityLabel: 'Search' },
1518
+ ]}
1519
+ />;
1520
+ ```
1521
+
1522
+ ---
1523
+
1524
+ - **BottomNav** (Navigation) — Tab-style bottom navigation bar
1525
+
1526
+ ## BottomNav
1527
+
1528
+ ```tsx
1529
+ import { BottomNav } from '@thewhileloop/whileui';
1530
+
1531
+ <BottomNav
1532
+ items={[
1533
+ { key: 'home', label: 'Home', icon: <Icon /> },
1534
+ { key: 'profile', label: 'Profile', icon: <Icon />, badge: 3 },
1535
+ ]}
1536
+ activeKey="home"
1537
+ onSelect={(key) => {}}
1538
+ />;
1539
+ ```
1540
+
1541
+ ---
1542
+
1543
+ - **FloatingBottomNav** (Navigation) — Elevated bottom nav with safe area support
1544
+
1545
+ ### FloatingBottomNav
1546
+
1547
+ Elevated bottom nav with safe area support
1548
+
1549
+ ```tsx
1550
+ import { FloatingBottomNav } from '@thewhileloop/whileui';
1551
+ ```
1552
+
1553
+
1554
+ | Prop | Type | Default | Description |
1555
+ | -------------------- | ------------------------- | ------- | ----------- |
1556
+ | items (required) | `FloatingBottomNavItem[]` | — | |
1557
+ | activeKey (required) | `string` | — | |
1558
+ | onSelect (required) | `(key: string) => void` | — | |
1559
+ | className | `string` | — | |
1560
+
1561
+ Also accepts the props of `ViewProps`.
1562
+
1563
+ ---
1564
+
1565
+ - **TabBar** (Navigation) — Top tab bar with indicator
1566
+
1567
+ ### TabBar
1568
+
1569
+ Top tab bar with indicator
1570
+
1571
+ ```tsx
1572
+ import { TabBar } from '@thewhileloop/whileui';
1573
+ ```
1574
+
1575
+
1576
+ | Prop | Type | Default | Description |
1577
+ | -------------------- | ------------------------------------- | ----------- | ----------- |
1578
+ | items (required) | `TabBarItem[]` | — | |
1579
+ | activeKey (required) | `string` | — | |
1580
+ | onSelect (required) | `(key: string) => void` | — | |
1581
+ | variant | `'default' \| 'pills' \| 'underline'` | `'default'` | |
1582
+ | scrollable | `boolean` | `false` | |
1583
+ | className | `string` | — | |
1584
+
1585
+ Also accepts the props of `ViewProps`.
1586
+
1587
+ ---
1588
+
1589
+ - **DrawerMenu** (Navigation) — Drawer with sections and items
1590
+
1591
+ ## DrawerMenu
1592
+
1593
+ ```tsx
1594
+ import { DrawerMenu } from '@thewhileloop/whileui';
1595
+
1596
+ <DrawerMenu
1597
+ visible={open}
1598
+ onClose={() => setOpen(false)}
1599
+ sections={[
1600
+ {
1601
+ title: 'Menu',
1602
+ items: [
1603
+ { key: 'home', label: 'Home', icon: <Icon /> },
1604
+ { key: 'settings', label: 'Settings', icon: <Icon /> },
1605
+ ],
1606
+ },
1607
+ ]}
1608
+ activeKey="home"
1609
+ onSelect={(key) => {}}
1610
+ header={<View>...</View>}
1611
+ footer={<Text>v1.0</Text>}
1612
+ />;
1613
+ ```
1614
+
1615
+
1616
+ | Prop | Type | Default | Description |
1617
+ | ------------------- | ----------------------- | ---------------------- | ----------------------------------------------------------- |
1618
+ | visible (required) | `boolean` | — | |
1619
+ | onClose (required) | `() => void` | — | |
1620
+ | sections (required) | `DrawerMenuSection[]` | — | |
1621
+ | activeKey | `string` | — | |
1622
+ | onSelect | `(key: string) => void` | — | |
1623
+ | header | `React.ReactNode` | — | |
1624
+ | footer | `React.ReactNode` | — | |
1625
+ | maxWidth | `number` | — | Max width in px. On web, defaults to 360 when not provided. |
1626
+ | className | `string` | — | |
1627
+ | style | `StyleProp<ViewStyle>` | — | |
1628
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1629
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1630
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1631
+
1632
+
1633
+ Closes from the backdrop, an item, Android back or Escape on web. It is hosted in a modal
1634
+ on every platform, so keyboard focus stays inside it while it is open.
1635
+
1636
+ ---
1637
+
1638
+ - **ActionBar** (Layout) — Sticky bottom action row with safe-area padding
1639
+
1640
+ ## ActionBar
1641
+
1642
+ ```tsx
1643
+ import { ActionBar, Button, ButtonText } from '@thewhileloop/whileui';
1644
+
1645
+ <ActionBar>
1646
+ <Button variant="outline" className="flex-1">
1647
+ <ButtonText>Cancel</ButtonText>
1648
+ </Button>
1649
+ <Button className="flex-1">
1650
+ <ButtonText>Save</ButtonText>
1651
+ </Button>
1652
+ </ActionBar>;
1653
+ ```
1654
+
1655
+ ---
1656
+
1657
+ - **ConfirmActionSheet** (Layout) — Reusable destructive confirmation sheet
1658
+
1659
+ ## ConfirmActionSheet
1660
+
1661
+ ```tsx
1662
+ import { ConfirmActionSheet } from '@thewhileloop/whileui';
1663
+
1664
+ <ConfirmActionSheet
1665
+ open={open}
1666
+ onOpenChange={setOpen}
1667
+ title="Delete project?"
1668
+ description="This action cannot be undone."
1669
+ confirmLabel="Delete"
1670
+ destructive
1671
+ onConfirm={() => deleteProject()}
1672
+ />;
1673
+ ```
1674
+
1675
+
1676
+ | Prop | Type | Default | Description |
1677
+ | ----------------------- | ---------------------------- | ---------------------- | ------------------------------------ |
1678
+ | open (required) | `boolean` | — | |
1679
+ | onOpenChange (required) | `(open: boolean) => void` | — | |
1680
+ | title (required) | `string` | — | |
1681
+ | description | `string` | — | |
1682
+ | actions | `ConfirmActionSheetAction[]` | — | |
1683
+ | confirmLabel | `string` | `'Confirm'` | |
1684
+ | cancelLabel | `string` | `'Cancel'` | |
1685
+ | onConfirm | `() => void` | — | |
1686
+ | onCancel | `() => void` | — | |
1687
+ | destructive | `boolean` | `true` | |
1688
+ | className | `string` | — | |
1689
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1690
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1691
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1692
+
1693
+ Also accepts the props of `ViewProps`.
1694
+
1695
+ ---
1696
+
1697
+ - **Sheet** (Layout) — Bottom sheet modal with header/content/footer slots
1698
+
1699
+ ## Sheet
1700
+
1701
+ Bottom sheet modal with slide animation. Slots: `SheetHeader`, `SheetContent`, `SheetFooter`, `SheetClose`.
1702
+
1703
+ ```tsx
1704
+ import {
1705
+ Sheet,
1706
+ SheetHeader,
1707
+ SheetContent,
1708
+ SheetFooter,
1709
+ SheetClose,
1710
+ Button,
1711
+ ButtonText,
1712
+ } from '@thewhileloop/whileui';
1713
+
1714
+ <Sheet open={open} onOpenChange={setOpen} maxHeight="half">
1715
+ <SheetHeader title="Settings" description="Adjust preferences" />
1716
+ <SheetContent>{/* Scrollable body */}</SheetContent>
1717
+ <SheetFooter>
1718
+ <SheetClose asChild>
1719
+ <Button>
1720
+ <ButtonText>Save</ButtonText>
1721
+ </Button>
1722
+ </SheetClose>
1723
+ </SheetFooter>
1724
+ </Sheet>;
1725
+ ```
1726
+
1727
+
1728
+ | Prop | Type | Default | Description |
1729
+ | ----------------------- | ---------------------------- | ---------------------- | ----------------------------------------------------------- |
1730
+ | open (required) | `boolean` | — | |
1731
+ | onOpenChange (required) | `(open: boolean) => void` | — | |
1732
+ | children (required) | `React.ReactNode` | — | |
1733
+ | maxHeight | `'half' \| 'full' \| number` | `'full'` | Max height: 'half' \| 'full' \| number (px). Default 'full' |
1734
+ | maxWidth | `number` | — | Web: max width. Default ~360 for mobile-first. |
1735
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1736
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1737
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1738
+
1739
+
1740
+ `SheetContent` is a `ScrollView` and accepts its props. It enables nested scrolling and
1741
+ keeps taps on inputs while the keyboard is open (`keyboardShouldPersistTaps="handled"`).
1742
+ Your `contentContainerStyle` is merged after the default padding. Tapping the backdrop
1743
+ closes the sheet; a drag across the backdrop does not, so a missed scroll never dismisses
1744
+ it. There is no drag-to-dismiss: close with the backdrop, Android back, Escape on web or
1745
+ `SheetClose`. A Select, Popover or confirmation Dialog placed inside the sheet opens above
1746
+ it, and back or Escape closes that inner layer before the sheet.
1747
+
1748
+ ---
1749
+
1750
+ - **FormModalScreen** (Layout) — Modal scaffold for forms with loading states
1751
+
1752
+ ### FormModalScreen
1753
+
1754
+ Modal scaffold for forms with loading states
1755
+
1756
+ ```tsx
1757
+ import { FormModalScreen } from '@thewhileloop/whileui';
1758
+ ```
1759
+
1760
+
1761
+ | Prop | Type | Default | Description |
1762
+ | ------------------- | ----------------- | ------------- | ----------------------------------------------------------------------------------------- |
1763
+ | className | `string` | — | |
1764
+ | title (required) | `string` | — | |
1765
+ | subtitle | `string` | — | |
1766
+ | leftSlot | `React.ReactNode` | — | |
1767
+ | rightSlot | `React.ReactNode` | — | |
1768
+ | onClose | `() => void` | — | |
1769
+ | loading | `boolean` | `false` | |
1770
+ | saving | `boolean` | `false` | |
1771
+ | savingText | `string` | `'Saving...'` | |
1772
+ | loadingSpinnerColor | `string` | — | Hex color for spinner in loading state. ActivityIndicator requires hex. |
1773
+ | savingSpinnerColor | `string` | — | Hex color for spinner in saving banner. Defaults to platform (Android blue) when not set. |
1774
+ | scrollEnabled | `boolean` | `true` | |
1775
+ | children (required) | `React.ReactNode` | — | |
1776
+
1777
+ Also accepts the props of `ViewProps`.
1778
+
1779
+ ---
1780
+
1781
+ - **ContentSkeleton** (Layout) — Page/content placeholder with variants (list, card, generic)
1782
+
1783
+ ## ContentSkeleton
1784
+
1785
+ Page/content placeholder with layout presets. Use while loading data instead of a spinner when you want to preview the layout.
1786
+
1787
+ ```tsx
1788
+ import { ContentSkeleton } from '@thewhileloop/whileui';
1789
+
1790
+ <ContentSkeleton variant="list" rows={4} />
1791
+ <ContentSkeleton variant="card" />
1792
+ <ContentSkeleton variant="generic" />
1793
+ ```
1794
+
1795
+
1796
+ | Prop | Type | Default | Description |
1797
+ | --------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
1798
+ | variant | `'list' \| 'card' \| 'generic'` | `'list'` | Layout preset. `list` = avatar + lines rows, `card` = card with image/body, `generic` = simple page lines. |
1799
+ | rows | `number` | `4` | Number of list rows (list variant only). Default 4. |
1800
+ | className | `string` | — | |
1801
+
1802
+ Also accepts the props of `ViewProps`.
1803
+
1804
+ ---
1805
+
1806
+ - **PageSkeleton** (Layout) — Variant-based page layouts (dashboard, list, settings, card, generic)
1807
+
1808
+ ## PageSkeleton
1809
+
1810
+ Variant-based page layouts for loading states. Replaces app-specific skeletons with presets for dashboard, list, settings, card, and generic pages.
1811
+
1812
+ ```tsx
1813
+ import { PageSkeleton } from '@thewhileloop/whileui';
1814
+
1815
+ <PageSkeleton variant="dashboard" />
1816
+ <PageSkeleton variant="list" count={5} />
1817
+ <PageSkeleton variant="settings" count={6} />
1818
+ <PageSkeleton variant="card" />
1819
+ <PageSkeleton variant="generic" />
1820
+ <PageSkeleton variant="dashboard" headerPlaceholder />
1821
+ <PageSkeleton variant="list" header={<Header title="Loading..." />} />
1822
+ <PageSkeleton variant="list" padding="none" className="flex-1" />
1823
+ ```
1824
+
1825
+
1826
+ | Prop | Type | Default | Description |
1827
+ | ------------------ | ------------------------------------------ | ----------- | ------------------------------------------------------------------- |
1828
+ | variant (required) | `PageSkeletonVariant` | — | Layout preset. |
1829
+ | count | `number` | `3` | Rows/items for list (default 3) or settings (default 4) variant. |
1830
+ | padding | `PageSkeletonPadding` | `'default'` | Container padding. |
1831
+ | header | `ReactNode` | — | Optional header slot rendered above skeleton content. |
1832
+ | headerPlaceholder | `boolean \| PageSkeletonHeaderPlaceholder` | `false` | Optional placeholder header (when no real header is available yet). |
1833
+ | className | `string` | — | |
1834
+
1835
+ Also accepts the props of `ViewProps`.
1836
+
1837
+ ---
1838
+
1839
+ - **ScreenSkeleton** (Layout) — Header slot/placeholder + PageSkeleton content in one block
1840
+
1841
+ ## ScreenSkeleton
1842
+
1843
+ Convenience block for loading screens that need both header and content continuity.
1844
+
1845
+ ```tsx
1846
+ import { ScreenSkeleton } from '@thewhileloop/whileui';
1847
+
1848
+ <ScreenSkeleton variant="dashboard" headerPlaceholder />
1849
+ <ScreenSkeleton variant="list" count={5} header={<Header title="Projects" />} />
1850
+ ```
1851
+
1852
+
1853
+ | Prop | Type | Default | Description |
1854
+ | ----------------- | ------------------------------------------ | ----------- | -------------------------------------- |
1855
+ | variant | `PageSkeletonVariant` | `'generic'` | Skeleton content preset. |
1856
+ | count | `number` | — | Rows/items for list/settings variants. |
1857
+ | padding | `PageSkeletonPadding` | `'default'` | Inner content padding. |
1858
+ | header | `ReactNode` | — | Real header slot. |
1859
+ | headerPlaceholder | `boolean \| PageSkeletonHeaderPlaceholder` | `'default'` | Placeholder header. |
1860
+ | className | `string` | — | |
1861
+
1862
+ Also accepts the props of `ViewProps`.
1863
+
1864
+ ---
1865
+
1866
+ - **ErrorBoundary** (Layout) — React ErrorBoundary that renders ErrorState by default
1867
+
1868
+ ## ErrorBoundary
1869
+
1870
+ React ErrorBoundary that catches render errors and renders ErrorState by default.
1871
+
1872
+ ```tsx
1873
+ import { ErrorBoundary } from '@thewhileloop/whileui';
1874
+
1875
+ <ErrorBoundary onError={(err) => console.error(err)}>
1876
+ <App />
1877
+ </ErrorBoundary>;
1878
+ ```
1879
+
1880
+
1881
+ | Prop | Type | Default | Description |
1882
+ | ------------------- | --------------------------------------------------------------- | ------- | -------------------------------------------------- |
1883
+ | children (required) | `ReactNode` | — | |
1884
+ | fallback | `ReactNode \| ((error: Error, reset: () => void) => ReactNode)` | — | Custom fallback. If not provided, uses ErrorState. |
1885
+ | onError | `(error: Error, errorInfo: ErrorInfo) => void` | — | Called when error is caught. |
1886
+
1887
+ ---
1888
+
1889
+ - **EmptyState** (Layout) — Empty content placeholder
1890
+
1891
+ ## EmptyState
1892
+
1893
+ ```tsx
1894
+ import { EmptyState } from '@thewhileloop/whileui';
1895
+
1896
+ <EmptyState
1897
+ icon={<Icon />}
1898
+ title="No items"
1899
+ description="Add your first item."
1900
+ action={{ label: 'Add Item', onPress: () => {} }}
1901
+ />;
1902
+ ```
1903
+
1904
+ ---
1905
+
1906
+ - **ErrorState** (Layout) — Error display with retry
1907
+
1908
+ ### ErrorState
1909
+
1910
+ Error display with retry
1911
+
1912
+ ```tsx
1913
+ import { ErrorState } from '@thewhileloop/whileui';
1914
+ ```
1915
+
1916
+
1917
+ | Prop | Type | Default | Description |
1918
+ | ----------- | ----------------- | ------------------------ | ----------- |
1919
+ | icon | `React.ReactNode` | — | |
1920
+ | title | `string` | `'Something went wrong'` | |
1921
+ | description | `string` | — | |
1922
+ | error | `Error \| string` | — | |
1923
+ | onRetry | `() => void` | — | |
1924
+ | retryLabel | `string` | `'Try again'` | |
1925
+ | className | `string` | — | |
1926
+
1927
+ Also accepts the props of `ViewProps`.
1928
+
1929
+ ---
1930
+
1931
+ - **LoadingState** (Layout) — In-flow loading placeholder for a section still fetching
1932
+
1933
+ ## LoadingState
1934
+
1935
+ ```tsx
1936
+ import { LoadingState } from '@thewhileloop/whileui';
1937
+
1938
+ <LoadingState label="Loading activity" />;
1939
+ ```
1940
+
1941
+ The in-flow counterpart of `EmptyState` and `ErrorState` for a section that is still
1942
+ fetching. The spinner is named for assistive technology by `label` (default "Loading").
1943
+
1944
+
1945
+ | Prop | Type | Default | Description |
1946
+ | --------- | --------------------------- | ----------- | ------------------------------------------------------------------------------ |
1947
+ | label | `string` | — | Read by assistive technology as the spinner's name; shown under it when given. |
1948
+ | size | `'sm' \| 'default' \| 'lg'` | `'default'` | |
1949
+ | className | `string` | — | |
1950
+
1951
+ Also accepts the props of `ViewProps`.
1952
+
1953
+ ---
1954
+
1955
+ - **LoadingScreen** (Layout) — Full-screen loading indicator
1956
+
1957
+ ### LoadingScreen
1958
+
1959
+ Full-screen loading indicator
1960
+
1961
+ ```tsx
1962
+ import { LoadingScreen } from '@thewhileloop/whileui';
1963
+ ```
1964
+
1965
+
1966
+ | Prop | Type | Default | Description |
1967
+ | ------------ | ---------------------- | ------- | ------------------------------------------------------ |
1968
+ | message | `string` | — | |
1969
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | |
1970
+ | spinnerColor | `string` | — | Hex color for spinner. ActivityIndicator requires hex. |
1971
+ | className | `string` | — | |
1972
+
1973
+ Also accepts the props of `ViewProps`.
1974
+
1975
+ ---
1976
+
1977
+ - **PullToRefreshScrollView** (Layout) — Themed ScrollView with RefreshControl (useThemeTokens for colors)
1978
+
1979
+ ## PullToRefreshScrollView
1980
+
1981
+ Themed ScrollView with RefreshControl. Uses `useThemeTokens` for spinner color.
1982
+
1983
+ ```tsx
1984
+ import { PullToRefreshScrollView } from '@thewhileloop/whileui';
1985
+
1986
+ <PullToRefreshScrollView refreshing={refreshing} onRefresh={fetchData} refreshColor="#22c55e">
1987
+ {content}
1988
+ </PullToRefreshScrollView>;
1989
+ ```
1990
+
1991
+
1992
+ | Prop | Type | Default | Description |
1993
+ | --------------------- | ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1994
+ | refreshing (required) | `boolean` | — | Whether refresh is in progress. |
1995
+ | onRefresh (required) | `() => void \| Promise<void>` | — | Refreshing is almost always async, so the signature says so. Nothing here awaits it — the spinner is driven by `refreshing`, which the owner controls — so a handler that can reject must catch its own errors. |
1996
+ | refreshColor | `string` | — | Override tint/colors. Defaults to theme primary. |
1997
+ | className | `string` | — | |
1998
+
1999
+ Also accepts the props of `ScrollViewProps`.
2000
+
2001
+ The scroll view owns the refresh gesture; don't pass `refreshControl`. It bounces even
2002
+ when content is shorter than the screen, so a short list can still be pulled. Screen
2003
+ reader users get the same refresh as a **Refresh** accessibility action, since they
2004
+ cannot pull. It is ignored while `refreshing` is true, and any `accessibilityActions` you
2005
+ pass are kept.
2006
+
2007
+ ---
2008
+
2009
+ - **KeyboardAwareScreen** (Layout) — One screen-level keyboard policy for bottom-docked controls
2010
+
2011
+ ## KeyboardAwareScreen and DockedInput
2012
+
2013
+ Use one keyboard-aware root for the screen, then place the composer in a bottom dock. Feed the dock height into the scroll content inset so the last item remains visible.
2014
+
2015
+ ```tsx
2016
+ import { DockedInput, KeyboardAwareScreen, SmartInput } from '@thewhileloop/whileui';
2017
+
2018
+ <KeyboardAwareScreen>
2019
+ <ScrollView contentContainerStyle={{ paddingBottom: inputHeight }} />
2020
+ <DockedInput onHeightChange={setInputHeight}>
2021
+ <SmartInput variant="card" safeArea={false} />
2022
+ </DockedInput>
2023
+ </KeyboardAwareScreen>;
2024
+ ```
2025
+
2026
+ ---
2027
+
2028
+ - **DockedInput** (Layout) — Safe-area-aware bottom dock that reports its complete height
2029
+
2030
+ Documented under KeyboardAwareScreen.
2031
+
2032
+ ---
2033
+
2034
+ - **SmartInput** (Layout) — Compose input with slots, variants, and optional rotating examples
2035
+
2036
+ ## SmartInput
2037
+
2038
+ Compose input for use inline or inside `DockedInput`. `variant`: "bar" or "card". Slots: `leftSlot`, `centerSlot` (intent selector), `rightSlot`. `submitBehavior`: "newline" (default) or "submit"/"blurAndSubmit". `placeholderExamples` animates rotating prompts and respects the platform reduced-motion setting. Forwards ref to `TextInput`.
2039
+
2040
+ ```tsx
2041
+ import { SmartInput, Button, ButtonText } from '@thewhileloop/whileui';
2042
+
2043
+ <SmartInput
2044
+ value={message}
2045
+ onChangeText={setMessage}
2046
+ placeholderExamples={['Log breakfast', 'Add a 30 minute walk', 'Set weight to 72 kg']}
2047
+ leftSlot={
2048
+ <Button variant="ghost" size="icon" accessibilityLabel="Add">
2049
+ <Icon name="add" />
2050
+ </Button>
2051
+ }
2052
+ rightSlot={
2053
+ <Button size="icon" onPress={handleSend}>
2054
+ <ButtonText>Send</ButtonText>
2055
+ </Button>
2056
+ }
2057
+ />;
2058
+ ```
2059
+
2060
+ ---
2061
+
2062
+ - **SplashScreen** (Layout) — Branded splash (fade/scale/slide variants)
2063
+
2064
+ ## SplashScreen
2065
+
2066
+ ```tsx
2067
+ import { SplashScreen } from '@thewhileloop/whileui';
2068
+
2069
+ <SplashScreen
2070
+ logo={<MyLogo />}
2071
+ appName="MyApp"
2072
+ tagline="Your tagline"
2073
+ variant="scale"
2074
+ duration={1500}
2075
+ showLoading
2076
+ onAnimationComplete={() => {}}
2077
+ />;
2078
+ ```
2079
+
2080
+
2081
+ | Prop | Type | Default | Description |
2082
+ | ------------------- | ------------------------------ | --------- | ----------------------------------------------- |
2083
+ | appName | `string` | — | App name displayed below logo |
2084
+ | tagline | `string` | — | Tagline displayed below app name |
2085
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2086
+ | variant | `'fade' \| 'scale' \| 'slide'` | `'scale'` | Animation variant |
2087
+ | duration | `number` | `800` | Animation duration in ms |
2088
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2089
+ | showLoading | `boolean` | `false` | Whether to show loading indicator |
2090
+ | loadingText | `string` | — | Loading text |
2091
+ | className | `string` | — | |
2092
+
2093
+ Also accepts the props of `ViewProps`.
2094
+
2095
+ ---
2096
+
2097
+ - **MinimalSplash** (Layout) — Minimal monochrome splash
2098
+
2099
+ ### MinimalSplash
2100
+
2101
+ Minimal monochrome splash
2102
+
2103
+ ```tsx
2104
+ import { MinimalSplash } from '@thewhileloop/whileui';
2105
+ ```
2106
+
2107
+
2108
+ | Prop | Type | Default | Description |
2109
+ | ------------------- | ----------------- | ------- | ----------------------------------------------- |
2110
+ | className | `string` | — | |
2111
+ | appName | `string` | — | App name displayed below logo |
2112
+ | tagline | `string` | — | Tagline displayed below app name |
2113
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2114
+ | duration | `number` | `800` | Animation duration in ms |
2115
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2116
+ | showLoading | `boolean` | `false` | Whether to show loading indicator |
2117
+ | loadingText | `string` | — | Loading text |
2118
+
2119
+ Also accepts the props of `SplashScreenProps`.
2120
+
2121
+ ---
2122
+
2123
+ - **BrandedSplash** (Layout) — Splash with brand imagery
2124
+
2125
+ ### BrandedSplash
2126
+
2127
+ Splash with brand imagery
2128
+
2129
+ ```tsx
2130
+ import { BrandedSplash } from '@thewhileloop/whileui';
2131
+ ```
2132
+
2133
+
2134
+ | Prop | Type | Default | Description |
2135
+ | ------------------- | ----------------- | ------- | ----------------------------------------------- |
2136
+ | className | `string` | — | |
2137
+ | appName | `string` | — | App name displayed below logo |
2138
+ | tagline | `string` | — | Tagline displayed below app name |
2139
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2140
+ | duration | `number` | `800` | Animation duration in ms |
2141
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2142
+ | loadingText | `string` | — | Loading text |
2143
+
2144
+ Also accepts the props of `SplashScreenProps`.
2145
+
2146
+ ---
2147
+
2148
+ - **Chat** (Chat) — Opinionated messages, suggestions, and SmartInput composition
2149
+
2150
+ ### Chat
2151
+
2152
+ | Block | Description |
2153
+ | --------------------- | --------------------------------------------------------------- |
2154
+ | **Chat** | Opinionated messages, suggestions, and SmartInput composition |
2155
+ | **ChatConversation** | Constrained conversation viewport that follows streaming output |
2156
+ | **ChatMessageBubble** | Message bubble (user/assistant, big/small text) |
2157
+ | **ChatSuggestions** | Suggestion chips when empty |
2158
+
2159
+ ---
2160
+
2161
+ - **ChatConversation** (Chat) — Constrained conversation viewport that follows streaming output
2162
+
2163
+ ## ChatConversation / Chat
2164
+
2165
+ `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.
2166
+
2167
+ 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.
2168
+
2169
+ ```tsx
2170
+ import { ChatConversation, SmartInput, View } from '@thewhileloop/whileui';
2171
+
2172
+ <View className="flex-1">
2173
+ <ChatConversation contentContainerClassName="gap-4 px-4 py-6">
2174
+ {messages.map((message) => (
2175
+ <ProductMessage key={message.id} message={message} />
2176
+ ))}
2177
+ </ChatConversation>
2178
+ <SmartInput
2179
+ value={value}
2180
+ onChangeText={setValue}
2181
+ onSubmitEditing={send}
2182
+ submitBehavior="submit"
2183
+ leftSlot={<AttachButton />}
2184
+ centerSlot={<ModelSelect />}
2185
+ rightSlot={<SendButton />}
2186
+ />
2187
+ </View>;
2188
+ ```
2189
+
2190
+ `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.
2191
+
2192
+ ```tsx
2193
+ import { Chat, type ChatMessage } from '@thewhileloop/whileui';
2194
+
2195
+ const [messages, setMessages] = useState<ChatMessage[]>([...]);
2196
+ const [value, setValue] = useState('');
2197
+
2198
+ <Chat
2199
+ messages={messages}
2200
+ value={value}
2201
+ onChangeText={setValue}
2202
+ onSend={() => { /* append user msg, clear input */ }}
2203
+ placeholder="Message..."
2204
+ suggestions={['Summarize this', 'Explain simply', 'Translate']}
2205
+ onSuggestionPress={(text) => setValue(text)}
2206
+ emptyTitle="How can I help?"
2207
+ emptyDescription="Ask anything."
2208
+ leftSlot={<Button variant="ghost" size="icon" accessibilityLabel="Attach"><Icon name="paperclip" /></Button>}
2209
+ rightSlot={<Button size="icon" accessibilityLabel="Send" onPress={handleSend}><Icon name="send" /></Button>}
2210
+ />;
2211
+ ```
2212
+
2213
+
2214
+ | Prop | Type | Default | Description |
2215
+ | ----------------------- | ------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------- |
2216
+ | messages (required) | `ChatMessage[]` | — | Messages to show, each `{ id, role, content, secondary?, contentSize? }`. |
2217
+ | value (required) | `string` | — | Input value. |
2218
+ | onChangeText (required) | `(text: string) => void` | — | Input change. |
2219
+ | onSend (required) | `() => void` | — | Send handler. |
2220
+ | placeholder | `string` | `'Message...'` | |
2221
+ | suggestions | `string[]` | `EMPTY_SUGGESTIONS` | Chips when empty. |
2222
+ | onSuggestionPress | `(text: string) => void` | — | |
2223
+ | leftSlot | `React.ReactNode` | — | |
2224
+ | rightSlot | `React.ReactNode` | — | |
2225
+ | loading | `boolean` | `false` | |
2226
+ | header | `React.ReactNode` | — | |
2227
+ | emptyTitle | `string` | — | |
2228
+ | emptyDescription | `string` | — | |
2229
+ | exampleMessage | `ChatMessage` | — | Example message shown in empty state so users see the bubble UI. |
2230
+ | renderMessage | `(message: ChatMessage) => React.ReactNode` | — | Custom message renderer (markdown, code, images). Falls back to ChatMessageBubble. |
2231
+ | loadingIndicator | `React.ReactNode` | — | Shown at bottom when loading (typing indicator). |
2232
+ | inputSafeArea | `boolean` | `true` | Passed to SmartInput. Use false when Chat is in a contained demo. |
2233
+ | keyboardVerticalOffset | `number` | `0` | Offset for KeyboardAvoidingView when Chat has a header (e.g. StatusBar + header height). |
2234
+ | className | `string` | — | |
2235
+ | messagesClassName | `string` | — | |
2236
+
2237
+
2238
+ `ChatConversation` extends React Native `ScrollViewProps`, forwards its ref, and adds `contentContainerClassName` to style the constrained inner column. Caller `onScroll` and `onContentSizeChange` callbacks are preserved.
2239
+
2240
+ ### Optional Markdown and code
2241
+
2242
+ These are explicit subpaths so the root package remains independent of a Markdown
2243
+ parser. Install `markdown-it` only in applications that render Markdown.
2244
+
2245
+ ```tsx
2246
+ import { Markdown } from '@thewhileloop/whileui/markdown';
2247
+ import { CodeBlock } from '@thewhileloop/whileui/code';
2248
+
2249
+ <Markdown onLinkPress={openLink}>{assistantText}</Markdown>
2250
+ <CodeBlock code={source} language="TypeScript" actions={<CopyButton />} />
2251
+ ```
2252
+
2253
+ `Markdown` supports headings, paragraphs, emphasis, links, lists, quotes, rules, inline
2254
+ code, and fenced code. It never opens links or writes to the clipboard itself:
2255
+ `onLinkPress` and `codeActions` keep those capabilities in the application. `CodeBlock`
2256
+ is dependency-free and accepts an application-owned `actions` slot.
2257
+
2258
+ ---
2259
+
2260
+ - **ChatMessageBubble** (Chat) — Message bubble (user/assistant, big/small text)
2261
+
2262
+ ### ChatMessageBubble
2263
+
2264
+ One message in a `Chat`, rendered on its own when a screen composes the conversation itself.
2265
+
2266
+ ```tsx
2267
+ import { ChatMessageBubble } from '@thewhileloop/whileui';
2268
+ ```
2269
+
2270
+
2271
+ | Prop | Type | Default | Description |
2272
+ | ------------------ | ------------- | ------- | --------------------------------------------------------------- |
2273
+ | message (required) | `ChatMessage` | — | The message to render; its `role` decides alignment and colour. |
2274
+ | contentClassName | `string` | — | Classes for the bubble's content wrapper. |
2275
+
2276
+ ---
2277
+
2278
+ - **ChatSuggestions** (Chat) — Suggestion chips when empty
2279
+
2280
+ ### ChatSuggestions
2281
+
2282
+ Suggestion chips when empty
2283
+
2284
+ ```tsx
2285
+ import { ChatSuggestions } from '@thewhileloop/whileui';
2286
+ ```
2287
+
2288
+
2289
+ | Prop | Type | Default | Description |
2290
+ | ---------------------- | ------------------------ | ------------------- | ----------- |
2291
+ | suggestions (required) | `string[]` | `EMPTY_SUGGESTIONS` | |
2292
+ | onSelect (required) | `(text: string) => void` | — | |
2293
+ | className | `string` | — | |
2294
+
2295
+ ---
2296
+
2297
+ - **ProfileHeader** (Profile & Settings, copy-paste template) — Profile header with stats
2298
+
2299
+ ## ProfileHeader
2300
+
2301
+ Copy from `apps/showcase/catalog/templates/profile/profile-header.tsx`, then:
2302
+
2303
+ ```tsx
2304
+ import { ProfileHeader } from './templates/profile'; // or your path
2305
+
2306
+ <ProfileHeader
2307
+ name="John Doe"
2308
+ username="johndoe"
2309
+ bio="Designer & Developer"
2310
+ avatarFallback="JD"
2311
+ avatarUrl="https://..."
2312
+ verified
2313
+ stats={[
2314
+ { label: 'Followers', value: '1.2K' },
2315
+ { label: 'Following', value: 234 },
2316
+ ]}
2317
+ action={{ label: 'Edit Profile', onPress: () => {} }}
2318
+ />;
2319
+ ```
2320
+
2321
+ ---
2322
+
2323
+ - **AccountCard** (Profile & Settings, copy-paste template) — Account summary card
2324
+
2325
+ No API section yet; the summary above is the full description.
2326
+
2327
+ ---
2328
+
2329
+ - **SettingsSection** (Profile & Settings, copy-paste template) — Section header with optional action
2330
+
2331
+ ## SettingsSection / SettingsItem
2332
+
2333
+ Copy from `apps/showcase/catalog/templates/profile/`, then:
2334
+
2335
+ ```tsx
2336
+ import { SettingsSection, SettingsItem } from './templates/profile'; // or your path
2337
+
2338
+ <SettingsSection title="Preferences">
2339
+ <SettingsItem
2340
+ icon={<Icon />}
2341
+ label="Notifications"
2342
+ type="toggle"
2343
+ toggleValue={enabled}
2344
+ onToggle={setEnabled}
2345
+ />
2346
+ <SettingsItem icon={<Icon />} label="Privacy" value="Public" onPress={() => {}} />
2347
+ <SettingsItem icon={<Icon />} label="Sign Out" type="action" destructive />
2348
+ </SettingsSection>;
2349
+ ```
2350
+
2351
+ ---
2352
+
2353
+ - **SettingsItem** (Profile & Settings, copy-paste template) — Row for toggles/links/settings
2354
+
2355
+ Documented under SettingsSection.
2356
+
2357
+ ---
2358
+
2359
+ - **ListItem** (Lists) — Title/subtitle row
2360
+
2361
+ ### ListItem
2362
+
2363
+ Title/subtitle row
2364
+
2365
+ ```tsx
2366
+ import { ListItem } from '@thewhileloop/whileui';
2367
+ ```
2368
+
2369
+
2370
+ | Prop | Type | Default | Description |
2371
+ | ---------------- | ----------------- | ------- | ----------------------------------------------- |
2372
+ | icon | `React.ReactNode` | — | |
2373
+ | title (required) | `string` | — | |
2374
+ | subtitle | `string` | — | |
2375
+ | description | `string` | — | |
2376
+ | rightIcon | `React.ReactNode` | — | |
2377
+ | rightText | `string` | — | |
2378
+ | action | `React.ReactNode` | — | |
2379
+ | showBorder | `boolean` | `true` | |
2380
+ | compact | `boolean` | `false` | |
2381
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2382
+ | className | `string` | — | |
2383
+
2384
+ Also accepts the props of `PressableProps`.
2385
+
2386
+ ---
2387
+
2388
+ - **NotificationItem** (Lists) — Notification row with metadata
2389
+
2390
+ ### NotificationItem
2391
+
2392
+ Notification row with metadata
2393
+
2394
+ ```tsx
2395
+ import { NotificationItem } from '@thewhileloop/whileui';
2396
+ ```
2397
+
2398
+
2399
+ | Prop | Type | Default | Description |
2400
+ | ------------------ | ----------------- | ------- | ----------------------------------------------- |
2401
+ | icon | `React.ReactNode` | — | |
2402
+ | avatarUrl | `string` | — | |
2403
+ | avatarFallback | `string` | — | |
2404
+ | title (required) | `string` | — | |
2405
+ | message (required) | `string` | — | |
2406
+ | time (required) | `string` | — | |
2407
+ | read | `boolean` | `false` | |
2408
+ | showDot | `boolean` | `true` | |
2409
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2410
+ | className | `string` | — | |
2411
+
2412
+ Also accepts the props of `PressableProps`.
2413
+
2414
+ ---
2415
+
2416
+ - **TimelineFeed** (Lists) — Vertical feed with connecting lines
2417
+
2418
+ ### TimelineFeed
2419
+
2420
+ Vertical feed with connecting lines
2421
+
2422
+ ```tsx
2423
+ import { TimelineFeed } from '@thewhileloop/whileui';
2424
+ ```
2425
+
2426
+
2427
+ | Prop | Type | Default | Description |
2428
+ | ---------------- | ---------------- | ------- | ----------- |
2429
+ | className | `string` | — | |
2430
+ | items (required) | `TimelineItem[]` | — | |
2431
+ | showConnector | `boolean` | `true` | |
2432
+
2433
+ Also accepts the props of `ViewProps`.
2434
+
2435
+ ---
2436
+
2437
+ - **ProductCard** (Commerce) — Product card with badge/media
2438
+
2439
+ ## ProductCard
2440
+
2441
+ ```tsx
2442
+ import { ProductCard } from '@thewhileloop/whileui';
2443
+
2444
+ <ProductCard
2445
+ title="Product Name"
2446
+ price="$99"
2447
+ originalPrice="$129"
2448
+ badge="-23%"
2449
+ rating={4.5}
2450
+ reviewCount={128}
2451
+ variant="vertical"
2452
+ onPress={() => {}}
2453
+ />;
2454
+ ```
2455
+
2456
+
2457
+ | Prop | Type | Default | Description |
2458
+ | ---------------- | ---------------------------- | ------------ | ----------------------------------------------- |
2459
+ | imageUrl | `string` | — | |
2460
+ | title (required) | `string` | — | |
2461
+ | description | `string` | — | |
2462
+ | price (required) | `string` | — | |
2463
+ | originalPrice | `string` | — | |
2464
+ | badge | `string` | — | |
2465
+ | rating | `number` | — | |
2466
+ | reviewCount | `number` | — | |
2467
+ | inStock | `boolean` | `true` | |
2468
+ | variant | `'vertical' \| 'horizontal'` | `'vertical'` | |
2469
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2470
+ | className | `string` | — | |
2471
+
2472
+ Also accepts the props of `PressableProps`.
2473
+
2474
+ ---
2475
+
2476
+ - **PricingCard** (Commerce) — Pricing tiers with feature list
2477
+
2478
+ ## PricingCard
2479
+
2480
+ ```tsx
2481
+ import { PricingCard } from '@thewhileloop/whileui';
2482
+
2483
+ <PricingCard
2484
+ name="Pro"
2485
+ description="For teams"
2486
+ price="$29"
2487
+ period="/month"
2488
+ badge="Popular"
2489
+ highlighted
2490
+ features={[
2491
+ { label: 'Unlimited users', included: true },
2492
+ { label: 'Priority support', included: true },
2493
+ { label: 'Custom domain', included: false },
2494
+ ]}
2495
+ onPress={() => {}}
2496
+ />;
2497
+ ```
2498
+
2499
+ ---
2500
+
2501
+ - **CheckoutSummary** (Commerce) — Cart summary with line items
2502
+
2503
+ ### CheckoutSummary
2504
+
2505
+ Cart summary with line items
2506
+
2507
+ ```tsx
2508
+ import { CheckoutSummary } from '@thewhileloop/whileui';
2509
+ ```
2510
+
2511
+
2512
+ | Prop | Type | Default | Description |
2513
+ | ------------------- | -------------------- | ------------ | ----------- |
2514
+ | items (required) | `CheckoutLineItem[]` | — | |
2515
+ | subtotal (required) | `string` | — | |
2516
+ | shipping | `string` | — | |
2517
+ | tax | `string` | — | |
2518
+ | discount | `string` | — | |
2519
+ | total (required) | `string` | — | |
2520
+ | buttonLabel | `string` | `'Checkout'` | |
2521
+ | onCheckout | `() => void` | — | |
2522
+ | loading | `boolean` | `false` | |
2523
+ | className | `string` | — | |
2524
+
2525
+ Also accepts the props of `ViewProps`.
2526
+
2527
+ ---
2528
+
2529
+ - **MetricCard** (Commerce) — Stats/progress card for dashboards
2530
+
2531
+ ### MetricCard
2532
+
2533
+ Stats/progress card for dashboards
2534
+
2535
+ ```tsx
2536
+ import { MetricCard } from '@thewhileloop/whileui';
2537
+ ```
2538
+
2539
+
2540
+ | Prop | Type | Default | Description |
2541
+ | ---------------- | ------------------------------------ | ----------- | ----------------------------------------------- |
2542
+ | className | `string` | — | |
2543
+ | label (required) | `string` | — | |
2544
+ | value (required) | `string \| number` | — | |
2545
+ | subtitle | `string` | — | |
2546
+ | progress | `number` | — | |
2547
+ | segments | `{ value: number; color: string }[]` | — | |
2548
+ | icon | `React.ReactNode` | — | |
2549
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2550
+ | variant | `'default' \| 'outlined'` | `'default'` | |
2551
+
2552
+ Also accepts the props of `ViewProps`.
2553
+
2554
+ ---
2555
+
2556
+ - **SubscriptionCard** (Commerce) — Current plan card with manage/upgrade actions
2557
+
2558
+ ## SubscriptionCard
2559
+
2560
+ ```tsx
2561
+ import { SubscriptionCard } from '@thewhileloop/whileui';
2562
+
2563
+ <SubscriptionCard
2564
+ planName="Pro"
2565
+ price="$29"
2566
+ period="/month"
2567
+ expiresAt="April 18, 2026"
2568
+ isActive
2569
+ onManage={() => {}}
2570
+ onUpgrade={() => {}}
2571
+ />;
2572
+ ```
2573
+
2574
+
2575
+ | Prop | Type | Default | Description |
2576
+ | ------------------- | ------------ | --------------- | ----------------------------------------------- |
2577
+ | planName (required) | `string` | — | Current plan label. |
2578
+ | price (required) | `string` | — | Plan price. |
2579
+ | period | `string` | `'/month'` | Billing period label. |
2580
+ | expiresAt | `string` | — | Renewal/expiry date text. |
2581
+ | isActive | `boolean` | `true` | Active/inactive badge state. |
2582
+ | onManage | `() => void` | — | Manage action. |
2583
+ | onUpgrade | `() => void` | — | Upgrade action. |
2584
+ | manageLabel | `string` | `'Manage plan'` | |
2585
+ | upgradeLabel | `string` | `'Upgrade'` | |
2586
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2587
+ | className | `string` | — | |
2588
+
2589
+ Also accepts the props of `ViewProps`.
2590
+
2591
+ ---
2592
+
2593
+ - **FeatureGate** (Commerce) — Locked feature state with upgrade CTA
2594
+
2595
+ ## FeatureGate
2596
+
2597
+ ```tsx
2598
+ import { FeatureGate } from '@thewhileloop/whileui';
2599
+
2600
+ <FeatureGate
2601
+ title="Advanced export is locked"
2602
+ description="Upgrade to unlock 4K export."
2603
+ buttonLabel="Upgrade"
2604
+ onUpgrade={() => {}}
2605
+ />;
2606
+ ```
2607
+
2608
+ Use `children` to show dimmed preview content with an overlay CTA.
2609
+
2610
+ ---
2611
+
2612
+ - **UsageBar** (Commerce) — Quota meter with warning/exceeded states
2613
+
2614
+ ## UsageBar
2615
+
2616
+ ```tsx
2617
+ import { UsageBar } from '@thewhileloop/whileui';
2618
+
2619
+ <UsageBar label="AI generations" used={8} limit={20} />;
2620
+ ```
2621
+
2622
+
2623
+ | Prop | Type | Default | Description |
2624
+ | ---------------- | ----------------- | ------- | ------------------------------- |
2625
+ | label (required) | `string` | — | Usage label. |
2626
+ | used (required) | `number` | — | Used amount. |
2627
+ | limit (required) | `number` | — | Quota limit. |
2628
+ | variant | `UsageBarVariant` | — | Optional explicit visual state. |
2629
+ | className | `string` | — | |
2630
+
2631
+ Also accepts the props of `ViewProps`.
2632
+
2633
+ ---
2634
+
2635
+ - **PlanToggle** (Commerce) — Monthly vs annual plan switcher
2636
+
2637
+ ## PlanToggle
2638
+
2639
+ ```tsx
2640
+ import { PlanToggle } from '@thewhileloop/whileui';
2641
+
2642
+ <PlanToggle
2643
+ selected="monthly"
2644
+ monthlyLabel="Monthly"
2645
+ annualLabel="Annual"
2646
+ annualDiscount="Save 20%"
2647
+ onChange={(next) => {}}
2648
+ />;
2649
+ ```
2650
+
2651
+ ---
2652
+
2653
+ - **UpgradeBanner** (Commerce) — Inline upgrade banner with action/dismiss
2654
+
2655
+ ## UpgradeBanner
2656
+
2657
+ ```tsx
2658
+ import { UpgradeBanner } from '@thewhileloop/whileui';
2659
+
2660
+ <UpgradeBanner
2661
+ message="Unlock unlimited exports with Pro."
2662
+ actionLabel="See plans"
2663
+ onAction={() => {}}
2664
+ onDismiss={() => {}}
2665
+ />;
2666
+ ```
2667
+
2668
+ ---
2669
+
2670
+ - **SmartImage** (Media) — Image with explicit loading, loaded, fallback, and error lifecycle
2671
+
2672
+ ## SmartImage
2673
+
2674
+ ```tsx
2675
+ import { SmartImage } from '@thewhileloop/whileui';
2676
+
2677
+ <SmartImage
2678
+ source={{ uri: imageUrl }}
2679
+ fallbackSource={{ uri: fallbackUrl }}
2680
+ alt="Product preview"
2681
+ className="h-48 w-full"
2682
+ />;
2683
+ ```
2684
+
2685
+
2686
+ | Prop | Type | Default | Description |
2687
+ | ----------------- | -------------------------------------------------- | --------- | ----------- |
2688
+ | className | `string` | — | |
2689
+ | source (required) | `ImageProps['source']` | — | |
2690
+ | alt (required) | `string` | — | |
2691
+ | skeleton | `boolean` | `true` | |
2692
+ | skeletonClassName | `string` | — | |
2693
+ | transition | `number` | `200` | |
2694
+ | onLoad | `() => void` | — | |
2695
+ | onError | `() => void` | — | |
2696
+ | fallbackSource | `ImageProps['source']` | — | |
2697
+ | radius | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | |
2698
+ | objectFit | `'fill' \| 'cover' \| 'contain' \| 'scale-down'` | `'cover'` | |
2699
+
2700
+ Also accepts the props of `ViewProps`.
2701
+
2702
+ The skeleton returns whenever the primary image starts loading again. A failed primary
2703
+ source switches to `fallbackSource` when supplied without re-entering a primary/fallback
2704
+ loop.
2705
+
2706
+ ---
2707
+
2708
+ - **DatePickerModal** (Date Picker) — Bottom sheet modal with calendar, compact trigger
2709
+
2710
+ ## DatePickerModal / DatePickerInline / DateRangePickerModal / TimePickerModal
2711
+
2712
+ 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.
2713
+
2714
+ **DatePickerModal** — Compact trigger opens bottom sheet with calendar. Use `DatePickerTrigger` as the trigger content.
2715
+
2716
+ **DatePickerInline** — Calendar embedded inline for forms or dashboards.
2717
+
2718
+ **DateRangePickerModal** — Range selection with period marking. Use `DateRangePickerTrigger` as the trigger content.
2719
+
2720
+ **TimePickerModal** — Hour and minute wheels (plus AM/PM under a 12-hour `hourCycle`) in the same bottom sheet, so a form's date and time fields open the same way. `value` is always `HH:MM` in 24-hour form. Each wheel is one `adjustable` element for assistive tech and the keyboard. Use `TimePickerTrigger` as the trigger content.
2721
+
2722
+ ```tsx
2723
+ import {
2724
+ DatePickerModal,
2725
+ DatePickerTrigger,
2726
+ DatePickerInline,
2727
+ DateRangePickerModal,
2728
+ DateRangePickerTrigger,
2729
+ TimePickerModal,
2730
+ TimePickerTrigger,
2731
+ type DateRange,
2732
+ } from '@thewhileloop/whileui';
2733
+
2734
+ // Single date (modal)
2735
+ const [date, setDate] = useState<string | null>(null);
2736
+ const [open, setOpen] = useState(false);
2737
+
2738
+ <DatePickerModal
2739
+ value={date}
2740
+ onValueChange={setDate}
2741
+ open={open}
2742
+ onOpenChange={setOpen}
2743
+ trigger={<DatePickerTrigger value={date} placeholder="Pick a date" />}
2744
+ title="Select date"
2745
+ />;
2746
+
2747
+ // Inline calendar
2748
+ <DatePickerInline value={date} onValueChange={setDate} />;
2749
+
2750
+ // Date range (modal)
2751
+ const [range, setRange] = useState<DateRange | null>(null);
2752
+
2753
+ <DateRangePickerModal
2754
+ value={range}
2755
+ onValueChange={setRange}
2756
+ open={rangeOpen}
2757
+ onOpenChange={setRangeOpen}
2758
+ trigger={<DateRangePickerTrigger value={range} placeholder="Pick range" />}
2759
+ />;
2760
+
2761
+ // Time (modal) — "HH:MM", 24-hour, whatever the wheels show
2762
+ const [time, setTime] = useState<string | null>(null);
2763
+
2764
+ <TimePickerModal
2765
+ value={time}
2766
+ onValueChange={setTime}
2767
+ open={timeOpen}
2768
+ onOpenChange={setTimeOpen}
2769
+ minuteStep={5}
2770
+ trigger={<TimePickerTrigger value={time} placeholder="Pick a time" />}
2771
+ />;
2772
+ ```
2773
+
2774
+
2775
+ | Prop | Type | Default | Description |
2776
+ | ----------------------- | -------------------------------- | ---------------------- | --------------------------------------------------------- |
2777
+ | value | `string \| null` | — | Controlled: selected date in YYYY-MM-DD |
2778
+ | onValueChange | `(date: string \| null) => void` | — | Controlled: change handler |
2779
+ | open (required) | `boolean` | — | Modal open state |
2780
+ | onOpenChange (required) | `(open: boolean) => void` | — | Modal open change handler |
2781
+ | minDate | `string` | — | Minimum selectable date YYYY-MM-DD |
2782
+ | maxDate | `string` | — | Maximum selectable date YYYY-MM-DD |
2783
+ | theme | `CalendarTheme` | — | Override calendar theme (RN color strings) |
2784
+ | trigger | `React.ReactNode` | — | Trigger element. If not provided, no trigger is rendered. |
2785
+ | title | `string` | `'Select date'` | Title shown in modal header |
2786
+ | confirmLabel | `string` | `'Done'` | Button label for confirm |
2787
+ | className | `string` | — | |
2788
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2789
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2790
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2791
+
2792
+ ---
2793
+
2794
+ - **DatePickerInline** (Date Picker) — Inline calendar for forms or dashboards
2795
+
2796
+ Documented under DatePickerModal.
2797
+
2798
+ ---
2799
+
2800
+ - **DateRangePickerModal** (Date Picker) — Range selection modal with period marking
2801
+
2802
+ Documented under DatePickerModal.
2803
+
2804
+ ---
2805
+
2806
+ - **TimePickerModal** (Date Picker) — Hour and minute wheels in the same bottom sheet
2807
+
2808
+ Documented under DatePickerModal.