@gv-tech/ui-native 2.22.1 → 2.22.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/carousel.d.ts +17 -5
- package/dist/carousel.d.ts.map +1 -1
- package/dist/carousel.test.d.ts +2 -0
- package/dist/carousel.test.d.ts.map +1 -0
- package/dist/form.d.ts +30 -1
- package/dist/form.d.ts.map +1 -1
- package/dist/form.test.d.ts +2 -0
- package/dist/form.test.d.ts.map +1 -0
- package/dist/scroll-area.d.ts.map +1 -1
- package/dist/search.d.ts +9 -2
- package/dist/search.d.ts.map +1 -1
- package/dist/sonner.d.ts.map +1 -1
- package/dist/ui-native.cjs +2 -2
- package/dist/ui-native.mjs +1114 -893
- package/package.json +10 -5
- package/src/carousel.tsx +191 -14
- package/src/form.tsx +166 -4
- package/src/scroll-area.tsx +4 -2
- package/src/search.tsx +74 -12
- package/src/sonner.tsx +7 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gv-tech/ui-native",
|
|
3
|
-
"version": "2.22.
|
|
3
|
+
"version": "2.22.2",
|
|
4
4
|
"description": "React Native implementations of the GV Tech design system components",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -36,8 +36,7 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@gv-tech/design-tokens": "^2.12.0",
|
|
38
38
|
"@gv-tech/ui-core": "^2.12.0",
|
|
39
|
-
"react-
|
|
40
|
-
"react-native-worklets": "0.8.1",
|
|
39
|
+
"react-hook-form": "^7.71.1",
|
|
41
40
|
"@rn-primitives/accordion": "^1.2.0",
|
|
42
41
|
"@rn-primitives/alert-dialog": "^1.2.0",
|
|
43
42
|
"@rn-primitives/aspect-ratio": "^1.2.0",
|
|
@@ -66,22 +65,28 @@
|
|
|
66
65
|
"@rn-primitives/toggle-group": "^1.2.0",
|
|
67
66
|
"@rn-primitives/tooltip": "^1.2.0",
|
|
68
67
|
"clsx": "^2.1.1",
|
|
69
|
-
"lucide-react-native": "^1.8.0",
|
|
70
68
|
"nativewind": "^4.2.1",
|
|
71
|
-
"react-native-svg": "^15.15.3",
|
|
72
69
|
"tailwind-merge": "^3.4.1",
|
|
73
70
|
"tailwindcss": "^4.1.18"
|
|
74
71
|
},
|
|
75
72
|
"peerDependencies": {
|
|
73
|
+
"lucide-react-native": "^1.8.0",
|
|
76
74
|
"react": ">=18",
|
|
77
75
|
"react-native": ">=0.72",
|
|
78
76
|
"react-native-reanimated": "^4.2.3",
|
|
77
|
+
"react-native-svg": "^15.15.3",
|
|
79
78
|
"react-native-worklets": "^0.7.2"
|
|
80
79
|
},
|
|
81
80
|
"peerDependenciesMeta": {
|
|
81
|
+
"lucide-react-native": {
|
|
82
|
+
"optional": false
|
|
83
|
+
},
|
|
82
84
|
"react-native-reanimated": {
|
|
83
85
|
"optional": false
|
|
84
86
|
},
|
|
87
|
+
"react-native-svg": {
|
|
88
|
+
"optional": false
|
|
89
|
+
},
|
|
85
90
|
"react-native-worklets": {
|
|
86
91
|
"optional": false
|
|
87
92
|
}
|
package/src/carousel.tsx
CHANGED
|
@@ -5,25 +5,202 @@ import type {
|
|
|
5
5
|
CarouselNextBaseProps,
|
|
6
6
|
CarouselPreviousBaseProps,
|
|
7
7
|
} from '@gv-tech/ui-core';
|
|
8
|
+
import { ArrowLeft, ArrowRight } from 'lucide-react-native';
|
|
8
9
|
import * as React from 'react';
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Dimensions,
|
|
12
|
+
ScrollView,
|
|
13
|
+
View,
|
|
14
|
+
type LayoutChangeEvent,
|
|
15
|
+
type NativeScrollEvent,
|
|
16
|
+
type NativeSyntheticEvent,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { Button } from './button';
|
|
19
|
+
import { cn } from './lib/utils';
|
|
20
|
+
type CarouselApi = unknown;
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
type CarouselContextType = {
|
|
23
|
+
orientation: 'horizontal' | 'vertical';
|
|
24
|
+
scrollRef: React.RefObject<ScrollView>;
|
|
25
|
+
scrollNext: () => void;
|
|
26
|
+
scrollPrev: () => void;
|
|
27
|
+
canScrollNext: boolean;
|
|
28
|
+
canScrollPrev: boolean;
|
|
29
|
+
itemWidth: number;
|
|
30
|
+
setItemWidth: (width: number) => void;
|
|
13
31
|
};
|
|
14
32
|
|
|
15
|
-
|
|
16
|
-
return <View className={className}>{children}</View>;
|
|
17
|
-
};
|
|
33
|
+
const CarouselContext = React.createContext<CarouselContextType | null>(null);
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
function useCarousel() {
|
|
36
|
+
const context = React.useContext(CarouselContext);
|
|
37
|
+
if (!context) {
|
|
38
|
+
throw new Error('useCarousel must be used within a <Carousel />');
|
|
39
|
+
}
|
|
40
|
+
return context;
|
|
41
|
+
}
|
|
22
42
|
|
|
23
|
-
export
|
|
24
|
-
|
|
43
|
+
export type CarouselProps = CarouselBaseProps & {
|
|
44
|
+
opts?: unknown;
|
|
45
|
+
plugins?: unknown;
|
|
46
|
+
setApi?: (api: CarouselApi) => void;
|
|
25
47
|
};
|
|
26
48
|
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
export const Carousel = React.forwardRef<View, CarouselProps>(
|
|
50
|
+
({ children, className, opts, orientation = 'horizontal', setApi, plugins, ...props }, ref) => {
|
|
51
|
+
const scrollRef = React.useRef<ScrollView>(null) as React.RefObject<ScrollView>;
|
|
52
|
+
const [canScrollNext, setCanScrollNext] = React.useState(true);
|
|
53
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
54
|
+
const [itemWidth, setItemWidth] = React.useState(Dimensions.get('window').width);
|
|
55
|
+
const [currentIndex, setCurrentIndex] = React.useState(0);
|
|
56
|
+
|
|
57
|
+
const scrollNext = React.useCallback(() => {
|
|
58
|
+
scrollRef.current?.scrollTo({ x: (currentIndex + 1) * itemWidth, animated: true });
|
|
59
|
+
}, [currentIndex, itemWidth]);
|
|
60
|
+
|
|
61
|
+
const scrollPrev = React.useCallback(() => {
|
|
62
|
+
scrollRef.current?.scrollTo({ x: Math.max(0, currentIndex - 1) * itemWidth, animated: true });
|
|
63
|
+
}, [currentIndex, itemWidth]);
|
|
64
|
+
|
|
65
|
+
// Very basic API shim
|
|
66
|
+
React.useEffect(() => {
|
|
67
|
+
if (setApi) {
|
|
68
|
+
setApi({
|
|
69
|
+
scrollNext,
|
|
70
|
+
scrollPrev,
|
|
71
|
+
canScrollNext: () => canScrollNext,
|
|
72
|
+
canScrollPrev: () => canScrollPrev,
|
|
73
|
+
on: () => {},
|
|
74
|
+
off: () => {},
|
|
75
|
+
} as unknown as CarouselApi);
|
|
76
|
+
}
|
|
77
|
+
}, [setApi, scrollNext, scrollPrev, canScrollNext, canScrollPrev]);
|
|
78
|
+
|
|
79
|
+
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
80
|
+
const offsetX = event.nativeEvent.contentOffset.x;
|
|
81
|
+
const contentWidth = event.nativeEvent.contentSize.width;
|
|
82
|
+
const layoutWidth = event.nativeEvent.layoutMeasurement.width;
|
|
83
|
+
|
|
84
|
+
const newIndex = Math.round(offsetX / itemWidth);
|
|
85
|
+
setCurrentIndex(newIndex);
|
|
86
|
+
setCanScrollPrev(offsetX > 0);
|
|
87
|
+
setCanScrollNext(offsetX + layoutWidth < contentWidth);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<CarouselContext.Provider
|
|
92
|
+
value={{
|
|
93
|
+
orientation,
|
|
94
|
+
scrollRef,
|
|
95
|
+
scrollNext,
|
|
96
|
+
scrollPrev,
|
|
97
|
+
canScrollNext,
|
|
98
|
+
canScrollPrev,
|
|
99
|
+
itemWidth,
|
|
100
|
+
setItemWidth,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<View ref={ref} className={cn('relative', className)} {...props}>
|
|
104
|
+
{children}
|
|
105
|
+
</View>
|
|
106
|
+
</CarouselContext.Provider>
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
Carousel.displayName = 'Carousel';
|
|
111
|
+
|
|
112
|
+
export const CarouselContent = React.forwardRef<ScrollView, CarouselContentBaseProps>(
|
|
113
|
+
({ children, className, ...props }, ref) => {
|
|
114
|
+
const { scrollRef, orientation } = useCarousel();
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<View className="overflow-hidden">
|
|
118
|
+
<ScrollView
|
|
119
|
+
ref={scrollRef}
|
|
120
|
+
horizontal={orientation === 'horizontal'}
|
|
121
|
+
showsHorizontalScrollIndicator={false}
|
|
122
|
+
showsVerticalScrollIndicator={false}
|
|
123
|
+
pagingEnabled
|
|
124
|
+
snapToInterval={orientation === 'horizontal' ? Dimensions.get('window').width : undefined}
|
|
125
|
+
decelerationRate="fast"
|
|
126
|
+
className={cn('flex', orientation === 'horizontal' ? 'flex-row' : 'flex-col', className)}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
{children}
|
|
130
|
+
</ScrollView>
|
|
131
|
+
</View>
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
CarouselContent.displayName = 'CarouselContent';
|
|
136
|
+
|
|
137
|
+
export const CarouselItem = React.forwardRef<View, CarouselItemBaseProps>(({ children, className, ...props }, ref) => {
|
|
138
|
+
const { orientation, setItemWidth } = useCarousel();
|
|
139
|
+
|
|
140
|
+
const handleLayout = (e: LayoutChangeEvent) => {
|
|
141
|
+
if (orientation === 'horizontal') {
|
|
142
|
+
setItemWidth(e.nativeEvent.layout.width);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<View ref={ref} onLayout={handleLayout} className={cn('min-w-0 shrink-0 grow-0 basis-full', className)} {...props}>
|
|
148
|
+
{children}
|
|
149
|
+
</View>
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
CarouselItem.displayName = 'CarouselItem';
|
|
153
|
+
|
|
154
|
+
export const CarouselPrevious = React.forwardRef<React.ElementRef<typeof Button>, CarouselPreviousBaseProps>(
|
|
155
|
+
({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
|
156
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<Button
|
|
160
|
+
ref={ref}
|
|
161
|
+
variant={variant as React.ComponentProps<typeof Button>['variant']}
|
|
162
|
+
size={size as React.ComponentProps<typeof Button>['size']}
|
|
163
|
+
className={cn(
|
|
164
|
+
'absolute h-8 w-8 rounded-full',
|
|
165
|
+
orientation === 'horizontal'
|
|
166
|
+
? 'top-1/2 -left-12 -translate-y-1/2'
|
|
167
|
+
: '-top-12 left-1/2 -translate-x-1/2 rotate-90',
|
|
168
|
+
className,
|
|
169
|
+
)}
|
|
170
|
+
disabled={!canScrollPrev}
|
|
171
|
+
onPress={scrollPrev}
|
|
172
|
+
{...props}
|
|
173
|
+
>
|
|
174
|
+
<ArrowLeft className="text-foreground h-4 w-4" size={16} />
|
|
175
|
+
</Button>
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
CarouselPrevious.displayName = 'CarouselPrevious';
|
|
180
|
+
|
|
181
|
+
export const CarouselNext = React.forwardRef<React.ElementRef<typeof Button>, CarouselNextBaseProps>(
|
|
182
|
+
({ className, variant = 'outline', size = 'icon', ...props }, ref) => {
|
|
183
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<Button
|
|
187
|
+
ref={ref}
|
|
188
|
+
variant={variant as React.ComponentProps<typeof Button>['variant']}
|
|
189
|
+
size={size as React.ComponentProps<typeof Button>['size']}
|
|
190
|
+
className={cn(
|
|
191
|
+
'absolute h-8 w-8 rounded-full',
|
|
192
|
+
orientation === 'horizontal'
|
|
193
|
+
? 'top-1/2 -right-12 -translate-y-1/2'
|
|
194
|
+
: '-bottom-12 left-1/2 -translate-x-1/2 rotate-90',
|
|
195
|
+
className,
|
|
196
|
+
)}
|
|
197
|
+
disabled={!canScrollNext}
|
|
198
|
+
onPress={scrollNext}
|
|
199
|
+
{...props}
|
|
200
|
+
>
|
|
201
|
+
<ArrowRight className="text-foreground h-4 w-4" size={16} />
|
|
202
|
+
</Button>
|
|
203
|
+
);
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
CarouselNext.displayName = 'CarouselNext';
|
package/src/form.tsx
CHANGED
|
@@ -1,9 +1,171 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Controller,
|
|
4
|
+
FormProvider,
|
|
5
|
+
useFormContext,
|
|
6
|
+
type ControllerProps,
|
|
7
|
+
type FieldPath,
|
|
8
|
+
type FieldValues,
|
|
9
|
+
} from 'react-hook-form';
|
|
1
10
|
import { Text, View } from 'react-native';
|
|
2
11
|
|
|
3
|
-
|
|
12
|
+
import {
|
|
13
|
+
FormControlBaseProps,
|
|
14
|
+
FormDescriptionBaseProps,
|
|
15
|
+
FormItemBaseProps,
|
|
16
|
+
FormLabelBaseProps,
|
|
17
|
+
FormMessageBaseProps,
|
|
18
|
+
} from '@gv-tech/ui-core';
|
|
19
|
+
import { Label } from './label';
|
|
20
|
+
import { cn } from './lib/utils';
|
|
21
|
+
// Assuming we have Slot from @rn-primitives/slot
|
|
22
|
+
import * as Slot from '@rn-primitives/slot';
|
|
23
|
+
|
|
24
|
+
const Form = FormProvider;
|
|
25
|
+
|
|
26
|
+
type FormFieldContextValue<
|
|
27
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
28
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
29
|
+
> = {
|
|
30
|
+
name: TName;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
|
|
34
|
+
|
|
35
|
+
const FormField = <
|
|
36
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
37
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
38
|
+
>({
|
|
39
|
+
...props
|
|
40
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
|
41
|
+
return (
|
|
42
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
43
|
+
<Controller {...props} />
|
|
44
|
+
</FormFieldContext.Provider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const useFormField = () => {
|
|
49
|
+
const fieldContext = React.useContext(FormFieldContext);
|
|
50
|
+
const itemContext = React.useContext(FormItemContext);
|
|
51
|
+
const { getFieldState, formState } = useFormContext();
|
|
52
|
+
|
|
53
|
+
if (!fieldContext) {
|
|
54
|
+
throw new Error('useFormField should be used within <FormField>');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!itemContext) {
|
|
58
|
+
throw new Error('useFormField should be used within <FormItem>');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fieldState = getFieldState(fieldContext.name, formState);
|
|
62
|
+
|
|
63
|
+
const { id } = itemContext;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
id,
|
|
67
|
+
name: fieldContext.name,
|
|
68
|
+
formItemId: `${id}-form-item`,
|
|
69
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
70
|
+
formMessageId: `${id}-form-item-message`,
|
|
71
|
+
...fieldState,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type FormItemContextValue = {
|
|
76
|
+
id: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const FormItemContext = React.createContext<FormItemContextValue | null>(null);
|
|
80
|
+
|
|
81
|
+
const FormItem = React.forwardRef<
|
|
82
|
+
React.ElementRef<typeof View>,
|
|
83
|
+
React.ComponentPropsWithoutRef<typeof View> & FormItemBaseProps
|
|
84
|
+
>(({ className, ...props }, ref) => {
|
|
85
|
+
const id = React.useId();
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<FormItemContext.Provider value={{ id }}>
|
|
89
|
+
<View ref={ref} className={cn('space-y-2', className)} {...props} />
|
|
90
|
+
</FormItemContext.Provider>
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
FormItem.displayName = 'FormItem';
|
|
94
|
+
|
|
95
|
+
const FormLabel = React.forwardRef<
|
|
96
|
+
React.ElementRef<typeof Label>,
|
|
97
|
+
React.ComponentPropsWithoutRef<typeof Label> & FormLabelBaseProps
|
|
98
|
+
>(({ className, ...props }, ref) => {
|
|
99
|
+
const { error, formItemId } = useFormField();
|
|
100
|
+
|
|
101
|
+
return <Label ref={ref} className={cn(error && 'text-destructive', className)} nativeID={formItemId} {...props} />;
|
|
102
|
+
});
|
|
103
|
+
FormLabel.displayName = 'FormLabel';
|
|
104
|
+
|
|
105
|
+
const FormControl = React.forwardRef<
|
|
106
|
+
React.ElementRef<typeof Slot.Slot>,
|
|
107
|
+
React.ComponentPropsWithoutRef<typeof Slot.Slot>
|
|
108
|
+
>(({ ...props }, ref) => {
|
|
109
|
+
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Slot.Slot
|
|
113
|
+
ref={ref}
|
|
114
|
+
nativeID={formItemId}
|
|
115
|
+
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
|
116
|
+
aria-invalid={!!error}
|
|
117
|
+
{...props}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
FormControl.displayName = 'FormControl';
|
|
122
|
+
|
|
123
|
+
const FormDescription = React.forwardRef<
|
|
124
|
+
React.ElementRef<typeof Text>,
|
|
125
|
+
React.ComponentPropsWithoutRef<typeof Text> & FormDescriptionBaseProps
|
|
126
|
+
>(({ className, ...props }, ref) => {
|
|
127
|
+
const { formDescriptionId } = useFormField();
|
|
128
|
+
|
|
4
129
|
return (
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
130
|
+
<Text
|
|
131
|
+
ref={ref}
|
|
132
|
+
nativeID={formDescriptionId}
|
|
133
|
+
className={cn('text-muted-foreground text-[13px]', className)}
|
|
134
|
+
{...props}
|
|
135
|
+
/>
|
|
8
136
|
);
|
|
137
|
+
});
|
|
138
|
+
FormDescription.displayName = 'FormDescription';
|
|
139
|
+
|
|
140
|
+
const FormMessage = React.forwardRef<
|
|
141
|
+
React.ElementRef<typeof Text>,
|
|
142
|
+
React.ComponentPropsWithoutRef<typeof Text> & FormMessageBaseProps
|
|
143
|
+
>(({ className, children, ...props }, ref) => {
|
|
144
|
+
const { error, formMessageId } = useFormField();
|
|
145
|
+
const body = error ? String(error?.message ?? '') : children;
|
|
146
|
+
|
|
147
|
+
if (!body) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Text
|
|
153
|
+
ref={ref}
|
|
154
|
+
nativeID={formMessageId}
|
|
155
|
+
className={cn('text-destructive text-[13px] font-medium', className)}
|
|
156
|
+
{...props}
|
|
157
|
+
>
|
|
158
|
+
{body}
|
|
159
|
+
</Text>
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
FormMessage.displayName = 'FormMessage';
|
|
163
|
+
|
|
164
|
+
export { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, useFormField };
|
|
165
|
+
export type {
|
|
166
|
+
FormControlBaseProps as FormControlProps,
|
|
167
|
+
FormDescriptionBaseProps as FormDescriptionProps,
|
|
168
|
+
FormItemBaseProps as FormItemProps,
|
|
169
|
+
FormLabelBaseProps as FormLabelProps,
|
|
170
|
+
FormMessageBaseProps as FormMessageProps,
|
|
9
171
|
};
|
package/src/scroll-area.tsx
CHANGED
|
@@ -9,8 +9,8 @@ export const ScrollArea = React.forwardRef<ScrollView, ScrollAreaBaseProps>(
|
|
|
9
9
|
<ScrollView
|
|
10
10
|
ref={ref}
|
|
11
11
|
className={cn('flex-1', className)}
|
|
12
|
-
showsVerticalScrollIndicator={
|
|
13
|
-
showsHorizontalScrollIndicator={
|
|
12
|
+
showsVerticalScrollIndicator={true}
|
|
13
|
+
showsHorizontalScrollIndicator={true}
|
|
14
14
|
{...props}
|
|
15
15
|
>
|
|
16
16
|
<View>{children}</View>
|
|
@@ -21,6 +21,8 @@ export const ScrollArea = React.forwardRef<ScrollView, ScrollAreaBaseProps>(
|
|
|
21
21
|
ScrollArea.displayName = 'ScrollArea';
|
|
22
22
|
|
|
23
23
|
export const ScrollBar: React.FC<ScrollBarBaseProps> = () => {
|
|
24
|
+
// Natively, we rely on the ScrollView's built-in indicators.
|
|
25
|
+
// This component is a no-op shim for contract compatibility.
|
|
24
26
|
return null;
|
|
25
27
|
};
|
|
26
28
|
ScrollBar.displayName = 'ScrollBar';
|
package/src/search.tsx
CHANGED
|
@@ -1,17 +1,79 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SearchBaseProps, SearchTriggerBaseProps } from '@gv-tech/ui-core';
|
|
2
|
+
import { Search as SearchIcon } from 'lucide-react-native';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Platform, Text, View } from 'react-native';
|
|
5
|
+
import { Button } from './button';
|
|
6
|
+
import { Dialog } from './dialog';
|
|
7
|
+
import { cn } from './lib/utils';
|
|
2
8
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
export type SearchProps = SearchBaseProps;
|
|
10
|
+
|
|
11
|
+
export function Search({ children, open: customOpen, onOpenChange }: SearchProps) {
|
|
12
|
+
const [open, setOpen] = React.useState(false);
|
|
13
|
+
const isControlled = customOpen !== undefined;
|
|
14
|
+
const isOpen = isControlled ? (customOpen as boolean) : open;
|
|
15
|
+
|
|
16
|
+
const setIsOpen = React.useCallback(
|
|
17
|
+
(value: boolean) => {
|
|
18
|
+
if (isControlled) {
|
|
19
|
+
onOpenChange?.(value);
|
|
20
|
+
} else {
|
|
21
|
+
setOpen(value);
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
[isControlled, onOpenChange],
|
|
8
25
|
);
|
|
9
|
-
};
|
|
10
26
|
|
|
11
|
-
export const SearchTrigger = () => {
|
|
12
27
|
return (
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
</
|
|
28
|
+
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
29
|
+
{children}
|
|
30
|
+
</Dialog>
|
|
16
31
|
);
|
|
17
|
-
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SearchTriggerProps
|
|
35
|
+
extends Omit<React.ComponentPropsWithoutRef<typeof Button>, 'variant'>, SearchTriggerBaseProps {}
|
|
36
|
+
|
|
37
|
+
export const SearchTrigger = React.forwardRef<React.ElementRef<typeof Button>, SearchTriggerProps>(
|
|
38
|
+
({ className, placeholder, variant = 'default', responsive = false, ...props }, ref) => {
|
|
39
|
+
const defaultPlaceholder = variant === 'compact' ? 'Search...' : 'Search docs...';
|
|
40
|
+
const activePlaceholder = placeholder || defaultPlaceholder;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Button
|
|
44
|
+
variant="outline"
|
|
45
|
+
className={cn(
|
|
46
|
+
'relative h-12 flex-row justify-start pl-3 text-sm transition-all sm:h-9',
|
|
47
|
+
variant === 'default'
|
|
48
|
+
? 'w-full pr-12'
|
|
49
|
+
: cn('w-12 px-0 sm:w-9 sm:justify-center', responsive && 'md:w-48 md:justify-start md:px-3 md:pr-12'),
|
|
50
|
+
className,
|
|
51
|
+
)}
|
|
52
|
+
ref={ref}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
<View className="flex-row items-center gap-2">
|
|
56
|
+
<SearchIcon className="text-muted-foreground shrink-0" size={18} />
|
|
57
|
+
<Text
|
|
58
|
+
className={cn(
|
|
59
|
+
'text-muted-foreground truncate',
|
|
60
|
+
variant === 'compact' && (responsive ? 'hidden md:flex' : 'hidden'),
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{activePlaceholder}
|
|
64
|
+
</Text>
|
|
65
|
+
</View>
|
|
66
|
+
<View
|
|
67
|
+
className={cn(
|
|
68
|
+
'bg-muted absolute top-2 right-2 hidden h-6 flex-row items-center gap-1 rounded border px-1.5 opacity-100',
|
|
69
|
+
variant === 'default' && Platform.OS !== 'android' && Platform.OS !== 'ios' && 'sm:flex',
|
|
70
|
+
variant === 'compact' && responsive && Platform.OS !== 'android' && Platform.OS !== 'ios' && 'md:flex',
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<Text className="text-muted-foreground font-mono text-[10px] font-medium">⌘K</Text>
|
|
74
|
+
</View>
|
|
75
|
+
</Button>
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
SearchTrigger.displayName = 'SearchTrigger';
|
package/src/sonner.tsx
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { SonnerBaseProps } from '@gv-tech/ui-core';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { ToastProvider, ToastViewport } from './toast';
|
|
4
4
|
|
|
5
|
-
export const Toaster: React.FC<SonnerBaseProps> = (
|
|
6
|
-
return
|
|
5
|
+
export const Toaster: React.FC<SonnerBaseProps> = () => {
|
|
6
|
+
return (
|
|
7
|
+
<ToastProvider>
|
|
8
|
+
<ToastViewport />
|
|
9
|
+
</ToastProvider>
|
|
10
|
+
);
|
|
7
11
|
};
|