@frosted-ui/react-native 0.0.1-canary.93 → 0.0.1-canary.94
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 +18 -5
- package/dist/components/avatar.d.ts.map +1 -1
- package/dist/components/avatar.js +1 -0
- package/dist/components/avatar.js.map +1 -1
- package/dist/components/badge.d.ts.map +1 -1
- package/dist/components/badge.js +2 -0
- package/dist/components/badge.js.map +1 -1
- package/dist/components/button.js +1 -1
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +2 -1
- package/dist/components/card.js.map +1 -1
- package/dist/components/heading.d.ts +2 -2
- package/dist/components/heading.d.ts.map +1 -1
- package/dist/components/icon-button.js +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/link.d.ts +19 -0
- package/dist/components/link.d.ts.map +1 -0
- package/dist/components/link.js +68 -0
- package/dist/components/link.js.map +1 -0
- package/dist/components/list.d.ts +37 -0
- package/dist/components/list.d.ts.map +1 -0
- package/dist/components/list.js +112 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/select.js +1 -1
- package/dist/components/separator.d.ts.map +1 -1
- package/dist/components/separator.js +2 -3
- package/dist/components/separator.js.map +1 -1
- package/dist/components/text-area.d.ts.map +1 -1
- package/dist/components/text-area.js +1 -1
- package/dist/components/text-area.js.map +1 -1
- package/dist/components/text-field.d.ts.map +1 -1
- package/dist/components/text-field.js +41 -3
- package/dist/components/text-field.js.map +1 -1
- package/dist/components/text.d.ts +2 -2
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +11 -2
- package/dist/components/text.js.map +1 -1
- package/docs/llm/COLOR_SYSTEM.md +799 -0
- package/docs/llm/COMPONENTS.md +1183 -0
- package/docs/llm/DESIGN_PATTERNS.md +2466 -0
- package/docs/llm/README.md +117 -0
- package/docs/llm/TYPOGRAPHY.md +516 -0
- package/package.json +6 -3
|
@@ -0,0 +1,2466 @@
|
|
|
1
|
+
# Frosted UI Design Patterns Guide
|
|
2
|
+
|
|
3
|
+
> **For Design Engineer AI Agents**: This guide covers UX patterns, layout composition, and visual design principles for building polished, user-friendly apps with Frosted UI.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Core Design Principles
|
|
8
|
+
|
|
9
|
+
### 1. Consistency Over Creativity
|
|
10
|
+
|
|
11
|
+
Use Frosted UI's built-in variants, sizes, and colors. Don't override component styles unless absolutely necessary. The design system exists to ensure visual consistency.
|
|
12
|
+
|
|
13
|
+
### 2. Hierarchy Through Size & Weight
|
|
14
|
+
|
|
15
|
+
Establish clear visual hierarchy using:
|
|
16
|
+
|
|
17
|
+
- **Typography scale** (`size="1"` to `size="9"`) for text importance
|
|
18
|
+
- **Font weight** (`weight="bold"` for headings, `weight="medium"` for labels, `weight="regular"` for body)
|
|
19
|
+
- **Component sizes** — larger sizes draw more attention
|
|
20
|
+
|
|
21
|
+
### 3. Color With Purpose
|
|
22
|
+
|
|
23
|
+
- Use **accent color** for interactive elements and primary actions
|
|
24
|
+
- Use **gray** for secondary/supporting UI
|
|
25
|
+
- Use **semantic colors** (`danger`, `warning`, `success`, `info`) only for status and feedback
|
|
26
|
+
- Don't use color decoratively — every color should communicate something
|
|
27
|
+
|
|
28
|
+
### 4. Colored Sections
|
|
29
|
+
|
|
30
|
+
When creating a themed section (e.g., promotional banner, newsletter signup), use the palette's alpha shades for a cohesive look:
|
|
31
|
+
|
|
32
|
+
| Element | Token | Example |
|
|
33
|
+
| -------------- | ---------------------------- | ----------------------- |
|
|
34
|
+
| Background | `palette.a2` | Card/section background |
|
|
35
|
+
| Border | `palette.a5` | Subtle themed border |
|
|
36
|
+
| Text (body) | `palette.a11` | Body text, descriptions |
|
|
37
|
+
| Text (heading) | `palette.a12` | High-contrast headings |
|
|
38
|
+
| Form inputs | `variant="soft" color="..."` | Match the section color |
|
|
39
|
+
|
|
40
|
+
### 5. Whitespace Is Your Friend
|
|
41
|
+
|
|
42
|
+
Generous spacing improves readability and touch targets. Use consistent gaps:
|
|
43
|
+
|
|
44
|
+
- `4px` — tight (related items)
|
|
45
|
+
- `8px` — standard (within groups)
|
|
46
|
+
- `12-16px` — comfortable (between groups)
|
|
47
|
+
- `24-32px` — sections
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Spacing Scale
|
|
52
|
+
|
|
53
|
+
Use these values for `gap`, `padding`, and `margin`:
|
|
54
|
+
|
|
55
|
+
| Value | Use Case |
|
|
56
|
+
| ----- | -------------------------------------------------- |
|
|
57
|
+
| `4` | Tight spacing: icon + text, badge content |
|
|
58
|
+
| `8` | Standard spacing: form fields, list items internal |
|
|
59
|
+
| `12` | Comfortable: between related groups |
|
|
60
|
+
| `16` | Section padding, card padding |
|
|
61
|
+
| `24` | Between distinct sections |
|
|
62
|
+
| `32` | Major section breaks, screen padding |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Responsive Design
|
|
67
|
+
|
|
68
|
+
Apps built with Frosted UI should be mobile-first but work well on web/desktop. Choose the right layout strategy based on app complexity.
|
|
69
|
+
|
|
70
|
+
### When to Use Each Layout Strategy
|
|
71
|
+
|
|
72
|
+
| App Type | Layout Strategy | Example |
|
|
73
|
+
| ----------------------------------- | ----------------- | ------------------------------- |
|
|
74
|
+
| Landing pages, forms, settings | **Centered** | Max-width container, centered |
|
|
75
|
+
| E-commerce, marketplace, dashboards | **Adaptive Grid** | Single column → multi-column |
|
|
76
|
+
| Chat, feed, detail views | **Centered** | Content-focused, easy reading |
|
|
77
|
+
| File browsers, admin panels | **Adaptive Grid** | Utilize full screen real estate |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### Strategy 1: Centered Layout (Simple Apps)
|
|
82
|
+
|
|
83
|
+
Best for: landing pages, forms, articles, settings, detail views.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { useWindowDimensions } from 'react-native';
|
|
87
|
+
|
|
88
|
+
const MAX_CONTENT_WIDTH = 600;
|
|
89
|
+
const BREAKPOINT = 768;
|
|
90
|
+
|
|
91
|
+
function useResponsiveLayout() {
|
|
92
|
+
const { width } = useWindowDimensions();
|
|
93
|
+
const isWide = width >= BREAKPOINT;
|
|
94
|
+
const horizontalPadding = isWide ? Math.max(24, (width - MAX_CONTENT_WIDTH) / 2) : 16;
|
|
95
|
+
|
|
96
|
+
return { isWide, horizontalPadding, screenWidth: width };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function SimpleScreen() {
|
|
100
|
+
const { colors } = useThemeTokens();
|
|
101
|
+
const { horizontalPadding, isWide } = useResponsiveLayout();
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ScrollView
|
|
105
|
+
style={{ flex: 1, backgroundColor: colors.background }}
|
|
106
|
+
contentContainerStyle={{
|
|
107
|
+
paddingHorizontal: horizontalPadding,
|
|
108
|
+
paddingVertical: 16,
|
|
109
|
+
gap: 24,
|
|
110
|
+
maxWidth: isWide ? MAX_CONTENT_WIDTH + horizontalPadding * 2 : undefined,
|
|
111
|
+
alignSelf: isWide ? 'center' : undefined,
|
|
112
|
+
width: '100%',
|
|
113
|
+
}}>
|
|
114
|
+
{/* Content */}
|
|
115
|
+
</ScrollView>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### Strategy 2: Adaptive Grid (Complex Apps)
|
|
123
|
+
|
|
124
|
+
Best for: e-commerce, marketplaces, dashboards, productivity apps, file browsers.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { useWindowDimensions } from 'react-native';
|
|
128
|
+
|
|
129
|
+
// Breakpoints
|
|
130
|
+
const TABLET = 768;
|
|
131
|
+
const DESKTOP = 1024;
|
|
132
|
+
const WIDE = 1280;
|
|
133
|
+
|
|
134
|
+
function useAdaptiveLayout() {
|
|
135
|
+
const { width } = useWindowDimensions();
|
|
136
|
+
|
|
137
|
+
// Calculate columns based on screen width
|
|
138
|
+
const getColumns = (minItemWidth: number, maxColumns: number = 4) => {
|
|
139
|
+
const availableWidth = width - 32; // Account for padding
|
|
140
|
+
const columns = Math.floor(availableWidth / minItemWidth);
|
|
141
|
+
return Math.max(1, Math.min(columns, maxColumns));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
screenWidth: width,
|
|
146
|
+
isTablet: width >= TABLET,
|
|
147
|
+
isDesktop: width >= DESKTOP,
|
|
148
|
+
isWide: width >= WIDE,
|
|
149
|
+
getColumns,
|
|
150
|
+
padding: width >= TABLET ? 24 : 16,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### Product Grid Example
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
function ProductGridScreen() {
|
|
159
|
+
const { colors } = useThemeTokens();
|
|
160
|
+
const { getColumns, padding, isDesktop } = useAdaptiveLayout();
|
|
161
|
+
|
|
162
|
+
// Min 200px per item for comfortable cards, max 3 columns
|
|
163
|
+
const columns = getColumns(200, 3);
|
|
164
|
+
const gap = 16;
|
|
165
|
+
|
|
166
|
+
const products = [...]; // Your product data
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<ScrollView
|
|
170
|
+
style={{ flex: 1, backgroundColor: colors.background }}
|
|
171
|
+
contentContainerStyle={{
|
|
172
|
+
padding,
|
|
173
|
+
gap: 24,
|
|
174
|
+
maxWidth: isDesktop ? 1200 : undefined,
|
|
175
|
+
alignSelf: isDesktop ? 'center' : undefined,
|
|
176
|
+
width: '100%',
|
|
177
|
+
}}>
|
|
178
|
+
{/* Header */}
|
|
179
|
+
<View style={{ gap: 4 }}>
|
|
180
|
+
<Heading size="5">Products</Heading>
|
|
181
|
+
<Text color="gray">{products.length} items</Text>
|
|
182
|
+
</View>
|
|
183
|
+
|
|
184
|
+
{/* Responsive Grid */}
|
|
185
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap }}>
|
|
186
|
+
{products.map((product) => (
|
|
187
|
+
<View
|
|
188
|
+
key={product.id}
|
|
189
|
+
style={{
|
|
190
|
+
flexGrow: 1,
|
|
191
|
+
flexBasis: columns === 1 ? '100%' : `${Math.floor(100 / columns) - 2}%`,
|
|
192
|
+
maxWidth: columns === 1 ? '100%' : `${Math.floor(100 / columns) - 1}%`,
|
|
193
|
+
}}>
|
|
194
|
+
<ProductCard product={product} />
|
|
195
|
+
</View>
|
|
196
|
+
))}
|
|
197
|
+
</View>
|
|
198
|
+
</ScrollView>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
#### Simpler Grid with Fixed Breakpoints
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
function SimpleGrid({ items, renderItem }) {
|
|
207
|
+
const { width } = useWindowDimensions();
|
|
208
|
+
|
|
209
|
+
// Simple breakpoint-based columns
|
|
210
|
+
const columns = width >= 1024 ? 3 : width >= 600 ? 2 : 1;
|
|
211
|
+
const gap = 16;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap }}>
|
|
215
|
+
{items.map((item, index) => (
|
|
216
|
+
<View
|
|
217
|
+
key={item.id ?? index}
|
|
218
|
+
style={{
|
|
219
|
+
flexGrow: 1,
|
|
220
|
+
flexBasis: columns === 1 ? '100%' : `${Math.floor(100 / columns) - 2}%`,
|
|
221
|
+
maxWidth: columns === 1 ? '100%' : `${Math.floor(100 / columns) - 1}%`,
|
|
222
|
+
}}>
|
|
223
|
+
{renderItem(item)}
|
|
224
|
+
</View>
|
|
225
|
+
))}
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### Strategy 3: Hybrid Layout (Mixed Content)
|
|
234
|
+
|
|
235
|
+
Best for: screens with both full-width and constrained sections.
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
function HybridScreen() {
|
|
239
|
+
const { colors } = useThemeTokens();
|
|
240
|
+
const { isDesktop, padding, getColumns } = useAdaptiveLayout();
|
|
241
|
+
|
|
242
|
+
const maxContentWidth = 800;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<ScrollView style={{ flex: 1, backgroundColor: colors.background }}>
|
|
246
|
+
{/* Full-width hero/banner */}
|
|
247
|
+
<View style={{ padding, backgroundColor: colors.palettes.accent.a2 }}>
|
|
248
|
+
<View
|
|
249
|
+
style={{
|
|
250
|
+
maxWidth: maxContentWidth,
|
|
251
|
+
alignSelf: 'center',
|
|
252
|
+
width: '100%',
|
|
253
|
+
}}>
|
|
254
|
+
<Heading size="6">Welcome Back</Heading>
|
|
255
|
+
<Text color="gray">Your dashboard overview</Text>
|
|
256
|
+
</View>
|
|
257
|
+
</View>
|
|
258
|
+
|
|
259
|
+
{/* Constrained content area */}
|
|
260
|
+
<View
|
|
261
|
+
style={{
|
|
262
|
+
padding,
|
|
263
|
+
gap: 24,
|
|
264
|
+
maxWidth: isDesktop ? 1200 : undefined,
|
|
265
|
+
alignSelf: 'center',
|
|
266
|
+
width: '100%',
|
|
267
|
+
}}>
|
|
268
|
+
{/* Stats Grid - adapts columns */}
|
|
269
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }}>
|
|
270
|
+
{stats.map((stat) => (
|
|
271
|
+
<View key={stat.label} style={{ flex: 1, minWidth: 150 }}>
|
|
272
|
+
<StatCard {...stat} />
|
|
273
|
+
</View>
|
|
274
|
+
))}
|
|
275
|
+
</View>
|
|
276
|
+
|
|
277
|
+
{/* Two-column layout on desktop */}
|
|
278
|
+
<View
|
|
279
|
+
style={{
|
|
280
|
+
flexDirection: isDesktop ? 'row' : 'column',
|
|
281
|
+
gap: 16,
|
|
282
|
+
}}>
|
|
283
|
+
<View style={{ flex: isDesktop ? 2 : 1 }}>
|
|
284
|
+
<Card>
|
|
285
|
+
<Heading size="4">Recent Activity</Heading>
|
|
286
|
+
{/* Activity list */}
|
|
287
|
+
</Card>
|
|
288
|
+
</View>
|
|
289
|
+
<View style={{ flex: 1 }}>
|
|
290
|
+
<Card>
|
|
291
|
+
<Heading size="4">Quick Actions</Heading>
|
|
292
|
+
{/* Actions */}
|
|
293
|
+
</Card>
|
|
294
|
+
</View>
|
|
295
|
+
</View>
|
|
296
|
+
</View>
|
|
297
|
+
</ScrollView>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### Responsive Design Principles
|
|
305
|
+
|
|
306
|
+
| Principle | Mobile | Tablet (768px+) | Desktop (1024px+) |
|
|
307
|
+
| --------------------- | --------------- | --------------- | ----------------- |
|
|
308
|
+
| **Grid columns** | 1-2 | 2-3 | 3-4 |
|
|
309
|
+
| **Content max-width** | Full | Full or 800px | 1200px |
|
|
310
|
+
| **Side padding** | 16px | 24px | 24-48px |
|
|
311
|
+
| **Card arrangement** | Stacked | Side-by-side | Multi-column |
|
|
312
|
+
| **Touch targets** | 44px+ (size 3+) | Same | Same |
|
|
313
|
+
| **Component sizes** | Don't change | Don't change | Don't change |
|
|
314
|
+
|
|
315
|
+
### Do's and Don'ts
|
|
316
|
+
|
|
317
|
+
**Do:**
|
|
318
|
+
|
|
319
|
+
- Use `flexWrap: 'wrap'` for responsive grids
|
|
320
|
+
- Set `minWidth` on grid items to control breakpoints
|
|
321
|
+
- Use `flex: 1` for equal-width columns
|
|
322
|
+
- Constrain max-width on very wide screens (1200-1400px)
|
|
323
|
+
- Keep consistent gap/padding at each breakpoint
|
|
324
|
+
|
|
325
|
+
**Don't:**
|
|
326
|
+
|
|
327
|
+
- Change button sizes based on screen width
|
|
328
|
+
- Use different font sizes for mobile vs desktop
|
|
329
|
+
- Create completely different layouts (keep hierarchy similar)
|
|
330
|
+
- Forget touch targets — desktop users may have touchscreens
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Layout Patterns
|
|
335
|
+
|
|
336
|
+
### Screen Structure
|
|
337
|
+
|
|
338
|
+
```tsx
|
|
339
|
+
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
|
340
|
+
{/* Header */}
|
|
341
|
+
<View style={{ paddingHorizontal: 16, paddingVertical: 12 }}>
|
|
342
|
+
<Heading size="6">Screen Title</Heading>
|
|
343
|
+
</View>
|
|
344
|
+
|
|
345
|
+
{/* Content */}
|
|
346
|
+
<ScrollView contentContainerStyle={{ padding: 16, gap: 24 }}>{/* Sections go here */}</ScrollView>
|
|
347
|
+
|
|
348
|
+
{/* Footer (optional - for primary actions) */}
|
|
349
|
+
<View style={{ padding: 16, borderTopWidth: 1, borderTopColor: colors.stroke }}>
|
|
350
|
+
<Button variant="solid">
|
|
351
|
+
<Text>Primary Action</Text>
|
|
352
|
+
</Button>
|
|
353
|
+
</View>
|
|
354
|
+
</View>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### Section with Header
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
<View style={{ gap: 12 }}>
|
|
361
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
362
|
+
<Heading size="4">Section Title</Heading>
|
|
363
|
+
<Button variant="ghost" size="1">
|
|
364
|
+
<Text>See All</Text>
|
|
365
|
+
</Button>
|
|
366
|
+
</View>
|
|
367
|
+
<Card>{/* Section content */}</Card>
|
|
368
|
+
</View>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Title + Description Pattern
|
|
372
|
+
|
|
373
|
+
Use this pattern for headings with supporting text:
|
|
374
|
+
|
|
375
|
+
```tsx
|
|
376
|
+
<View style={{ gap: 4 }}>
|
|
377
|
+
<Heading size="5">Welcome back</Heading>
|
|
378
|
+
<Text size="3" color="gray">
|
|
379
|
+
Here's what's happening today
|
|
380
|
+
</Text>
|
|
381
|
+
</View>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Form Field Pattern
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
<View style={{ gap: 6 }}>
|
|
388
|
+
<Label nativeID="field-id">Field Label</Label>
|
|
389
|
+
<TextField.Input placeholder="Placeholder..." aria-labelledby="field-id" />
|
|
390
|
+
<Text size="1" color="gray">
|
|
391
|
+
Helper text explaining the field
|
|
392
|
+
</Text>
|
|
393
|
+
</View>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Form Section Pattern
|
|
397
|
+
|
|
398
|
+
Group related fields together:
|
|
399
|
+
|
|
400
|
+
```tsx
|
|
401
|
+
<View style={{ gap: 16 }}>
|
|
402
|
+
<Heading size="3">Personal Information</Heading>
|
|
403
|
+
|
|
404
|
+
<View style={{ gap: 12 }}>
|
|
405
|
+
{/* First Name */}
|
|
406
|
+
<View style={{ gap: 6 }}>
|
|
407
|
+
<Label nativeID="first-name">First Name</Label>
|
|
408
|
+
<TextField.Input placeholder="John" aria-labelledby="first-name" />
|
|
409
|
+
</View>
|
|
410
|
+
|
|
411
|
+
{/* Last Name */}
|
|
412
|
+
<View style={{ gap: 6 }}>
|
|
413
|
+
<Label nativeID="last-name">Last Name</Label>
|
|
414
|
+
<TextField.Input placeholder="Doe" aria-labelledby="last-name" />
|
|
415
|
+
</View>
|
|
416
|
+
|
|
417
|
+
{/* Email */}
|
|
418
|
+
<View style={{ gap: 6 }}>
|
|
419
|
+
<Label nativeID="email">Email</Label>
|
|
420
|
+
<TextField.Input
|
|
421
|
+
placeholder="john@example.com"
|
|
422
|
+
aria-labelledby="email"
|
|
423
|
+
keyboardType="email-address"
|
|
424
|
+
/>
|
|
425
|
+
</View>
|
|
426
|
+
</View>
|
|
427
|
+
</View>
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## List Patterns
|
|
433
|
+
|
|
434
|
+
> **Use the `List` component** for structured lists with items, slots, and separators. It renders a `Card` internally with proper padding and handles press states automatically.
|
|
435
|
+
|
|
436
|
+
### Basic List
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
<List.Root>
|
|
440
|
+
{users.map((user, index) => (
|
|
441
|
+
<React.Fragment key={user.id}>
|
|
442
|
+
{index > 0 && <List.Separator />}
|
|
443
|
+
<List.Item onPress={() => {}}>
|
|
444
|
+
<List.ItemSlot>
|
|
445
|
+
<Avatar fallback={user.name} size="3" />
|
|
446
|
+
</List.ItemSlot>
|
|
447
|
+
<List.ItemContent>
|
|
448
|
+
<List.ItemTitle>{user.name}</List.ItemTitle>
|
|
449
|
+
<List.ItemDescription>{user.email}</List.ItemDescription>
|
|
450
|
+
</List.ItemContent>
|
|
451
|
+
<List.ItemSlot>
|
|
452
|
+
<Badge color={user.status === 'Active' ? 'success' : 'warning'} size="1">
|
|
453
|
+
<Text>{user.status}</Text>
|
|
454
|
+
</Badge>
|
|
455
|
+
</List.ItemSlot>
|
|
456
|
+
<List.ItemSlot>
|
|
457
|
+
<Icon as={ChevronRight} size={16} color={colors.palettes.gray.a8} />
|
|
458
|
+
</List.ItemSlot>
|
|
459
|
+
</List.Item>
|
|
460
|
+
</React.Fragment>
|
|
461
|
+
))}
|
|
462
|
+
</List.Root>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Settings List
|
|
466
|
+
|
|
467
|
+
```tsx
|
|
468
|
+
<List.Root>
|
|
469
|
+
{/* Switch setting */}
|
|
470
|
+
<List.Item>
|
|
471
|
+
<List.ItemSlot>
|
|
472
|
+
<View style={iconBoxStyle}>
|
|
473
|
+
<Icon as={Bell} size={20} color={colors.palettes.blue.a11} />
|
|
474
|
+
</View>
|
|
475
|
+
</List.ItemSlot>
|
|
476
|
+
<List.ItemContent>
|
|
477
|
+
<List.ItemTitle>Notifications</List.ItemTitle>
|
|
478
|
+
</List.ItemContent>
|
|
479
|
+
<List.ItemSlot>
|
|
480
|
+
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
481
|
+
</List.ItemSlot>
|
|
482
|
+
</List.Item>
|
|
483
|
+
|
|
484
|
+
<List.Separator />
|
|
485
|
+
|
|
486
|
+
{/* Pressable setting with checkbox */}
|
|
487
|
+
<List.Item onPress={() => setDarkMode(!darkMode)}>
|
|
488
|
+
<List.ItemSlot>
|
|
489
|
+
<View style={iconBoxStyle}>
|
|
490
|
+
<Icon as={Settings} size={20} color={colors.palettes.purple.a11} />
|
|
491
|
+
</View>
|
|
492
|
+
</List.ItemSlot>
|
|
493
|
+
<List.ItemContent>
|
|
494
|
+
<List.ItemTitle>Dark Mode</List.ItemTitle>
|
|
495
|
+
</List.ItemContent>
|
|
496
|
+
<List.ItemSlot>
|
|
497
|
+
<Checkbox checked={darkMode} onCheckedChange={setDarkMode} />
|
|
498
|
+
</List.ItemSlot>
|
|
499
|
+
</List.Item>
|
|
500
|
+
</List.Root>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### List with RadioGroup (Shipping Options)
|
|
504
|
+
|
|
505
|
+
```tsx
|
|
506
|
+
<RadioGroup.Root value={selected} onValueChange={setSelected}>
|
|
507
|
+
<List.Root>
|
|
508
|
+
{options.map((option, index) => (
|
|
509
|
+
<React.Fragment key={option.id}>
|
|
510
|
+
{index > 0 && <List.Separator />}
|
|
511
|
+
<List.Item onPress={() => setSelected(option.id)}>
|
|
512
|
+
<List.ItemSlot>
|
|
513
|
+
<RadioGroup.Item value={option.id} />
|
|
514
|
+
</List.ItemSlot>
|
|
515
|
+
<List.ItemSlot>
|
|
516
|
+
<View style={iconBoxStyle}>
|
|
517
|
+
<Icon as={option.icon} size={20} />
|
|
518
|
+
</View>
|
|
519
|
+
</List.ItemSlot>
|
|
520
|
+
<List.ItemContent>
|
|
521
|
+
<List.ItemTitle>{option.name}</List.ItemTitle>
|
|
522
|
+
<List.ItemDescription>{option.time}</List.ItemDescription>
|
|
523
|
+
</List.ItemContent>
|
|
524
|
+
<List.ItemSlot>
|
|
525
|
+
<Text weight="medium" color={option.price === 'Free' ? 'success' : undefined}>
|
|
526
|
+
{option.price}
|
|
527
|
+
</Text>
|
|
528
|
+
</List.ItemSlot>
|
|
529
|
+
</List.Item>
|
|
530
|
+
</React.Fragment>
|
|
531
|
+
))}
|
|
532
|
+
</List.Root>
|
|
533
|
+
</RadioGroup.Root>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### List Variants
|
|
537
|
+
|
|
538
|
+
Use the `variant` prop on `List.Root` to match different contexts:
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
// Default surface style (bordered, elevated)
|
|
542
|
+
<List.Root variant="surface">...</List.Root>
|
|
543
|
+
|
|
544
|
+
// Soft background for highlighted lists
|
|
545
|
+
<List.Root variant="soft">...</List.Root>
|
|
546
|
+
|
|
547
|
+
// Ghost for minimal style
|
|
548
|
+
<List.Root variant="ghost">...</List.Root>
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Leaderboard
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
<List.Root variant="soft">
|
|
555
|
+
{entries.map((entry, index) => (
|
|
556
|
+
<React.Fragment key={entry.rank}>
|
|
557
|
+
{index > 0 && <List.Separator />}
|
|
558
|
+
<List.Item style={entry.isUser ? { backgroundColor: colors.palettes.gray.a3 } : undefined}>
|
|
559
|
+
<List.ItemSlot>
|
|
560
|
+
<Text size="2" weight="bold" style={{ width: 24, textAlign: 'center' }}>
|
|
561
|
+
{entry.rank}
|
|
562
|
+
</Text>
|
|
563
|
+
</List.ItemSlot>
|
|
564
|
+
<List.ItemSlot>
|
|
565
|
+
<Avatar fallback={entry.avatar} size="2" color={entry.color} />
|
|
566
|
+
</List.ItemSlot>
|
|
567
|
+
<List.ItemContent>
|
|
568
|
+
<Text weight={entry.isUser ? 'bold' : 'medium'}>{entry.name}</Text>
|
|
569
|
+
</List.ItemContent>
|
|
570
|
+
<List.ItemSlot>
|
|
571
|
+
<Text weight="medium" color={entry.color}>
|
|
572
|
+
{entry.points.toLocaleString()} pts
|
|
573
|
+
</Text>
|
|
574
|
+
</List.ItemSlot>
|
|
575
|
+
</List.Item>
|
|
576
|
+
</React.Fragment>
|
|
577
|
+
))}
|
|
578
|
+
</List.Root>
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## Card Patterns
|
|
584
|
+
|
|
585
|
+
### Card Variants
|
|
586
|
+
|
|
587
|
+
Cards come in three variants. Choose based on visual weight needed:
|
|
588
|
+
|
|
589
|
+
| Variant | Visual Style | When to Use |
|
|
590
|
+
| --------- | --------------------------------------- | -------------------------------------------------------- |
|
|
591
|
+
| `surface` | Solid background, border, subtle shadow | **Default** — Elevated content like messages, profiles |
|
|
592
|
+
| `soft` | Translucent tinted background | Highlighted sections, tips, promotions, feature callouts |
|
|
593
|
+
| `ghost` | No background or border (just padding) | Section grouping, layout containers, minimal UI |
|
|
594
|
+
|
|
595
|
+
#### Surface — Message Card (default)
|
|
596
|
+
|
|
597
|
+
```tsx
|
|
598
|
+
<Card variant="surface">
|
|
599
|
+
<View style={{ flexDirection: 'row', gap: 12 }}>
|
|
600
|
+
<Avatar fallback="SJ" color="blue" size="3" />
|
|
601
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
602
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
603
|
+
<Text weight="medium">Sarah Johnson</Text>
|
|
604
|
+
<Text size="1" color="gray">
|
|
605
|
+
2m ago
|
|
606
|
+
</Text>
|
|
607
|
+
</View>
|
|
608
|
+
<Text size="3" color="gray">
|
|
609
|
+
Hey! Just finished the design review. The new dashboard looks amazing! 🎉
|
|
610
|
+
</Text>
|
|
611
|
+
</View>
|
|
612
|
+
</View>
|
|
613
|
+
</Card>
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
#### Soft — Pro Tip / Feature Highlight
|
|
617
|
+
|
|
618
|
+
```tsx
|
|
619
|
+
<Card variant="soft">
|
|
620
|
+
<View style={{ flexDirection: 'row', gap: 12, alignItems: 'flex-start' }}>
|
|
621
|
+
<View
|
|
622
|
+
style={{
|
|
623
|
+
width: 40,
|
|
624
|
+
height: 40,
|
|
625
|
+
borderRadius: 10,
|
|
626
|
+
backgroundColor: colors.palettes.amber.a3,
|
|
627
|
+
alignItems: 'center',
|
|
628
|
+
justifyContent: 'center',
|
|
629
|
+
}}>
|
|
630
|
+
<Icon as={Lightbulb} size={20} color={colors.palettes.amber.a11} />
|
|
631
|
+
</View>
|
|
632
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
633
|
+
<Text weight="medium">Pro Tip</Text>
|
|
634
|
+
<Text size="3" color="gray">
|
|
635
|
+
Enable notifications to stay updated on new messages and activity from your team.
|
|
636
|
+
</Text>
|
|
637
|
+
<Button variant="ghost" size="2" style={{ alignSelf: 'flex-start', marginTop: 4 }}>
|
|
638
|
+
<Text>Enable Notifications</Text>
|
|
639
|
+
<Icon as={ChevronRight} size={16} />
|
|
640
|
+
</Button>
|
|
641
|
+
</View>
|
|
642
|
+
</View>
|
|
643
|
+
</Card>
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
#### Ghost — Section Container
|
|
647
|
+
|
|
648
|
+
Use `ghost` when you need layout grouping but no visual container:
|
|
649
|
+
|
|
650
|
+
```tsx
|
|
651
|
+
<Card variant="ghost" style={{ padding: 0 }}>
|
|
652
|
+
<View style={{ gap: 12 }}>
|
|
653
|
+
{/* Section header */}
|
|
654
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
655
|
+
<Heading size="4">Recent Activity</Heading>
|
|
656
|
+
<Button variant="ghost" size="2">
|
|
657
|
+
<Text>See All</Text>
|
|
658
|
+
</Button>
|
|
659
|
+
</View>
|
|
660
|
+
|
|
661
|
+
{/* Content in a surface card (has overflow: hidden for separators) */}
|
|
662
|
+
<Card variant="surface" style={{ padding: 0 }}>
|
|
663
|
+
{items.map((item, index, arr) => (
|
|
664
|
+
<View key={index}>
|
|
665
|
+
<Pressable
|
|
666
|
+
style={({ pressed }) => ({
|
|
667
|
+
flexDirection: 'row',
|
|
668
|
+
alignItems: 'center',
|
|
669
|
+
gap: 12,
|
|
670
|
+
paddingHorizontal: 16,
|
|
671
|
+
paddingVertical: 14,
|
|
672
|
+
backgroundColor: pressed ? colors.palettes.gray.a3 : 'transparent',
|
|
673
|
+
})}>
|
|
674
|
+
<Avatar fallback={item.initials} size="3" color={item.color} />
|
|
675
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
676
|
+
<Text size="2" weight="medium" numberOfLines={1}>
|
|
677
|
+
{item.name}
|
|
678
|
+
</Text>
|
|
679
|
+
<Text size="2" color="gray" numberOfLines={1}>
|
|
680
|
+
{item.action}
|
|
681
|
+
</Text>
|
|
682
|
+
</View>
|
|
683
|
+
<Text size="1" color="gray">
|
|
684
|
+
{item.time}
|
|
685
|
+
</Text>
|
|
686
|
+
</Pressable>
|
|
687
|
+
{index < arr.length - 1 && <Separator size="4" />}
|
|
688
|
+
</View>
|
|
689
|
+
))}
|
|
690
|
+
</Card>
|
|
691
|
+
</View>
|
|
692
|
+
</Card>
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
> **Tip**: Card has `overflow: 'hidden'` by default, so full-width separators won't overflow the rounded corners.
|
|
696
|
+
|
|
697
|
+
### Info Card
|
|
698
|
+
|
|
699
|
+
```tsx
|
|
700
|
+
<Card>
|
|
701
|
+
<View style={{ gap: 12 }}>
|
|
702
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
703
|
+
<Icon as={Info} size={16} />
|
|
704
|
+
<Text weight="medium">Card Title</Text>
|
|
705
|
+
</View>
|
|
706
|
+
<Text size="3" color="gray">
|
|
707
|
+
Supporting description text that provides more context.
|
|
708
|
+
</Text>
|
|
709
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
710
|
+
<Button variant="soft" color="gray" size="2">
|
|
711
|
+
<Text>Dismiss</Text>
|
|
712
|
+
</Button>
|
|
713
|
+
<Button variant="solid" size="2">
|
|
714
|
+
<Text>Action</Text>
|
|
715
|
+
</Button>
|
|
716
|
+
</View>
|
|
717
|
+
</View>
|
|
718
|
+
</Card>
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Stat Card
|
|
722
|
+
|
|
723
|
+
```tsx
|
|
724
|
+
<Card>
|
|
725
|
+
<View style={{ gap: 4 }}>
|
|
726
|
+
<Text size="1" color="gray">
|
|
727
|
+
Total Revenue
|
|
728
|
+
</Text>
|
|
729
|
+
<Heading size="6">$12,345</Heading>
|
|
730
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
731
|
+
<Badge color="success" size="1">
|
|
732
|
+
<Text>+12%</Text>
|
|
733
|
+
</Badge>
|
|
734
|
+
<Text size="1" color="gray">
|
|
735
|
+
vs last month
|
|
736
|
+
</Text>
|
|
737
|
+
</View>
|
|
738
|
+
</View>
|
|
739
|
+
</Card>
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### Buy Box (E-commerce)
|
|
743
|
+
|
|
744
|
+
Use `size="4"` buttons for prominent CTAs in e-commerce and conversion-focused screens:
|
|
745
|
+
|
|
746
|
+
```tsx
|
|
747
|
+
<Card>
|
|
748
|
+
<View style={{ gap: 16 }}>
|
|
749
|
+
{/* Product Image */}
|
|
750
|
+
<View
|
|
751
|
+
style={{
|
|
752
|
+
height: 200,
|
|
753
|
+
backgroundColor: colors.palettes.gray.a3,
|
|
754
|
+
borderRadius: 8,
|
|
755
|
+
alignItems: 'center',
|
|
756
|
+
justifyContent: 'center',
|
|
757
|
+
}}>
|
|
758
|
+
<Text color="gray">Product Image</Text>
|
|
759
|
+
</View>
|
|
760
|
+
|
|
761
|
+
{/* Product Info */}
|
|
762
|
+
<View style={{ gap: 8 }}>
|
|
763
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
764
|
+
<Badge color="success" size="1">
|
|
765
|
+
<Text>In Stock</Text>
|
|
766
|
+
</Badge>
|
|
767
|
+
<Badge variant="soft" color="gray" size="1">
|
|
768
|
+
<Text>Free Shipping</Text>
|
|
769
|
+
</Badge>
|
|
770
|
+
</View>
|
|
771
|
+
<Heading size="5">Premium Wireless Headphones</Heading>
|
|
772
|
+
<Text size="3" color="gray">
|
|
773
|
+
High-fidelity audio with active noise cancellation and 30-hour battery life.
|
|
774
|
+
</Text>
|
|
775
|
+
</View>
|
|
776
|
+
|
|
777
|
+
{/* Price */}
|
|
778
|
+
<View style={{ gap: 4 }}>
|
|
779
|
+
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 8 }}>
|
|
780
|
+
<Heading size="6">$299</Heading>
|
|
781
|
+
<Text size="2" color="gray" style={{ textDecorationLine: 'line-through' }}>
|
|
782
|
+
$349
|
|
783
|
+
</Text>
|
|
784
|
+
</View>
|
|
785
|
+
<Text size="1" color="success">
|
|
786
|
+
Save $50 (14% off)
|
|
787
|
+
</Text>
|
|
788
|
+
</View>
|
|
789
|
+
|
|
790
|
+
<Separator size="4" />
|
|
791
|
+
|
|
792
|
+
{/* CTA Buttons - Use size="4" for prominent actions */}
|
|
793
|
+
<View style={{ gap: 12 }}>
|
|
794
|
+
<Button variant="solid" size="4">
|
|
795
|
+
<Text>Add to Cart</Text>
|
|
796
|
+
</Button>
|
|
797
|
+
<Button variant="soft" color="gray" size="4">
|
|
798
|
+
<Icon as={Heart} size={18} />
|
|
799
|
+
<Text>Add to Wishlist</Text>
|
|
800
|
+
</Button>
|
|
801
|
+
</View>
|
|
802
|
+
</View>
|
|
803
|
+
</Card>
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
> **Tip**: Use `size="4"` buttons for important conversion actions like "Add to Cart", "Buy Now", "Subscribe", or "Sign Up". The larger touch target and visual weight helps drive conversions.
|
|
807
|
+
|
|
808
|
+
### Promotional Banner (Apple-like)
|
|
809
|
+
|
|
810
|
+
Create clean, premium promotional sections with structured layout:
|
|
811
|
+
|
|
812
|
+
```tsx
|
|
813
|
+
<Card
|
|
814
|
+
style={{
|
|
815
|
+
padding: 0,
|
|
816
|
+
backgroundColor: colors.palettes.pink.a2,
|
|
817
|
+
borderWidth: 1,
|
|
818
|
+
borderColor: colors.palettes.pink.a4,
|
|
819
|
+
}}>
|
|
820
|
+
<View style={{ padding: 20, gap: 16 }}>
|
|
821
|
+
{/* Header row with badge + timer */}
|
|
822
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
823
|
+
<Badge color="pink" variant="soft" size="1">
|
|
824
|
+
<Icon as={Zap} size={10} />
|
|
825
|
+
<Text>Limited Time</Text>
|
|
826
|
+
</Badge>
|
|
827
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
828
|
+
<View
|
|
829
|
+
style={{
|
|
830
|
+
width: 6,
|
|
831
|
+
height: 6,
|
|
832
|
+
borderRadius: 3,
|
|
833
|
+
backgroundColor: colors.palettes.pink['9'],
|
|
834
|
+
}}
|
|
835
|
+
/>
|
|
836
|
+
<Text size="1" weight="medium" style={{ color: colors.palettes.pink.a11 }}>
|
|
837
|
+
Ends in 02:34:56
|
|
838
|
+
</Text>
|
|
839
|
+
</View>
|
|
840
|
+
</View>
|
|
841
|
+
|
|
842
|
+
{/* Main content */}
|
|
843
|
+
<View style={{ gap: 4 }}>
|
|
844
|
+
<Text size="5" weight="bold" style={{ color: colors.palettes.pink.a12 }}>
|
|
845
|
+
Flash Sale
|
|
846
|
+
</Text>
|
|
847
|
+
<Text size="3" style={{ color: colors.palettes.pink.a11 }}>
|
|
848
|
+
Up to 50% off on selected items. Don't miss out.
|
|
849
|
+
</Text>
|
|
850
|
+
</View>
|
|
851
|
+
|
|
852
|
+
{/* CTA */}
|
|
853
|
+
<Button variant="solid" color="pink" size="3">
|
|
854
|
+
<Text>Shop Now</Text>
|
|
855
|
+
<Icon as={ChevronRight} size={16} />
|
|
856
|
+
</Button>
|
|
857
|
+
</View>
|
|
858
|
+
</Card>
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
> **Key Pattern**: Use a consistent color palette (e.g., pink) across background (`a2`), border (`a4`), text (`a11`, `a12`), and button for a cohesive, premium feel.
|
|
862
|
+
|
|
863
|
+
### Achievement Card (Apple-like)
|
|
864
|
+
|
|
865
|
+
For gamification elements like achievements, badges, or milestones:
|
|
866
|
+
|
|
867
|
+
```tsx
|
|
868
|
+
<Card style={{ padding: 0, borderWidth: 1, borderColor: colors.stroke }}>
|
|
869
|
+
{/* Header section with award */}
|
|
870
|
+
<View
|
|
871
|
+
style={{
|
|
872
|
+
paddingVertical: 24,
|
|
873
|
+
paddingHorizontal: 20,
|
|
874
|
+
alignItems: 'center',
|
|
875
|
+
gap: 16,
|
|
876
|
+
backgroundColor: colors.palettes.gray.a2,
|
|
877
|
+
borderBottomWidth: 1,
|
|
878
|
+
borderBottomColor: colors.stroke,
|
|
879
|
+
}}>
|
|
880
|
+
{/* Centered badge with glow effect */}
|
|
881
|
+
<Badge
|
|
882
|
+
size="2"
|
|
883
|
+
color="amber"
|
|
884
|
+
variant="soft"
|
|
885
|
+
style={{
|
|
886
|
+
alignSelf: 'center',
|
|
887
|
+
shadowColor: colors.palettes.amber['9'],
|
|
888
|
+
shadowOffset: { width: 0, height: 0 },
|
|
889
|
+
shadowOpacity: 0.3,
|
|
890
|
+
shadowRadius: 12,
|
|
891
|
+
elevation: 4,
|
|
892
|
+
}}>
|
|
893
|
+
<Icon as={Award} size={14} />
|
|
894
|
+
<Text>First Purchase</Text>
|
|
895
|
+
</Badge>
|
|
896
|
+
|
|
897
|
+
<View style={{ gap: 4, alignItems: 'center' }}>
|
|
898
|
+
<Text size="4" weight="bold">
|
|
899
|
+
Achievement Unlocked!
|
|
900
|
+
</Text>
|
|
901
|
+
<Text size="2" color="gray" style={{ textAlign: 'center' }}>
|
|
902
|
+
You've made your first purchase and earned 100 bonus points.
|
|
903
|
+
</Text>
|
|
904
|
+
</View>
|
|
905
|
+
</View>
|
|
906
|
+
|
|
907
|
+
{/* Stats row */}
|
|
908
|
+
<View style={{ flexDirection: 'row', paddingVertical: 16, paddingHorizontal: 20 }}>
|
|
909
|
+
<View style={{ flex: 1, alignItems: 'center', gap: 2 }}>
|
|
910
|
+
<Text size="4" weight="bold">
|
|
911
|
+
100
|
|
912
|
+
</Text>
|
|
913
|
+
<Text size="1" color="gray">
|
|
914
|
+
Points Earned
|
|
915
|
+
</Text>
|
|
916
|
+
</View>
|
|
917
|
+
<View style={{ width: 1, backgroundColor: colors.stroke }} />
|
|
918
|
+
<View style={{ flex: 1, alignItems: 'center', gap: 2 }}>
|
|
919
|
+
<Text size="4" weight="bold">
|
|
920
|
+
3
|
|
921
|
+
</Text>
|
|
922
|
+
<Text size="1" color="gray">
|
|
923
|
+
Achievements
|
|
924
|
+
</Text>
|
|
925
|
+
</View>
|
|
926
|
+
<View style={{ width: 1, backgroundColor: colors.stroke }} />
|
|
927
|
+
<View style={{ flex: 1, alignItems: 'center', gap: 2 }}>
|
|
928
|
+
<Text size="4" weight="bold">
|
|
929
|
+
Gold
|
|
930
|
+
</Text>
|
|
931
|
+
<Text size="1" color="gray">
|
|
932
|
+
Next Tier
|
|
933
|
+
</Text>
|
|
934
|
+
</View>
|
|
935
|
+
</View>
|
|
936
|
+
</Card>
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
> **Key Pattern**: Use `colors.stroke` for subtle borders between sections. Center important elements using `alignSelf: 'center'`. Add shadow to badges for a "glow" effect.
|
|
940
|
+
|
|
941
|
+
### Newsletter Signup (Themed)
|
|
942
|
+
|
|
943
|
+
Use the palette's alpha shades to create cohesive themed sections:
|
|
944
|
+
|
|
945
|
+
```tsx
|
|
946
|
+
<Card
|
|
947
|
+
style={{
|
|
948
|
+
backgroundColor: colors.palettes.accent.a2,
|
|
949
|
+
borderWidth: 1,
|
|
950
|
+
borderColor: colors.palettes.accent.a5,
|
|
951
|
+
}}>
|
|
952
|
+
<View style={{ gap: 16 }}>
|
|
953
|
+
<View style={{ gap: 4 }}>
|
|
954
|
+
<Text size="4" weight="bold" style={{ color: colors.palettes.accent.a12 }}>
|
|
955
|
+
Stay in the loop
|
|
956
|
+
</Text>
|
|
957
|
+
<Text size="3" style={{ color: colors.palettes.accent.a11 }}>
|
|
958
|
+
Get weekly updates on new features and tips.
|
|
959
|
+
</Text>
|
|
960
|
+
</View>
|
|
961
|
+
|
|
962
|
+
<TextField.Root variant="soft" color="accent">
|
|
963
|
+
<TextField.Input placeholder="Enter your email" keyboardType="email-address" />
|
|
964
|
+
</TextField.Root>
|
|
965
|
+
|
|
966
|
+
<Button variant="solid" size="3">
|
|
967
|
+
<Text>Subscribe</Text>
|
|
968
|
+
</Button>
|
|
969
|
+
|
|
970
|
+
<Text size="1" style={{ color: colors.palettes.accent.a11 }}>
|
|
971
|
+
No spam, unsubscribe anytime.
|
|
972
|
+
</Text>
|
|
973
|
+
</View>
|
|
974
|
+
</Card>
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
979
|
+
## Button Placement
|
|
980
|
+
|
|
981
|
+
### Primary Action at Bottom
|
|
982
|
+
|
|
983
|
+
For screens with a clear primary action, place it at the bottom:
|
|
984
|
+
|
|
985
|
+
```tsx
|
|
986
|
+
<View style={{ flex: 1 }}>
|
|
987
|
+
<ScrollView style={{ flex: 1 }}>{/* Content */}</ScrollView>
|
|
988
|
+
<View style={{ padding: 16, gap: 8 }}>
|
|
989
|
+
<Button variant="solid" size="3">
|
|
990
|
+
<Text>Continue</Text>
|
|
991
|
+
</Button>
|
|
992
|
+
</View>
|
|
993
|
+
</View>
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
### Action Pairs
|
|
997
|
+
|
|
998
|
+
When you have two actions (primary + secondary):
|
|
999
|
+
|
|
1000
|
+
```tsx
|
|
1001
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
1002
|
+
<Button variant="soft" color="gray" style={{ flex: 1 }}>
|
|
1003
|
+
<Text>Cancel</Text>
|
|
1004
|
+
</Button>
|
|
1005
|
+
<Button variant="solid" style={{ flex: 1 }}>
|
|
1006
|
+
<Text>Confirm</Text>
|
|
1007
|
+
</Button>
|
|
1008
|
+
</View>
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
### Inline Actions
|
|
1012
|
+
|
|
1013
|
+
For less prominent actions within content:
|
|
1014
|
+
|
|
1015
|
+
```tsx
|
|
1016
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
1017
|
+
<Button variant="ghost" size="2">
|
|
1018
|
+
<Icon as={Heart} size={16} />
|
|
1019
|
+
<Text>Like</Text>
|
|
1020
|
+
</Button>
|
|
1021
|
+
<Button variant="ghost" size="2">
|
|
1022
|
+
<Icon as={MessageCircle} size={16} />
|
|
1023
|
+
<Text>Comment</Text>
|
|
1024
|
+
</Button>
|
|
1025
|
+
<Button variant="ghost" size="2">
|
|
1026
|
+
<Icon as={Share} size={16} />
|
|
1027
|
+
<Text>Share</Text>
|
|
1028
|
+
</Button>
|
|
1029
|
+
</View>
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
1034
|
+
## Empty States
|
|
1035
|
+
|
|
1036
|
+
Always design for empty states:
|
|
1037
|
+
|
|
1038
|
+
```tsx
|
|
1039
|
+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', padding: 32, gap: 16 }}>
|
|
1040
|
+
<View
|
|
1041
|
+
style={{
|
|
1042
|
+
width: 64,
|
|
1043
|
+
height: 64,
|
|
1044
|
+
borderRadius: 32,
|
|
1045
|
+
backgroundColor: colors.palettes.gray.a3,
|
|
1046
|
+
alignItems: 'center',
|
|
1047
|
+
justifyContent: 'center',
|
|
1048
|
+
}}>
|
|
1049
|
+
<Icon as={Inbox} size={32} color={colors.palettes.gray.a11} />
|
|
1050
|
+
</View>
|
|
1051
|
+
<View style={{ gap: 4, alignItems: 'center' }}>
|
|
1052
|
+
<Heading size="4">No messages yet</Heading>
|
|
1053
|
+
<Text size="3" color="gray" style={{ textAlign: 'center' }}>
|
|
1054
|
+
When you receive messages, they'll appear here
|
|
1055
|
+
</Text>
|
|
1056
|
+
</View>
|
|
1057
|
+
<Button variant="solid">
|
|
1058
|
+
<Text>Start a conversation</Text>
|
|
1059
|
+
</Button>
|
|
1060
|
+
</View>
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
---
|
|
1064
|
+
|
|
1065
|
+
## Loading States
|
|
1066
|
+
|
|
1067
|
+
### Skeleton Loading
|
|
1068
|
+
|
|
1069
|
+
Match skeleton dimensions to actual content:
|
|
1070
|
+
|
|
1071
|
+
```tsx
|
|
1072
|
+
{
|
|
1073
|
+
isLoading ? (
|
|
1074
|
+
<View style={{ gap: 12 }}>
|
|
1075
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
1076
|
+
<Skeleton.Avatar size="2" />
|
|
1077
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
1078
|
+
<Skeleton.Text size="2" style={{ width: '60%' }} />
|
|
1079
|
+
<Skeleton.Text size="1" style={{ width: '40%' }} />
|
|
1080
|
+
</View>
|
|
1081
|
+
</View>
|
|
1082
|
+
</View>
|
|
1083
|
+
) : (
|
|
1084
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
1085
|
+
<Avatar fallback={user.name} size="2" />
|
|
1086
|
+
<View style={{ gap: 4 }}>
|
|
1087
|
+
<Text weight="medium">{user.name}</Text>
|
|
1088
|
+
<Text size="1" color="gray">
|
|
1089
|
+
{user.email}
|
|
1090
|
+
</Text>
|
|
1091
|
+
</View>
|
|
1092
|
+
</View>
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
### Button Loading
|
|
1098
|
+
|
|
1099
|
+
The Spinner component wraps content and automatically shows/hides based on the `loading` prop:
|
|
1100
|
+
|
|
1101
|
+
```tsx
|
|
1102
|
+
<Button variant="solid" disabled={isLoading} onPress={handleSubmit}>
|
|
1103
|
+
<Spinner loading={isLoading} size="1">
|
|
1104
|
+
<Text>Submit</Text>
|
|
1105
|
+
</Spinner>
|
|
1106
|
+
</Button>
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
### Page Loading
|
|
1110
|
+
|
|
1111
|
+
```tsx
|
|
1112
|
+
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
|
1113
|
+
<Spinner size="4" />
|
|
1114
|
+
</View>
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
---
|
|
1118
|
+
|
|
1119
|
+
## Feedback & Status
|
|
1120
|
+
|
|
1121
|
+
### Success State
|
|
1122
|
+
|
|
1123
|
+
```tsx
|
|
1124
|
+
<Callout.Root color="success">
|
|
1125
|
+
<Callout.Icon>
|
|
1126
|
+
<Icon as={CheckCircle} size={16} />
|
|
1127
|
+
</Callout.Icon>
|
|
1128
|
+
<Callout.Text>
|
|
1129
|
+
<Text>Your changes have been saved successfully.</Text>
|
|
1130
|
+
</Callout.Text>
|
|
1131
|
+
</Callout.Root>
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
### Error State
|
|
1135
|
+
|
|
1136
|
+
```tsx
|
|
1137
|
+
<Callout.Root color="danger">
|
|
1138
|
+
<Callout.Icon>
|
|
1139
|
+
<Icon as={AlertCircle} size={16} />
|
|
1140
|
+
</Callout.Icon>
|
|
1141
|
+
<Callout.Text>
|
|
1142
|
+
<Text>Something went wrong. Please try again.</Text>
|
|
1143
|
+
</Callout.Text>
|
|
1144
|
+
</Callout.Root>
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
### Inline Validation Error
|
|
1148
|
+
|
|
1149
|
+
```tsx
|
|
1150
|
+
<View style={{ gap: 6 }}>
|
|
1151
|
+
<Label nativeID="email">Email</Label>
|
|
1152
|
+
<TextField.Input
|
|
1153
|
+
placeholder="you@example.com"
|
|
1154
|
+
aria-labelledby="email"
|
|
1155
|
+
style={{ borderColor: colors.palettes.danger['7'] }}
|
|
1156
|
+
/>
|
|
1157
|
+
<Text size="1" color="danger">
|
|
1158
|
+
Please enter a valid email address
|
|
1159
|
+
</Text>
|
|
1160
|
+
</View>
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
---
|
|
1164
|
+
|
|
1165
|
+
## Modal & Dialog Best Practices
|
|
1166
|
+
|
|
1167
|
+
### Dialog Content Structure
|
|
1168
|
+
|
|
1169
|
+
```tsx
|
|
1170
|
+
<Dialog.Content>
|
|
1171
|
+
{/* Title + Description */}
|
|
1172
|
+
<Dialog.Title>Confirm Action</Dialog.Title>
|
|
1173
|
+
<Dialog.Description>Are you sure you want to proceed?</Dialog.Description>
|
|
1174
|
+
|
|
1175
|
+
{/* Optional: Form or additional content */}
|
|
1176
|
+
<View style={{ gap: 12, marginVertical: 16 }}>{/* Content */}</View>
|
|
1177
|
+
|
|
1178
|
+
{/* Actions - always at bottom, right-aligned */}
|
|
1179
|
+
<View style={{ flexDirection: 'row', gap: 8, justifyContent: 'flex-end' }}>
|
|
1180
|
+
<Dialog.Close>
|
|
1181
|
+
<Button variant="soft" color="gray">
|
|
1182
|
+
<Text>Cancel</Text>
|
|
1183
|
+
</Button>
|
|
1184
|
+
</Dialog.Close>
|
|
1185
|
+
<Dialog.Close>
|
|
1186
|
+
<Button variant="solid">
|
|
1187
|
+
<Text>Confirm</Text>
|
|
1188
|
+
</Button>
|
|
1189
|
+
</Dialog.Close>
|
|
1190
|
+
</View>
|
|
1191
|
+
</Dialog.Content>
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
### Destructive Dialog
|
|
1195
|
+
|
|
1196
|
+
```tsx
|
|
1197
|
+
<AlertDialog.Content>
|
|
1198
|
+
<AlertDialog.Header>
|
|
1199
|
+
<AlertDialog.Title>Delete Account</AlertDialog.Title>
|
|
1200
|
+
<AlertDialog.Description>
|
|
1201
|
+
This will permanently delete your account and all associated data. This action cannot be
|
|
1202
|
+
undone.
|
|
1203
|
+
</AlertDialog.Description>
|
|
1204
|
+
</AlertDialog.Header>
|
|
1205
|
+
<AlertDialog.Footer>
|
|
1206
|
+
<AlertDialog.Cancel>
|
|
1207
|
+
<Button variant="soft" color="gray">
|
|
1208
|
+
<Text>Cancel</Text>
|
|
1209
|
+
</Button>
|
|
1210
|
+
</AlertDialog.Cancel>
|
|
1211
|
+
<AlertDialog.Action>
|
|
1212
|
+
<Button variant="solid" color="danger">
|
|
1213
|
+
<Text>Delete Account</Text>
|
|
1214
|
+
</Button>
|
|
1215
|
+
</AlertDialog.Action>
|
|
1216
|
+
</AlertDialog.Footer>
|
|
1217
|
+
</AlertDialog.Content>
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
---
|
|
1221
|
+
|
|
1222
|
+
## Navigation Patterns
|
|
1223
|
+
|
|
1224
|
+
### Header with Back Button
|
|
1225
|
+
|
|
1226
|
+
```tsx
|
|
1227
|
+
<View
|
|
1228
|
+
style={{
|
|
1229
|
+
flexDirection: 'row',
|
|
1230
|
+
alignItems: 'center',
|
|
1231
|
+
paddingHorizontal: 8,
|
|
1232
|
+
paddingVertical: 12,
|
|
1233
|
+
gap: 8,
|
|
1234
|
+
}}>
|
|
1235
|
+
<IconButton variant="ghost" onPress={goBack}>
|
|
1236
|
+
<Icon as={ArrowLeft} size={20} />
|
|
1237
|
+
</IconButton>
|
|
1238
|
+
<Heading size="4" style={{ flex: 1 }}>
|
|
1239
|
+
Page Title
|
|
1240
|
+
</Heading>
|
|
1241
|
+
<IconButton variant="ghost">
|
|
1242
|
+
<Icon as={MoreHorizontal} size={20} />
|
|
1243
|
+
</IconButton>
|
|
1244
|
+
</View>
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
### Tab Navigation
|
|
1248
|
+
|
|
1249
|
+
```tsx
|
|
1250
|
+
<Tabs.Root value={activeTab} onValueChange={setActiveTab}>
|
|
1251
|
+
<Tabs.List>
|
|
1252
|
+
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
|
1253
|
+
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
|
|
1254
|
+
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
|
1255
|
+
</Tabs.List>
|
|
1256
|
+
|
|
1257
|
+
<Tabs.Content value="overview">{/* Overview content */}</Tabs.Content>
|
|
1258
|
+
<Tabs.Content value="activity">{/* Activity content */}</Tabs.Content>
|
|
1259
|
+
<Tabs.Content value="settings">{/* Settings content */}</Tabs.Content>
|
|
1260
|
+
</Tabs.Root>
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
### Segmented Control
|
|
1264
|
+
|
|
1265
|
+
For switching between mutually exclusive views:
|
|
1266
|
+
|
|
1267
|
+
```tsx
|
|
1268
|
+
const [view, setView] = React.useState('list');
|
|
1269
|
+
|
|
1270
|
+
<SegmentedControl.Root value={view} onValueChange={setView}>
|
|
1271
|
+
<SegmentedControl.List>
|
|
1272
|
+
<SegmentedControl.Trigger value="list">List</SegmentedControl.Trigger>
|
|
1273
|
+
<SegmentedControl.Trigger value="grid">Grid</SegmentedControl.Trigger>
|
|
1274
|
+
<SegmentedControl.Trigger value="table">Table</SegmentedControl.Trigger>
|
|
1275
|
+
</SegmentedControl.List>
|
|
1276
|
+
</SegmentedControl.Root>;
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
### Radio Group
|
|
1280
|
+
|
|
1281
|
+
For selecting one option from a list:
|
|
1282
|
+
|
|
1283
|
+
```tsx
|
|
1284
|
+
const [selected, setSelected] = React.useState('option1');
|
|
1285
|
+
|
|
1286
|
+
<RadioGroup.Root value={selected} onValueChange={setSelected}>
|
|
1287
|
+
<View style={{ gap: 8 }}>
|
|
1288
|
+
<Pressable
|
|
1289
|
+
style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}
|
|
1290
|
+
onPress={() => setSelected('option1')}>
|
|
1291
|
+
<RadioGroup.Item value="option1" />
|
|
1292
|
+
<Text>Option 1</Text>
|
|
1293
|
+
</Pressable>
|
|
1294
|
+
<Pressable
|
|
1295
|
+
style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}
|
|
1296
|
+
onPress={() => setSelected('option2')}>
|
|
1297
|
+
<RadioGroup.Item value="option2" />
|
|
1298
|
+
<Text>Option 2</Text>
|
|
1299
|
+
</Pressable>
|
|
1300
|
+
</View>
|
|
1301
|
+
</RadioGroup.Root>;
|
|
1302
|
+
```
|
|
1303
|
+
|
|
1304
|
+
### Search Field
|
|
1305
|
+
|
|
1306
|
+
```tsx
|
|
1307
|
+
<TextField.Root>
|
|
1308
|
+
<TextField.Slot>
|
|
1309
|
+
<Icon as={Search} size={16} />
|
|
1310
|
+
</TextField.Slot>
|
|
1311
|
+
<TextField.Input placeholder="Search..." />
|
|
1312
|
+
<TextField.Slot>
|
|
1313
|
+
<IconButton variant="ghost" size="1">
|
|
1314
|
+
<Icon as={X} size={14} />
|
|
1315
|
+
</IconButton>
|
|
1316
|
+
</TextField.Slot>
|
|
1317
|
+
</TextField.Root>
|
|
1318
|
+
```
|
|
1319
|
+
|
|
1320
|
+
---
|
|
1321
|
+
|
|
1322
|
+
## Accessibility Checklist
|
|
1323
|
+
|
|
1324
|
+
- [ ] All interactive elements have sufficient touch targets (minimum 44×44px, use `size="3"` or larger)
|
|
1325
|
+
- [ ] Form inputs have associated `<Label>` with `nativeID` and `aria-labelledby`
|
|
1326
|
+
- [ ] Color is not the only way to convey information (add icons or text)
|
|
1327
|
+
- [ ] Text has sufficient contrast (Frosted UI handles this automatically)
|
|
1328
|
+
- [ ] Loading states are announced (use `aria-busy`)
|
|
1329
|
+
- [ ] Error messages are associated with inputs (use `aria-describedby`)
|
|
1330
|
+
|
|
1331
|
+
---
|
|
1332
|
+
|
|
1333
|
+
## Visual Polish Tips
|
|
1334
|
+
|
|
1335
|
+
### 1. Align Everything
|
|
1336
|
+
|
|
1337
|
+
Use consistent padding and alignment. If your screen padding is 16px, all content should align to that grid.
|
|
1338
|
+
|
|
1339
|
+
### 2. Group Related Items
|
|
1340
|
+
|
|
1341
|
+
Use `gap` to show relationships:
|
|
1342
|
+
|
|
1343
|
+
- Tighter gaps (4-8px) = closely related
|
|
1344
|
+
- Larger gaps (16-24px) = separate groups
|
|
1345
|
+
|
|
1346
|
+
### 3. Use Cards to Elevate
|
|
1347
|
+
|
|
1348
|
+
Wrap distinct content sections in `<Card>` to create visual separation and hierarchy.
|
|
1349
|
+
|
|
1350
|
+
### 4. Consistent Icon Sizes
|
|
1351
|
+
|
|
1352
|
+
- `14-16px` — inline with text, buttons
|
|
1353
|
+
- `18-20px` — list items, navigation
|
|
1354
|
+
- `24-32px` — feature icons, empty states
|
|
1355
|
+
|
|
1356
|
+
### 5. Icon Colors
|
|
1357
|
+
|
|
1358
|
+
When icons are standalone (not inside Frosted UI components):
|
|
1359
|
+
|
|
1360
|
+
```tsx
|
|
1361
|
+
// Gray icons (neutral)
|
|
1362
|
+
<Icon as={Settings} size={20} color={colors.palettes.gray.a11} />
|
|
1363
|
+
|
|
1364
|
+
// Colored icons (e.g., in icon boxes)
|
|
1365
|
+
<View style={{ backgroundColor: colors.palettes.blue.a3 }}>
|
|
1366
|
+
<Icon as={Bell} size={20} color={colors.palettes.blue.a11} />
|
|
1367
|
+
</View>
|
|
1368
|
+
|
|
1369
|
+
// Semantic icons
|
|
1370
|
+
<Icon as={AlertCircle} size={20} color={colors.palettes.danger['9']} />
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
> **Important**: Use `palette.a11` for colored icons, not `palette['9']`. The alpha shades adapt better to light/dark mode.
|
|
1374
|
+
|
|
1375
|
+
### 5. Balance Whitespace
|
|
1376
|
+
|
|
1377
|
+
If something feels cramped, add padding. If something feels disconnected, reduce gaps. Trust your visual instincts.
|
|
1378
|
+
|
|
1379
|
+
---
|
|
1380
|
+
|
|
1381
|
+
## Store & Marketing Patterns
|
|
1382
|
+
|
|
1383
|
+
### Pricing Tier
|
|
1384
|
+
|
|
1385
|
+
```tsx
|
|
1386
|
+
<Card>
|
|
1387
|
+
<View style={{ gap: 16 }}>
|
|
1388
|
+
<Badge color="accent" size="1" style={{ alignSelf: 'flex-start' }}>
|
|
1389
|
+
<Text>MOST POPULAR</Text>
|
|
1390
|
+
</Badge>
|
|
1391
|
+
|
|
1392
|
+
<View style={{ gap: 4 }}>
|
|
1393
|
+
<Text size="3" weight="bold">
|
|
1394
|
+
Pro Plan
|
|
1395
|
+
</Text>
|
|
1396
|
+
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 4 }}>
|
|
1397
|
+
<Text size="7" weight="bold">
|
|
1398
|
+
$19
|
|
1399
|
+
</Text>
|
|
1400
|
+
<Text size="2" color="gray">
|
|
1401
|
+
/month
|
|
1402
|
+
</Text>
|
|
1403
|
+
</View>
|
|
1404
|
+
</View>
|
|
1405
|
+
|
|
1406
|
+
<Separator size="4" />
|
|
1407
|
+
|
|
1408
|
+
<View style={{ gap: 12 }}>
|
|
1409
|
+
{['Unlimited projects', 'Advanced analytics', 'Priority support', 'Custom integrations'].map(
|
|
1410
|
+
(feature) => (
|
|
1411
|
+
<View key={feature} style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
1412
|
+
<View
|
|
1413
|
+
style={{
|
|
1414
|
+
width: 20,
|
|
1415
|
+
height: 20,
|
|
1416
|
+
borderRadius: 10,
|
|
1417
|
+
backgroundColor: colors.palettes.success.a3,
|
|
1418
|
+
alignItems: 'center',
|
|
1419
|
+
justifyContent: 'center',
|
|
1420
|
+
}}>
|
|
1421
|
+
<Icon as={Check} size={12} color={colors.palettes.success['9']} />
|
|
1422
|
+
</View>
|
|
1423
|
+
<Text size="2">{feature}</Text>
|
|
1424
|
+
</View>
|
|
1425
|
+
)
|
|
1426
|
+
)}
|
|
1427
|
+
</View>
|
|
1428
|
+
|
|
1429
|
+
<Button variant="solid" size="4">
|
|
1430
|
+
<Text>Get Started</Text>
|
|
1431
|
+
</Button>
|
|
1432
|
+
</View>
|
|
1433
|
+
</Card>
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
### Testimonial
|
|
1437
|
+
|
|
1438
|
+
```tsx
|
|
1439
|
+
<Card>
|
|
1440
|
+
<View style={{ gap: 16 }}>
|
|
1441
|
+
<Icon as={Quote} size={32} color={colors.palettes.gray.a6} />
|
|
1442
|
+
|
|
1443
|
+
<Text size="3" style={{ fontStyle: 'italic' }}>
|
|
1444
|
+
"This product has completely transformed how our team works. We've seen a 40% increase in
|
|
1445
|
+
productivity and the support team is incredibly responsive."
|
|
1446
|
+
</Text>
|
|
1447
|
+
|
|
1448
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
1449
|
+
{[1, 2, 3, 4, 5].map((star) => (
|
|
1450
|
+
<Icon
|
|
1451
|
+
key={star}
|
|
1452
|
+
as={Star}
|
|
1453
|
+
size={16}
|
|
1454
|
+
color={colors.palettes.amber['9']}
|
|
1455
|
+
fill={colors.palettes.amber['9']}
|
|
1456
|
+
/>
|
|
1457
|
+
))}
|
|
1458
|
+
</View>
|
|
1459
|
+
|
|
1460
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
|
1461
|
+
<Avatar fallback="JD" size="3" color="blue" />
|
|
1462
|
+
<View style={{ gap: 2 }}>
|
|
1463
|
+
<Text weight="medium">Jennifer Davis</Text>
|
|
1464
|
+
<Text size="1" color="gray">
|
|
1465
|
+
CTO at TechCorp
|
|
1466
|
+
</Text>
|
|
1467
|
+
</View>
|
|
1468
|
+
</View>
|
|
1469
|
+
</View>
|
|
1470
|
+
</Card>
|
|
1471
|
+
```
|
|
1472
|
+
|
|
1473
|
+
### Feature Showcase
|
|
1474
|
+
|
|
1475
|
+
```tsx
|
|
1476
|
+
const features = [
|
|
1477
|
+
{ icon: Zap, title: 'Lightning Fast', description: 'Sub-100ms response times', color: 'amber' },
|
|
1478
|
+
{ icon: Users, title: 'Team Collaboration', description: 'Real-time sync', color: 'blue' },
|
|
1479
|
+
{ icon: Sparkles, title: 'AI Powered', description: 'Smart suggestions', color: 'purple' },
|
|
1480
|
+
];
|
|
1481
|
+
|
|
1482
|
+
<View style={{ gap: 12 }}>
|
|
1483
|
+
{features.map((feature) => (
|
|
1484
|
+
<Card key={feature.title} variant="soft">
|
|
1485
|
+
<View style={{ flexDirection: 'row', gap: 16 }}>
|
|
1486
|
+
<View
|
|
1487
|
+
style={{
|
|
1488
|
+
width: 48,
|
|
1489
|
+
height: 48,
|
|
1490
|
+
borderRadius: 12,
|
|
1491
|
+
backgroundColor: colors.palettes[feature.color].a3,
|
|
1492
|
+
alignItems: 'center',
|
|
1493
|
+
justifyContent: 'center',
|
|
1494
|
+
}}>
|
|
1495
|
+
<Icon as={feature.icon} size={24} color={colors.palettes[feature.color].a11} />
|
|
1496
|
+
</View>
|
|
1497
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
1498
|
+
<Text weight="medium">{feature.title}</Text>
|
|
1499
|
+
<Text size="2" color="gray">
|
|
1500
|
+
{feature.description}
|
|
1501
|
+
</Text>
|
|
1502
|
+
</View>
|
|
1503
|
+
</View>
|
|
1504
|
+
</Card>
|
|
1505
|
+
))}
|
|
1506
|
+
</View>;
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
### App Stats
|
|
1510
|
+
|
|
1511
|
+
```tsx
|
|
1512
|
+
const stats = [
|
|
1513
|
+
{ value: '10M+', label: 'Downloads', icon: Download },
|
|
1514
|
+
{ value: '4.8★', label: 'Rating', icon: Star },
|
|
1515
|
+
{ value: '#1', label: 'Top Charts', icon: Trophy },
|
|
1516
|
+
];
|
|
1517
|
+
|
|
1518
|
+
<View style={{ flexDirection: 'row', gap: 12 }}>
|
|
1519
|
+
{stats.map((stat) => (
|
|
1520
|
+
<Card key={stat.label} style={{ flex: 1, alignItems: 'center' }}>
|
|
1521
|
+
<View style={{ alignItems: 'center', gap: 8 }}>
|
|
1522
|
+
<Icon as={stat.icon} size={24} color={colors.palettes.accent.a11} />
|
|
1523
|
+
<Text size="4" weight="bold">
|
|
1524
|
+
{stat.value}
|
|
1525
|
+
</Text>
|
|
1526
|
+
<Text size="1" color="gray">
|
|
1527
|
+
{stat.label}
|
|
1528
|
+
</Text>
|
|
1529
|
+
</View>
|
|
1530
|
+
</Card>
|
|
1531
|
+
))}
|
|
1532
|
+
</View>;
|
|
1533
|
+
```
|
|
1534
|
+
|
|
1535
|
+
---
|
|
1536
|
+
|
|
1537
|
+
## Apple-like Design Principles
|
|
1538
|
+
|
|
1539
|
+
When creating premium, polished interfaces, follow these principles:
|
|
1540
|
+
|
|
1541
|
+
### 1. Structure Over Decoration
|
|
1542
|
+
|
|
1543
|
+
- Use clear sections with subtle borders (`colors.stroke`)
|
|
1544
|
+
- Separate header areas with different background shades (`gray.a2`)
|
|
1545
|
+
- Avoid gratuitous gradients or shadows
|
|
1546
|
+
|
|
1547
|
+
### 2. Consistent Color Theming
|
|
1548
|
+
|
|
1549
|
+
When theming a section:
|
|
1550
|
+
|
|
1551
|
+
```tsx
|
|
1552
|
+
// Use a single palette consistently
|
|
1553
|
+
backgroundColor: colors.palettes.pink.a2, // Subtle background
|
|
1554
|
+
borderColor: colors.palettes.pink.a4, // Border
|
|
1555
|
+
color: colors.palettes.pink.a11, // Body text
|
|
1556
|
+
color: colors.palettes.pink.a12, // Heading
|
|
1557
|
+
```
|
|
1558
|
+
|
|
1559
|
+
### 3. Subtle Emphasis
|
|
1560
|
+
|
|
1561
|
+
- Use `shadowRadius: 12` with `shadowOpacity: 0.3` for a soft "glow"
|
|
1562
|
+
- Add small indicator dots (6×6) for live/active states
|
|
1563
|
+
- Use `weight="medium"` more than `weight="bold"` for cleaner text
|
|
1564
|
+
|
|
1565
|
+
### 4. Centered Focal Points
|
|
1566
|
+
|
|
1567
|
+
For achievements, promotions, or highlights:
|
|
1568
|
+
|
|
1569
|
+
```tsx
|
|
1570
|
+
<Badge style={{ alignSelf: 'center' }}>
|
|
1571
|
+
<Icon as={Award} size={14} />
|
|
1572
|
+
<Text>Achievement Name</Text>
|
|
1573
|
+
</Badge>
|
|
1574
|
+
```
|
|
1575
|
+
|
|
1576
|
+
### 5. Stats Rows
|
|
1577
|
+
|
|
1578
|
+
Display multiple metrics in a clean horizontal layout:
|
|
1579
|
+
|
|
1580
|
+
```tsx
|
|
1581
|
+
<View style={{ flexDirection: 'row' }}>
|
|
1582
|
+
<View style={{ flex: 1, alignItems: 'center', gap: 2 }}>
|
|
1583
|
+
<Text size="4" weight="bold">
|
|
1584
|
+
100
|
|
1585
|
+
</Text>
|
|
1586
|
+
<Text size="1" color="gray">
|
|
1587
|
+
Points
|
|
1588
|
+
</Text>
|
|
1589
|
+
</View>
|
|
1590
|
+
<View style={{ width: 1, backgroundColor: colors.stroke }} />
|
|
1591
|
+
{/* More stats... */}
|
|
1592
|
+
</View>
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
---
|
|
1596
|
+
|
|
1597
|
+
## Common Mistakes to Avoid
|
|
1598
|
+
|
|
1599
|
+
| Mistake | Better Approach |
|
|
1600
|
+
| --------------------------- | ------------------------------------------------------ |
|
|
1601
|
+
| Using many different colors | Stick to accent + gray + semantic colors |
|
|
1602
|
+
| Inconsistent spacing | Use the spacing scale (4, 8, 12, 16, 24, 32) |
|
|
1603
|
+
| Too many primary buttons | One `solid` button per view/section |
|
|
1604
|
+
| Overriding component styles | Use built-in variants and props |
|
|
1605
|
+
| Missing loading states | Always show feedback during async operations |
|
|
1606
|
+
| Missing empty states | Design what users see with no data |
|
|
1607
|
+
| Text without hierarchy | Use Heading for titles, Text with size/weight for body |
|
|
1608
|
+
| Cramped touch targets | Use `size="2"` or `size="3"` for interactive elements |
|
|
1609
|
+
|
|
1610
|
+
---
|
|
1611
|
+
|
|
1612
|
+
## Quick Reference: Common Compositions
|
|
1613
|
+
|
|
1614
|
+
| Pattern | Components Used |
|
|
1615
|
+
| ------------ | ------------------------------------------------- |
|
|
1616
|
+
| Page header | `<Heading>` + optional `<Text>` description |
|
|
1617
|
+
| Form field | `<Label>` + `<TextField.Input>` + helper `<Text>` |
|
|
1618
|
+
| List item | `<Avatar>` + `<Text>` stack + `<Badge>` or action |
|
|
1619
|
+
| Card action | `<Card>` + content + `<Button>` row |
|
|
1620
|
+
| Empty state | Icon + `<Heading>` + `<Text>` + `<Button>` |
|
|
1621
|
+
| Settings row | Label + `<Switch>` or `<Select>` |
|
|
1622
|
+
| Dialog | Title + Description + content + button row |
|
|
1623
|
+
| Feedback | `<Callout>` with semantic color |
|
|
1624
|
+
|
|
1625
|
+
---
|
|
1626
|
+
|
|
1627
|
+
## E-commerce Patterns
|
|
1628
|
+
|
|
1629
|
+
### Product Card
|
|
1630
|
+
|
|
1631
|
+
```tsx
|
|
1632
|
+
<Card style={{ padding: 0 }}>
|
|
1633
|
+
{/* Product Image */}
|
|
1634
|
+
<View style={{ height: 200, backgroundColor: colors.palettes.gray.a3 }} />
|
|
1635
|
+
|
|
1636
|
+
<View style={{ padding: 16, gap: 12 }}>
|
|
1637
|
+
{/* Category + Rating */}
|
|
1638
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1639
|
+
<Badge variant="soft" color="gray" size="1">
|
|
1640
|
+
<Text>Electronics</Text>
|
|
1641
|
+
</Badge>
|
|
1642
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
1643
|
+
<Icon
|
|
1644
|
+
as={Star}
|
|
1645
|
+
size={14}
|
|
1646
|
+
color={colors.palettes.amber['9']}
|
|
1647
|
+
fill={colors.palettes.amber['9']}
|
|
1648
|
+
/>
|
|
1649
|
+
<Text size="1" weight="medium">
|
|
1650
|
+
4.8
|
|
1651
|
+
</Text>
|
|
1652
|
+
<Text size="1" color="gray">
|
|
1653
|
+
(128)
|
|
1654
|
+
</Text>
|
|
1655
|
+
</View>
|
|
1656
|
+
</View>
|
|
1657
|
+
|
|
1658
|
+
{/* Title */}
|
|
1659
|
+
<Text size="3" weight="medium" numberOfLines={2}>
|
|
1660
|
+
Wireless Noise-Cancelling Headphones
|
|
1661
|
+
</Text>
|
|
1662
|
+
|
|
1663
|
+
{/* Price */}
|
|
1664
|
+
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 8 }}>
|
|
1665
|
+
<Text size="4" weight="bold">
|
|
1666
|
+
$299
|
|
1667
|
+
</Text>
|
|
1668
|
+
<Text size="2" color="gray" style={{ textDecorationLine: 'line-through' }}>
|
|
1669
|
+
$349
|
|
1670
|
+
</Text>
|
|
1671
|
+
</View>
|
|
1672
|
+
|
|
1673
|
+
{/* Quick action */}
|
|
1674
|
+
<Button variant="solid" size="3">
|
|
1675
|
+
<Text>Add to Cart</Text>
|
|
1676
|
+
</Button>
|
|
1677
|
+
</View>
|
|
1678
|
+
</Card>
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
### Cart Item
|
|
1682
|
+
|
|
1683
|
+
```tsx
|
|
1684
|
+
<View
|
|
1685
|
+
style={{
|
|
1686
|
+
flexDirection: 'row',
|
|
1687
|
+
gap: 12,
|
|
1688
|
+
paddingVertical: 16,
|
|
1689
|
+
borderBottomWidth: 1,
|
|
1690
|
+
borderBottomColor: colors.stroke,
|
|
1691
|
+
}}>
|
|
1692
|
+
{/* Thumbnail */}
|
|
1693
|
+
<View
|
|
1694
|
+
style={{
|
|
1695
|
+
width: 80,
|
|
1696
|
+
height: 80,
|
|
1697
|
+
borderRadius: 8,
|
|
1698
|
+
backgroundColor: colors.palettes.gray.a3,
|
|
1699
|
+
}}
|
|
1700
|
+
/>
|
|
1701
|
+
|
|
1702
|
+
{/* Details */}
|
|
1703
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
1704
|
+
<Text size="2" weight="medium" numberOfLines={2}>
|
|
1705
|
+
Premium Wireless Headphones
|
|
1706
|
+
</Text>
|
|
1707
|
+
<Text size="1" color="gray">
|
|
1708
|
+
Black · Qty: 1
|
|
1709
|
+
</Text>
|
|
1710
|
+
<Text size="3" weight="bold">
|
|
1711
|
+
$299
|
|
1712
|
+
</Text>
|
|
1713
|
+
</View>
|
|
1714
|
+
|
|
1715
|
+
{/* Remove button */}
|
|
1716
|
+
<IconButton variant="ghost" size="2" color="gray">
|
|
1717
|
+
<Icon as={Trash2} size={16} />
|
|
1718
|
+
</IconButton>
|
|
1719
|
+
</View>
|
|
1720
|
+
```
|
|
1721
|
+
|
|
1722
|
+
### Order Summary
|
|
1723
|
+
|
|
1724
|
+
```tsx
|
|
1725
|
+
<Card>
|
|
1726
|
+
<View style={{ gap: 16 }}>
|
|
1727
|
+
<Heading size="4">Order Summary</Heading>
|
|
1728
|
+
|
|
1729
|
+
<View style={{ gap: 12 }}>
|
|
1730
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
1731
|
+
<Text color="gray">Subtotal (3 items)</Text>
|
|
1732
|
+
<Text weight="medium">$239.97</Text>
|
|
1733
|
+
</View>
|
|
1734
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
1735
|
+
<Text color="gray">Shipping</Text>
|
|
1736
|
+
<Text weight="medium" color="success">
|
|
1737
|
+
Free
|
|
1738
|
+
</Text>
|
|
1739
|
+
</View>
|
|
1740
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
1741
|
+
<Text color="gray">Tax</Text>
|
|
1742
|
+
<Text weight="medium">$19.20</Text>
|
|
1743
|
+
</View>
|
|
1744
|
+
</View>
|
|
1745
|
+
|
|
1746
|
+
<Separator size="4" />
|
|
1747
|
+
|
|
1748
|
+
{/* Discount Code */}
|
|
1749
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
1750
|
+
<View style={{ flex: 1 }}>
|
|
1751
|
+
<TextField.Input placeholder="Discount code" />
|
|
1752
|
+
</View>
|
|
1753
|
+
<Button variant="surface">
|
|
1754
|
+
<Text>Apply</Text>
|
|
1755
|
+
</Button>
|
|
1756
|
+
</View>
|
|
1757
|
+
|
|
1758
|
+
<Separator size="4" />
|
|
1759
|
+
|
|
1760
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1761
|
+
<Text size="4" weight="bold">
|
|
1762
|
+
Total
|
|
1763
|
+
</Text>
|
|
1764
|
+
<Text size="5" weight="bold">
|
|
1765
|
+
$259.17
|
|
1766
|
+
</Text>
|
|
1767
|
+
</View>
|
|
1768
|
+
|
|
1769
|
+
<Button variant="solid" size="4">
|
|
1770
|
+
<Text>Checkout</Text>
|
|
1771
|
+
</Button>
|
|
1772
|
+
</View>
|
|
1773
|
+
</Card>
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1776
|
+
### Shipping Options
|
|
1777
|
+
|
|
1778
|
+
Use `List` with `RadioGroup` for selection lists like shipping methods:
|
|
1779
|
+
|
|
1780
|
+
```tsx
|
|
1781
|
+
const [selected, setSelected] = React.useState('standard');
|
|
1782
|
+
|
|
1783
|
+
const options = [
|
|
1784
|
+
{
|
|
1785
|
+
id: 'standard',
|
|
1786
|
+
name: 'Standard Shipping',
|
|
1787
|
+
price: 'Free',
|
|
1788
|
+
time: '5-7 business days',
|
|
1789
|
+
icon: Truck,
|
|
1790
|
+
},
|
|
1791
|
+
{ id: 'express', name: 'Express Shipping', price: '$9.99', time: '2-3 business days', icon: Zap },
|
|
1792
|
+
{ id: 'overnight', name: 'Overnight', price: '$24.99', time: 'Next business day', icon: Clock },
|
|
1793
|
+
];
|
|
1794
|
+
|
|
1795
|
+
<RadioGroup.Root value={selected} onValueChange={setSelected}>
|
|
1796
|
+
<List.Root>
|
|
1797
|
+
{options.map((option, index) => (
|
|
1798
|
+
<React.Fragment key={option.id}>
|
|
1799
|
+
{index > 0 && <List.Separator />}
|
|
1800
|
+
<List.Item onPress={() => setSelected(option.id)}>
|
|
1801
|
+
<List.ItemSlot>
|
|
1802
|
+
<RadioGroup.Item value={option.id} />
|
|
1803
|
+
</List.ItemSlot>
|
|
1804
|
+
<List.ItemSlot>
|
|
1805
|
+
<View style={iconBoxStyle}>
|
|
1806
|
+
<Icon as={option.icon} size={20} />
|
|
1807
|
+
</View>
|
|
1808
|
+
</List.ItemSlot>
|
|
1809
|
+
<List.ItemContent>
|
|
1810
|
+
<List.ItemTitle>{option.name}</List.ItemTitle>
|
|
1811
|
+
<List.ItemDescription>{option.time}</List.ItemDescription>
|
|
1812
|
+
</List.ItemContent>
|
|
1813
|
+
<List.ItemSlot>
|
|
1814
|
+
<Text weight="medium" color={option.price === 'Free' ? 'success' : undefined}>
|
|
1815
|
+
{option.price}
|
|
1816
|
+
</Text>
|
|
1817
|
+
</List.ItemSlot>
|
|
1818
|
+
</List.Item>
|
|
1819
|
+
</React.Fragment>
|
|
1820
|
+
))}
|
|
1821
|
+
</List.Root>
|
|
1822
|
+
</RadioGroup.Root>;
|
|
1823
|
+
```
|
|
1824
|
+
|
|
1825
|
+
### Payment Method
|
|
1826
|
+
|
|
1827
|
+
```tsx
|
|
1828
|
+
const [selected, setSelected] = React.useState('visa');
|
|
1829
|
+
|
|
1830
|
+
<RadioGroup.Root value={selected} onValueChange={setSelected}>
|
|
1831
|
+
<List.Root>
|
|
1832
|
+
{[
|
|
1833
|
+
{ id: 'visa', name: 'Visa', last4: '4242', expiry: '12/25' },
|
|
1834
|
+
{ id: 'mastercard', name: 'Mastercard', last4: '8888', expiry: '03/26' },
|
|
1835
|
+
].map((card, index) => (
|
|
1836
|
+
<React.Fragment key={card.id}>
|
|
1837
|
+
{index > 0 && <List.Separator />}
|
|
1838
|
+
<List.Item onPress={() => setSelected(card.id)}>
|
|
1839
|
+
<List.ItemSlot>
|
|
1840
|
+
<RadioGroup.Item value={card.id} />
|
|
1841
|
+
</List.ItemSlot>
|
|
1842
|
+
<List.ItemSlot>
|
|
1843
|
+
<View style={cardIconStyle}>
|
|
1844
|
+
<Icon as={CreditCard} size={20} color={colors.palettes.gray.a11} />
|
|
1845
|
+
</View>
|
|
1846
|
+
</List.ItemSlot>
|
|
1847
|
+
<List.ItemContent>
|
|
1848
|
+
<List.ItemTitle>
|
|
1849
|
+
{card.name} •••• {card.last4}
|
|
1850
|
+
</List.ItemTitle>
|
|
1851
|
+
<List.ItemDescription>Expires {card.expiry}</List.ItemDescription>
|
|
1852
|
+
</List.ItemContent>
|
|
1853
|
+
</List.Item>
|
|
1854
|
+
</React.Fragment>
|
|
1855
|
+
))}
|
|
1856
|
+
|
|
1857
|
+
<List.Separator />
|
|
1858
|
+
|
|
1859
|
+
{/* Add new card option */}
|
|
1860
|
+
<List.Item onPress={() => {}}>
|
|
1861
|
+
<List.ItemSlot>
|
|
1862
|
+
<View style={addButtonStyle}>
|
|
1863
|
+
<Icon as={Plus} size={14} color={colors.palettes.accent.a11} />
|
|
1864
|
+
</View>
|
|
1865
|
+
</List.ItemSlot>
|
|
1866
|
+
<List.ItemContent>
|
|
1867
|
+
<Text weight="medium" color="accent">
|
|
1868
|
+
Add new card
|
|
1869
|
+
</Text>
|
|
1870
|
+
</List.ItemContent>
|
|
1871
|
+
</List.Item>
|
|
1872
|
+
</List.Root>
|
|
1873
|
+
</RadioGroup.Root>;
|
|
1874
|
+
```
|
|
1875
|
+
|
|
1876
|
+
### Order Status (Timeline)
|
|
1877
|
+
|
|
1878
|
+
```tsx
|
|
1879
|
+
const steps = [
|
|
1880
|
+
{ id: 'ordered', label: 'Ordered', date: 'Dec 15', completed: true },
|
|
1881
|
+
{ id: 'shipped', label: 'Shipped', date: 'Dec 16', completed: true },
|
|
1882
|
+
{ id: 'transit', label: 'In Transit', date: 'Dec 17', completed: true, active: true },
|
|
1883
|
+
{ id: 'delivered', label: 'Delivered', date: 'Dec 19', completed: false },
|
|
1884
|
+
];
|
|
1885
|
+
|
|
1886
|
+
<Card>
|
|
1887
|
+
<View style={{ gap: 16 }}>
|
|
1888
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
1889
|
+
<Heading size="4">Order Status</Heading>
|
|
1890
|
+
<Badge color="info" size="1">
|
|
1891
|
+
<Text>In Transit</Text>
|
|
1892
|
+
</Badge>
|
|
1893
|
+
</View>
|
|
1894
|
+
|
|
1895
|
+
<View style={{ gap: 0 }}>
|
|
1896
|
+
{steps.map((step, index) => (
|
|
1897
|
+
<View key={step.id} style={{ flexDirection: 'row', gap: 12 }}>
|
|
1898
|
+
{/* Timeline */}
|
|
1899
|
+
<View style={{ alignItems: 'center', width: 24 }}>
|
|
1900
|
+
<View
|
|
1901
|
+
style={{
|
|
1902
|
+
width: 24,
|
|
1903
|
+
height: 24,
|
|
1904
|
+
borderRadius: 12,
|
|
1905
|
+
backgroundColor: step.completed
|
|
1906
|
+
? colors.palettes.success['9']
|
|
1907
|
+
: colors.palettes.gray.a4,
|
|
1908
|
+
alignItems: 'center',
|
|
1909
|
+
justifyContent: 'center',
|
|
1910
|
+
}}>
|
|
1911
|
+
{step.completed && <Icon as={Check} size={14} color="white" />}
|
|
1912
|
+
</View>
|
|
1913
|
+
{index < steps.length - 1 && (
|
|
1914
|
+
<View
|
|
1915
|
+
style={{
|
|
1916
|
+
width: 2,
|
|
1917
|
+
height: 32,
|
|
1918
|
+
backgroundColor: steps[index + 1].completed
|
|
1919
|
+
? colors.palettes.success['9']
|
|
1920
|
+
: colors.palettes.gray.a4,
|
|
1921
|
+
}}
|
|
1922
|
+
/>
|
|
1923
|
+
)}
|
|
1924
|
+
</View>
|
|
1925
|
+
{/* Content */}
|
|
1926
|
+
<View style={{ flex: 1, paddingBottom: index < steps.length - 1 ? 16 : 0 }}>
|
|
1927
|
+
<Text weight={step.active ? 'bold' : 'medium'}>{step.label}</Text>
|
|
1928
|
+
<Text size="1" color="gray">
|
|
1929
|
+
{step.date}
|
|
1930
|
+
</Text>
|
|
1931
|
+
</View>
|
|
1932
|
+
</View>
|
|
1933
|
+
))}
|
|
1934
|
+
</View>
|
|
1935
|
+
</View>
|
|
1936
|
+
</Card>;
|
|
1937
|
+
```
|
|
1938
|
+
|
|
1939
|
+
### Product Review
|
|
1940
|
+
|
|
1941
|
+
```tsx
|
|
1942
|
+
<Card>
|
|
1943
|
+
<View style={{ gap: 12 }}>
|
|
1944
|
+
<View
|
|
1945
|
+
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
1946
|
+
<View style={{ flexDirection: 'row', gap: 12 }}>
|
|
1947
|
+
<Avatar fallback="MJ" size="3" color="blue" />
|
|
1948
|
+
<View style={{ gap: 2 }}>
|
|
1949
|
+
<Text weight="medium">Michael Johnson</Text>
|
|
1950
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
1951
|
+
{[1, 2, 3, 4, 5].map((star) => (
|
|
1952
|
+
<Icon
|
|
1953
|
+
key={star}
|
|
1954
|
+
as={Star}
|
|
1955
|
+
size={12}
|
|
1956
|
+
color={star <= 5 ? colors.palettes.amber['9'] : colors.palettes.gray.a6}
|
|
1957
|
+
fill={star <= 5 ? colors.palettes.amber['9'] : 'transparent'}
|
|
1958
|
+
/>
|
|
1959
|
+
))}
|
|
1960
|
+
</View>
|
|
1961
|
+
</View>
|
|
1962
|
+
</View>
|
|
1963
|
+
<Text size="1" color="gray">
|
|
1964
|
+
2 days ago
|
|
1965
|
+
</Text>
|
|
1966
|
+
</View>
|
|
1967
|
+
|
|
1968
|
+
<Text size="3" color="gray">
|
|
1969
|
+
Absolutely love these headphones! The noise cancellation is incredible and the battery life
|
|
1970
|
+
exceeds expectations. Highly recommend for anyone looking for premium audio quality.
|
|
1971
|
+
</Text>
|
|
1972
|
+
|
|
1973
|
+
<Button variant="ghost" size="2" style={{ alignSelf: 'flex-start' }}>
|
|
1974
|
+
<Icon as={ThumbsUp} size={14} />
|
|
1975
|
+
<Text>Helpful (24)</Text>
|
|
1976
|
+
</Button>
|
|
1977
|
+
</View>
|
|
1978
|
+
</Card>
|
|
1979
|
+
```
|
|
1980
|
+
|
|
1981
|
+
### Wishlist Item
|
|
1982
|
+
|
|
1983
|
+
```tsx
|
|
1984
|
+
<Card style={{ padding: 0 }}>
|
|
1985
|
+
<View style={{ flexDirection: 'row', padding: 16, gap: 12 }}>
|
|
1986
|
+
<View
|
|
1987
|
+
style={{
|
|
1988
|
+
width: 80,
|
|
1989
|
+
height: 80,
|
|
1990
|
+
backgroundColor: colors.palettes.gray.a3,
|
|
1991
|
+
borderRadius: 8,
|
|
1992
|
+
alignItems: 'center',
|
|
1993
|
+
justifyContent: 'center',
|
|
1994
|
+
}}>
|
|
1995
|
+
<Icon as={ShoppingBag} size={32} color={colors.palettes.gray.a8} />
|
|
1996
|
+
</View>
|
|
1997
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
1998
|
+
<Text size="2" weight="medium" numberOfLines={2}>
|
|
1999
|
+
Premium Leather Wallet
|
|
2000
|
+
</Text>
|
|
2001
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
2002
|
+
{[1, 2, 3, 4, 5].map((star) => (
|
|
2003
|
+
<Icon
|
|
2004
|
+
key={star}
|
|
2005
|
+
as={Star}
|
|
2006
|
+
size={10}
|
|
2007
|
+
color={star <= 4 ? colors.palettes.amber['9'] : colors.palettes.gray.a6}
|
|
2008
|
+
fill={star <= 4 ? colors.palettes.amber['9'] : 'transparent'}
|
|
2009
|
+
/>
|
|
2010
|
+
))}
|
|
2011
|
+
<Text size="0" color="gray">
|
|
2012
|
+
(89)
|
|
2013
|
+
</Text>
|
|
2014
|
+
</View>
|
|
2015
|
+
<Text size="3" weight="bold">
|
|
2016
|
+
$49.99
|
|
2017
|
+
</Text>
|
|
2018
|
+
</View>
|
|
2019
|
+
</View>
|
|
2020
|
+
<Separator size="4" />
|
|
2021
|
+
<View style={{ flexDirection: 'row', paddingHorizontal: 16, paddingVertical: 12, gap: 8 }}>
|
|
2022
|
+
<Button variant="solid" size="2" style={{ flex: 1 }}>
|
|
2023
|
+
<Icon as={ShoppingCart} size={14} />
|
|
2024
|
+
<Text>Move to Cart</Text>
|
|
2025
|
+
</Button>
|
|
2026
|
+
<IconButton variant="surface" size="2" color="danger">
|
|
2027
|
+
<Icon as={Trash2} size={16} />
|
|
2028
|
+
</IconButton>
|
|
2029
|
+
</View>
|
|
2030
|
+
</Card>
|
|
2031
|
+
```
|
|
2032
|
+
|
|
2033
|
+
---
|
|
2034
|
+
|
|
2035
|
+
## Profile & Social Patterns
|
|
2036
|
+
|
|
2037
|
+
### Team Member Card
|
|
2038
|
+
|
|
2039
|
+
```tsx
|
|
2040
|
+
<Card>
|
|
2041
|
+
<View style={{ alignItems: 'center', gap: 12 }}>
|
|
2042
|
+
<Avatar fallback="AK" size="7" color="blue" />
|
|
2043
|
+
<View style={{ alignItems: 'center', gap: 4 }}>
|
|
2044
|
+
<Text size="3" weight="bold">
|
|
2045
|
+
Alex Kim
|
|
2046
|
+
</Text>
|
|
2047
|
+
<Text size="2" color="gray">
|
|
2048
|
+
Senior Designer
|
|
2049
|
+
</Text>
|
|
2050
|
+
</View>
|
|
2051
|
+
<View style={{ flexDirection: 'row', gap: 8 }}>
|
|
2052
|
+
<IconButton variant="soft" size="2" color="gray">
|
|
2053
|
+
<Icon as={Twitter} size={16} />
|
|
2054
|
+
</IconButton>
|
|
2055
|
+
<IconButton variant="soft" size="2" color="gray">
|
|
2056
|
+
<Icon as={Linkedin} size={16} />
|
|
2057
|
+
</IconButton>
|
|
2058
|
+
<IconButton variant="soft" size="2" color="gray">
|
|
2059
|
+
<Icon as={Send} size={16} />
|
|
2060
|
+
</IconButton>
|
|
2061
|
+
</View>
|
|
2062
|
+
</View>
|
|
2063
|
+
</Card>
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
### User Stats Row
|
|
2067
|
+
|
|
2068
|
+
```tsx
|
|
2069
|
+
<View style={{ flexDirection: 'row' }}>
|
|
2070
|
+
<View style={{ flex: 1, alignItems: 'center', gap: 2 }}>
|
|
2071
|
+
<Text size="5" weight="bold">
|
|
2072
|
+
2.4k
|
|
2073
|
+
</Text>
|
|
2074
|
+
<Text size="1" color="gray">
|
|
2075
|
+
Followers
|
|
2076
|
+
</Text>
|
|
2077
|
+
</View>
|
|
2078
|
+
<View style={{ width: 1, backgroundColor: colors.stroke }} />
|
|
2079
|
+
<View style={{ flex: 1, alignItems: 'center', gap: 2 }}>
|
|
2080
|
+
<Text size="5" weight="bold">
|
|
2081
|
+
486
|
|
2082
|
+
</Text>
|
|
2083
|
+
<Text size="1" color="gray">
|
|
2084
|
+
Following
|
|
2085
|
+
</Text>
|
|
2086
|
+
</View>
|
|
2087
|
+
<View style={{ width: 1, backgroundColor: colors.stroke }} />
|
|
2088
|
+
<View style={{ flex: 1, alignItems: 'center', gap: 2 }}>
|
|
2089
|
+
<Text size="5" weight="bold">
|
|
2090
|
+
12
|
|
2091
|
+
</Text>
|
|
2092
|
+
<Text size="1" color="gray">
|
|
2093
|
+
Projects
|
|
2094
|
+
</Text>
|
|
2095
|
+
</View>
|
|
2096
|
+
</View>
|
|
2097
|
+
```
|
|
2098
|
+
|
|
2099
|
+
### Social Post
|
|
2100
|
+
|
|
2101
|
+
```tsx
|
|
2102
|
+
const [liked, setLiked] = React.useState(false);
|
|
2103
|
+
const [likes, setLikes] = React.useState(42);
|
|
2104
|
+
|
|
2105
|
+
<Card style={{ padding: 0 }}>
|
|
2106
|
+
{/* Header */}
|
|
2107
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, padding: 16 }}>
|
|
2108
|
+
<Avatar fallback="EW" size="3" color="purple" />
|
|
2109
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
2110
|
+
<Text weight="medium">Emma Wilson</Text>
|
|
2111
|
+
<Text size="1" color="gray">
|
|
2112
|
+
2 hours ago
|
|
2113
|
+
</Text>
|
|
2114
|
+
</View>
|
|
2115
|
+
<IconButton variant="ghost" size="2">
|
|
2116
|
+
<Icon as={MoreHorizontal} size={18} />
|
|
2117
|
+
</IconButton>
|
|
2118
|
+
</View>
|
|
2119
|
+
|
|
2120
|
+
{/* Content */}
|
|
2121
|
+
<View style={{ paddingHorizontal: 16, paddingBottom: 12 }}>
|
|
2122
|
+
<Text size="3">
|
|
2123
|
+
Just finished my morning run! 🏃♀️ Nothing beats starting the day with some exercise.
|
|
2124
|
+
</Text>
|
|
2125
|
+
</View>
|
|
2126
|
+
|
|
2127
|
+
{/* Image placeholder */}
|
|
2128
|
+
<View
|
|
2129
|
+
style={{
|
|
2130
|
+
height: 200,
|
|
2131
|
+
backgroundColor: colors.palettes.gray.a3,
|
|
2132
|
+
alignItems: 'center',
|
|
2133
|
+
justifyContent: 'center',
|
|
2134
|
+
}}>
|
|
2135
|
+
<Icon as={MapPin} size={48} color={colors.palettes.gray.a8} />
|
|
2136
|
+
</View>
|
|
2137
|
+
|
|
2138
|
+
{/* Actions */}
|
|
2139
|
+
<View style={{ flexDirection: 'row', padding: 12, gap: 16 }}>
|
|
2140
|
+
<Button
|
|
2141
|
+
variant="ghost"
|
|
2142
|
+
size="2"
|
|
2143
|
+
color={liked ? 'danger' : 'gray'}
|
|
2144
|
+
onPress={() => {
|
|
2145
|
+
setLiked(!liked);
|
|
2146
|
+
setLikes(liked ? likes - 1 : likes + 1);
|
|
2147
|
+
}}>
|
|
2148
|
+
<Icon as={Heart} size={18} />
|
|
2149
|
+
<Text>{likes}</Text>
|
|
2150
|
+
</Button>
|
|
2151
|
+
<Button variant="ghost" size="2" color="gray">
|
|
2152
|
+
<Icon as={MessageCircle} size={18} />
|
|
2153
|
+
<Text>12</Text>
|
|
2154
|
+
</Button>
|
|
2155
|
+
<Button variant="ghost" size="2" color="gray">
|
|
2156
|
+
<Icon as={Share} size={18} />
|
|
2157
|
+
</Button>
|
|
2158
|
+
</View>
|
|
2159
|
+
</Card>;
|
|
2160
|
+
```
|
|
2161
|
+
|
|
2162
|
+
---
|
|
2163
|
+
|
|
2164
|
+
## Gamification Patterns
|
|
2165
|
+
|
|
2166
|
+
### Streak Counter
|
|
2167
|
+
|
|
2168
|
+
```tsx
|
|
2169
|
+
<Card>
|
|
2170
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16 }}>
|
|
2171
|
+
<View
|
|
2172
|
+
style={{
|
|
2173
|
+
width: 56,
|
|
2174
|
+
height: 56,
|
|
2175
|
+
borderRadius: 14,
|
|
2176
|
+
backgroundColor: colors.palettes.orange.a3,
|
|
2177
|
+
alignItems: 'center',
|
|
2178
|
+
justifyContent: 'center',
|
|
2179
|
+
}}>
|
|
2180
|
+
<Icon as={Flame} size={28} color={colors.palettes.orange['9']} />
|
|
2181
|
+
</View>
|
|
2182
|
+
<View style={{ flex: 1, gap: 4 }}>
|
|
2183
|
+
<View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 4 }}>
|
|
2184
|
+
<Text size="6" weight="bold">
|
|
2185
|
+
7
|
|
2186
|
+
</Text>
|
|
2187
|
+
<Text size="3" weight="medium">
|
|
2188
|
+
Day Streak
|
|
2189
|
+
</Text>
|
|
2190
|
+
</View>
|
|
2191
|
+
<Text size="2" color="gray">
|
|
2192
|
+
Keep it up! You're on fire 🔥
|
|
2193
|
+
</Text>
|
|
2194
|
+
</View>
|
|
2195
|
+
</View>
|
|
2196
|
+
</Card>
|
|
2197
|
+
```
|
|
2198
|
+
|
|
2199
|
+
### Leaderboard
|
|
2200
|
+
|
|
2201
|
+
```tsx
|
|
2202
|
+
const entries = [
|
|
2203
|
+
{ rank: 1, name: 'Sarah Chen', points: 12450, avatar: 'SC', color: 'pink' },
|
|
2204
|
+
{ rank: 2, name: 'Alex Kim', points: 11200, avatar: 'AK', color: 'blue' },
|
|
2205
|
+
{ rank: 3, name: 'Jordan Lee', points: 10890, avatar: 'JL', color: 'green' },
|
|
2206
|
+
{ rank: 4, name: 'You', points: 9540, avatar: 'ME', color: 'accent', isUser: true },
|
|
2207
|
+
];
|
|
2208
|
+
|
|
2209
|
+
<List.Root variant="soft">
|
|
2210
|
+
{entries.map((entry, index) => (
|
|
2211
|
+
<React.Fragment key={entry.rank}>
|
|
2212
|
+
{index > 0 && <List.Separator />}
|
|
2213
|
+
<List.Item style={entry.isUser ? { backgroundColor: colors.palettes.gray.a3 } : undefined}>
|
|
2214
|
+
<List.ItemSlot>
|
|
2215
|
+
<Text
|
|
2216
|
+
size="2"
|
|
2217
|
+
weight="bold"
|
|
2218
|
+
style={{ width: 24, textAlign: 'center' }}
|
|
2219
|
+
color={entry.rank <= 3 ? undefined : 'gray'}>
|
|
2220
|
+
{entry.rank}
|
|
2221
|
+
</Text>
|
|
2222
|
+
</List.ItemSlot>
|
|
2223
|
+
<List.ItemSlot>
|
|
2224
|
+
<Avatar fallback={entry.avatar} size="2" color={entry.color} />
|
|
2225
|
+
</List.ItemSlot>
|
|
2226
|
+
<List.ItemContent>
|
|
2227
|
+
<Text weight={entry.isUser ? 'bold' : 'medium'}>{entry.name}</Text>
|
|
2228
|
+
</List.ItemContent>
|
|
2229
|
+
<List.ItemSlot>
|
|
2230
|
+
<Text weight="medium" color={entry.color}>
|
|
2231
|
+
{entry.points.toLocaleString()} pts
|
|
2232
|
+
</Text>
|
|
2233
|
+
</List.ItemSlot>
|
|
2234
|
+
</List.Item>
|
|
2235
|
+
</React.Fragment>
|
|
2236
|
+
))}
|
|
2237
|
+
</List.Root>;
|
|
2238
|
+
```
|
|
2239
|
+
|
|
2240
|
+
### XP Progress
|
|
2241
|
+
|
|
2242
|
+
```tsx
|
|
2243
|
+
<Card>
|
|
2244
|
+
<View style={{ gap: 12 }}>
|
|
2245
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
2246
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
2247
|
+
<View
|
|
2248
|
+
style={{
|
|
2249
|
+
width: 32,
|
|
2250
|
+
height: 32,
|
|
2251
|
+
borderRadius: 8,
|
|
2252
|
+
backgroundColor: colors.palettes.purple['9'],
|
|
2253
|
+
alignItems: 'center',
|
|
2254
|
+
justifyContent: 'center',
|
|
2255
|
+
}}>
|
|
2256
|
+
<Text size="2" weight="bold" style={{ color: 'white' }}>
|
|
2257
|
+
12
|
|
2258
|
+
</Text>
|
|
2259
|
+
</View>
|
|
2260
|
+
<Text weight="medium">Level 12</Text>
|
|
2261
|
+
</View>
|
|
2262
|
+
<Badge color="purple" size="1">
|
|
2263
|
+
<Icon as={Sparkles} size={10} />
|
|
2264
|
+
<Text>250 XP to go</Text>
|
|
2265
|
+
</Badge>
|
|
2266
|
+
</View>
|
|
2267
|
+
<Progress value={75} size="2" color="purple" />
|
|
2268
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
2269
|
+
<Text size="1" color="gray">
|
|
2270
|
+
750 / 1,000 XP
|
|
2271
|
+
</Text>
|
|
2272
|
+
<Text size="1" color="gray">
|
|
2273
|
+
Next: Level 13
|
|
2274
|
+
</Text>
|
|
2275
|
+
</View>
|
|
2276
|
+
</View>
|
|
2277
|
+
</Card>
|
|
2278
|
+
```
|
|
2279
|
+
|
|
2280
|
+
### Daily Challenge
|
|
2281
|
+
|
|
2282
|
+
```tsx
|
|
2283
|
+
<Card>
|
|
2284
|
+
<View style={{ gap: 12 }}>
|
|
2285
|
+
<View
|
|
2286
|
+
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
|
2287
|
+
<View style={{ flexDirection: 'row', gap: 12 }}>
|
|
2288
|
+
<View
|
|
2289
|
+
style={{
|
|
2290
|
+
width: 48,
|
|
2291
|
+
height: 48,
|
|
2292
|
+
borderRadius: 12,
|
|
2293
|
+
backgroundColor: colors.palettes.cyan.a3,
|
|
2294
|
+
alignItems: 'center',
|
|
2295
|
+
justifyContent: 'center',
|
|
2296
|
+
}}>
|
|
2297
|
+
<Icon as={Timer} size={24} color={colors.palettes.cyan.a11} />
|
|
2298
|
+
</View>
|
|
2299
|
+
<View style={{ gap: 2 }}>
|
|
2300
|
+
<Text size="1" color="gray" weight="medium">
|
|
2301
|
+
DAILY CHALLENGE
|
|
2302
|
+
</Text>
|
|
2303
|
+
<Text weight="medium">Complete 5 lessons</Text>
|
|
2304
|
+
</View>
|
|
2305
|
+
</View>
|
|
2306
|
+
<Badge color="amber" size="1">
|
|
2307
|
+
<Icon as={Gift} size={10} />
|
|
2308
|
+
<Text>+50 XP</Text>
|
|
2309
|
+
</Badge>
|
|
2310
|
+
</View>
|
|
2311
|
+
<Progress value={60} size="2" color="cyan" />
|
|
2312
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
2313
|
+
<Text size="1" color="gray">
|
|
2314
|
+
3 of 5 completed
|
|
2315
|
+
</Text>
|
|
2316
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
2317
|
+
<Icon as={Clock} size={12} color={colors.palettes.gray.a11} />
|
|
2318
|
+
<Text size="1" color="gray">
|
|
2319
|
+
8h remaining
|
|
2320
|
+
</Text>
|
|
2321
|
+
</View>
|
|
2322
|
+
</View>
|
|
2323
|
+
</View>
|
|
2324
|
+
</Card>
|
|
2325
|
+
```
|
|
2326
|
+
|
|
2327
|
+
---
|
|
2328
|
+
|
|
2329
|
+
## Media Patterns
|
|
2330
|
+
|
|
2331
|
+
### Now Playing (Music Player)
|
|
2332
|
+
|
|
2333
|
+
```tsx
|
|
2334
|
+
const [isPlaying, setIsPlaying] = React.useState(true);
|
|
2335
|
+
|
|
2336
|
+
<Card>
|
|
2337
|
+
<View style={{ gap: 16 }}>
|
|
2338
|
+
{/* Album Art + Info */}
|
|
2339
|
+
<View style={{ flexDirection: 'row', gap: 16 }}>
|
|
2340
|
+
<View
|
|
2341
|
+
style={{
|
|
2342
|
+
width: 80,
|
|
2343
|
+
height: 80,
|
|
2344
|
+
borderRadius: 8,
|
|
2345
|
+
backgroundColor: colors.palettes.pink.a3,
|
|
2346
|
+
alignItems: 'center',
|
|
2347
|
+
justifyContent: 'center',
|
|
2348
|
+
}}>
|
|
2349
|
+
<Icon as={Music} size={32} color={colors.palettes.pink.a11} />
|
|
2350
|
+
</View>
|
|
2351
|
+
<View style={{ flex: 1, justifyContent: 'center', gap: 4 }}>
|
|
2352
|
+
<Text size="3" weight="bold" numberOfLines={1}>
|
|
2353
|
+
Midnight Dreams
|
|
2354
|
+
</Text>
|
|
2355
|
+
<Text size="2" color="gray" numberOfLines={1}>
|
|
2356
|
+
Aurora Synth
|
|
2357
|
+
</Text>
|
|
2358
|
+
<Text size="1" color="gray">
|
|
2359
|
+
Neon Horizons • 2024
|
|
2360
|
+
</Text>
|
|
2361
|
+
</View>
|
|
2362
|
+
<IconButton variant="ghost" size="2">
|
|
2363
|
+
<Icon as={Heart} size={20} />
|
|
2364
|
+
</IconButton>
|
|
2365
|
+
</View>
|
|
2366
|
+
|
|
2367
|
+
{/* Progress */}
|
|
2368
|
+
<View style={{ gap: 8 }}>
|
|
2369
|
+
<Progress value={35} size="1" />
|
|
2370
|
+
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
2371
|
+
<Text size="0" color="gray">
|
|
2372
|
+
1:24
|
|
2373
|
+
</Text>
|
|
2374
|
+
<Text size="0" color="gray">
|
|
2375
|
+
3:45
|
|
2376
|
+
</Text>
|
|
2377
|
+
</View>
|
|
2378
|
+
</View>
|
|
2379
|
+
|
|
2380
|
+
{/* Controls */}
|
|
2381
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 24 }}>
|
|
2382
|
+
<IconButton variant="ghost" size="3">
|
|
2383
|
+
<Icon as={SkipBack} size={24} />
|
|
2384
|
+
</IconButton>
|
|
2385
|
+
<IconButton variant="solid" size="4" onPress={() => setIsPlaying(!isPlaying)}>
|
|
2386
|
+
<Icon as={isPlaying ? Pause : Play} size={24} />
|
|
2387
|
+
</IconButton>
|
|
2388
|
+
<IconButton variant="ghost" size="3">
|
|
2389
|
+
<Icon as={SkipForward} size={24} />
|
|
2390
|
+
</IconButton>
|
|
2391
|
+
</View>
|
|
2392
|
+
</View>
|
|
2393
|
+
</Card>;
|
|
2394
|
+
```
|
|
2395
|
+
|
|
2396
|
+
### Poll Card
|
|
2397
|
+
|
|
2398
|
+
```tsx
|
|
2399
|
+
const [voted, setVoted] = React.useState(null);
|
|
2400
|
+
|
|
2401
|
+
const options = [
|
|
2402
|
+
{ id: 'react', label: 'React Native', votes: 156 },
|
|
2403
|
+
{ id: 'flutter', label: 'Flutter', votes: 89 },
|
|
2404
|
+
{ id: 'native', label: 'Native (Swift/Kotlin)', votes: 67 },
|
|
2405
|
+
];
|
|
2406
|
+
|
|
2407
|
+
const totalVotes = options.reduce((sum, opt) => sum + opt.votes, 0);
|
|
2408
|
+
|
|
2409
|
+
<Card>
|
|
2410
|
+
<View style={{ gap: 16 }}>
|
|
2411
|
+
<View style={{ gap: 8 }}>
|
|
2412
|
+
<Text size="3" weight="medium">
|
|
2413
|
+
What's your preferred mobile framework?
|
|
2414
|
+
</Text>
|
|
2415
|
+
<Text size="1" color="gray">
|
|
2416
|
+
{totalVotes} votes • 2 days left
|
|
2417
|
+
</Text>
|
|
2418
|
+
</View>
|
|
2419
|
+
|
|
2420
|
+
<View style={{ gap: 8 }}>
|
|
2421
|
+
{options.map((option) => {
|
|
2422
|
+
const percentage = Math.round((option.votes / totalVotes) * 100);
|
|
2423
|
+
const isSelected = voted === option.id;
|
|
2424
|
+
|
|
2425
|
+
return (
|
|
2426
|
+
<Pressable
|
|
2427
|
+
key={option.id}
|
|
2428
|
+
onPress={() => !voted && setVoted(option.id)}
|
|
2429
|
+
style={{
|
|
2430
|
+
borderRadius: 8,
|
|
2431
|
+
overflow: 'hidden',
|
|
2432
|
+
borderWidth: 1,
|
|
2433
|
+
borderColor: isSelected ? colors.palettes.accent['8'] : colors.stroke,
|
|
2434
|
+
}}>
|
|
2435
|
+
{/* Progress background */}
|
|
2436
|
+
<View
|
|
2437
|
+
style={{
|
|
2438
|
+
position: 'absolute',
|
|
2439
|
+
left: 0,
|
|
2440
|
+
top: 0,
|
|
2441
|
+
bottom: 0,
|
|
2442
|
+
width: voted ? `${percentage}%` : '0%',
|
|
2443
|
+
backgroundColor: isSelected ? colors.palettes.accent.a3 : colors.palettes.gray.a3,
|
|
2444
|
+
}}
|
|
2445
|
+
/>
|
|
2446
|
+
<View
|
|
2447
|
+
style={{
|
|
2448
|
+
flexDirection: 'row',
|
|
2449
|
+
alignItems: 'center',
|
|
2450
|
+
justifyContent: 'space-between',
|
|
2451
|
+
padding: 12,
|
|
2452
|
+
}}>
|
|
2453
|
+
<Text weight={isSelected ? 'medium' : 'regular'}>{option.label}</Text>
|
|
2454
|
+
{voted && (
|
|
2455
|
+
<Text size="2" weight="medium">
|
|
2456
|
+
{percentage}%
|
|
2457
|
+
</Text>
|
|
2458
|
+
)}
|
|
2459
|
+
</View>
|
|
2460
|
+
</Pressable>
|
|
2461
|
+
);
|
|
2462
|
+
})}
|
|
2463
|
+
</View>
|
|
2464
|
+
</View>
|
|
2465
|
+
</Card>;
|
|
2466
|
+
```
|