@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gv-tech/ui-native",
3
- "version": "2.22.1",
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-native-reanimated": "4.3.0",
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 { View } from 'react-native';
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
- export const Carousel: React.FC<CarouselBaseProps> = ({ children, className }) => {
12
- return <View className={className}>{children}</View>;
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
- export const CarouselContent: React.FC<CarouselContentBaseProps> = ({ children, className }) => {
16
- return <View className={className}>{children}</View>;
17
- };
33
+ const CarouselContext = React.createContext<CarouselContextType | null>(null);
18
34
 
19
- export const CarouselItem: React.FC<CarouselItemBaseProps> = ({ children, className }) => {
20
- return <View className={className}>{children}</View>;
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 const CarouselPrevious: React.FC<CarouselPreviousBaseProps> = ({ className }) => {
24
- return <View className={className} />;
43
+ export type CarouselProps = CarouselBaseProps & {
44
+ opts?: unknown;
45
+ plugins?: unknown;
46
+ setApi?: (api: CarouselApi) => void;
25
47
  };
26
48
 
27
- export const CarouselNext: React.FC<CarouselNextBaseProps> = ({ className }) => {
28
- return <View className={className} />;
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
- export const Form = () => {
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
- <View>
6
- <Text>form is not yet implemented for React Native</Text>
7
- </View>
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
  };
@@ -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={false}
13
- showsHorizontalScrollIndicator={false}
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 { Text, View } from 'react-native';
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 const Search = () => {
4
- return (
5
- <View>
6
- <Text>Search is not yet implemented for React Native</Text>
7
- </View>
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
- <View>
14
- <Text>SearchTrigger is not yet implemented for React Native</Text>
15
- </View>
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 { View } from 'react-native';
3
+ import { ToastProvider, ToastViewport } from './toast';
4
4
 
5
- export const Toaster: React.FC<SonnerBaseProps> = ({ className }) => {
6
- return <View className={className} />;
5
+ export const Toaster: React.FC<SonnerBaseProps> = () => {
6
+ return (
7
+ <ToastProvider>
8
+ <ToastViewport />
9
+ </ToastProvider>
10
+ );
7
11
  };