@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.
- package/dist/index.js +8 -2
- package/package.json +5 -3
- 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,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
|
+
});
|