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