@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.
Files changed (49) hide show
  1. package/dist/index.js +70 -7
  2. package/package.json +7 -5
  3. package/registry/registry.json +717 -0
  4. package/registry/ui/accordion.tsx +416 -0
  5. package/registry/ui/action-sheet.tsx +396 -0
  6. package/registry/ui/alert-dialog.tsx +355 -0
  7. package/registry/ui/avatar-stack.tsx +278 -0
  8. package/registry/ui/avatar.tsx +116 -0
  9. package/registry/ui/badge.tsx +125 -0
  10. package/registry/ui/button.tsx +240 -0
  11. package/registry/ui/card.tsx +675 -0
  12. package/registry/ui/carousel.tsx +431 -0
  13. package/registry/ui/checkbox.tsx +252 -0
  14. package/registry/ui/chip.tsx +271 -0
  15. package/registry/ui/column.tsx +133 -0
  16. package/registry/ui/datetime-picker.tsx +578 -0
  17. package/registry/ui/dialog.tsx +292 -0
  18. package/registry/ui/fab.tsx +225 -0
  19. package/registry/ui/form.tsx +323 -0
  20. package/registry/ui/horizontal-list.tsx +200 -0
  21. package/registry/ui/icon-button.tsx +244 -0
  22. package/registry/ui/image-gallery.tsx +455 -0
  23. package/registry/ui/image.tsx +283 -0
  24. package/registry/ui/input.tsx +242 -0
  25. package/registry/ui/label.tsx +99 -0
  26. package/registry/ui/list.tsx +519 -0
  27. package/registry/ui/progress.tsx +168 -0
  28. package/registry/ui/pull-to-refresh.tsx +231 -0
  29. package/registry/ui/radio-group.tsx +294 -0
  30. package/registry/ui/rating.tsx +311 -0
  31. package/registry/ui/row.tsx +136 -0
  32. package/registry/ui/screen.tsx +153 -0
  33. package/registry/ui/search-input.tsx +281 -0
  34. package/registry/ui/section-header.tsx +258 -0
  35. package/registry/ui/segmented-control.tsx +229 -0
  36. package/registry/ui/select.tsx +311 -0
  37. package/registry/ui/separator.tsx +74 -0
  38. package/registry/ui/sheet.tsx +362 -0
  39. package/registry/ui/skeleton.tsx +156 -0
  40. package/registry/ui/slider.tsx +307 -0
  41. package/registry/ui/spinner.tsx +100 -0
  42. package/registry/ui/stepper.tsx +314 -0
  43. package/registry/ui/stories.tsx +463 -0
  44. package/registry/ui/swipeable-row.tsx +362 -0
  45. package/registry/ui/switch.tsx +246 -0
  46. package/registry/ui/tabs.tsx +348 -0
  47. package/registry/ui/textarea.tsx +265 -0
  48. package/registry/ui/toast.tsx +316 -0
  49. 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
+ });