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