@thewhileloop/whileui 4.2.3 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +1235 -663
  2. package/dist/agent/catalog.json +1737 -0
  3. package/dist/agent/llms-components.txt +2787 -0
  4. package/dist/agent/llms-full.txt +2962 -0
  5. package/dist/agent/llms.txt +152 -0
  6. package/dist/blocks/chat/chat.d.ts +10 -2
  7. package/dist/blocks/chat/chat.js +1 -1
  8. package/dist/blocks/chat/index.d.ts +1 -1
  9. package/dist/blocks/commerce/feature-gate.js +2 -2
  10. package/dist/blocks/commerce/metric-card.js +1 -1
  11. package/dist/blocks/commerce/subscription-card.d.ts +7 -0
  12. package/dist/blocks/commerce/usage-bar.d.ts +4 -0
  13. package/dist/blocks/layout/app-shell.d.ts +4 -0
  14. package/dist/blocks/layout/empty-state.js +1 -1
  15. package/dist/blocks/layout/error-state.js +1 -1
  16. package/dist/blocks/layout/page-skeleton.d.ts +1 -0
  17. package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
  18. package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
  19. package/dist/blocks/lists/list-item.js +1 -1
  20. package/dist/blocks/lists/notification-item.js +1 -1
  21. package/dist/blocks/media/smart-image.js +4 -2
  22. package/dist/blocks/navigation/bottom-nav.js +1 -1
  23. package/dist/blocks/navigation/drawer-menu.js +1 -1
  24. package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
  25. package/dist/blocks/navigation/tab-bar.js +1 -1
  26. package/dist/components/alert/alert.d.ts +7 -0
  27. package/dist/components/alert/alert.js +23 -4
  28. package/dist/components/button/button.d.ts +3 -0
  29. package/dist/components/button/button.js +2 -1
  30. package/dist/components/form-field/form-field.d.ts +18 -0
  31. package/dist/components/form-field/form-field.js +25 -4
  32. package/dist/components/input/input.js +2 -2
  33. package/dist/components/numeric-input/numeric-input.d.ts +3 -0
  34. package/dist/components/numeric-input/numeric-input.js +2 -2
  35. package/dist/components/otp-input/otp-input.d.ts +8 -0
  36. package/dist/components/pressable/pressable.d.ts +1 -0
  37. package/dist/components/segmented-control/segmented-control.d.ts +1 -0
  38. package/dist/components/surface/surface.d.ts +5 -0
  39. package/dist/components/textarea/textarea.js +2 -2
  40. package/dist/components/toast/toast.d.ts +10 -10
  41. package/dist/components/toast/toast.js +53 -35
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.js +1 -0
  44. package/dist/lib/frosted-surface.d.ts +3 -0
  45. package/dist/lib/theme-contract.js +12 -12
  46. package/dist/lib/theme-presets.js +9 -9
  47. package/dist/lib/theme-seeds.d.ts +40 -0
  48. package/dist/lib/theme-seeds.js +143 -0
  49. package/dist/metro.cjs +5 -4
  50. package/dist/tokens.d.ts +1 -0
  51. package/dist/tokens.js +1 -0
  52. package/package.json +3 -2
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
  | ------------------- | ---------------------- | ----------------------------------- |
@@ -545,11 +497,17 @@ import { Stack, Row } from '@thewhileloop/whileui';
545
497
  </Stack>;
546
498
  ```
547
499
 
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 |
500
+ <!-- api:Row -->
501
+
502
+ | Prop | Type | Default | Description |
503
+ | --------- | ------------------------------------------------------------------- | ---------- | ----------- |
504
+ | className | `string` | — | |
505
+ | gap | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | `'md'` | |
506
+ | align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | `'center'` | |
507
+ | justify | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | `'start'` | |
508
+
509
+ Also accepts the props of `ViewProps`.
510
+ <!-- /api:Row -->
553
511
 
554
512
  ### Container
555
513
 
@@ -561,11 +519,16 @@ controls drift apart.
561
519
  <Container size="lg">{children}</Container>
562
520
  ```
563
521
 
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 |
522
+ <!-- api:Container -->
523
+
524
+ | Prop | Type | Default | Description |
525
+ | --------- | ---------------------------------------- | ------- | ----------- |
526
+ | className | `string` | — | |
527
+ | size | `'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | |
528
+ | fill | `boolean` | `false` | |
529
+
530
+ Also accepts the props of `ViewProps`.
531
+ <!-- /api:Container -->
569
532
 
570
533
  No platform branch, and none is needed: every size is wider than any phone, so on iOS
571
534
  and Android the cap never engages and it renders as a plain full-width `View`. The same
@@ -630,142 +593,51 @@ Blocks with `loading` support: ProductCard, ListItem, NotificationItem, MetricCa
630
593
 
631
594
  Block props: see TypeScript interfaces in `packages/ui/src/blocks`.
632
595
 
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
596
  ## Theming
684
597
 
685
- Themes are defined in `global.css` using CSS variables with OKLCH colors:
598
+ A theme is a set of CSS variables in `global.css`, in OKLCH, with a `light` and a `dark`
599
+ variant. Every component reads the same contract, so a theme must define **every** token
600
+ in both modes: a missing one silently falls back to an unstyled value at runtime.
686
601
 
687
- ```css
688
- @variant light {
689
- --color-primary: oklch(0.6 0.2 160);
690
- --color-background: oklch(1 0 0);
691
- /* ... */
692
- }
602
+ ### Generate a theme
693
603
 
694
- @variant dark {
695
- --color-primary: oklch(0.6 0.2 160);
696
- --color-background: oklch(0.145 0 0);
697
- /* ... */
698
- }
699
- ```
604
+ `themeFromSeeds` builds a complete light and dark theme from three colours, and
605
+ `themePresetToCss` writes every token, including the hex fallbacks React Native APIs need,
606
+ ready to paste into `global.css`:
700
607
 
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-*`)
710
-
711
- Minimal contract example:
712
-
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:
742
-
743
- ```tsx
744
- import { Uniwind } from 'uniwind';
608
+ ```ts
609
+ import { checkThemeContrast, themeFromSeeds, themePresetToCss } from '@thewhileloop/whileui/tokens';
610
+
611
+ const theme = themeFromSeeds({
612
+ name: 'studio',
613
+ description: 'Bone paper and burnt amber',
614
+ light: { background: '#f0ede9', foreground: '#13161c', primary: '#b65400' },
615
+ // optional: `dark` seeds (otherwise a dark page with the brand lifted to read on it),
616
+ // `overrides` for any base colour, `visual` for radius and density tokens
617
+ });
745
618
 
746
- Uniwind.setTheme('dark'); // or 'light' or 'system'
619
+ checkThemeContrast(theme.light).filter((c) => !c.pass);
620
+ // [{ pair: 'primary on background', ratio: 4.23, min: 4.5, pass: false }, …]
621
+ const css = themePresetToCss(theme); // paste into global.css
747
622
  ```
748
623
 
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.
624
+ Seeds are kept as given; the colours it derives (muted text, text on each fill, status
625
+ colours) are tuned to WCAG AA. `checkThemeContrast` checks any palette, hand-written ones
626
+ included: every text pairing at 4.5:1 and the focus ring at 3:1. A failing seed is
627
+ reported, never changed, because a brand colour is a brand decision. The built-in presets,
628
+ `WHILEUI_PRESET_SOFT` and `WHILEUI_PRESET_ATELIER`, go through `themePresetToCss` the same
629
+ way.
752
630
 
753
- **Option A — Override in existing light/dark:** In your `@variant light` and `@variant dark` blocks, set surface tokens to semi-transparent values:
631
+ Switch at runtime with `Uniwind.setTheme('dark')` (or `'light'`, `'system'`). A named
632
+ extra theme is registered with `extraThemes: ['name']` in `metro.config.js` and defined
633
+ with `@variant name`. The full token list, with each default, is in `theme-contract.ts`,
634
+ and agents can read it from the MCP server's `get_theme_tokens`.
754
635
 
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
- }
762
- ```
636
+ ### Frosted surfaces
763
637
 
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.
765
-
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.
767
-
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.
638
+ Cards and overlays take `frosted`, `blurIntensity` and `blurTintToken`
639
+ (`'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover'`). Without a blur view
640
+ they tint only; register `expo-blur` once for real blur:
769
641
 
770
642
  ```tsx
771
643
  import { BlurView } from 'expo-blur';
@@ -774,62 +646,40 @@ import { registerFrostedBlurView } from '@thewhileloop/whileui';
774
646
  registerFrostedBlurView(BlurView);
775
647
  ```
776
648
 
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:
649
+ For a frosted look across a whole theme, make `surface-elevated`, `surface-border` and
650
+ `surface-highlight` translucent, in your light and dark variants or in a separate theme.
651
+ The tuning tokens:
786
652
 
787
- - subtle: `--ui-blur-intensity-subtle` (default `18`)
788
- - medium: `--ui-blur-intensity-medium` (default `22`)
789
- - strong: `--ui-blur-intensity-strong` (default `28`)
653
+ | Token | Default | Effect |
654
+ | ------------------------------------------------------------------------- | ------------ | -------------------------------------------------------------- |
655
+ | `--ui-blur-intensity-subtle` / `-medium` / `-strong` | 18 / 26 / 34 | The three blur presets |
656
+ | `--ui-frosted-android-tint-alpha-scale` | 1.18 | Android needs denser tint; `1` matches iOS and web |
657
+ | `--ui-frosted-android-experimental-blur` | 1 | Uses `expo-blur`'s experimental Android blur; `0` turns it off |
658
+ | `--ui-frosted-highlight-height` | 2 | Top sheen in px; `0` removes it |
659
+ | `--ui-blur-saturation-pct` | 145 | Web saturation multiplier |
660
+ | `--ui-frosted-backdrop-blur-intensity`, `-scale` | 22, 0.72 | Backdrop blur, and its ratio when a component overrides blur |
661
+ | `--ui-drawer-frosted-inset`, `-radius`, `--ui-drawer-content-top-padding` | 10, 34, 0 | Frosted drawer shell |
790
662
 
791
- Additional frosted tuning tokens:
663
+ ### Colours for React Native APIs
792
664
 
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.
665
+ Icons, `ActivityIndicator` and other native APIs need hex or rgb, not OKLCH.
666
+ `useThemeColors()` returns the whole palette as React Native colours, falling back to the
667
+ `--app-color-*` tokens a generated theme includes; `useIconColors()` is the subset icons
668
+ use, with `muted` mapped to `mutedForeground`.
806
669
 
807
670
  ```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
671
+ const icons = useIconColors();
672
+ <Feather name="heart" size={20} color={icons.muted} />;
819
673
  ```
820
674
 
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`.
675
+ Input, Textarea, NumericInput, SmartInput, Spinner and LoadingScreen pick theme colours
676
+ when you omit `placeholderTextColor` or `spinnerColor`.
825
677
 
826
678
  ### Interaction and motion
827
679
 
828
680
  Press opacity, disabled opacity, motion durations, the responsive spring, and the drawer
829
681
  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.
682
+ variables, so a theme does not change them.
833
683
 
834
684
  For a custom control, read that table:
835
685
 
@@ -853,118 +703,6 @@ motion.durations.normal;
853
703
  motion.springs.responsive;
854
704
  ```
855
705
 
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
706
  ## Focus rings
969
707
 
970
708
  Every interactive component draws a themed focus ring on web. It uses `:focus-visible`,
@@ -992,14 +730,6 @@ supplies a themed replacement in the same class. The one exemption is a visually
992
730
  input, where a visible proxy shows the focus instead — `OTPInput` does this with its
993
731
  blinking cursor.
994
732
 
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
733
  # API Reference
1004
734
 
1005
735
  ## Pressable
@@ -1022,12 +752,17 @@ import { Pressable, Text } from '@thewhileloop/whileui';
1022
752
  `style` contracts. Feedback changes opacity without changing layout. Native press state
1023
753
  comes directly from React Native; web adds a motion-policy-aware opacity transition.
1024
754
 
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 |
755
+ <!-- api:Pressable -->
756
+
757
+ | Prop | Type | Default | Description |
758
+ | ---------------- | ----------------- | ----------- | --------------------------------------------------------------------------- |
759
+ | className | `string` | — | Additional Tailwind classes. |
760
+ | feedback | `PressedVariant` | `'none'` | Semantic press feedback. `none` preserves a plain React Native Pressable. |
761
+ | disabledFeedback | `DisabledVariant` | `'default'` | Opacity used while the control is unavailable. |
762
+ | loading | `boolean` | `false` | Loading controls are busy and non-interactive, just like disabled controls. |
763
+
764
+ Also accepts the props of React Native's `PressableProps`.
765
+ <!-- /api:Pressable -->
1031
766
 
1032
767
  ## Button
1033
768
 
@@ -1046,13 +781,20 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
1046
781
  </Button>;
1047
782
  ```
1048
783
 
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 |
784
+ <!-- api:Button -->
785
+
786
+ | Prop | Type | Default | Description |
787
+ | ---------------- | ----------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- |
788
+ | children | `React.ReactNode` | — | |
789
+ | className | `string` | — | Additional Tailwind classes. |
790
+ | disabled | `boolean` | `false` | Disable the button. |
791
+ | loading | `boolean` | `false` | Shows a centered spinner, exposes busy state, and prevents interaction. |
792
+ | disabledFeedback | `DisabledVariant` | — | Opacity used while the control is unavailable. |
793
+ | size | `'sm' \| 'lg' \| 'default' \| 'icon'` | `'default'` | |
794
+ | variant | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'` | |
795
+
796
+ Also accepts the props of `PressableProps`.
797
+ <!-- /api:Button -->
1056
798
 
1057
799
  ## Text
1058
800
 
@@ -1060,10 +802,16 @@ import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
1060
802
  classes are unnecessary for normal hierarchy. `heading` also sets
1061
803
  `accessibilityRole="header"` unless the caller supplies another role.
1062
804
 
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 |
805
+ <!-- api:Text -->
806
+
807
+ | Prop | Type | Default | Description |
808
+ | --------- | ---------------------------------------------------------------------------------------------------------- | ----------- | ----------- |
809
+ | className | `string` | — | |
810
+ | size | `'base' \| 'xs' \| 'sm' \| 'lg' \| 'xl' \| '2xl' \| '3xl'` | — | |
811
+ | variant | `'default' \| 'heading' \| 'title' \| 'subtitle' \| 'body' \| 'caption' \| 'label' \| 'eyebrow' \| 'code'` | `'default'` | |
812
+
813
+ Also accepts the props of React Native's `TextProps`.
814
+ <!-- /api:Text -->
1067
815
 
1068
816
  ## Input
1069
817
 
@@ -1076,12 +824,19 @@ import { Input } from '@thewhileloop/whileui';
1076
824
  On web, filled Input, Textarea, NumericInput, and SmartInput surfaces preserve their
1077
825
  themed background, foreground, and caret when the browser autofills them.
1078
826
 
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 |
827
+ <!-- api:Input -->
828
+
829
+ | Prop | Type | Default | Description |
830
+ | -------------- | --------------------------------- | ----------- | ----------- |
831
+ | aria-invalid | `boolean` | — | |
832
+ | className | `string` | — | |
833
+ | prefix | `React.ReactNode` | — | |
834
+ | suffix | `React.ReactNode` | — | |
835
+ | inputClassName | `string` | — | |
836
+ | variant | `'default' \| 'error' \| 'plain'` | `'default'` | |
837
+
838
+ Also accepts the props of `TextInputProps`.
839
+ <!-- /api:Input -->
1085
840
 
1086
841
  ## NumericInput
1087
842
 
@@ -1100,15 +855,28 @@ import { NumericInput } from '@thewhileloop/whileui';
1100
855
  />;
1101
856
  ```
1102
857
 
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 |
858
+ <!-- api:NumericInput -->
859
+
860
+ | Prop | Type | Default | Description |
861
+ | -------------- | --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
862
+ | aria-invalid | `boolean` | — | |
863
+ | className | `string` | — | |
864
+ | inputClassName | `string` | — | |
865
+ | prefix | `React.ReactNode` | — | |
866
+ | suffix | `React.ReactNode` | — | |
867
+ | value | `number \| null` | — | Controlled numeric value. |
868
+ | defaultValue | `number` | — | |
869
+ | onValueChange | `(value: number \| null) => void` | — | Numeric value change callback. |
870
+ | min | `number` | — | |
871
+ | max | `number` | — | |
872
+ | step | `number` | `1` | |
873
+ | showSteppers | `boolean` | `false` | Show decrement/increment controls. |
874
+ | unstyled | `boolean` | `false` | Drop the field shell (border, background, radius, padding) for embedding inside another field shell like LabeledField. |
875
+ | size | `'default' \| 'compact'` | `'default'` | |
876
+ | variant | `'default' \| 'error'` | `'default'` | |
877
+
878
+ Also accepts the props of `TextInputProps`.
879
+ <!-- /api:NumericInput -->
1112
880
 
1113
881
  ## OTPInput
1114
882
 
@@ -1124,16 +892,23 @@ import { OTPInput } from '@thewhileloop/whileui';
1124
892
  />;
1125
893
  ```
1126
894
 
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 |
895
+ <!-- api:OTPInput -->
896
+
897
+ | Prop | Type | Default | Description |
898
+ | ------------- | ------------------------- | ----------- | ---------------------------------- |
899
+ | length | `number` | `6` | Number of digit cells. |
900
+ | value | `string` | — | Controlled value. |
901
+ | onValueChange | `(value: string) => void` | — | Called on each digit change. |
902
+ | onComplete | `(code: string) => void` | — | Called when all digits are filled. |
903
+ | variant | `'default' \| 'error'` | `'default'` | Error variant triggers shake. |
904
+ | size | `'default' \| 'compact'` | `'default'` | Cell size. |
905
+ | secure | `boolean` | `false` | Mask digits with dots. |
906
+ | disabled | `boolean` | `false` | |
907
+ | autoFocus | `boolean` | `false` | Focus first cell on mount. |
908
+ | className | `string` | — | |
909
+
910
+ Also accepts the props of `ViewProps`.
911
+ <!-- /api:OTPInput -->
1137
912
 
1138
913
  ## FormField
1139
914
 
@@ -1161,6 +936,10 @@ import {
1161
936
  custom controls; labels, hints, messages, density, and required indicators remain part
1162
937
  of the compound field rather than each input.
1163
938
 
939
+ `FormLabel` names the `Input`, `Textarea` or `NumericInput` beside it for assistive
940
+ technology (`aria-labelledby` on web and Android; `useFormFieldState().labelId` gives a
941
+ custom control the same id). A label passed to the control itself wins.
942
+
1164
943
  ## Surface
1165
944
 
1166
945
  ```tsx
@@ -1171,14 +950,22 @@ import { Surface } from '@thewhileloop/whileui';
1171
950
  </Surface>;
1172
951
  ```
1173
952
 
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 |
953
+ <!-- api:Surface -->
954
+
955
+ | Prop | Type | Default | Description |
956
+ | ------------- | ------------------------------ | ------------------- | ------------------------------------ |
957
+ | level | `SurfaceLevel` | `'base'` | Semantic background and depth tier. |
958
+ | padding | `SurfacePaddingSize` | `'default'` | Token-backed interior spacing. |
959
+ | radius | `'sm' \| 'md' \| 'lg' \| 'xl'` | `'xl'` | Token-backed corner radius. |
960
+ | bordered | `boolean` | `true` | Draw the theme hairline border. |
961
+ | shadow | `ShadowTier` | — | Override the level's depth. |
962
+ | className | `string` | — | |
963
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
964
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
965
+ | blurTintToken | `FrostedTintToken` | `LEVEL_TINT[level]` | Tint token used for frosted mode. |
966
+
967
+ Also accepts the props of `ViewProps`.
968
+ <!-- /api:Surface -->
1182
969
 
1183
970
  ## LabeledField
1184
971
 
@@ -1220,9 +1007,21 @@ import {
1220
1007
  </SegmentedControl>;
1221
1008
  ```
1222
1009
 
1223
- | Prop | Type | Default | Description |
1224
- | ------- | --------------------- | ----------- | ------------------------- |
1225
- | variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items |
1010
+ <!-- api:SegmentedControl -->
1011
+
1012
+ | Prop | Type | Default | Description |
1013
+ | ------------- | ------------------------- | ----------- | -------------------------- |
1014
+ | value | `string` | — | |
1015
+ | defaultValue | `string` | `''` | |
1016
+ | onValueChange | `(value: string) => void` | — | |
1017
+ | variant | `'default' \| 'pill'` | `'default'` | Pill = rounded-full items. |
1018
+ | wrap | `boolean \| 'auto'` | `true` | |
1019
+ | size | `'default' \| 'compact'` | `'default'` | |
1020
+ | disabled | `boolean` | `false` | |
1021
+ | className | `string` | — | |
1022
+
1023
+ Also accepts the props of `ViewProps`.
1024
+ <!-- /api:SegmentedControl -->
1226
1025
 
1227
1026
  ## DataRow
1228
1027
 
@@ -1281,13 +1080,20 @@ import {
1281
1080
  </Card>;
1282
1081
  ```
1283
1082
 
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 |
1083
+ <!-- api:Card -->
1084
+
1085
+ | Prop | Type | Default | Description |
1086
+ | ------------- | ------------------------------------- | ----------- | ------------------------------------------------------- |
1087
+ | shadow | `ShadowTier` | `'sm'` | Elevation tier; 'sm' by default, 'none' for flat cards. |
1088
+ | className | `string` | — | |
1089
+ | padding | `'none' \| 'sm' \| 'lg' \| 'default'` | `'default'` | |
1090
+ | unstyled | `boolean` | `false` | |
1091
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1092
+ | blurIntensity | `number` | — | Override blur amount directly. |
1093
+ | blurTintToken | `FrostedTintToken` | — | Tint token used for frosted mode. |
1094
+
1095
+ Also accepts the props of `ViewProps`.
1096
+ <!-- /api:Card -->
1291
1097
 
1292
1098
  ## Badge
1293
1099
 
@@ -1299,9 +1105,15 @@ import { Badge, BadgeText } from '@thewhileloop/whileui';
1299
1105
  </Badge>;
1300
1106
  ```
1301
1107
 
1302
- | Prop | Type | Default |
1303
- | ------- | -------------------------------------------------------------------------------------------- | ----------- |
1304
- | variant | `'default' \| 'secondary' \| 'destructive' \| 'outline' \| 'success' \| 'warning' \| 'info'` | `'default'` |
1108
+ <!-- api:Badge -->
1109
+
1110
+ | Prop | Type | Default | Description |
1111
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ----------- |
1112
+ | className | `string` | — | |
1113
+ | variant | `'default' \| 'soft' \| 'destructive' \| 'outline' \| 'secondary' \| 'success' \| 'warning' \| 'info' \| 'soft-secondary' \| 'soft-destructive' \| 'soft-success' \| 'soft-warning' \| 'soft-info'` | `'default'` | |
1114
+
1115
+ Also accepts the props of `ViewProps`.
1116
+ <!-- /api:Badge -->
1305
1117
 
1306
1118
  ## Alert
1307
1119
 
@@ -1312,11 +1124,24 @@ import { Alert, AlertTitle, AlertDescription } from '@thewhileloop/whileui';
1312
1124
  <AlertTitle>Heads up!</AlertTitle>
1313
1125
  <AlertDescription>Something happened.</AlertDescription>
1314
1126
  </Alert>;
1127
+
1128
+ // Shown after a failed submit: also spoken on iOS when it appears.
1129
+ <Alert variant="destructive" announce>
1130
+ <AlertTitle>Could not save</AlertTitle>
1131
+ <AlertDescription>Check your connection and try again.</AlertDescription>
1132
+ </Alert>;
1315
1133
  ```
1316
1134
 
1317
- | Prop | Type | Default |
1318
- | ------- | ------------------------------------------------------ | ----------- |
1319
- | variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` |
1135
+ <!-- api:Alert -->
1136
+
1137
+ | Prop | Type | Default | Description |
1138
+ | --------- | ---------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1139
+ | 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. |
1140
+ | className | `string` | — | |
1141
+ | variant | `'default' \| 'destructive' \| 'success' \| 'warning' \| 'info'` | `'default'` | |
1142
+
1143
+ Also accepts the props of `ViewProps`.
1144
+ <!-- /api:Alert -->
1320
1145
 
1321
1146
  ## Dialog
1322
1147
 
@@ -1357,11 +1182,17 @@ import {
1357
1182
 
1358
1183
  `DialogContent` supports frosted props:
1359
1184
 
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 |
1185
+ <!-- api:DialogContent -->
1186
+
1187
+ | Prop | Type | Default | Description |
1188
+ | ------------- | ------------------ | ---------------------- | ------------------------------------ |
1189
+ | className | `string` | — | |
1190
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1191
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1192
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1193
+
1194
+ Also accepts the props of `ViewProps`.
1195
+ <!-- /api:DialogContent -->
1365
1196
 
1366
1197
  ## AlertDialog
1367
1198
 
@@ -1399,12 +1230,18 @@ import {
1399
1230
 
1400
1231
  `AlertDialogContent` supports the same frosted props as `DialogContent`.
1401
1232
 
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` |
1233
+ <!-- api:AlertDialogContent -->
1234
+
1235
+ | Prop | Type | Default | Description |
1236
+ | ------------- | ------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
1237
+ | className | `string` | — | |
1238
+ | 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`). |
1239
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1240
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1241
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1242
+
1243
+ Also accepts the props of `ViewProps`.
1244
+ <!-- /api:AlertDialogContent -->
1408
1245
 
1409
1246
  ### React Native: stacked modals
1410
1247
 
@@ -1423,11 +1260,18 @@ import { Checkbox } from '@thewhileloop/whileui';
1423
1260
  <Checkbox checked={checked} onCheckedChange={setChecked} />;
1424
1261
  ```
1425
1262
 
1426
- | Prop | Type | Default |
1427
- | --------------- | ---------------------------- | ------- |
1428
- | checked | `boolean` | `false` |
1429
- | onCheckedChange | `(checked: boolean) => void` | — |
1430
- | disabled | `boolean` | `false` |
1263
+ <!-- api:Checkbox -->
1264
+
1265
+ | Prop | Type | Default | Description |
1266
+ | --------------- | ---------------------------- | ------- | ----------- |
1267
+ | checked | `boolean` | — | |
1268
+ | defaultChecked | `boolean` | `false` | |
1269
+ | onCheckedChange | `(checked: boolean) => void` | — | |
1270
+ | disabled | `boolean` | `false` | |
1271
+ | className | `string` | — | |
1272
+
1273
+ Also accepts the props of `ViewProps`.
1274
+ <!-- /api:Checkbox -->
1431
1275
 
1432
1276
  ## Switch
1433
1277
 
@@ -1437,10 +1281,18 @@ import { Switch } from '@thewhileloop/whileui';
1437
1281
  <Switch checked={checked} onCheckedChange={setChecked} />;
1438
1282
  ```
1439
1283
 
1440
- | Prop | Type | Default |
1441
- | --------------- | ---------------------------- | ------- |
1442
- | checked | `boolean` | `false` |
1443
- | onCheckedChange | `(checked: boolean) => void` | — |
1284
+ <!-- api:Switch -->
1285
+
1286
+ | Prop | Type | Default | Description |
1287
+ | --------------- | ---------------------------- | ------- | ----------- |
1288
+ | checked | `boolean` | — | |
1289
+ | defaultChecked | `boolean` | `false` | |
1290
+ | onCheckedChange | `(checked: boolean) => void` | — | |
1291
+ | disabled | `boolean` | `false` | |
1292
+ | className | `string` | — | |
1293
+
1294
+ Also accepts the props of `ViewProps`.
1295
+ <!-- /api:Switch -->
1444
1296
 
1445
1297
  ## RadioGroup
1446
1298
 
@@ -1483,11 +1335,19 @@ itself to the collision-available viewport, and wraps long option labels inside
1483
1335
 
1484
1336
  `SelectContent` supports optional frosted props:
1485
1337
 
1486
- | Prop | Type | Default |
1487
- | ------------- | ------------------------------------------------------------------ | ------------- |
1488
- | frosted | `boolean` | `false` |
1489
- | blurIntensity | `number` | subtle preset |
1490
- | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
1338
+ <!-- api:SelectContent -->
1339
+
1340
+ | Prop | Type | Default | Description |
1341
+ | ------------- | ------------------------------------------------------------------ | ------------- | ------------------------------------ |
1342
+ | className | `string` | — | |
1343
+ | portalHost | `string` | — | |
1344
+ | insets | `{ top?: number; bottom?: number; left?: number; right?: number }` | — | |
1345
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1346
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
1347
+ | blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |
1348
+
1349
+ Also accepts the props of the underlying primitive.
1350
+ <!-- /api:SelectContent -->
1491
1351
 
1492
1352
  `SelectContent` keeps its listbox inside the safe area by default; pass `insets` only to
1493
1353
  change that margin. A list taller than the screen scrolls inside the listbox on every
@@ -1536,10 +1396,17 @@ import {
1536
1396
  </Accordion>;
1537
1397
  ```
1538
1398
 
1539
- | Prop | Type | Default |
1540
- | ----------- | ------------------------ | ---------- |
1541
- | type | `'single' \| 'multiple'` | `'single'` |
1542
- | collapsible | `boolean` | `false` |
1399
+ <!-- api:Accordion -->
1400
+
1401
+ | Prop | Type | Default | Description |
1402
+ | ------------ | -------------------- | ---------- | ----------- |
1403
+ | type | `AccordionType` | `'single'` | |
1404
+ | defaultValue | `string \| string[]` | — | |
1405
+ | collapsible | `boolean` | `true` | |
1406
+ | className | `string` | — | |
1407
+
1408
+ Also accepts the props of `ViewProps`.
1409
+ <!-- /api:Accordion -->
1543
1410
 
1544
1411
  ## Avatar
1545
1412
 
@@ -1552,9 +1419,15 @@ import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
1552
1419
  </Avatar>;
1553
1420
  ```
1554
1421
 
1555
- | Prop | Type | Default |
1556
- | ---- | --------------------------- | ----------- |
1557
- | size | `'sm' \| 'default' \| 'lg'` | `'default'` |
1422
+ <!-- api:Avatar -->
1423
+
1424
+ | Prop | Type | Default | Description |
1425
+ | --------- | ----------------------------------- | ----------- | ----------- |
1426
+ | className | `string` | — | |
1427
+ | size | `'sm' \| 'lg' \| 'xl' \| 'default'` | `'default'` | |
1428
+
1429
+ Also accepts the props of `ViewProps`.
1430
+ <!-- /api:Avatar -->
1558
1431
 
1559
1432
  `AvatarImage` keeps `AvatarFallback` visible while loading or after an error. Its
1560
1433
  `onLoadingStatusChange` callback reports `'loading'`, `'loaded'`, and `'error'`, including
@@ -1568,10 +1441,17 @@ import { Progress } from '@thewhileloop/whileui';
1568
1441
  <Progress value={50} size="default" />;
1569
1442
  ```
1570
1443
 
1571
- | Prop | Type | Default |
1572
- | ----- | --------------------------- | ----------- |
1573
- | value | `number` | `0` |
1574
- | size | `'sm' \| 'default' \| 'lg'` | `'default'` |
1444
+ <!-- api:Progress -->
1445
+
1446
+ | Prop | Type | Default | Description |
1447
+ | ------------------ | --------------------------- | ----------- | ----------- |
1448
+ | value | `number` | `0` | |
1449
+ | indicatorClassName | `string` | — | |
1450
+ | className | `string` | — | |
1451
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
1452
+
1453
+ Also accepts the props of `ViewProps`.
1454
+ <!-- /api:Progress -->
1575
1455
 
1576
1456
  ## Toast
1577
1457
 
@@ -1599,6 +1479,11 @@ toast({ title: 'Success', description: 'Saved!', variant: 'success' });
1599
1479
  Each toast owns its duration from the moment it appears; adding or dismissing another
1600
1480
  toast does not restart timers already in progress.
1601
1481
 
1482
+ Every variant sits on the theme's `popover` surface; `success` and `destructive` mark the
1483
+ toast with a status dot and tinted border rather than a solid fill. The stack is capped
1484
+ at `max-w-md` and centred, which never engages on a phone. A toast is announced politely
1485
+ to screen readers without taking focus, and its slide-in follows the motion policy.
1486
+
1602
1487
  ## Popover
1603
1488
 
1604
1489
  ```tsx
@@ -1618,11 +1503,19 @@ import { Popover, PopoverTrigger, PopoverContent } from '@thewhileloop/whileui';
1618
1503
 
1619
1504
  `PopoverContent` supports frosted props:
1620
1505
 
1621
- | Prop | Type | Default |
1622
- | ------------- | ------------------------------------------------------------------ | ------------- |
1623
- | frosted | `boolean` | `false` |
1624
- | blurIntensity | `number` | subtle preset |
1625
- | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
1506
+ <!-- api:PopoverContent -->
1507
+
1508
+ | Prop | Type | Default | Description |
1509
+ | ------------- | ------------------------ | ------------- | ----------------------------------------------------------------------------------------------------- |
1510
+ | className | `string` | — | |
1511
+ | portalHost | `string` | — | |
1512
+ | insets | `Partial<OverlayInsets>` | — | Collision padding. Defaults to safe area plus a margin so the panel never sits flush against an edge. |
1513
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1514
+ | blurIntensity | `number` | subtle preset | Override blur amount directly. |
1515
+ | blurTintToken | `FrostedTintToken` | `'popover'` | Tint token used for frosted mode. |
1516
+
1517
+ Also accepts the props of the underlying primitive.
1518
+ <!-- /api:PopoverContent -->
1626
1519
 
1627
1520
  ## DropdownMenu
1628
1521
 
@@ -1710,7 +1603,7 @@ announces as a heading to assistive technology — as do `DropdownMenuLabel` and
1710
1603
  ## SignInForm
1711
1604
 
1712
1605
  Copy `sign-in-form.tsx` and its sibling `auth-form-footer.tsx` from
1713
- `apps/showcase/templates/auth/`, then:
1606
+ `apps/showcase/catalog/templates/auth/`, then:
1714
1607
 
1715
1608
  ```tsx
1716
1609
  import { SignInForm } from './templates/auth'; // or your path
@@ -1735,7 +1628,7 @@ import { SignInForm } from './templates/auth'; // or your path
1735
1628
  ## SignUpForm
1736
1629
 
1737
1630
  Copy `sign-up-form.tsx` and its sibling `auth-form-footer.tsx` from
1738
- `apps/showcase/templates/auth/`, then:
1631
+ `apps/showcase/catalog/templates/auth/`, then:
1739
1632
 
1740
1633
  ```tsx
1741
1634
  import { SignUpForm } from './templates/auth'; // or your path
@@ -1789,14 +1682,20 @@ import { AppShell, PageSkeleton } from '@thewhileloop/whileui';
1789
1682
  </AppShell>;
1790
1683
  ```
1791
1684
 
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` |
1685
+ <!-- api:AppShell -->
1686
+
1687
+ | Prop | Type | Default | Description |
1688
+ | --------- | ----------------- | ------- | --------------------------------------------------------- |
1689
+ | header | `React.ReactNode` | — | Header slot. |
1690
+ | footer | `React.ReactNode` | — | Footer slot. |
1691
+ | bottomNav | `React.ReactNode` | — | Bottom navigation slot. |
1692
+ | safeArea | `boolean` | `true` | Wrap in SafeAreaView. |
1693
+ | loading | `boolean` | `false` | Keep shell mounted while swapping content with skeleton. |
1694
+ | skeleton | `React.ReactNode` | — | Optional custom loading placeholder for the content area. |
1695
+ | className | `string` | — | |
1696
+
1697
+ Also accepts the props of `ViewProps`.
1698
+ <!-- /api:AppShell -->
1800
1699
 
1801
1700
  ## ActionBar
1802
1701
 
@@ -1840,7 +1739,7 @@ import { SmartInput, Button, ButtonText } from '@thewhileloop/whileui';
1840
1739
  onChangeText={setMessage}
1841
1740
  placeholderExamples={['Log breakfast', 'Add a 30 minute walk', 'Set weight to 72 kg']}
1842
1741
  leftSlot={
1843
- <Button variant="ghost" size="icon">
1742
+ <Button variant="ghost" size="icon" accessibilityLabel="Add">
1844
1743
  <Icon name="add" />
1845
1744
  </Button>
1846
1745
  }
@@ -1897,24 +1796,37 @@ const [value, setValue] = useState('');
1897
1796
  onSuggestionPress={(text) => setValue(text)}
1898
1797
  emptyTitle="How can I help?"
1899
1798
  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>}
1799
+ leftSlot={<Button variant="ghost" size="icon" accessibilityLabel="Attach"><Icon name="paperclip" /></Button>}
1800
+ rightSlot={<Button size="icon" accessibilityLabel="Send" onPress={handleSend}><Icon name="send" /></Button>}
1902
1801
  />;
1903
1802
  ```
1904
1803
 
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 |
1804
+ <!-- api:Chat -->
1805
+
1806
+ | Prop | Type | Default | Description |
1807
+ | ----------------------- | ------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------- |
1808
+ | messages (required) | `ChatMessage[]` | — | Messages to show, each `{ id, role, content, secondary?, contentSize? }`. |
1809
+ | value (required) | `string` | — | Input value. |
1810
+ | onChangeText (required) | `(text: string) => void` | — | Input change. |
1811
+ | onSend (required) | `() => void` | — | Send handler. |
1812
+ | placeholder | `string` | `'Message...'` | |
1813
+ | suggestions | `string[]` | `EMPTY_SUGGESTIONS` | Chips when empty. |
1814
+ | onSuggestionPress | `(text: string) => void` | — | |
1815
+ | leftSlot | `React.ReactNode` | — | |
1816
+ | rightSlot | `React.ReactNode` | — | |
1817
+ | loading | `boolean` | `false` | |
1818
+ | header | `React.ReactNode` | — | |
1819
+ | emptyTitle | `string` | — | |
1820
+ | emptyDescription | `string` | — | |
1821
+ | exampleMessage | `ChatMessage` | — | Example message shown in empty state so users see the bubble UI. |
1822
+ | renderMessage | `(message: ChatMessage) => React.ReactNode` | — | Custom message renderer (markdown, code, images). Falls back to ChatMessageBubble. |
1823
+ | loadingIndicator | `React.ReactNode` | — | Shown at bottom when loading (typing indicator). |
1824
+ | inputSafeArea | `boolean` | `true` | Passed to SmartInput. Use false when Chat is in a contained demo. |
1825
+ | keyboardVerticalOffset | `number` | `0` | Offset for KeyboardAvoidingView when Chat has a header (e.g. StatusBar + header height). |
1826
+ | className | `string` | — | |
1827
+ | messagesClassName | `string` | — | |
1828
+
1829
+ <!-- /api:Chat -->
1918
1830
 
1919
1831
  `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
1832
 
@@ -1949,18 +1861,24 @@ import { SmartImage } from '@thewhileloop/whileui';
1949
1861
  />;
1950
1862
  ```
1951
1863
 
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` | — |
1864
+ <!-- api:SmartImage -->
1865
+
1866
+ | Prop | Type | Default | Description |
1867
+ | ----------------- | -------------------------------------------------- | --------- | ----------- |
1868
+ | className | `string` | — | |
1869
+ | source (required) | `ImageProps['source']` | — | |
1870
+ | alt (required) | `string` | — | |
1871
+ | skeleton | `boolean` | `true` | |
1872
+ | skeletonClassName | `string` | — | |
1873
+ | transition | `number` | `200` | |
1874
+ | onLoad | `() => void` | — | |
1875
+ | onError | `() => void` | — | |
1876
+ | fallbackSource | `ImageProps['source']` | — | |
1877
+ | radius | `'none' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'full'` | `'md'` | |
1878
+ | objectFit | `'fill' \| 'cover' \| 'contain' \| 'scale-down'` | `'cover'` | |
1879
+
1880
+ Also accepts the props of `ViewProps`.
1881
+ <!-- /api:SmartImage -->
1964
1882
 
1965
1883
  The skeleton returns whenever the primary image starts loading again. A failed primary
1966
1884
  source switches to `fallbackSource` when supplied without re-entering a primary/fallback
@@ -2030,19 +1948,26 @@ const [time, setTime] = useState<string | null>(null);
2030
1948
  />;
2031
1949
  ```
2032
1950
 
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 |
1951
+ <!-- api:DatePickerModal -->
1952
+
1953
+ | Prop | Type | Default | Description |
1954
+ | ----------------------- | -------------------------------- | ---------------------- | --------------------------------------------------------- |
1955
+ | value | `string \| null` | — | Controlled: selected date in YYYY-MM-DD |
1956
+ | onValueChange | `(date: string \| null) => void` | — | Controlled: change handler |
1957
+ | open (required) | `boolean` | — | Modal open state |
1958
+ | onOpenChange (required) | `(open: boolean) => void` | — | Modal open change handler |
1959
+ | minDate | `string` | — | Minimum selectable date YYYY-MM-DD |
1960
+ | maxDate | `string` | — | Maximum selectable date YYYY-MM-DD |
1961
+ | theme | `CalendarTheme` | — | Override calendar theme (RN color strings) |
1962
+ | trigger | `React.ReactNode` | — | Trigger element. If not provided, no trigger is rendered. |
1963
+ | title | `string` | `'Select date'` | Title shown in modal header |
1964
+ | confirmLabel | `string` | `'Done'` | Button label for confirm |
1965
+ | className | `string` | — | |
1966
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
1967
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
1968
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
1969
+
1970
+ <!-- /api:DatePickerModal -->
2046
1971
 
2047
1972
  ## ConfirmActionSheet
2048
1973
 
@@ -2060,11 +1985,27 @@ import { ConfirmActionSheet } from '@thewhileloop/whileui';
2060
1985
  />;
2061
1986
  ```
2062
1987
 
2063
- | Prop | Type | Default |
2064
- | --------------- | ------------------------------------------------------------------ | ---------------------- |
2065
- | `frosted` | `boolean` | `false` |
2066
- | `blurIntensity` | `number` | medium preset |
2067
- | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
1988
+ <!-- api:ConfirmActionSheet -->
1989
+
1990
+ | Prop | Type | Default | Description |
1991
+ | ----------------------- | ---------------------------- | ---------------------- | ------------------------------------ |
1992
+ | open (required) | `boolean` | — | |
1993
+ | onOpenChange (required) | `(open: boolean) => void` | — | |
1994
+ | title (required) | `string` | — | |
1995
+ | description | `string` | — | |
1996
+ | actions | `ConfirmActionSheetAction[]` | — | |
1997
+ | confirmLabel | `string` | `'Confirm'` | |
1998
+ | cancelLabel | `string` | `'Cancel'` | |
1999
+ | onConfirm | `() => void` | — | |
2000
+ | onCancel | `() => void` | — | |
2001
+ | destructive | `boolean` | `true` | |
2002
+ | className | `string` | — | |
2003
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2004
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2005
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2006
+
2007
+ Also accepts the props of `ViewProps`.
2008
+ <!-- /api:ConfirmActionSheet -->
2068
2009
 
2069
2010
  ## Sheet
2070
2011
 
@@ -2094,15 +2035,20 @@ import {
2094
2035
  </Sheet>;
2095
2036
  ```
2096
2037
 
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'` |
2038
+ <!-- api:Sheet -->
2039
+
2040
+ | Prop | Type | Default | Description |
2041
+ | ----------------------- | ---------------------------- | ---------------------- | ----------------------------------------------------------- |
2042
+ | open (required) | `boolean` | — | |
2043
+ | onOpenChange (required) | `(open: boolean) => void` | — | |
2044
+ | children (required) | `React.ReactNode` | — | |
2045
+ | maxHeight | `'half' \| 'full' \| number` | `'full'` | Max height: 'half' \| 'full' \| number (px). Default 'full' |
2046
+ | maxWidth | `number` | — | Web: max width. Default ~360 for mobile-first. |
2047
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2048
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2049
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2050
+
2051
+ <!-- /api:Sheet -->
2106
2052
 
2107
2053
  `SheetContent` is a `ScrollView` and accepts its props. It enables nested scrolling and
2108
2054
  keeps taps on inputs while the keyboard is open (`keyboardShouldPersistTaps="handled"`).
@@ -2143,11 +2089,22 @@ import { SplashScreen } from '@thewhileloop/whileui';
2143
2089
  />;
2144
2090
  ```
2145
2091
 
2146
- | Prop | Type | Default |
2147
- | ----------- | ------------------------------ | --------- |
2148
- | variant | `'fade' \| 'scale' \| 'slide'` | `'scale'` |
2149
- | duration | `number` | `800` |
2150
- | showLoading | `boolean` | `false` |
2092
+ <!-- api:SplashScreen -->
2093
+
2094
+ | Prop | Type | Default | Description |
2095
+ | ------------------- | ------------------------------ | --------- | ----------------------------------------------- |
2096
+ | appName | `string` | — | App name displayed below logo |
2097
+ | tagline | `string` | — | Tagline displayed below app name |
2098
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2099
+ | variant | `'fade' \| 'scale' \| 'slide'` | `'scale'` | Animation variant |
2100
+ | duration | `number` | `800` | Animation duration in ms |
2101
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2102
+ | showLoading | `boolean` | `false` | Whether to show loading indicator |
2103
+ | loadingText | `string` | — | Loading text |
2104
+ | className | `string` | — | |
2105
+
2106
+ Also accepts the props of `ViewProps`.
2107
+ <!-- /api:SplashScreen -->
2151
2108
 
2152
2109
  ## ContentSkeleton
2153
2110
 
@@ -2161,10 +2118,16 @@ import { ContentSkeleton } from '@thewhileloop/whileui';
2161
2118
  <ContentSkeleton variant="generic" />
2162
2119
  ```
2163
2120
 
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) |
2121
+ <!-- api:ContentSkeleton -->
2122
+
2123
+ | Prop | Type | Default | Description |
2124
+ | --------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- |
2125
+ | variant | `'list' \| 'card' \| 'generic'` | `'list'` | Layout preset. `list` = avatar + lines rows, `card` = card with image/body, `generic` = simple page lines. |
2126
+ | rows | `number` | `4` | Number of list rows (list variant only). Default 4. |
2127
+ | className | `string` | — | |
2128
+
2129
+ Also accepts the props of `ViewProps`.
2130
+ <!-- /api:ContentSkeleton -->
2168
2131
 
2169
2132
  ## PageSkeleton
2170
2133
 
@@ -2183,14 +2146,19 @@ import { PageSkeleton } from '@thewhileloop/whileui';
2183
2146
  <PageSkeleton variant="list" padding="none" className="flex-1" />
2184
2147
  ```
2185
2148
 
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 |
2149
+ <!-- api:PageSkeleton -->
2150
+
2151
+ | Prop | Type | Default | Description |
2152
+ | ------------------ | ------------------------------------------ | ----------- | ------------------------------------------------------------------- |
2153
+ | variant (required) | `PageSkeletonVariant` | — | Layout preset. |
2154
+ | count | `number` | `3` | Rows/items for list (default 3) or settings (default 4) variant. |
2155
+ | padding | `PageSkeletonPadding` | `'default'` | Container padding. |
2156
+ | header | `ReactNode` | — | Optional header slot rendered above skeleton content. |
2157
+ | headerPlaceholder | `boolean \| PageSkeletonHeaderPlaceholder` | `false` | Optional placeholder header (when no real header is available yet). |
2158
+ | className | `string` | — | |
2159
+
2160
+ Also accepts the props of `ViewProps`.
2161
+ <!-- /api:PageSkeleton -->
2194
2162
 
2195
2163
  ## ScreenSkeleton
2196
2164
 
@@ -2203,13 +2171,19 @@ import { ScreenSkeleton } from '@thewhileloop/whileui';
2203
2171
  <ScreenSkeleton variant="list" count={5} header={<Header title="Projects" />} />
2204
2172
  ```
2205
2173
 
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 |
2174
+ <!-- api:ScreenSkeleton -->
2175
+
2176
+ | Prop | Type | Default | Description |
2177
+ | ----------------- | ------------------------------------------ | ----------- | -------------------------------------- |
2178
+ | variant | `PageSkeletonVariant` | `'generic'` | Skeleton content preset. |
2179
+ | count | `number` | — | Rows/items for list/settings variants. |
2180
+ | padding | `PageSkeletonPadding` | `'default'` | Inner content padding. |
2181
+ | header | `ReactNode` | — | Real header slot. |
2182
+ | headerPlaceholder | `boolean \| PageSkeletonHeaderPlaceholder` | `'default'` | Placeholder header. |
2183
+ | className | `string` | — | |
2184
+
2185
+ Also accepts the props of `ViewProps`.
2186
+ <!-- /api:ScreenSkeleton -->
2213
2187
 
2214
2188
  ## ErrorBoundary
2215
2189
 
@@ -2223,10 +2197,15 @@ import { ErrorBoundary } from '@thewhileloop/whileui';
2223
2197
  </ErrorBoundary>;
2224
2198
  ```
2225
2199
 
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 |
2200
+ <!-- api:ErrorBoundary -->
2201
+
2202
+ | Prop | Type | Default | Description |
2203
+ | ------------------- | --------------------------------------------------------------- | ------- | -------------------------------------------------- |
2204
+ | children (required) | `ReactNode` | — | |
2205
+ | fallback | `ReactNode \| ((error: Error, reset: () => void) => ReactNode)` | — | Custom fallback. If not provided, uses ErrorState. |
2206
+ | onError | `(error: Error, errorInfo: ErrorInfo) => void` | — | Called when error is caught. |
2207
+
2208
+ <!-- /api:ErrorBoundary -->
2230
2209
 
2231
2210
  ## PullToRefreshScrollView
2232
2211
 
@@ -2240,11 +2219,17 @@ import { PullToRefreshScrollView } from '@thewhileloop/whileui';
2240
2219
  </PullToRefreshScrollView>;
2241
2220
  ```
2242
2221
 
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 |
2222
+ <!-- api:PullToRefreshScrollView -->
2223
+
2224
+ | Prop | Type | Default | Description |
2225
+ | --------------------- | ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2226
+ | refreshing (required) | `boolean` | — | Whether refresh is in progress. |
2227
+ | 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. |
2228
+ | refreshColor | `string` | — | Override tint/colors. Defaults to theme primary. |
2229
+ | className | `string` | — | |
2230
+
2231
+ Also accepts the props of `ScrollViewProps`.
2232
+ <!-- /api:PullToRefreshScrollView -->
2248
2233
 
2249
2234
  The scroll view owns the refresh gesture; don't pass `refreshControl`. It bounces even
2250
2235
  when content is shorter than the screen, so a short list can still be pulled. Screen
@@ -2276,14 +2261,20 @@ import { LoadingState } from '@thewhileloop/whileui';
2276
2261
  The in-flow counterpart of `EmptyState` and `ErrorState` for a section that is still
2277
2262
  fetching. The spinner is named for assistive technology by `label` (default "Loading").
2278
2263
 
2279
- | Prop | Type | Default |
2280
- | ----- | --------------------------- | ----------- |
2281
- | label | `string` | `'Loading'` |
2282
- | size | `'sm' \| 'default' \| 'lg'` | `'default'` |
2264
+ <!-- api:LoadingState -->
2265
+
2266
+ | Prop | Type | Default | Description |
2267
+ | --------- | --------------------------- | ----------- | ------------------------------------------------------------------------------ |
2268
+ | label | `string` | — | Read by assistive technology as the spinner's name; shown under it when given. |
2269
+ | size | `'sm' \| 'default' \| 'lg'` | `'default'` | |
2270
+ | className | `string` | — | |
2271
+
2272
+ Also accepts the props of `ViewProps`.
2273
+ <!-- /api:LoadingState -->
2283
2274
 
2284
2275
  ## ProfileHeader
2285
2276
 
2286
- Copy from `apps/showcase/templates/profile/profile-header.tsx`, then:
2277
+ Copy from `apps/showcase/catalog/templates/profile/profile-header.tsx`, then:
2287
2278
 
2288
2279
  ```tsx
2289
2280
  import { ProfileHeader } from './templates/profile'; // or your path
@@ -2305,7 +2296,7 @@ import { ProfileHeader } from './templates/profile'; // or your path
2305
2296
 
2306
2297
  ## SettingsSection / SettingsItem
2307
2298
 
2308
- Copy from `apps/showcase/templates/profile/`, then:
2299
+ Copy from `apps/showcase/catalog/templates/profile/`, then:
2309
2300
 
2310
2301
  ```tsx
2311
2302
  import { SettingsSection, SettingsItem } from './templates/profile'; // or your path
@@ -2340,10 +2331,25 @@ import { ProductCard } from '@thewhileloop/whileui';
2340
2331
  />;
2341
2332
  ```
2342
2333
 
2343
- | Prop | Type | Default |
2344
- | ------- | ---------------------------- | ------------ |
2345
- | variant | `'vertical' \| 'horizontal'` | `'vertical'` |
2346
- | loading | `boolean` | `false` |
2334
+ <!-- api:ProductCard -->
2335
+
2336
+ | Prop | Type | Default | Description |
2337
+ | ---------------- | ---------------------------- | ------------ | ----------------------------------------------- |
2338
+ | imageUrl | `string` | — | |
2339
+ | title (required) | `string` | — | |
2340
+ | description | `string` | — | |
2341
+ | price (required) | `string` | — | |
2342
+ | originalPrice | `string` | — | |
2343
+ | badge | `string` | — | |
2344
+ | rating | `number` | — | |
2345
+ | reviewCount | `number` | — | |
2346
+ | inStock | `boolean` | `true` | |
2347
+ | variant | `'vertical' \| 'horizontal'` | `'vertical'` | |
2348
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2349
+ | className | `string` | — | |
2350
+
2351
+ Also accepts the props of `PressableProps`.
2352
+ <!-- /api:ProductCard -->
2347
2353
 
2348
2354
  ## PricingCard
2349
2355
 
@@ -2382,16 +2388,24 @@ import { SubscriptionCard } from '@thewhileloop/whileui';
2382
2388
  />;
2383
2389
  ```
2384
2390
 
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 |
2391
+ <!-- api:SubscriptionCard -->
2392
+
2393
+ | Prop | Type | Default | Description |
2394
+ | ------------------- | ------------ | --------------- | ----------------------------------------------- |
2395
+ | planName (required) | `string` | — | Current plan label. |
2396
+ | price (required) | `string` | — | Plan price. |
2397
+ | period | `string` | `'/month'` | Billing period label. |
2398
+ | expiresAt | `string` | — | Renewal/expiry date text. |
2399
+ | isActive | `boolean` | `true` | Active/inactive badge state. |
2400
+ | onManage | `() => void` | — | Manage action. |
2401
+ | onUpgrade | `() => void` | — | Upgrade action. |
2402
+ | manageLabel | `string` | `'Manage plan'` | |
2403
+ | upgradeLabel | `string` | `'Upgrade'` | |
2404
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2405
+ | className | `string` | — | |
2406
+
2407
+ Also accepts the props of `ViewProps`.
2408
+ <!-- /api:SubscriptionCard -->
2395
2409
 
2396
2410
  ## FeatureGate
2397
2411
 
@@ -2416,12 +2430,18 @@ import { UsageBar } from '@thewhileloop/whileui';
2416
2430
  <UsageBar label="AI generations" used={8} limit={20} />;
2417
2431
  ```
2418
2432
 
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 |
2433
+ <!-- api:UsageBar -->
2434
+
2435
+ | Prop | Type | Default | Description |
2436
+ | ---------------- | ----------------- | ------- | ------------------------------- |
2437
+ | label (required) | `string` | — | Usage label. |
2438
+ | used (required) | `number` | — | Used amount. |
2439
+ | limit (required) | `number` | — | Quota limit. |
2440
+ | variant | `UsageBarVariant` | — | Optional explicit visual state. |
2441
+ | className | `string` | — | |
2442
+
2443
+ Also accepts the props of `ViewProps`.
2444
+ <!-- /api:UsageBar -->
2425
2445
 
2426
2446
  ## PlanToggle
2427
2447
 
@@ -2474,48 +2494,600 @@ import { DrawerMenu } from '@thewhileloop/whileui';
2474
2494
  />;
2475
2495
  ```
2476
2496
 
2477
- | Prop | Type | Default |
2478
- | --------------- | ------------------------------------------------------------------ | ---------------------- |
2479
- | `frosted` | `boolean` | `false` |
2480
- | `blurIntensity` | `number` | medium preset |
2481
- | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
2497
+ <!-- api:DrawerMenu -->
2498
+
2499
+ | Prop | Type | Default | Description |
2500
+ | ------------------- | ----------------------- | ---------------------- | ----------------------------------------------------------- |
2501
+ | visible (required) | `boolean` | — | |
2502
+ | onClose (required) | `() => void` | — | |
2503
+ | sections (required) | `DrawerMenuSection[]` | — | |
2504
+ | activeKey | `string` | — | |
2505
+ | onSelect | `(key: string) => void` | — | |
2506
+ | header | `React.ReactNode` | — | |
2507
+ | footer | `React.ReactNode` | — | |
2508
+ | maxWidth | `number` | — | Max width in px. On web, defaults to 360 when not provided. |
2509
+ | className | `string` | — | |
2510
+ | style | `StyleProp<ViewStyle>` | — | |
2511
+ | frosted | `boolean` | `false` | Enable the optional frosted overlay. |
2512
+ | blurIntensity | `number` | medium preset | Override blur amount directly. |
2513
+ | blurTintToken | `FrostedTintToken` | `'surfaceTranslucent'` | Tint token used for frosted mode. |
2514
+
2515
+ <!-- /api:DrawerMenu -->
2482
2516
 
2483
2517
  Closes from the backdrop, an item, Android back or Escape on web. It is hosted in a modal
2484
2518
  on every platform, so keyboard focus stays inside it while it is open.
2485
2519
 
2486
- ## Roadmap
2520
+ ## More components
2521
+
2522
+ The rest of the catalog: what each is for and the props its type declares.
2523
+
2524
+ ### View
2525
+
2526
+ Themed view wrapper
2527
+
2528
+ ```tsx
2529
+ import { View } from '@thewhileloop/whileui';
2530
+ ```
2531
+
2532
+ <!-- api:View -->
2533
+
2534
+ | Prop | Type | Default | Description |
2535
+ | --------- | -------- | ------- | ----------- |
2536
+ | className | `string` | — | |
2537
+
2538
+ Also accepts the props of React Native's `ViewProps`.
2539
+ <!-- /api:View -->
2540
+
2541
+ ### Stack
2487
2542
 
2488
- Tracked work items for future releases.
2543
+ Vertical flex layout with gap
2489
2544
 
2490
- ### New Components
2545
+ ```tsx
2546
+ import { Stack } from '@thewhileloop/whileui';
2547
+ ```
2548
+
2549
+ <!-- api:Stack -->
2550
+
2551
+ | Prop | Type | Default | Description |
2552
+ | --------- | ------------------------------------------------------------------- | ----------- | ----------- |
2553
+ | className | `string` | — | |
2554
+ | gap | `'none' \| 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | `'md'` | |
2555
+ | align | `'start' \| 'center' \| 'end' \| 'stretch' \| 'baseline'` | `'stretch'` | |
2556
+ | justify | `'start' \| 'center' \| 'end' \| 'between' \| 'around' \| 'evenly'` | `'start'` | |
2557
+
2558
+ Also accepts the props of `ViewProps`.
2559
+ <!-- /api:Stack -->
2560
+
2561
+ ### MotionProvider
2562
+
2563
+ System reduced motion, explicit preview modes, and deterministic testing
2564
+
2565
+ ```tsx
2566
+ import { MotionProvider } from '@thewhileloop/whileui';
2567
+ ```
2568
+
2569
+ <!-- api:MotionProvider -->
2491
2570
 
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
2571
+ | Prop | Type | Default | Description |
2572
+ | ------------------- | ----------------- | ---------- | ----------- |
2573
+ | children (required) | `React.ReactNode` | — | |
2574
+ | mode | `MotionMode` | `'system'` | |
2502
2575
 
2503
- ### Component-Level Loading (`loading` prop)
2576
+ <!-- /api:MotionProvider -->
2504
2577
 
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
2578
+ ### Textarea
2508
2579
 
2509
- ### Documentation Gaps
2580
+ Multi-line text input
2581
+
2582
+ ```tsx
2583
+ import { Textarea } from '@thewhileloop/whileui';
2584
+ ```
2585
+
2586
+ <!-- api:Textarea -->
2587
+
2588
+ | Prop | Type | Default | Description |
2589
+ | ------------ | --------------------------- | ----------- | ----------- |
2590
+ | aria-invalid | `boolean` | — | |
2591
+ | className | `string` | — | |
2592
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
2593
+
2594
+ Also accepts the props of `TextInputProps`.
2595
+ <!-- /api:Textarea -->
2596
+
2597
+ ### Label
2598
+
2599
+ Form field label
2600
+
2601
+ ```tsx
2602
+ import { Label } from '@thewhileloop/whileui';
2603
+ ```
2604
+
2605
+ <!-- api:Label -->
2606
+
2607
+ | Prop | Type | Default | Description |
2608
+ | --------- | -------- | ------- | ----------- |
2609
+ | className | `string` | — | |
2610
+
2611
+ Also accepts the props of `TextProps`.
2612
+ <!-- /api:Label -->
2613
+
2614
+ ### Toggle
2615
+
2616
+ ToggleText sub-component
2617
+
2618
+ ```tsx
2619
+ import { Toggle } from '@thewhileloop/whileui';
2620
+ ```
2621
+
2622
+ <!-- api:Toggle -->
2623
+
2624
+ | Prop | Type | Default | Description |
2625
+ | --------------- | ------------------------------------------------ | ----------- | ----------- |
2626
+ | pressed | `boolean` | `false` | |
2627
+ | defaultPressed | `boolean` | `false` | |
2628
+ | onPressedChange | `(pressed: boolean) => void` | — | |
2629
+ | variant | `VariantProps<typeof toggleVariants>['variant']` | `'default'` | |
2630
+ | size | `VariantProps<typeof toggleVariants>['size']` | `'default'` | |
2631
+ | className | `string` | — | |
2632
+ | children | `React.ReactNode` | — | |
2633
+
2634
+ Also accepts the props of `PressableProps`.
2635
+ <!-- /api:Toggle -->
2636
+
2637
+ ### ToggleGroup
2638
+
2639
+ Group of toggle items
2640
+
2641
+ ```tsx
2642
+ import { ToggleGroup } from '@thewhileloop/whileui';
2643
+ ```
2644
+
2645
+ <!-- api:ToggleGroup -->
2646
+
2647
+ | Prop | Type | Default | Description |
2648
+ | ------------- | ------------------------------------- | ----------- | ----------- |
2649
+ | type | `ToggleGroupType` | `'single'` | |
2650
+ | defaultValue | `string \| string[]` | — | |
2651
+ | value | `string \| string[]` | — | |
2652
+ | onValueChange | `(value: string \| string[]) => void` | — | |
2653
+ | variant | `'default' \| 'outline'` | `'default'` | |
2654
+ | size | `'default' \| 'sm' \| 'lg'` | `'default'` | |
2655
+ | className | `string` | — | |
2656
+
2657
+ Also accepts the props of `ViewProps`.
2658
+ <!-- /api:ToggleGroup -->
2659
+
2660
+ ### Separator
2661
+
2662
+ Themed divider
2663
+
2664
+ ```tsx
2665
+ import { Separator } from '@thewhileloop/whileui';
2666
+ ```
2667
+
2668
+ <!-- api:Separator -->
2669
+
2670
+ | Prop | Type | Default | Description |
2671
+ | ----------- | ---------------------------- | -------------- | ----------- |
2672
+ | orientation | `'horizontal' \| 'vertical'` | `'horizontal'` | |
2673
+ | className | `string` | — | |
2674
+
2675
+ Also accepts the props of `ViewProps`.
2676
+ <!-- /api:Separator -->
2677
+
2678
+ ### Spinner
2679
+
2680
+ ActivityIndicator wrapper
2681
+
2682
+ ```tsx
2683
+ import { Spinner } from '@thewhileloop/whileui';
2684
+ ```
2685
+
2686
+ <!-- api:Spinner -->
2687
+
2688
+ | Prop | Type | Default | Description |
2689
+ | --------- | --------------------------- | ----------- | ----------- |
2690
+ | className | `string` | — | |
2691
+ | size | `'sm' \| 'lg' \| 'default'` | `'default'` | |
2692
+
2693
+ Also accepts the props of `ActivityIndicatorProps`.
2694
+ <!-- /api:Spinner -->
2695
+
2696
+ ### Skeleton
2697
+
2698
+ Loading placeholder (pulse = opacity fade, shimmer = sweep)
2699
+
2700
+ ```tsx
2701
+ import { Skeleton } from '@thewhileloop/whileui';
2702
+ ```
2703
+
2704
+ <!-- api:Skeleton -->
2705
+
2706
+ | Prop | Type | Default | Description |
2707
+ | --------- | ---------------------- | --------- | ----------- |
2708
+ | className | `string` | — | |
2709
+ | variant | `'pulse' \| 'shimmer'` | `'pulse'` | |
2710
+
2711
+ Also accepts the props of `ViewProps`.
2712
+ <!-- /api:Skeleton -->
2713
+
2714
+ ### AspectRatio
2715
+
2716
+ Maintain aspect ratio container
2717
+
2718
+ ```tsx
2719
+ import { AspectRatio } from '@thewhileloop/whileui';
2720
+ ```
2721
+
2722
+ <!-- api:AspectRatio -->
2723
+
2724
+ | Prop | Type | Default | Description |
2725
+ | --------- | -------- | ------- | ----------- |
2726
+ | ratio | `number` | `1` | |
2727
+ | className | `string` | — | |
2728
+
2729
+ Also accepts the props of `ViewProps`.
2730
+ <!-- /api:AspectRatio -->
2731
+
2732
+ ### Collapsible
2733
+
2734
+ CollapsibleTrigger, CollapsibleContent
2735
+
2736
+ ```tsx
2737
+ import { Collapsible } from '@thewhileloop/whileui';
2738
+ ```
2739
+
2740
+ <!-- api:Collapsible -->
2741
+
2742
+ | Prop | Type | Default | Description |
2743
+ | ------------ | ------------------------- | ------- | ----------- |
2744
+ | defaultOpen | `boolean` | `false` | |
2745
+ | open | `boolean` | — | |
2746
+ | onOpenChange | `(open: boolean) => void` | — | |
2747
+ | disabled | `boolean` | `false` | |
2748
+ | className | `string` | — | |
2749
+
2750
+ Also accepts the props of `ViewProps`.
2751
+ <!-- /api:Collapsible -->
2752
+
2753
+ ### Tooltip
2754
+
2755
+ Position-aware tooltip (requires PortalHost)
2756
+
2757
+ ```tsx
2758
+ import { Tooltip } from '@thewhileloop/whileui';
2759
+ ```
2760
+
2761
+ <!-- api:Tooltip -->
2762
+
2763
+ | Prop | Type | Default | Description |
2764
+ | ----------------------- | ------------------------- | ------- | ----------- |
2765
+ | onOpenChange | `(open: boolean) => void` | — | |
2766
+ | delayDuration | `number` | — | |
2767
+ | skipDelayDuration | `number` | — | |
2768
+ | disableHoverableContent | `boolean` | — | |
2769
+ | children (required) | `React.ReactNode` | — | |
2770
+
2771
+ <!-- /api:Tooltip -->
2772
+
2773
+ ### FloatingBottomNav
2774
+
2775
+ Elevated bottom nav with safe area support
2776
+
2777
+ ```tsx
2778
+ import { FloatingBottomNav } from '@thewhileloop/whileui';
2779
+ ```
2780
+
2781
+ <!-- api:FloatingBottomNav -->
2782
+
2783
+ | Prop | Type | Default | Description |
2784
+ | -------------------- | ------------------------- | ------- | ----------- |
2785
+ | items (required) | `FloatingBottomNavItem[]` | — | |
2786
+ | activeKey (required) | `string` | — | |
2787
+ | onSelect (required) | `(key: string) => void` | — | |
2788
+ | className | `string` | — | |
2789
+
2790
+ Also accepts the props of `ViewProps`.
2791
+ <!-- /api:FloatingBottomNav -->
2792
+
2793
+ ### TabBar
2794
+
2795
+ Top tab bar with indicator
2796
+
2797
+ ```tsx
2798
+ import { TabBar } from '@thewhileloop/whileui';
2799
+ ```
2800
+
2801
+ <!-- api:TabBar -->
2802
+
2803
+ | Prop | Type | Default | Description |
2804
+ | -------------------- | ------------------------------------- | ----------- | ----------- |
2805
+ | items (required) | `TabBarItem[]` | — | |
2806
+ | activeKey (required) | `string` | — | |
2807
+ | onSelect (required) | `(key: string) => void` | — | |
2808
+ | variant | `'default' \| 'pills' \| 'underline'` | `'default'` | |
2809
+ | scrollable | `boolean` | `false` | |
2810
+ | className | `string` | — | |
2811
+
2812
+ Also accepts the props of `ViewProps`.
2813
+ <!-- /api:TabBar -->
2814
+
2815
+ ### FormModalScreen
2816
+
2817
+ Modal scaffold for forms with loading states
2818
+
2819
+ ```tsx
2820
+ import { FormModalScreen } from '@thewhileloop/whileui';
2821
+ ```
2822
+
2823
+ <!-- api:FormModalScreen -->
2824
+
2825
+ | Prop | Type | Default | Description |
2826
+ | ------------------- | ----------------- | ------------- | ----------------------------------------------------------------------------------------- |
2827
+ | className | `string` | — | |
2828
+ | title (required) | `string` | — | |
2829
+ | subtitle | `string` | — | |
2830
+ | leftSlot | `React.ReactNode` | — | |
2831
+ | rightSlot | `React.ReactNode` | — | |
2832
+ | onClose | `() => void` | — | |
2833
+ | loading | `boolean` | `false` | |
2834
+ | saving | `boolean` | `false` | |
2835
+ | savingText | `string` | `'Saving...'` | |
2836
+ | loadingSpinnerColor | `string` | — | Hex color for spinner in loading state. ActivityIndicator requires hex. |
2837
+ | savingSpinnerColor | `string` | — | Hex color for spinner in saving banner. Defaults to platform (Android blue) when not set. |
2838
+ | scrollEnabled | `boolean` | `true` | |
2839
+ | children (required) | `React.ReactNode` | — | |
2840
+
2841
+ Also accepts the props of `ViewProps`.
2842
+ <!-- /api:FormModalScreen -->
2843
+
2844
+ ### ErrorState
2845
+
2846
+ Error display with retry
2847
+
2848
+ ```tsx
2849
+ import { ErrorState } from '@thewhileloop/whileui';
2850
+ ```
2851
+
2852
+ <!-- api:ErrorState -->
2853
+
2854
+ | Prop | Type | Default | Description |
2855
+ | ----------- | ----------------- | ------------------------ | ----------- |
2856
+ | icon | `React.ReactNode` | — | |
2857
+ | title | `string` | `'Something went wrong'` | |
2858
+ | description | `string` | — | |
2859
+ | error | `Error \| string` | — | |
2860
+ | onRetry | `() => void` | — | |
2861
+ | retryLabel | `string` | `'Try again'` | |
2862
+ | className | `string` | — | |
2863
+
2864
+ Also accepts the props of `ViewProps`.
2865
+ <!-- /api:ErrorState -->
2866
+
2867
+ ### LoadingScreen
2868
+
2869
+ Full-screen loading indicator
2870
+
2871
+ ```tsx
2872
+ import { LoadingScreen } from '@thewhileloop/whileui';
2873
+ ```
2874
+
2875
+ <!-- api:LoadingScreen -->
2876
+
2877
+ | Prop | Type | Default | Description |
2878
+ | ------------ | ---------------------- | ------- | ------------------------------------------------------ |
2879
+ | message | `string` | — | |
2880
+ | size | `'sm' \| 'md' \| 'lg'` | `'md'` | |
2881
+ | spinnerColor | `string` | — | Hex color for spinner. ActivityIndicator requires hex. |
2882
+ | className | `string` | — | |
2883
+
2884
+ Also accepts the props of `ViewProps`.
2885
+ <!-- /api:LoadingScreen -->
2886
+
2887
+ ### MinimalSplash
2888
+
2889
+ Minimal monochrome splash
2890
+
2891
+ ```tsx
2892
+ import { MinimalSplash } from '@thewhileloop/whileui';
2893
+ ```
2894
+
2895
+ <!-- api:MinimalSplash -->
2896
+
2897
+ | Prop | Type | Default | Description |
2898
+ | ------------------- | ----------------- | ------- | ----------------------------------------------- |
2899
+ | className | `string` | — | |
2900
+ | appName | `string` | — | App name displayed below logo |
2901
+ | tagline | `string` | — | Tagline displayed below app name |
2902
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2903
+ | duration | `number` | `800` | Animation duration in ms |
2904
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2905
+ | showLoading | `boolean` | `false` | Whether to show loading indicator |
2906
+ | loadingText | `string` | — | Loading text |
2907
+
2908
+ Also accepts the props of `SplashScreenProps`.
2909
+ <!-- /api:MinimalSplash -->
2910
+
2911
+ ### BrandedSplash
2912
+
2913
+ Splash with brand imagery
2914
+
2915
+ ```tsx
2916
+ import { BrandedSplash } from '@thewhileloop/whileui';
2917
+ ```
2918
+
2919
+ <!-- api:BrandedSplash -->
2920
+
2921
+ | Prop | Type | Default | Description |
2922
+ | ------------------- | ----------------- | ------- | ----------------------------------------------- |
2923
+ | className | `string` | — | |
2924
+ | appName | `string` | — | App name displayed below logo |
2925
+ | tagline | `string` | — | Tagline displayed below app name |
2926
+ | logo | `React.ReactNode` | — | Logo element (icon, image, or custom component) |
2927
+ | duration | `number` | `800` | Animation duration in ms |
2928
+ | onAnimationComplete | `() => void` | — | Callback when animation completes |
2929
+ | loadingText | `string` | — | Loading text |
2930
+
2931
+ Also accepts the props of `SplashScreenProps`.
2932
+ <!-- /api:BrandedSplash -->
2933
+
2934
+ ### ChatMessageBubble
2935
+
2936
+ One message in a `Chat`, rendered on its own when a screen composes the conversation itself.
2937
+
2938
+ ```tsx
2939
+ import { ChatMessageBubble } from '@thewhileloop/whileui';
2940
+ ```
2941
+
2942
+ <!-- api:ChatMessageBubble -->
2943
+
2944
+ | Prop | Type | Default | Description |
2945
+ | ------------------ | ------------- | ------- | --------------------------------------------------------------- |
2946
+ | message (required) | `ChatMessage` | — | The message to render; its `role` decides alignment and colour. |
2947
+ | contentClassName | `string` | — | Classes for the bubble's content wrapper. |
2948
+
2949
+ <!-- /api:ChatMessageBubble -->
2950
+
2951
+ ### ChatSuggestions
2952
+
2953
+ Suggestion chips when empty
2954
+
2955
+ ```tsx
2956
+ import { ChatSuggestions } from '@thewhileloop/whileui';
2957
+ ```
2958
+
2959
+ <!-- api:ChatSuggestions -->
2960
+
2961
+ | Prop | Type | Default | Description |
2962
+ | ---------------------- | ------------------------ | ------------------- | ----------- |
2963
+ | suggestions (required) | `string[]` | `EMPTY_SUGGESTIONS` | |
2964
+ | onSelect (required) | `(text: string) => void` | — | |
2965
+ | className | `string` | — | |
2966
+
2967
+ <!-- /api:ChatSuggestions -->
2968
+
2969
+ ### ListItem
2970
+
2971
+ Title/subtitle row
2972
+
2973
+ ```tsx
2974
+ import { ListItem } from '@thewhileloop/whileui';
2975
+ ```
2976
+
2977
+ <!-- api:ListItem -->
2978
+
2979
+ | Prop | Type | Default | Description |
2980
+ | ---------------- | ----------------- | ------- | ----------------------------------------------- |
2981
+ | icon | `React.ReactNode` | — | |
2982
+ | title (required) | `string` | — | |
2983
+ | subtitle | `string` | — | |
2984
+ | description | `string` | — | |
2985
+ | rightIcon | `React.ReactNode` | — | |
2986
+ | rightText | `string` | — | |
2987
+ | action | `React.ReactNode` | — | |
2988
+ | showBorder | `boolean` | `true` | |
2989
+ | compact | `boolean` | `false` | |
2990
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
2991
+ | className | `string` | — | |
2992
+
2993
+ Also accepts the props of `PressableProps`.
2994
+ <!-- /api:ListItem -->
2995
+
2996
+ ### NotificationItem
2997
+
2998
+ Notification row with metadata
2999
+
3000
+ ```tsx
3001
+ import { NotificationItem } from '@thewhileloop/whileui';
3002
+ ```
3003
+
3004
+ <!-- api:NotificationItem -->
3005
+
3006
+ | Prop | Type | Default | Description |
3007
+ | ------------------ | ----------------- | ------- | ----------------------------------------------- |
3008
+ | icon | `React.ReactNode` | — | |
3009
+ | avatarUrl | `string` | — | |
3010
+ | avatarFallback | `string` | — | |
3011
+ | title (required) | `string` | — | |
3012
+ | message (required) | `string` | — | |
3013
+ | time (required) | `string` | — | |
3014
+ | read | `boolean` | `false` | |
3015
+ | showDot | `boolean` | `true` | |
3016
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
3017
+ | className | `string` | — | |
3018
+
3019
+ Also accepts the props of `PressableProps`.
3020
+ <!-- /api:NotificationItem -->
3021
+
3022
+ ### TimelineFeed
3023
+
3024
+ Vertical feed with connecting lines
3025
+
3026
+ ```tsx
3027
+ import { TimelineFeed } from '@thewhileloop/whileui';
3028
+ ```
3029
+
3030
+ <!-- api:TimelineFeed -->
3031
+
3032
+ | Prop | Type | Default | Description |
3033
+ | ---------------- | ---------------- | ------- | ----------- |
3034
+ | className | `string` | — | |
3035
+ | items (required) | `TimelineItem[]` | — | |
3036
+ | showConnector | `boolean` | `true` | |
3037
+
3038
+ Also accepts the props of `ViewProps`.
3039
+ <!-- /api:TimelineFeed -->
3040
+
3041
+ ### CheckoutSummary
3042
+
3043
+ Cart summary with line items
3044
+
3045
+ ```tsx
3046
+ import { CheckoutSummary } from '@thewhileloop/whileui';
3047
+ ```
3048
+
3049
+ <!-- api:CheckoutSummary -->
3050
+
3051
+ | Prop | Type | Default | Description |
3052
+ | ------------------- | -------------------- | ------------ | ----------- |
3053
+ | items (required) | `CheckoutLineItem[]` | — | |
3054
+ | subtotal (required) | `string` | — | |
3055
+ | shipping | `string` | — | |
3056
+ | tax | `string` | — | |
3057
+ | discount | `string` | — | |
3058
+ | total (required) | `string` | — | |
3059
+ | buttonLabel | `string` | `'Checkout'` | |
3060
+ | onCheckout | `() => void` | — | |
3061
+ | loading | `boolean` | `false` | |
3062
+ | className | `string` | — | |
3063
+
3064
+ Also accepts the props of `ViewProps`.
3065
+ <!-- /api:CheckoutSummary -->
3066
+
3067
+ ### MetricCard
3068
+
3069
+ Stats/progress card for dashboards
3070
+
3071
+ ```tsx
3072
+ import { MetricCard } from '@thewhileloop/whileui';
3073
+ ```
2510
3074
 
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
3075
+ <!-- api:MetricCard -->
2513
3076
 
2514
- ### Infrastructure
3077
+ | Prop | Type | Default | Description |
3078
+ | ---------------- | ------------------------------------ | ----------- | ----------------------------------------------- |
3079
+ | className | `string` | — | |
3080
+ | label (required) | `string` | — | |
3081
+ | value (required) | `string \| number` | — | |
3082
+ | subtitle | `string` | — | |
3083
+ | progress | `number` | — | |
3084
+ | segments | `{ value: number; color: string }[]` | — | |
3085
+ | icon | `React.ReactNode` | — | |
3086
+ | loading | `boolean` | `false` | Show a skeleton placeholder instead of content. |
3087
+ | variant | `'default' \| 'outlined'` | `'default'` | |
2515
3088
 
2516
- - [ ] Publish to npm (`@thewhileloop/whileui`)
2517
- - [ ] CI: typecheck + format check on PR
2518
- - [ ] Automated visual regression tests (screenshot comparison)
3089
+ Also accepts the props of `ViewProps`.
3090
+ <!-- /api:MetricCard -->
2519
3091
 
2520
3092
  ## License
2521
3093