@thewhileloop/whileui 4.2.3 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +1271 -663
  2. package/dist/agent/catalog.json +1737 -0
  3. package/dist/agent/llms-components.txt +2808 -0
  4. package/dist/agent/llms-full.txt +2998 -0
  5. package/dist/agent/llms.txt +152 -0
  6. package/dist/blocks/chat/chat.d.ts +10 -2
  7. package/dist/blocks/chat/chat.js +1 -1
  8. package/dist/blocks/chat/index.d.ts +1 -1
  9. package/dist/blocks/commerce/feature-gate.js +2 -2
  10. package/dist/blocks/commerce/metric-card.js +1 -1
  11. package/dist/blocks/commerce/subscription-card.d.ts +7 -0
  12. package/dist/blocks/commerce/usage-bar.d.ts +4 -0
  13. package/dist/blocks/layout/app-shell.d.ts +4 -0
  14. package/dist/blocks/layout/empty-state.js +1 -1
  15. package/dist/blocks/layout/error-state.js +1 -1
  16. package/dist/blocks/layout/page-skeleton.d.ts +1 -0
  17. package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
  18. package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
  19. package/dist/blocks/layout/smart-input.js +2 -2
  20. package/dist/blocks/lists/list-item.js +1 -1
  21. package/dist/blocks/lists/notification-item.js +1 -1
  22. package/dist/blocks/media/smart-image.js +4 -2
  23. package/dist/blocks/navigation/bottom-nav.js +1 -1
  24. package/dist/blocks/navigation/drawer-menu.js +1 -1
  25. package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
  26. package/dist/blocks/navigation/tab-bar.js +1 -1
  27. package/dist/components/alert/alert.d.ts +7 -0
  28. package/dist/components/alert/alert.js +23 -4
  29. package/dist/components/button/button.d.ts +3 -0
  30. package/dist/components/button/button.js +2 -1
  31. package/dist/components/fab/fab.d.ts +12 -0
  32. package/dist/components/fab/fab.js +14 -2
  33. package/dist/components/fab/index.d.ts +1 -1
  34. package/dist/components/fab/index.js +1 -1
  35. package/dist/components/form-field/form-field.d.ts +18 -0
  36. package/dist/components/form-field/form-field.js +25 -4
  37. package/dist/components/input/input.d.ts +5 -0
  38. package/dist/components/input/input.js +4 -4
  39. package/dist/components/numeric-input/numeric-input.d.ts +3 -0
  40. package/dist/components/numeric-input/numeric-input.js +4 -4
  41. package/dist/components/otp-input/otp-input.d.ts +8 -0
  42. package/dist/components/popover/popover.js +9 -1
  43. package/dist/components/pressable/pressable.d.ts +1 -0
  44. package/dist/components/segmented-control/segmented-control.d.ts +39 -1
  45. package/dist/components/segmented-control/segmented-control.js +29 -5
  46. package/dist/components/select/select.js +7 -1
  47. package/dist/components/surface/surface.d.ts +5 -0
  48. package/dist/components/textarea/textarea.js +3 -3
  49. package/dist/components/toast/toast.d.ts +10 -10
  50. package/dist/components/toast/toast.js +53 -35
  51. package/dist/index.d.ts +2 -1
  52. package/dist/index.js +2 -1
  53. package/dist/lib/dismiss-keyboard-on-open.d.ts +16 -0
  54. package/dist/lib/dismiss-keyboard-on-open.js +22 -0
  55. package/dist/lib/frosted-surface.d.ts +3 -0
  56. package/dist/lib/recipes.d.ts +20 -0
  57. package/dist/lib/recipes.js +19 -2
  58. package/dist/lib/theme-bridge.d.ts +9 -0
  59. package/dist/lib/theme-bridge.js +3 -1
  60. package/dist/lib/theme-contract.js +12 -12
  61. package/dist/lib/theme-presets.js +9 -9
  62. package/dist/lib/theme-seeds.d.ts +40 -0
  63. package/dist/lib/theme-seeds.js +143 -0
  64. package/dist/metro.cjs +5 -4
  65. package/dist/tokens.d.ts +1 -0
  66. package/dist/tokens.js +1 -0
  67. package/package.json +3 -2
package/README.md CHANGED
@@ -1,26 +1,35 @@
1
1
  # WhileUI Native
2
2
 
3
- > Copy-paste components for React Native. You own the code.
3
+ > Accessible, themeable React Native components for iOS, Android and the web.
4
4
 
5
- Beautiful, accessible, themeable components built with [Uniwind](https://uniwind.dev) + Tailwind CSS v4.
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`](apps/showcase/catalog/templates) rather than package
10
+ exports: copy them and make them yours.
6
11
 
7
- **Requirements:** React 19.3+, React Native 0.88+, Expo 58+ (if using Expo). Supports React Native Web.
8
-
9
- WhileUI has one renderer: React Native. iOS, Android and React Native Web are all
10
- production targets — iOS and Android set the interaction baseline. See
11
- [ROADMAP.md](./ROADMAP.md) for the product boundary and extraction rules, and
12
- [PLAN.md](./PLAN.md) for the supported stack and production contracts.
12
+ **Requirements:** React 19.3+, React Native 0.88+, Expo 58+ (if using Expo). See
13
+ [ROADMAP.md](./ROADMAP.md) for what the package owns and [PLAN.md](./PLAN.md) for the
14
+ supported stack.
13
15
 
14
16
  ## See it in action
15
17
 
16
- One React Native component contract across native and web, with semantic typography,
17
- token-driven light and dark themes, accessible controls, and application-owned chat
18
- composition. The same component tree can feel airy and atmospheric or compact and
19
- editorial by changing tokens alone.
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`](apps/showcase/catalog)):
23
+
24
+ <p align="center">
25
+ <img src="./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`](apps/showcase/clayhouse)):
20
30
 
21
31
  <p align="center">
22
- <img src="./docs/assets/whileui-showcase.gif" width="300" alt="WhileUI Cosmic Aura mobile theme demonstrating light and dark modes, controls, Select, and composable chat" />
23
- <img src="./docs/assets/whileui-showcase-atelier.gif" width="300" alt="WhileUI Atelier mobile theme demonstrating compact controls, authentication forms, Select, and composable chat" />
32
+ <img src="./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" />
24
33
  </p>
25
34
 
26
35
  ## Installation
@@ -57,75 +66,19 @@ module.exports = withWhileUIMetro(config, {
57
66
  > `expo-font` and `@expo/vector-icons` on the real `react-native`. Without that, Uniwind's
58
67
  > import rewrite leaves every icon glyph silently missing on iOS and Android while web
59
68
  > renders fine. Pass `exemptFromUniwindRewrite: ['pkg']` for any other package that
60
- > imports a non-component export from `react-native`. It must be the outermost wrapper.
69
+ > imports a `react-native` export Uniwind's wrapper lacks. It must be the outermost wrapper.
61
70
 
62
- 2. **global.css** at app root:
71
+ 2. **global.css** at app root, imported from `App.tsx`:
63
72
 
64
73
  ```css
65
74
  @import 'tailwindcss';
66
75
  @import 'uniwind';
67
76
 
68
- /* Required when WhileUI is installed from npm. Tailwind does not scan
69
- node_modules, so without this its class names are never detected and every
70
- component renders unstyled. */
77
+ /* Required when installed from npm: Tailwind does not scan node_modules, so without
78
+ this line every component renders unstyled. */
71
79
  @source './node_modules/@thewhileloop/whileui/dist';
72
80
 
73
- /* WhileUI Noir theme - copy this or create your own */
74
- @layer theme {
75
- :root {
76
- @variant light {
77
- --color-background: oklch(1 0 0);
78
- --color-foreground: oklch(0.1316 0.0041 17.69);
79
- --color-card: oklch(1 0 0);
80
- --color-card-foreground: oklch(0.1316 0.0041 17.69);
81
- --color-primary: oklch(0.1316 0.0041 17.69);
82
- --color-primary-foreground: oklch(0.98 0 0);
83
- --color-secondary: oklch(0.9598 0.0017 17.69);
84
- --color-secondary-foreground: oklch(0.1316 0.0041 17.69);
85
- --color-muted: oklch(0.9598 0.0017 17.69);
86
- --color-muted-foreground: oklch(0.5415 0.0135 17.69);
87
- --color-accent: oklch(0.9598 0.0017 17.69);
88
- --color-accent-foreground: oklch(0.1316 0.0041 17.69);
89
- --color-destructive: oklch(0.5 0.19 27);
90
- --color-destructive-foreground: oklch(0.98 0 0);
91
- --color-border: oklch(0.9039 0.0034 17.69);
92
- --color-input: oklch(0.9039 0.0034 17.69);
93
- --color-ring: oklch(0.1316 0.0041 17.69);
94
- --color-success: oklch(0.59 0.16 145);
95
- --color-success-foreground: oklch(0.98 0 0);
96
- --color-warning: oklch(0.75 0.18 85);
97
- --color-warning-foreground: oklch(0.1316 0.0041 17.69);
98
- --color-info: oklch(0.65 0.15 245);
99
- --color-info-foreground: oklch(0.98 0 0);
100
- }
101
-
102
- @variant dark {
103
- --color-background: oklch(0.1316 0.0041 17.69);
104
- --color-foreground: oklch(0.98 0 0);
105
- --color-card: oklch(0.1316 0.0041 17.69);
106
- --color-card-foreground: oklch(0.98 0 0);
107
- --color-primary: oklch(0.98 0 0);
108
- --color-primary-foreground: oklch(0.1316 0.0041 17.69);
109
- --color-secondary: oklch(0.2104 0.0084 17.69);
110
- --color-secondary-foreground: oklch(0.98 0 0);
111
- --color-muted: oklch(0.2104 0.0084 17.69);
112
- --color-muted-foreground: oklch(0.6961 0.0174 17.69);
113
- --color-accent: oklch(0.2104 0.0084 17.69);
114
- --color-accent-foreground: oklch(0.98 0 0);
115
- --color-destructive: oklch(0.45 0.18 27);
116
- --color-destructive-foreground: oklch(0.98 0 0);
117
- --color-border: oklch(0.2104 0.0084 17.69);
118
- --color-input: oklch(0.2104 0.0084 17.69);
119
- --color-ring: oklch(0.8267 0.0206 17.69);
120
- --color-success: oklch(0.59 0.16 145);
121
- --color-success-foreground: oklch(0.98 0 0);
122
- --color-warning: oklch(0.75 0.18 85);
123
- --color-warning-foreground: oklch(0.1316 0.0041 17.69);
124
- --color-info: oklch(0.65 0.15 245);
125
- --color-info-foreground: oklch(0.98 0 0);
126
- }
127
- }
128
- }
81
+ /* Then your theme: generate it (Theming, below) rather than writing tokens by hand. */
129
82
  ```
130
83
 
131
84
  3. **App.tsx**:
@@ -145,13 +98,11 @@ export default function App() {
145
98
  }
146
99
  ```
147
100
 
148
- > **Note:** If you use Select, Popover, Tooltip, or HoverCard, add `<PortalHost />` at the root of your app (as the last child).
149
-
150
- Inside a Dialog, AlertDialog, Sheet, ConfirmActionSheet, DrawerMenu, menu or picker, those
151
- four render into that overlay's own layer automatically, so a Select in a sheet opens above
152
- the sheet. Android back and Escape on web close the innermost open layer first. Pass
153
- `portalHost` on their content only to target a `PortalHost` you placed yourself, such as
154
- inside a navigation-presented modal screen that WhileUI does not host.
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.
155
106
 
156
107
  ### Vite + React Native Web + WhileUI setup
157
108
 
@@ -215,36 +166,37 @@ function MyScreen() {
215
166
  }
216
167
  ```
217
168
 
218
- ## Blocks Strategy: Core vs Templates
169
+ ## AI agents
219
170
 
220
- **Core package** exports:
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:
221
173
 
222
- - **Primitives** — Button, Input, Card, Text, etc.
223
- - **Generic layout blocks** — EmptyState, ErrorState, LoadingScreen, ContentSkeleton, PageSkeleton, ScreenSkeleton
224
- - **Layout infrastructure** — FormModalScreen, ConfirmActionSheet, SmartInput, ActionBar
225
- - **Navigation, Chat, Lists, Commerce, Media, DatePicker** — Blocks that rarely need app-specific customization
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`](packages/mcp/README.md) serves the same catalog over MCP
178
+ (`list_components`, `get_component_docs`, `get_theme_tokens`, `get_guide`):
226
179
 
227
- **Opinionated blocks** (auth, profile) are **copy-paste templates** in the showcase app:
180
+ ```bash
181
+ claude mcp add whileui -- npx -y @thewhileloop/whileui-mcp
182
+ ```
228
183
 
229
- - **Auth:** `apps/showcase/templates/auth/` — SignInForm, SignUpForm, ForgotPasswordForm, ResetPasswordForm, VerifyEmailForm, SocialConnections, UserMenu
230
- - **Profile:** `apps/showcase/templates/profile/` — ProfileHeader, SettingsSection, SettingsItem, AccountCard
184
+ - A [skill](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/`).
231
190
 
232
- **To use auth or profile blocks:** Copy the template file(s) from `apps/showcase/templates/` into your app. Customize as needed (error handling, loading state, social auth slot, branding). Each template imports primitives from `@thewhileloop/whileui`.
233
-
234
- ## Philosophy
235
-
236
- - **Copy-Paste Ownership** — Components live in _your_ project. No `node_modules` lock-in.
237
- - **Beautiful by Default** — OKLCH color system, light/dark themes, polished out of the box.
238
- - **Fully Customizable** — Every component uses `tv()` variants and accepts `className` overrides.
239
- - **Accessible** — Proper ARIA roles, keyboard support, controlled/uncontrolled state.
240
- - **Tree-Shakeable** — Only imports what you use. `sideEffects: false`.
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.
241
193
 
242
194
  ## Quick Reference (AI / Code Generation)
243
195
 
244
196
  - **Full-screen:** `AppShell` + `Header` in `header` + `BottomNav` in `bottomNav` + content in `children`
245
197
  - **No-jump loading:** keep `AppShell` mounted, set `loading` + `skeleton` (e.g., `PageSkeleton`/`ScreenSkeleton`)
246
198
  - **Layout:** `Stack` (vertical), `Row` (horizontal) — both support `gap`, `align`, `justify`
247
- - **Auth callbacks:** Auth templates use objects: `onSubmit({ email, password })`, `onSubmit({ firstName, lastName, email, password })`, etc. Copy templates from `apps/showcase/templates/auth/`.
199
+ - **Auth callbacks:** Auth templates use objects: `onSubmit({ email, password })`, `onSubmit({ firstName, lastName, email, password })`, etc. Copy templates from `apps/showcase/catalog/templates/auth/`.
248
200
  - **PortalHost:** Add `<PortalHost />` at app root for Select, Popover, Tooltip, HoverCard.
249
201
  Inside any WhileUI overlay they render into that overlay's layer without configuration.
250
202
  - **Motion:** Wrap the root in `<MotionProvider>`; use `useMotionPolicy()` for semantic
@@ -255,7 +207,7 @@ function MyScreen() {
255
207
  `@source './node_modules/@thewhileloop/whileui/dist';` or every component renders unstyled.
256
208
  - **States:** `LoadingState`, `EmptyState` and `ErrorState` share one shape; `ErrorState`
257
209
  takes `onRetry`. `Text variant="eyebrow"` is the muted section label above a title.
258
- - **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
210
+ - **Reference:** Block props in `packages/ui/src/blocks` (core) and `apps/showcase/catalog/templates/` (auth, profile); flow patterns in README "Flow Patterns" section.
259
211
 
260
212
  ### Composition grammar
261
213
 
@@ -360,7 +312,7 @@ function MyScreen() {
360
312
 
361
313
  ### Auth (Copy from showcase templates)
362
314
 
363
- Copy from `apps/showcase/templates/auth/`:
315
+ Copy from `apps/showcase/catalog/templates/auth/`:
364
316
 
365
317
  | Block | File | Description |
366
318
  | ---------------------- | -------------------------- | ------------------------------------- |
@@ -418,7 +370,7 @@ Copy from `apps/showcase/templates/auth/`:
418
370
 
419
371
  ### Profile & Settings (Copy from showcase templates)
420
372
 
421
- Copy from `apps/showcase/templates/profile/`:
373
+ Copy from `apps/showcase/catalog/templates/profile/`:
422
374
 
423
375
  | Block | File | Description |
424
376
  | ------------------- | ---------------------- | ----------------------------------- |
@@ -525,6 +477,10 @@ const icons = useIconColors();
525
477
  `FABGroup` stacks secondary actions above one. Sizes start at 48dp, so a FAB never
526
478
  needs `hitSlop`.
527
479
 
480
+ A floating FAB covers whatever scrolls under it, so a list behind one ends with room for
481
+ it: `contentContainerStyle={{ paddingBottom: fabClearance() }}`, passing the FAB's `size`
482
+ when it is not the default.
483
+
528
484
  ## Layout Primitives (Stack, Row)
529
485
 
530
486
  Use `Stack` for vertical layouts, `Row` for horizontal layouts. Both support `gap`, `align`, and `justify` variants.
@@ -545,11 +501,17 @@ import { Stack, Row } from '@thewhileloop/whileui';
545
501
  </Stack>;
546
502
  ```
547
503
 
548
- | Prop | Stack/Row | Values |
549
- | ------- | ------------------------------------------------------------------- | ------------------------ |
550
- | gap | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | Spacing between children |
551
- | align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | Cross-axis alignment |
552
- | justify | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | Main-axis alignment |
504
+ <!-- api:Row -->
505
+
506
+ | Prop | Type | Default | Description |
507
+ | --------- | ------------------------------------------------------------------- | ---------- | ----------- |
508
+ | className | `string` | — | |
509
+ | gap | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | `'md'` | |
510
+ | align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | `'center'` | |
511
+ | justify | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | `'start'` | |
512
+
513
+ Also accepts the props of `ViewProps`.
514
+ <!-- /api:Row -->
553
515
 
554
516
  ### Container
555
517
 
@@ -561,11 +523,16 @@ controls drift apart.
561
523
  <Container size="lg">{children}</Container>
562
524
  ```
563
525
 
564
- | Prop | Type | Default | Notes |
565
- | ----------- | -------------------------------------- | ------- | --------------------------- |
566
- | `size` | `sm` \| `md` \| `lg` \| `xl` \| `full` | `md` | `sm` forms, `lg` dashboards |
567
- | `fill` | `boolean` | `false` | `flex-1`, for a screen root |
568
- | `className` | `string` | — | Merged after the variant |
526
+ <!-- api:Container -->
527
+
528
+ | Prop | Type | Default | Description |
529
+ | --------- | ---------------------------------------- | ------- | ----------- |
530
+ | className | `string` | — | |
531
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | |
532
+ | fill | `boolean` | `false` | |
533
+
534
+ Also accepts the props of `ViewProps`.
535
+ <!-- /api:Container -->
569
536
 
570
537
  No platform branch, and none is needed: every size is wider than any phone, so on iOS
571
538
  and Android the cap never engages and it renders as a plain full-width `View`. The same
@@ -630,142 +597,66 @@ Blocks with `loading` support: ProductCard, ListItem, NotificationItem, MetricCa
630
597
 
631
598
  Block props: see TypeScript interfaces in `packages/ui/src/blocks`.
632
599
 
633
- ## Quick Start
634
-
635
- ```bash
636
- bun install
637
- cd apps/showcase
638
- bun run dev
639
- # Or: npx expo start --web
640
- ```
641
-
642
- ## Project Structure
643
-
644
- ```
645
- whileui/
646
- ├── packages/
647
- │ └── ui/
648
- │ └── src/
649
- │ ├── components/ # All components (copy these!)
650
- │ │ ├── button/
651
- │ │ ├── card/
652
- │ │ ├── form-field/
653
- │ │ ├── numeric-input/
654
- │ │ ├── segmented-control/
655
- │ │ ├── data-row/
656
- │ │ ├── dialog/
657
- │ │ └── ...
658
- │ ├── blocks/ # Pre-built screens (core only)
659
- │ │ ├── chat/
660
- │ │ ├── navigation/
661
- │ │ ├── layout/
662
- │ │ ├── lists/
663
- │ │ ├── commerce/
664
- │ │ ├── splash/
665
- │ │ └── media/
666
- │ ├── lib/ # Utilities
667
- │ │ ├── cn.ts # clsx + tailwind-merge
668
- │ │ ├── tv.ts # tailwind-variants re-export
669
- │ │ ├── font-context.ts
670
- │ │ └── theme-bridge.ts
671
- │ └── index.ts # Barrel export
672
- ├── apps/
673
- │ └── showcase/ # Expo demo app
674
- │ ├── templates/ # Copy-paste templates (auth, profile)
675
- │ │ ├── auth/
676
- │ │ └── profile/
677
- │ ├── App.tsx # Component showcase
678
- │ ├── global.css # Theme variables (OKLCH) — at app root!
679
- │ └── metro.config.js # Uniwind + monorepo config
680
- └── package.json # bun monorepo root
681
- ```
682
-
683
600
  ## Theming
684
601
 
685
- Themes are defined in `global.css` using CSS variables with OKLCH colors:
686
-
687
- ```css
688
- @variant light {
689
- --color-primary: oklch(0.6 0.2 160);
690
- --color-background: oklch(1 0 0);
691
- /* ... */
692
- }
693
-
694
- @variant dark {
695
- --color-primary: oklch(0.6 0.2 160);
696
- --color-background: oklch(0.145 0 0);
697
- /* ... */
698
- }
699
- ```
700
-
701
- ### Strict Theme Token Contract
702
-
703
- The WhileUI token contract is strict for cross-app reuse. Define these in **every** theme variant (`@variant light`, `@variant dark`, and custom variants):
704
-
705
- - Required core tokens: `background`, `foreground`, `card`, `card-foreground`, `popover`, `popover-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `border`, `input`, `ring`
706
- - Optional status tokens: `success`, `success-foreground`, `warning`, `warning-foreground`, `info`, `info-foreground`
707
- - Optional effect tokens: `overlay`, `overlay-strong`, `surface-elevated`, `surface-border`, `surface-highlight`, `surface-translucent`, `surface-translucent-border`, `state-hover`, `state-pressed`, `state-disabled`
708
- - Optional visual tokens, generated from a preset: `--ui-blur-intensity-subtle`, `--ui-blur-intensity-medium`, `--ui-blur-intensity-strong`, `--ui-blur-saturation-pct`, `--ui-frosted-highlight-height`, `--ui-frosted-backdrop-blur-intensity`, `--ui-frosted-backdrop-blur-scale`, `--ui-frosted-android-tint-alpha-scale`, `--ui-frosted-android-experimental-blur`, `--ui-drawer-frosted-inset`, `--ui-drawer-frosted-radius`, `--ui-drawer-content-top-padding`
709
- - Optional scale tokens: spacing (`--spacing`, `--spacing-*`), typography (`--text-*`, `--leading-*`, `--tracking-*`), radius (`--radius-*`), elevation (`--shadow-*`)
602
+ A theme is a set of CSS variables in `global.css`, in OKLCH, with a `light` and a `dark`
603
+ variant. Every component reads the same contract, so a theme must define **every** token
604
+ in both modes: a missing one silently falls back to an unstyled value at runtime.
710
605
 
711
- Minimal contract example:
606
+ ### Generate a theme
712
607
 
713
- ```css
714
- @layer theme {
715
- :root {
716
- @variant light {
717
- --color-background: oklch(1 0 0);
718
- --color-foreground: oklch(0.15 0 0);
719
- --color-card: oklch(1 0 0);
720
- --color-card-foreground: oklch(0.15 0 0);
721
- --color-popover: oklch(1 0 0);
722
- --color-popover-foreground: oklch(0.15 0 0);
723
- --color-primary: oklch(0.2 0 0);
724
- --color-primary-foreground: oklch(0.98 0 0);
725
- --color-secondary: oklch(0.95 0 0);
726
- --color-secondary-foreground: oklch(0.15 0 0);
727
- --color-muted: oklch(0.95 0 0);
728
- --color-muted-foreground: oklch(0.45 0 0);
729
- --color-accent: oklch(0.9 0.05 180);
730
- --color-accent-foreground: oklch(0.15 0 0);
731
- --color-destructive: oklch(0.58 0.2 26);
732
- --color-destructive-foreground: oklch(0.98 0 0);
733
- --color-border: oklch(0.9 0 0);
734
- --color-input: oklch(0.92 0 0);
735
- --color-ring: oklch(0.22 0 0);
736
- }
737
- }
738
- }
739
- ```
740
-
741
- Switch themes at runtime via Uniwind:
608
+ `themeFromSeeds` builds a complete light and dark theme from three colours, and
609
+ `themePresetToCss` writes every token, including the hex fallbacks React Native APIs need,
610
+ ready to paste into `global.css`:
742
611
 
743
- ```tsx
744
- import { Uniwind } from 'uniwind';
612
+ ```ts
613
+ import { checkThemeContrast, themeFromSeeds, themePresetToCss } from '@thewhileloop/whileui/tokens';
614
+
615
+ const theme = themeFromSeeds({
616
+ name: 'studio',
617
+ description: 'Bone paper and burnt amber',
618
+ light: { background: '#f0ede9', foreground: '#13161c', primary: '#b65400' },
619
+ // optional: `dark` seeds (otherwise a dark page with the brand lifted to read on it),
620
+ // `overrides` for any base colour, `visual` for radius and density tokens
621
+ });
745
622
 
746
- Uniwind.setTheme('dark'); // or 'light' or 'system'
623
+ checkThemeContrast(theme.light).filter((c) => !c.pass);
624
+ // [{ pair: 'primary on background', ratio: 4.23, min: 4.5, pass: false }, …]
625
+ const css = themePresetToCss(theme); // paste into global.css
747
626
  ```
748
627
 
749
- ### Frosted / Translucent Theme
750
-
751
- Some apps want a frosted or translucent look for floating panels (modals, sheets, toolbars). WhileUI stays neutral — no "glass" in core token names. Apps that want this effect opt in by overriding surface tokens in their theme.
628
+ Seeds are kept as given; the colours it derives (muted text, text on each fill, status
629
+ colours) are tuned to WCAG AA. `checkThemeContrast` checks any palette, hand-written ones
630
+ included: every text pairing at 4.5:1 and the focus ring at 3:1. A failing seed is
631
+ reported, never changed, because a brand colour is a brand decision. The built-in presets,
632
+ `WHILEUI_PRESET_SOFT` and `WHILEUI_PRESET_ATELIER`, go through `themePresetToCss` the same
633
+ way.
752
634
 
753
- **Option A — Override in existing light/dark:** In your `@variant light` and `@variant dark` blocks, set surface tokens to semi-transparent values:
635
+ Switch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`), or let
636
+ `useThemeBridge` hold the choice. It returns the chosen `mode`, the `resolvedTheme` on
637
+ screen, `setMode`, `toggleTheme` and `cycleMode`. A sun/moon button calls `toggleTheme`,
638
+ which always flips what is on screen; `cycleMode` steps light → dark → system and suits
639
+ a control that shows all three, because a step into `system` that resolves to the
640
+ current theme looks like a dead tap. Pass an `adapter` to keep the choice across launches:
754
641
 
755
- ```css
756
- @variant light {
757
- /* ... other tokens ... */
758
- --color-surface-elevated: oklch(0.98 0.01 95 / 0.4);
759
- --color-surface-border: oklch(1 0 0 / 0.25);
760
- --color-surface-highlight: oklch(1 0 0 / 0.4);
761
- }
642
+ ```tsx
643
+ const { resolvedTheme, toggleTheme } = useThemeBridge({
644
+ adapter: {
645
+ loadThemeMode: () => storage.getItem('theme') as ThemeMode | null,
646
+ saveThemeMode: (mode) => storage.setItem('theme', mode),
647
+ },
648
+ });
762
649
  ```
763
650
 
764
- **Option B — Separate frosted theme:** Register `extraThemes: ['frosted']` and define `:root.frosted` with the same structure as your base theme, but with translucent surface values. Then `Uniwind.setTheme('frosted')` when desired.
651
+ A named extra theme is registered with `extraThemes: ['name']` in `metro.config.js` and
652
+ defined with `@variant name`. The full token list, with each default, is in `theme-contract.ts`,
653
+ and agents can read it from the MCP server's `get_theme_tokens`.
765
654
 
766
- **Optional:** Add `expo-blur` and register its `BlurView` once at app startup for full frosted blur. For tint-only (no blur), translucent surface tokens are sufficient.
655
+ ### Frosted surfaces
767
656
 
768
- **Android note:** frosted panels usually need denser tint than iOS/web. WhileUI now scales frosted tint alpha on Android with `--ui-frosted-android-tint-alpha-scale` (default `1.18`). Set it to `1` to match other platforms exactly, or raise it if your Android surfaces still feel washed out.
657
+ Cards and overlays take `frosted`, `blurIntensity` and `blurTintToken`
658
+ (`'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover'`). Without a blur view
659
+ they tint only; register `expo-blur` once for real blur:
769
660
 
770
661
  ```tsx
771
662
  import { BlurView } from 'expo-blur';
@@ -774,62 +665,40 @@ import { registerFrostedBlurView } from '@thewhileloop/whileui';
774
665
  registerFrostedBlurView(BlurView);
775
666
  ```
776
667
 
777
- Optional generic tokens: `surface-translucent`, `surface-translucent-border` — use them if you need a distinct token from `surface-elevated` for overlay panels.
778
-
779
- Built-in overlays and cards support opt-in frosted mode via:
780
-
781
- - `frosted?: boolean`
782
- - `blurIntensity?: number`
783
- - `blurTintToken?: 'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover'`
784
-
785
- Default blur presets map to CSS visual tokens:
668
+ For a frosted look across a whole theme, make `surface-elevated`, `surface-border` and
669
+ `surface-highlight` translucent, in your light and dark variants or in a separate theme.
670
+ The tuning tokens:
786
671
 
787
- - subtle: `--ui-blur-intensity-subtle` (default `18`)
788
- - medium: `--ui-blur-intensity-medium` (default `22`)
789
- - strong: `--ui-blur-intensity-strong` (default `28`)
672
+ | Token | Default | Effect |
673
+ | ------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------- |
674
+ | `--ui-blur-intensity-subtle` / `-medium` / `-strong` | 18 / 26 / 34 | The three blur presets |
675
+ | `--ui-frosted-android-tint-alpha-scale` | 1.18 | Android needs denser tint; `1` matches iOS and web |
676
+ | `--ui-frosted-android-experimental-blur` | 1 | Uses `expo-blur`'s experimental Android blur; `0` turns it off |
677
+ | `--ui-frosted-highlight-height` | 2 | Top sheen in px; `0` removes it |
678
+ | `--ui-blur-saturation-pct` | 145 | Web saturation multiplier |
679
+ | `--ui-frosted-backdrop-blur-intensity`, `-scale` | 22, 0.72 | Backdrop blur, and its ratio when a component overrides blur |
680
+ | `--ui-drawer-frosted-inset`, `-radius`, `--ui-drawer-content-top-padding` | 10, 34, 0 | Frosted drawer shell |
790
681
 
791
- Additional frosted tuning tokens:
682
+ ### Colours for React Native APIs
792
683
 
793
- - `--ui-blur-saturation-pct` (web fallback saturation multiplier)
794
- - `--ui-frosted-highlight-height` (top highlight strip height in px, set `0` to disable hard top sheen)
795
- - `--ui-frosted-backdrop-blur-intensity` (default backdrop blur amount)
796
- - `--ui-frosted-backdrop-blur-scale` (ratio used when component blur is overridden)
797
- - `--ui-frosted-android-tint-alpha-scale` (Android-only alpha multiplier applied to frosted tint and backdrop layers)
798
- - `--ui-frosted-android-experimental-blur` (`1` enables `expo-blur` Android experimental path)
799
- - `--ui-drawer-frosted-inset` (floating inset for frosted drawer shells)
800
- - `--ui-drawer-frosted-radius` (drawer corner radius in px)
801
- - `--ui-drawer-content-top-padding` (drawer content top spacing baseline)
802
-
803
- ### Theme Colors for RN Primitives
804
-
805
- Some React Native APIs require native color strings (hex/rgb/hsl/rgba/named). Use `useThemeColors` or `useIconColors` to read from your `global.css` theme. If your theme uses `oklch(...)`, add `--app-color-*` fallbacks — `useThemeColors` will use them when `--color-*` is not RN-native.
684
+ Icons, `ActivityIndicator` and other native APIs need hex or rgb, not OKLCH.
685
+ `useThemeColors()` returns the whole palette as React Native colours, falling back to the
686
+ `--app-color-*` tokens a generated theme includes; `useIconColors()` is the subset icons
687
+ use, with `muted` mapped to `mutedForeground`.
806
688
 
807
689
  ```tsx
808
- import { useThemeColors, useIconColors } from '@thewhileloop/whileui';
809
- import { Feather } from '@expo/vector-icons';
810
-
811
- // Full palette (core semantic colors + status + overlay/effect tokens)
812
- const colors = useThemeColors();
813
-
814
- // Shorthand for icons: foreground, muted, primary, primaryForeground, accent, destructive
815
- const iconColors = useIconColors();
816
-
817
- <Feather name="heart" size={20} color={iconColors.muted} />
818
- <Spinner color={colors.foreground} /> // Spinner defaults to this when color not passed
690
+ const icons = useIconColors();
691
+ <Feather name="heart" size={20} color={icons.muted} />;
819
692
  ```
820
693
 
821
- - **useThemeColors** / **useThemeTokens** — Returns RN-safe color strings (hex/rgb/rgba) for semantic tokens (`background`, `card`, `popover`, `primary`, `secondary`, `muted`, `accent`, `destructive`, status colors) plus effect tokens (`overlay`, `overlayStrong`, `surfaceElevated`, `surfaceTranslucent`, `surfaceTranslucentBorder`, etc.). Falls back to `--app-color-*` when `--color-*` is missing/non-RN-native.
822
- - **useIconColors** — Subset for icons. Maps `muted` → `mutedForeground` (readable on backgrounds).
823
-
824
- Input, Textarea, NumericInput, SmartInput, Spinner, and LoadingScreen default to theme colors when you omit `placeholderTextColor` or `spinnerColor`.
694
+ Input, Textarea, NumericInput, SmartInput, Spinner and LoadingScreen pick theme colours
695
+ when you omit `placeholderTextColor` or `spinnerColor`.
825
696
 
826
697
  ### Interaction and motion
827
698
 
828
699
  Press opacity, disabled opacity, motion durations, the responsive spring, and the drawer
829
700
  open and close durations are package constants on `INTERACTION_TOKENS`. They are not CSS
830
- variables and they do not change with a preset. Delete any `--ui-press-*`,
831
- `--ui-disabled-*`, `--ui-inactive-opacity`, `--ui-motion-*`, or `--ui-drawer-open-duration`
832
- / `--ui-drawer-close-duration` declarations from a theme: nothing reads them.
701
+ variables, so a theme does not change them.
833
702
 
834
703
  For a custom control, read that table:
835
704
 
@@ -853,118 +722,6 @@ motion.durations.normal;
853
722
  motion.springs.responsive;
854
723
  ```
855
724
 
856
- Optional RN fallback tokens (hex/rgb/hsl/named):
857
-
858
- ```css
859
- @layer theme {
860
- :root {
861
- @variant light {
862
- --app-color-primary: #000000;
863
- --app-color-primary-foreground: #ffffff;
864
- --app-color-foreground: #000000;
865
- --app-color-background: #ffffff;
866
- --app-color-card: #ffffff;
867
- --app-color-card-foreground: #000000;
868
- --app-color-popover: #ffffff;
869
- --app-color-popover-foreground: #000000;
870
- --app-color-secondary: #f5f5f5;
871
- --app-color-secondary-foreground: #171717;
872
- --app-color-muted: #f5f5f5;
873
- --app-color-muted-foreground: #737373;
874
- --app-color-border: #e5e5e5;
875
- --app-color-input: #e5e5e5;
876
- --app-color-ring: #94a3b8;
877
- --app-color-accent: #22c55e;
878
- --app-color-accent-foreground: #0a0a0a;
879
- --app-color-destructive: #dc2626;
880
- --app-color-destructive-foreground: #ffffff;
881
- --app-color-success: #16a34a;
882
- --app-color-success-foreground: #ffffff;
883
- --app-color-warning: #f59e0b;
884
- --app-color-warning-foreground: #111827;
885
- --app-color-info: #3b82f6;
886
- --app-color-info-foreground: #ffffff;
887
- --app-color-overlay: rgba(0, 0, 0, 0.4);
888
- --app-color-overlay-strong: rgba(0, 0, 0, 0.55);
889
- --app-color-surface-elevated: #ffffff;
890
- --app-color-surface-translucent: rgba(255, 255, 255, 0.5);
891
- --app-color-surface-translucent-border: rgba(255, 255, 255, 0.18);
892
- --app-color-surface-border: rgba(255, 255, 255, 0.3);
893
- --app-color-surface-highlight: rgba(255, 255, 255, 0.3);
894
- --app-color-state-hover: rgba(0, 0, 0, 0.05);
895
- --app-color-state-pressed: rgba(0, 0, 0, 0.12);
896
- --app-color-state-disabled: rgba(0, 0, 0, 0.4);
897
- }
898
- @variant dark {
899
- --app-color-primary: #ffffff;
900
- --app-color-primary-foreground: #000000;
901
- --app-color-foreground: #ffffff;
902
- --app-color-background: #000000;
903
- --app-color-card: #0f0f10;
904
- --app-color-card-foreground: #ffffff;
905
- --app-color-popover: #121214;
906
- --app-color-popover-foreground: #ffffff;
907
- --app-color-secondary: #2e2e2e;
908
- --app-color-secondary-foreground: #ffffff;
909
- --app-color-muted: #2e2e2e;
910
- --app-color-muted-foreground: #999999;
911
- --app-color-border: #3d3d3d;
912
- --app-color-input: #3d3d3d;
913
- --app-color-ring: #7c889a;
914
- --app-color-accent: #22c55e;
915
- --app-color-accent-foreground: #0a0a0a;
916
- --app-color-destructive: #dc2626;
917
- --app-color-destructive-foreground: #ffffff;
918
- --app-color-success: #22c55e;
919
- --app-color-success-foreground: #0a0a0a;
920
- --app-color-warning: #fbbf24;
921
- --app-color-warning-foreground: #111827;
922
- --app-color-info: #60a5fa;
923
- --app-color-info-foreground: #111827;
924
- --app-color-overlay: rgba(0, 0, 0, 0.5);
925
- --app-color-overlay-strong: rgba(0, 0, 0, 0.7);
926
- --app-color-surface-elevated: #111315;
927
- --app-color-surface-translucent: rgba(17, 19, 21, 0.45);
928
- --app-color-surface-translucent-border: rgba(255, 255, 255, 0.18);
929
- --app-color-surface-border: rgba(255, 255, 255, 0.18);
930
- --app-color-surface-highlight: rgba(255, 255, 255, 0.18);
931
- --app-color-state-hover: rgba(255, 255, 255, 0.08);
932
- --app-color-state-pressed: rgba(255, 255, 255, 0.15);
933
- --app-color-state-disabled: rgba(255, 255, 255, 0.4);
934
- }
935
- }
936
- }
937
- ```
938
-
939
- Or use the first-party theme bridge with optional persistence. The hook owns theme
940
- state and exposes event-driven setters:
941
-
942
- ```tsx
943
- import { useThemeBridge, type ThemeBridgeAdapter } from '@thewhileloop/whileui';
944
-
945
- const adapter: ThemeBridgeAdapter = {
946
- loadThemeMode: async () => 'system',
947
- saveThemeMode: async (mode) => {
948
- await storage.setItem('theme-mode', mode);
949
- },
950
- };
951
-
952
- const { mode, resolvedTheme, setMode, cycleMode } = useThemeBridge({ adapter });
953
- ```
954
-
955
- The bridge loads a new adapter before saving into it. If a user changes the mode while
956
- storage is still loading, that explicit choice wins over the late persisted value.
957
-
958
- When another store already owns the mode, mount the controlled bridge once near the
959
- application root. Change `mode` in the owning event handler; the bridge applies and
960
- persists it without mirroring state back through a callback effect.
961
-
962
- ```tsx
963
- import { ThemeBridge } from '@thewhileloop/whileui';
964
-
965
- <ThemeBridge mode={mode} adapter={adapter} />;
966
- ```
967
-
968
725
  ## Focus rings
969
726
 
970
727
  Every interactive component draws a themed focus ring on web. It uses `:focus-visible`,
@@ -992,14 +749,6 @@ supplies a themed replacement in the same class. The one exemption is a visually
992
749
  input, where a visible proxy shows the focus instead — `OTPInput` does this with its
993
750
  blinking cursor.
994
751
 
995
- ## Tech Stack
996
-
997
- - **Styling**: [Uniwind](https://uniwind.dev) (free tier) + Tailwind CSS v4
998
- - **Variants**: [tailwind-variants](https://www.tailwind-variants.org/) (`tv()`)
999
- - **Merging**: [clsx](https://github.com/lukeed/clsx) + [tailwind-merge](https://github.com/dcastil/tailwind-merge)
1000
- - **Expo**: SDK 54
1001
- - **Monorepo**: bun + Turborepo
1002
-
1003
752
  # API Reference
1004
753
 
1005
754
  ## Pressable
@@ -1022,12 +771,17 @@ import { Pressable, Text } from '@thewhileloop/whileui';
1022
771
  `style` contracts. Feedback changes opacity without changing layout. Native press state
1023
772
  comes directly from React Native; web adds a motion-policy-aware opacity transition.
1024
773
 
1025
- | Prop | Type | Default | Description |
1026
- | ---------------- | ------------------------------------------- | ----------- | ------------------------------------------ |
1027
- | feedback | `'none' \| 'default' \| 'strong'` | `'none'` | Semantic pressed opacity |
1028
- | disabledFeedback | `'none' \| 'default' \| 'soft' \| 'subtle'` | `'default'` | Semantic unavailable opacity |
1029
- | loading | `boolean` | `false` | Expose busy/disabled state and block input |
1030
- | className | `string` | — | Additional Tailwind classes |
774
+ <!-- api:Pressable -->
775
+
776
+ | Prop | Type | Default | Description |
777
+ | ---------------- | ----------------- | ----------- | --------------------------------------------------------------------------- |
778
+ | className | `string` | — | Additional Tailwind classes. |
779
+ | feedback | `PressedVariant` | `'none'` | Semantic press feedback. `none` preserves a plain React Native Pressable. |
780
+ | disabledFeedback | `DisabledVariant` | `'default'` | Opacity used while the control is unavailable. |
781
+ | loading | `boolean` | `false` | Loading controls are busy and non-interactive, just like disabled controls. |
782
+
783
+ Also accepts the props of React Native's `PressableProps`.
784
+ <!-- /api:Pressable -->
1031
785
 
1032
786
  ## Button
1033
787
 
@@ -1046,13 +800,20 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
1046
800
  </Button>;
1047
801
  ```
1048
802
 
1049
- | Prop | Type | Default | Description |
1050
- | --------- | ----------------------------------------------------------------------------- | ----------- | -------------------------------------------------- |
1051
- | variant | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'` | Button style variant |
1052
- | size | `'default' \| 'sm' \| 'lg' \| 'icon'` | `'default'` | Button size |
1053
- | disabled | `boolean` | `false` | Disable the button |
1054
- | loading | `boolean` | `false` | Show a spinner, expose busy state, and block input |
1055
- | className | `string` | — | Additional Tailwind classes |
803
+ <!-- api:Button -->
804
+
805
+ | Prop | Type | Default | Description |
806
+ | ---------------- | ----------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- |
807
+ | children | `React.ReactNode` | — | |
808
+ | className | `string` | — | Additional Tailwind classes. |
809
+ | disabled | `boolean` | `false` | Disable the button. |
810
+ | loading | `boolean` | `false` | Shows a centered spinner, exposes busy state, and prevents interaction. |
811
+ | disabledFeedback | `DisabledVariant` | — | Opacity used while the control is unavailable. |
812
+ | size | `'sm' \| 'lg' \| 'default' \| 'icon'` | `'default'` | |
813
+ | variant | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'` | |
814
+
815
+ Also accepts the props of `PressableProps`.
816
+ <!-- /api:Button -->
1056
817
 
1057
818
  ## Text
1058
819
 
@@ -1060,28 +821,51 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
1060
821
  classes are unnecessary for normal hierarchy. `heading` also sets
1061
822
  `accessibilityRole="header"` unless the caller supplies another role.
1062
823
 
1063
- | Prop | Type | Default |
1064
- | ------- | ---------------------------------------------------------------------------------------------------------- | ----------- |
1065
- | variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'eyebrow' \| 'code'` | `'default'` |
1066
- | size | `'xs' \| 'sm' \| 'base' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | by variant |
824
+ <!-- api:Text -->
825
+
826
+ | Prop | Type | Default | Description |
827
+ | --------- | ---------------------------------------------------------------------------------------------------------- | ----------- | ----------- |
828
+ | className | `string` | — | |
829
+ | size | `'base' \| 'xs' \| 'sm' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | — | |
830
+ | variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'eyebrow' \| 'code'` | `'default'` | |
831
+
832
+ Also accepts the props of React Native's `TextProps`.
833
+ <!-- /api:Text -->
1067
834
 
1068
835
  ## Input
1069
836
 
1070
837
  ```tsx
1071
- import { Input } from '@thewhileloop/whileui';
838
+ import { Input, useIconColors } from '@thewhileloop/whileui';
839
+ import { Feather } from '@expo/vector-icons';
840
+
841
+ const icons = useIconColors();
1072
842
 
1073
843
  <Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
844
+
845
+ <Input
846
+ placeholder="Search"
847
+ prefix={<Feather name="search" size={16} color={icons.muted} aria-hidden />}
848
+ />;
1074
849
  ```
1075
850
 
1076
- On web, filled Input, Textarea, NumericInput, and SmartInput surfaces preserve their
1077
- themed background, foreground, and caret when the browser autofills them.
851
+ On iOS and Android, Input, Textarea, NumericInput and SmartInput draw the caret and
852
+ selection in the theme's primary colour; pass `selectionColor` (and `cursorColor` on
853
+ Android) to override it. On web, filled surfaces keep their themed background,
854
+ foreground and caret when the browser autofills them.
1078
855
 
1079
- | Prop | Type | Default | Description |
1080
- | -------------------- | --------------------------------- | ----------- | --------------------------------------------------------------------- |
1081
- | variant | `'default' \| 'error' \| 'plain'` | `'default'` | Input style; `plain` drops border and fill for a large display figure |
1082
- | placeholder | `string` | — | Placeholder text |
1083
- | placeholderTextColor | `string` | — | Hex for placeholder; defaults to theme `mutedForeground` |
1084
- | editable | `boolean` | `true` | Whether input is editable |
856
+ <!-- api:Input -->
857
+
858
+ | Prop | Type | Default | Description |
859
+ | -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
860
+ | aria-invalid | `boolean` | — | |
861
+ | className | `string` | — | |
862
+ | prefix | `React.ReactNode` | — | Shown before the text. A decorative icon here needs `aria-hidden`; screen readers otherwise stop on it as a separate element and read out its glyph. |
863
+ | suffix | `React.ReactNode` | — | Shown after the text: a unit, or a control such as a clear button. |
864
+ | inputClassName | `string` | — | |
865
+ | variant | `'default' \| 'error' \| 'plain'` | `'default'` | |
866
+
867
+ Also accepts the props of `TextInputProps`.
868
+ <!-- /api:Input -->
1085
869
 
1086
870
  ## NumericInput
1087
871
 
@@ -1100,15 +884,28 @@ import { NumericInput } from '@thewhileloop/whileui';
1100
884
  />;
1101
885
  ```
1102
886
 
1103
- | Prop | Type | Default | Description |
1104
- | -------------------- | --------------------------------- | ----------- | --------------------------------- |
1105
- | variant | `'default' \| 'error'` | `'default'` | Input style |
1106
- | size | `'default' \| 'compact'` | `'default'` | Density size |
1107
- | value | `number \| null` | — | Controlled numeric value |
1108
- | onValueChange | `(value: number \| null) => void` | — | Numeric value change callback |
1109
- | placeholderTextColor | `string` | — | Hex for placeholder |
1110
- | prefix / suffix | `ReactNode` | — | Left/right slots |
1111
- | showSteppers | `boolean` | `false` | Show decrement/increment controls |
887
+ <!-- api:NumericInput -->
888
+
889
+ | Prop | Type | Default | Description |
890
+ | -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
891
+ | aria-invalid | `boolean` | — | |
892
+ | className | `string` | — | |
893
+ | inputClassName | `string` | — | |
894
+ | prefix | `React.ReactNode` | — | |
895
+ | suffix | `React.ReactNode` | — | |
896
+ | value | `number \| null` | — | Controlled numeric value. |
897
+ | defaultValue | `number` | — | |
898
+ | onValueChange | `(value: number \| null) => void` | — | Numeric value change callback. |
899
+ | min | `number` | — | |
900
+ | max | `number` | — | |
901
+ | step | `number` | `1` | |
902
+ | showSteppers | `boolean` | `false` | Show decrement/increment controls. |
903
+ | unstyled | `boolean` | `false` | Drop the field shell (border, background, radius, padding) for embedding inside another field shell like LabeledField. |
904
+ | size | `'default' \| 'compact'` | `'default'` | |
905
+ | variant | `'default' \| 'error'` | `'default'` | |
906
+
907
+ Also accepts the props of `TextInputProps`.
908
+ <!-- /api:NumericInput -->
1112
909
 
1113
910
  ## OTPInput
1114
911
 
@@ -1124,16 +921,23 @@ import { OTPInput } from '@thewhileloop/whileui';
1124
921
  />;
1125
922
  ```
1126
923
 
1127
- | Prop | Type | Default | Description |
1128
- | ------------- | ------------------------- | ----------- | --------------------------------- |
1129
- | length | `number` | `6` | Number of digit cells |
1130
- | value | `string` | — | Controlled value |
1131
- | onValueChange | `(value: string) => void` | — | Called on each digit change |
1132
- | onComplete | `(code: string) => void` | — | Called when all digits are filled |
1133
- | variant | `'default' \| 'error'` | `'default'` | Error variant triggers shake |
1134
- | size | `'default' \| 'compact'` | `'default'` | Cell size |
1135
- | secure | `boolean` | `false` | Mask digits with dots |
1136
- | autoFocus | `boolean` | `false` | Focus first cell on mount |
924
+ <!-- api:OTPInput -->
925
+
926
+ | Prop | Type | Default | Description |
927
+ | ------------- | ------------------------- | ----------- | ---------------------------------- |
928
+ | length | `number` | `6` | Number of digit cells. |
929
+ | value | `string` | — | Controlled value. |
930
+ | onValueChange | `(value: string) => void` | — | Called on each digit change. |
931
+ | onComplete | `(code: string) => void` | — | Called when all digits are filled. |
932
+ | variant | `'default' \| 'error'` | `'default'` | Error variant triggers shake. |
933
+ | size | `'default' \| 'compact'` | `'default'` | Cell size. |
934
+ | secure | `boolean` | `false` | Mask digits with dots. |
935
+ | disabled | `boolean` | `false` | |
936
+ | autoFocus | `boolean` | `false` | Focus first cell on mount. |
937
+ | className | `string` | — | |
938
+
939
+ Also accepts the props of `ViewProps`.
940
+ <!-- /api:OTPInput -->
1137
941
 
1138
942
  ## FormField
1139
943
 
@@ -1161,6 +965,10 @@ import {
1161
965
  custom controls; labels, hints, messages, density, and required indicators remain part
1162
966
  of the compound field rather than each input.
1163
967
 
968
+ `FormLabel` names the `Input`, `Textarea` or `NumericInput` beside it for assistive
969
+ technology (`aria-labelledby` on web and Android; `useFormFieldState().labelId` gives a
970
+ custom control the same id). A label passed to the control itself wins.
971
+
1164
972
  ## Surface
1165
973
 
1166
974
  ```tsx
@@ -1171,14 +979,22 @@ import { Surface } from '@thewhileloop/whileui';
1171
979
  </Surface>;
1172
980
  ```
1173
981
 
1174
- | Prop | Type | Default | Description |
1175
- | -------- | ------------------------------------- | ----------- | ----------------------------------- |
1176
- | level | `'base' \| 'raised' \| 'overlay'` | `'base'` | Semantic background and depth tier |
1177
- | padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Token-backed interior spacing |
1178
- | radius | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'xl'` | Token-backed corner radius |
1179
- | bordered | `boolean` | `true` | Draw the theme hairline border |
1180
- | shadow | `'none' \| 'sm' \| 'md' \| 'lg'` | by level | Override the level's depth |
1181
- | frosted | `boolean` | `false` | Enable the optional frosted overlay |
982
+ <!-- api:Surface -->
983
+
984
+ | Prop | Type | Default | Description |
985
+ | ------------- | ------------------------------ | ------------------- | ------------------------------------ |
986
+ | level | `SurfaceLevel` | `'base'` | Semantic background and depth tier. |
987
+ | padding | `SurfacePaddingSize` | `'default'` | Token-backed interior spacing. |
988
+ | radius | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'xl'` | Token-backed corner radius. |
989
+ | bordered | `boolean` | `true` | Draw the theme hairline border. |
990
+ | shadow | `ShadowTier` | — | Override the level's depth. |
991
+ | className | `string` | — | |
992
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
993
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
994
+ | blurTintToken | `FrostedTintToken` | `LEVEL_TINT[level]` | Tint token used for frosted mode. |
995
+
996
+ Also accepts the props of `ViewProps`.
997
+ <!-- /api:Surface -->
1182
998
 
1183
999
  ## LabeledField
1184
1000
 
@@ -1220,9 +1036,24 @@ import {
1220
1036
  </SegmentedControl>;
1221
1037
  ```
1222
1038
 
1223
- | Prop | Type | Default | Description |
1224
- | ------- | --------------------- | ----------- | ------------------------- |
1225
- | variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items |
1039
+ When the labels do not fit a phone, `wrap="scroll"` keeps them on one row at full width
1040
+ and scrolls sideways, instead of wrapping or truncating them.
1041
+
1042
+ <!-- api:SegmentedControl -->
1043
+
1044
+ | Prop | Type | Default | Description |
1045
+ | ------------- | ------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1046
+ | value | `string` | — | |
1047
+ | defaultValue | `string` | `''` | |
1048
+ | onValueChange | `(value: string) => void` | — | |
1049
+ | variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items. |
1050
+ | wrap | `boolean \| 'auto' \| 'scroll'` | `'auto'` | `true` wraps segments onto more rows; `false` keeps one row and shares its width. `'scroll'` keeps one row with each segment as wide as its label and scrolls sideways when they do not fit — for labels a phone cannot show in full. `'auto'` wraps above three segments. |
1051
+ | size | `'default' \| 'compact'` | `'default'` | |
1052
+ | disabled | `boolean` | `false` | |
1053
+ | className | `string` | — | |
1054
+
1055
+ Also accepts the props of `ViewProps`.
1056
+ <!-- /api:SegmentedControl -->
1226
1057
 
1227
1058
  ## DataRow
1228
1059
 
@@ -1281,13 +1112,20 @@ import {
1281
1112
  </Card>;
1282
1113
  ```
1283
1114
 
1284
- | Prop | Type | Default | Description |
1285
- | ------------- | ------------------------------------------------------------------ | ------------ | ----------------------------------- |
1286
- | padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Card interior padding |
1287
- | unstyled | `boolean` | `false` | Remove built-in card surface styles |
1288
- | frosted | `boolean` | `false` | Enables frosted tint + blur layer |
1289
- | blurIntensity | `number` | token preset | Override blur amount directly |
1290
- | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'card'` | Tint token used for frosted mode |
1115
+ <!-- api:Card -->
1116
+
1117
+ | Prop | Type | Default | Description |
1118
+ | ------------- | ------------------------------------- | ----------- | ------------------------------------------------------- |
1119
+ | shadow | `ShadowTier` | `'sm'` | Elevation tier; 'sm' by default, 'none' for flat cards. |
1120
+ | className | `string` | — | |
1121
+ | padding | `'none' \| 'sm' \| 'lg' \| 'default'` | `'default'` | |
1122
+ | unstyled | `boolean` | `false` | |
1123
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1124
+ | blurIntensity | `number` | — | Override blur amount directly. |
1125
+ | blurTintToken | `FrostedTintToken` | — | Tint token used for frosted mode. |
1126
+
1127
+ Also accepts the props of `ViewProps`.
1128
+ <!-- /api:Card -->
1291
1129
 
1292
1130
  ## Badge
1293
1131
 
@@ -1299,9 +1137,15 @@ import { Badge, BadgeText } from '@thewhileloop/whileui';
1299
1137
  </Badge>;
1300
1138
  ```
1301
1139
 
1302
- | Prop | Type | Default |
1303
- | ------- | -------------------------------------------------------------------------------------------- | ----------- |
1304
- | variant | `'default' \| 'secondary' \| 'destructive' \| 'outline' \| 'success' \| 'warning' \| 'info'` | `'default'` |
1140
+ <!-- api:Badge -->
1141
+
1142
+ | Prop | Type | Default | Description |
1143
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ----------- |
1144
+ | className | `string` | — | |
1145
+ | variant | `'default' \| 'soft' \| 'destructive' \| 'outline' \| 'secondary' \| 'success' \| 'warning' \| 'info' \| 'soft-secondary' \| 'soft-destructive' \| 'soft-success' \| 'soft-warning' \| 'soft-info'` | `'default'` | |
1146
+
1147
+ Also accepts the props of `ViewProps`.
1148
+ <!-- /api:Badge -->
1305
1149
 
1306
1150
  ## Alert
1307
1151
 
@@ -1312,11 +1156,24 @@ import { Alert, AlertTitle, AlertDescription } from '@thewhileloop/whileui';
1312
1156
  <AlertTitle>Heads up!</AlertTitle>
1313
1157
  <AlertDescription>Something happened.</AlertDescription>
1314
1158
  </Alert>;
1159
+
1160
+ // Shown after a failed submit: also spoken on iOS when it appears.
1161
+ <Alert variant="destructive" announce>
1162
+ <AlertTitle>Could not save</AlertTitle>
1163
+ <AlertDescription>Check your connection and try again.</AlertDescription>
1164
+ </Alert>;
1315
1165
  ```
1316
1166
 
1317
- | Prop | Type | Default |
1318
- | ------- | ------------------------------------------------------ | ----------- |
1319
- | variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` |
1167
+ <!-- api:Alert -->
1168
+
1169
+ | Prop | Type | Default | Description |
1170
+ | --------- | ---------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1171
+ | 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. |
1172
+ | className | `string` | — | |
1173
+ | variant | `'default' \| 'destructive' \| 'success' \| 'warning' \| 'info'` | `'default'` | |
1174
+
1175
+ Also accepts the props of `ViewProps`.
1176
+ <!-- /api:Alert -->
1320
1177
 
1321
1178
  ## Dialog
1322
1179
 
@@ -1357,11 +1214,17 @@ import {
1357
1214
 
1358
1215
  `DialogContent` supports frosted props:
1359
1216
 
1360
- | Prop | Type | Default | Description |
1361
- | ------------- | ------------------------------------------------------------------ | ---------------------- | -------------------------------- |
1362
- | frosted | `boolean` | `false` | Enables frosted tint + blur |
1363
- | blurIntensity | `number` | medium preset | Override blur amount |
1364
- | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` | Tint token used for frosted mode |
1217
+ <!-- api:DialogContent -->
1218
+
1219
+ | Prop | Type | Default | Description |
1220
+ | ------------- | ------------------ | ---------------------- | ------------------------------------ |
1221
+ | className | `string` | — | |
1222
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1223
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1224
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1225
+
1226
+ Also accepts the props of `ViewProps`.
1227
+ <!-- /api:DialogContent -->
1365
1228
 
1366
1229
  ## AlertDialog
1367
1230
 
@@ -1399,12 +1262,18 @@ import {
1399
1262
 
1400
1263
  `AlertDialogContent` supports the same frosted props as `DialogContent`.
1401
1264
 
1402
- | Prop | Type | Default | Description |
1403
- | ------------- | ---------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1404
- | presentation | `'modal' \| 'native' \| 'overlay'` | `'modal'` | **`modal`** — themed card in an RN `Modal` (iOS `overFullScreen`). **`native`** — `Alert.alert` on iOS/Android (system UI); use when a second `Modal` won’t stack; ignored on web. **`overlay`** — same themed card as `modal`, but absolutely positioned **inside the parent view** (no second `Modal`). Nest `<AlertDialog>` under your fullscreen sheet’s root so the confirm matches app styling. **Web:** `'native'` is ignored; `overlay` behaves like `modal`. |
1405
- | frosted | `boolean` | `false` | Same as `DialogContent` |
1406
- | blurIntensity | `number` | — | Same as `DialogContent` |
1407
- | blurTintToken | `'surfaceElevated' \| …` | — | Same as `DialogContent` |
1265
+ <!-- api:AlertDialogContent -->
1266
+
1267
+ | Prop | Type | Default | Description |
1268
+ | ------------- | ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1269
+ | className | `string` | — | |
1270
+ | 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`). |
1271
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1272
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1273
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1274
+
1275
+ Also accepts the props of `ViewProps`.
1276
+ <!-- /api:AlertDialogContent -->
1408
1277
 
1409
1278
  ### React Native: stacked modals
1410
1279
 
@@ -1423,11 +1292,18 @@ import { Checkbox } from '@thewhileloop/whileui';
1423
1292
  <Checkbox checked={checked} onCheckedChange={setChecked} />;
1424
1293
  ```
1425
1294
 
1426
- | Prop | Type | Default |
1427
- | --------------- | ---------------------------- | ------- |
1428
- | checked | `boolean` | `false` |
1429
- | onCheckedChange | `(checked: boolean) => void` | — |
1430
- | disabled | `boolean` | `false` |
1295
+ <!-- api:Checkbox -->
1296
+
1297
+ | Prop | Type | Default | Description |
1298
+ | --------------- | ---------------------------- | ------- | ----------- |
1299
+ | checked | `boolean` | — | |
1300
+ | defaultChecked | `boolean` | `false` | |
1301
+ | onCheckedChange | `(checked: boolean) => void` | — | |
1302
+ | disabled | `boolean` | `false` | |
1303
+ | className | `string` | — | |
1304
+
1305
+ Also accepts the props of `ViewProps`.
1306
+ <!-- /api:Checkbox -->
1431
1307
 
1432
1308
  ## Switch
1433
1309
 
@@ -1437,10 +1313,18 @@ import { Switch } from '@thewhileloop/whileui';
1437
1313
  <Switch checked={checked} onCheckedChange={setChecked} />;
1438
1314
  ```
1439
1315
 
1440
- | Prop | Type | Default |
1441
- | --------------- | ---------------------------- | ------- |
1442
- | checked | `boolean` | `false` |
1443
- | onCheckedChange | `(checked: boolean) => void` | — |
1316
+ <!-- api:Switch -->
1317
+
1318
+ | Prop | Type | Default | Description |
1319
+ | --------------- | ---------------------------- | ------- | ----------- |
1320
+ | checked | `boolean` | — | |
1321
+ | defaultChecked | `boolean` | `false` | |
1322
+ | onCheckedChange | `(checked: boolean) => void` | — | |
1323
+ | disabled | `boolean` | `false` | |
1324
+ | className | `string` | — | |
1325
+
1326
+ Also accepts the props of `ViewProps`.
1327
+ <!-- /api:Switch -->
1444
1328
 
1445
1329
  ## RadioGroup
1446
1330
 
@@ -1481,13 +1365,25 @@ selected values stay on one line and truncate without displacing the trigger che
1481
1365
  For popover presentation on web, the options panel matches the trigger width, caps
1482
1366
  itself to the collision-available viewport, and wraps long option labels inside it.
1483
1367
 
1368
+ Opening a Select, or a Popover, closes the on-screen keyboard first, so options opened
1369
+ from a form are never hidden behind it. Both draw in a portal, where the platform would
1370
+ otherwise leave the keyboard up; the Modal-hosted overlays close it on their own.
1371
+
1484
1372
  `SelectContent` supports optional frosted props:
1485
1373
 
1486
- | Prop | Type | Default |
1487
- | ------------- | ------------------------------------------------------------------ | ------------- |
1488
- | frosted | `boolean` | `false` |
1489
- | blurIntensity | `number` | subtle preset |
1490
- | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
1374
+ <!-- api:SelectContent -->
1375
+
1376
+ | Prop | Type | Default | Description |
1377
+ | ------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------ |
1378
+ | className | `string` | — | |
1379
+ | portalHost | `string` | — | |
1380
+ | insets | `{ top?: number; bottom?: number; left?: number; right?: number }` | — | |
1381
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1382
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
1383
+ | blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |
1384
+
1385
+ Also accepts the props of the underlying primitive.
1386
+ <!-- /api:SelectContent -->
1491
1387
 
1492
1388
  `SelectContent` keeps its listbox inside the safe area by default; pass `insets` only to
1493
1389
  change that margin. A list taller than the screen scrolls inside the listbox on every
@@ -1536,10 +1432,17 @@ import {
1536
1432
  </Accordion>;
1537
1433
  ```
1538
1434
 
1539
- | Prop | Type | Default |
1540
- | ----------- | ------------------------ | ---------- |
1541
- | type | `'single' \| 'multiple'` | `'single'` |
1542
- | collapsible | `boolean` | `false` |
1435
+ <!-- api:Accordion -->
1436
+
1437
+ | Prop | Type | Default | Description |
1438
+ | ------------ | -------------------- | ---------- | ----------- |
1439
+ | type | `AccordionType` | `'single'` | |
1440
+ | defaultValue | `string \| string[]` | — | |
1441
+ | collapsible | `boolean` | `true` | |
1442
+ | className | `string` | — | |
1443
+
1444
+ Also accepts the props of `ViewProps`.
1445
+ <!-- /api:Accordion -->
1543
1446
 
1544
1447
  ## Avatar
1545
1448
 
@@ -1552,9 +1455,15 @@ import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
1552
1455
  </Avatar>;
1553
1456
  ```
1554
1457
 
1555
- | Prop | Type | Default |
1556
- | ---- | --------------------------- | ----------- |
1557
- | size | `'sm' \| 'default' \| 'lg'` | `'default'` |
1458
+ <!-- api:Avatar -->
1459
+
1460
+ | Prop | Type | Default | Description |
1461
+ | --------- | ----------------------------------- | ----------- | ----------- |
1462
+ | className | `string` | — | |
1463
+ | size | `'sm' \| 'lg' \| 'xl' \| 'default'` | `'default'` | |
1464
+
1465
+ Also accepts the props of `ViewProps`.
1466
+ <!-- /api:Avatar -->
1558
1467
 
1559
1468
  `AvatarImage` keeps `AvatarFallback` visible while loading or after an error. Its
1560
1469
  `onLoadingStatusChange` callback reports `'loading'`, `'loaded'`, and `'error'`, including
@@ -1568,10 +1477,17 @@ import { Progress } from '@thewhileloop/whileui';
1568
1477
  <Progress value={50} size="default" />;
1569
1478
  ```
1570
1479
 
1571
- | Prop | Type | Default |
1572
- | ----- | --------------------------- | ----------- |
1573
- | value | `number` | `0` |
1574
- | size | `'sm' \| 'default' \| 'lg'` | `'default'` |
1480
+ <!-- api:Progress -->
1481
+
1482
+ | Prop | Type | Default | Description |
1483
+ | ------------------ | --------------------------- | ----------- | ----------- |
1484
+ | value | `number` | `0` | |
1485
+ | indicatorClassName | `string` | — | |
1486
+ | className | `string` | — | |
1487
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
1488
+
1489
+ Also accepts the props of `ViewProps`.
1490
+ <!-- /api:Progress -->
1575
1491
 
1576
1492
  ## Toast
1577
1493
 
@@ -1599,6 +1515,11 @@ toast({ title: 'Success', description: 'Saved!', variant: 'success' });
1599
1515
  Each toast owns its duration from the moment it appears; adding or dismissing another
1600
1516
  toast does not restart timers already in progress.
1601
1517
 
1518
+ Every variant sits on the theme's `popover` surface; `success` and `destructive` mark the
1519
+ toast with a status dot and tinted border rather than a solid fill. The stack is capped
1520
+ at `max-w-md` and centred, which never engages on a phone. A toast is announced politely
1521
+ to screen readers without taking focus, and its slide-in follows the motion policy.
1522
+
1602
1523
  ## Popover
1603
1524
 
1604
1525
  ```tsx
@@ -1618,11 +1539,19 @@ import { Popover, PopoverTrigger, PopoverContent } from '@thewhileloop/whileui';
1618
1539
 
1619
1540
  `PopoverContent` supports frosted props:
1620
1541
 
1621
- | Prop | Type | Default |
1622
- | ------------- | ------------------------------------------------------------------ | ------------- |
1623
- | frosted | `boolean` | `false` |
1624
- | blurIntensity | `number` | subtle preset |
1625
- | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
1542
+ <!-- api:PopoverContent -->
1543
+
1544
+ | Prop | Type | Default | Description |
1545
+ | ------------- | ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------- |
1546
+ | className | `string` | — | |
1547
+ | portalHost | `string` | — | |
1548
+ | insets | `Partial<OverlayInsets>` | — | Collision padding. Defaults to safe area plus a margin so the panel never sits flush against an edge. |
1549
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1550
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
1551
+ | blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |
1552
+
1553
+ Also accepts the props of the underlying primitive.
1554
+ <!-- /api:PopoverContent -->
1626
1555
 
1627
1556
  ## DropdownMenu
1628
1557
 
@@ -1710,7 +1639,7 @@ announces as a heading to assistive technology — as do `DropdownMenuLabel` and
1710
1639
  ## SignInForm
1711
1640
 
1712
1641
  Copy `sign-in-form.tsx` and its sibling `auth-form-footer.tsx` from
1713
- `apps/showcase/templates/auth/`, then:
1642
+ `apps/showcase/catalog/templates/auth/`, then:
1714
1643
 
1715
1644
  ```tsx
1716
1645
  import { SignInForm } from './templates/auth'; // or your path
@@ -1735,7 +1664,7 @@ import { SignInForm } from './templates/auth'; // or your path
1735
1664
  ## SignUpForm
1736
1665
 
1737
1666
  Copy `sign-up-form.tsx` and its sibling `auth-form-footer.tsx` from
1738
- `apps/showcase/templates/auth/`, then:
1667
+ `apps/showcase/catalog/templates/auth/`, then:
1739
1668
 
1740
1669
  ```tsx
1741
1670
  import { SignUpForm } from './templates/auth'; // or your path
@@ -1789,14 +1718,20 @@ import { AppShell, PageSkeleton } from '@thewhileloop/whileui';
1789
1718
  </AppShell>;
1790
1719
  ```
1791
1720
 
1792
- | Prop | Type | Default | Description |
1793
- | ----------- | ----------- | ------- | --------------------------------------------------- |
1794
- | `header` | `ReactNode` | — | Header slot |
1795
- | `footer` | `ReactNode` | — | Footer slot |
1796
- | `bottomNav` | `ReactNode` | — | Bottom navigation slot |
1797
- | `safeArea` | `boolean` | `true` | Wrap in SafeAreaView |
1798
- | `loading` | `boolean` | `false` | Show `skeleton` in content area, keep shell mounted |
1799
- | `skeleton` | `ReactNode` | — | Content placeholder rendered when `loading=true` |
1721
+ <!-- api:AppShell -->
1722
+
1723
+ | Prop | Type | Default | Description |
1724
+ | --------- | ----------------- | ------- | --------------------------------------------------------- |
1725
+ | header | `React.ReactNode` | — | Header slot. |
1726
+ | footer | `React.ReactNode` | — | Footer slot. |
1727
+ | bottomNav | `React.ReactNode` | — | Bottom navigation slot. |
1728
+ | safeArea | `boolean` | `true` | Wrap in SafeAreaView. |
1729
+ | loading | `boolean` | `false` | Keep shell mounted while swapping content with skeleton. |
1730
+ | skeleton | `React.ReactNode` | — | Optional custom loading placeholder for the content area. |
1731
+ | className | `string` | — | |
1732
+
1733
+ Also accepts the props of `ViewProps`.
1734
+ <!-- /api:AppShell -->
1800
1735
 
1801
1736
  ## ActionBar
1802
1737
 
@@ -1840,7 +1775,7 @@ import { SmartInput, Button, ButtonText } from '@thewhileloop/whileui';
1840
1775
  onChangeText={setMessage}
1841
1776
  placeholderExamples={['Log breakfast', 'Add a 30 minute walk', 'Set weight to 72 kg']}
1842
1777
  leftSlot={
1843
- <Button variant="ghost" size="icon">
1778
+ <Button variant="ghost" size="icon" accessibilityLabel="Add">
1844
1779
  <Icon name="add" />
1845
1780
  </Button>
1846
1781
  }
@@ -1897,24 +1832,37 @@ const [value, setValue] = useState('');
1897
1832
  onSuggestionPress={(text) => setValue(text)}
1898
1833
  emptyTitle="How can I help?"
1899
1834
  emptyDescription="Ask anything."
1900
- leftSlot={<Button variant="ghost" size="icon"><Icon name="paperclip" /></Button>}
1901
- rightSlot={<Button size="icon" onPress={handleSend}><Icon name="send" /></Button>}
1835
+ leftSlot={<Button variant="ghost" size="icon" accessibilityLabel="Attach"><Icon name="paperclip" /></Button>}
1836
+ rightSlot={<Button size="icon" accessibilityLabel="Send" onPress={handleSend}><Icon name="send" /></Button>}
1902
1837
  />;
1903
1838
  ```
1904
1839
 
1905
- | Prop | Type | Description |
1906
- | ------------------------ | -------------------- | ------------------------------------------------- |
1907
- | `messages` | `ChatMessage[]` | `{ id, role, content, secondary?, contentSize? }` |
1908
- | `value` | `string` | Input value |
1909
- | `onChangeText` | `(text) => void` | Input change |
1910
- | `onSend` | `() => void` | Send handler |
1911
- | `suggestions` | `string[]` | Chips when empty |
1912
- | `leftSlot` / `rightSlot` | `ReactNode` | Attach, send, etc. |
1913
- | `exampleMessage` | `ChatMessage` | Shown in empty state |
1914
- | `renderMessage` | `(msg) => ReactNode` | Custom message (markdown, code, images) |
1915
- | `loadingIndicator` | `ReactNode` | Shown when loading (typing dots) |
1916
- | `inputSafeArea` | `boolean` | SmartInput safe-area (default `true`) |
1917
- | `keyboardVerticalOffset` | `number` | For header offset when keyboard opens |
1840
+ <!-- api:Chat -->
1841
+
1842
+ | Prop | Type | Default | Description |
1843
+ | ----------------------- | ------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------- |
1844
+ | messages (required) | `ChatMessage[]` | — | Messages to show, each `{ id, role, content, secondary?, contentSize? }`. |
1845
+ | value (required) | `string` | — | Input value. |
1846
+ | onChangeText (required) | `(text: string) => void` | — | Input change. |
1847
+ | onSend (required) | `() => void` | — | Send handler. |
1848
+ | placeholder | `string` | `'Message...'` | |
1849
+ | suggestions | `string[]` | `EMPTY_SUGGESTIONS` | Chips when empty. |
1850
+ | onSuggestionPress | `(text: string) => void` | — | |
1851
+ | leftSlot | `React.ReactNode` | — | |
1852
+ | rightSlot | `React.ReactNode` | — | |
1853
+ | loading | `boolean` | `false` | |
1854
+ | header | `React.ReactNode` | — | |
1855
+ | emptyTitle | `string` | — | |
1856
+ | emptyDescription | `string` | — | |
1857
+ | exampleMessage | `ChatMessage` | — | Example message shown in empty state so users see the bubble UI. |
1858
+ | renderMessage | `(message: ChatMessage) => React.ReactNode` | — | Custom message renderer (markdown, code, images). Falls back to ChatMessageBubble. |
1859
+ | loadingIndicator | `React.ReactNode` | — | Shown at bottom when loading (typing indicator). |
1860
+ | inputSafeArea | `boolean` | `true` | Passed to SmartInput. Use false when Chat is in a contained demo. |
1861
+ | keyboardVerticalOffset | `number` | `0` | Offset for KeyboardAvoidingView when Chat has a header (e.g. StatusBar + header height). |
1862
+ | className | `string` | — | |
1863
+ | messagesClassName | `string` | — | |
1864
+
1865
+ <!-- /api:Chat -->
1918
1866
 
1919
1867
  `ChatConversation` extends React Native `ScrollViewProps`, forwards its ref, and adds `contentContainerClassName` to style the constrained inner column. Caller `onScroll` and `onContentSizeChange` callbacks are preserved.
1920
1868
 
@@ -1949,18 +1897,24 @@ import { SmartImage } from '@thewhileloop/whileui';
1949
1897
  />;
1950
1898
  ```
1951
1899
 
1952
- | Prop | Type | Default |
1953
- | ------------------- | -------------------------------------------------- | --------- |
1954
- | `source` | `ImageProps['source']` | required |
1955
- | `alt` | `string` | required |
1956
- | `fallbackSource` | `ImageProps['source']` | — |
1957
- | `skeleton` | `boolean` | `true` |
1958
- | `skeletonClassName` | `string` | — |
1959
- | `transition` | `number` | `200` |
1960
- | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` |
1961
- | `objectFit` | `'cover' \| 'contain' \| 'fill' \| 'scale-down'` | `'cover'` |
1962
- | `onLoad` | `() => void` | — |
1963
- | `onError` | `() => void` | — |
1900
+ <!-- api:SmartImage -->
1901
+
1902
+ | Prop | Type | Default | Description |
1903
+ | ----------------- | -------------------------------------------------- | --------- | ----------- |
1904
+ | className | `string` | — | |
1905
+ | source (required) | `ImageProps['source']` | — | |
1906
+ | alt (required) | `string` | — | |
1907
+ | skeleton | `boolean` | `true` | |
1908
+ | skeletonClassName | `string` | — | |
1909
+ | transition | `number` | `200` | |
1910
+ | onLoad | `() => void` | — | |
1911
+ | onError | `() => void` | — | |
1912
+ | fallbackSource | `ImageProps['source']` | — | |
1913
+ | radius | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | |
1914
+ | objectFit | `'fill' \| 'cover' \| 'contain' \| 'scale-down'` | `'cover'` | |
1915
+
1916
+ Also accepts the props of `ViewProps`.
1917
+ <!-- /api:SmartImage -->
1964
1918
 
1965
1919
  The skeleton returns whenever the primary image starts loading again. A failed primary
1966
1920
  source switches to `fallbackSource` when supplied without re-entering a primary/fallback
@@ -2030,19 +1984,26 @@ const [time, setTime] = useState<string | null>(null);
2030
1984
  />;
2031
1985
  ```
2032
1986
 
2033
- | Prop | Type | Description |
2034
- | ----------------------- | ------------------------------------------------------------------ | -------------------------------------- |
2035
- | `value` | `string \| null` / `DateRange \| null` | Selected date(s) YYYY-MM-DD |
2036
- | `onValueChange` | `(date) => void` | Change handler |
2037
- | `open` / `onOpenChange` | — | Modal state (modal variants) |
2038
- | `trigger` | `ReactNode` | Custom trigger (modal variants) |
2039
- | `minDate` / `maxDate` | `string` | YYYY-MM-DD bounds |
2040
- | `theme` | `CalendarTheme` | Override calendar colors |
2041
- | `hourCycle` | `'h12' \| 'h23'` | Time picker wheels; defaults to locale |
2042
- | `minuteStep` | `number` | Minutes between options (default 1) |
2043
- | `frosted` | `boolean` | Enable frosted surface for modal panel |
2044
- | `blurIntensity` | `number` | Override blur amount |
2045
- | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | Tint token for frosted panel |
1987
+ <!-- api:DatePickerModal -->
1988
+
1989
+ | Prop | Type | Default | Description |
1990
+ | ----------------------- | -------------------------------- | ---------------------- | --------------------------------------------------------- |
1991
+ | value | `string \| null` | — | Controlled: selected date in YYYY-MM-DD |
1992
+ | onValueChange | `(date: string \| null) => void` | — | Controlled: change handler |
1993
+ | open (required) | `boolean` | — | Modal open state |
1994
+ | onOpenChange (required) | `(open: boolean) => void` | — | Modal open change handler |
1995
+ | minDate | `string` | — | Minimum selectable date YYYY-MM-DD |
1996
+ | maxDate | `string` | — | Maximum selectable date YYYY-MM-DD |
1997
+ | theme | `CalendarTheme` | — | Override calendar theme (RN color strings) |
1998
+ | trigger | `React.ReactNode` | — | Trigger element. If not provided, no trigger is rendered. |
1999
+ | title | `string` | `'Select date'` | Title shown in modal header |
2000
+ | confirmLabel | `string` | `'Done'` | Button label for confirm |
2001
+ | className | `string` | — | |
2002
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2003
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2004
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2005
+
2006
+ <!-- /api:DatePickerModal -->
2046
2007
 
2047
2008
  ## ConfirmActionSheet
2048
2009
 
@@ -2060,11 +2021,27 @@ import { ConfirmActionSheet } from '@thewhileloop/whileui';
2060
2021
  />;
2061
2022
  ```
2062
2023
 
2063
- | Prop | Type | Default |
2064
- | --------------- | ------------------------------------------------------------------ | ---------------------- |
2065
- | `frosted` | `boolean` | `false` |
2066
- | `blurIntensity` | `number` | medium preset |
2067
- | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
2024
+ <!-- api:ConfirmActionSheet -->
2025
+
2026
+ | Prop | Type | Default | Description |
2027
+ | ----------------------- | ---------------------------- | ---------------------- | ------------------------------------ |
2028
+ | open (required) | `boolean` | — | |
2029
+ | onOpenChange (required) | `(open: boolean) => void` | — | |
2030
+ | title (required) | `string` | — | |
2031
+ | description | `string` | — | |
2032
+ | actions | `ConfirmActionSheetAction[]` | — | |
2033
+ | confirmLabel | `string` | `'Confirm'` | |
2034
+ | cancelLabel | `string` | `'Cancel'` | |
2035
+ | onConfirm | `() => void` | — | |
2036
+ | onCancel | `() => void` | — | |
2037
+ | destructive | `boolean` | `true` | |
2038
+ | className | `string` | — | |
2039
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2040
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2041
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2042
+
2043
+ Also accepts the props of `ViewProps`.
2044
+ <!-- /api:ConfirmActionSheet -->
2068
2045
 
2069
2046
  ## Sheet
2070
2047
 
@@ -2094,15 +2071,20 @@ import {
2094
2071
  </Sheet>;
2095
2072
  ```
2096
2073
 
2097
- | Prop | Type | Default |
2098
- | ------------- | ------------------------------------------------------------------ | ---------------------- |
2099
- | open | `boolean` | — |
2100
- | onOpenChange | `(open: boolean) => void` | — |
2101
- | maxHeight | `'half' \| 'full' \| number` | `'full'` |
2102
- | maxWidth | `number` | `360` |
2103
- | frosted | `boolean` | `false` |
2104
- | blurIntensity | `number` | medium preset |
2105
- | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
2074
+ <!-- api:Sheet -->
2075
+
2076
+ | Prop | Type | Default | Description |
2077
+ | ----------------------- | ---------------------------- | ---------------------- | ----------------------------------------------------------- |
2078
+ | open (required) | `boolean` | — | |
2079
+ | onOpenChange (required) | `(open: boolean) => void` | — | |
2080
+ | children (required) | `React.ReactNode` | — | |
2081
+ | maxHeight | `'half' \| 'full' \| number` | `'full'` | Max height: 'half' \| 'full' \| number (px). Default 'full' |
2082
+ | maxWidth | `number` | — | Web: max width. Default ~360 for mobile-first. |
2083
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2084
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2085
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2086
+
2087
+ <!-- /api:Sheet -->
2106
2088
 
2107
2089
  `SheetContent` is a `ScrollView` and accepts its props. It enables nested scrolling and
2108
2090
  keeps taps on inputs while the keyboard is open (`keyboardShouldPersistTaps="handled"`).
@@ -2143,11 +2125,22 @@ import { SplashScreen } from '@thewhileloop/whileui';
2143
2125
  />;
2144
2126
  ```
2145
2127
 
2146
- | Prop | Type | Default |
2147
- | ----------- | ------------------------------ | --------- |
2148
- | variant | `'fade' \| 'scale' \| 'slide'` | `'scale'` |
2149
- | duration | `number` | `800` |
2150
- | showLoading | `boolean` | `false` |
2128
+ <!-- api:SplashScreen -->
2129
+
2130
+ | Prop | Type | Default | Description |
2131
+ | ------------------- | ------------------------------ | --------- | ----------------------------------------------- |
2132
+ | appName | `string` | — | App name displayed below logo |
2133
+ | tagline | `string` | — | Tagline displayed below app name |
2134
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2135
+ | variant | `'fade' \| 'scale' \| 'slide'` | `'scale'` | Animation variant |
2136
+ | duration | `number` | `800` | Animation duration in ms |
2137
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2138
+ | showLoading | `boolean` | `false` | Whether to show loading indicator |
2139
+ | loadingText | `string` | — | Loading text |
2140
+ | className | `string` | — | |
2141
+
2142
+ Also accepts the props of `ViewProps`.
2143
+ <!-- /api:SplashScreen -->
2151
2144
 
2152
2145
  ## ContentSkeleton
2153
2146
 
@@ -2161,10 +2154,16 @@ import { ContentSkeleton } from '@thewhileloop/whileui';
2161
2154
  <ContentSkeleton variant="generic" />
2162
2155
  ```
2163
2156
 
2164
- | Prop | Type | Default | Description |
2165
- | ------- | ------------------------------- | -------- | --------------------------------------- |
2166
- | variant | `'list' \| 'card' \| 'generic'` | `'list'` | Layout preset |
2167
- | rows | `number` | `4` | Number of list rows (list variant only) |
2157
+ <!-- api:ContentSkeleton -->
2158
+
2159
+ | Prop | Type | Default | Description |
2160
+ | --------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
2161
+ | variant | `'list' \| 'card' \| 'generic'` | `'list'` | Layout preset. `list` = avatar + lines rows, `card` = card with image/body, `generic` = simple page lines. |
2162
+ | rows | `number` | `4` | Number of list rows (list variant only). Default 4. |
2163
+ | className | `string` | — | |
2164
+
2165
+ Also accepts the props of `ViewProps`.
2166
+ <!-- /api:ContentSkeleton -->
2168
2167
 
2169
2168
  ## PageSkeleton
2170
2169
 
@@ -2183,14 +2182,19 @@ import { PageSkeleton } from '@thewhileloop/whileui';
2183
2182
  <PageSkeleton variant="list" padding="none" className="flex-1" />
2184
2183
  ```
2185
2184
 
2186
- | Prop | Type | Default | Description |
2187
- | ------------------- | ------------------------------------------------------------ | ---------------------- | --------------------------------------------- |
2188
- | `variant` | `'dashboard' \| 'list' \| 'settings' \| 'card' \| 'generic'` | required | Layout preset |
2189
- | `count` | `number` | 3 (list), 4 (settings) | Rows/items for list or settings variant |
2190
- | `padding` | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Container padding |
2191
- | `header` | `ReactNode` | — | Optional real header slot above content |
2192
- | `headerPlaceholder` | `boolean \| 'compact' \| 'default'` | `false` | Skeleton header when real header is not ready |
2193
- | `className` | `string` | — | Outer container classes |
2185
+ <!-- api:PageSkeleton -->
2186
+
2187
+ | Prop | Type | Default | Description |
2188
+ | ------------------ | ------------------------------------------ | ----------- | ------------------------------------------------------------------- |
2189
+ | variant (required) | `PageSkeletonVariant` | — | Layout preset. |
2190
+ | count | `number` | `3` | Rows/items for list (default 3) or settings (default 4) variant. |
2191
+ | padding | `PageSkeletonPadding` | `'default'` | Container padding. |
2192
+ | header | `ReactNode` | — | Optional header slot rendered above skeleton content. |
2193
+ | headerPlaceholder | `boolean \| PageSkeletonHeaderPlaceholder` | `false` | Optional placeholder header (when no real header is available yet). |
2194
+ | className | `string` | — | |
2195
+
2196
+ Also accepts the props of `ViewProps`.
2197
+ <!-- /api:PageSkeleton -->
2194
2198
 
2195
2199
  ## ScreenSkeleton
2196
2200
 
@@ -2203,13 +2207,19 @@ import { ScreenSkeleton } from '@thewhileloop/whileui';
2203
2207
  <ScreenSkeleton variant="list" count={5} header={<Header title="Projects" />} />
2204
2208
  ```
2205
2209
 
2206
- | Prop | Type | Default | Description |
2207
- | ------------------- | ----------------------------------- | ----------- | ------------------------------------- |
2208
- | `variant` | `PageSkeletonVariant` | `'generic'` | Skeleton content preset |
2209
- | `count` | `number` | — | Rows/items for list/settings variants |
2210
- | `padding` | `PageSkeletonPadding` | `'default'` | Inner content padding |
2211
- | `header` | `ReactNode` | — | Real header slot |
2212
- | `headerPlaceholder` | `boolean \| 'compact' \| 'default'` | `'default'` | Placeholder header |
2210
+ <!-- api:ScreenSkeleton -->
2211
+
2212
+ | Prop | Type | Default | Description |
2213
+ | ----------------- | ------------------------------------------ | ----------- | -------------------------------------- |
2214
+ | variant | `PageSkeletonVariant` | `'generic'` | Skeleton content preset. |
2215
+ | count | `number` | — | Rows/items for list/settings variants. |
2216
+ | padding | `PageSkeletonPadding` | `'default'` | Inner content padding. |
2217
+ | header | `ReactNode` | — | Real header slot. |
2218
+ | headerPlaceholder | `boolean \| PageSkeletonHeaderPlaceholder` | `'default'` | Placeholder header. |
2219
+ | className | `string` | — | |
2220
+
2221
+ Also accepts the props of `ViewProps`.
2222
+ <!-- /api:ScreenSkeleton -->
2213
2223
 
2214
2224
  ## ErrorBoundary
2215
2225
 
@@ -2223,10 +2233,15 @@ import { ErrorBoundary } from '@thewhileloop/whileui';
2223
2233
  </ErrorBoundary>;
2224
2234
  ```
2225
2235
 
2226
- | Prop | Type | Description |
2227
- | -------- | ------------------------------------------ | --------------------------------------- |
2228
- | fallback | `ReactNode \| (error, reset) => ReactNode` | Custom fallback; defaults to ErrorState |
2229
- | onError | `(error, errorInfo) => void` | Called when error is caught |
2236
+ <!-- api:ErrorBoundary -->
2237
+
2238
+ | Prop | Type | Default | Description |
2239
+ | ------------------- | --------------------------------------------------------------- | ------- | -------------------------------------------------- |
2240
+ | children (required) | `ReactNode` | — | |
2241
+ | fallback | `ReactNode \| ((error: Error, reset: () => void) => ReactNode)` | — | Custom fallback. If not provided, uses ErrorState. |
2242
+ | onError | `(error: Error, errorInfo: ErrorInfo) => void` | — | Called when error is caught. |
2243
+
2244
+ <!-- /api:ErrorBoundary -->
2230
2245
 
2231
2246
  ## PullToRefreshScrollView
2232
2247
 
@@ -2240,11 +2255,17 @@ import { PullToRefreshScrollView } from '@thewhileloop/whileui';
2240
2255
  </PullToRefreshScrollView>;
2241
2256
  ```
2242
2257
 
2243
- | Prop | Type | Description |
2244
- | ------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------- |
2245
- | refreshing | `boolean` | Whether refresh is in progress |
2246
- | onRefresh | `() => void \| Promise<void>` | Called when user pulls to refresh. Not awaited — drive the spinner with `refreshing`, and catch your own errors |
2247
- | refreshColor | `string` | Optional hex override; defaults to primary |
2258
+ <!-- api:PullToRefreshScrollView -->
2259
+
2260
+ | Prop | Type | Default | Description |
2261
+ | --------------------- | ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2262
+ | refreshing (required) | `boolean` | — | Whether refresh is in progress. |
2263
+ | 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. |
2264
+ | refreshColor | `string` | — | Override tint/colors. Defaults to theme primary. |
2265
+ | className | `string` | — | |
2266
+
2267
+ Also accepts the props of `ScrollViewProps`.
2268
+ <!-- /api:PullToRefreshScrollView -->
2248
2269
 
2249
2270
  The scroll view owns the refresh gesture; don't pass `refreshControl`. It bounces even
2250
2271
  when content is shorter than the screen, so a short list can still be pulled. Screen
@@ -2276,14 +2297,20 @@ import { LoadingState } from '@thewhileloop/whileui';
2276
2297
  The in-flow counterpart of `EmptyState` and `ErrorState` for a section that is still
2277
2298
  fetching. The spinner is named for assistive technology by `label` (default "Loading").
2278
2299
 
2279
- | Prop | Type | Default |
2280
- | ----- | --------------------------- | ----------- |
2281
- | label | `string` | `'Loading'` |
2282
- | size | `'sm' \| 'default' \| 'lg'` | `'default'` |
2300
+ <!-- api:LoadingState -->
2301
+
2302
+ | Prop | Type | Default | Description |
2303
+ | --------- | --------------------------- | ----------- | ------------------------------------------------------------------------------ |
2304
+ | label | `string` | — | Read by assistive technology as the spinner's name; shown under it when given. |
2305
+ | size | `'sm' \| 'default' \| 'lg'` | `'default'` | |
2306
+ | className | `string` | — | |
2307
+
2308
+ Also accepts the props of `ViewProps`.
2309
+ <!-- /api:LoadingState -->
2283
2310
 
2284
2311
  ## ProfileHeader
2285
2312
 
2286
- Copy from `apps/showcase/templates/profile/profile-header.tsx`, then:
2313
+ Copy from `apps/showcase/catalog/templates/profile/profile-header.tsx`, then:
2287
2314
 
2288
2315
  ```tsx
2289
2316
  import { ProfileHeader } from './templates/profile'; // or your path
@@ -2305,7 +2332,7 @@ import { ProfileHeader } from './templates/profile'; // or your path
2305
2332
 
2306
2333
  ## SettingsSection / SettingsItem
2307
2334
 
2308
- Copy from `apps/showcase/templates/profile/`, then:
2335
+ Copy from `apps/showcase/catalog/templates/profile/`, then:
2309
2336
 
2310
2337
  ```tsx
2311
2338
  import { SettingsSection, SettingsItem } from './templates/profile'; // or your path
@@ -2340,10 +2367,25 @@ import { ProductCard } from '@thewhileloop/whileui';
2340
2367
  />;
2341
2368
  ```
2342
2369
 
2343
- | Prop | Type | Default |
2344
- | ------- | ---------------------------- | ------------ |
2345
- | variant | `'vertical' \| 'horizontal'` | `'vertical'` |
2346
- | loading | `boolean` | `false` |
2370
+ <!-- api:ProductCard -->
2371
+
2372
+ | Prop | Type | Default | Description |
2373
+ | ---------------- | ---------------------------- | ------------ | ----------------------------------------------- |
2374
+ | imageUrl | `string` | — | |
2375
+ | title (required) | `string` | — | |
2376
+ | description | `string` | — | |
2377
+ | price (required) | `string` | — | |
2378
+ | originalPrice | `string` | — | |
2379
+ | badge | `string` | — | |
2380
+ | rating | `number` | — | |
2381
+ | reviewCount | `number` | — | |
2382
+ | inStock | `boolean` | `true` | |
2383
+ | variant | `'vertical' \| 'horizontal'` | `'vertical'` | |
2384
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2385
+ | className | `string` | — | |
2386
+
2387
+ Also accepts the props of `PressableProps`.
2388
+ <!-- /api:ProductCard -->
2347
2389
 
2348
2390
  ## PricingCard
2349
2391
 
@@ -2382,16 +2424,24 @@ import { SubscriptionCard } from '@thewhileloop/whileui';
2382
2424
  />;
2383
2425
  ```
2384
2426
 
2385
- | Prop | Type | Description |
2386
- | ----------- | ------------ | --------------------------- |
2387
- | `planName` | `string` | Current plan label |
2388
- | `price` | `string` | Plan price |
2389
- | `period` | `string` | Billing period label |
2390
- | `expiresAt` | `string` | Renewal/expiry date text |
2391
- | `isActive` | `boolean` | Active/inactive badge state |
2392
- | `onManage` | `() => void` | Manage action |
2393
- | `onUpgrade` | `() => void` | Upgrade action |
2394
- | `loading` | `boolean` | Show skeleton placeholder |
2427
+ <!-- api:SubscriptionCard -->
2428
+
2429
+ | Prop | Type | Default | Description |
2430
+ | ------------------- | ------------ | --------------- | ----------------------------------------------- |
2431
+ | planName (required) | `string` | — | Current plan label. |
2432
+ | price (required) | `string` | — | Plan price. |
2433
+ | period | `string` | `'/month'` | Billing period label. |
2434
+ | expiresAt | `string` | — | Renewal/expiry date text. |
2435
+ | isActive | `boolean` | `true` | Active/inactive badge state. |
2436
+ | onManage | `() => void` | — | Manage action. |
2437
+ | onUpgrade | `() => void` | — | Upgrade action. |
2438
+ | manageLabel | `string` | `'Manage plan'` | |
2439
+ | upgradeLabel | `string` | `'Upgrade'` | |
2440
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2441
+ | className | `string` | — | |
2442
+
2443
+ Also accepts the props of `ViewProps`.
2444
+ <!-- /api:SubscriptionCard -->
2395
2445
 
2396
2446
  ## FeatureGate
2397
2447
 
@@ -2416,12 +2466,18 @@ import { UsageBar } from '@thewhileloop/whileui';
2416
2466
  <UsageBar label="AI generations" used={8} limit={20} />;
2417
2467
  ```
2418
2468
 
2419
- | Prop | Type | Default | Description |
2420
- | --------- | -------------------------------------- | ------- | ------------------------------ |
2421
- | `label` | `string` | — | Usage label |
2422
- | `used` | `number` | — | Used amount |
2423
- | `limit` | `number` | — | Quota limit |
2424
- | `variant` | `'default' \| 'warning' \| 'exceeded'` | auto | Optional explicit visual state |
2469
+ <!-- api:UsageBar -->
2470
+
2471
+ | Prop | Type | Default | Description |
2472
+ | ---------------- | ----------------- | ------- | ------------------------------- |
2473
+ | label (required) | `string` | — | Usage label. |
2474
+ | used (required) | `number` | — | Used amount. |
2475
+ | limit (required) | `number` | — | Quota limit. |
2476
+ | variant | `UsageBarVariant` | — | Optional explicit visual state. |
2477
+ | className | `string` | — | |
2478
+
2479
+ Also accepts the props of `ViewProps`.
2480
+ <!-- /api:UsageBar -->
2425
2481
 
2426
2482
  ## PlanToggle
2427
2483
 
@@ -2474,48 +2530,600 @@ import { DrawerMenu } from '@thewhileloop/whileui';
2474
2530
  />;
2475
2531
  ```
2476
2532
 
2477
- | Prop | Type | Default |
2478
- | --------------- | ------------------------------------------------------------------ | ---------------------- |
2479
- | `frosted` | `boolean` | `false` |
2480
- | `blurIntensity` | `number` | medium preset |
2481
- | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
2533
+ <!-- api:DrawerMenu -->
2534
+
2535
+ | Prop | Type | Default | Description |
2536
+ | ------------------- | ----------------------- | ---------------------- | ----------------------------------------------------------- |
2537
+ | visible (required) | `boolean` | — | |
2538
+ | onClose (required) | `() => void` | — | |
2539
+ | sections (required) | `DrawerMenuSection[]` | — | |
2540
+ | activeKey | `string` | — | |
2541
+ | onSelect | `(key: string) => void` | — | |
2542
+ | header | `React.ReactNode` | — | |
2543
+ | footer | `React.ReactNode` | — | |
2544
+ | maxWidth | `number` | — | Max width in px. On web, defaults to 360 when not provided. |
2545
+ | className | `string` | — | |
2546
+ | style | `StyleProp<ViewStyle>` | — | |
2547
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2548
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2549
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2550
+
2551
+ <!-- /api:DrawerMenu -->
2482
2552
 
2483
2553
  Closes from the backdrop, an item, Android back or Escape on web. It is hosted in a modal
2484
2554
  on every platform, so keyboard focus stays inside it while it is open.
2485
2555
 
2486
- ## Roadmap
2556
+ ## More components
2557
+
2558
+ The rest of the catalog: what each is for and the props its type declares.
2559
+
2560
+ ### View
2561
+
2562
+ Themed view wrapper
2563
+
2564
+ ```tsx
2565
+ import { View } from '@thewhileloop/whileui';
2566
+ ```
2567
+
2568
+ <!-- api:View -->
2569
+
2570
+ | Prop | Type | Default | Description |
2571
+ | --------- | -------- | ------- | ----------- |
2572
+ | className | `string` | — | |
2573
+
2574
+ Also accepts the props of React Native's `ViewProps`.
2575
+ <!-- /api:View -->
2576
+
2577
+ ### Stack
2578
+
2579
+ Vertical flex layout with gap
2580
+
2581
+ ```tsx
2582
+ import { Stack } from '@thewhileloop/whileui';
2583
+ ```
2584
+
2585
+ <!-- api:Stack -->
2586
+
2587
+ | Prop | Type | Default | Description |
2588
+ | --------- | ------------------------------------------------------------------- | ----------- | ----------- |
2589
+ | className | `string` | — | |
2590
+ | gap | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | `'md'` | |
2591
+ | align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | `'stretch'` | |
2592
+ | justify | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | `'start'` | |
2593
+
2594
+ Also accepts the props of `ViewProps`.
2595
+ <!-- /api:Stack -->
2596
+
2597
+ ### MotionProvider
2598
+
2599
+ System reduced motion, explicit preview modes, and deterministic testing
2600
+
2601
+ ```tsx
2602
+ import { MotionProvider } from '@thewhileloop/whileui';
2603
+ ```
2604
+
2605
+ <!-- api:MotionProvider -->
2606
+
2607
+ | Prop | Type | Default | Description |
2608
+ | ------------------- | ----------------- | ---------- | ----------- |
2609
+ | children (required) | `React.ReactNode` | — | |
2610
+ | mode | `MotionMode` | `'system'` | |
2611
+
2612
+ <!-- /api:MotionProvider -->
2613
+
2614
+ ### Textarea
2615
+
2616
+ Multi-line text input
2487
2617
 
2488
- Tracked work items for future releases.
2618
+ ```tsx
2619
+ import { Textarea } from '@thewhileloop/whileui';
2620
+ ```
2621
+
2622
+ <!-- api:Textarea -->
2623
+
2624
+ | Prop | Type | Default | Description |
2625
+ | ------------ | --------------------------- | ----------- | ----------- |
2626
+ | aria-invalid | `boolean` | — | |
2627
+ | className | `string` | — | |
2628
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
2629
+
2630
+ Also accepts the props of `TextInputProps`.
2631
+ <!-- /api:Textarea -->
2632
+
2633
+ ### Label
2634
+
2635
+ Form field label
2636
+
2637
+ ```tsx
2638
+ import { Label } from '@thewhileloop/whileui';
2639
+ ```
2640
+
2641
+ <!-- api:Label -->
2642
+
2643
+ | Prop | Type | Default | Description |
2644
+ | --------- | -------- | ------- | ----------- |
2645
+ | className | `string` | — | |
2646
+
2647
+ Also accepts the props of `TextProps`.
2648
+ <!-- /api:Label -->
2649
+
2650
+ ### Toggle
2651
+
2652
+ ToggleText sub-component
2653
+
2654
+ ```tsx
2655
+ import { Toggle } from '@thewhileloop/whileui';
2656
+ ```
2657
+
2658
+ <!-- api:Toggle -->
2659
+
2660
+ | Prop | Type | Default | Description |
2661
+ | --------------- | ------------------------------------------------ | ----------- | ----------- |
2662
+ | pressed | `boolean` | `false` | |
2663
+ | defaultPressed | `boolean` | `false` | |
2664
+ | onPressedChange | `(pressed: boolean) => void` | — | |
2665
+ | variant | `VariantProps<typeof toggleVariants>['variant']` | `'default'` | |
2666
+ | size | `VariantProps<typeof toggleVariants>['size']` | `'default'` | |
2667
+ | className | `string` | — | |
2668
+ | children | `React.ReactNode` | — | |
2669
+
2670
+ Also accepts the props of `PressableProps`.
2671
+ <!-- /api:Toggle -->
2672
+
2673
+ ### ToggleGroup
2674
+
2675
+ Group of toggle items
2676
+
2677
+ ```tsx
2678
+ import { ToggleGroup } from '@thewhileloop/whileui';
2679
+ ```
2680
+
2681
+ <!-- api:ToggleGroup -->
2682
+
2683
+ | Prop | Type | Default | Description |
2684
+ | ------------- | ------------------------------------- | ----------- | ----------- |
2685
+ | type | `ToggleGroupType` | `'single'` | |
2686
+ | defaultValue | `string \| string[]` | — | |
2687
+ | value | `string \| string[]` | — | |
2688
+ | onValueChange | `(value: string \| string[]) => void` | — | |
2689
+ | variant | `'default' \| 'outline'` | `'default'` | |
2690
+ | size | `'default' \| 'sm' \| 'lg'` | `'default'` | |
2691
+ | className | `string` | — | |
2692
+
2693
+ Also accepts the props of `ViewProps`.
2694
+ <!-- /api:ToggleGroup -->
2695
+
2696
+ ### Separator
2697
+
2698
+ Themed divider
2699
+
2700
+ ```tsx
2701
+ import { Separator } from '@thewhileloop/whileui';
2702
+ ```
2703
+
2704
+ <!-- api:Separator -->
2705
+
2706
+ | Prop | Type | Default | Description |
2707
+ | ----------- | ---------------------------- | -------------- | ----------- |
2708
+ | orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | |
2709
+ | className | `string` | — | |
2710
+
2711
+ Also accepts the props of `ViewProps`.
2712
+ <!-- /api:Separator -->
2713
+
2714
+ ### Spinner
2715
+
2716
+ ActivityIndicator wrapper
2717
+
2718
+ ```tsx
2719
+ import { Spinner } from '@thewhileloop/whileui';
2720
+ ```
2721
+
2722
+ <!-- api:Spinner -->
2723
+
2724
+ | Prop | Type | Default | Description |
2725
+ | --------- | --------------------------- | ----------- | ----------- |
2726
+ | className | `string` | — | |
2727
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
2728
+
2729
+ Also accepts the props of `ActivityIndicatorProps`.
2730
+ <!-- /api:Spinner -->
2731
+
2732
+ ### Skeleton
2733
+
2734
+ Loading placeholder (pulse = opacity fade, shimmer = sweep)
2735
+
2736
+ ```tsx
2737
+ import { Skeleton } from '@thewhileloop/whileui';
2738
+ ```
2739
+
2740
+ <!-- api:Skeleton -->
2741
+
2742
+ | Prop | Type | Default | Description |
2743
+ | --------- | ---------------------- | --------- | ----------- |
2744
+ | className | `string` | — | |
2745
+ | variant | `'pulse' \| 'shimmer'` | `'pulse'` | |
2746
+
2747
+ Also accepts the props of `ViewProps`.
2748
+ <!-- /api:Skeleton -->
2749
+
2750
+ ### AspectRatio
2751
+
2752
+ Maintain aspect ratio container
2753
+
2754
+ ```tsx
2755
+ import { AspectRatio } from '@thewhileloop/whileui';
2756
+ ```
2757
+
2758
+ <!-- api:AspectRatio -->
2759
+
2760
+ | Prop | Type | Default | Description |
2761
+ | --------- | -------- | ------- | ----------- |
2762
+ | ratio | `number` | `1` | |
2763
+ | className | `string` | — | |
2764
+
2765
+ Also accepts the props of `ViewProps`.
2766
+ <!-- /api:AspectRatio -->
2767
+
2768
+ ### Collapsible
2769
+
2770
+ CollapsibleTrigger, CollapsibleContent
2771
+
2772
+ ```tsx
2773
+ import { Collapsible } from '@thewhileloop/whileui';
2774
+ ```
2775
+
2776
+ <!-- api:Collapsible -->
2777
+
2778
+ | Prop | Type | Default | Description |
2779
+ | ------------ | ------------------------- | ------- | ----------- |
2780
+ | defaultOpen | `boolean` | `false` | |
2781
+ | open | `boolean` | — | |
2782
+ | onOpenChange | `(open: boolean) => void` | — | |
2783
+ | disabled | `boolean` | `false` | |
2784
+ | className | `string` | — | |
2785
+
2786
+ Also accepts the props of `ViewProps`.
2787
+ <!-- /api:Collapsible -->
2788
+
2789
+ ### Tooltip
2790
+
2791
+ Position-aware tooltip (requires PortalHost)
2792
+
2793
+ ```tsx
2794
+ import { Tooltip } from '@thewhileloop/whileui';
2795
+ ```
2796
+
2797
+ <!-- api:Tooltip -->
2798
+
2799
+ | Prop | Type | Default | Description |
2800
+ | ----------------------- | ------------------------- | ------- | ----------- |
2801
+ | onOpenChange | `(open: boolean) => void` | — | |
2802
+ | delayDuration | `number` | — | |
2803
+ | skipDelayDuration | `number` | — | |
2804
+ | disableHoverableContent | `boolean` | — | |
2805
+ | children (required) | `React.ReactNode` | — | |
2806
+
2807
+ <!-- /api:Tooltip -->
2808
+
2809
+ ### FloatingBottomNav
2810
+
2811
+ Elevated bottom nav with safe area support
2812
+
2813
+ ```tsx
2814
+ import { FloatingBottomNav } from '@thewhileloop/whileui';
2815
+ ```
2816
+
2817
+ <!-- api:FloatingBottomNav -->
2818
+
2819
+ | Prop | Type | Default | Description |
2820
+ | -------------------- | ------------------------- | ------- | ----------- |
2821
+ | items (required) | `FloatingBottomNavItem[]` | — | |
2822
+ | activeKey (required) | `string` | — | |
2823
+ | onSelect (required) | `(key: string) => void` | — | |
2824
+ | className | `string` | — | |
2825
+
2826
+ Also accepts the props of `ViewProps`.
2827
+ <!-- /api:FloatingBottomNav -->
2828
+
2829
+ ### TabBar
2830
+
2831
+ Top tab bar with indicator
2832
+
2833
+ ```tsx
2834
+ import { TabBar } from '@thewhileloop/whileui';
2835
+ ```
2836
+
2837
+ <!-- api:TabBar -->
2489
2838
 
2490
- ### New Components
2839
+ | Prop | Type | Default | Description |
2840
+ | -------------------- | ------------------------------------- | ----------- | ----------- |
2841
+ | items (required) | `TabBarItem[]` | — | |
2842
+ | activeKey (required) | `string` | — | |
2843
+ | onSelect (required) | `(key: string) => void` | — | |
2844
+ | variant | `'default' \| 'pills' \| 'underline'` | `'default'` | |
2845
+ | scrollable | `boolean` | `false` | |
2846
+ | className | `string` | — | |
2491
2847
 
2492
- - [ ] **Chip / Tag** — selectable, dismissible, multi-select with `tv()` variants, loading skeleton
2493
- - [ ] **Rating / Stars** — interactive + read-only, half-star precision, swipe gesture, pairs with ProductCard
2494
- - [ ] **Slider** — single + range (two thumbs), step marks, labels, Reanimated gesture, haptic on snap
2495
- - [ ] **Carousel** — Reanimated-powered, auto-play, pagination dots, snap-to-item
2496
- - [ ] **FAB (Floating Action Button)** — expandable action menu, Reanimated spring, auto-hide on scroll
2497
- - [ ] **Combobox** — searchable select with keyboard navigation, empty state, async loading
2498
- - [ ] **Data Table** — sortable headers, skeleton loading rows, row actions, responsive stacking
2499
- - [ ] **Banner** — dismissible info/warning/success/destructive bar with icon + action, auto-dismiss timer
2500
- - [ ] **Pagination** — compact (dots) + expanded (numbers) variants, edge-aware ellipsis, 44px touch targets
2501
- - [ ] **Inline Calendar** — standalone calendar view reusing DatePicker logic, range selection
2848
+ Also accepts the props of `ViewProps`.
2849
+ <!-- /api:TabBar -->
2502
2850
 
2503
- ### Component-Level Loading (`loading` prop)
2851
+ ### FormModalScreen
2504
2852
 
2505
- - [x] ProductCard, ListItem, NotificationItem, MetricCard, SubscriptionCard, PricingCard
2506
- - [x] AppShell, Chat, FormModalScreen, CheckoutSummary (already had `loading`)
2507
- - [ ] Add `loading` to remaining blocks: Header, BottomNav, DrawerMenu, TimelineFeed
2853
+ Modal scaffold for forms with loading states
2508
2854
 
2509
- ### Documentation Gaps
2855
+ ```tsx
2856
+ import { FormModalScreen } from '@thewhileloop/whileui';
2857
+ ```
2858
+
2859
+ <!-- api:FormModalScreen -->
2860
+
2861
+ | Prop | Type | Default | Description |
2862
+ | ------------------- | ----------------- | ------------- | ----------------------------------------------------------------------------------------- |
2863
+ | className | `string` | — | |
2864
+ | title (required) | `string` | — | |
2865
+ | subtitle | `string` | — | |
2866
+ | leftSlot | `React.ReactNode` | — | |
2867
+ | rightSlot | `React.ReactNode` | — | |
2868
+ | onClose | `() => void` | — | |
2869
+ | loading | `boolean` | `false` | |
2870
+ | saving | `boolean` | `false` | |
2871
+ | savingText | `string` | `'Saving...'` | |
2872
+ | loadingSpinnerColor | `string` | — | Hex color for spinner in loading state. ActivityIndicator requires hex. |
2873
+ | savingSpinnerColor | `string` | — | Hex color for spinner in saving banner. Defaults to platform (Android blue) when not set. |
2874
+ | scrollEnabled | `boolean` | `true` | |
2875
+ | children (required) | `React.ReactNode` | — | |
2876
+
2877
+ Also accepts the props of `ViewProps`.
2878
+ <!-- /api:FormModalScreen -->
2879
+
2880
+ ### ErrorState
2881
+
2882
+ Error display with retry
2883
+
2884
+ ```tsx
2885
+ import { ErrorState } from '@thewhileloop/whileui';
2886
+ ```
2887
+
2888
+ <!-- api:ErrorState -->
2889
+
2890
+ | Prop | Type | Default | Description |
2891
+ | ----------- | ----------------- | ------------------------ | ----------- |
2892
+ | icon | `React.ReactNode` | — | |
2893
+ | title | `string` | `'Something went wrong'` | |
2894
+ | description | `string` | — | |
2895
+ | error | `Error \| string` | — | |
2896
+ | onRetry | `() => void` | — | |
2897
+ | retryLabel | `string` | `'Try again'` | |
2898
+ | className | `string` | — | |
2899
+
2900
+ Also accepts the props of `ViewProps`.
2901
+ <!-- /api:ErrorState -->
2902
+
2903
+ ### LoadingScreen
2904
+
2905
+ Full-screen loading indicator
2906
+
2907
+ ```tsx
2908
+ import { LoadingScreen } from '@thewhileloop/whileui';
2909
+ ```
2910
+
2911
+ <!-- api:LoadingScreen -->
2912
+
2913
+ | Prop | Type | Default | Description |
2914
+ | ------------ | ---------------------- | ------- | ------------------------------------------------------ |
2915
+ | message | `string` | — | |
2916
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | |
2917
+ | spinnerColor | `string` | — | Hex color for spinner. ActivityIndicator requires hex. |
2918
+ | className | `string` | — | |
2919
+
2920
+ Also accepts the props of `ViewProps`.
2921
+ <!-- /api:LoadingScreen -->
2922
+
2923
+ ### MinimalSplash
2924
+
2925
+ Minimal monochrome splash
2926
+
2927
+ ```tsx
2928
+ import { MinimalSplash } from '@thewhileloop/whileui';
2929
+ ```
2930
+
2931
+ <!-- api:MinimalSplash -->
2932
+
2933
+ | Prop | Type | Default | Description |
2934
+ | ------------------- | ----------------- | ------- | ----------------------------------------------- |
2935
+ | className | `string` | — | |
2936
+ | appName | `string` | — | App name displayed below logo |
2937
+ | tagline | `string` | — | Tagline displayed below app name |
2938
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2939
+ | duration | `number` | `800` | Animation duration in ms |
2940
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2941
+ | showLoading | `boolean` | `false` | Whether to show loading indicator |
2942
+ | loadingText | `string` | — | Loading text |
2943
+
2944
+ Also accepts the props of `SplashScreenProps`.
2945
+ <!-- /api:MinimalSplash -->
2946
+
2947
+ ### BrandedSplash
2948
+
2949
+ Splash with brand imagery
2950
+
2951
+ ```tsx
2952
+ import { BrandedSplash } from '@thewhileloop/whileui';
2953
+ ```
2954
+
2955
+ <!-- api:BrandedSplash -->
2956
+
2957
+ | Prop | Type | Default | Description |
2958
+ | ------------------- | ----------------- | ------- | ----------------------------------------------- |
2959
+ | className | `string` | — | |
2960
+ | appName | `string` | — | App name displayed below logo |
2961
+ | tagline | `string` | — | Tagline displayed below app name |
2962
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2963
+ | duration | `number` | `800` | Animation duration in ms |
2964
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2965
+ | loadingText | `string` | — | Loading text |
2966
+
2967
+ Also accepts the props of `SplashScreenProps`.
2968
+ <!-- /api:BrandedSplash -->
2969
+
2970
+ ### ChatMessageBubble
2971
+
2972
+ One message in a `Chat`, rendered on its own when a screen composes the conversation itself.
2973
+
2974
+ ```tsx
2975
+ import { ChatMessageBubble } from '@thewhileloop/whileui';
2976
+ ```
2977
+
2978
+ <!-- api:ChatMessageBubble -->
2979
+
2980
+ | Prop | Type | Default | Description |
2981
+ | ------------------ | ------------- | ------- | --------------------------------------------------------------- |
2982
+ | message (required) | `ChatMessage` | — | The message to render; its `role` decides alignment and colour. |
2983
+ | contentClassName | `string` | — | Classes for the bubble's content wrapper. |
2984
+
2985
+ <!-- /api:ChatMessageBubble -->
2986
+
2987
+ ### ChatSuggestions
2988
+
2989
+ Suggestion chips when empty
2990
+
2991
+ ```tsx
2992
+ import { ChatSuggestions } from '@thewhileloop/whileui';
2993
+ ```
2994
+
2995
+ <!-- api:ChatSuggestions -->
2996
+
2997
+ | Prop | Type | Default | Description |
2998
+ | ---------------------- | ------------------------ | ------------------- | ----------- |
2999
+ | suggestions (required) | `string[]` | `EMPTY_SUGGESTIONS` | |
3000
+ | onSelect (required) | `(text: string) => void` | — | |
3001
+ | className | `string` | — | |
3002
+
3003
+ <!-- /api:ChatSuggestions -->
3004
+
3005
+ ### ListItem
3006
+
3007
+ Title/subtitle row
3008
+
3009
+ ```tsx
3010
+ import { ListItem } from '@thewhileloop/whileui';
3011
+ ```
3012
+
3013
+ <!-- api:ListItem -->
3014
+
3015
+ | Prop | Type | Default | Description |
3016
+ | ---------------- | ----------------- | ------- | ----------------------------------------------- |
3017
+ | icon | `React.ReactNode` | — | |
3018
+ | title (required) | `string` | — | |
3019
+ | subtitle | `string` | — | |
3020
+ | description | `string` | — | |
3021
+ | rightIcon | `React.ReactNode` | — | |
3022
+ | rightText | `string` | — | |
3023
+ | action | `React.ReactNode` | — | |
3024
+ | showBorder | `boolean` | `true` | |
3025
+ | compact | `boolean` | `false` | |
3026
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
3027
+ | className | `string` | — | |
3028
+
3029
+ Also accepts the props of `PressableProps`.
3030
+ <!-- /api:ListItem -->
3031
+
3032
+ ### NotificationItem
3033
+
3034
+ Notification row with metadata
3035
+
3036
+ ```tsx
3037
+ import { NotificationItem } from '@thewhileloop/whileui';
3038
+ ```
3039
+
3040
+ <!-- api:NotificationItem -->
3041
+
3042
+ | Prop | Type | Default | Description |
3043
+ | ------------------ | ----------------- | ------- | ----------------------------------------------- |
3044
+ | icon | `React.ReactNode` | — | |
3045
+ | avatarUrl | `string` | — | |
3046
+ | avatarFallback | `string` | — | |
3047
+ | title (required) | `string` | — | |
3048
+ | message (required) | `string` | — | |
3049
+ | time (required) | `string` | — | |
3050
+ | read | `boolean` | `false` | |
3051
+ | showDot | `boolean` | `true` | |
3052
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
3053
+ | className | `string` | — | |
3054
+
3055
+ Also accepts the props of `PressableProps`.
3056
+ <!-- /api:NotificationItem -->
3057
+
3058
+ ### TimelineFeed
3059
+
3060
+ Vertical feed with connecting lines
3061
+
3062
+ ```tsx
3063
+ import { TimelineFeed } from '@thewhileloop/whileui';
3064
+ ```
3065
+
3066
+ <!-- api:TimelineFeed -->
3067
+
3068
+ | Prop | Type | Default | Description |
3069
+ | ---------------- | ---------------- | ------- | ----------- |
3070
+ | className | `string` | — | |
3071
+ | items (required) | `TimelineItem[]` | — | |
3072
+ | showConnector | `boolean` | `true` | |
3073
+
3074
+ Also accepts the props of `ViewProps`.
3075
+ <!-- /api:TimelineFeed -->
3076
+
3077
+ ### CheckoutSummary
3078
+
3079
+ Cart summary with line items
3080
+
3081
+ ```tsx
3082
+ import { CheckoutSummary } from '@thewhileloop/whileui';
3083
+ ```
3084
+
3085
+ <!-- api:CheckoutSummary -->
3086
+
3087
+ | Prop | Type | Default | Description |
3088
+ | ------------------- | -------------------- | ------------ | ----------- |
3089
+ | items (required) | `CheckoutLineItem[]` | — | |
3090
+ | subtotal (required) | `string` | — | |
3091
+ | shipping | `string` | — | |
3092
+ | tax | `string` | — | |
3093
+ | discount | `string` | — | |
3094
+ | total (required) | `string` | — | |
3095
+ | buttonLabel | `string` | `'Checkout'` | |
3096
+ | onCheckout | `() => void` | — | |
3097
+ | loading | `boolean` | `false` | |
3098
+ | className | `string` | — | |
3099
+
3100
+ Also accepts the props of `ViewProps`.
3101
+ <!-- /api:CheckoutSummary -->
3102
+
3103
+ ### MetricCard
3104
+
3105
+ Stats/progress card for dashboards
3106
+
3107
+ ```tsx
3108
+ import { MetricCard } from '@thewhileloop/whileui';
3109
+ ```
2510
3110
 
2511
- - [ ] Add `## API` sections for: Textarea, Toggle, ToggleGroup, Label, Separator, Skeleton, AspectRatio, Collapsible, Text, View
2512
- - [ ] Add `## API` sections for blocks: FloatingBottomNav, TabBar, FormModalScreen, ErrorState, LoadingScreen, ListItem, NotificationItem, MetricCard, TimelineFeed
3111
+ <!-- api:MetricCard -->
2513
3112
 
2514
- ### Infrastructure
3113
+ | Prop | Type | Default | Description |
3114
+ | ---------------- | ------------------------------------ | ----------- | ----------------------------------------------- |
3115
+ | className | `string` | — | |
3116
+ | label (required) | `string` | — | |
3117
+ | value (required) | `string \| number` | — | |
3118
+ | subtitle | `string` | — | |
3119
+ | progress | `number` | — | |
3120
+ | segments | `{ value: number; color: string }[]` | — | |
3121
+ | icon | `React.ReactNode` | — | |
3122
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
3123
+ | variant | `'default' \| 'outlined'` | `'default'` | |
2515
3124
 
2516
- - [ ] Publish to npm (`@thewhileloop/whileui`)
2517
- - [ ] CI: typecheck + format check on PR
2518
- - [ ] Automated visual regression tests (screenshot comparison)
3125
+ Also accepts the props of `ViewProps`.
3126
+ <!-- /api:MetricCard -->
2519
3127
 
2520
3128
  ## License
2521
3129