@thewhileloop/whileui 4.2.3 → 4.3.0

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