@metacells/mcellui-mcp-server 0.1.0 → 0.1.2
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/dist/index.js +70 -7
- package/package.json +7 -5
- package/registry/registry.json +717 -0
- package/registry/ui/accordion.tsx +416 -0
- package/registry/ui/action-sheet.tsx +396 -0
- package/registry/ui/alert-dialog.tsx +355 -0
- package/registry/ui/avatar-stack.tsx +278 -0
- package/registry/ui/avatar.tsx +116 -0
- package/registry/ui/badge.tsx +125 -0
- package/registry/ui/button.tsx +240 -0
- package/registry/ui/card.tsx +675 -0
- package/registry/ui/carousel.tsx +431 -0
- package/registry/ui/checkbox.tsx +252 -0
- package/registry/ui/chip.tsx +271 -0
- package/registry/ui/column.tsx +133 -0
- package/registry/ui/datetime-picker.tsx +578 -0
- package/registry/ui/dialog.tsx +292 -0
- package/registry/ui/fab.tsx +225 -0
- package/registry/ui/form.tsx +323 -0
- package/registry/ui/horizontal-list.tsx +200 -0
- package/registry/ui/icon-button.tsx +244 -0
- package/registry/ui/image-gallery.tsx +455 -0
- package/registry/ui/image.tsx +283 -0
- package/registry/ui/input.tsx +242 -0
- package/registry/ui/label.tsx +99 -0
- package/registry/ui/list.tsx +519 -0
- package/registry/ui/progress.tsx +168 -0
- package/registry/ui/pull-to-refresh.tsx +231 -0
- package/registry/ui/radio-group.tsx +294 -0
- package/registry/ui/rating.tsx +311 -0
- package/registry/ui/row.tsx +136 -0
- package/registry/ui/screen.tsx +153 -0
- package/registry/ui/search-input.tsx +281 -0
- package/registry/ui/section-header.tsx +258 -0
- package/registry/ui/segmented-control.tsx +229 -0
- package/registry/ui/select.tsx +311 -0
- package/registry/ui/separator.tsx +74 -0
- package/registry/ui/sheet.tsx +362 -0
- package/registry/ui/skeleton.tsx +156 -0
- package/registry/ui/slider.tsx +307 -0
- package/registry/ui/spinner.tsx +100 -0
- package/registry/ui/stepper.tsx +314 -0
- package/registry/ui/stories.tsx +463 -0
- package/registry/ui/swipeable-row.tsx +362 -0
- package/registry/ui/switch.tsx +246 -0
- package/registry/ui/tabs.tsx +348 -0
- package/registry/ui/textarea.tsx +265 -0
- package/registry/ui/toast.tsx +316 -0
- package/registry/ui/tooltip.tsx +369 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List & ListItem
|
|
3
|
+
*
|
|
4
|
+
* A flexible list component with slots for left/right content,
|
|
5
|
+
* chevron indicators, dividers, and pressable support.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // Basic usage
|
|
10
|
+
* <List>
|
|
11
|
+
* <ListItem title="Settings" />
|
|
12
|
+
* <ListItem title="Profile" subtitle="John Doe" />
|
|
13
|
+
* </List>
|
|
14
|
+
*
|
|
15
|
+
* // With icons and chevron
|
|
16
|
+
* <List>
|
|
17
|
+
* <ListItem
|
|
18
|
+
* title="Notifications"
|
|
19
|
+
* left={<BellIcon />}
|
|
20
|
+
* showChevron
|
|
21
|
+
* onPress={handlePress}
|
|
22
|
+
* />
|
|
23
|
+
* </List>
|
|
24
|
+
*
|
|
25
|
+
* // With right content
|
|
26
|
+
* <List>
|
|
27
|
+
* <ListItem
|
|
28
|
+
* title="Dark Mode"
|
|
29
|
+
* right={<Switch value={isDark} onValueChange={setIsDark} />}
|
|
30
|
+
* />
|
|
31
|
+
* </List>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import React, { createContext, useContext } from 'react';
|
|
36
|
+
import {
|
|
37
|
+
View,
|
|
38
|
+
Text,
|
|
39
|
+
Pressable,
|
|
40
|
+
StyleSheet,
|
|
41
|
+
ViewStyle,
|
|
42
|
+
TextStyle,
|
|
43
|
+
Image,
|
|
44
|
+
ImageSourcePropType,
|
|
45
|
+
} from 'react-native';
|
|
46
|
+
import Animated, {
|
|
47
|
+
useSharedValue,
|
|
48
|
+
useAnimatedStyle,
|
|
49
|
+
withSpring,
|
|
50
|
+
} from 'react-native-reanimated';
|
|
51
|
+
import Svg, { Path } from 'react-native-svg';
|
|
52
|
+
import { useTheme } from '@nativeui/core';
|
|
53
|
+
import { haptic } from '@nativeui/core';
|
|
54
|
+
|
|
55
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
// Types
|
|
59
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export interface ListProps {
|
|
62
|
+
/** List items */
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
/** Show dividers between items */
|
|
65
|
+
showDividers?: boolean;
|
|
66
|
+
/** Inset dividers from left edge */
|
|
67
|
+
insetDividers?: boolean;
|
|
68
|
+
/** Container style */
|
|
69
|
+
style?: ViewStyle;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Metadata item for thumbnail variant */
|
|
73
|
+
export interface ListItemMetadata {
|
|
74
|
+
/** Icon to display */
|
|
75
|
+
icon: React.ReactNode;
|
|
76
|
+
/** Label text */
|
|
77
|
+
label: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ListItemProps {
|
|
81
|
+
/** Visual variant */
|
|
82
|
+
variant?: 'default' | 'thumbnail';
|
|
83
|
+
/** Primary text */
|
|
84
|
+
title: string;
|
|
85
|
+
/** Secondary text below title (used as subtitle in default, description in thumbnail) */
|
|
86
|
+
subtitle?: string;
|
|
87
|
+
/** Longer description text (thumbnail variant only) */
|
|
88
|
+
description?: string;
|
|
89
|
+
/** Thumbnail image source (thumbnail variant only) */
|
|
90
|
+
thumbnail?: ImageSourcePropType;
|
|
91
|
+
/** Thumbnail size (thumbnail variant only, default: 100) */
|
|
92
|
+
thumbnailSize?: number;
|
|
93
|
+
/** Metadata items with icon and label (thumbnail variant only) */
|
|
94
|
+
metadata?: ListItemMetadata[];
|
|
95
|
+
/** Content to render on the left (icon, avatar, etc.) - default variant only */
|
|
96
|
+
left?: React.ReactNode;
|
|
97
|
+
/** Content to render on the right (switch, badge, etc.) */
|
|
98
|
+
right?: React.ReactNode;
|
|
99
|
+
/** Show chevron indicator on the right */
|
|
100
|
+
showChevron?: boolean;
|
|
101
|
+
/** Press handler - makes item pressable */
|
|
102
|
+
onPress?: () => void;
|
|
103
|
+
/** Long press handler */
|
|
104
|
+
onLongPress?: () => void;
|
|
105
|
+
/** Disabled state */
|
|
106
|
+
disabled?: boolean;
|
|
107
|
+
/** Container style */
|
|
108
|
+
style?: ViewStyle;
|
|
109
|
+
/** Title text style */
|
|
110
|
+
titleStyle?: TextStyle;
|
|
111
|
+
/** Subtitle text style */
|
|
112
|
+
subtitleStyle?: TextStyle;
|
|
113
|
+
/** Description text style (thumbnail variant only) */
|
|
114
|
+
descriptionStyle?: TextStyle;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
// Context
|
|
119
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
interface ListContextValue {
|
|
122
|
+
showDividers: boolean;
|
|
123
|
+
insetDividers: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ListContext = createContext<ListContextValue>({
|
|
127
|
+
showDividers: true,
|
|
128
|
+
insetDividers: false,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
// Icons
|
|
133
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function ChevronRightIcon({ color, size = 20 }: { color: string; size?: number }) {
|
|
136
|
+
return (
|
|
137
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
|
|
138
|
+
<Path
|
|
139
|
+
d="M9 18l6-6-6-6"
|
|
140
|
+
stroke={color}
|
|
141
|
+
strokeWidth="2"
|
|
142
|
+
strokeLinecap="round"
|
|
143
|
+
strokeLinejoin="round"
|
|
144
|
+
/>
|
|
145
|
+
</Svg>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// List Component
|
|
151
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export function List({
|
|
154
|
+
children,
|
|
155
|
+
showDividers = true,
|
|
156
|
+
insetDividers = false,
|
|
157
|
+
style,
|
|
158
|
+
}: ListProps) {
|
|
159
|
+
const { colors, radius } = useTheme();
|
|
160
|
+
|
|
161
|
+
const childArray = React.Children.toArray(children);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<ListContext.Provider value={{ showDividers, insetDividers }}>
|
|
165
|
+
<View
|
|
166
|
+
style={[
|
|
167
|
+
styles.list,
|
|
168
|
+
{
|
|
169
|
+
backgroundColor: colors.card,
|
|
170
|
+
borderRadius: radius.lg,
|
|
171
|
+
borderWidth: 1,
|
|
172
|
+
borderColor: colors.border,
|
|
173
|
+
},
|
|
174
|
+
style,
|
|
175
|
+
]}
|
|
176
|
+
>
|
|
177
|
+
{childArray.map((child, index) => (
|
|
178
|
+
<React.Fragment key={index}>
|
|
179
|
+
{child}
|
|
180
|
+
{showDividers && index < childArray.length - 1 && (
|
|
181
|
+
<View
|
|
182
|
+
style={[
|
|
183
|
+
styles.divider,
|
|
184
|
+
{
|
|
185
|
+
backgroundColor: colors.border,
|
|
186
|
+
marginLeft: insetDividers ? 56 : 0,
|
|
187
|
+
},
|
|
188
|
+
]}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
</React.Fragment>
|
|
192
|
+
))}
|
|
193
|
+
</View>
|
|
194
|
+
</ListContext.Provider>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
// ListItem Component
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export function ListItem({
|
|
203
|
+
variant = 'default',
|
|
204
|
+
title,
|
|
205
|
+
subtitle,
|
|
206
|
+
description,
|
|
207
|
+
thumbnail,
|
|
208
|
+
thumbnailSize = 100,
|
|
209
|
+
metadata,
|
|
210
|
+
left,
|
|
211
|
+
right,
|
|
212
|
+
showChevron = false,
|
|
213
|
+
onPress,
|
|
214
|
+
onLongPress,
|
|
215
|
+
disabled = false,
|
|
216
|
+
style,
|
|
217
|
+
titleStyle,
|
|
218
|
+
subtitleStyle,
|
|
219
|
+
descriptionStyle,
|
|
220
|
+
}: ListItemProps) {
|
|
221
|
+
const { colors, spacing, fontSize, fontWeight, radius, springs } = useTheme();
|
|
222
|
+
const scale = useSharedValue(1);
|
|
223
|
+
|
|
224
|
+
const isPressable = !!onPress || !!onLongPress;
|
|
225
|
+
|
|
226
|
+
const handlePressIn = () => {
|
|
227
|
+
if (isPressable && !disabled) {
|
|
228
|
+
scale.value = withSpring(0.98, springs.snappy);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const handlePressOut = () => {
|
|
233
|
+
if (isPressable && !disabled) {
|
|
234
|
+
scale.value = withSpring(1, springs.snappy);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const handlePress = () => {
|
|
239
|
+
if (!disabled) {
|
|
240
|
+
haptic('light');
|
|
241
|
+
onPress?.();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleLongPress = () => {
|
|
246
|
+
if (!disabled) {
|
|
247
|
+
haptic('medium');
|
|
248
|
+
onLongPress?.();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
253
|
+
transform: [{ scale: scale.value }],
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
// Thumbnail variant content
|
|
257
|
+
const thumbnailContent = (
|
|
258
|
+
<>
|
|
259
|
+
{/* Thumbnail Image */}
|
|
260
|
+
{thumbnail && (
|
|
261
|
+
<Image
|
|
262
|
+
source={thumbnail}
|
|
263
|
+
style={[
|
|
264
|
+
styles.thumbnail,
|
|
265
|
+
{
|
|
266
|
+
width: thumbnailSize,
|
|
267
|
+
height: thumbnailSize,
|
|
268
|
+
borderRadius: radius.md,
|
|
269
|
+
marginRight: spacing[3],
|
|
270
|
+
},
|
|
271
|
+
]}
|
|
272
|
+
resizeMode="cover"
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* Content */}
|
|
277
|
+
<View style={styles.thumbnailContent}>
|
|
278
|
+
<Text
|
|
279
|
+
style={[
|
|
280
|
+
styles.title,
|
|
281
|
+
{
|
|
282
|
+
color: disabled ? colors.foregroundMuted : colors.foreground,
|
|
283
|
+
fontSize: fontSize.base,
|
|
284
|
+
fontWeight: fontWeight.semibold,
|
|
285
|
+
},
|
|
286
|
+
titleStyle,
|
|
287
|
+
]}
|
|
288
|
+
numberOfLines={1}
|
|
289
|
+
>
|
|
290
|
+
{title}
|
|
291
|
+
</Text>
|
|
292
|
+
{(description || subtitle) && (
|
|
293
|
+
<Text
|
|
294
|
+
style={[
|
|
295
|
+
styles.description,
|
|
296
|
+
{
|
|
297
|
+
color: colors.foregroundMuted,
|
|
298
|
+
fontSize: fontSize.sm,
|
|
299
|
+
marginTop: spacing[1],
|
|
300
|
+
lineHeight: fontSize.sm * 1.4,
|
|
301
|
+
},
|
|
302
|
+
descriptionStyle,
|
|
303
|
+
]}
|
|
304
|
+
numberOfLines={2}
|
|
305
|
+
>
|
|
306
|
+
{description || subtitle}
|
|
307
|
+
</Text>
|
|
308
|
+
)}
|
|
309
|
+
{metadata && metadata.length > 0 && (
|
|
310
|
+
<View style={[styles.metadataRow, { marginTop: spacing[2], gap: spacing[3] }]}>
|
|
311
|
+
{metadata.map((item, index) => (
|
|
312
|
+
<View key={index} style={[styles.metadataItem, { gap: spacing[1] }]}>
|
|
313
|
+
{React.isValidElement(item.icon)
|
|
314
|
+
? React.cloneElement(item.icon as React.ReactElement<{ color?: string; width?: number; height?: number }>, {
|
|
315
|
+
color: colors.foregroundMuted,
|
|
316
|
+
width: 14,
|
|
317
|
+
height: 14,
|
|
318
|
+
})
|
|
319
|
+
: item.icon}
|
|
320
|
+
<Text
|
|
321
|
+
style={{
|
|
322
|
+
color: colors.foregroundMuted,
|
|
323
|
+
fontSize: fontSize.xs,
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
{item.label}
|
|
327
|
+
</Text>
|
|
328
|
+
</View>
|
|
329
|
+
))}
|
|
330
|
+
</View>
|
|
331
|
+
)}
|
|
332
|
+
</View>
|
|
333
|
+
|
|
334
|
+
{/* Right Slot */}
|
|
335
|
+
{right && (
|
|
336
|
+
<View style={[styles.rightSlot, { marginLeft: spacing[2] }]}>
|
|
337
|
+
{right}
|
|
338
|
+
</View>
|
|
339
|
+
)}
|
|
340
|
+
|
|
341
|
+
{/* Chevron */}
|
|
342
|
+
{showChevron && (
|
|
343
|
+
<View style={[styles.chevron, { marginLeft: spacing[2] }]}>
|
|
344
|
+
<ChevronRightIcon color={colors.foregroundMuted} />
|
|
345
|
+
</View>
|
|
346
|
+
)}
|
|
347
|
+
</>
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
// Default variant content
|
|
351
|
+
const defaultContent = (
|
|
352
|
+
<>
|
|
353
|
+
{/* Left Slot */}
|
|
354
|
+
{left && (
|
|
355
|
+
<View style={[styles.leftSlot, { marginRight: spacing[3] }]}>
|
|
356
|
+
{React.isValidElement(left)
|
|
357
|
+
? React.cloneElement(left as React.ReactElement<{ color?: string; width?: number; height?: number }>, {
|
|
358
|
+
color: colors.foreground,
|
|
359
|
+
width: 24,
|
|
360
|
+
height: 24,
|
|
361
|
+
})
|
|
362
|
+
: left}
|
|
363
|
+
</View>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{/* Content */}
|
|
367
|
+
<View style={styles.content}>
|
|
368
|
+
<Text
|
|
369
|
+
style={[
|
|
370
|
+
styles.title,
|
|
371
|
+
{
|
|
372
|
+
color: disabled ? colors.foregroundMuted : colors.foreground,
|
|
373
|
+
fontSize: fontSize.base,
|
|
374
|
+
fontWeight: fontWeight.medium,
|
|
375
|
+
},
|
|
376
|
+
titleStyle,
|
|
377
|
+
]}
|
|
378
|
+
numberOfLines={1}
|
|
379
|
+
>
|
|
380
|
+
{title}
|
|
381
|
+
</Text>
|
|
382
|
+
{subtitle && (
|
|
383
|
+
<Text
|
|
384
|
+
style={[
|
|
385
|
+
styles.subtitle,
|
|
386
|
+
{
|
|
387
|
+
color: colors.foregroundMuted,
|
|
388
|
+
fontSize: fontSize.sm,
|
|
389
|
+
marginTop: spacing[0.5],
|
|
390
|
+
},
|
|
391
|
+
subtitleStyle,
|
|
392
|
+
]}
|
|
393
|
+
numberOfLines={2}
|
|
394
|
+
>
|
|
395
|
+
{subtitle}
|
|
396
|
+
</Text>
|
|
397
|
+
)}
|
|
398
|
+
</View>
|
|
399
|
+
|
|
400
|
+
{/* Right Slot */}
|
|
401
|
+
{right && (
|
|
402
|
+
<View style={[styles.rightSlot, { marginLeft: spacing[3] }]}>
|
|
403
|
+
{right}
|
|
404
|
+
</View>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* Chevron */}
|
|
408
|
+
{showChevron && (
|
|
409
|
+
<View style={[styles.chevron, { marginLeft: spacing[2] }]}>
|
|
410
|
+
<ChevronRightIcon color={colors.foregroundMuted} />
|
|
411
|
+
</View>
|
|
412
|
+
)}
|
|
413
|
+
</>
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const content = variant === 'thumbnail' ? thumbnailContent : defaultContent;
|
|
417
|
+
|
|
418
|
+
const containerStyle = [
|
|
419
|
+
styles.item,
|
|
420
|
+
{
|
|
421
|
+
paddingHorizontal: spacing[4],
|
|
422
|
+
paddingVertical: spacing[3],
|
|
423
|
+
minHeight: variant === 'thumbnail' ? thumbnailSize + spacing[3] * 2 : 56,
|
|
424
|
+
},
|
|
425
|
+
variant === 'thumbnail' && styles.thumbnailItem,
|
|
426
|
+
disabled && styles.disabled,
|
|
427
|
+
style,
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
if (isPressable) {
|
|
431
|
+
return (
|
|
432
|
+
<AnimatedPressable
|
|
433
|
+
style={[containerStyle, animatedStyle]}
|
|
434
|
+
onPressIn={handlePressIn}
|
|
435
|
+
onPressOut={handlePressOut}
|
|
436
|
+
onPress={handlePress}
|
|
437
|
+
onLongPress={onLongPress ? handleLongPress : undefined}
|
|
438
|
+
disabled={disabled}
|
|
439
|
+
accessibilityRole="button"
|
|
440
|
+
accessibilityLabel={title}
|
|
441
|
+
accessibilityHint={description || subtitle}
|
|
442
|
+
accessibilityState={{ disabled }}
|
|
443
|
+
>
|
|
444
|
+
{content}
|
|
445
|
+
</AnimatedPressable>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<View style={containerStyle} accessibilityRole="text" accessibilityLabel={title}>
|
|
451
|
+
{content}
|
|
452
|
+
</View>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
457
|
+
// Styles
|
|
458
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
const styles = StyleSheet.create({
|
|
461
|
+
list: {
|
|
462
|
+
overflow: 'hidden',
|
|
463
|
+
},
|
|
464
|
+
item: {
|
|
465
|
+
flexDirection: 'row',
|
|
466
|
+
alignItems: 'center',
|
|
467
|
+
},
|
|
468
|
+
thumbnailItem: {
|
|
469
|
+
alignItems: 'flex-start',
|
|
470
|
+
},
|
|
471
|
+
leftSlot: {
|
|
472
|
+
flexShrink: 0,
|
|
473
|
+
alignItems: 'center',
|
|
474
|
+
justifyContent: 'center',
|
|
475
|
+
},
|
|
476
|
+
content: {
|
|
477
|
+
flex: 1,
|
|
478
|
+
justifyContent: 'center',
|
|
479
|
+
},
|
|
480
|
+
thumbnailContent: {
|
|
481
|
+
flex: 1,
|
|
482
|
+
justifyContent: 'flex-start',
|
|
483
|
+
},
|
|
484
|
+
thumbnail: {
|
|
485
|
+
flexShrink: 0,
|
|
486
|
+
},
|
|
487
|
+
title: {
|
|
488
|
+
// Dynamic styles applied inline
|
|
489
|
+
},
|
|
490
|
+
subtitle: {
|
|
491
|
+
// Dynamic styles applied inline
|
|
492
|
+
},
|
|
493
|
+
description: {
|
|
494
|
+
// Dynamic styles applied inline
|
|
495
|
+
},
|
|
496
|
+
metadataRow: {
|
|
497
|
+
flexDirection: 'row',
|
|
498
|
+
flexWrap: 'wrap',
|
|
499
|
+
alignItems: 'center',
|
|
500
|
+
},
|
|
501
|
+
metadataItem: {
|
|
502
|
+
flexDirection: 'row',
|
|
503
|
+
alignItems: 'center',
|
|
504
|
+
},
|
|
505
|
+
rightSlot: {
|
|
506
|
+
flexShrink: 0,
|
|
507
|
+
alignItems: 'center',
|
|
508
|
+
justifyContent: 'center',
|
|
509
|
+
},
|
|
510
|
+
chevron: {
|
|
511
|
+
flexShrink: 0,
|
|
512
|
+
},
|
|
513
|
+
divider: {
|
|
514
|
+
height: StyleSheet.hairlineWidth,
|
|
515
|
+
},
|
|
516
|
+
disabled: {
|
|
517
|
+
opacity: 0.5,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress
|
|
3
|
+
*
|
|
4
|
+
* A progress bar component with animated fill.
|
|
5
|
+
* Supports determinate and indeterminate states.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Progress value={50} />
|
|
10
|
+
* <Progress value={75} size="lg" />
|
|
11
|
+
* <Progress indeterminate />
|
|
12
|
+
* <Progress value={100} color="success" />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import React, { useEffect } from 'react';
|
|
17
|
+
import { View, StyleSheet, ViewStyle } from 'react-native';
|
|
18
|
+
import Animated, {
|
|
19
|
+
useSharedValue,
|
|
20
|
+
useAnimatedStyle,
|
|
21
|
+
withTiming,
|
|
22
|
+
withRepeat,
|
|
23
|
+
withSequence,
|
|
24
|
+
Easing,
|
|
25
|
+
} from 'react-native-reanimated';
|
|
26
|
+
import { useTheme } from '@nativeui/core';
|
|
27
|
+
|
|
28
|
+
export type ProgressSize = 'sm' | 'md' | 'lg';
|
|
29
|
+
export type ProgressColor = 'default' | 'primary' | 'success' | 'warning' | 'destructive';
|
|
30
|
+
|
|
31
|
+
export interface ProgressProps {
|
|
32
|
+
/** Current progress value (0-100) */
|
|
33
|
+
value?: number;
|
|
34
|
+
/** Maximum value */
|
|
35
|
+
max?: number;
|
|
36
|
+
/** Size preset */
|
|
37
|
+
size?: ProgressSize;
|
|
38
|
+
/** Color variant */
|
|
39
|
+
color?: ProgressColor;
|
|
40
|
+
/** Show indeterminate animation */
|
|
41
|
+
indeterminate?: boolean;
|
|
42
|
+
/** Container style */
|
|
43
|
+
style?: ViewStyle;
|
|
44
|
+
/** Track style */
|
|
45
|
+
trackStyle?: ViewStyle;
|
|
46
|
+
/** Fill style */
|
|
47
|
+
fillStyle?: ViewStyle;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sizeMap: Record<ProgressSize, number> = {
|
|
51
|
+
sm: 4,
|
|
52
|
+
md: 8,
|
|
53
|
+
lg: 12,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function Progress({
|
|
57
|
+
value = 0,
|
|
58
|
+
max = 100,
|
|
59
|
+
size = 'md',
|
|
60
|
+
color = 'primary',
|
|
61
|
+
indeterminate = false,
|
|
62
|
+
style,
|
|
63
|
+
trackStyle,
|
|
64
|
+
fillStyle,
|
|
65
|
+
}: ProgressProps) {
|
|
66
|
+
const { colors } = useTheme();
|
|
67
|
+
const progress = useSharedValue(0);
|
|
68
|
+
const indeterminateProgress = useSharedValue(0);
|
|
69
|
+
|
|
70
|
+
const normalizedValue = Math.min(Math.max(value, 0), max);
|
|
71
|
+
const percentage = (normalizedValue / max) * 100;
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!indeterminate) {
|
|
75
|
+
progress.value = withTiming(percentage, {
|
|
76
|
+
duration: 300,
|
|
77
|
+
easing: Easing.out(Easing.ease),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}, [percentage, indeterminate]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (indeterminate) {
|
|
84
|
+
indeterminateProgress.value = withRepeat(
|
|
85
|
+
withSequence(
|
|
86
|
+
withTiming(1, { duration: 1000, easing: Easing.inOut(Easing.ease) }),
|
|
87
|
+
withTiming(0, { duration: 1000, easing: Easing.inOut(Easing.ease) })
|
|
88
|
+
),
|
|
89
|
+
-1,
|
|
90
|
+
false
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}, [indeterminate]);
|
|
94
|
+
|
|
95
|
+
const getFillColor = (): string => {
|
|
96
|
+
switch (color) {
|
|
97
|
+
case 'primary':
|
|
98
|
+
return colors.primary;
|
|
99
|
+
case 'success':
|
|
100
|
+
return colors.success ?? '#22c55e';
|
|
101
|
+
case 'warning':
|
|
102
|
+
return colors.warning ?? '#f59e0b';
|
|
103
|
+
case 'destructive':
|
|
104
|
+
return colors.destructive;
|
|
105
|
+
case 'default':
|
|
106
|
+
default:
|
|
107
|
+
return colors.foreground;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const determinateStyle = useAnimatedStyle(() => ({
|
|
112
|
+
width: `${progress.value}%`,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const indeterminateStyle = useAnimatedStyle(() => ({
|
|
116
|
+
width: '30%',
|
|
117
|
+
left: `${indeterminateProgress.value * 70}%`,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
const height = sizeMap[size];
|
|
121
|
+
const fillColor = getFillColor();
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View
|
|
125
|
+
style={[
|
|
126
|
+
styles.track,
|
|
127
|
+
{
|
|
128
|
+
height,
|
|
129
|
+
borderRadius: height / 2,
|
|
130
|
+
backgroundColor: colors.backgroundMuted,
|
|
131
|
+
},
|
|
132
|
+
trackStyle,
|
|
133
|
+
style,
|
|
134
|
+
]}
|
|
135
|
+
accessibilityRole="progressbar"
|
|
136
|
+
accessibilityValue={{
|
|
137
|
+
min: 0,
|
|
138
|
+
max,
|
|
139
|
+
now: indeterminate ? undefined : normalizedValue,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
<Animated.View
|
|
143
|
+
style={[
|
|
144
|
+
styles.fill,
|
|
145
|
+
{
|
|
146
|
+
height,
|
|
147
|
+
borderRadius: height / 2,
|
|
148
|
+
backgroundColor: fillColor,
|
|
149
|
+
},
|
|
150
|
+
indeterminate ? indeterminateStyle : determinateStyle,
|
|
151
|
+
fillStyle,
|
|
152
|
+
]}
|
|
153
|
+
/>
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const styles = StyleSheet.create({
|
|
159
|
+
track: {
|
|
160
|
+
width: '100%',
|
|
161
|
+
overflow: 'hidden',
|
|
162
|
+
},
|
|
163
|
+
fill: {
|
|
164
|
+
position: 'absolute',
|
|
165
|
+
left: 0,
|
|
166
|
+
top: 0,
|
|
167
|
+
},
|
|
168
|
+
});
|