@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,323 @@
1
+ /**
2
+ * Form
3
+ *
4
+ * Form components with react-hook-form integration.
5
+ * Provides type-safe form handling with Zod validation.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { useForm } from 'react-hook-form';
10
+ * import { zodResolver } from '@hookform/resolvers/zod';
11
+ * import { z } from 'zod';
12
+ *
13
+ * const schema = z.object({
14
+ * email: z.string().email('Invalid email'),
15
+ * password: z.string().min(8, 'Min 8 characters'),
16
+ * });
17
+ *
18
+ * type FormData = z.infer<typeof schema>;
19
+ *
20
+ * function LoginForm() {
21
+ * const form = useForm<FormData>({
22
+ * resolver: zodResolver(schema),
23
+ * });
24
+ *
25
+ * const onSubmit = (data: FormData) => {
26
+ * console.log(data);
27
+ * };
28
+ *
29
+ * return (
30
+ * <Form form={form} onSubmit={onSubmit}>
31
+ * <FormField
32
+ * control={form.control}
33
+ * name="email"
34
+ * render={({ field, fieldState }) => (
35
+ * <FormItem>
36
+ * <FormLabel>Email</FormLabel>
37
+ * <Input
38
+ * placeholder="email@example.com"
39
+ * value={field.value}
40
+ * onChangeText={field.onChange}
41
+ * onBlur={field.onBlur}
42
+ * error={fieldState.error?.message}
43
+ * />
44
+ * <FormMessage />
45
+ * </FormItem>
46
+ * )}
47
+ * />
48
+ * <Button onPress={form.handleSubmit(onSubmit)}>Submit</Button>
49
+ * </Form>
50
+ * );
51
+ * }
52
+ * ```
53
+ */
54
+
55
+ import React, { createContext, useContext, useId } from 'react';
56
+ import {
57
+ View,
58
+ Text,
59
+ StyleSheet,
60
+ ViewStyle,
61
+ TextStyle,
62
+ } from 'react-native';
63
+ import {
64
+ Controller,
65
+ ControllerProps,
66
+ FieldPath,
67
+ FieldValues,
68
+ FormProvider,
69
+ UseFormReturn,
70
+ useFormContext,
71
+ ControllerRenderProps,
72
+ ControllerFieldState,
73
+ UseFormStateReturn,
74
+ } from 'react-hook-form';
75
+ import { useTheme } from '@nativeui/core';
76
+
77
+ // ============================================================================
78
+ // Form Context
79
+ // ============================================================================
80
+
81
+ type FormFieldContextValue<
82
+ TFieldValues extends FieldValues = FieldValues,
83
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
84
+ > = {
85
+ name: TName;
86
+ };
87
+
88
+ const FormFieldContext = createContext<FormFieldContextValue | null>(null);
89
+
90
+ type FormItemContextValue = {
91
+ id: string;
92
+ };
93
+
94
+ const FormItemContext = createContext<FormItemContextValue | null>(null);
95
+
96
+ // ============================================================================
97
+ // useFormField Hook
98
+ // ============================================================================
99
+
100
+ export function useFormField() {
101
+ const fieldContext = useContext(FormFieldContext);
102
+ const itemContext = useContext(FormItemContext);
103
+ const { getFieldState, formState } = useFormContext();
104
+
105
+ if (!fieldContext) {
106
+ throw new Error('useFormField must be used within <FormField>');
107
+ }
108
+
109
+ const fieldState = getFieldState(fieldContext.name, formState);
110
+
111
+ return {
112
+ id: itemContext?.id ?? '',
113
+ name: fieldContext.name,
114
+ formItemId: `${itemContext?.id}-form-item`,
115
+ formDescriptionId: `${itemContext?.id}-form-description`,
116
+ formMessageId: `${itemContext?.id}-form-message`,
117
+ ...fieldState,
118
+ };
119
+ }
120
+
121
+ // ============================================================================
122
+ // Form Component
123
+ // ============================================================================
124
+
125
+ export interface FormProps<TFieldValues extends FieldValues> {
126
+ /** react-hook-form form instance */
127
+ form: UseFormReturn<TFieldValues>;
128
+ /** Form children */
129
+ children: React.ReactNode;
130
+ /** Submit handler */
131
+ onSubmit?: (data: TFieldValues) => void | Promise<void>;
132
+ /** Container style */
133
+ style?: ViewStyle;
134
+ }
135
+
136
+ export function Form<TFieldValues extends FieldValues>({
137
+ form,
138
+ children,
139
+ style,
140
+ }: FormProps<TFieldValues>) {
141
+ return (
142
+ <FormProvider {...form}>
143
+ <View style={[styles.form, style]}>{children}</View>
144
+ </FormProvider>
145
+ );
146
+ }
147
+
148
+ // ============================================================================
149
+ // FormField Component
150
+ // ============================================================================
151
+
152
+ export interface FormFieldProps<
153
+ TFieldValues extends FieldValues = FieldValues,
154
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
155
+ > extends Omit<ControllerProps<TFieldValues, TName>, 'render'> {
156
+ render: (props: {
157
+ field: ControllerRenderProps<TFieldValues, TName>;
158
+ fieldState: ControllerFieldState;
159
+ formState: UseFormStateReturn<TFieldValues>;
160
+ }) => React.ReactElement;
161
+ }
162
+
163
+ export function FormField<
164
+ TFieldValues extends FieldValues = FieldValues,
165
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
166
+ >({ name, ...props }: FormFieldProps<TFieldValues, TName>) {
167
+ return (
168
+ <FormFieldContext.Provider value={{ name }}>
169
+ <Controller name={name} {...props} />
170
+ </FormFieldContext.Provider>
171
+ );
172
+ }
173
+
174
+ // ============================================================================
175
+ // FormItem Component
176
+ // ============================================================================
177
+
178
+ export interface FormItemProps {
179
+ /** Children elements */
180
+ children: React.ReactNode;
181
+ /** Container style */
182
+ style?: ViewStyle;
183
+ }
184
+
185
+ export function FormItem({ children, style }: FormItemProps) {
186
+ const id = useId();
187
+ const { spacing } = useTheme();
188
+
189
+ return (
190
+ <FormItemContext.Provider value={{ id }}>
191
+ <View style={[styles.formItem, { marginBottom: spacing[4] }, style]}>
192
+ {children}
193
+ </View>
194
+ </FormItemContext.Provider>
195
+ );
196
+ }
197
+
198
+ // ============================================================================
199
+ // FormLabel Component
200
+ // ============================================================================
201
+
202
+ export interface FormLabelProps {
203
+ /** Label text */
204
+ children: React.ReactNode;
205
+ /** Show required asterisk */
206
+ required?: boolean;
207
+ /** Additional text styles */
208
+ style?: TextStyle;
209
+ }
210
+
211
+ export function FormLabel({ children, required, style }: FormLabelProps) {
212
+ const { colors, spacing } = useTheme();
213
+ const { error } = useFormField();
214
+
215
+ return (
216
+ <Text
217
+ style={[
218
+ styles.formLabel,
219
+ {
220
+ color: error ? colors.destructive : colors.foreground,
221
+ marginBottom: spacing[1.5],
222
+ },
223
+ style,
224
+ ]}
225
+ >
226
+ {children}
227
+ {required && (
228
+ <Text style={{ color: colors.destructive }}> *</Text>
229
+ )}
230
+ </Text>
231
+ );
232
+ }
233
+
234
+ // ============================================================================
235
+ // FormDescription Component
236
+ // ============================================================================
237
+
238
+ export interface FormDescriptionProps {
239
+ /** Description text */
240
+ children: React.ReactNode;
241
+ /** Additional text styles */
242
+ style?: TextStyle;
243
+ }
244
+
245
+ export function FormDescription({ children, style }: FormDescriptionProps) {
246
+ const { colors, spacing } = useTheme();
247
+
248
+ return (
249
+ <Text
250
+ style={[
251
+ styles.formDescription,
252
+ {
253
+ color: colors.foregroundMuted,
254
+ marginTop: spacing[1],
255
+ },
256
+ style,
257
+ ]}
258
+ >
259
+ {children}
260
+ </Text>
261
+ );
262
+ }
263
+
264
+ // ============================================================================
265
+ // FormMessage Component
266
+ // ============================================================================
267
+
268
+ export interface FormMessageProps {
269
+ /** Override error message */
270
+ children?: React.ReactNode;
271
+ /** Additional text styles */
272
+ style?: TextStyle;
273
+ }
274
+
275
+ export function FormMessage({ children, style }: FormMessageProps) {
276
+ const { colors, spacing } = useTheme();
277
+ const { error } = useFormField();
278
+ const message = error?.message;
279
+
280
+ // Don't render if no message
281
+ if (!message && !children) {
282
+ return null;
283
+ }
284
+
285
+ return (
286
+ <Text
287
+ style={[
288
+ styles.formMessage,
289
+ {
290
+ color: colors.destructive,
291
+ marginTop: spacing[1],
292
+ },
293
+ style,
294
+ ]}
295
+ >
296
+ {children ?? message}
297
+ </Text>
298
+ );
299
+ }
300
+
301
+ // ============================================================================
302
+ // Styles
303
+ // ============================================================================
304
+
305
+ const styles = StyleSheet.create({
306
+ form: {
307
+ width: '100%',
308
+ },
309
+ formItem: {
310
+ width: '100%',
311
+ },
312
+ formLabel: {
313
+ fontSize: 14,
314
+ fontWeight: '500',
315
+ },
316
+ formDescription: {
317
+ fontSize: 12,
318
+ },
319
+ formMessage: {
320
+ fontSize: 12,
321
+ fontWeight: '500',
322
+ },
323
+ });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * HorizontalList
3
+ *
4
+ * A horizontal scrolling container with snap-to-item behavior,
5
+ * commonly used for carousels, card lists, and horizontal galleries.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Basic usage - edge-to-edge scroll with content inset
10
+ * <HorizontalList contentInset={16}>
11
+ * {items.map(item => (
12
+ * <ProductCard key={item.id} style={{ width: 200 }} {...item} />
13
+ * ))}
14
+ * </HorizontalList>
15
+ *
16
+ * // With active index tracking
17
+ * <HorizontalList
18
+ * contentInset={16}
19
+ * onActiveIndexChange={(index) => console.log('Active:', index)}
20
+ * >
21
+ * {banners.map(banner => (
22
+ * <BannerCard key={banner.id} style={{ width: screenWidth - 48 }} />
23
+ * ))}
24
+ * </HorizontalList>
25
+ * ```
26
+ */
27
+
28
+ import React, { useRef, useState, useCallback } from 'react';
29
+ import {
30
+ ScrollView,
31
+ View,
32
+ StyleSheet,
33
+ ViewStyle,
34
+ NativeSyntheticEvent,
35
+ NativeScrollEvent,
36
+ LayoutChangeEvent,
37
+ Dimensions,
38
+ } from 'react-native';
39
+ import { useTheme } from '@nativeui/core';
40
+
41
+ const { width: SCREEN_WIDTH } = Dimensions.get('window');
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Types
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ export interface HorizontalListProps {
48
+ /** List items */
49
+ children: React.ReactNode;
50
+ /** Content inset from edges (first/last item padding) */
51
+ contentInset?: number;
52
+ /** Spacing between items */
53
+ itemSpacing?: number;
54
+ /** Enable snap scrolling */
55
+ snapEnabled?: boolean;
56
+ /** Deceleration rate for scrolling */
57
+ decelerationRate?: 'normal' | 'fast';
58
+ /** Show horizontal scroll indicator */
59
+ showsScrollIndicator?: boolean;
60
+ /** Called when active (centered) item changes */
61
+ onActiveIndexChange?: (index: number) => void;
62
+ /** Called when scroll position changes */
63
+ onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
64
+ /** Container style */
65
+ style?: ViewStyle;
66
+ /** Content container style */
67
+ contentContainerStyle?: ViewStyle;
68
+ }
69
+
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+ // Component
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+
74
+ export function HorizontalList({
75
+ children,
76
+ contentInset,
77
+ itemSpacing,
78
+ snapEnabled = true,
79
+ decelerationRate = 'fast',
80
+ showsScrollIndicator = false,
81
+ onActiveIndexChange,
82
+ onScroll,
83
+ style,
84
+ contentContainerStyle,
85
+ }: HorizontalListProps) {
86
+ const { spacing } = useTheme();
87
+ const scrollRef = useRef<ScrollView>(null);
88
+
89
+ // Use theme spacing as defaults
90
+ const inset = contentInset ?? spacing[4];
91
+ const gap = itemSpacing ?? spacing[3];
92
+
93
+ // Track item positions for snap offsets
94
+ const [snapOffsets, setSnapOffsets] = useState<number[]>([]);
95
+ const itemPositions = useRef<{ x: number; width: number }[]>([]);
96
+ const containerWidth = useRef(SCREEN_WIDTH);
97
+
98
+ const childArray = React.Children.toArray(children);
99
+
100
+ // Calculate snap offsets when items are laid out
101
+ const handleItemLayout = useCallback(
102
+ (index: number) => (event: LayoutChangeEvent) => {
103
+ const { x, width } = event.nativeEvent.layout;
104
+ itemPositions.current[index] = { x, width };
105
+
106
+ // Recalculate snap offsets when all items are measured
107
+ if (itemPositions.current.filter(Boolean).length === childArray.length) {
108
+ const offsets = itemPositions.current.map((pos, index) => {
109
+ if (index === 0) return 0;
110
+ // Snap so previous item is completely off-screen
111
+ const prevItem = itemPositions.current[index - 1];
112
+ if (!prevItem) return 0;
113
+ return prevItem.x + prevItem.width;
114
+ });
115
+ setSnapOffsets(offsets);
116
+ }
117
+ },
118
+ [childArray.length, inset]
119
+ );
120
+
121
+ const handleContainerLayout = useCallback((event: LayoutChangeEvent) => {
122
+ containerWidth.current = event.nativeEvent.layout.width;
123
+ }, []);
124
+
125
+ // Track active index on scroll end
126
+ const handleScrollEnd = useCallback(
127
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
128
+ if (!onActiveIndexChange) return;
129
+
130
+ const offsetX = event.nativeEvent.contentOffset.x;
131
+
132
+ // Find closest snap point
133
+ let closestIndex = 0;
134
+ let minDistance = Infinity;
135
+
136
+ snapOffsets.forEach((offset, index) => {
137
+ const distance = Math.abs(offsetX - offset);
138
+ if (distance < minDistance) {
139
+ minDistance = distance;
140
+ closestIndex = index;
141
+ }
142
+ });
143
+
144
+ onActiveIndexChange(closestIndex);
145
+ },
146
+ [onActiveIndexChange, snapOffsets]
147
+ );
148
+
149
+ return (
150
+ <ScrollView
151
+ ref={scrollRef}
152
+ horizontal
153
+ showsHorizontalScrollIndicator={showsScrollIndicator}
154
+ decelerationRate={decelerationRate}
155
+ snapToOffsets={snapEnabled ? snapOffsets : undefined}
156
+ snapToStart={snapEnabled}
157
+ snapToEnd={snapEnabled}
158
+ onScroll={onScroll}
159
+ onMomentumScrollEnd={handleScrollEnd}
160
+ scrollEventThrottle={16}
161
+ style={[styles.container, style]}
162
+ contentContainerStyle={[styles.content, contentContainerStyle]}
163
+ onLayout={handleContainerLayout}
164
+ accessibilityRole="list"
165
+ >
166
+ {childArray.map((child, index) => {
167
+ const isFirst = index === 0;
168
+ const isLast = index === childArray.length - 1;
169
+
170
+ return (
171
+ <View
172
+ key={index}
173
+ style={{
174
+ marginLeft: isFirst ? inset : gap,
175
+ marginRight: isLast ? inset : 0,
176
+ }}
177
+ onLayout={handleItemLayout(index)}
178
+ accessible
179
+ >
180
+ {child}
181
+ </View>
182
+ );
183
+ })}
184
+ </ScrollView>
185
+ );
186
+ }
187
+
188
+ // ─────────────────────────────────────────────────────────────────────────────
189
+ // Styles
190
+ // ─────────────────────────────────────────────────────────────────────────────
191
+
192
+ const styles = StyleSheet.create({
193
+ container: {
194
+ flexGrow: 0,
195
+ },
196
+ content: {
197
+ flexDirection: 'row',
198
+ alignItems: 'stretch',
199
+ },
200
+ });