@metacells/mcellui-mcp-server 0.1.1 → 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 +8 -2
  2. package/package.json +5 -3
  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,362 @@
1
+ /**
2
+ * Swipeable Row
3
+ *
4
+ * A list row with swipe-to-reveal actions.
5
+ * Supports left and right swipe actions with haptic feedback.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * <SwipeableRow
10
+ * rightActions={[
11
+ * {
12
+ * label: 'Archive',
13
+ * color: '#3b82f6',
14
+ * onPress: () => handleArchive(),
15
+ * },
16
+ * {
17
+ * label: 'Delete',
18
+ * color: '#ef4444',
19
+ * onPress: () => handleDelete(),
20
+ * },
21
+ * ]}
22
+ * >
23
+ * <ListItem title="Swipe me" />
24
+ * </SwipeableRow>
25
+ * ```
26
+ */
27
+
28
+ import React, { useCallback, useRef } from 'react';
29
+ import {
30
+ View,
31
+ Text,
32
+ Pressable,
33
+ StyleSheet,
34
+ ViewStyle,
35
+ TextStyle,
36
+ Dimensions,
37
+ } from 'react-native';
38
+ import Animated, {
39
+ useSharedValue,
40
+ useAnimatedStyle,
41
+ withSpring,
42
+ withTiming,
43
+ runOnJS,
44
+ interpolate,
45
+ Extrapolation,
46
+ } from 'react-native-reanimated';
47
+ import {
48
+ Gesture,
49
+ GestureDetector,
50
+ } from 'react-native-gesture-handler';
51
+ import { useTheme } from '@nativeui/core';
52
+ import { haptic } from '@nativeui/core';
53
+
54
+ const SCREEN_WIDTH = Dimensions.get('window').width;
55
+ const ACTION_WIDTH = 80;
56
+ const SPRING_CONFIG = { damping: 20, stiffness: 200 };
57
+
58
+ export interface SwipeAction {
59
+ /** Action label */
60
+ label: string;
61
+ /** Icon component (optional) */
62
+ icon?: React.ReactNode;
63
+ /** Background color */
64
+ color: string;
65
+ /** Text color */
66
+ textColor?: string;
67
+ /** Action callback */
68
+ onPress: () => void;
69
+ }
70
+
71
+ export interface SwipeableRowProps {
72
+ /** Content to display */
73
+ children: React.ReactNode;
74
+ /** Actions revealed on left swipe (right side) */
75
+ rightActions?: SwipeAction[];
76
+ /** Actions revealed on right swipe (left side) */
77
+ leftActions?: SwipeAction[];
78
+ /** Width of each action button */
79
+ actionWidth?: number;
80
+ /** Enable full swipe to trigger first action */
81
+ fullSwipeEnabled?: boolean;
82
+ /** Callback when row is swiped open */
83
+ onSwipeOpen?: (direction: 'left' | 'right') => void;
84
+ /** Callback when row is closed */
85
+ onSwipeClose?: () => void;
86
+ /** Container style */
87
+ style?: ViewStyle;
88
+ }
89
+
90
+ export function SwipeableRow({
91
+ children,
92
+ rightActions = [],
93
+ leftActions = [],
94
+ actionWidth = ACTION_WIDTH,
95
+ fullSwipeEnabled = true,
96
+ onSwipeOpen,
97
+ onSwipeClose,
98
+ style,
99
+ }: SwipeableRowProps) {
100
+ const { colors } = useTheme();
101
+
102
+ const translateX = useSharedValue(0);
103
+ const contextX = useSharedValue(0);
104
+ const isOpen = useRef<'left' | 'right' | null>(null);
105
+
106
+ const rightActionsWidth = rightActions.length * actionWidth;
107
+ const leftActionsWidth = leftActions.length * actionWidth;
108
+ const fullSwipeThreshold = SCREEN_WIDTH * 0.5;
109
+
110
+ const close = useCallback(() => {
111
+ translateX.value = withSpring(0, SPRING_CONFIG);
112
+ if (isOpen.current) {
113
+ isOpen.current = null;
114
+ onSwipeClose?.();
115
+ }
116
+ }, [onSwipeClose]);
117
+
118
+ const openRight = useCallback(() => {
119
+ translateX.value = withSpring(-rightActionsWidth, SPRING_CONFIG);
120
+ if (isOpen.current !== 'right') {
121
+ isOpen.current = 'right';
122
+ onSwipeOpen?.('right');
123
+ }
124
+ }, [rightActionsWidth, onSwipeOpen]);
125
+
126
+ const openLeft = useCallback(() => {
127
+ translateX.value = withSpring(leftActionsWidth, SPRING_CONFIG);
128
+ if (isOpen.current !== 'left') {
129
+ isOpen.current = 'left';
130
+ onSwipeOpen?.('left');
131
+ }
132
+ }, [leftActionsWidth, onSwipeOpen]);
133
+
134
+ const triggerFullSwipe = useCallback(
135
+ (direction: 'left' | 'right') => {
136
+ haptic('medium');
137
+ const action = direction === 'right' ? rightActions[0] : leftActions[0];
138
+ if (action) {
139
+ // Animate off screen then reset
140
+ const target = direction === 'right' ? -SCREEN_WIDTH : SCREEN_WIDTH;
141
+ translateX.value = withTiming(target, { duration: 200 }, () => {
142
+ runOnJS(action.onPress)();
143
+ translateX.value = 0;
144
+ });
145
+ }
146
+ },
147
+ [rightActions, leftActions]
148
+ );
149
+
150
+ const panGesture = Gesture.Pan()
151
+ .activeOffsetX([-10, 10])
152
+ .onStart(() => {
153
+ contextX.value = translateX.value;
154
+ })
155
+ .onUpdate((event) => {
156
+ let newX = contextX.value + event.translationX;
157
+
158
+ // Limit swipe based on available actions
159
+ if (rightActions.length === 0 && newX < 0) newX = 0;
160
+ if (leftActions.length === 0 && newX > 0) newX = 0;
161
+
162
+ // Determine max swipe distances
163
+ const maxSwipeLeft = fullSwipeEnabled ? SCREEN_WIDTH * 0.75 : rightActionsWidth;
164
+ const maxSwipeRight = fullSwipeEnabled ? SCREEN_WIDTH * 0.75 : leftActionsWidth;
165
+
166
+ // Clamp the swipe to prevent seeing both sides at once
167
+ // Add rubber band resistance when going past the action buttons
168
+ if (newX < 0) {
169
+ // Swiping left (revealing right actions)
170
+ if (newX < -rightActionsWidth) {
171
+ // Add resistance past the buttons
172
+ const overshoot = -newX - rightActionsWidth;
173
+ const resistance = Math.min(overshoot * 0.3, maxSwipeLeft - rightActionsWidth);
174
+ newX = -rightActionsWidth - resistance;
175
+ }
176
+ } else if (newX > 0) {
177
+ // Swiping right (revealing left actions)
178
+ if (newX > leftActionsWidth) {
179
+ // Add resistance past the buttons
180
+ const overshoot = newX - leftActionsWidth;
181
+ const resistance = Math.min(overshoot * 0.3, maxSwipeRight - leftActionsWidth);
182
+ newX = leftActionsWidth + resistance;
183
+ }
184
+ }
185
+
186
+ translateX.value = newX;
187
+ })
188
+ .onEnd((event) => {
189
+ 'worklet';
190
+ const velocity = event.velocityX;
191
+ const x = translateX.value;
192
+
193
+ // Full swipe detection
194
+ if (fullSwipeEnabled) {
195
+ if (x < -fullSwipeThreshold && rightActions.length > 0) {
196
+ runOnJS(triggerFullSwipe)('right');
197
+ return;
198
+ }
199
+ if (x > fullSwipeThreshold && leftActions.length > 0) {
200
+ runOnJS(triggerFullSwipe)('left');
201
+ return;
202
+ }
203
+ }
204
+
205
+ // Determine final position based on position and velocity
206
+ if (x < -rightActionsWidth / 2 || velocity < -500) {
207
+ if (rightActions.length > 0) {
208
+ runOnJS(openRight)();
209
+ runOnJS(haptic)('selection');
210
+ } else {
211
+ runOnJS(close)();
212
+ }
213
+ } else if (x > leftActionsWidth / 2 || velocity > 500) {
214
+ if (leftActions.length > 0) {
215
+ runOnJS(openLeft)();
216
+ runOnJS(haptic)('selection');
217
+ } else {
218
+ runOnJS(close)();
219
+ }
220
+ } else {
221
+ runOnJS(close)();
222
+ }
223
+ });
224
+
225
+ const rowStyle = useAnimatedStyle(() => ({
226
+ transform: [{ translateX: translateX.value }],
227
+ }));
228
+
229
+ const rightActionsStyle = useAnimatedStyle(() => {
230
+ // Only show when swiping left (negative translateX)
231
+ const isSwipingLeft = translateX.value < 0;
232
+ return {
233
+ width: isSwipingLeft
234
+ ? Math.min(-translateX.value, rightActionsWidth + 50) // Allow slight overshoot for full swipe
235
+ : 0,
236
+ opacity: isSwipingLeft ? 1 : 0,
237
+ };
238
+ });
239
+
240
+ const leftActionsStyle = useAnimatedStyle(() => {
241
+ // Only show when swiping right (positive translateX)
242
+ const isSwipingRight = translateX.value > 0;
243
+ return {
244
+ width: isSwipingRight
245
+ ? Math.min(translateX.value, leftActionsWidth + 50) // Allow slight overshoot for full swipe
246
+ : 0,
247
+ opacity: isSwipingRight ? 1 : 0,
248
+ };
249
+ });
250
+
251
+ return (
252
+ <View style={[styles.container, style]}>
253
+ {/* Left Actions (revealed on right swipe) */}
254
+ {leftActions.length > 0 && (
255
+ <Animated.View style={[styles.actionsContainer, styles.leftActions, leftActionsStyle]}>
256
+ {leftActions.map((action, index) => (
257
+ <ActionButton
258
+ key={index}
259
+ action={action}
260
+ width={actionWidth}
261
+ onPress={() => {
262
+ haptic('light');
263
+ action.onPress();
264
+ close();
265
+ }}
266
+ />
267
+ ))}
268
+ </Animated.View>
269
+ )}
270
+
271
+ {/* Right Actions (revealed on left swipe) */}
272
+ {rightActions.length > 0 && (
273
+ <Animated.View style={[styles.actionsContainer, styles.rightActions, rightActionsStyle]}>
274
+ {rightActions.map((action, index) => (
275
+ <ActionButton
276
+ key={index}
277
+ action={action}
278
+ width={actionWidth}
279
+ onPress={() => {
280
+ haptic('light');
281
+ action.onPress();
282
+ close();
283
+ }}
284
+ />
285
+ ))}
286
+ </Animated.View>
287
+ )}
288
+
289
+ {/* Main Content */}
290
+ <GestureDetector gesture={panGesture}>
291
+ <Animated.View style={[styles.row, { backgroundColor: colors.background }, rowStyle]}>
292
+ {children}
293
+ </Animated.View>
294
+ </GestureDetector>
295
+ </View>
296
+ );
297
+ }
298
+
299
+ interface ActionButtonProps {
300
+ action: SwipeAction;
301
+ width: number;
302
+ onPress: () => void;
303
+ }
304
+
305
+ function ActionButton({ action, width, onPress }: ActionButtonProps) {
306
+ return (
307
+ <Pressable
308
+ style={[
309
+ styles.actionButton,
310
+ { width, backgroundColor: action.color },
311
+ ]}
312
+ onPress={onPress}
313
+ >
314
+ {action.icon && <View style={styles.actionIcon}>{action.icon}</View>}
315
+ <Text
316
+ style={[
317
+ styles.actionLabel,
318
+ { color: action.textColor ?? '#fff' },
319
+ ]}
320
+ numberOfLines={1}
321
+ >
322
+ {action.label}
323
+ </Text>
324
+ </Pressable>
325
+ );
326
+ }
327
+
328
+ const styles = StyleSheet.create({
329
+ container: {
330
+ overflow: 'hidden',
331
+ },
332
+ row: {
333
+ zIndex: 1,
334
+ },
335
+ actionsContainer: {
336
+ position: 'absolute',
337
+ top: 0,
338
+ bottom: 0,
339
+ flexDirection: 'row',
340
+ overflow: 'hidden',
341
+ },
342
+ leftActions: {
343
+ left: 0,
344
+ justifyContent: 'flex-start',
345
+ },
346
+ rightActions: {
347
+ right: 0,
348
+ justifyContent: 'flex-end',
349
+ },
350
+ actionButton: {
351
+ justifyContent: 'center',
352
+ alignItems: 'center',
353
+ paddingHorizontal: 8,
354
+ },
355
+ actionIcon: {
356
+ marginBottom: 4,
357
+ },
358
+ actionLabel: {
359
+ fontSize: 12,
360
+ fontWeight: '600',
361
+ },
362
+ });
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Switch
3
+ *
4
+ * An iOS-style toggle switch with animated thumb.
5
+ * Uses design tokens for consistent styling.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const [enabled, setEnabled] = useState(false);
10
+ * <Switch checked={enabled} onCheckedChange={setEnabled} />
11
+ * <Switch checked={enabled} onCheckedChange={setEnabled} label="Notifications" />
12
+ * <Switch label="Dark Mode" description="Enable dark theme" />
13
+ * <Switch disabled />
14
+ * ```
15
+ */
16
+
17
+ import React, { useEffect, useCallback } from 'react';
18
+ import {
19
+ Pressable,
20
+ View,
21
+ Text,
22
+ StyleSheet,
23
+ ViewStyle,
24
+ AccessibilityInfo,
25
+ } from 'react-native';
26
+ import Animated, {
27
+ useSharedValue,
28
+ useAnimatedStyle,
29
+ withSpring,
30
+ interpolate,
31
+ interpolateColor,
32
+ Extrapolation,
33
+ } from 'react-native-reanimated';
34
+ import { useTheme } from '@nativeui/core';
35
+ import { haptic } from '@nativeui/core';
36
+
37
+ export type SwitchSize = 'sm' | 'md' | 'lg';
38
+
39
+ export interface SwitchProps {
40
+ /** Whether the switch is on */
41
+ checked?: boolean;
42
+ /** Callback when switch state changes */
43
+ onCheckedChange?: (checked: boolean) => void;
44
+ /** Disable the switch */
45
+ disabled?: boolean;
46
+ /** Label text shown next to switch */
47
+ label?: string;
48
+ /** Description text below label */
49
+ description?: string;
50
+ /** Additional accessibility label */
51
+ accessibilityLabel?: string;
52
+ /** Size variant */
53
+ size?: SwitchSize;
54
+ /** Additional container styles */
55
+ style?: ViewStyle;
56
+ }
57
+
58
+ export function Switch({
59
+ checked = false,
60
+ onCheckedChange,
61
+ disabled = false,
62
+ label,
63
+ description,
64
+ accessibilityLabel,
65
+ size = 'md',
66
+ style,
67
+ }: SwitchProps) {
68
+ const { colors, components, platformShadow, springs } = useTheme();
69
+ const tokens = components.switch[size];
70
+ const progress = useSharedValue(checked ? 1 : 0);
71
+ const scale = useSharedValue(1);
72
+ const [reduceMotion, setReduceMotion] = React.useState(false);
73
+
74
+ // Calculate thumb travel distance from tokens
75
+ const thumbTravel = tokens.trackWidth - tokens.thumbSize - (tokens.thumbOffset * 2);
76
+
77
+ // Check for reduce motion preference
78
+ useEffect(() => {
79
+ AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
80
+ const subscription = AccessibilityInfo.addEventListener(
81
+ 'reduceMotionChanged',
82
+ setReduceMotion
83
+ );
84
+ return () => subscription.remove();
85
+ }, []);
86
+
87
+ // Animate on state change
88
+ useEffect(() => {
89
+ const target = checked ? 1 : 0;
90
+ if (reduceMotion) {
91
+ progress.value = target;
92
+ } else {
93
+ progress.value = withSpring(target, springs.snappy);
94
+ }
95
+ }, [checked, reduceMotion, springs.snappy]);
96
+
97
+ const handlePressIn = useCallback(() => {
98
+ if (!disabled) {
99
+ scale.value = withSpring(0.95, springs.snappy);
100
+ }
101
+ }, [disabled, springs.snappy]);
102
+
103
+ const handlePressOut = useCallback(() => {
104
+ scale.value = withSpring(1, springs.snappy);
105
+ }, [springs.snappy]);
106
+
107
+ const handlePress = useCallback(() => {
108
+ if (disabled || !onCheckedChange) return;
109
+ haptic('medium');
110
+ onCheckedChange(!checked);
111
+ }, [disabled, onCheckedChange, checked]);
112
+
113
+ const trackAnimatedStyle = useAnimatedStyle(() => ({
114
+ backgroundColor: interpolateColor(
115
+ progress.value,
116
+ [0, 1],
117
+ [colors.backgroundMuted, colors.primary]
118
+ ),
119
+ transform: [{ scale: scale.value }],
120
+ }));
121
+
122
+ const thumbAnimatedStyle = useAnimatedStyle(() => ({
123
+ transform: [
124
+ {
125
+ translateX: interpolate(
126
+ progress.value,
127
+ [0, 1],
128
+ [0, thumbTravel],
129
+ Extrapolation.CLAMP
130
+ ),
131
+ },
132
+ {
133
+ scale: interpolate(
134
+ progress.value,
135
+ [0, 0.5, 1],
136
+ [1, 0.9, 1],
137
+ Extrapolation.CLAMP
138
+ ),
139
+ },
140
+ ],
141
+ }));
142
+
143
+ return (
144
+ <Pressable
145
+ onPressIn={handlePressIn}
146
+ onPressOut={handlePressOut}
147
+ onPress={handlePress}
148
+ disabled={disabled}
149
+ style={[
150
+ styles.container,
151
+ { gap: tokens.gap },
152
+ style,
153
+ ]}
154
+ accessible
155
+ accessibilityRole="switch"
156
+ accessibilityLabel={accessibilityLabel ?? label}
157
+ accessibilityState={{
158
+ checked,
159
+ disabled,
160
+ }}
161
+ >
162
+ {(label || description) && (
163
+ <View style={styles.labelContainer}>
164
+ {label && (
165
+ <Text
166
+ style={[
167
+ styles.label,
168
+ {
169
+ fontSize: tokens.labelFontSize,
170
+ color: disabled ? colors.foregroundMuted : colors.foreground,
171
+ },
172
+ ]}
173
+ >
174
+ {label}
175
+ </Text>
176
+ )}
177
+ {description && (
178
+ <Text
179
+ style={[
180
+ styles.description,
181
+ { color: colors.foregroundMuted },
182
+ ]}
183
+ >
184
+ {description}
185
+ </Text>
186
+ )}
187
+ </View>
188
+ )}
189
+
190
+ <Animated.View
191
+ style={[
192
+ styles.track,
193
+ {
194
+ width: tokens.trackWidth,
195
+ height: tokens.trackHeight,
196
+ borderRadius: tokens.borderRadius,
197
+ paddingHorizontal: tokens.thumbOffset,
198
+ },
199
+ trackAnimatedStyle,
200
+ disabled && styles.disabled,
201
+ ]}
202
+ >
203
+ <Animated.View
204
+ style={[
205
+ styles.thumb,
206
+ {
207
+ width: tokens.thumbSize,
208
+ height: tokens.thumbSize,
209
+ borderRadius: tokens.borderRadius,
210
+ backgroundColor: colors.background,
211
+ },
212
+ platformShadow('sm'),
213
+ thumbAnimatedStyle,
214
+ ]}
215
+ />
216
+ </Animated.View>
217
+ </Pressable>
218
+ );
219
+ }
220
+
221
+ const styles = StyleSheet.create({
222
+ container: {
223
+ flexDirection: 'row',
224
+ alignItems: 'center',
225
+ justifyContent: 'space-between',
226
+ },
227
+ labelContainer: {
228
+ flex: 1,
229
+ gap: 2,
230
+ },
231
+ label: {
232
+ fontWeight: '500',
233
+ },
234
+ description: {
235
+ fontSize: 13,
236
+ },
237
+ track: {
238
+ justifyContent: 'center',
239
+ },
240
+ thumb: {
241
+ // Shadow applied via platformShadow
242
+ },
243
+ disabled: {
244
+ opacity: 0.5,
245
+ },
246
+ });