@thewhileloop/whileui 1.1.1 → 1.2.1

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 (238) hide show
  1. package/README.md +505 -38
  2. package/dist/blocks/chat/chat.d.ts.map +1 -1
  3. package/dist/blocks/chat/chat.js +15 -5
  4. package/dist/blocks/chat/chat.js.map +1 -1
  5. package/dist/blocks/commerce/feature-gate.d.ts +13 -0
  6. package/dist/blocks/commerce/feature-gate.d.ts.map +1 -0
  7. package/dist/blocks/commerce/feature-gate.js +13 -0
  8. package/dist/blocks/commerce/feature-gate.js.map +1 -0
  9. package/dist/blocks/commerce/index.d.ts +5 -0
  10. package/dist/blocks/commerce/index.d.ts.map +1 -1
  11. package/dist/blocks/commerce/index.js +5 -0
  12. package/dist/blocks/commerce/index.js.map +1 -1
  13. package/dist/blocks/commerce/metric-card.d.ts +3 -1
  14. package/dist/blocks/commerce/metric-card.d.ts.map +1 -1
  15. package/dist/blocks/commerce/metric-card.js +8 -1
  16. package/dist/blocks/commerce/metric-card.js.map +1 -1
  17. package/dist/blocks/commerce/plan-toggle.d.ts +12 -0
  18. package/dist/blocks/commerce/plan-toggle.d.ts.map +1 -0
  19. package/dist/blocks/commerce/plan-toggle.js +19 -0
  20. package/dist/blocks/commerce/plan-toggle.js.map +1 -0
  21. package/dist/blocks/commerce/pricing-card.d.ts +3 -1
  22. package/dist/blocks/commerce/pricing-card.d.ts.map +1 -1
  23. package/dist/blocks/commerce/pricing-card.js +9 -1
  24. package/dist/blocks/commerce/pricing-card.js.map +1 -1
  25. package/dist/blocks/commerce/product-card.d.ts +3 -1
  26. package/dist/blocks/commerce/product-card.d.ts.map +1 -1
  27. package/dist/blocks/commerce/product-card.js +16 -2
  28. package/dist/blocks/commerce/product-card.js.map +1 -1
  29. package/dist/blocks/commerce/subscription-card.d.ts +17 -0
  30. package/dist/blocks/commerce/subscription-card.d.ts.map +1 -0
  31. package/dist/blocks/commerce/subscription-card.js +18 -0
  32. package/dist/blocks/commerce/subscription-card.js.map +1 -0
  33. package/dist/blocks/commerce/upgrade-banner.d.ts +12 -0
  34. package/dist/blocks/commerce/upgrade-banner.d.ts.map +1 -0
  35. package/dist/blocks/commerce/upgrade-banner.js +9 -0
  36. package/dist/blocks/commerce/upgrade-banner.js.map +1 -0
  37. package/dist/blocks/commerce/usage-bar.d.ts +11 -0
  38. package/dist/blocks/commerce/usage-bar.d.ts.map +1 -0
  39. package/dist/blocks/commerce/usage-bar.js +33 -0
  40. package/dist/blocks/commerce/usage-bar.js.map +1 -0
  41. package/dist/blocks/datepicker/date-range-picker-modal.d.ts +3 -2
  42. package/dist/blocks/datepicker/date-range-picker-modal.d.ts.map +1 -1
  43. package/dist/blocks/datepicker/date-range-picker-modal.js +16 -3
  44. package/dist/blocks/datepicker/date-range-picker-modal.js.map +1 -1
  45. package/dist/blocks/datepicker/datepicker-inline.d.ts.map +1 -1
  46. package/dist/blocks/datepicker/datepicker-inline.js +3 -1
  47. package/dist/blocks/datepicker/datepicker-inline.js.map +1 -1
  48. package/dist/blocks/datepicker/datepicker-modal.d.ts +3 -2
  49. package/dist/blocks/datepicker/datepicker-modal.d.ts.map +1 -1
  50. package/dist/blocks/datepicker/datepicker-modal.js +16 -3
  51. package/dist/blocks/datepicker/datepicker-modal.js.map +1 -1
  52. package/dist/blocks/datepicker/use-calendar-theme.d.ts.map +1 -1
  53. package/dist/blocks/datepicker/use-calendar-theme.js +2 -1
  54. package/dist/blocks/datepicker/use-calendar-theme.js.map +1 -1
  55. package/dist/blocks/layout/app-shell.d.ts +5 -1
  56. package/dist/blocks/layout/app-shell.d.ts.map +1 -1
  57. package/dist/blocks/layout/app-shell.js +2 -2
  58. package/dist/blocks/layout/app-shell.js.map +1 -1
  59. package/dist/blocks/layout/confirm-action-sheet.d.ts +3 -2
  60. package/dist/blocks/layout/confirm-action-sheet.d.ts.map +1 -1
  61. package/dist/blocks/layout/confirm-action-sheet.js +23 -4
  62. package/dist/blocks/layout/confirm-action-sheet.js.map +1 -1
  63. package/dist/blocks/layout/form-modal-screen.d.ts.map +1 -1
  64. package/dist/blocks/layout/form-modal-screen.js +23 -1
  65. package/dist/blocks/layout/form-modal-screen.js.map +1 -1
  66. package/dist/blocks/layout/index.d.ts +2 -1
  67. package/dist/blocks/layout/index.d.ts.map +1 -1
  68. package/dist/blocks/layout/index.js +1 -0
  69. package/dist/blocks/layout/index.js.map +1 -1
  70. package/dist/blocks/layout/page-skeleton.d.ts +7 -1
  71. package/dist/blocks/layout/page-skeleton.d.ts.map +1 -1
  72. package/dist/blocks/layout/page-skeleton.js +7 -14
  73. package/dist/blocks/layout/page-skeleton.js.map +1 -1
  74. package/dist/blocks/layout/screen-skeleton.d.ts +16 -0
  75. package/dist/blocks/layout/screen-skeleton.d.ts.map +1 -0
  76. package/dist/blocks/layout/screen-skeleton.js +8 -0
  77. package/dist/blocks/layout/screen-skeleton.js.map +1 -0
  78. package/dist/blocks/layout/sheet.d.ts +3 -2
  79. package/dist/blocks/layout/sheet.d.ts.map +1 -1
  80. package/dist/blocks/layout/sheet.js +39 -6
  81. package/dist/blocks/layout/sheet.js.map +1 -1
  82. package/dist/blocks/layout/smart-input.d.ts.map +1 -1
  83. package/dist/blocks/layout/smart-input.js +8 -1
  84. package/dist/blocks/layout/smart-input.js.map +1 -1
  85. package/dist/blocks/lists/list-item.d.ts +3 -1
  86. package/dist/blocks/lists/list-item.d.ts.map +1 -1
  87. package/dist/blocks/lists/list-item.js +9 -1
  88. package/dist/blocks/lists/list-item.js.map +1 -1
  89. package/dist/blocks/lists/notification-item.d.ts +3 -1
  90. package/dist/blocks/lists/notification-item.d.ts.map +1 -1
  91. package/dist/blocks/lists/notification-item.js +10 -2
  92. package/dist/blocks/lists/notification-item.js.map +1 -1
  93. package/dist/blocks/lists/swipeable-item.d.ts.map +1 -1
  94. package/dist/blocks/lists/swipeable-item.js +5 -3
  95. package/dist/blocks/lists/swipeable-item.js.map +1 -1
  96. package/dist/blocks/navigation/bottom-nav.d.ts.map +1 -1
  97. package/dist/blocks/navigation/bottom-nav.js +9 -2
  98. package/dist/blocks/navigation/bottom-nav.js.map +1 -1
  99. package/dist/blocks/navigation/drawer-menu.d.ts +3 -2
  100. package/dist/blocks/navigation/drawer-menu.d.ts.map +1 -1
  101. package/dist/blocks/navigation/drawer-menu.js +88 -25
  102. package/dist/blocks/navigation/drawer-menu.js.map +1 -1
  103. package/dist/blocks/navigation/header.d.ts +1 -1
  104. package/dist/blocks/navigation/header.d.ts.map +1 -1
  105. package/dist/blocks/navigation/header.js +12 -3
  106. package/dist/blocks/navigation/header.js.map +1 -1
  107. package/dist/blocks/navigation/navigation-sidebar.d.ts.map +1 -1
  108. package/dist/blocks/navigation/navigation-sidebar.js +9 -2
  109. package/dist/blocks/navigation/navigation-sidebar.js.map +1 -1
  110. package/dist/blocks/navigation/tab-bar.d.ts.map +1 -1
  111. package/dist/blocks/navigation/tab-bar.js +8 -2
  112. package/dist/blocks/navigation/tab-bar.js.map +1 -1
  113. package/dist/components/accordion/accordion.d.ts +1 -1
  114. package/dist/components/accordion/accordion.d.ts.map +1 -1
  115. package/dist/components/accordion/accordion.js +10 -4
  116. package/dist/components/accordion/accordion.js.map +1 -1
  117. package/dist/components/alert/alert.js +4 -4
  118. package/dist/components/alert/alert.js.map +1 -1
  119. package/dist/components/alert-dialog/alert-dialog.d.ts +8 -4
  120. package/dist/components/alert-dialog/alert-dialog.d.ts.map +1 -1
  121. package/dist/components/alert-dialog/alert-dialog.js +159 -14
  122. package/dist/components/alert-dialog/alert-dialog.js.map +1 -1
  123. package/dist/components/alert-dialog/index.d.ts +1 -1
  124. package/dist/components/alert-dialog/index.d.ts.map +1 -1
  125. package/dist/components/alert-dialog/index.js.map +1 -1
  126. package/dist/components/badge/badge.js +6 -6
  127. package/dist/components/badge/badge.js.map +1 -1
  128. package/dist/components/button/button.d.ts.map +1 -1
  129. package/dist/components/button/button.js +11 -5
  130. package/dist/components/button/button.js.map +1 -1
  131. package/dist/components/card/card.d.ts +2 -1
  132. package/dist/components/card/card.d.ts.map +1 -1
  133. package/dist/components/card/card.js +12 -3
  134. package/dist/components/card/card.js.map +1 -1
  135. package/dist/components/checkbox/checkbox.d.ts.map +1 -1
  136. package/dist/components/checkbox/checkbox.js +7 -1
  137. package/dist/components/checkbox/checkbox.js.map +1 -1
  138. package/dist/components/context-menu/context-menu.d.ts +3 -2
  139. package/dist/components/context-menu/context-menu.d.ts.map +1 -1
  140. package/dist/components/context-menu/context-menu.js +12 -3
  141. package/dist/components/context-menu/context-menu.js.map +1 -1
  142. package/dist/components/data-row/data-row.d.ts.map +1 -1
  143. package/dist/components/data-row/data-row.js +8 -5
  144. package/dist/components/data-row/data-row.js.map +1 -1
  145. package/dist/components/dialog/dialog.d.ts +3 -2
  146. package/dist/components/dialog/dialog.d.ts.map +1 -1
  147. package/dist/components/dialog/dialog.js +14 -3
  148. package/dist/components/dialog/dialog.js.map +1 -1
  149. package/dist/components/dropdown-menu/dropdown-menu.d.ts +3 -2
  150. package/dist/components/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  151. package/dist/components/dropdown-menu/dropdown-menu.js +12 -3
  152. package/dist/components/dropdown-menu/dropdown-menu.js.map +1 -1
  153. package/dist/components/form-field/form-field.d.ts.map +1 -1
  154. package/dist/components/form-field/form-field.js +6 -3
  155. package/dist/components/form-field/form-field.js.map +1 -1
  156. package/dist/components/hover-card/hover-card.d.ts +2 -1
  157. package/dist/components/hover-card/hover-card.d.ts.map +1 -1
  158. package/dist/components/hover-card/hover-card.js +17 -2
  159. package/dist/components/hover-card/hover-card.js.map +1 -1
  160. package/dist/components/input/input.d.ts.map +1 -1
  161. package/dist/components/input/input.js +3 -1
  162. package/dist/components/input/input.js.map +1 -1
  163. package/dist/components/labeled-field/labeled-field.d.ts.map +1 -1
  164. package/dist/components/labeled-field/labeled-field.js +3 -1
  165. package/dist/components/labeled-field/labeled-field.js.map +1 -1
  166. package/dist/components/menubar/menubar.d.ts +3 -2
  167. package/dist/components/menubar/menubar.d.ts.map +1 -1
  168. package/dist/components/menubar/menubar.js +12 -3
  169. package/dist/components/menubar/menubar.js.map +1 -1
  170. package/dist/components/numeric-input/numeric-input.d.ts.map +1 -1
  171. package/dist/components/numeric-input/numeric-input.js +15 -3
  172. package/dist/components/numeric-input/numeric-input.js.map +1 -1
  173. package/dist/components/otp-input/index.d.ts +2 -0
  174. package/dist/components/otp-input/index.d.ts.map +1 -0
  175. package/dist/components/otp-input/index.js +2 -0
  176. package/dist/components/otp-input/index.js.map +1 -0
  177. package/dist/components/otp-input/otp-input.d.ts +97 -0
  178. package/dist/components/otp-input/otp-input.d.ts.map +1 -0
  179. package/dist/components/otp-input/otp-input.js +88 -0
  180. package/dist/components/otp-input/otp-input.js.map +1 -0
  181. package/dist/components/popover/popover.d.ts +2 -1
  182. package/dist/components/popover/popover.d.ts.map +1 -1
  183. package/dist/components/popover/popover.js +16 -3
  184. package/dist/components/popover/popover.js.map +1 -1
  185. package/dist/components/segmented-control/segmented-control.d.ts +3 -3
  186. package/dist/components/segmented-control/segmented-control.d.ts.map +1 -1
  187. package/dist/components/segmented-control/segmented-control.js +19 -5
  188. package/dist/components/segmented-control/segmented-control.js.map +1 -1
  189. package/dist/components/select/select.d.ts +2 -1
  190. package/dist/components/select/select.d.ts.map +1 -1
  191. package/dist/components/select/select.js +30 -11
  192. package/dist/components/select/select.js.map +1 -1
  193. package/dist/components/skeleton/skeleton.d.ts.map +1 -1
  194. package/dist/components/skeleton/skeleton.js +6 -2
  195. package/dist/components/skeleton/skeleton.js.map +1 -1
  196. package/dist/components/switch/switch.d.ts.map +1 -1
  197. package/dist/components/switch/switch.js +7 -1
  198. package/dist/components/switch/switch.js.map +1 -1
  199. package/dist/components/tabs/tabs.d.ts +3 -1
  200. package/dist/components/tabs/tabs.d.ts.map +1 -1
  201. package/dist/components/tabs/tabs.js +9 -3
  202. package/dist/components/tabs/tabs.js.map +1 -1
  203. package/dist/components/textarea/textarea.d.ts +2 -2
  204. package/dist/components/textarea/textarea.d.ts.map +1 -1
  205. package/dist/components/textarea/textarea.js +19 -6
  206. package/dist/components/textarea/textarea.js.map +1 -1
  207. package/dist/components/toast/toast.d.ts.map +1 -1
  208. package/dist/components/toast/toast.js +12 -2
  209. package/dist/components/toast/toast.js.map +1 -1
  210. package/dist/index.d.ts +6 -1
  211. package/dist/index.d.ts.map +1 -1
  212. package/dist/index.js +5 -0
  213. package/dist/index.js.map +1 -1
  214. package/dist/lib/frosted-surface.d.ts +33 -0
  215. package/dist/lib/frosted-surface.d.ts.map +1 -0
  216. package/dist/lib/frosted-surface.js +181 -0
  217. package/dist/lib/frosted-surface.js.map +1 -0
  218. package/dist/lib/interaction-tokens.d.ts +26 -0
  219. package/dist/lib/interaction-tokens.d.ts.map +1 -0
  220. package/dist/lib/interaction-tokens.js +70 -0
  221. package/dist/lib/interaction-tokens.js.map +1 -0
  222. package/dist/lib/react-native-classname.d.ts +23 -0
  223. package/dist/lib/react-native-classname.d.ts.map +1 -0
  224. package/dist/lib/react-native-classname.js +2 -0
  225. package/dist/lib/react-native-classname.js.map +1 -0
  226. package/dist/lib/theme-colors.d.ts +33 -4
  227. package/dist/lib/theme-colors.d.ts.map +1 -1
  228. package/dist/lib/theme-colors.js +249 -37
  229. package/dist/lib/theme-colors.js.map +1 -1
  230. package/dist/lib/visual-tokens.d.ts +39 -0
  231. package/dist/lib/visual-tokens.d.ts.map +1 -0
  232. package/dist/lib/visual-tokens.js +91 -0
  233. package/dist/lib/visual-tokens.js.map +1 -0
  234. package/dist/vite.d.ts +47 -0
  235. package/dist/vite.d.ts.map +1 -0
  236. package/dist/vite.js +110 -0
  237. package/dist/vite.js.map +1 -0
  238. package/package.json +10 -1
package/README.md CHANGED
@@ -116,6 +116,37 @@ export default function App() {
116
116
 
117
117
  > **Note:** If you use Select, Popover, Tooltip, or HoverCard, add `<PortalHost />` at the root of your app (as the last child).
118
118
 
119
+ ### Vite + React Native Web + WhileUI setup
120
+
121
+ Use the WhileUI Vite compatibility helper when your app includes portal-based primitives.
122
+
123
+ 1. Install Vite + RN web basics:
124
+
125
+ ```bash
126
+ bun add react-dom react-native-web
127
+ bun add -d vite @vitejs/plugin-react
128
+ ```
129
+
130
+ 2. `vite.config.ts`:
131
+
132
+ ```ts
133
+ import { defineConfig } from 'vite';
134
+ import react from '@vitejs/plugin-react';
135
+ import { withWhileUIViteCompat } from '@thewhileloop/whileui/vite';
136
+
137
+ export default defineConfig(withWhileUIViteCompat({ plugins: [react()] }));
138
+ ```
139
+
140
+ 3. Keep `PortalHost` at app root when using Select/Popover/Tooltip/HoverCard.
141
+
142
+ Troubleshooting:
143
+
144
+ | Symptom | Likely cause | Fix |
145
+ | -------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
146
+ | `Unexpected token <` from `@rn-primitives/*/dist/*.mjs` | rn-primitives ships raw JSX in dist for some packages | Use `withWhileUIViteCompat(...)` in `vite.config.ts` |
147
+ | `className` rejected on RN primitives in consumer TS app | RN `className` augmentation not loaded from package entry | Import components from `@thewhileloop/whileui` (or side-effect import the package entry before use) |
148
+ | Theme token resolves differently on native/web | Missing `--app-color-*` fallback for non-RN-native values | Add `--app-color-*` fallbacks in `global.css` for any `oklch(...)` token used by native APIs |
149
+
119
150
  ## Usage
120
151
 
121
152
  ```tsx
@@ -152,7 +183,7 @@ function MyScreen() {
152
183
  **Core package** exports:
153
184
 
154
185
  - **Primitives** — Button, Input, Card, Text, etc.
155
- - **Generic layout blocks** — EmptyState, ErrorState, LoadingScreen, ContentSkeleton, PageSkeleton
186
+ - **Generic layout blocks** — EmptyState, ErrorState, LoadingScreen, ContentSkeleton, PageSkeleton, ScreenSkeleton
156
187
  - **Layout infrastructure** — FormModalScreen, ConfirmActionSheet, SmartInput, ActionBar
157
188
  - **Navigation, Chat, Lists, Commerce, Media, DatePicker** — Blocks that rarely need app-specific customization
158
189
 
@@ -174,6 +205,7 @@ function MyScreen() {
174
205
  ## Quick Reference (AI / Code Generation)
175
206
 
176
207
  - **Full-screen:** `AppShell` + `Header` in `header` + `BottomNav` in `bottomNav` + content in `children`
208
+ - **No-jump loading:** keep `AppShell` mounted, set `loading` + `skeleton` (e.g., `PageSkeleton`/`ScreenSkeleton`)
177
209
  - **Layout:** `Stack` (vertical), `Row` (horizontal) — both support `gap`, `align`, `justify`
178
210
  - **Auth callbacks:** Auth templates use objects: `onSubmit({ email, password })`, `onSubmit({ firstName, lastName, email, password })`, etc. Copy templates from `apps/showcase/templates/auth/`.
179
211
  - **PortalHost:** Add `<PortalHost />` at app root for Select, Popover, Tooltip, HoverCard.
@@ -200,6 +232,7 @@ function MyScreen() {
200
232
  | **Button** | default, destructive, outline, secondary, ghost, link | 4 sizes, ButtonText & ButtonIcon sub-components |
201
233
  | **Input** | default, error | TextInput wrapper with themed styling |
202
234
  | **NumericInput** | default, error | Numeric input with prefix/suffix slots, optional steppers, and compact size |
235
+ | **OTPInput** | default, error; default, compact | Verification code input with auto-focus, paste, secure mask, loading skeleton, error shake |
203
236
  | **FormField** | default, compact | Compound API: FormField, FormLabel, FormControl, FormHint, FormMessage |
204
237
  | **LabeledField** | default, compact | Field wrapper with label/helper/error plus left/right slots |
205
238
  | **Textarea** | — | Multi-line text input |
@@ -292,6 +325,7 @@ Copy from `apps/showcase/templates/auth/`:
292
325
  | **FormModalScreen** | Modal scaffold for forms with loading states |
293
326
  | **ContentSkeleton** | Page/content placeholder with variants (list, card, generic) |
294
327
  | **PageSkeleton** | Variant-based page layouts (dashboard, list, settings, card, generic) |
328
+ | **ScreenSkeleton** | Header slot/placeholder + PageSkeleton content in one block |
295
329
  | **ErrorBoundary** | React ErrorBoundary that renders ErrorState by default |
296
330
  | **EmptyState** | Empty content placeholder |
297
331
  | **ErrorState** | Error display with retry |
@@ -333,12 +367,17 @@ Copy from `apps/showcase/templates/profile/`:
333
367
 
334
368
  ### Commerce
335
369
 
336
- | Block | Description |
337
- | ------------------- | ---------------------------------- |
338
- | **ProductCard** | Product card with badge/media |
339
- | **PricingCard** | Pricing tiers with feature list |
340
- | **CheckoutSummary** | Cart summary with line items |
341
- | **MetricCard** | Stats/progress card for dashboards |
370
+ | Block | Description |
371
+ | -------------------- | --------------------------------------------- |
372
+ | **ProductCard** | Product card with badge/media |
373
+ | **PricingCard** | Pricing tiers with feature list |
374
+ | **CheckoutSummary** | Cart summary with line items |
375
+ | **MetricCard** | Stats/progress card for dashboards |
376
+ | **SubscriptionCard** | Current plan card with manage/upgrade actions |
377
+ | **FeatureGate** | Locked feature state with upgrade CTA |
378
+ | **UsageBar** | Quota meter with warning/exceeded states |
379
+ | **PlanToggle** | Monthly vs annual plan switcher |
380
+ | **UpgradeBanner** | Inline upgrade banner with action/dismiss |
342
381
 
343
382
  ### Media
344
383
 
@@ -415,6 +454,22 @@ import { AppShell, Header, BottomNav, ScrollView } from '@thewhileloop/whileui';
415
454
  | E-commerce | ProductCard list → CheckoutSummary + ActionBar |
416
455
  | Chat | Chat + ChatSuggestions + SmartInput (attach, send). Extensible for images/tags |
417
456
  | App shell | AppShell + Header + BottomNav + content |
457
+ | Loading | AppShell (`loading`) + PageSkeleton/ScreenSkeleton (keep header mounted) |
458
+
459
+ ### Component-Level Loading
460
+
461
+ Many blocks accept a `loading` prop that renders a skeleton placeholder matching the component's own shape. No separate skeleton component needed — the block knows its own layout.
462
+
463
+ ```tsx
464
+ <ProductCard loading title="" price="" />
465
+ <ListItem loading title="" />
466
+ <NotificationItem loading title="" message="" time="" />
467
+ <MetricCard loading label="" value="" />
468
+ <SubscriptionCard loading planName="" price="" />
469
+ <PricingCard loading name="" price="" features={[]} />
470
+ ```
471
+
472
+ Blocks with `loading` support: ProductCard, ListItem, NotificationItem, MetricCard, SubscriptionCard, PricingCard, AppShell, Chat, FormModalScreen, CheckoutSummary.
418
473
 
419
474
  Block props: see TypeScript interfaces in `packages/ui/src/blocks`.
420
475
 
@@ -490,8 +545,10 @@ Themes are defined in `global.css` using CSS variables with OKLCH colors:
490
545
 
491
546
  The WhileUI token contract is strict for cross-app reuse. Define these in **every** theme variant (`@variant light`, `@variant dark`, and custom variants):
492
547
 
493
- - Required core tokens: `background`, `foreground`, `card`, `card-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `border`, `input`, `ring`
548
+ - 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`
494
549
  - Optional status tokens: `success`, `success-foreground`, `warning`, `warning-foreground`, `info`, `info-foreground`
550
+ - Optional effect tokens: `overlay`, `overlay-strong`, `surface-elevated`, `surface-border`, `surface-highlight`, `surface-translucent`, `surface-translucent-border`, `state-hover`, `state-pressed`, `state-disabled`
551
+ - Optional interaction/motion tokens: `--ui-press-opacity`, `--ui-press-opacity-strong`, `--ui-disabled-opacity`, `--ui-disabled-opacity-soft`, `--ui-disabled-opacity-subtle`, `--ui-inactive-opacity`, `--ui-motion-fast`, `--ui-motion-normal`, `--ui-motion-slow`, `--ui-drawer-open-duration`, `--ui-drawer-close-duration`, `--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-experimental-blur`, `--ui-drawer-frosted-inset`, `--ui-drawer-frosted-radius`, `--ui-drawer-content-top-padding`
495
552
  - Optional scale tokens: spacing (`--spacing`, `--spacing-*`), typography (`--text-*`, `--leading-*`, `--tracking-*`), radius (`--radius-*`), elevation (`--shadow-*`)
496
553
 
497
554
  Minimal contract example:
@@ -504,6 +561,8 @@ Minimal contract example:
504
561
  --color-foreground: oklch(0.15 0 0);
505
562
  --color-card: oklch(1 0 0);
506
563
  --color-card-foreground: oklch(0.15 0 0);
564
+ --color-popover: oklch(1 0 0);
565
+ --color-popover-foreground: oklch(0.15 0 0);
507
566
  --color-primary: oklch(0.2 0 0);
508
567
  --color-primary-foreground: oklch(0.98 0 0);
509
568
  --color-secondary: oklch(0.95 0 0);
@@ -530,15 +589,66 @@ import { Uniwind } from 'uniwind';
530
589
  Uniwind.setTheme('dark'); // or 'light' or 'system'
531
590
  ```
532
591
 
592
+ ### Frosted / Translucent Theme
593
+
594
+ 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.
595
+
596
+ **Option A — Override in existing light/dark:** In your `@variant light` and `@variant dark` blocks, set surface tokens to semi-transparent values:
597
+
598
+ ```css
599
+ @variant light {
600
+ /* ... other tokens ... */
601
+ --color-surface-elevated: oklch(0.98 0.01 95 / 0.4);
602
+ --color-surface-border: oklch(1 0 0 / 0.25);
603
+ --color-surface-highlight: oklch(1 0 0 / 0.4);
604
+ }
605
+ ```
606
+
607
+ **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.
608
+
609
+ **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.
610
+
611
+ ```tsx
612
+ import { BlurView } from 'expo-blur';
613
+ import { registerFrostedBlurView } from '@thewhileloop/whileui';
614
+
615
+ registerFrostedBlurView(BlurView);
616
+ ```
617
+
618
+ Optional generic tokens: `surface-translucent`, `surface-translucent-border` — use them if you need a distinct token from `surface-elevated` for overlay panels.
619
+
620
+ Built-in overlays and cards support opt-in frosted mode via:
621
+
622
+ - `frosted?: boolean`
623
+ - `blurIntensity?: number`
624
+ - `blurTintToken?: 'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover'`
625
+
626
+ Default blur presets map to CSS visual tokens:
627
+
628
+ - subtle: `--ui-blur-intensity-subtle` (default `18`)
629
+ - medium: `--ui-blur-intensity-medium` (default `22`)
630
+ - strong: `--ui-blur-intensity-strong` (default `28`)
631
+
632
+ Additional frosted tuning tokens:
633
+
634
+ - `--ui-blur-saturation-pct` (web fallback saturation multiplier)
635
+ - `--ui-frosted-highlight-height` (top highlight strip height in px, set `0` to disable hard top sheen)
636
+ - `--ui-frosted-backdrop-blur-intensity` (default backdrop blur amount)
637
+ - `--ui-frosted-backdrop-blur-scale` (ratio used when component blur is overridden)
638
+ - `--ui-frosted-android-experimental-blur` (`1` enables `expo-blur` Android experimental path)
639
+ - `--ui-drawer-frosted-inset` (floating inset for frosted drawer shells)
640
+ - `--ui-drawer-frosted-radius` (drawer corner radius in px)
641
+ - `--ui-drawer-content-top-padding` (drawer content top spacing baseline)
642
+
533
643
  ### Theme Colors for RN Primitives
534
644
 
535
- Some React Native APIs require native color strings (hex/rgb/hsl/named). Use `useThemeColors` or `useIconColors` to read from your `global.css` theme. If your theme uses `oklch(...)`, add `--app-color-*` hex fallbacks — `useThemeColors` will use them when `--color-*` is not RN-native.
645
+ 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.
536
646
 
537
647
  ```tsx
538
648
  import { useThemeColors, useIconColors } from '@thewhileloop/whileui';
539
649
  import { Feather } from '@expo/vector-icons';
540
650
 
541
- // Full palette (primary, foreground, muted, background, border, accent, destructive, etc.)
651
+ // Full palette (core semantic colors + status + overlay/effect tokens)
542
652
  const colors = useThemeColors();
543
653
 
544
654
  // Shorthand for icons: foreground, muted, primary, primaryForeground, accent, destructive
@@ -548,11 +658,48 @@ const iconColors = useIconColors();
548
658
  <Spinner color={colors.foreground} /> // Spinner defaults to this when color not passed
549
659
  ```
550
660
 
551
- - **useThemeColors** / **useThemeTokens** — Returns RN-safe color strings (hex) for all semantic tokens. Use for RefreshControl, LinearGradient, charts. Falls back to `--app-color-*` when `--color-*` is not RN-native (e.g. `oklch(...)`).
661
+ - **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.
552
662
  - **useIconColors** — Subset for icons. Maps `muted` → `mutedForeground` (readable on backgrounds).
553
663
 
554
664
  Input, Textarea, NumericInput, SmartInput, Spinner, and LoadingScreen default to theme colors when you omit `placeholderTextColor` or `spinnerColor`.
555
665
 
666
+ ### Interaction + Motion Tokens
667
+
668
+ WhileUI components also read optional `--ui-*` tokens for deeper control of press feedback, disabled states, and motion timing:
669
+
670
+ ```css
671
+ @theme {
672
+ --ui-press-opacity: 0.72;
673
+ --ui-press-opacity-strong: 0.9;
674
+ --ui-disabled-opacity: 0.5;
675
+ --ui-disabled-opacity-soft: 0.6;
676
+ --ui-disabled-opacity-subtle: 0.4;
677
+ --ui-inactive-opacity: 0.5;
678
+ --ui-motion-fast: 160;
679
+ --ui-motion-normal: 220;
680
+ --ui-motion-slow: 300;
681
+ --ui-drawer-open-duration: 300;
682
+ --ui-drawer-close-duration: 220;
683
+ --ui-blur-intensity-subtle: 18;
684
+ --ui-blur-intensity-medium: 22;
685
+ --ui-blur-intensity-strong: 28;
686
+ --ui-blur-saturation-pct: 185;
687
+ --ui-frosted-highlight-height: 0;
688
+ --ui-frosted-backdrop-blur-intensity: 14;
689
+ --ui-frosted-backdrop-blur-scale: 0.55;
690
+ --ui-frosted-android-experimental-blur: 1;
691
+ --ui-drawer-frosted-inset: 0;
692
+ --ui-drawer-frosted-radius: 28;
693
+ --ui-drawer-content-top-padding: 0;
694
+ }
695
+ ```
696
+
697
+ For custom components, use:
698
+
699
+ ```tsx
700
+ import { useInteractionTokens, withInteractivePressableStyle } from '@thewhileloop/whileui';
701
+ ```
702
+
556
703
  Optional RN fallback tokens (hex/rgb/hsl/named):
557
704
 
558
705
  ```css
@@ -562,23 +709,75 @@ Optional RN fallback tokens (hex/rgb/hsl/named):
562
709
  --app-color-primary: #000000;
563
710
  --app-color-primary-foreground: #ffffff;
564
711
  --app-color-foreground: #000000;
712
+ --app-color-background: #ffffff;
713
+ --app-color-card: #ffffff;
714
+ --app-color-card-foreground: #000000;
715
+ --app-color-popover: #ffffff;
716
+ --app-color-popover-foreground: #000000;
717
+ --app-color-secondary: #f5f5f5;
718
+ --app-color-secondary-foreground: #171717;
565
719
  --app-color-muted: #f5f5f5;
566
720
  --app-color-muted-foreground: #737373;
567
- --app-color-background: #ffffff;
568
721
  --app-color-border: #e5e5e5;
722
+ --app-color-input: #e5e5e5;
723
+ --app-color-ring: #94a3b8;
569
724
  --app-color-accent: #22c55e;
725
+ --app-color-accent-foreground: #0a0a0a;
570
726
  --app-color-destructive: #dc2626;
727
+ --app-color-destructive-foreground: #ffffff;
728
+ --app-color-success: #16a34a;
729
+ --app-color-success-foreground: #ffffff;
730
+ --app-color-warning: #f59e0b;
731
+ --app-color-warning-foreground: #111827;
732
+ --app-color-info: #3b82f6;
733
+ --app-color-info-foreground: #ffffff;
734
+ --app-color-overlay: rgba(0, 0, 0, 0.4);
735
+ --app-color-overlay-strong: rgba(0, 0, 0, 0.55);
736
+ --app-color-surface-elevated: #ffffff;
737
+ --app-color-surface-translucent: rgba(255, 255, 255, 0.5);
738
+ --app-color-surface-translucent-border: rgba(255, 255, 255, 0.18);
739
+ --app-color-surface-border: rgba(255, 255, 255, 0.3);
740
+ --app-color-surface-highlight: rgba(255, 255, 255, 0.3);
741
+ --app-color-state-hover: rgba(0, 0, 0, 0.05);
742
+ --app-color-state-pressed: rgba(0, 0, 0, 0.12);
743
+ --app-color-state-disabled: rgba(0, 0, 0, 0.4);
571
744
  }
572
745
  @variant dark {
573
746
  --app-color-primary: #ffffff;
574
747
  --app-color-primary-foreground: #000000;
575
748
  --app-color-foreground: #ffffff;
749
+ --app-color-background: #000000;
750
+ --app-color-card: #0f0f10;
751
+ --app-color-card-foreground: #ffffff;
752
+ --app-color-popover: #121214;
753
+ --app-color-popover-foreground: #ffffff;
754
+ --app-color-secondary: #2e2e2e;
755
+ --app-color-secondary-foreground: #ffffff;
576
756
  --app-color-muted: #2e2e2e;
577
757
  --app-color-muted-foreground: #999999;
578
- --app-color-background: #000000;
579
758
  --app-color-border: #3d3d3d;
759
+ --app-color-input: #3d3d3d;
760
+ --app-color-ring: #7c889a;
580
761
  --app-color-accent: #22c55e;
762
+ --app-color-accent-foreground: #0a0a0a;
581
763
  --app-color-destructive: #dc2626;
764
+ --app-color-destructive-foreground: #ffffff;
765
+ --app-color-success: #22c55e;
766
+ --app-color-success-foreground: #0a0a0a;
767
+ --app-color-warning: #fbbf24;
768
+ --app-color-warning-foreground: #111827;
769
+ --app-color-info: #60a5fa;
770
+ --app-color-info-foreground: #111827;
771
+ --app-color-overlay: rgba(0, 0, 0, 0.5);
772
+ --app-color-overlay-strong: rgba(0, 0, 0, 0.7);
773
+ --app-color-surface-elevated: #111315;
774
+ --app-color-surface-translucent: rgba(17, 19, 21, 0.45);
775
+ --app-color-surface-translucent-border: rgba(255, 255, 255, 0.18);
776
+ --app-color-surface-border: rgba(255, 255, 255, 0.18);
777
+ --app-color-surface-highlight: rgba(255, 255, 255, 0.18);
778
+ --app-color-state-hover: rgba(255, 255, 255, 0.08);
779
+ --app-color-state-pressed: rgba(255, 255, 255, 0.15);
780
+ --app-color-state-disabled: rgba(255, 255, 255, 0.4);
582
781
  }
583
782
  }
584
783
  }
@@ -674,6 +873,31 @@ import { NumericInput } from '@thewhileloop/whileui';
674
873
  | prefix / suffix | `ReactNode` | — | Left/right slots |
675
874
  | showSteppers | `boolean` | `false` | Show decrement/increment controls |
676
875
 
876
+ ## OTPInput
877
+
878
+ ```tsx
879
+ import { OTPInput } from '@thewhileloop/whileui';
880
+
881
+ <OTPInput
882
+ value={otp}
883
+ onValueChange={setOtp}
884
+ onComplete={(code) => verify(code)}
885
+ length={6}
886
+ variant="default"
887
+ />;
888
+ ```
889
+
890
+ | Prop | Type | Default | Description |
891
+ | ------------- | ------------------------- | ----------- | --------------------------------- |
892
+ | length | `number` | `6` | Number of digit cells |
893
+ | value | `string` | — | Controlled value |
894
+ | onValueChange | `(value: string) => void` | — | Called on each digit change |
895
+ | onComplete | `(code: string) => void` | — | Called when all digits are filled |
896
+ | variant | `'default' \| 'error'` | `'default'` | Error variant triggers shake |
897
+ | size | `'default' \| 'compact'` | `'default'` | Cell size |
898
+ | secure | `boolean` | `false` | Mask digits with dots |
899
+ | autoFocus | `boolean` | `false` | Focus first cell on mount |
900
+
677
901
  ## FormField
678
902
 
679
903
  ```tsx
@@ -796,10 +1020,13 @@ import {
796
1020
  </Card>;
797
1021
  ```
798
1022
 
799
- | Prop | Type | Default | Description |
800
- | -------- | ------------------------------------- | ----------- | ----------------------------------- |
801
- | padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Card interior padding |
802
- | unstyled | `boolean` | `false` | Remove built-in card surface styles |
1023
+ | Prop | Type | Default | Description |
1024
+ | ------------- | ------------------------------------------------------------------ | ------------ | ----------------------------------- |
1025
+ | padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Card interior padding |
1026
+ | unstyled | `boolean` | `false` | Remove built-in card surface styles |
1027
+ | frosted | `boolean` | `false` | Enables frosted tint + blur layer |
1028
+ | blurIntensity | `number` | token preset | Override blur amount directly |
1029
+ | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'card'` | Tint token used for frosted mode |
803
1030
 
804
1031
  ## Badge
805
1032
 
@@ -867,6 +1094,14 @@ import {
867
1094
  </Dialog>;
868
1095
  ```
869
1096
 
1097
+ `DialogContent` supports frosted props:
1098
+
1099
+ | Prop | Type | Default | Description |
1100
+ | ------------- | ------------------------------------------------------------------ | ---------------------- | -------------------------------- |
1101
+ | frosted | `boolean` | `false` | Enables frosted tint + blur |
1102
+ | blurIntensity | `number` | medium preset | Override blur amount |
1103
+ | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` | Tint token used for frosted mode |
1104
+
870
1105
  ## AlertDialog
871
1106
 
872
1107
  ```tsx
@@ -901,6 +1136,21 @@ import {
901
1136
  </AlertDialog>;
902
1137
  ```
903
1138
 
1139
+ `AlertDialogContent` supports the same frosted props as `DialogContent`.
1140
+
1141
+ | Prop | Type | Default | Description |
1142
+ | ------------- | ------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1143
+ | presentation | `'modal' \| 'native'` | `'modal'` | **Native (iOS/Android):** `'modal'` uses `Modal` (with iOS `overFullScreen` to improve stacking). `'native'` uses `Alert.alert` with copy from `AlertDialogTitle`, `AlertDialogDescription`, `AlertDialogCancel`, and `AlertDialogAction` — reliable when another fullscreen `Modal` is already open. **Web:** `'native'` is ignored; the custom `Modal` UI is always used. |
1144
+ | frosted | `boolean` | `false` | Same as `DialogContent` |
1145
+ | blurIntensity | `number` | — | Same as `DialogContent` |
1146
+ | blurTintToken | `'surfaceElevated' \| …` | — | Same as `DialogContent` |
1147
+
1148
+ ### React Native: stacked modals
1149
+
1150
+ If `AlertDialog` is a **sibling** of another fullscreen `Modal` (e.g. `<><Modal>…</Modal><AlertDialog>…</AlertDialog></>`), iOS often fails to show a second `Modal` on top, or taps feel dead, even though `open` is `true`. Prefer **`presentation="native"`** on `AlertDialogContent` in those flows so the system alert appears above the sheet. Alternatively, render the `AlertDialog` **inside** the visible `Modal` so both layers share one RN modal host.
1151
+
1152
+ **Manual QA (iOS + Android):** Open a fullscreen `Modal`, trigger a delete/confirm that uses `presentation="native"`, confirm the system alert is visible, **Cancel** closes without side effects, **Delete** runs the action handler and dismisses.
1153
+
904
1154
  ## Checkbox
905
1155
 
906
1156
  ```tsx
@@ -954,13 +1204,21 @@ import {
954
1204
  <SelectTrigger>
955
1205
  <SelectValue placeholder="Select..." />
956
1206
  </SelectTrigger>
957
- <SelectContent>
1207
+ <SelectContent frosted blurIntensity={24} blurTintToken="surfaceTranslucent">
958
1208
  <SelectItem label="Option 1" value="1" />
959
1209
  <SelectItem label="Option 2" value="2" />
960
1210
  </SelectContent>
961
1211
  </Select>;
962
1212
  ```
963
1213
 
1214
+ `SelectContent` supports optional frosted props:
1215
+
1216
+ | Prop | Type | Default |
1217
+ | ------------- | ------------------------------------------------------------------ | ------------- |
1218
+ | frosted | `boolean` | `false` |
1219
+ | blurIntensity | `number` | subtle preset |
1220
+ | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
1221
+
964
1222
  ## Tabs
965
1223
 
966
1224
  ```tsx
@@ -1075,6 +1333,14 @@ import { Popover, PopoverTrigger, PopoverContent } from '@thewhileloop/whileui';
1075
1333
  </Popover>;
1076
1334
  ```
1077
1335
 
1336
+ `PopoverContent` supports frosted props:
1337
+
1338
+ | Prop | Type | Default |
1339
+ | ------------- | ------------------------------------------------------------------ | ------------- |
1340
+ | frosted | `boolean` | `false` |
1341
+ | blurIntensity | `number` | subtle preset |
1342
+ | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
1343
+
1078
1344
  ## DropdownMenu
1079
1345
 
1080
1346
  ```tsx
@@ -1106,6 +1372,20 @@ import {
1106
1372
  </DropdownMenu>;
1107
1373
  ```
1108
1374
 
1375
+ `DropdownMenuContent`, `ContextMenuContent`, and `MenubarContent` support the same frosted props as `PopoverContent` (default blur preset: medium).
1376
+
1377
+ ## HoverCard
1378
+
1379
+ `HoverCardContent` supports the same frosted props as `PopoverContent` (default blur preset: subtle).
1380
+
1381
+ ## ContextMenu
1382
+
1383
+ `ContextMenuContent` supports the same frosted props as `PopoverContent` (default blur preset: medium).
1384
+
1385
+ ## Menubar
1386
+
1387
+ `MenubarContent` supports the same frosted props as `PopoverContent` (default blur preset: medium).
1388
+
1109
1389
  ---
1110
1390
 
1111
1391
  # Blocks API
@@ -1173,6 +1453,32 @@ import { BottomNav } from '@thewhileloop/whileui';
1173
1453
  />;
1174
1454
  ```
1175
1455
 
1456
+ ## AppShell
1457
+
1458
+ Layout shell for full-screen pages. Keep shell chrome mounted and swap only content via `loading` + `skeleton`.
1459
+
1460
+ ```tsx
1461
+ import { AppShell, PageSkeleton } from '@thewhileloop/whileui';
1462
+
1463
+ <AppShell
1464
+ header={<Header title="Settings" />}
1465
+ bottomNav={<BottomNav items={[...]} activeKey="settings" onSelect={setTab} />}
1466
+ loading={loading}
1467
+ skeleton={<PageSkeleton variant="settings" headerPlaceholder />}
1468
+ >
1469
+ <ScrollView className="flex-1 p-4">{/* Content */}</ScrollView>
1470
+ </AppShell>;
1471
+ ```
1472
+
1473
+ | Prop | Type | Default | Description |
1474
+ | ----------- | ----------- | ------- | --------------------------------------------------- |
1475
+ | `header` | `ReactNode` | — | Header slot |
1476
+ | `footer` | `ReactNode` | — | Footer slot |
1477
+ | `bottomNav` | `ReactNode` | — | Bottom navigation slot |
1478
+ | `safeArea` | `boolean` | `true` | Wrap in SafeAreaView |
1479
+ | `loading` | `boolean` | `false` | Show `skeleton` in content area, keep shell mounted |
1480
+ | `skeleton` | `ReactNode` | — | Content placeholder rendered when `loading=true` |
1481
+
1176
1482
  ## ActionBar
1177
1483
 
1178
1484
  ```tsx
@@ -1299,14 +1605,17 @@ const [range, setRange] = useState<DateRange | null>(null);
1299
1605
  />;
1300
1606
  ```
1301
1607
 
1302
- | Prop | Type | Description |
1303
- | ----------------------- | -------------------------------------- | ------------------------------- |
1304
- | `value` | `string \| null` / `DateRange \| null` | Selected date(s) YYYY-MM-DD |
1305
- | `onValueChange` | `(date) => void` | Change handler |
1306
- | `open` / `onOpenChange` | — | Modal state (modal variants) |
1307
- | `trigger` | `ReactNode` | Custom trigger (modal variants) |
1308
- | `minDate` / `maxDate` | `string` | YYYY-MM-DD bounds |
1309
- | `theme` | `CalendarTheme` | Override calendar colors |
1608
+ | Prop | Type | Description |
1609
+ | ----------------------- | ------------------------------------------------------------------ | -------------------------------------- |
1610
+ | `value` | `string \| null` / `DateRange \| null` | Selected date(s) YYYY-MM-DD |
1611
+ | `onValueChange` | `(date) => void` | Change handler |
1612
+ | `open` / `onOpenChange` | — | Modal state (modal variants) |
1613
+ | `trigger` | `ReactNode` | Custom trigger (modal variants) |
1614
+ | `minDate` / `maxDate` | `string` | YYYY-MM-DD bounds |
1615
+ | `theme` | `CalendarTheme` | Override calendar colors |
1616
+ | `frosted` | `boolean` | Enable frosted surface for modal panel |
1617
+ | `blurIntensity` | `number` | Override blur amount |
1618
+ | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | Tint token for frosted panel |
1310
1619
 
1311
1620
  ## ConfirmActionSheet
1312
1621
 
@@ -1324,6 +1633,12 @@ import { ConfirmActionSheet } from '@thewhileloop/whileui';
1324
1633
  />;
1325
1634
  ```
1326
1635
 
1636
+ | Prop | Type | Default |
1637
+ | --------------- | ------------------------------------------------------------------ | ---------------------- |
1638
+ | `frosted` | `boolean` | `false` |
1639
+ | `blurIntensity` | `number` | medium preset |
1640
+ | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
1641
+
1327
1642
  ## Sheet
1328
1643
 
1329
1644
  Bottom sheet modal with slide animation. Slots: `SheetHeader`, `SheetContent`, `SheetFooter`, `SheetClose`.
@@ -1352,12 +1667,15 @@ import {
1352
1667
  </Sheet>;
1353
1668
  ```
1354
1669
 
1355
- | Prop | Type | Default |
1356
- | ------------ | ---------------------------- | -------- |
1357
- | open | `boolean` | — |
1358
- | onOpenChange | `(open: boolean) => void` | — |
1359
- | maxHeight | `'half' \| 'full' \| number` | `'full'` |
1360
- | maxWidth | `number` | `360` |
1670
+ | Prop | Type | Default |
1671
+ | ------------- | ------------------------------------------------------------------ | ---------------------- |
1672
+ | open | `boolean` | — |
1673
+ | onOpenChange | `(open: boolean) => void` | — |
1674
+ | maxHeight | `'half' \| 'full' \| number` | `'full'` |
1675
+ | maxWidth | `number` | `360` |
1676
+ | frosted | `boolean` | `false` |
1677
+ | blurIntensity | `number` | medium preset |
1678
+ | blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
1361
1679
 
1362
1680
  ## NavigationSidebar
1363
1681
 
@@ -1445,15 +1763,38 @@ import { PageSkeleton } from '@thewhileloop/whileui';
1445
1763
  <PageSkeleton variant="settings" count={6} />
1446
1764
  <PageSkeleton variant="card" />
1447
1765
  <PageSkeleton variant="generic" />
1766
+ <PageSkeleton variant="dashboard" headerPlaceholder />
1767
+ <PageSkeleton variant="list" header={<Header title="Loading..." />} />
1448
1768
  <PageSkeleton variant="list" padding="none" className="flex-1" />
1449
1769
  ```
1450
1770
 
1451
- | Prop | Type | Default | Description |
1452
- | --------- | ------------------------------------------------------------ | ---------------------- | --------------------------------------- |
1453
- | variant | `'dashboard' \| 'list' \| 'settings' \| 'card' \| 'generic'` | required | Layout preset |
1454
- | count | `number` | 3 (list), 4 (settings) | Rows/items for list or settings variant |
1455
- | padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Container padding |
1456
- | className | `string` | — | Outer container classes |
1771
+ | Prop | Type | Default | Description |
1772
+ | ------------------- | ------------------------------------------------------------ | ---------------------- | --------------------------------------------- |
1773
+ | `variant` | `'dashboard' \| 'list' \| 'settings' \| 'card' \| 'generic'` | required | Layout preset |
1774
+ | `count` | `number` | 3 (list), 4 (settings) | Rows/items for list or settings variant |
1775
+ | `padding` | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Container padding |
1776
+ | `header` | `ReactNode` | — | Optional real header slot above content |
1777
+ | `headerPlaceholder` | `boolean \| 'compact' \| 'default'` | `false` | Skeleton header when real header is not ready |
1778
+ | `className` | `string` | — | Outer container classes |
1779
+
1780
+ ## ScreenSkeleton
1781
+
1782
+ Convenience block for loading screens that need both header and content continuity.
1783
+
1784
+ ```tsx
1785
+ import { ScreenSkeleton } from '@thewhileloop/whileui';
1786
+
1787
+ <ScreenSkeleton variant="dashboard" headerPlaceholder />
1788
+ <ScreenSkeleton variant="list" count={5} header={<Header title="Projects" />} />
1789
+ ```
1790
+
1791
+ | Prop | Type | Default | Description |
1792
+ | ------------------- | ----------------------------------- | ----------- | ------------------------------------- |
1793
+ | `variant` | `PageSkeletonVariant` | `'generic'` | Skeleton content preset |
1794
+ | `count` | `number` | — | Rows/items for list/settings variants |
1795
+ | `padding` | `PageSkeletonPadding` | `'default'` | Inner content padding |
1796
+ | `header` | `ReactNode` | — | Real header slot |
1797
+ | `headerPlaceholder` | `boolean \| 'compact' \| 'default'` | `'default'` | Placeholder header |
1457
1798
 
1458
1799
  ## ErrorBoundary
1459
1800
 
@@ -1565,6 +1906,7 @@ import { ProductCard } from '@thewhileloop/whileui';
1565
1906
  | Prop | Type | Default |
1566
1907
  | ------- | ---------------------------- | ------------ |
1567
1908
  | variant | `'vertical' \| 'horizontal'` | `'vertical'` |
1909
+ | loading | `boolean` | `false` |
1568
1910
 
1569
1911
  ## PricingCard
1570
1912
 
@@ -1587,6 +1929,90 @@ import { PricingCard } from '@thewhileloop/whileui';
1587
1929
  />;
1588
1930
  ```
1589
1931
 
1932
+ ## SubscriptionCard
1933
+
1934
+ ```tsx
1935
+ import { SubscriptionCard } from '@thewhileloop/whileui';
1936
+
1937
+ <SubscriptionCard
1938
+ planName="Pro"
1939
+ price="$29"
1940
+ period="/month"
1941
+ expiresAt="April 18, 2026"
1942
+ isActive
1943
+ onManage={() => {}}
1944
+ onUpgrade={() => {}}
1945
+ />;
1946
+ ```
1947
+
1948
+ | Prop | Type | Description |
1949
+ | ----------- | ------------ | --------------------------- |
1950
+ | `planName` | `string` | Current plan label |
1951
+ | `price` | `string` | Plan price |
1952
+ | `period` | `string` | Billing period label |
1953
+ | `expiresAt` | `string` | Renewal/expiry date text |
1954
+ | `isActive` | `boolean` | Active/inactive badge state |
1955
+ | `onManage` | `() => void` | Manage action |
1956
+ | `onUpgrade` | `() => void` | Upgrade action |
1957
+ | `loading` | `boolean` | Show skeleton placeholder |
1958
+
1959
+ ## FeatureGate
1960
+
1961
+ ```tsx
1962
+ import { FeatureGate } from '@thewhileloop/whileui';
1963
+
1964
+ <FeatureGate
1965
+ title="Advanced export is locked"
1966
+ description="Upgrade to unlock 4K export."
1967
+ buttonLabel="Upgrade"
1968
+ onUpgrade={() => {}}
1969
+ />;
1970
+ ```
1971
+
1972
+ Use `children` to show dimmed preview content with an overlay CTA.
1973
+
1974
+ ## UsageBar
1975
+
1976
+ ```tsx
1977
+ import { UsageBar } from '@thewhileloop/whileui';
1978
+
1979
+ <UsageBar label="AI generations" used={8} limit={20} />;
1980
+ ```
1981
+
1982
+ | Prop | Type | Default | Description |
1983
+ | --------- | -------------------------------------- | ------- | ------------------------------ |
1984
+ | `label` | `string` | — | Usage label |
1985
+ | `used` | `number` | — | Used amount |
1986
+ | `limit` | `number` | — | Quota limit |
1987
+ | `variant` | `'default' \| 'warning' \| 'exceeded'` | auto | Optional explicit visual state |
1988
+
1989
+ ## PlanToggle
1990
+
1991
+ ```tsx
1992
+ import { PlanToggle } from '@thewhileloop/whileui';
1993
+
1994
+ <PlanToggle
1995
+ selected="monthly"
1996
+ monthlyLabel="Monthly"
1997
+ annualLabel="Annual"
1998
+ annualDiscount="Save 20%"
1999
+ onChange={(next) => {}}
2000
+ />;
2001
+ ```
2002
+
2003
+ ## UpgradeBanner
2004
+
2005
+ ```tsx
2006
+ import { UpgradeBanner } from '@thewhileloop/whileui';
2007
+
2008
+ <UpgradeBanner
2009
+ message="Unlock unlimited exports with Pro."
2010
+ actionLabel="See plans"
2011
+ onAction={() => {}}
2012
+ onDismiss={() => {}}
2013
+ />;
2014
+ ```
2015
+
1590
2016
  ## DrawerMenu
1591
2017
 
1592
2018
  ```tsx
@@ -1611,6 +2037,47 @@ import { DrawerMenu } from '@thewhileloop/whileui';
1611
2037
  />;
1612
2038
  ```
1613
2039
 
2040
+ | Prop | Type | Default |
2041
+ | --------------- | ------------------------------------------------------------------ | ---------------------- |
2042
+ | `frosted` | `boolean` | `false` |
2043
+ | `blurIntensity` | `number` | medium preset |
2044
+ | `blurTintToken` | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'surfaceTranslucent'` |
2045
+
2046
+ ## Roadmap
2047
+
2048
+ Tracked work items for future releases.
2049
+
2050
+ ### New Components
2051
+
2052
+ - [ ] **Chip / Tag** — selectable, dismissible, multi-select with `tv()` variants, loading skeleton
2053
+ - [ ] **Rating / Stars** — interactive + read-only, half-star precision, swipe gesture, pairs with ProductCard
2054
+ - [ ] **Slider** — single + range (two thumbs), step marks, labels, Reanimated gesture, haptic on snap
2055
+ - [ ] **Carousel** — Reanimated-powered, auto-play, pagination dots, snap-to-item
2056
+ - [ ] **FAB (Floating Action Button)** — expandable action menu, Reanimated spring, auto-hide on scroll
2057
+ - [ ] **Combobox** — searchable select with keyboard navigation, empty state, async loading
2058
+ - [ ] **Data Table** — sortable headers, skeleton loading rows, row actions, responsive stacking
2059
+ - [ ] **Banner** — dismissible info/warning/success/destructive bar with icon + action, auto-dismiss timer
2060
+ - [ ] **Pagination** — compact (dots) + expanded (numbers) variants, edge-aware ellipsis, 44px touch targets
2061
+ - [ ] **Inline Calendar** — standalone calendar view reusing DatePicker logic, range selection
2062
+
2063
+ ### Component-Level Loading (`loading` prop)
2064
+
2065
+ - [x] ProductCard, ListItem, NotificationItem, MetricCard, SubscriptionCard, PricingCard
2066
+ - [x] AppShell, Chat, FormModalScreen, CheckoutSummary (already had `loading`)
2067
+ - [ ] Add `loading` to remaining blocks: Header, BottomNav, DrawerMenu, TimelineFeed, SwipeableItem
2068
+
2069
+ ### Documentation Gaps
2070
+
2071
+ - [ ] Add `## API` sections for: Textarea, Toggle, ToggleGroup, Label, Separator, Skeleton, AspectRatio, Collapsible, Text, View, Pressable
2072
+ - [ ] Add `## API` sections for blocks: FloatingBottomNav, TabBar, FormModalScreen, ErrorState, LoadingScreen, OnboardingScreen, ListItem, NotificationItem, MetricCard, SmartImage, TimelineFeed
2073
+ - [ ] Build `apps/site/` (docs website) with `registry.ts`, `demos.tsx`, `block-demos.tsx`, `props-data.ts`
2074
+
2075
+ ### Infrastructure
2076
+
2077
+ - [ ] Publish to npm (`@thewhileloop/whileui`)
2078
+ - [ ] CI: typecheck + format check on PR
2079
+ - [ ] Automated visual regression tests (screenshot comparison)
2080
+
1614
2081
  ## License
1615
2082
 
1616
2083
  MIT — [Source](https://github.com/whileloophq/whileui)