@thewhileloop/whileui 4.2.3 → 4.4.0

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