@gv-tech/ui-native 2.24.0 → 2.25.1

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.24.0",
3
+ "version": "2.25.1",
4
4
  "description": "React Native implementations of the GV Tech design system components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,6 +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-community/datetimepicker": "^9.1.0",
39
40
  "@rn-primitives/accordion": "^1.2.0",
40
41
  "@rn-primitives/alert-dialog": "^1.2.0",
41
42
  "@rn-primitives/aspect-ratio": "^1.2.0",
package/src/calendar.tsx CHANGED
@@ -1,7 +1,56 @@
1
1
  import type { CalendarBaseProps } from '@gv-tech/ui-core';
2
+ import DateTimePicker from '@react-native-community/datetimepicker';
2
3
  import * as React from 'react';
3
4
  import { View } from 'react-native';
5
+ import { cn } from './lib/utils';
4
6
 
5
- export const Calendar: React.FC<CalendarBaseProps> = ({ className }) => {
6
- return <View className={className} />;
7
- };
7
+ export type CalendarProps = Omit<
8
+ React.ComponentProps<typeof DateTimePicker>,
9
+ 'value' | 'className' | 'display' | 'minuteInterval'
10
+ > &
11
+ CalendarBaseProps & {
12
+ value?: Date;
13
+ onChange?: (event: unknown, date?: Date) => void;
14
+ };
15
+
16
+ export const Calendar = React.forwardRef<View, CalendarProps>(
17
+ ({ className, value, onChange, showOutsideDays, ...props }, ref) => {
18
+ // If no value is provided, default to current date so the picker doesn't crash
19
+ const [date, setDate] = React.useState<Date>(value || new Date());
20
+
21
+ // Sync internal state if external value changes
22
+ React.useEffect(() => {
23
+ if (value) {
24
+ setDate(value);
25
+ }
26
+ }, [value]);
27
+
28
+ const handleValueChange = (event: unknown, selectedDate: Date) => {
29
+ setDate(selectedDate);
30
+ if (onChange) {
31
+ onChange(event, selectedDate);
32
+ }
33
+ };
34
+
35
+ const handleDismiss = () => {
36
+ if (onChange) {
37
+ onChange({ type: 'dismissed' }, date);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <View ref={ref} className={cn('bg-background overflow-hidden rounded-md', className)}>
43
+ <DateTimePicker
44
+ value={date}
45
+ mode="date"
46
+ display="inline"
47
+ onValueChange={handleValueChange}
48
+ onDismiss={handleDismiss}
49
+ {...props}
50
+ />
51
+ </View>
52
+ );
53
+ },
54
+ );
55
+
56
+ Calendar.displayName = 'Calendar';
package/src/combobox.tsx CHANGED
@@ -15,37 +15,126 @@ import type {
15
15
  ComboboxTriggerBaseProps,
16
16
  ComboboxValueBaseProps,
17
17
  } from '@gv-tech/ui-core';
18
+ import { Check, ChevronDown, X } from 'lucide-react-native';
18
19
  import * as React from 'react';
19
- import { Text, View } from 'react-native';
20
+ import { Pressable, TextInput, View } from 'react-native';
21
+ import { Dialog, DialogContent, DialogTrigger } from './dialog';
22
+ import { wrapTextChildren } from './lib/render-native';
20
23
  import { cn } from './lib/utils';
24
+ import { Text } from './text';
25
+
26
+ // Context to share state across compound components
27
+ const ComboboxContext = React.createContext<{
28
+ open: boolean;
29
+ setOpen: (open: boolean) => void;
30
+ value: string;
31
+ setValue: (value: string) => void;
32
+ searchQuery: string;
33
+ setSearchQuery: (query: string) => void;
34
+ } | null>(null);
35
+
36
+ function useCombobox() {
37
+ const context = React.useContext(ComboboxContext);
38
+ if (!context) {
39
+ throw new Error('Combobox components must be rendered within a Combobox provider');
40
+ }
41
+ return context;
42
+ }
43
+
44
+ export interface ComboboxProps extends React.ComponentProps<typeof View> {
45
+ value?: string;
46
+ onValueChange?: (value: string) => void;
47
+ }
48
+
49
+ function Combobox({ className, value: controlledValue, onValueChange, ...props }: ComboboxProps) {
50
+ const [open, setOpen] = React.useState(false);
51
+ const [internalValue, setInternalValue] = React.useState(controlledValue || '');
52
+ const [searchQuery, setSearchQuery] = React.useState('');
53
+
54
+ const value = controlledValue !== undefined ? controlledValue : internalValue;
55
+ const setValue = (newValue: string) => {
56
+ setInternalValue(newValue);
57
+ onValueChange?.(newValue);
58
+ setOpen(false);
59
+ };
21
60
 
22
- function Combobox({ className, ...props }: React.ComponentProps<typeof View>) {
23
61
  return (
24
- <View className={cn('border-destructive/50 rounded-md border border-dashed p-4', className)} {...props}>
25
- <Text className="text-destructive font-mono text-xs">Combobox (Not Implemented)</Text>
26
- </View>
62
+ <ComboboxContext.Provider value={{ open, setOpen, value, setValue, searchQuery, setSearchQuery }}>
63
+ <Dialog open={open} onOpenChange={setOpen}>
64
+ <View className={cn('relative', className)} {...props} />
65
+ </Dialog>
66
+ </ComboboxContext.Provider>
27
67
  );
28
68
  }
29
69
 
30
- function ComboboxValue({ className, ...props }: React.ComponentProps<typeof View> & ComboboxValueBaseProps) {
31
- return <View className={className} {...props} />;
70
+ function ComboboxValue({ className, children, ...props }: React.ComponentProps<typeof View> & ComboboxValueBaseProps) {
71
+ const { value } = useCombobox();
72
+ return (
73
+ <View className={className} {...props}>
74
+ <Text className="text-sm">{children || value || 'Select an item...'}</Text>
75
+ </View>
76
+ );
32
77
  }
33
78
 
34
- function ComboboxTrigger({ className, ...props }: React.ComponentProps<typeof View> & ComboboxTriggerBaseProps) {
35
- return <View className={className} {...props} />;
79
+ function ComboboxTrigger({
80
+ className,
81
+ children,
82
+ ...props
83
+ }: React.ComponentProps<typeof Pressable> & ComboboxTriggerBaseProps) {
84
+ return (
85
+ <DialogTrigger asChild>
86
+ <Pressable
87
+ className={cn(
88
+ 'border-input bg-background text-foreground flex h-10 w-full flex-row items-center justify-between rounded-md border px-3 py-2 text-sm',
89
+ className,
90
+ )}
91
+ {...props}
92
+ >
93
+ <View className="flex-1 flex-row items-center">{wrapTextChildren(children, Text)}</View>
94
+ <ChevronDown size={16} className="text-muted-foreground opacity-50" />
95
+ </Pressable>
96
+ </DialogTrigger>
97
+ );
36
98
  }
37
99
 
38
- function ComboboxClear({ className, ...props }: React.ComponentProps<typeof View> & ComboboxClearBaseProps) {
39
- return <View className={className} {...props} />;
100
+ function ComboboxClear({
101
+ className,
102
+ children,
103
+ ...props
104
+ }: React.ComponentProps<typeof Pressable> & ComboboxClearBaseProps) {
105
+ const { setValue } = useCombobox();
106
+ return (
107
+ <Pressable
108
+ className={cn('flex items-center justify-center p-1', className)}
109
+ onPress={() => setValue('')}
110
+ {...props}
111
+ >
112
+ {children || <X size={14} className="text-muted-foreground" />}
113
+ </Pressable>
114
+ );
40
115
  }
41
116
 
42
117
  function ComboboxInput({
43
118
  className,
44
119
  showClear,
45
120
  showTrigger,
121
+ placeholder = 'Search...',
46
122
  ...props
47
- }: React.ComponentProps<typeof View> & ComboboxInputBaseProps) {
48
- return <View className={className} {...props} />;
123
+ }: React.ComponentProps<typeof TextInput> & ComboboxInputBaseProps) {
124
+ const { searchQuery, setSearchQuery } = useCombobox();
125
+ return (
126
+ <View className="border-border flex flex-row items-center border-b px-3">
127
+ <TextInput
128
+ className={cn('flex h-12 w-full rounded-md bg-transparent py-3 text-sm outline-none', className)}
129
+ placeholder={placeholder}
130
+ placeholderTextColor="#a1a1aa"
131
+ value={searchQuery}
132
+ onChangeText={setSearchQuery}
133
+ autoFocus
134
+ {...props}
135
+ />
136
+ </View>
137
+ );
49
138
  }
50
139
 
51
140
  function ComboboxContent({
@@ -57,47 +146,121 @@ function ComboboxContent({
57
146
  anchor,
58
147
  ...props
59
148
  }: React.ComponentProps<typeof View> & ComboboxContentBaseProps) {
60
- return <View className={className} {...props} />;
149
+ return (
150
+ <DialogContent className="p-0">
151
+ <View
152
+ className={cn('bg-popover flex max-h-96 w-full flex-col overflow-hidden rounded-md', className)}
153
+ {...props}
154
+ />
155
+ </DialogContent>
156
+ );
61
157
  }
62
158
 
63
159
  function ComboboxList({ className, ...props }: React.ComponentProps<typeof View> & ComboboxListBaseProps) {
64
- return <View className={className} {...props} />;
160
+ return <View className={cn('max-h-[300px] overflow-hidden', className)} {...props} />;
65
161
  }
66
162
 
67
- function ComboboxItem({ className, ...props }: React.ComponentProps<typeof View> & ComboboxItemBaseProps) {
68
- return <View className={className} {...props} />;
163
+ export type ComboboxItemProps = React.ComponentProps<typeof Pressable> &
164
+ ComboboxItemBaseProps & {
165
+ value: string;
166
+ };
167
+
168
+ function ComboboxItem({ className, children, value: itemValue, ...props }: ComboboxItemProps) {
169
+ const { value, setValue, searchQuery } = useCombobox();
170
+
171
+ // Basic filtering mechanism
172
+ const textContent = React.Children.toArray(children).join('').toLowerCase();
173
+ if (
174
+ searchQuery &&
175
+ !textContent.includes(searchQuery.toLowerCase()) &&
176
+ !itemValue.toLowerCase().includes(searchQuery.toLowerCase())
177
+ ) {
178
+ return null;
179
+ }
180
+
181
+ const isSelected = value === itemValue;
182
+
183
+ return (
184
+ <Pressable
185
+ className={cn(
186
+ 'active:bg-accent relative flex w-full flex-row items-center rounded-sm py-2.5 pr-8 pl-2 text-sm outline-none',
187
+ isSelected && 'bg-accent',
188
+ className,
189
+ )}
190
+ onPress={() => setValue(itemValue)}
191
+ {...props}
192
+ >
193
+ <View className="flex-1">
194
+ {wrapTextChildren(children, Text, { className: isSelected ? 'font-semibold' : '' })}
195
+ </View>
196
+ {isSelected && (
197
+ <View className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
198
+ <Check size={16} className="text-foreground" />
199
+ </View>
200
+ )}
201
+ </Pressable>
202
+ );
69
203
  }
70
204
 
71
205
  function ComboboxGroup({ className, ...props }: React.ComponentProps<typeof View> & ComboboxGroupBaseProps) {
72
- return <View className={className} {...props} />;
206
+ return <View className={cn('text-foreground overflow-hidden p-1', className)} {...props} />;
73
207
  }
74
208
 
75
209
  function ComboboxLabel({ className, ...props }: React.ComponentProps<typeof View> & ComboboxLabelBaseProps) {
76
- return <View className={className} {...props} />;
210
+ return <View className={cn('text-muted-foreground px-2 py-1.5 text-xs font-semibold', className)} {...props} />;
77
211
  }
78
212
 
79
213
  function ComboboxCollection({ className, ...props }: React.ComponentProps<typeof View> & ComboboxCollectionBaseProps) {
80
214
  return <View className={className} {...props} />;
81
215
  }
82
216
 
83
- function ComboboxEmpty({ className, ...props }: React.ComponentProps<typeof View> & ComboboxEmptyBaseProps) {
84
- return <View className={className} {...props} />;
217
+ function ComboboxEmpty({ className, children, ...props }: React.ComponentProps<typeof View> & ComboboxEmptyBaseProps) {
218
+ const { searchQuery } = useCombobox();
219
+
220
+ if (!searchQuery) {
221
+ return null;
222
+ }
223
+
224
+ return (
225
+ <View className={cn('py-6 text-center text-sm', className)} {...props}>
226
+ <Text className="text-muted-foreground text-center">{children || 'No results found.'}</Text>
227
+ </View>
228
+ );
85
229
  }
86
230
 
87
231
  function ComboboxSeparator({ className, ...props }: React.ComponentProps<typeof View> & ComboboxSeparatorBaseProps) {
88
- return <View className={className} {...props} />;
232
+ return <View className={cn('bg-muted -mx-1 h-px', className)} {...props} />;
89
233
  }
90
234
 
91
235
  function ComboboxChips({ className, ...props }: React.ComponentProps<typeof View> & ComboboxChipsBaseProps) {
92
- return <View className={className} {...props} />;
236
+ return <View className={cn('flex flex-row flex-wrap gap-1', className)} {...props} />;
93
237
  }
94
238
 
95
- function ComboboxChip({ className, showRemove, ...props }: React.ComponentProps<typeof View> & ComboboxChipBaseProps) {
96
- return <View className={className} {...props} />;
239
+ function ComboboxChip({
240
+ className,
241
+ showRemove,
242
+ children,
243
+ ...props
244
+ }: React.ComponentProps<typeof View> & ComboboxChipBaseProps) {
245
+ return (
246
+ <View
247
+ className={cn(
248
+ 'bg-secondary text-secondary-foreground flex flex-row items-center rounded-md px-2 py-1',
249
+ className,
250
+ )}
251
+ {...props}
252
+ >
253
+ {wrapTextChildren(children, Text, { className: 'text-xs' })}
254
+ {showRemove && <X size={12} className="ml-1 opacity-50" />}
255
+ </View>
256
+ );
97
257
  }
98
258
 
99
- function ComboboxChipsInput({ className, ...props }: React.ComponentProps<typeof View> & ComboboxChipsInputBaseProps) {
100
- return <View className={className} {...props} />;
259
+ function ComboboxChipsInput({
260
+ className,
261
+ ...props
262
+ }: React.ComponentProps<typeof TextInput> & ComboboxChipsInputBaseProps) {
263
+ return <TextInput className={cn('flex-1 bg-transparent px-2 py-1 outline-none', className)} {...props} />;
101
264
  }
102
265
 
103
266
  function useComboboxAnchor() {
package/src/dialog.tsx CHANGED
@@ -2,7 +2,7 @@ import { DialogBaseProps, DialogContentBaseProps } from '@gv-tech/ui-core';
2
2
  import * as DialogPrimitive from '@rn-primitives/dialog';
3
3
  import { X } from 'lucide-react-native';
4
4
  import * as React from 'react';
5
- import { StyleSheet, View, type ViewStyle } from 'react-native';
5
+ import { Platform, View, type StyleProp, type ViewStyle } from 'react-native';
6
6
  import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
7
7
 
8
8
  import { cn } from './lib/utils';
@@ -28,13 +28,28 @@ export type DialogOverlayProps = React.ComponentPropsWithoutRef<typeof DialogPri
28
28
  export type DialogOverlayRef = React.ComponentRef<typeof DialogPrimitive.Overlay>;
29
29
 
30
30
  const DialogOverlay: React.ForwardRefExoticComponent<DialogOverlayProps & React.RefAttributes<DialogOverlayRef>> =
31
- React.forwardRef<DialogOverlayRef, DialogOverlayProps>(({ className, ...props }, ref) => {
31
+ React.forwardRef<DialogOverlayRef, DialogOverlayProps>(({ className, style, ...props }, ref) => {
32
32
  return (
33
- <DialogPrimitive.Overlay style={StyleSheet.absoluteFill} asChild ref={ref} {...props}>
33
+ <DialogPrimitive.Overlay
34
+ style={[
35
+ {
36
+ position: Platform.OS === 'web' ? 'fixed' : 'absolute',
37
+ top: 0,
38
+ right: 0,
39
+ bottom: 0,
40
+ left: 0,
41
+ zIndex: 50,
42
+ } as unknown as ViewStyle,
43
+ style as StyleProp<ViewStyle>,
44
+ ]}
45
+ asChild
46
+ ref={ref}
47
+ {...props}
48
+ >
34
49
  <Animated.View
35
50
  entering={FadeIn.duration(150)}
36
51
  exiting={FadeOut.duration(150)}
37
- className={cn('z-50 flex items-center justify-center bg-black/80 p-2', className)}
52
+ className={cn('flex items-center justify-center bg-black/80 p-2', className)}
38
53
  />
39
54
  </DialogPrimitive.Overlay>
40
55
  );
@@ -45,31 +60,63 @@ export type DialogContentRef = React.ComponentRef<typeof DialogPrimitive.Content
45
60
  const DialogContent: React.ForwardRefExoticComponent<DialogContentProps & React.RefAttributes<DialogContentRef>> =
46
61
  React.forwardRef<DialogContentRef, DialogContentProps>(
47
62
  ({ className, children, portalHost, overlayClassName, overlayStyle, ...props }, ref) => {
63
+ const PlatformWrapper = React.useCallback(({ children }: { children: React.ReactNode }) => {
64
+ if (Platform.OS === 'web') {
65
+ return <>{children}</>;
66
+ }
67
+ return (
68
+ <View
69
+ pointerEvents="box-none"
70
+ style={{
71
+ position: 'absolute',
72
+ top: 0,
73
+ right: 0,
74
+ bottom: 0,
75
+ left: 0,
76
+ zIndex: 50,
77
+ alignItems: 'center',
78
+ justifyContent: 'center',
79
+ padding: 16,
80
+ }}
81
+ >
82
+ {children}
83
+ </View>
84
+ );
85
+ }, []);
86
+
48
87
  return (
49
88
  <DialogPortal hostName={portalHost}>
50
89
  <DialogOverlay className={overlayClassName} style={overlayStyle} />
51
- <DialogPrimitive.Content ref={ref} asChild {...props}>
52
- <Animated.View
53
- entering={FadeIn.duration(150)}
54
- exiting={FadeOut.duration(150)}
55
- className={cn(
56
- 'border-border bg-background z-50 w-full max-w-lg gap-4 rounded-xl border p-6 shadow-lg sm:rounded-lg',
57
- className,
58
- )}
59
- >
60
- {children}
61
- <DialogPrimitive.Close
62
- className={
63
- 'ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none'
64
- }
90
+ <PlatformWrapper>
91
+ <DialogPrimitive.Content ref={ref} {...props}>
92
+ <View
93
+ pointerEvents="box-none"
94
+ className="absolute inset-0 z-50 flex items-center justify-center"
95
+ style={Platform.OS === 'web' ? ({ position: 'fixed' } as unknown as ViewStyle) : undefined}
65
96
  >
66
- <X size={18} className="text-muted-foreground" />
67
- <View className="sr-only">
68
- <DialogPrimitive.Title>Close</DialogPrimitive.Title>
69
- </View>
70
- </DialogPrimitive.Close>
71
- </Animated.View>
72
- </DialogPrimitive.Content>
97
+ <Animated.View
98
+ entering={FadeIn.duration(150)}
99
+ exiting={FadeOut.duration(150)}
100
+ className={cn(
101
+ 'border-border bg-background w-full max-w-lg gap-4 rounded-xl border p-6 shadow-lg sm:rounded-lg',
102
+ className,
103
+ )}
104
+ >
105
+ {children}
106
+ <DialogPrimitive.Close
107
+ className={
108
+ 'ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none'
109
+ }
110
+ >
111
+ <X size={18} className="text-muted-foreground" />
112
+ <View className="sr-only">
113
+ <DialogPrimitive.Title>Close</DialogPrimitive.Title>
114
+ </View>
115
+ </DialogPrimitive.Close>
116
+ </Animated.View>
117
+ </View>
118
+ </DialogPrimitive.Content>
119
+ </PlatformWrapper>
73
120
  </DialogPortal>
74
121
  );
75
122
  },
@@ -33,28 +33,31 @@ export function ThemeToggle({ variant = 'binary', onThemeChange, customTheme, cl
33
33
  };
34
34
 
35
35
  const IconToggle = () => (
36
- <View className="flex items-center justify-center">
37
- <Sun
38
- size={18}
36
+ <View className="flex h-6 w-6 items-center justify-center">
37
+ <View
39
38
  className={cn(
40
- 'text-foreground transition-all',
41
- !isSystem && !isDark ? 'scale-100 rotate-0 opacity-100' : 'absolute scale-0 -rotate-90 opacity-0',
39
+ 'items-center justify-center transition-all',
40
+ !isSystem && !isDark ? 'rotate-0 opacity-100' : 'absolute -rotate-90 opacity-0',
42
41
  )}
43
- />
44
- <Moon
45
- size={18}
42
+ >
43
+ <Sun size={18} className="text-foreground" />
44
+ </View>
45
+ <View
46
46
  className={cn(
47
- 'text-foreground transition-all',
48
- !isSystem && isDark ? 'scale-100 rotate-0 opacity-100' : 'absolute scale-0 rotate-90 opacity-0',
47
+ 'items-center justify-center transition-all',
48
+ !isSystem && isDark ? 'rotate-0 opacity-100' : 'absolute rotate-90 opacity-0',
49
49
  )}
50
- />
51
- <SunMoon
52
- size={18}
50
+ >
51
+ <Moon size={18} className="text-foreground" />
52
+ </View>
53
+ <View
53
54
  className={cn(
54
- 'text-foreground transition-all',
55
- isSystem ? 'scale-100 rotate-0 opacity-100' : 'absolute scale-0 rotate-90 opacity-0',
55
+ 'items-center justify-center transition-all',
56
+ isSystem ? 'rotate-0 opacity-100' : 'absolute rotate-90 opacity-0',
56
57
  )}
57
- />
58
+ >
59
+ <SunMoon size={18} className="text-foreground" />
60
+ </View>
58
61
  </View>
59
62
  );
60
63