@metacells/mcellui-mcp-server 0.1.1 → 0.1.3

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 +14 -3
  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,348 @@
1
+ /**
2
+ * Tabs
3
+ *
4
+ * A tabbed navigation component with animated indicator.
5
+ * Supports both controlled and uncontrolled modes.
6
+ * Two visual variants: "pill" (segmented control) and "underline" (bottom border).
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * // Pill variant (default) - segmented control style
11
+ * <Tabs defaultValue="tab1">
12
+ * <TabsList>
13
+ * <TabsTrigger value="tab1">Account</TabsTrigger>
14
+ * <TabsTrigger value="tab2">Settings</TabsTrigger>
15
+ * </TabsList>
16
+ * <TabsContent value="tab1">
17
+ * <Text>Account content</Text>
18
+ * </TabsContent>
19
+ * <TabsContent value="tab2">
20
+ * <Text>Settings content</Text>
21
+ * </TabsContent>
22
+ * </Tabs>
23
+ *
24
+ * // Underline variant - bottom border style (great for profiles)
25
+ * <Tabs defaultValue="posts">
26
+ * <TabsList variant="underline">
27
+ * <TabsTrigger value="posts">Posts</TabsTrigger>
28
+ * <TabsTrigger value="media">Media</TabsTrigger>
29
+ * <TabsTrigger value="about">About</TabsTrigger>
30
+ * </TabsList>
31
+ * <TabsContent value="posts">...</TabsContent>
32
+ * </Tabs>
33
+ * ```
34
+ */
35
+
36
+ import React, {
37
+ createContext,
38
+ useContext,
39
+ useState,
40
+ useCallback,
41
+ useRef,
42
+ useEffect,
43
+ } from 'react';
44
+ import {
45
+ View,
46
+ Text,
47
+ Pressable,
48
+ StyleSheet,
49
+ ViewStyle,
50
+ TextStyle,
51
+ LayoutChangeEvent,
52
+ LayoutRectangle,
53
+ } from 'react-native';
54
+ import Animated, {
55
+ useSharedValue,
56
+ useAnimatedStyle,
57
+ withSpring,
58
+ } from 'react-native-reanimated';
59
+ import { useTheme } from '@nativeui/core';
60
+ import { haptic } from '@nativeui/core';
61
+
62
+ // Context
63
+ interface TabsContextValue {
64
+ value: string;
65
+ onValueChange: (value: string) => void;
66
+ }
67
+
68
+ const TabsContext = createContext<TabsContextValue | null>(null);
69
+
70
+ function useTabsContext() {
71
+ const context = useContext(TabsContext);
72
+ if (!context) {
73
+ throw new Error('Tabs components must be used within a Tabs provider');
74
+ }
75
+ return context;
76
+ }
77
+
78
+ // Tabs Root
79
+ export interface TabsProps {
80
+ /** Controlled value */
81
+ value?: string;
82
+ /** Default value for uncontrolled mode */
83
+ defaultValue?: string;
84
+ /** Callback when value changes */
85
+ onValueChange?: (value: string) => void;
86
+ /** Children */
87
+ children: React.ReactNode;
88
+ /** Container style */
89
+ style?: ViewStyle;
90
+ }
91
+
92
+ export function Tabs({
93
+ value: controlledValue,
94
+ defaultValue = '',
95
+ onValueChange,
96
+ children,
97
+ style,
98
+ }: TabsProps) {
99
+ const [internalValue, setInternalValue] = useState(defaultValue);
100
+ const isControlled = controlledValue !== undefined;
101
+ const value = isControlled ? controlledValue : internalValue;
102
+
103
+ const handleValueChange = useCallback(
104
+ (newValue: string) => {
105
+ if (!isControlled) {
106
+ setInternalValue(newValue);
107
+ }
108
+ onValueChange?.(newValue);
109
+ },
110
+ [isControlled, onValueChange]
111
+ );
112
+
113
+ return (
114
+ <TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
115
+ <View style={[styles.container, style]}>{children}</View>
116
+ </TabsContext.Provider>
117
+ );
118
+ }
119
+
120
+ // TabsList Context for indicator
121
+ export type TabsVariant = 'pill' | 'underline';
122
+
123
+ interface TabsListContextValue {
124
+ registerTab: (value: string, layout: LayoutRectangle) => void;
125
+ tabLayouts: Map<string, LayoutRectangle>;
126
+ variant: TabsVariant;
127
+ }
128
+
129
+ const TabsListContext = createContext<TabsListContextValue | null>(null);
130
+
131
+ // TabsList
132
+ export interface TabsListProps {
133
+ children: React.ReactNode;
134
+ /** Visual variant: "pill" (segmented control) or "underline" (bottom border) */
135
+ variant?: TabsVariant;
136
+ style?: ViewStyle;
137
+ }
138
+
139
+ export function TabsList({ children, variant = 'pill', style }: TabsListProps) {
140
+ const { colors, radius } = useTheme();
141
+ const { value } = useTabsContext();
142
+ const tabLayouts = useRef(new Map<string, LayoutRectangle>()).current;
143
+
144
+ const indicatorX = useSharedValue(0);
145
+ const indicatorWidth = useSharedValue(0);
146
+
147
+ const registerTab = useCallback(
148
+ (tabValue: string, layout: LayoutRectangle) => {
149
+ tabLayouts.set(tabValue, layout);
150
+
151
+ // Update indicator if this is the active tab
152
+ if (tabValue === value) {
153
+ indicatorX.value = withSpring(layout.x, { damping: 20, stiffness: 200 });
154
+ indicatorWidth.value = withSpring(layout.width, {
155
+ damping: 20,
156
+ stiffness: 200,
157
+ });
158
+ }
159
+ },
160
+ [value, tabLayouts]
161
+ );
162
+
163
+ // Update indicator when value changes
164
+ useEffect(() => {
165
+ const layout = tabLayouts.get(value);
166
+ if (layout) {
167
+ indicatorX.value = withSpring(layout.x, { damping: 20, stiffness: 200 });
168
+ indicatorWidth.value = withSpring(layout.width, {
169
+ damping: 20,
170
+ stiffness: 200,
171
+ });
172
+ }
173
+ }, [value, tabLayouts]);
174
+
175
+ const indicatorStyle = useAnimatedStyle(() => ({
176
+ transform: [{ translateX: indicatorX.value }],
177
+ width: indicatorWidth.value,
178
+ }));
179
+
180
+ const isPill = variant === 'pill';
181
+
182
+ return (
183
+ <TabsListContext.Provider value={{ registerTab, tabLayouts, variant }}>
184
+ <View
185
+ style={[
186
+ styles.list,
187
+ isPill
188
+ ? {
189
+ backgroundColor: colors.backgroundMuted,
190
+ borderRadius: radius.lg,
191
+ padding: 4,
192
+ }
193
+ : {
194
+ backgroundColor: 'transparent',
195
+ borderBottomWidth: 1,
196
+ borderBottomColor: colors.border,
197
+ },
198
+ style,
199
+ ]}
200
+ >
201
+ {children}
202
+ <Animated.View
203
+ style={[
204
+ isPill ? styles.indicatorPill : styles.indicatorUnderline,
205
+ isPill
206
+ ? {
207
+ backgroundColor: colors.background,
208
+ borderRadius: radius.md,
209
+ }
210
+ : {
211
+ backgroundColor: colors.primary,
212
+ },
213
+ indicatorStyle,
214
+ ]}
215
+ />
216
+ </View>
217
+ </TabsListContext.Provider>
218
+ );
219
+ }
220
+
221
+ // TabsTrigger
222
+ export interface TabsTriggerProps {
223
+ value: string;
224
+ children: React.ReactNode;
225
+ disabled?: boolean;
226
+ style?: ViewStyle;
227
+ textStyle?: TextStyle;
228
+ }
229
+
230
+ export function TabsTrigger({
231
+ value: tabValue,
232
+ children,
233
+ disabled = false,
234
+ style,
235
+ textStyle,
236
+ }: TabsTriggerProps) {
237
+ const { colors, spacing } = useTheme();
238
+ const { value, onValueChange } = useTabsContext();
239
+ const listContext = useContext(TabsListContext);
240
+ const isActive = value === tabValue;
241
+ const variant = listContext?.variant ?? 'pill';
242
+ const isUnderline = variant === 'underline';
243
+
244
+ const handleLayout = (event: LayoutChangeEvent) => {
245
+ listContext?.registerTab(tabValue, event.nativeEvent.layout);
246
+ };
247
+
248
+ const handlePress = () => {
249
+ if (disabled) return;
250
+ haptic('selection');
251
+ onValueChange(tabValue);
252
+ };
253
+
254
+ return (
255
+ <Pressable
256
+ style={[
257
+ styles.trigger,
258
+ {
259
+ paddingVertical: isUnderline ? spacing[3] : spacing[2],
260
+ paddingHorizontal: isUnderline ? spacing[4] : spacing[3],
261
+ opacity: disabled ? 0.5 : 1,
262
+ },
263
+ style,
264
+ ]}
265
+ onLayout={handleLayout}
266
+ onPress={handlePress}
267
+ disabled={disabled}
268
+ accessibilityRole="tab"
269
+ accessibilityState={{ selected: isActive, disabled }}
270
+ >
271
+ <Text
272
+ style={[
273
+ styles.triggerText,
274
+ {
275
+ color: isActive
276
+ ? isUnderline
277
+ ? colors.primary
278
+ : colors.foreground
279
+ : colors.foregroundMuted,
280
+ fontWeight: isActive ? '600' : '500',
281
+ },
282
+ textStyle,
283
+ ]}
284
+ >
285
+ {children}
286
+ </Text>
287
+ </Pressable>
288
+ );
289
+ }
290
+
291
+ // TabsContent
292
+ export interface TabsContentProps {
293
+ value: string;
294
+ children: React.ReactNode;
295
+ style?: ViewStyle;
296
+ }
297
+
298
+ export function TabsContent({ value: tabValue, children, style }: TabsContentProps) {
299
+ const { value } = useTabsContext();
300
+ const { spacing } = useTheme();
301
+
302
+ if (value !== tabValue) {
303
+ return null;
304
+ }
305
+
306
+ return (
307
+ <View style={[styles.content, { marginTop: spacing[4] }, style]}>
308
+ {children}
309
+ </View>
310
+ );
311
+ }
312
+
313
+ const styles = StyleSheet.create({
314
+ container: {
315
+ width: '100%',
316
+ },
317
+ list: {
318
+ flexDirection: 'row',
319
+ position: 'relative',
320
+ },
321
+ indicatorPill: {
322
+ position: 'absolute',
323
+ top: 4,
324
+ bottom: 4,
325
+ left: 0,
326
+ shadowColor: '#000',
327
+ shadowOffset: { width: 0, height: 1 },
328
+ shadowOpacity: 0.05,
329
+ shadowRadius: 2,
330
+ elevation: 1,
331
+ },
332
+ indicatorUnderline: {
333
+ position: 'absolute',
334
+ bottom: 0,
335
+ left: 0,
336
+ height: 2,
337
+ },
338
+ trigger: {
339
+ flex: 1,
340
+ alignItems: 'center',
341
+ justifyContent: 'center',
342
+ zIndex: 1,
343
+ },
344
+ triggerText: {
345
+ fontSize: 14,
346
+ },
347
+ content: {},
348
+ });
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Textarea
3
+ *
4
+ * A multiline text input component with auto-grow support.
5
+ * Extends Input with multiline-specific features.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * <Textarea label="Bio" placeholder="Tell us about yourself" />
10
+ * <Textarea label="Description" rows={5} maxLength={500} showCount />
11
+ * <Textarea autoGrow minRows={2} maxRows={6} />
12
+ * ```
13
+ */
14
+
15
+ import React, { forwardRef, useCallback, useState } from 'react';
16
+ import {
17
+ View,
18
+ Text,
19
+ TextInput,
20
+ StyleSheet,
21
+ ViewStyle,
22
+ TextStyle,
23
+ TextInputProps,
24
+ NativeSyntheticEvent,
25
+ TextInputContentSizeChangeEventData,
26
+ } from 'react-native';
27
+ import Animated, {
28
+ useSharedValue,
29
+ useAnimatedStyle,
30
+ withTiming,
31
+ interpolateColor,
32
+ Easing,
33
+ } from 'react-native-reanimated';
34
+ import { useTheme } from '@nativeui/core';
35
+ import { haptic } from '@nativeui/core';
36
+
37
+ const TIMING_CONFIG = { duration: 200, easing: Easing.out(Easing.quad) };
38
+
39
+ const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
40
+
41
+ export interface TextareaProps extends Omit<TextInputProps, 'style' | 'multiline'> {
42
+ /** Label text above input */
43
+ label?: string;
44
+ /** Error message below input */
45
+ error?: string;
46
+ /** Helper text below input (hidden when error is present) */
47
+ helperText?: string;
48
+ /** Number of visible rows (default: 4) */
49
+ rows?: number;
50
+ /** Auto-grow based on content */
51
+ autoGrow?: boolean;
52
+ /** Minimum rows when autoGrow is true */
53
+ minRows?: number;
54
+ /** Maximum rows when autoGrow is true */
55
+ maxRows?: number;
56
+ /** Show character count */
57
+ showCount?: boolean;
58
+ /** Container style */
59
+ containerStyle?: ViewStyle;
60
+ /** Input field style */
61
+ style?: TextStyle;
62
+ /** Label style */
63
+ labelStyle?: TextStyle;
64
+ }
65
+
66
+ const LINE_HEIGHT = 20;
67
+ const PADDING_VERTICAL = 12;
68
+
69
+ export const Textarea = forwardRef<TextInput, TextareaProps>(
70
+ (
71
+ {
72
+ label,
73
+ error,
74
+ helperText,
75
+ rows = 4,
76
+ autoGrow = false,
77
+ minRows = 2,
78
+ maxRows = 8,
79
+ showCount = false,
80
+ maxLength,
81
+ containerStyle,
82
+ style,
83
+ labelStyle,
84
+ editable = true,
85
+ onFocus,
86
+ onBlur,
87
+ onChangeText,
88
+ value,
89
+ ...props
90
+ },
91
+ ref
92
+ ) => {
93
+ const { colors, radius, spacing } = useTheme();
94
+ const focusProgress = useSharedValue(0);
95
+ const [textLength, setTextLength] = useState(value?.length ?? 0);
96
+ const [height, setHeight] = useState(rows * LINE_HEIGHT + PADDING_VERTICAL * 2);
97
+
98
+ const hasError = !!error;
99
+ const isDisabled = editable === false;
100
+
101
+ const handleFocus = useCallback(
102
+ (e: any) => {
103
+ focusProgress.value = withTiming(1, TIMING_CONFIG);
104
+ haptic('selection');
105
+ onFocus?.(e);
106
+ },
107
+ [onFocus]
108
+ );
109
+
110
+ const handleBlur = useCallback(
111
+ (e: any) => {
112
+ focusProgress.value = withTiming(0, TIMING_CONFIG);
113
+ onBlur?.(e);
114
+ },
115
+ [onBlur]
116
+ );
117
+
118
+ const handleChangeText = useCallback(
119
+ (text: string) => {
120
+ setTextLength(text.length);
121
+ onChangeText?.(text);
122
+ },
123
+ [onChangeText]
124
+ );
125
+
126
+ const handleContentSizeChange = useCallback(
127
+ (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
128
+ if (!autoGrow) return;
129
+
130
+ const contentHeight = e.nativeEvent.contentSize.height;
131
+ const minHeight = minRows * LINE_HEIGHT + PADDING_VERTICAL * 2;
132
+ const maxHeight = maxRows * LINE_HEIGHT + PADDING_VERTICAL * 2;
133
+
134
+ const newHeight = Math.min(Math.max(contentHeight, minHeight), maxHeight);
135
+ setHeight(newHeight);
136
+ },
137
+ [autoGrow, minRows, maxRows]
138
+ );
139
+
140
+ const animatedBorderStyle = useAnimatedStyle(() => {
141
+ if (hasError) {
142
+ return {
143
+ borderColor: colors.destructive,
144
+ borderWidth: 2,
145
+ };
146
+ }
147
+
148
+ return {
149
+ borderColor: interpolateColor(
150
+ focusProgress.value,
151
+ [0, 1],
152
+ [colors.border, colors.primary]
153
+ ),
154
+ borderWidth: focusProgress.value > 0.5 ? 2 : 1,
155
+ };
156
+ }, [hasError, colors]);
157
+
158
+ const baseHeight = autoGrow ? height : rows * LINE_HEIGHT + PADDING_VERTICAL * 2;
159
+
160
+ return (
161
+ <View style={[styles.container, containerStyle]}>
162
+ {label && (
163
+ <Text
164
+ style={[
165
+ styles.label,
166
+ {
167
+ fontSize: 14,
168
+ marginBottom: spacing[1.5],
169
+ color: hasError ? colors.destructive : colors.foreground,
170
+ },
171
+ labelStyle,
172
+ ]}
173
+ >
174
+ {label}
175
+ </Text>
176
+ )}
177
+
178
+ <AnimatedTextInput
179
+ ref={ref}
180
+ style={[
181
+ styles.input,
182
+ {
183
+ height: baseHeight,
184
+ paddingHorizontal: 12,
185
+ paddingVertical: PADDING_VERTICAL,
186
+ borderRadius: radius.md,
187
+ fontSize: 14,
188
+ lineHeight: LINE_HEIGHT,
189
+ backgroundColor: isDisabled ? colors.backgroundMuted : colors.background,
190
+ color: isDisabled ? colors.foregroundMuted : colors.foreground,
191
+ textAlignVertical: 'top',
192
+ },
193
+ animatedBorderStyle,
194
+ style,
195
+ ]}
196
+ multiline
197
+ placeholderTextColor={colors.foregroundMuted}
198
+ editable={editable}
199
+ onFocus={handleFocus}
200
+ onBlur={handleBlur}
201
+ onChangeText={handleChangeText}
202
+ onContentSizeChange={handleContentSizeChange}
203
+ maxLength={maxLength}
204
+ value={value}
205
+ accessibilityLabel={label}
206
+ accessibilityState={{ disabled: isDisabled }}
207
+ {...props}
208
+ />
209
+
210
+ <View style={styles.footer}>
211
+ {(error || helperText) && (
212
+ <Text
213
+ style={[
214
+ styles.helperText,
215
+ {
216
+ fontSize: 12,
217
+ marginTop: spacing[1],
218
+ color: hasError ? colors.destructive : colors.foregroundMuted,
219
+ },
220
+ ]}
221
+ >
222
+ {error || helperText}
223
+ </Text>
224
+ )}
225
+ {showCount && maxLength && (
226
+ <Text
227
+ style={[
228
+ styles.count,
229
+ {
230
+ fontSize: 12,
231
+ marginTop: spacing[1],
232
+ color: textLength >= maxLength ? colors.destructive : colors.foregroundMuted,
233
+ },
234
+ ]}
235
+ >
236
+ {textLength}/{maxLength}
237
+ </Text>
238
+ )}
239
+ </View>
240
+ </View>
241
+ );
242
+ }
243
+ );
244
+
245
+ Textarea.displayName = 'Textarea';
246
+
247
+ const styles = StyleSheet.create({
248
+ container: {
249
+ width: '100%',
250
+ },
251
+ label: {
252
+ fontWeight: '500',
253
+ },
254
+ input: {},
255
+ footer: {
256
+ flexDirection: 'row',
257
+ justifyContent: 'space-between',
258
+ },
259
+ helperText: {
260
+ flex: 1,
261
+ },
262
+ count: {
263
+ marginLeft: 8,
264
+ },
265
+ });