@thewhileloop/whileui 0.1.1 → 0.2.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 +1243 -0
- package/dist/blocks/layout/action-bar.d.ts +8 -0
- package/dist/blocks/layout/action-bar.d.ts.map +1 -0
- package/dist/blocks/layout/action-bar.js +10 -0
- package/dist/blocks/layout/action-bar.js.map +1 -0
- package/dist/blocks/layout/confirm-action-sheet.d.ts +24 -0
- package/dist/blocks/layout/confirm-action-sheet.d.ts.map +1 -0
- package/dist/blocks/layout/confirm-action-sheet.js +41 -0
- package/dist/blocks/layout/confirm-action-sheet.js.map +1 -0
- package/dist/blocks/layout/index.d.ts +2 -0
- package/dist/blocks/layout/index.d.ts.map +1 -1
- package/dist/blocks/layout/index.js +2 -0
- package/dist/blocks/layout/index.js.map +1 -1
- package/dist/blocks/navigation/index.d.ts +1 -0
- package/dist/blocks/navigation/index.d.ts.map +1 -1
- package/dist/blocks/navigation/index.js +1 -0
- package/dist/blocks/navigation/index.js.map +1 -1
- package/dist/blocks/navigation/navigation-sidebar.d.ts +23 -0
- package/dist/blocks/navigation/navigation-sidebar.d.ts.map +1 -0
- package/dist/blocks/navigation/navigation-sidebar.js +11 -0
- package/dist/blocks/navigation/navigation-sidebar.js.map +1 -0
- package/dist/components/card/card.d.ts +81 -2
- package/dist/components/card/card.d.ts.map +1 -1
- package/dist/components/card/card.js +27 -6
- package/dist/components/card/card.js.map +1 -1
- package/dist/components/card/index.d.ts +1 -1
- package/dist/components/card/index.d.ts.map +1 -1
- package/dist/components/card/index.js +1 -1
- package/dist/components/card/index.js.map +1 -1
- package/dist/components/data-row/data-row.d.ts +69 -0
- package/dist/components/data-row/data-row.d.ts.map +1 -0
- package/dist/components/data-row/data-row.js +89 -0
- package/dist/components/data-row/data-row.js.map +1 -0
- package/dist/components/data-row/index.d.ts +2 -0
- package/dist/components/data-row/index.d.ts.map +1 -0
- package/dist/components/data-row/index.js +2 -0
- package/dist/components/data-row/index.js.map +1 -0
- package/dist/components/form-field/form-field.d.ts +124 -0
- package/dist/components/form-field/form-field.d.ts.map +1 -0
- package/dist/components/form-field/form-field.js +68 -0
- package/dist/components/form-field/form-field.js.map +1 -0
- package/dist/components/form-field/index.d.ts +2 -0
- package/dist/components/form-field/index.d.ts.map +1 -0
- package/dist/components/form-field/index.js +2 -0
- package/dist/components/form-field/index.js.map +1 -0
- package/dist/components/labeled-field/index.d.ts +2 -0
- package/dist/components/labeled-field/index.d.ts.map +1 -0
- package/dist/components/labeled-field/index.js +2 -0
- package/dist/components/labeled-field/index.js.map +1 -0
- package/dist/components/labeled-field/labeled-field.d.ts +19 -0
- package/dist/components/labeled-field/labeled-field.d.ts.map +1 -0
- package/dist/components/labeled-field/labeled-field.js +15 -0
- package/dist/components/labeled-field/labeled-field.js.map +1 -0
- package/dist/components/numeric-input/index.d.ts +2 -0
- package/dist/components/numeric-input/index.d.ts.map +1 -0
- package/dist/components/numeric-input/index.js +2 -0
- package/dist/components/numeric-input/index.js.map +1 -0
- package/dist/components/numeric-input/numeric-input.d.ts +119 -0
- package/dist/components/numeric-input/numeric-input.d.ts.map +1 -0
- package/dist/components/numeric-input/numeric-input.js +129 -0
- package/dist/components/numeric-input/numeric-input.js.map +1 -0
- package/dist/components/segmented-control/index.d.ts +2 -0
- package/dist/components/segmented-control/index.d.ts.map +1 -0
- package/dist/components/segmented-control/index.js +2 -0
- package/dist/components/segmented-control/index.js.map +1 -0
- package/dist/components/segmented-control/segmented-control.d.ts +281 -0
- package/dist/components/segmented-control/segmented-control.d.ts.map +1 -0
- package/dist/components/segmented-control/segmented-control.js +110 -0
- package/dist/components/segmented-control/segmented-control.js.map +1 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/theme-bridge.d.ts +29 -0
- package/dist/lib/theme-bridge.d.ts.map +1 -0
- package/dist/lib/theme-bridge.js +79 -0
- package/dist/lib/theme-bridge.js.map +1 -0
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,1243 @@
|
|
|
1
|
+
# WhileUI Native
|
|
2
|
+
|
|
3
|
+
> **shadcn/ui for React Native** — Copy-paste components you own.
|
|
4
|
+
|
|
5
|
+
Beautiful, accessible, themeable React Native components built with [Uniwind](https://uniwind.dev) + Tailwind CSS v4. Inspired by [shadcn/ui](https://ui.shadcn.com/).
|
|
6
|
+
|
|
7
|
+
Current version: **0.2.0**
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @thewhileloop/whileui
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @thewhileloop/whileui
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Required Peer Dependencies
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm add react@^19.0.0 react-native@^0.81.0 uniwind@^1.0.0 tailwindcss@^4.0.0
|
|
21
|
+
pnpm add react-native-reanimated react-native-safe-area-context react-native-screens
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Portal Dependencies (Select, Popover, Tooltip, HoverCard)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add @rn-primitives/portal @rn-primitives/hooks @rn-primitives/slot @rn-primitives/select @rn-primitives/popover @rn-primitives/tooltip @rn-primitives/hover-card
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Setup Uniwind (required)
|
|
31
|
+
|
|
32
|
+
1. **metro.config.js** (wrap with withUniwindConfig):
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
const { withUniwindConfig } = require('uniwind/metro');
|
|
36
|
+
|
|
37
|
+
module.exports = withUniwindConfig({
|
|
38
|
+
cssEntryFile: './global.css',
|
|
39
|
+
})({
|
|
40
|
+
// your metro config
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
> `withUniwindConfig` must be the outermost wrapper. `cssEntryFile` must be a relative path string.
|
|
45
|
+
|
|
46
|
+
2. **global.css** at app root:
|
|
47
|
+
|
|
48
|
+
```css
|
|
49
|
+
@import 'tailwindcss';
|
|
50
|
+
@import 'uniwind';
|
|
51
|
+
|
|
52
|
+
/* WhileUI Noir theme - copy this or create your own */
|
|
53
|
+
@layer theme {
|
|
54
|
+
:root {
|
|
55
|
+
@variant light {
|
|
56
|
+
--color-background: oklch(1 0 0);
|
|
57
|
+
--color-foreground: oklch(0.1316 0.0041 17.69);
|
|
58
|
+
--color-card: oklch(1 0 0);
|
|
59
|
+
--color-card-foreground: oklch(0.1316 0.0041 17.69);
|
|
60
|
+
--color-primary: oklch(0.1316 0.0041 17.69);
|
|
61
|
+
--color-primary-foreground: oklch(0.98 0 0);
|
|
62
|
+
--color-secondary: oklch(0.9598 0.0017 17.69);
|
|
63
|
+
--color-secondary-foreground: oklch(0.1316 0.0041 17.69);
|
|
64
|
+
--color-muted: oklch(0.9598 0.0017 17.69);
|
|
65
|
+
--color-muted-foreground: oklch(0.5415 0.0135 17.69);
|
|
66
|
+
--color-accent: oklch(0.9598 0.0017 17.69);
|
|
67
|
+
--color-accent-foreground: oklch(0.1316 0.0041 17.69);
|
|
68
|
+
--color-destructive: oklch(0.5 0.19 27);
|
|
69
|
+
--color-destructive-foreground: oklch(0.98 0 0);
|
|
70
|
+
--color-border: oklch(0.9039 0.0034 17.69);
|
|
71
|
+
--color-input: oklch(0.9039 0.0034 17.69);
|
|
72
|
+
--color-ring: oklch(0.1316 0.0041 17.69);
|
|
73
|
+
--color-success: oklch(0.59 0.16 145);
|
|
74
|
+
--color-success-foreground: oklch(0.98 0 0);
|
|
75
|
+
--color-warning: oklch(0.75 0.18 85);
|
|
76
|
+
--color-warning-foreground: oklch(0.1316 0.0041 17.69);
|
|
77
|
+
--color-info: oklch(0.65 0.15 245);
|
|
78
|
+
--color-info-foreground: oklch(0.98 0 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@variant dark {
|
|
82
|
+
--color-background: oklch(0.1316 0.0041 17.69);
|
|
83
|
+
--color-foreground: oklch(0.98 0 0);
|
|
84
|
+
--color-card: oklch(0.1316 0.0041 17.69);
|
|
85
|
+
--color-card-foreground: oklch(0.98 0 0);
|
|
86
|
+
--color-primary: oklch(0.98 0 0);
|
|
87
|
+
--color-primary-foreground: oklch(0.1316 0.0041 17.69);
|
|
88
|
+
--color-secondary: oklch(0.2104 0.0084 17.69);
|
|
89
|
+
--color-secondary-foreground: oklch(0.98 0 0);
|
|
90
|
+
--color-muted: oklch(0.2104 0.0084 17.69);
|
|
91
|
+
--color-muted-foreground: oklch(0.6961 0.0174 17.69);
|
|
92
|
+
--color-accent: oklch(0.2104 0.0084 17.69);
|
|
93
|
+
--color-accent-foreground: oklch(0.98 0 0);
|
|
94
|
+
--color-destructive: oklch(0.45 0.18 27);
|
|
95
|
+
--color-destructive-foreground: oklch(0.98 0 0);
|
|
96
|
+
--color-border: oklch(0.2104 0.0084 17.69);
|
|
97
|
+
--color-input: oklch(0.2104 0.0084 17.69);
|
|
98
|
+
--color-ring: oklch(0.8267 0.0206 17.69);
|
|
99
|
+
--color-success: oklch(0.59 0.16 145);
|
|
100
|
+
--color-success-foreground: oklch(0.98 0 0);
|
|
101
|
+
--color-warning: oklch(0.75 0.18 85);
|
|
102
|
+
--color-warning-foreground: oklch(0.1316 0.0041 17.69);
|
|
103
|
+
--color-info: oklch(0.65 0.15 245);
|
|
104
|
+
--color-info-foreground: oklch(0.98 0 0);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
3. **App.tsx**:
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
import './global.css';
|
|
114
|
+
import { PortalHost } from '@thewhileloop/whileui';
|
|
115
|
+
import { Button, ButtonText } from '@thewhileloop/whileui';
|
|
116
|
+
|
|
117
|
+
export default function App() {
|
|
118
|
+
return (
|
|
119
|
+
<>
|
|
120
|
+
{/* Your app content */}
|
|
121
|
+
<PortalHost />
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
> **Note:** `<PortalHost />` must be added at the root of your app (as the last child) to enable portal-based components like Select, Popover, Tooltip, and HoverCard to render correctly.
|
|
128
|
+
|
|
129
|
+
## Usage
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import {
|
|
133
|
+
Button,
|
|
134
|
+
ButtonText,
|
|
135
|
+
Card,
|
|
136
|
+
CardHeader,
|
|
137
|
+
CardTitle,
|
|
138
|
+
CardContent,
|
|
139
|
+
Input,
|
|
140
|
+
Text,
|
|
141
|
+
} from '@thewhileloop/whileui';
|
|
142
|
+
|
|
143
|
+
function MyScreen() {
|
|
144
|
+
return (
|
|
145
|
+
<Card>
|
|
146
|
+
<CardHeader>
|
|
147
|
+
<CardTitle>Welcome</CardTitle>
|
|
148
|
+
</CardHeader>
|
|
149
|
+
<CardContent>
|
|
150
|
+
<Input placeholder="Email" />
|
|
151
|
+
<Button className="mt-4">
|
|
152
|
+
<ButtonText>Continue</ButtonText>
|
|
153
|
+
</Button>
|
|
154
|
+
</CardContent>
|
|
155
|
+
</Card>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Philosophy
|
|
161
|
+
|
|
162
|
+
- **Copy-Paste Ownership** — Components live in _your_ project. No `node_modules` lock-in.
|
|
163
|
+
- **Beautiful by Default** — OKLCH color system, light/dark themes, polished out of the box.
|
|
164
|
+
- **Fully Customizable** — Every component uses `tv()` variants and accepts `className` overrides.
|
|
165
|
+
- **Accessible** — Proper ARIA roles, keyboard support, controlled/uncontrolled state.
|
|
166
|
+
- **Tree-Shakeable** — Only imports what you use. `sideEffects: false`.
|
|
167
|
+
|
|
168
|
+
## Components
|
|
169
|
+
|
|
170
|
+
### Primitives
|
|
171
|
+
|
|
172
|
+
| Component | Notes |
|
|
173
|
+
| ------------- | -------------------------------- |
|
|
174
|
+
| **Text** | Themed text with variant support |
|
|
175
|
+
| **View** | Themed view wrapper |
|
|
176
|
+
| **Pressable** | Themed pressable wrapper |
|
|
177
|
+
|
|
178
|
+
### Form Controls
|
|
179
|
+
|
|
180
|
+
| Component | Variants | Notes |
|
|
181
|
+
| -------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
182
|
+
| **Button** | default, destructive, outline, secondary, ghost, link | 4 sizes, ButtonText & ButtonIcon sub-components |
|
|
183
|
+
| **Input** | default, error | TextInput wrapper with themed styling |
|
|
184
|
+
| **NumericInput** | default, error | Numeric input with prefix/suffix slots, optional steppers, and compact size |
|
|
185
|
+
| **FormField** | default, compact | Compound API: FormField, FormLabel, FormControl, FormHint, FormMessage |
|
|
186
|
+
| **LabeledField** | default, compact | Field wrapper with label/helper/error plus left/right slots |
|
|
187
|
+
| **Textarea** | — | Multi-line text input |
|
|
188
|
+
| **Checkbox** | — | Controlled/uncontrolled, accessibility roles |
|
|
189
|
+
| **Switch** | — | Controlled/uncontrolled, accessibility roles |
|
|
190
|
+
| **RadioGroup** | — | RadioGroup + RadioGroupItem |
|
|
191
|
+
| **Select** | — | Uses `SelectOption` type `{value, label}`. Includes SelectGroup, SelectLabel, SelectSeparator |
|
|
192
|
+
| **Label** | — | Form field label |
|
|
193
|
+
| **SegmentedControl** | single select | SegmentedControl, SegmentedControlItem, SegmentedControlItemText with wrapping layout support |
|
|
194
|
+
| **Toggle** | default, outline | ToggleText sub-component |
|
|
195
|
+
| **ToggleGroup** | single, multiple | Group of toggle items |
|
|
196
|
+
|
|
197
|
+
### Display
|
|
198
|
+
|
|
199
|
+
| Component | Variants | Notes |
|
|
200
|
+
| --------------- | ------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
201
|
+
| **Card** | padding (none, sm, default, lg), unstyled | Compound: Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter |
|
|
202
|
+
| **Badge** | default, secondary, destructive, outline, success | BadgeText sub-component |
|
|
203
|
+
| **Alert** | default, destructive, success, warning | AlertTitle & AlertDescription |
|
|
204
|
+
| **Avatar** | sm, default, lg | AvatarImage + AvatarFallback |
|
|
205
|
+
| **DataRow** | default, compact | DataRow, DataRowLeft/Center/Right, DataRowLabel/Description/Value |
|
|
206
|
+
| **Separator** | horizontal, vertical | Themed divider |
|
|
207
|
+
| **Progress** | sm, default, lg | Value-based progress bar with accessibility |
|
|
208
|
+
| **Spinner** | sm, default, lg | ActivityIndicator wrapper |
|
|
209
|
+
| **Skeleton** | — | Loading placeholder |
|
|
210
|
+
| **AspectRatio** | — | Maintain aspect ratio container |
|
|
211
|
+
|
|
212
|
+
### Layout
|
|
213
|
+
|
|
214
|
+
| Component | Notes |
|
|
215
|
+
| --------------- | ---------------------------------------- |
|
|
216
|
+
| **Tabs** | TabsList, TabsTrigger, TabsContent |
|
|
217
|
+
| **Accordion** | AccordionItem, AccordionTrigger, Content |
|
|
218
|
+
| **Collapsible** | CollapsibleTrigger, CollapsibleContent |
|
|
219
|
+
|
|
220
|
+
### Overlays & Menus
|
|
221
|
+
|
|
222
|
+
| Component | Notes |
|
|
223
|
+
| ---------------- | ---------------------------------------------------- |
|
|
224
|
+
| **Dialog** | Modal dialog with Header, Footer, Title, Description |
|
|
225
|
+
| **AlertDialog** | Confirmation dialog with Action/Cancel buttons |
|
|
226
|
+
| **Popover** | Position-aware popover (requires PortalHost) |
|
|
227
|
+
| **Tooltip** | Position-aware tooltip (requires PortalHost) |
|
|
228
|
+
| **DropdownMenu** | DropdownMenuTrigger, Content, Item, Label, Separator |
|
|
229
|
+
| **ContextMenu** | Long-press context menu |
|
|
230
|
+
| **HoverCard** | Position-aware hover card (requires PortalHost) |
|
|
231
|
+
| **Menubar** | Horizontal menu bar |
|
|
232
|
+
|
|
233
|
+
### Feedback
|
|
234
|
+
|
|
235
|
+
| Component | Notes |
|
|
236
|
+
| --------- | ---------------------------------------------- |
|
|
237
|
+
| **Toast** | ToastProvider, ToastContainer, useToast() hook |
|
|
238
|
+
|
|
239
|
+
## Blocks (Pre-built Screens)
|
|
240
|
+
|
|
241
|
+
### Auth
|
|
242
|
+
|
|
243
|
+
| Block | Description |
|
|
244
|
+
| ---------------------- | ------------------------------- |
|
|
245
|
+
| **SignInForm** | Email/password sign in |
|
|
246
|
+
| **SignUpForm** | Registration form |
|
|
247
|
+
| **ForgotPasswordForm** | Password reset request |
|
|
248
|
+
| **ResetPasswordForm** | Set new password |
|
|
249
|
+
| **VerifyEmailForm** | Email verification code input |
|
|
250
|
+
| **SocialConnections** | OAuth provider buttons |
|
|
251
|
+
| **UserMenu** | Profile dropdown for auth flows |
|
|
252
|
+
|
|
253
|
+
### Navigation
|
|
254
|
+
|
|
255
|
+
| Block | Description |
|
|
256
|
+
| --------------------- | ------------------------------------------------- |
|
|
257
|
+
| **AppShell** | Layout shell with slots for navigation |
|
|
258
|
+
| **NavigationSidebar** | Sidebar nav with grouped sections and footer slot |
|
|
259
|
+
| **Header** | Top app bar with back/actions |
|
|
260
|
+
| **BottomNav** | Tab-style bottom navigation bar |
|
|
261
|
+
| **FloatingBottomNav** | Elevated bottom nav with safe area support |
|
|
262
|
+
| **TabBar** | Top tab bar with indicator |
|
|
263
|
+
| **DrawerMenu** | Drawer with sections and items |
|
|
264
|
+
|
|
265
|
+
### Lists
|
|
266
|
+
|
|
267
|
+
| Block | Description |
|
|
268
|
+
| -------------------- | ------------------------------------ |
|
|
269
|
+
| **ListItem** | Title/subtitle row |
|
|
270
|
+
| **NotificationItem** | Notification row with metadata |
|
|
271
|
+
| **SwipeableItem** | Swipe actions (left/right) list item |
|
|
272
|
+
|
|
273
|
+
### Commerce
|
|
274
|
+
|
|
275
|
+
| Block | Description |
|
|
276
|
+
| ------------------- | ------------------------------- |
|
|
277
|
+
| **ProductCard** | Product card with badge/media |
|
|
278
|
+
| **PricingCard** | Pricing tiers with feature list |
|
|
279
|
+
| **CheckoutSummary** | Cart summary with line items |
|
|
280
|
+
|
|
281
|
+
### Profile & Settings
|
|
282
|
+
|
|
283
|
+
| Block | Description |
|
|
284
|
+
| ------------------- | ----------------------------------- |
|
|
285
|
+
| **ProfileHeader** | Profile header with stats |
|
|
286
|
+
| **AccountCard** | Account summary card |
|
|
287
|
+
| **SettingsSection** | Section header with optional action |
|
|
288
|
+
| **SettingsItem** | Row for toggles/links/settings |
|
|
289
|
+
|
|
290
|
+
### Splash & States
|
|
291
|
+
|
|
292
|
+
| Block | Description |
|
|
293
|
+
| -------------------- | ------------------------------ |
|
|
294
|
+
| **SplashScreen** | Branded splash screen |
|
|
295
|
+
| **MinimalSplash** | Minimal monochrome splash |
|
|
296
|
+
| **BrandedSplash** | Splash with brand imagery |
|
|
297
|
+
| **OnboardingScreen** | Paged onboarding with slides |
|
|
298
|
+
| **LoadingScreen** | Full-screen loading state |
|
|
299
|
+
| **EmptyState** | Placeholder empty/content-less |
|
|
300
|
+
| **ErrorState** | Error message with action |
|
|
301
|
+
| **UserMenu** | Avatar dropdown with user info |
|
|
302
|
+
|
|
303
|
+
### Navigation
|
|
304
|
+
|
|
305
|
+
| Block | Description |
|
|
306
|
+
| --------------------- | --------------------------------- |
|
|
307
|
+
| **NavigationSidebar** | Sidebar nav with grouped sections |
|
|
308
|
+
| **Header** | App header with title & actions |
|
|
309
|
+
| **BottomNav** | Bottom tab navigation |
|
|
310
|
+
| **FloatingBottomNav** | Floating bottom navigation |
|
|
311
|
+
| **TabBar** | Horizontal tab bar |
|
|
312
|
+
| **DrawerMenu** | Side drawer navigation |
|
|
313
|
+
|
|
314
|
+
### Layout
|
|
315
|
+
|
|
316
|
+
| Block | Description |
|
|
317
|
+
| ---------------------- | ----------------------------------------------- |
|
|
318
|
+
| **AppShell** | Main app layout wrapper |
|
|
319
|
+
| **ActionBar** | Sticky bottom action row with safe-area padding |
|
|
320
|
+
| **ConfirmActionSheet** | Reusable destructive confirmation sheet |
|
|
321
|
+
| **EmptyState** | Empty content placeholder |
|
|
322
|
+
| **ErrorState** | Error display with retry |
|
|
323
|
+
| **LoadingScreen** | Full-screen loading indicator |
|
|
324
|
+
| **OnboardingScreen** | Onboarding flow screen |
|
|
325
|
+
|
|
326
|
+
### Splash
|
|
327
|
+
|
|
328
|
+
| Block | Description |
|
|
329
|
+
| ----------------- | ------------------------------------------------ |
|
|
330
|
+
| **SplashScreen** | Animated app launch splash with fade/scale/slide |
|
|
331
|
+
| **MinimalSplash** | Preset: Simple fade animation |
|
|
332
|
+
| **BrandedSplash** | Preset: Scale animation with loading indicator |
|
|
333
|
+
|
|
334
|
+
### Profile
|
|
335
|
+
|
|
336
|
+
| Block | Description |
|
|
337
|
+
| ------------------- | -------------------------- |
|
|
338
|
+
| **ProfileHeader** | User profile header |
|
|
339
|
+
| **AccountCard** | Account info card |
|
|
340
|
+
| **SettingsItem** | Settings list item |
|
|
341
|
+
| **SettingsSection** | Settings group with header |
|
|
342
|
+
|
|
343
|
+
### Lists
|
|
344
|
+
|
|
345
|
+
| Block | Description |
|
|
346
|
+
| -------------------- | -------------------------------- |
|
|
347
|
+
| **ListItem** | Standard list item |
|
|
348
|
+
| **NotificationItem** | Notification list item |
|
|
349
|
+
| **SwipeableItem** | Swipeable list item with actions |
|
|
350
|
+
|
|
351
|
+
### Commerce
|
|
352
|
+
|
|
353
|
+
| Block | Description |
|
|
354
|
+
| ------------------- | -------------------------- |
|
|
355
|
+
| **ProductCard** | Product display card |
|
|
356
|
+
| **PricingCard** | Pricing tier card |
|
|
357
|
+
| **CheckoutSummary** | Order summary for checkout |
|
|
358
|
+
|
|
359
|
+
## Quick Start
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
# Install dependencies
|
|
363
|
+
pnpm install
|
|
364
|
+
|
|
365
|
+
# Run the showcase app
|
|
366
|
+
cd apps/showcase
|
|
367
|
+
npx expo start
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Project Structure
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
whileui/
|
|
374
|
+
├── packages/
|
|
375
|
+
│ └── ui/
|
|
376
|
+
│ └── src/
|
|
377
|
+
│ ├── components/ # All components (copy these!)
|
|
378
|
+
│ │ ├── button/
|
|
379
|
+
│ │ ├── card/
|
|
380
|
+
│ │ ├── form-field/
|
|
381
|
+
│ │ ├── numeric-input/
|
|
382
|
+
│ │ ├── segmented-control/
|
|
383
|
+
│ │ ├── data-row/
|
|
384
|
+
│ │ ├── dialog/
|
|
385
|
+
│ │ └── ...
|
|
386
|
+
│ ├── blocks/ # Pre-built screens
|
|
387
|
+
│ │ ├── auth/
|
|
388
|
+
│ │ ├── navigation/
|
|
389
|
+
│ │ ├── layout/
|
|
390
|
+
│ │ ├── profile/
|
|
391
|
+
│ │ ├── lists/
|
|
392
|
+
│ │ └── commerce/
|
|
393
|
+
│ ├── lib/ # Utilities
|
|
394
|
+
│ │ ├── cn.ts # clsx + tailwind-merge
|
|
395
|
+
│ │ ├── tv.ts # tailwind-variants re-export
|
|
396
|
+
│ │ ├── font-context.ts
|
|
397
|
+
│ │ └── theme-bridge.ts
|
|
398
|
+
│ └── index.ts # Barrel export
|
|
399
|
+
├── apps/
|
|
400
|
+
│ └── showcase/ # Expo demo app
|
|
401
|
+
│ ├── App.tsx # Component showcase
|
|
402
|
+
│ ├── global.css # Theme variables (OKLCH) — at app root!
|
|
403
|
+
│ └── metro.config.js # Uniwind + monorepo config
|
|
404
|
+
└── package.json # pnpm monorepo root
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Theming
|
|
408
|
+
|
|
409
|
+
Themes are defined in `global.css` using CSS variables with OKLCH colors:
|
|
410
|
+
|
|
411
|
+
```css
|
|
412
|
+
@variant light {
|
|
413
|
+
--color-primary: oklch(0.6 0.2 160);
|
|
414
|
+
--color-background: oklch(1 0 0);
|
|
415
|
+
/* ... */
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@variant dark {
|
|
419
|
+
--color-primary: oklch(0.6 0.2 160);
|
|
420
|
+
--color-background: oklch(0.145 0 0);
|
|
421
|
+
/* ... */
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### Token Contract (Release Baseline)
|
|
426
|
+
|
|
427
|
+
The WhileUI token contract is strict for cross-app reuse. Define these in **every** theme variant (`@variant light`, `@variant dark`, and custom variants):
|
|
428
|
+
|
|
429
|
+
- Required core tokens: `background`, `foreground`, `card`, `card-foreground`, `primary`, `primary-foreground`, `secondary`, `secondary-foreground`, `muted`, `muted-foreground`, `accent`, `accent-foreground`, `destructive`, `destructive-foreground`, `border`, `input`, `ring`
|
|
430
|
+
- Optional status tokens: `success`, `success-foreground`, `warning`, `warning-foreground`, `info`, `info-foreground`
|
|
431
|
+
- Optional scale tokens: spacing (`--spacing`, `--spacing-*`), typography (`--text-*`, `--leading-*`, `--tracking-*`), radius (`--radius-*`), elevation (`--shadow-*`)
|
|
432
|
+
|
|
433
|
+
Minimal contract example:
|
|
434
|
+
|
|
435
|
+
```css
|
|
436
|
+
@layer theme {
|
|
437
|
+
:root {
|
|
438
|
+
@variant light {
|
|
439
|
+
--color-background: oklch(1 0 0);
|
|
440
|
+
--color-foreground: oklch(0.15 0 0);
|
|
441
|
+
--color-card: oklch(1 0 0);
|
|
442
|
+
--color-card-foreground: oklch(0.15 0 0);
|
|
443
|
+
--color-primary: oklch(0.2 0 0);
|
|
444
|
+
--color-primary-foreground: oklch(0.98 0 0);
|
|
445
|
+
--color-secondary: oklch(0.95 0 0);
|
|
446
|
+
--color-secondary-foreground: oklch(0.15 0 0);
|
|
447
|
+
--color-muted: oklch(0.95 0 0);
|
|
448
|
+
--color-muted-foreground: oklch(0.45 0 0);
|
|
449
|
+
--color-accent: oklch(0.9 0.05 180);
|
|
450
|
+
--color-accent-foreground: oklch(0.15 0 0);
|
|
451
|
+
--color-destructive: oklch(0.58 0.2 26);
|
|
452
|
+
--color-destructive-foreground: oklch(0.98 0 0);
|
|
453
|
+
--color-border: oklch(0.9 0 0);
|
|
454
|
+
--color-input: oklch(0.92 0 0);
|
|
455
|
+
--color-ring: oklch(0.22 0 0);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Switch themes at runtime via Uniwind:
|
|
462
|
+
|
|
463
|
+
```tsx
|
|
464
|
+
import { Uniwind } from 'uniwind';
|
|
465
|
+
|
|
466
|
+
Uniwind.setTheme('dark'); // or 'light' or 'system'
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Or use the first-party ThemeBridge helper with optional persistence:
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
import { useThemeBridge, type ThemeBridgeAdapter } from '@thewhileloop/whileui';
|
|
473
|
+
|
|
474
|
+
const adapter: ThemeBridgeAdapter = {
|
|
475
|
+
loadThemeMode: async () => 'system',
|
|
476
|
+
saveThemeMode: async (mode) => {
|
|
477
|
+
await storage.setItem('theme-mode', mode);
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const { mode, resolvedTheme, setMode, cycleMode } = useThemeBridge({ adapter });
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
## Using Components
|
|
485
|
+
|
|
486
|
+
Copy any component folder from `packages/ui/src/components/` into your project:
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
import { cn } from './lib/cn';
|
|
490
|
+
import { tv, type VariantProps } from './lib/tv';
|
|
491
|
+
|
|
492
|
+
// Use the component with className overrides
|
|
493
|
+
<Button className="mt-4">
|
|
494
|
+
<ButtonText>Get Started</ButtonText>
|
|
495
|
+
</Button>;
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
## Tech Stack
|
|
499
|
+
|
|
500
|
+
- **Styling**: [Uniwind](https://uniwind.dev) (free tier) + Tailwind CSS v4
|
|
501
|
+
- **Variants**: [tailwind-variants](https://www.tailwind-variants.org/) (`tv()`)
|
|
502
|
+
- **Merging**: [clsx](https://github.com/lukeed/clsx) + [tailwind-merge](https://github.com/dcastil/tailwind-merge)
|
|
503
|
+
- **Expo**: SDK 54
|
|
504
|
+
- **Monorepo**: pnpm + Turborepo
|
|
505
|
+
|
|
506
|
+
## License
|
|
507
|
+
|
|
508
|
+
MIT
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
# API Reference
|
|
513
|
+
|
|
514
|
+
## Button
|
|
515
|
+
|
|
516
|
+
```tsx
|
|
517
|
+
import { Button, ButtonText, ButtonIcon } from '@thewhileloop/whileui';
|
|
518
|
+
|
|
519
|
+
<Button variant="default" size="default" disabled={false} onPress={() => {}}>
|
|
520
|
+
<ButtonIcon>
|
|
521
|
+
<Icon />
|
|
522
|
+
</ButtonIcon>
|
|
523
|
+
<ButtonText>Click me</ButtonText>
|
|
524
|
+
<ButtonIcon position="right">
|
|
525
|
+
<Icon />
|
|
526
|
+
</ButtonIcon>
|
|
527
|
+
</Button>;
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
| Prop | Type | Default | Description |
|
|
531
|
+
| --------- | ----------------------------------------------------------------------------- | ----------- | --------------------------- |
|
|
532
|
+
| variant | `'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'` | `'default'` | Button style variant |
|
|
533
|
+
| size | `'default' \| 'sm' \| 'lg' \| 'icon'` | `'default'` | Button size |
|
|
534
|
+
| disabled | `boolean` | `false` | Disable the button |
|
|
535
|
+
| className | `string` | — | Additional Tailwind classes |
|
|
536
|
+
|
|
537
|
+
## Input
|
|
538
|
+
|
|
539
|
+
```tsx
|
|
540
|
+
import { Input } from '@thewhileloop/whileui';
|
|
541
|
+
|
|
542
|
+
<Input placeholder="Email" variant="default" value={value} onChangeText={setValue} />;
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
| Prop | Type | Default | Description |
|
|
546
|
+
| ----------- | ---------------------- | ----------- | ------------------------- |
|
|
547
|
+
| variant | `'default' \| 'error'` | `'default'` | Input style variant |
|
|
548
|
+
| placeholder | `string` | — | Placeholder text |
|
|
549
|
+
| editable | `boolean` | `true` | Whether input is editable |
|
|
550
|
+
|
|
551
|
+
## NumericInput
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
import { NumericInput } from '@thewhileloop/whileui';
|
|
555
|
+
|
|
556
|
+
<NumericInput
|
|
557
|
+
value={amount}
|
|
558
|
+
onValueChange={setAmount}
|
|
559
|
+
prefix={<Text className="text-muted-foreground">$</Text>}
|
|
560
|
+
suffix={<Text className="text-muted-foreground">USD</Text>}
|
|
561
|
+
min={0}
|
|
562
|
+
step={0.5}
|
|
563
|
+
showSteppers
|
|
564
|
+
size="compact"
|
|
565
|
+
/>;
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
| Prop | Type | Default | Description |
|
|
569
|
+
| --------------- | --------------------------------- | ----------- | --------------------------------- |
|
|
570
|
+
| variant | `'default' \| 'error'` | `'default'` | Input style |
|
|
571
|
+
| size | `'default' \| 'compact'` | `'default'` | Density size |
|
|
572
|
+
| value | `number \| null` | — | Controlled numeric value |
|
|
573
|
+
| onValueChange | `(value: number \| null) => void` | — | Numeric value change callback |
|
|
574
|
+
| prefix / suffix | `ReactNode` | — | Left/right slots |
|
|
575
|
+
| showSteppers | `boolean` | `false` | Show decrement/increment controls |
|
|
576
|
+
|
|
577
|
+
## FormField
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
import {
|
|
581
|
+
FormField,
|
|
582
|
+
FormLabel,
|
|
583
|
+
FormControl,
|
|
584
|
+
FormHint,
|
|
585
|
+
FormMessage,
|
|
586
|
+
Input,
|
|
587
|
+
} from '@thewhileloop/whileui';
|
|
588
|
+
|
|
589
|
+
<FormField required invalid={Boolean(error)}>
|
|
590
|
+
<FormLabel>Email</FormLabel>
|
|
591
|
+
<FormControl>
|
|
592
|
+
<Input placeholder="you@example.com" />
|
|
593
|
+
</FormControl>
|
|
594
|
+
{error ? <FormMessage>{error}</FormMessage> : <FormHint>We'll never share your email.</FormHint>}
|
|
595
|
+
</FormField>;
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
## LabeledField
|
|
599
|
+
|
|
600
|
+
```tsx
|
|
601
|
+
import { LabeledField, LabeledFieldControl, Input } from '@thewhileloop/whileui';
|
|
602
|
+
|
|
603
|
+
<LabeledField
|
|
604
|
+
label="Username"
|
|
605
|
+
hint="3-20 characters"
|
|
606
|
+
leftSlot={<Icon />}
|
|
607
|
+
rightSlot={
|
|
608
|
+
<Button size="sm">
|
|
609
|
+
<ButtonText>Check</ButtonText>
|
|
610
|
+
</Button>
|
|
611
|
+
}
|
|
612
|
+
>
|
|
613
|
+
<LabeledFieldControl>
|
|
614
|
+
<Input className="border-0 bg-transparent px-0" />
|
|
615
|
+
</LabeledFieldControl>
|
|
616
|
+
</LabeledField>;
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
## SegmentedControl
|
|
620
|
+
|
|
621
|
+
```tsx
|
|
622
|
+
import {
|
|
623
|
+
SegmentedControl,
|
|
624
|
+
SegmentedControlItem,
|
|
625
|
+
SegmentedControlItemText,
|
|
626
|
+
} from '@thewhileloop/whileui';
|
|
627
|
+
|
|
628
|
+
<SegmentedControl value={unit} onValueChange={setUnit} wrap>
|
|
629
|
+
<SegmentedControlItem value="metric">
|
|
630
|
+
<SegmentedControlItemText>Metric</SegmentedControlItemText>
|
|
631
|
+
</SegmentedControlItem>
|
|
632
|
+
<SegmentedControlItem value="imperial">
|
|
633
|
+
<SegmentedControlItemText>Imperial</SegmentedControlItemText>
|
|
634
|
+
</SegmentedControlItem>
|
|
635
|
+
</SegmentedControl>;
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## DataRow
|
|
639
|
+
|
|
640
|
+
```tsx
|
|
641
|
+
import {
|
|
642
|
+
DataRow,
|
|
643
|
+
DataRowLeft,
|
|
644
|
+
DataRowCenter,
|
|
645
|
+
DataRowRight,
|
|
646
|
+
DataRowLabel,
|
|
647
|
+
DataRowDescription,
|
|
648
|
+
DataRowValue,
|
|
649
|
+
Avatar,
|
|
650
|
+
AvatarFallback,
|
|
651
|
+
} from '@thewhileloop/whileui';
|
|
652
|
+
|
|
653
|
+
<DataRow>
|
|
654
|
+
<DataRowLeft>
|
|
655
|
+
<Avatar size="sm">
|
|
656
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
657
|
+
</Avatar>
|
|
658
|
+
</DataRowLeft>
|
|
659
|
+
<DataRowCenter>
|
|
660
|
+
<DataRowLabel>Jane Doe</DataRowLabel>
|
|
661
|
+
<DataRowDescription>Product Designer</DataRowDescription>
|
|
662
|
+
</DataRowCenter>
|
|
663
|
+
<DataRowRight>
|
|
664
|
+
<DataRowValue>Owner</DataRowValue>
|
|
665
|
+
</DataRowRight>
|
|
666
|
+
</DataRow>;
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
## Card
|
|
670
|
+
|
|
671
|
+
```tsx
|
|
672
|
+
import {
|
|
673
|
+
Card,
|
|
674
|
+
CardHeader,
|
|
675
|
+
CardTitle,
|
|
676
|
+
CardDescription,
|
|
677
|
+
CardContent,
|
|
678
|
+
CardFooter,
|
|
679
|
+
} from '@thewhileloop/whileui';
|
|
680
|
+
|
|
681
|
+
<Card padding="default">
|
|
682
|
+
<CardHeader>
|
|
683
|
+
<CardTitle>Title</CardTitle>
|
|
684
|
+
<CardDescription>Description</CardDescription>
|
|
685
|
+
</CardHeader>
|
|
686
|
+
<CardContent>{/* Content */}</CardContent>
|
|
687
|
+
<CardFooter>{/* Footer */}</CardFooter>
|
|
688
|
+
</Card>;
|
|
689
|
+
|
|
690
|
+
<Card unstyled padding="none" className="rounded-xl border border-border bg-card">
|
|
691
|
+
{/* advanced custom layouts */}
|
|
692
|
+
</Card>;
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
| Prop | Type | Default | Description |
|
|
696
|
+
| -------- | ------------------------------------- | ----------- | ----------------------------------- |
|
|
697
|
+
| padding | `'none' \| 'sm' \| 'default' \| 'lg'` | `'default'` | Card interior padding |
|
|
698
|
+
| unstyled | `boolean` | `false` | Remove built-in card surface styles |
|
|
699
|
+
|
|
700
|
+
## Badge
|
|
701
|
+
|
|
702
|
+
```tsx
|
|
703
|
+
import { Badge, BadgeText } from '@thewhileloop/whileui';
|
|
704
|
+
|
|
705
|
+
<Badge variant="default">
|
|
706
|
+
<BadgeText>New</BadgeText>
|
|
707
|
+
</Badge>;
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
| Prop | Type | Default |
|
|
711
|
+
| ------- | -------------------------------------------------------------------------------------------- | ----------- |
|
|
712
|
+
| variant | `'default' \| 'secondary' \| 'destructive' \| 'outline' \| 'success' \| 'warning' \| 'info'` | `'default'` |
|
|
713
|
+
|
|
714
|
+
## Alert
|
|
715
|
+
|
|
716
|
+
```tsx
|
|
717
|
+
import { Alert, AlertTitle, AlertDescription } from '@thewhileloop/whileui';
|
|
718
|
+
|
|
719
|
+
<Alert variant="default">
|
|
720
|
+
<AlertTitle>Heads up!</AlertTitle>
|
|
721
|
+
<AlertDescription>Something happened.</AlertDescription>
|
|
722
|
+
</Alert>;
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
| Prop | Type | Default |
|
|
726
|
+
| ------- | ------------------------------------------------------ | ----------- |
|
|
727
|
+
| variant | `'default' \| 'destructive' \| 'success' \| 'warning'` | `'default'` |
|
|
728
|
+
|
|
729
|
+
## Dialog
|
|
730
|
+
|
|
731
|
+
```tsx
|
|
732
|
+
import {
|
|
733
|
+
Dialog,
|
|
734
|
+
DialogTrigger,
|
|
735
|
+
DialogContent,
|
|
736
|
+
DialogHeader,
|
|
737
|
+
DialogTitle,
|
|
738
|
+
DialogDescription,
|
|
739
|
+
DialogFooter,
|
|
740
|
+
DialogClose,
|
|
741
|
+
} from '@thewhileloop/whileui';
|
|
742
|
+
|
|
743
|
+
<Dialog>
|
|
744
|
+
<DialogTrigger asChild>
|
|
745
|
+
<Button>
|
|
746
|
+
<ButtonText>Open</ButtonText>
|
|
747
|
+
</Button>
|
|
748
|
+
</DialogTrigger>
|
|
749
|
+
<DialogContent>
|
|
750
|
+
<DialogHeader>
|
|
751
|
+
<DialogTitle>Title</DialogTitle>
|
|
752
|
+
<DialogDescription>Description</DialogDescription>
|
|
753
|
+
</DialogHeader>
|
|
754
|
+
{/* Content */}
|
|
755
|
+
<DialogFooter>
|
|
756
|
+
<DialogClose asChild>
|
|
757
|
+
<Button>
|
|
758
|
+
<ButtonText>Close</ButtonText>
|
|
759
|
+
</Button>
|
|
760
|
+
</DialogClose>
|
|
761
|
+
</DialogFooter>
|
|
762
|
+
</DialogContent>
|
|
763
|
+
</Dialog>;
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
## AlertDialog
|
|
767
|
+
|
|
768
|
+
```tsx
|
|
769
|
+
import {
|
|
770
|
+
AlertDialog,
|
|
771
|
+
AlertDialogTrigger,
|
|
772
|
+
AlertDialogContent,
|
|
773
|
+
AlertDialogHeader,
|
|
774
|
+
AlertDialogTitle,
|
|
775
|
+
AlertDialogDescription,
|
|
776
|
+
AlertDialogFooter,
|
|
777
|
+
AlertDialogAction,
|
|
778
|
+
AlertDialogCancel,
|
|
779
|
+
} from '@thewhileloop/whileui';
|
|
780
|
+
|
|
781
|
+
<AlertDialog>
|
|
782
|
+
<AlertDialogTrigger asChild>
|
|
783
|
+
<Button variant="destructive">
|
|
784
|
+
<ButtonText>Delete</ButtonText>
|
|
785
|
+
</Button>
|
|
786
|
+
</AlertDialogTrigger>
|
|
787
|
+
<AlertDialogContent>
|
|
788
|
+
<AlertDialogHeader>
|
|
789
|
+
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
790
|
+
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
|
791
|
+
</AlertDialogHeader>
|
|
792
|
+
<AlertDialogFooter>
|
|
793
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
794
|
+
<AlertDialogAction>Delete</AlertDialogAction>
|
|
795
|
+
</AlertDialogFooter>
|
|
796
|
+
</AlertDialogContent>
|
|
797
|
+
</AlertDialog>;
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
## Checkbox
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
import { Checkbox } from '@thewhileloop/whileui';
|
|
804
|
+
|
|
805
|
+
<Checkbox checked={checked} onCheckedChange={setChecked} />;
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
| Prop | Type | Default |
|
|
809
|
+
| --------------- | ---------------------------- | ------- |
|
|
810
|
+
| checked | `boolean` | `false` |
|
|
811
|
+
| onCheckedChange | `(checked: boolean) => void` | — |
|
|
812
|
+
| disabled | `boolean` | `false` |
|
|
813
|
+
|
|
814
|
+
## Switch
|
|
815
|
+
|
|
816
|
+
```tsx
|
|
817
|
+
import { Switch } from '@thewhileloop/whileui';
|
|
818
|
+
|
|
819
|
+
<Switch checked={checked} onCheckedChange={setChecked} />;
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
| Prop | Type | Default |
|
|
823
|
+
| --------------- | ---------------------------- | ------- |
|
|
824
|
+
| checked | `boolean` | `false` |
|
|
825
|
+
| onCheckedChange | `(checked: boolean) => void` | — |
|
|
826
|
+
|
|
827
|
+
## RadioGroup
|
|
828
|
+
|
|
829
|
+
```tsx
|
|
830
|
+
import { RadioGroup, RadioGroupItem } from '@thewhileloop/whileui';
|
|
831
|
+
|
|
832
|
+
<RadioGroup value={value} onValueChange={setValue}>
|
|
833
|
+
<RadioGroupItem value="option1" />
|
|
834
|
+
<RadioGroupItem value="option2" />
|
|
835
|
+
</RadioGroup>;
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
## Select
|
|
839
|
+
|
|
840
|
+
```tsx
|
|
841
|
+
import {
|
|
842
|
+
Select,
|
|
843
|
+
SelectTrigger,
|
|
844
|
+
SelectValue,
|
|
845
|
+
SelectContent,
|
|
846
|
+
SelectItem,
|
|
847
|
+
} from '@thewhileloop/whileui';
|
|
848
|
+
|
|
849
|
+
<Select value={value} onValueChange={setValue}>
|
|
850
|
+
<SelectTrigger>
|
|
851
|
+
<SelectValue placeholder="Select..." />
|
|
852
|
+
</SelectTrigger>
|
|
853
|
+
<SelectContent>
|
|
854
|
+
<SelectItem label="Option 1" value="1" />
|
|
855
|
+
<SelectItem label="Option 2" value="2" />
|
|
856
|
+
</SelectContent>
|
|
857
|
+
</Select>;
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
## Tabs
|
|
861
|
+
|
|
862
|
+
```tsx
|
|
863
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@thewhileloop/whileui';
|
|
864
|
+
|
|
865
|
+
<Tabs defaultValue="tab1">
|
|
866
|
+
<TabsList>
|
|
867
|
+
<TabsTrigger value="tab1">
|
|
868
|
+
<Text>Tab 1</Text>
|
|
869
|
+
</TabsTrigger>
|
|
870
|
+
<TabsTrigger value="tab2">
|
|
871
|
+
<Text>Tab 2</Text>
|
|
872
|
+
</TabsTrigger>
|
|
873
|
+
</TabsList>
|
|
874
|
+
<TabsContent value="tab1">{/* Content 1 */}</TabsContent>
|
|
875
|
+
<TabsContent value="tab2">{/* Content 2 */}</TabsContent>
|
|
876
|
+
</Tabs>;
|
|
877
|
+
```
|
|
878
|
+
|
|
879
|
+
## Accordion
|
|
880
|
+
|
|
881
|
+
```tsx
|
|
882
|
+
import {
|
|
883
|
+
Accordion,
|
|
884
|
+
AccordionItem,
|
|
885
|
+
AccordionTrigger,
|
|
886
|
+
AccordionContent,
|
|
887
|
+
} from '@thewhileloop/whileui';
|
|
888
|
+
|
|
889
|
+
<Accordion type="single" collapsible>
|
|
890
|
+
<AccordionItem value="item1">
|
|
891
|
+
<AccordionTrigger>
|
|
892
|
+
<Text>Section 1</Text>
|
|
893
|
+
</AccordionTrigger>
|
|
894
|
+
<AccordionContent>
|
|
895
|
+
<Text>Content 1</Text>
|
|
896
|
+
</AccordionContent>
|
|
897
|
+
</AccordionItem>
|
|
898
|
+
</Accordion>;
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
| Prop | Type | Default |
|
|
902
|
+
| ----------- | ------------------------ | ---------- |
|
|
903
|
+
| type | `'single' \| 'multiple'` | `'single'` |
|
|
904
|
+
| collapsible | `boolean` | `false` |
|
|
905
|
+
|
|
906
|
+
## Avatar
|
|
907
|
+
|
|
908
|
+
```tsx
|
|
909
|
+
import { Avatar, AvatarImage, AvatarFallback } from '@thewhileloop/whileui';
|
|
910
|
+
|
|
911
|
+
<Avatar size="default">
|
|
912
|
+
<AvatarImage src="https://..." />
|
|
913
|
+
<AvatarFallback>JD</AvatarFallback>
|
|
914
|
+
</Avatar>;
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
| Prop | Type | Default |
|
|
918
|
+
| ---- | --------------------------- | ----------- |
|
|
919
|
+
| size | `'sm' \| 'default' \| 'lg'` | `'default'` |
|
|
920
|
+
|
|
921
|
+
## Progress
|
|
922
|
+
|
|
923
|
+
```tsx
|
|
924
|
+
import { Progress } from '@thewhileloop/whileui';
|
|
925
|
+
|
|
926
|
+
<Progress value={50} size="default" />;
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
| Prop | Type | Default |
|
|
930
|
+
| ----- | --------------------------- | ----------- |
|
|
931
|
+
| value | `number` | `0` |
|
|
932
|
+
| size | `'sm' \| 'default' \| 'lg'` | `'default'` |
|
|
933
|
+
|
|
934
|
+
## Toast
|
|
935
|
+
|
|
936
|
+
```tsx
|
|
937
|
+
import { ToastProvider, ToastContainer, useToast } from '@thewhileloop/whileui';
|
|
938
|
+
|
|
939
|
+
// Wrap app
|
|
940
|
+
<ToastProvider>
|
|
941
|
+
<App />
|
|
942
|
+
<ToastContainer position="top" />
|
|
943
|
+
</ToastProvider>;
|
|
944
|
+
|
|
945
|
+
// Use in component
|
|
946
|
+
const { toast } = useToast();
|
|
947
|
+
toast({ title: 'Success', description: 'Saved!', variant: 'success' });
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
| Toast Options | Type |
|
|
951
|
+
| ------------- | ----------------------------------------- |
|
|
952
|
+
| title | `string` |
|
|
953
|
+
| description | `string` |
|
|
954
|
+
| variant | `'default' \| 'success' \| 'destructive'` |
|
|
955
|
+
| duration | `number` (ms) |
|
|
956
|
+
|
|
957
|
+
## Popover
|
|
958
|
+
|
|
959
|
+
```tsx
|
|
960
|
+
import { Popover, PopoverTrigger, PopoverContent } from '@thewhileloop/whileui';
|
|
961
|
+
|
|
962
|
+
<Popover>
|
|
963
|
+
<PopoverTrigger asChild>
|
|
964
|
+
<Button>
|
|
965
|
+
<ButtonText>Open</ButtonText>
|
|
966
|
+
</Button>
|
|
967
|
+
</PopoverTrigger>
|
|
968
|
+
<PopoverContent>
|
|
969
|
+
<Text>Popover content</Text>
|
|
970
|
+
</PopoverContent>
|
|
971
|
+
</Popover>;
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
## DropdownMenu
|
|
975
|
+
|
|
976
|
+
```tsx
|
|
977
|
+
import {
|
|
978
|
+
DropdownMenu,
|
|
979
|
+
DropdownMenuTrigger,
|
|
980
|
+
DropdownMenuContent,
|
|
981
|
+
DropdownMenuItem,
|
|
982
|
+
DropdownMenuLabel,
|
|
983
|
+
DropdownMenuSeparator,
|
|
984
|
+
} from '@thewhileloop/whileui';
|
|
985
|
+
|
|
986
|
+
<DropdownMenu>
|
|
987
|
+
<DropdownMenuTrigger asChild>
|
|
988
|
+
<Button>
|
|
989
|
+
<ButtonText>Menu</ButtonText>
|
|
990
|
+
</Button>
|
|
991
|
+
</DropdownMenuTrigger>
|
|
992
|
+
<DropdownMenuContent>
|
|
993
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
994
|
+
<DropdownMenuSeparator />
|
|
995
|
+
<DropdownMenuItem>
|
|
996
|
+
<Text>Edit</Text>
|
|
997
|
+
</DropdownMenuItem>
|
|
998
|
+
<DropdownMenuItem>
|
|
999
|
+
<Text>Delete</Text>
|
|
1000
|
+
</DropdownMenuItem>
|
|
1001
|
+
</DropdownMenuContent>
|
|
1002
|
+
</DropdownMenu>;
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
---
|
|
1006
|
+
|
|
1007
|
+
# Blocks API
|
|
1008
|
+
|
|
1009
|
+
## SignInForm
|
|
1010
|
+
|
|
1011
|
+
```tsx
|
|
1012
|
+
import { SignInForm } from '@thewhileloop/whileui';
|
|
1013
|
+
|
|
1014
|
+
<SignInForm onSubmit={(email, password) => {}} onForgotPassword={() => {}} onSignUp={() => {}} />;
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
## SignUpForm
|
|
1018
|
+
|
|
1019
|
+
```tsx
|
|
1020
|
+
import { SignUpForm } from '@thewhileloop/whileui';
|
|
1021
|
+
|
|
1022
|
+
<SignUpForm onSubmit={(name, email, password) => {}} onSignIn={() => {}} />;
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
## BottomNav
|
|
1026
|
+
|
|
1027
|
+
```tsx
|
|
1028
|
+
import { BottomNav } from '@thewhileloop/whileui';
|
|
1029
|
+
|
|
1030
|
+
<BottomNav
|
|
1031
|
+
items={[
|
|
1032
|
+
{ key: 'home', label: 'Home', icon: <Icon /> },
|
|
1033
|
+
{ key: 'profile', label: 'Profile', icon: <Icon />, badge: 3 },
|
|
1034
|
+
]}
|
|
1035
|
+
activeKey="home"
|
|
1036
|
+
onSelect={(key) => {}}
|
|
1037
|
+
/>;
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
## ActionBar
|
|
1041
|
+
|
|
1042
|
+
```tsx
|
|
1043
|
+
import { ActionBar, Button, ButtonText } from '@thewhileloop/whileui';
|
|
1044
|
+
|
|
1045
|
+
<ActionBar>
|
|
1046
|
+
<Button variant="outline" className="flex-1">
|
|
1047
|
+
<ButtonText>Cancel</ButtonText>
|
|
1048
|
+
</Button>
|
|
1049
|
+
<Button className="flex-1">
|
|
1050
|
+
<ButtonText>Save</ButtonText>
|
|
1051
|
+
</Button>
|
|
1052
|
+
</ActionBar>;
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
## ConfirmActionSheet
|
|
1056
|
+
|
|
1057
|
+
```tsx
|
|
1058
|
+
import { ConfirmActionSheet } from '@thewhileloop/whileui';
|
|
1059
|
+
|
|
1060
|
+
<ConfirmActionSheet
|
|
1061
|
+
open={open}
|
|
1062
|
+
onOpenChange={setOpen}
|
|
1063
|
+
title="Delete project?"
|
|
1064
|
+
description="This action cannot be undone."
|
|
1065
|
+
confirmLabel="Delete"
|
|
1066
|
+
destructive
|
|
1067
|
+
onConfirm={() => deleteProject()}
|
|
1068
|
+
/>;
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
## NavigationSidebar
|
|
1072
|
+
|
|
1073
|
+
```tsx
|
|
1074
|
+
import { NavigationSidebar } from '@thewhileloop/whileui';
|
|
1075
|
+
|
|
1076
|
+
<NavigationSidebar
|
|
1077
|
+
sections={[
|
|
1078
|
+
{
|
|
1079
|
+
title: 'Workspace',
|
|
1080
|
+
items: [
|
|
1081
|
+
{ key: 'overview', label: 'Overview', icon: <Icon /> },
|
|
1082
|
+
{ key: 'billing', label: 'Billing', icon: <Icon />, badge: 2 },
|
|
1083
|
+
],
|
|
1084
|
+
},
|
|
1085
|
+
]}
|
|
1086
|
+
activeKey="overview"
|
|
1087
|
+
onSelect={(key) => {}}
|
|
1088
|
+
header={<Text>Acme Inc.</Text>}
|
|
1089
|
+
footer={<Text className="text-xs text-muted-foreground">v1.0.0</Text>}
|
|
1090
|
+
/>;
|
|
1091
|
+
```
|
|
1092
|
+
|
|
1093
|
+
## Header
|
|
1094
|
+
|
|
1095
|
+
```tsx
|
|
1096
|
+
import { Header, HeaderBackButton } from '@thewhileloop/whileui';
|
|
1097
|
+
|
|
1098
|
+
<Header
|
|
1099
|
+
title="Settings"
|
|
1100
|
+
subtitle="Manage preferences"
|
|
1101
|
+
leftAction={<HeaderBackButton onPress={() => {}} />}
|
|
1102
|
+
rightActions={[{ key: 'search', icon: <Icon />, onPress: () => {} }]}
|
|
1103
|
+
/>;
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
## SplashScreen
|
|
1107
|
+
|
|
1108
|
+
```tsx
|
|
1109
|
+
import { SplashScreen } from '@thewhileloop/whileui';
|
|
1110
|
+
|
|
1111
|
+
<SplashScreen
|
|
1112
|
+
logo={<MyLogo />}
|
|
1113
|
+
appName="MyApp"
|
|
1114
|
+
tagline="Your tagline"
|
|
1115
|
+
variant="scale"
|
|
1116
|
+
duration={1500}
|
|
1117
|
+
showLoading
|
|
1118
|
+
onAnimationComplete={() => {}}
|
|
1119
|
+
/>;
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
| Prop | Type | Default |
|
|
1123
|
+
| ----------- | ------------------------------ | --------- |
|
|
1124
|
+
| variant | `'fade' \| 'scale' \| 'slide'` | `'scale'` |
|
|
1125
|
+
| duration | `number` | `800` |
|
|
1126
|
+
| showLoading | `boolean` | `false` |
|
|
1127
|
+
|
|
1128
|
+
## EmptyState
|
|
1129
|
+
|
|
1130
|
+
```tsx
|
|
1131
|
+
import { EmptyState } from '@thewhileloop/whileui';
|
|
1132
|
+
|
|
1133
|
+
<EmptyState
|
|
1134
|
+
icon={<Icon />}
|
|
1135
|
+
title="No items"
|
|
1136
|
+
description="Add your first item."
|
|
1137
|
+
action={{ label: 'Add Item', onPress: () => {} }}
|
|
1138
|
+
/>;
|
|
1139
|
+
```
|
|
1140
|
+
|
|
1141
|
+
## ProfileHeader
|
|
1142
|
+
|
|
1143
|
+
```tsx
|
|
1144
|
+
import { ProfileHeader } from '@thewhileloop/whileui';
|
|
1145
|
+
|
|
1146
|
+
<ProfileHeader
|
|
1147
|
+
name="John Doe"
|
|
1148
|
+
username="johndoe"
|
|
1149
|
+
bio="Designer & Developer"
|
|
1150
|
+
avatarFallback="JD"
|
|
1151
|
+
avatarUrl="https://..."
|
|
1152
|
+
verified
|
|
1153
|
+
stats={[
|
|
1154
|
+
{ label: 'Followers', value: '1.2K' },
|
|
1155
|
+
{ label: 'Following', value: 234 },
|
|
1156
|
+
]}
|
|
1157
|
+
action={{ label: 'Edit Profile', onPress: () => {} }}
|
|
1158
|
+
/>;
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
## SettingsSection / SettingsItem
|
|
1162
|
+
|
|
1163
|
+
```tsx
|
|
1164
|
+
import { SettingsSection, SettingsItem } from '@thewhileloop/whileui';
|
|
1165
|
+
|
|
1166
|
+
<SettingsSection title="Preferences">
|
|
1167
|
+
<SettingsItem
|
|
1168
|
+
icon={<Icon />}
|
|
1169
|
+
label="Notifications"
|
|
1170
|
+
type="toggle"
|
|
1171
|
+
toggleValue={enabled}
|
|
1172
|
+
onToggle={setEnabled}
|
|
1173
|
+
/>
|
|
1174
|
+
<SettingsItem icon={<Icon />} label="Privacy" value="Public" onPress={() => {}} />
|
|
1175
|
+
<SettingsItem icon={<Icon />} label="Sign Out" type="action" destructive />
|
|
1176
|
+
</SettingsSection>;
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
## ProductCard
|
|
1180
|
+
|
|
1181
|
+
```tsx
|
|
1182
|
+
import { ProductCard } from '@thewhileloop/whileui';
|
|
1183
|
+
|
|
1184
|
+
<ProductCard
|
|
1185
|
+
title="Product Name"
|
|
1186
|
+
price="$99"
|
|
1187
|
+
originalPrice="$129"
|
|
1188
|
+
badge="-23%"
|
|
1189
|
+
rating={4.5}
|
|
1190
|
+
reviewCount={128}
|
|
1191
|
+
variant="vertical"
|
|
1192
|
+
onPress={() => {}}
|
|
1193
|
+
/>;
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
| Prop | Type | Default |
|
|
1197
|
+
| ------- | ---------------------------- | ------------ |
|
|
1198
|
+
| variant | `'vertical' \| 'horizontal'` | `'vertical'` |
|
|
1199
|
+
|
|
1200
|
+
## PricingCard
|
|
1201
|
+
|
|
1202
|
+
```tsx
|
|
1203
|
+
import { PricingCard } from '@thewhileloop/whileui';
|
|
1204
|
+
|
|
1205
|
+
<PricingCard
|
|
1206
|
+
name="Pro"
|
|
1207
|
+
description="For teams"
|
|
1208
|
+
price="$29"
|
|
1209
|
+
period="/month"
|
|
1210
|
+
badge="Popular"
|
|
1211
|
+
highlighted
|
|
1212
|
+
features={[
|
|
1213
|
+
{ label: 'Unlimited users', included: true },
|
|
1214
|
+
{ label: 'Priority support', included: true },
|
|
1215
|
+
{ label: 'Custom domain', included: false },
|
|
1216
|
+
]}
|
|
1217
|
+
onPress={() => {}}
|
|
1218
|
+
/>;
|
|
1219
|
+
```
|
|
1220
|
+
|
|
1221
|
+
## DrawerMenu
|
|
1222
|
+
|
|
1223
|
+
```tsx
|
|
1224
|
+
import { DrawerMenu } from '@thewhileloop/whileui';
|
|
1225
|
+
|
|
1226
|
+
<DrawerMenu
|
|
1227
|
+
visible={open}
|
|
1228
|
+
onClose={() => setOpen(false)}
|
|
1229
|
+
sections={[
|
|
1230
|
+
{
|
|
1231
|
+
title: 'Menu',
|
|
1232
|
+
items: [
|
|
1233
|
+
{ key: 'home', label: 'Home', icon: <Icon /> },
|
|
1234
|
+
{ key: 'settings', label: 'Settings', icon: <Icon /> },
|
|
1235
|
+
],
|
|
1236
|
+
},
|
|
1237
|
+
]}
|
|
1238
|
+
activeKey="home"
|
|
1239
|
+
onSelect={(key) => {}}
|
|
1240
|
+
header={<View>...</View>}
|
|
1241
|
+
footer={<Text>v1.0</Text>}
|
|
1242
|
+
/>;
|
|
1243
|
+
```
|