@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.
- package/dist/index.js +70 -7
- package/package.json +7 -5
- 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,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accordion
|
|
3
|
+
*
|
|
4
|
+
* An expandable/collapsible content panel component.
|
|
5
|
+
* Supports single or multiple expanded items with smooth height animations.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* <Accordion type="single" defaultValue="item-1">
|
|
10
|
+
* <AccordionItem value="item-1">
|
|
11
|
+
* <AccordionTrigger>Section 1</AccordionTrigger>
|
|
12
|
+
* <AccordionContent>
|
|
13
|
+
* <Text>Content for section 1</Text>
|
|
14
|
+
* </AccordionContent>
|
|
15
|
+
* </AccordionItem>
|
|
16
|
+
* <AccordionItem value="item-2">
|
|
17
|
+
* <AccordionTrigger>Section 2</AccordionTrigger>
|
|
18
|
+
* <AccordionContent>
|
|
19
|
+
* <Text>Content for section 2</Text>
|
|
20
|
+
* </AccordionContent>
|
|
21
|
+
* </AccordionItem>
|
|
22
|
+
* </Accordion>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
27
|
+
import {
|
|
28
|
+
View,
|
|
29
|
+
Text,
|
|
30
|
+
Pressable,
|
|
31
|
+
StyleSheet,
|
|
32
|
+
ViewStyle,
|
|
33
|
+
TextStyle,
|
|
34
|
+
LayoutChangeEvent,
|
|
35
|
+
} from 'react-native';
|
|
36
|
+
import Animated, {
|
|
37
|
+
useSharedValue,
|
|
38
|
+
useAnimatedStyle,
|
|
39
|
+
withSpring,
|
|
40
|
+
interpolate,
|
|
41
|
+
Extrapolation,
|
|
42
|
+
} from 'react-native-reanimated';
|
|
43
|
+
import Svg, { Path } from 'react-native-svg';
|
|
44
|
+
import { useTheme } from '@nativeui/core';
|
|
45
|
+
import { haptic } from '@nativeui/core';
|
|
46
|
+
|
|
47
|
+
// Smooth spring config for natural feel
|
|
48
|
+
const SPRING_CONFIG = {
|
|
49
|
+
damping: 20,
|
|
50
|
+
stiffness: 200,
|
|
51
|
+
mass: 0.5,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Accordion Context
|
|
55
|
+
interface AccordionContextValue {
|
|
56
|
+
type: 'single' | 'multiple';
|
|
57
|
+
value: string[];
|
|
58
|
+
onValueChange: (itemValue: string) => void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const AccordionContext = createContext<AccordionContextValue | null>(null);
|
|
62
|
+
|
|
63
|
+
function useAccordionContext() {
|
|
64
|
+
const context = useContext(AccordionContext);
|
|
65
|
+
if (!context) {
|
|
66
|
+
throw new Error('Accordion components must be used within an Accordion provider');
|
|
67
|
+
}
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Item Context
|
|
72
|
+
interface AccordionItemContextValue {
|
|
73
|
+
value: string;
|
|
74
|
+
isOpen: boolean;
|
|
75
|
+
isLast: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const AccordionItemContext = createContext<AccordionItemContextValue | null>(null);
|
|
79
|
+
|
|
80
|
+
function useAccordionItemContext() {
|
|
81
|
+
const context = useContext(AccordionItemContext);
|
|
82
|
+
if (!context) {
|
|
83
|
+
throw new Error('AccordionItem components must be used within an AccordionItem');
|
|
84
|
+
}
|
|
85
|
+
return context;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Animated SVG Chevron
|
|
89
|
+
const AnimatedSvg = Animated.createAnimatedComponent(Svg);
|
|
90
|
+
|
|
91
|
+
function ChevronIcon({ color, style }: { color: string; style?: any }) {
|
|
92
|
+
return (
|
|
93
|
+
<AnimatedSvg width={16} height={16} viewBox="0 0 24 24" fill="none" style={style}>
|
|
94
|
+
<Path
|
|
95
|
+
d="M6 9l6 6 6-6"
|
|
96
|
+
stroke={color}
|
|
97
|
+
strokeWidth={2}
|
|
98
|
+
strokeLinecap="round"
|
|
99
|
+
strokeLinejoin="round"
|
|
100
|
+
/>
|
|
101
|
+
</AnimatedSvg>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Accordion Root
|
|
106
|
+
export interface AccordionProps {
|
|
107
|
+
/** Selection mode */
|
|
108
|
+
type?: 'single' | 'multiple';
|
|
109
|
+
/** Controlled value */
|
|
110
|
+
value?: string | string[];
|
|
111
|
+
/** Default value */
|
|
112
|
+
defaultValue?: string | string[];
|
|
113
|
+
/** Callback when value changes */
|
|
114
|
+
onValueChange?: (value: string | string[]) => void;
|
|
115
|
+
/** Whether to allow collapsing all items (single mode only) */
|
|
116
|
+
collapsible?: boolean;
|
|
117
|
+
children: React.ReactNode;
|
|
118
|
+
style?: ViewStyle;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function Accordion({
|
|
122
|
+
type = 'single',
|
|
123
|
+
value: controlledValue,
|
|
124
|
+
defaultValue,
|
|
125
|
+
onValueChange,
|
|
126
|
+
collapsible = true,
|
|
127
|
+
children,
|
|
128
|
+
style,
|
|
129
|
+
}: AccordionProps) {
|
|
130
|
+
const { colors, radius } = useTheme();
|
|
131
|
+
|
|
132
|
+
// Normalize values to array
|
|
133
|
+
const normalizeValue = (val: string | string[] | undefined): string[] => {
|
|
134
|
+
if (val === undefined) return [];
|
|
135
|
+
return Array.isArray(val) ? val : [val];
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const [internalValue, setInternalValue] = useState(() =>
|
|
139
|
+
normalizeValue(defaultValue)
|
|
140
|
+
);
|
|
141
|
+
const isControlled = controlledValue !== undefined;
|
|
142
|
+
const value = isControlled ? normalizeValue(controlledValue) : internalValue;
|
|
143
|
+
|
|
144
|
+
const handleValueChange = useCallback(
|
|
145
|
+
(itemValue: string) => {
|
|
146
|
+
let newValue: string[];
|
|
147
|
+
|
|
148
|
+
if (type === 'single') {
|
|
149
|
+
if (value.includes(itemValue)) {
|
|
150
|
+
newValue = collapsible ? [] : [itemValue];
|
|
151
|
+
} else {
|
|
152
|
+
newValue = [itemValue];
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
if (value.includes(itemValue)) {
|
|
156
|
+
newValue = value.filter((v) => v !== itemValue);
|
|
157
|
+
} else {
|
|
158
|
+
newValue = [...value, itemValue];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!isControlled) {
|
|
163
|
+
setInternalValue(newValue);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const returnValue = type === 'single' ? newValue[0] ?? '' : newValue;
|
|
167
|
+
onValueChange?.(returnValue);
|
|
168
|
+
},
|
|
169
|
+
[type, value, collapsible, isControlled, onValueChange]
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Count children to determine last item
|
|
173
|
+
const childArray = React.Children.toArray(children);
|
|
174
|
+
const childCount = childArray.length;
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<AccordionContext.Provider value={{ type, value, onValueChange: handleValueChange }}>
|
|
178
|
+
<View
|
|
179
|
+
style={[
|
|
180
|
+
styles.container,
|
|
181
|
+
{
|
|
182
|
+
borderRadius: radius.lg,
|
|
183
|
+
borderWidth: 1,
|
|
184
|
+
borderColor: colors.border,
|
|
185
|
+
overflow: 'hidden',
|
|
186
|
+
},
|
|
187
|
+
style,
|
|
188
|
+
]}
|
|
189
|
+
>
|
|
190
|
+
{React.Children.map(children, (child, index) => {
|
|
191
|
+
if (React.isValidElement(child)) {
|
|
192
|
+
return React.cloneElement(child as React.ReactElement<any>, {
|
|
193
|
+
__isLast: index === childCount - 1,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return child;
|
|
197
|
+
})}
|
|
198
|
+
</View>
|
|
199
|
+
</AccordionContext.Provider>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// AccordionItem
|
|
204
|
+
export interface AccordionItemProps {
|
|
205
|
+
value: string;
|
|
206
|
+
children: React.ReactNode;
|
|
207
|
+
disabled?: boolean;
|
|
208
|
+
style?: ViewStyle;
|
|
209
|
+
/** @internal */
|
|
210
|
+
__isLast?: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function AccordionItem({
|
|
214
|
+
value,
|
|
215
|
+
children,
|
|
216
|
+
disabled = false,
|
|
217
|
+
style,
|
|
218
|
+
__isLast = false,
|
|
219
|
+
}: AccordionItemProps) {
|
|
220
|
+
const { colors } = useTheme();
|
|
221
|
+
const { value: selectedValues } = useAccordionContext();
|
|
222
|
+
const isOpen = selectedValues.includes(value);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<AccordionItemContext.Provider value={{ value, isOpen, isLast: __isLast }}>
|
|
226
|
+
<View
|
|
227
|
+
style={[
|
|
228
|
+
styles.item,
|
|
229
|
+
{
|
|
230
|
+
borderBottomWidth: __isLast ? 0 : 1,
|
|
231
|
+
borderBottomColor: colors.border,
|
|
232
|
+
opacity: disabled ? 0.5 : 1,
|
|
233
|
+
},
|
|
234
|
+
style,
|
|
235
|
+
]}
|
|
236
|
+
>
|
|
237
|
+
{children}
|
|
238
|
+
</View>
|
|
239
|
+
</AccordionItemContext.Provider>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// AccordionTrigger
|
|
244
|
+
export interface AccordionTriggerProps {
|
|
245
|
+
children: React.ReactNode;
|
|
246
|
+
disabled?: boolean;
|
|
247
|
+
style?: ViewStyle;
|
|
248
|
+
textStyle?: TextStyle;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function AccordionTrigger({
|
|
252
|
+
children,
|
|
253
|
+
disabled = false,
|
|
254
|
+
style,
|
|
255
|
+
textStyle,
|
|
256
|
+
}: AccordionTriggerProps) {
|
|
257
|
+
const { colors, spacing } = useTheme();
|
|
258
|
+
const { onValueChange } = useAccordionContext();
|
|
259
|
+
const { value, isOpen } = useAccordionItemContext();
|
|
260
|
+
|
|
261
|
+
const progress = useSharedValue(isOpen ? 1 : 0);
|
|
262
|
+
|
|
263
|
+
React.useEffect(() => {
|
|
264
|
+
progress.value = withSpring(isOpen ? 1 : 0, SPRING_CONFIG);
|
|
265
|
+
}, [isOpen]);
|
|
266
|
+
|
|
267
|
+
const handlePress = () => {
|
|
268
|
+
if (disabled) return;
|
|
269
|
+
haptic('selection');
|
|
270
|
+
onValueChange(value);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const chevronStyle = useAnimatedStyle(() => ({
|
|
274
|
+
transform: [
|
|
275
|
+
{ rotate: `${interpolate(progress.value, [0, 1], [0, 180])}deg` },
|
|
276
|
+
],
|
|
277
|
+
}));
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<Pressable
|
|
281
|
+
style={({ pressed }) => [
|
|
282
|
+
styles.trigger,
|
|
283
|
+
{
|
|
284
|
+
paddingVertical: spacing[4],
|
|
285
|
+
paddingHorizontal: spacing[4],
|
|
286
|
+
backgroundColor: pressed ? colors.secondary : colors.background,
|
|
287
|
+
},
|
|
288
|
+
style,
|
|
289
|
+
]}
|
|
290
|
+
onPress={handlePress}
|
|
291
|
+
disabled={disabled}
|
|
292
|
+
accessibilityRole="button"
|
|
293
|
+
accessibilityState={{ expanded: isOpen, disabled }}
|
|
294
|
+
>
|
|
295
|
+
<Text
|
|
296
|
+
style={[
|
|
297
|
+
styles.triggerText,
|
|
298
|
+
{ color: colors.foreground },
|
|
299
|
+
textStyle,
|
|
300
|
+
]}
|
|
301
|
+
>
|
|
302
|
+
{children}
|
|
303
|
+
</Text>
|
|
304
|
+
<Animated.View style={chevronStyle}>
|
|
305
|
+
<ChevronIcon color={colors.foregroundMuted} />
|
|
306
|
+
</Animated.View>
|
|
307
|
+
</Pressable>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// AccordionContent
|
|
312
|
+
export interface AccordionContentProps {
|
|
313
|
+
children: React.ReactNode;
|
|
314
|
+
style?: ViewStyle;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function AccordionContent({ children, style }: AccordionContentProps) {
|
|
318
|
+
const { colors, spacing } = useTheme();
|
|
319
|
+
const { isOpen } = useAccordionItemContext();
|
|
320
|
+
|
|
321
|
+
// Track if we've ever been opened (for initial render optimization)
|
|
322
|
+
const hasBeenOpened = React.useRef(isOpen);
|
|
323
|
+
const [measuredHeight, setMeasuredHeight] = useState(0);
|
|
324
|
+
const heightValue = useSharedValue(0);
|
|
325
|
+
const progress = useSharedValue(isOpen ? 1 : 0);
|
|
326
|
+
|
|
327
|
+
// Update ref synchronously before render logic
|
|
328
|
+
if (isOpen && !hasBeenOpened.current) {
|
|
329
|
+
hasBeenOpened.current = true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Keep a ref to isOpen for use in callbacks
|
|
333
|
+
const isOpenRef = React.useRef(isOpen);
|
|
334
|
+
isOpenRef.current = isOpen;
|
|
335
|
+
|
|
336
|
+
// Sync measured height to shared value
|
|
337
|
+
React.useEffect(() => {
|
|
338
|
+
if (measuredHeight > 0) {
|
|
339
|
+
heightValue.value = measuredHeight;
|
|
340
|
+
}
|
|
341
|
+
}, [measuredHeight]);
|
|
342
|
+
|
|
343
|
+
// Animate open/close
|
|
344
|
+
React.useEffect(() => {
|
|
345
|
+
progress.value = withSpring(isOpen ? 1 : 0, SPRING_CONFIG);
|
|
346
|
+
}, [isOpen]);
|
|
347
|
+
|
|
348
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
349
|
+
const h = event.nativeEvent.layout.height;
|
|
350
|
+
// Only accept heights LARGER than current measurement
|
|
351
|
+
// This prevents accepting small heights from lingering collapse animation
|
|
352
|
+
if (isOpenRef.current && h > 0 && h > measuredHeight) {
|
|
353
|
+
setMeasuredHeight(h);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
358
|
+
if (heightValue.value === 0) {
|
|
359
|
+
// Not measured yet - show full height so onLayout can measure
|
|
360
|
+
return { overflow: 'hidden' as const };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
height: interpolate(
|
|
365
|
+
progress.value,
|
|
366
|
+
[0, 1],
|
|
367
|
+
[0, heightValue.value],
|
|
368
|
+
Extrapolation.CLAMP
|
|
369
|
+
),
|
|
370
|
+
overflow: 'hidden' as const,
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// Don't render until first opened
|
|
375
|
+
if (!hasBeenOpened.current) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<Animated.View style={animatedStyle}>
|
|
381
|
+
<View
|
|
382
|
+
onLayout={onLayout}
|
|
383
|
+
style={[
|
|
384
|
+
styles.content,
|
|
385
|
+
{
|
|
386
|
+
// Fix height after measurement to prevent text reflow during animation
|
|
387
|
+
height: measuredHeight > 0 ? measuredHeight : undefined,
|
|
388
|
+
paddingHorizontal: spacing[4],
|
|
389
|
+
paddingBottom: spacing[4],
|
|
390
|
+
backgroundColor: colors.background,
|
|
391
|
+
},
|
|
392
|
+
style,
|
|
393
|
+
]}
|
|
394
|
+
>
|
|
395
|
+
{children}
|
|
396
|
+
</View>
|
|
397
|
+
</Animated.View>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const styles = StyleSheet.create({
|
|
402
|
+
container: {},
|
|
403
|
+
item: {},
|
|
404
|
+
trigger: {
|
|
405
|
+
flexDirection: 'row',
|
|
406
|
+
alignItems: 'center',
|
|
407
|
+
justifyContent: 'space-between',
|
|
408
|
+
},
|
|
409
|
+
triggerText: {
|
|
410
|
+
fontSize: 15,
|
|
411
|
+
fontWeight: '500',
|
|
412
|
+
flex: 1,
|
|
413
|
+
marginRight: 8,
|
|
414
|
+
},
|
|
415
|
+
content: {},
|
|
416
|
+
});
|