cnnative-ui 1.0.0

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.
Files changed (175) hide show
  1. package/README.md +34 -0
  2. package/babel.config.js +6 -0
  3. package/jest.config.js +22 -0
  4. package/jest.init.js +5 -0
  5. package/jest.setup.js +173 -0
  6. package/package.json +87 -0
  7. package/src/__tests__/a11y/accessibility.test.tsx +33 -0
  8. package/src/__tests__/components/badge.test.tsx +25 -0
  9. package/src/__tests__/components/button.test.tsx +53 -0
  10. package/src/__tests__/components/card.test.tsx +28 -0
  11. package/src/__tests__/components/input.test.tsx +33 -0
  12. package/src/__tests__/hooks/use-controllable.test.ts +58 -0
  13. package/src/__tests__/integration.test.tsx +35 -0
  14. package/src/__tests__/lib/utils.test.ts +23 -0
  15. package/src/__tests__/mocks/handlers.ts +19 -0
  16. package/src/components/accordion/accordion.tsx +143 -0
  17. package/src/components/accordion/index.ts +1 -0
  18. package/src/components/alert/alert.tsx +65 -0
  19. package/src/components/alert/index.ts +1 -0
  20. package/src/components/alert-dialog/alert-dialog.tsx +145 -0
  21. package/src/components/alert-dialog/index.ts +1 -0
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +18 -0
  23. package/src/components/aspect-ratio/index.ts +1 -0
  24. package/src/components/avatar/avatar.tsx +93 -0
  25. package/src/components/avatar/index.ts +1 -0
  26. package/src/components/badge/badge.tsx +64 -0
  27. package/src/components/badge/index.ts +1 -0
  28. package/src/components/breadcrumb/breadcrumb.tsx +75 -0
  29. package/src/components/breadcrumb/index.ts +1 -0
  30. package/src/components/button/button.tsx +119 -0
  31. package/src/components/button/index.ts +1 -0
  32. package/src/components/card/card.tsx +40 -0
  33. package/src/components/card/index.ts +1 -0
  34. package/src/components/checkbox/checkbox.tsx +87 -0
  35. package/src/components/checkbox/index.ts +1 -0
  36. package/src/components/collapsible/collapsible.tsx +92 -0
  37. package/src/components/collapsible/index.ts +1 -0
  38. package/src/components/context-menu/context-menu.tsx +121 -0
  39. package/src/components/context-menu/index.ts +1 -0
  40. package/src/components/dialog/dialog.tsx +124 -0
  41. package/src/components/dialog/index.ts +1 -0
  42. package/src/components/dropdown-menu/dropdown-menu.tsx +145 -0
  43. package/src/components/dropdown-menu/index.ts +1 -0
  44. package/src/components/form/form.tsx +84 -0
  45. package/src/components/form/index.ts +1 -0
  46. package/src/components/input/index.ts +1 -0
  47. package/src/components/input/input.tsx +115 -0
  48. package/src/components/label/index.ts +1 -0
  49. package/src/components/label/label.tsx +13 -0
  50. package/src/components/navigation-menu/index.ts +1 -0
  51. package/src/components/navigation-menu/navigation-menu.tsx +68 -0
  52. package/src/components/pagination/index.ts +1 -0
  53. package/src/components/pagination/pagination.tsx +70 -0
  54. package/src/components/progress/index.ts +1 -0
  55. package/src/components/progress/progress.tsx +66 -0
  56. package/src/components/radio-group/index.ts +1 -0
  57. package/src/components/radio-group/radio-group.tsx +90 -0
  58. package/src/components/scroll-area/index.ts +1 -0
  59. package/src/components/scroll-area/scroll-area.tsx +27 -0
  60. package/src/components/select/index.ts +1 -0
  61. package/src/components/select/select.tsx +154 -0
  62. package/src/components/separator/index.ts +1 -0
  63. package/src/components/separator/separator.tsx +37 -0
  64. package/src/components/sheet/index.ts +1 -0
  65. package/src/components/sheet/sheet.tsx +128 -0
  66. package/src/components/skeleton/index.ts +1 -0
  67. package/src/components/skeleton/skeleton.tsx +84 -0
  68. package/src/components/slider/index.ts +1 -0
  69. package/src/components/slider/slider.tsx +145 -0
  70. package/src/components/switch/index.ts +1 -0
  71. package/src/components/switch/switch.tsx +78 -0
  72. package/src/components/table/index.ts +1 -0
  73. package/src/components/table/table.tsx +71 -0
  74. package/src/components/tabs/index.ts +1 -0
  75. package/src/components/tabs/tabs.tsx +124 -0
  76. package/src/components/textarea/index.ts +1 -0
  77. package/src/components/textarea/textarea.tsx +83 -0
  78. package/src/components/toast/index.ts +1 -0
  79. package/src/components/toast/toast.tsx +124 -0
  80. package/src/components/toggle/index.ts +1 -0
  81. package/src/components/toggle/toggle.tsx +87 -0
  82. package/src/components/toggle-group/index.ts +1 -0
  83. package/src/components/toggle-group/toggle-group.tsx +87 -0
  84. package/src/components/tooltip/index.ts +1 -0
  85. package/src/components/tooltip/tooltip.tsx +103 -0
  86. package/src/components/typography/index.ts +1 -0
  87. package/src/components/typography/typography.tsx +57 -0
  88. package/src/context/index.ts +3 -0
  89. package/src/context/provider.tsx +35 -0
  90. package/src/context/theme-context.tsx +81 -0
  91. package/src/context/toast-context.tsx +63 -0
  92. package/src/env.d.ts +2 -0
  93. package/src/hooks/index.ts +15 -0
  94. package/src/hooks/use-biometric.ts +27 -0
  95. package/src/hooks/use-color-scheme.ts +10 -0
  96. package/src/hooks/use-controllable.ts +40 -0
  97. package/src/hooks/use-countdown.ts +33 -0
  98. package/src/hooks/use-debounce.ts +18 -0
  99. package/src/hooks/use-disclosure.ts +14 -0
  100. package/src/hooks/use-haptics.ts +47 -0
  101. package/src/hooks/use-keyboard.ts +35 -0
  102. package/src/hooks/use-media-query.ts +27 -0
  103. package/src/hooks/use-press-animation.ts +45 -0
  104. package/src/hooks/use-previous.ts +14 -0
  105. package/src/hooks/use-scroll-header.ts +42 -0
  106. package/src/hooks/use-spring.ts +18 -0
  107. package/src/hooks/use-theme.ts +6 -0
  108. package/src/hooks/use-toast.ts +6 -0
  109. package/src/index.ts +53 -0
  110. package/src/lib/create-animated.tsx +25 -0
  111. package/src/lib/create-component.tsx +56 -0
  112. package/src/lib/index.ts +4 -0
  113. package/src/lib/platform.ts +25 -0
  114. package/src/lib/types.ts +28 -0
  115. package/src/lib/utils.ts +35 -0
  116. package/src/lib/variants.ts +7 -0
  117. package/src/premium/ai/chat-bubble.tsx +58 -0
  118. package/src/premium/ai/typing-indicator.tsx +59 -0
  119. package/src/premium/charts/bar-chart.tsx +66 -0
  120. package/src/premium/charts/progress-ring.tsx +63 -0
  121. package/src/premium/glass/glass-bottom-sheet.tsx +50 -0
  122. package/src/premium/glass/glass-card.tsx +51 -0
  123. package/src/premium/glass/glass-header.tsx +61 -0
  124. package/src/premium/glass/glass-panel.tsx +32 -0
  125. package/src/premium/glass/glass-sidebar.tsx +56 -0
  126. package/src/premium/index.ts +44 -0
  127. package/src/premium/index2.ts +13 -0
  128. package/src/premium/index3.ts +1 -0
  129. package/src/premium/inputs/color-picker.tsx +92 -0
  130. package/src/premium/inputs/currency-input.tsx +50 -0
  131. package/src/premium/inputs/otp-input.tsx +92 -0
  132. package/src/premium/inputs/phone-input.tsx +58 -0
  133. package/src/premium/inputs/rating.tsx +51 -0
  134. package/src/premium/layout/carousel.tsx +57 -0
  135. package/src/premium/layout/floating-dock.tsx +63 -0
  136. package/src/premium/layout/masonry-grid.tsx +41 -0
  137. package/src/premium/layout/parallax-scroll.tsx +81 -0
  138. package/src/premium/magic/animated-number.tsx +104 -0
  139. package/src/premium/magic/bento-grid.tsx +55 -0
  140. package/src/premium/magic/border-beam.tsx +68 -0
  141. package/src/premium/magic/confetti.tsx +88 -0
  142. package/src/premium/magic/magic-card.tsx +65 -0
  143. package/src/premium/magic/meteors.tsx +95 -0
  144. package/src/premium/magic/ripple.tsx +70 -0
  145. package/src/premium/magic/shimmer.tsx +58 -0
  146. package/src/premium/magic/shiny-button.tsx +70 -0
  147. package/src/premium/mobile/biometric-button.tsx +82 -0
  148. package/src/premium/mobile/bottom-tab-bar.tsx +81 -0
  149. package/src/premium/mobile/fab.tsx +74 -0
  150. package/src/premium/mobile/haptic-pressable.tsx +53 -0
  151. package/src/premium/mobile/notification-badge.tsx +61 -0
  152. package/src/premium/mobile/pull-to-refresh.tsx +84 -0
  153. package/src/premium/mobile/scroll-header.tsx +57 -0
  154. package/src/premium/mobile/swipe-row.tsx +128 -0
  155. package/src/premium/mobile/swipeable-card-stack.tsx +121 -0
  156. package/src/premium/motion/blur-fade.tsx +51 -0
  157. package/src/premium/motion/fade-up.tsx +34 -0
  158. package/src/premium/motion/marquee.tsx +67 -0
  159. package/src/premium/motion/pulsating-button.tsx +95 -0
  160. package/src/premium/motion/slide-in.tsx +38 -0
  161. package/src/premium/motion/stagger-children.tsx +28 -0
  162. package/src/premium/motion/typing-text.tsx +55 -0
  163. package/src/premium/motion/word-pull-up.tsx +34 -0
  164. package/src/premium/onboarding/step-indicator.tsx +65 -0
  165. package/src/tokens/colors.ts +83 -0
  166. package/src/tokens/global.css +83 -0
  167. package/src/tokens/index.ts +10 -0
  168. package/src/tokens/layout.ts +121 -0
  169. package/src/tokens/motion.ts +94 -0
  170. package/src/tokens/themes/dark.ts +7 -0
  171. package/src/tokens/themes/default.ts +8 -0
  172. package/src/tokens/themes/ocean.ts +28 -0
  173. package/src/tokens/themes/rose.ts +29 -0
  174. package/src/tokens/typography.ts +127 -0
  175. package/tsconfig.json +15 -0
@@ -0,0 +1,71 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { cn } from '../../lib/utils';
4
+ import { Text } from '../typography';
5
+
6
+ export const Table = forwardRef<React.ElementRef<typeof View>, ViewProps>(
7
+ ({ className, ...props }, ref) => (
8
+ <View className="w-full">
9
+ <View
10
+ ref={ref}
11
+ className={cn('w-full max-w-full flex-col text-sm', className)}
12
+ {...props}
13
+ />
14
+ </View>
15
+ )
16
+ );
17
+ Table.displayName = 'Table';
18
+
19
+ export const TableHeader = forwardRef<React.ElementRef<typeof View>, ViewProps>(
20
+ ({ className, ...props }, ref) => (
21
+ <View ref={ref} className={cn('flex-row border-b border-border bg-muted/50', className)} {...props} />
22
+ )
23
+ );
24
+ TableHeader.displayName = 'TableHeader';
25
+
26
+ export const TableBody = forwardRef<React.ElementRef<typeof View>, ViewProps>(
27
+ ({ className, ...props }, ref) => (
28
+ <View ref={ref} className={cn('flex-col', className)} {...props} />
29
+ )
30
+ );
31
+ TableBody.displayName = 'TableBody';
32
+
33
+ export const TableFooter = forwardRef<React.ElementRef<typeof View>, ViewProps>(
34
+ ({ className, ...props }, ref) => (
35
+ <View ref={ref} className={cn('flex-row border-t border-border bg-muted/50 font-medium', className)} {...props} />
36
+ )
37
+ );
38
+ TableFooter.displayName = 'TableFooter';
39
+
40
+ export const TableRow = forwardRef<React.ElementRef<typeof View>, ViewProps>(
41
+ ({ className, ...props }, ref) => (
42
+ <View
43
+ ref={ref}
44
+ className={cn('flex-row border-b border-border transition-colors hover:bg-muted/50', className)}
45
+ {...props}
46
+ />
47
+ )
48
+ );
49
+ TableRow.displayName = 'TableRow';
50
+
51
+ export const TableHead = forwardRef<React.ElementRef<typeof View>, ViewProps & { title: string }>(
52
+ ({ className, title, ...props }, ref) => (
53
+ <View
54
+ ref={ref}
55
+ className={cn('h-12 flex-1 justify-center px-4 align-middle font-medium', className)}
56
+ {...props}
57
+ >
58
+ <Text className="text-muted-foreground font-semibold">{title}</Text>
59
+ </View>
60
+ )
61
+ );
62
+ TableHead.displayName = 'TableHead';
63
+
64
+ export const TableCell = forwardRef<React.ElementRef<typeof View>, ViewProps>(
65
+ ({ className, children, ...props }, ref) => (
66
+ <View ref={ref} className={cn('flex-1 justify-center p-4 align-middle', className)} {...props}>
67
+ {typeof children === 'string' ? <Text>{children}</Text> : children}
68
+ </View>
69
+ )
70
+ );
71
+ TableCell.displayName = 'TableCell';
@@ -0,0 +1 @@
1
+ export * from './tabs';
@@ -0,0 +1,124 @@
1
+ import React, { forwardRef, useState } from 'react';
2
+ import { View, Pressable, type ViewProps, type PressableProps } from 'react-native';
3
+ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
4
+ import { useControllableState } from '../../hooks/use-controllable';
5
+ import { useHaptics } from '../../hooks/use-haptics';
6
+ import { useSpring } from '../../hooks/use-spring';
7
+ import { cn } from '../../lib/utils';
8
+ import { Text } from '../typography';
9
+
10
+ const TabsContext = React.createContext<{
11
+ value: string;
12
+ onValueChange: (value: string) => void;
13
+ } | null>(null);
14
+
15
+ export interface TabsProps extends ViewProps {
16
+ value?: string;
17
+ defaultValue?: string;
18
+ onValueChange?: (value: string) => void;
19
+ }
20
+
21
+ export const Tabs = forwardRef<React.ElementRef<typeof View>, TabsProps>(
22
+ ({ className, value: valueProp, defaultValue, onValueChange, children, ...props }, ref) => {
23
+ const [value, setValue] = useControllableState({
24
+ prop: valueProp,
25
+ defaultProp: defaultValue || '',
26
+ onChange: onValueChange,
27
+ });
28
+
29
+ return (
30
+ <TabsContext.Provider value={{ value, onValueChange: setValue }}>
31
+ <View ref={ref} className={cn('w-full', className)} {...props}>
32
+ {children}
33
+ </View>
34
+ </TabsContext.Provider>
35
+ );
36
+ }
37
+ );
38
+ Tabs.displayName = 'Tabs';
39
+
40
+ export const TabsList = forwardRef<React.ElementRef<typeof View>, ViewProps>(
41
+ ({ className, children, ...props }, ref) => (
42
+ <View
43
+ ref={ref}
44
+ className={cn(
45
+ 'inline-flex h-10 flex-row items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ {children}
51
+ </View>
52
+ )
53
+ );
54
+ TabsList.displayName = 'TabsList';
55
+
56
+ export interface TabsTriggerProps extends PressableProps {
57
+ value: string;
58
+ }
59
+
60
+ export const TabsTrigger = forwardRef<React.ElementRef<typeof Pressable>, TabsTriggerProps>(
61
+ ({ className, value, disabled, children, ...props }, ref) => {
62
+ const context = React.useContext(TabsContext);
63
+ if (!context) throw new Error('TabsTrigger must be used within Tabs');
64
+ const triggerHaptic = useHaptics();
65
+
66
+ const isActive = context.value === value;
67
+
68
+ const handlePress = () => {
69
+ if (disabled) return;
70
+ if (!isActive) {
71
+ triggerHaptic('selection');
72
+ context.onValueChange(value);
73
+ }
74
+ };
75
+
76
+ return (
77
+ <Pressable
78
+ ref={ref}
79
+ onPress={handlePress}
80
+ disabled={disabled}
81
+ className={cn(
82
+ 'inline-flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all',
83
+ isActive ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground',
84
+ disabled && 'opacity-50',
85
+ className
86
+ )}
87
+ {...props}
88
+ >
89
+ {typeof children === 'string' ? (
90
+ <Text className={cn('text-sm font-medium', isActive ? 'text-foreground' : 'text-muted-foreground')}>
91
+ {children}
92
+ </Text>
93
+ ) : (
94
+ children
95
+ )}
96
+ </Pressable>
97
+ );
98
+ }
99
+ );
100
+ TabsTrigger.displayName = 'TabsTrigger';
101
+
102
+ export interface TabsContentProps extends ViewProps {
103
+ value: string;
104
+ }
105
+
106
+ export const TabsContent = forwardRef<React.ElementRef<typeof View>, TabsContentProps>(
107
+ ({ className, value, children, ...props }, ref) => {
108
+ const context = React.useContext(TabsContext);
109
+ if (!context) throw new Error('TabsContent must be used within Tabs');
110
+
111
+ if (context.value !== value) return null;
112
+
113
+ return (
114
+ <View
115
+ ref={ref}
116
+ className={cn('mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', className)}
117
+ {...props}
118
+ >
119
+ {children}
120
+ </View>
121
+ );
122
+ }
123
+ );
124
+ TabsContent.displayName = 'TabsContent';
@@ -0,0 +1 @@
1
+ export * from './textarea';
@@ -0,0 +1,83 @@
1
+ import React, { useState } from 'react';
2
+ import { TextInput, View, type TextInputProps, type NativeSyntheticEvent, type TextInputContentSizeChangeEventData } from 'react-native';
3
+ import { cva, type VariantProps } from '../../lib/variants';
4
+ import { useThemeContext } from '../../context/theme-context';
5
+ import { cn } from '../../lib/utils';
6
+ import { Text } from '../typography';
7
+
8
+ const textareaVariants = cva(
9
+ 'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: '',
14
+ glass: 'bg-glass border-glass-border',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'default',
19
+ },
20
+ }
21
+ );
22
+
23
+ export interface TextareaProps extends TextInputProps, VariantProps<typeof textareaVariants> {
24
+ error?: string;
25
+ helperText?: string;
26
+ autoGrow?: boolean;
27
+ maxHeight?: number;
28
+ showCharacterCount?: boolean;
29
+ maxLength?: number;
30
+ }
31
+
32
+ export const Textarea = React.forwardRef<React.ElementRef<typeof TextInput>, TextareaProps>(
33
+ ({ className, variant, error, helperText, autoGrow = true, maxHeight = 200, showCharacterCount, maxLength, value, onChangeText, style, ...props }, ref) => {
34
+ const { theme } = useThemeContext();
35
+ const [height, setHeight] = useState(80);
36
+
37
+ const handleContentSizeChange = (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
38
+ if (autoGrow) {
39
+ setHeight(Math.min(Math.max(80, e.nativeEvent.contentSize.height), maxHeight));
40
+ }
41
+ props.onContentSizeChange?.(e);
42
+ };
43
+
44
+ const count = value?.length || 0;
45
+
46
+ return (
47
+ <View className="flex flex-col space-y-1.5 w-full">
48
+ <TextInput
49
+ ref={ref}
50
+ className={cn(
51
+ textareaVariants({ variant }),
52
+ error && 'border-destructive focus-visible:ring-destructive',
53
+ className
54
+ )}
55
+ style={[style, autoGrow && { height }]}
56
+ placeholderTextColor={theme.muted.foreground as string}
57
+ multiline
58
+ textAlignVertical="top"
59
+ onContentSizeChange={handleContentSizeChange}
60
+ value={value}
61
+ onChangeText={onChangeText}
62
+ maxLength={maxLength}
63
+ {...props}
64
+ />
65
+
66
+ <View className="flex flex-row justify-between w-full">
67
+ {(helperText || error) ? (
68
+ <Text className={cn('text-sm flex-1', error ? 'text-destructive' : 'text-muted-foreground')}>
69
+ {error || helperText}
70
+ </Text>
71
+ ) : <View />}
72
+
73
+ {showCharacterCount && maxLength && (
74
+ <Text className="text-sm text-muted-foreground text-right pl-2">
75
+ {count}/{maxLength}
76
+ </Text>
77
+ )}
78
+ </View>
79
+ </View>
80
+ );
81
+ }
82
+ );
83
+ Textarea.displayName = 'Textarea';
@@ -0,0 +1 @@
1
+ export * from './toast';
@@ -0,0 +1,124 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, Pressable, type ViewProps, SafeAreaView } from 'react-native';
3
+ import Animated, {
4
+ useAnimatedStyle,
5
+ useSharedValue,
6
+ withSpring,
7
+ withTiming,
8
+ withDelay,
9
+ runOnJS,
10
+ } from 'react-native-reanimated';
11
+ import { X } from 'lucide-react-native';
12
+ import { useToastContext, type ToastProps } from '../../context/toast-context';
13
+ import { cn } from '../../lib/utils';
14
+ import { Text } from '../typography';
15
+ import { useHaptics } from '../../hooks/use-haptics';
16
+
17
+ const ToastItem = ({ toast, onDismiss }: { toast: ToastProps; onDismiss: (id: string) => void }) => {
18
+ const translateY = useSharedValue(-100);
19
+ const opacity = useSharedValue(0);
20
+ const triggerHaptic = useHaptics();
21
+
22
+ useEffect(() => {
23
+ // Entrance animation
24
+ translateY.value = withSpring(0, { damping: 15, stiffness: 200 });
25
+ opacity.value = withTiming(1, { duration: 300 });
26
+
27
+ // Haptic feedback based on type
28
+ if (toast.type === 'error') triggerHaptic('error');
29
+ else if (toast.type === 'success') triggerHaptic('success');
30
+ else if (toast.type === 'warning') triggerHaptic('warning');
31
+ else triggerHaptic('light');
32
+
33
+ // Setup exit animation manually if it wasn't auto-dismissed early
34
+ return () => {
35
+ // Cleanup happens via the dismiss function animating out
36
+ };
37
+ }, []);
38
+
39
+ const handleDismiss = () => {
40
+ translateY.value = withTiming(-100, { duration: 300 });
41
+ opacity.value = withTiming(0, { duration: 300 }, () => {
42
+ runOnJS(onDismiss)(toast.id);
43
+ });
44
+ };
45
+
46
+ const animatedStyle = useAnimatedStyle(() => ({
47
+ transform: [{ translateY: translateY.value }],
48
+ opacity: opacity.value,
49
+ }));
50
+
51
+ const getVariantStyles = () => {
52
+ switch (toast.type) {
53
+ case 'destructive':
54
+ case 'error':
55
+ return 'bg-destructive border-destructive text-destructive-foreground';
56
+ case 'success':
57
+ return 'bg-success border-success text-success-foreground';
58
+ case 'warning':
59
+ return 'bg-warning border-warning text-warning-foreground';
60
+ default:
61
+ return 'bg-background border-border text-foreground';
62
+ }
63
+ };
64
+
65
+ return (
66
+ <Animated.View
67
+ style={animatedStyle}
68
+ className={cn(
69
+ 'pointer-events-auto relative w-full flex-row items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all',
70
+ getVariantStyles()
71
+ )}
72
+ >
73
+ <View className="flex-1 flex-col gap-1">
74
+ <Text className={cn('text-sm font-semibold', toast.type && toast.type !== 'default' ? 'text-white' : 'text-foreground')}>
75
+ {toast.title}
76
+ </Text>
77
+ {toast.description && (
78
+ <Text className={cn('text-sm opacity-90', toast.type && toast.type !== 'default' ? 'text-white' : 'text-muted-foreground')}>
79
+ {toast.description}
80
+ </Text>
81
+ )}
82
+ </View>
83
+
84
+ {toast.action && (
85
+ <Pressable
86
+ onPress={toast.action.onPress}
87
+ className="inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
88
+ >
89
+ <Text className={toast.type && toast.type !== 'default' ? 'text-white' : 'text-foreground'}>
90
+ {toast.action.label}
91
+ </Text>
92
+ </Pressable>
93
+ )}
94
+
95
+ <Pressable
96
+ onPress={handleDismiss}
97
+ className="absolute right-2 top-2 rounded-md p-1 opacity-70 transition-opacity hover:opacity-100"
98
+ >
99
+ <X size={16} color={toast.type && toast.type !== 'default' ? 'white' : 'gray'} />
100
+ </Pressable>
101
+ </Animated.View>
102
+ );
103
+ };
104
+
105
+ export const ToastViewport = ({ className, ...props }: ViewProps) => {
106
+ const { toasts, dismiss } = useToastContext();
107
+
108
+ return (
109
+ <SafeAreaView pointerEvents="box-none" className="absolute inset-0 z-50">
110
+ <View
111
+ pointerEvents="box-none"
112
+ className={cn(
113
+ 'flex-col items-center justify-start gap-2 p-4 pt-10 sm:justify-end sm:p-6 w-full max-w-[420px] self-center',
114
+ className
115
+ )}
116
+ {...props}
117
+ >
118
+ {toasts.map((toast) => (
119
+ <ToastItem key={toast.id} toast={toast} onDismiss={dismiss} />
120
+ ))}
121
+ </View>
122
+ </SafeAreaView>
123
+ );
124
+ };
@@ -0,0 +1 @@
1
+ export * from './toggle';
@@ -0,0 +1,87 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { Pressable, type PressableProps } from 'react-native';
3
+ import { cva, type VariantProps } from '../../lib/variants';
4
+ import { createComponent } from '../../lib/create-component';
5
+ import { Text } from '../typography';
6
+ import { useControllableState } from '../../hooks/use-controllable';
7
+ import { useHaptics } from '../../hooks/use-haptics';
8
+
9
+ const toggleVariants = cva(
10
+ 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: 'bg-transparent',
15
+ outline: 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
16
+ },
17
+ size: {
18
+ default: 'h-10 px-3',
19
+ sm: 'h-9 px-2.5',
20
+ lg: 'h-11 px-5',
21
+ },
22
+ pressed: {
23
+ true: '',
24
+ false: '',
25
+ }
26
+ },
27
+ compoundVariants: [
28
+ { variant: 'default', pressed: true, className: 'bg-accent text-accent-foreground' },
29
+ { variant: 'outline', pressed: true, className: 'bg-accent text-accent-foreground' },
30
+ ],
31
+ defaultVariants: {
32
+ variant: 'default',
33
+ size: 'default',
34
+ },
35
+ }
36
+ );
37
+
38
+ export interface ToggleProps extends Omit<PressableProps, 'value'>, VariantProps<typeof toggleVariants> {
39
+ pressed?: boolean;
40
+ defaultPressed?: boolean;
41
+ onPressedChange?: (pressed: boolean) => void;
42
+ }
43
+
44
+ export const Toggle = forwardRef<React.ElementRef<typeof Pressable>, ToggleProps>(
45
+ ({ className, variant, size, pressed: pressedProp, defaultPressed, onPressedChange, disabled, children, style, ...props }, ref) => {
46
+ const triggerHaptic = useHaptics();
47
+
48
+ const [pressed, setPressed] = useControllableState({
49
+ prop: pressedProp,
50
+ defaultProp: defaultPressed || false,
51
+ onChange: onPressedChange,
52
+ });
53
+
54
+ const handlePress = () => {
55
+ if (disabled) return;
56
+ triggerHaptic('light');
57
+ setPressed(!pressed);
58
+ };
59
+
60
+ return (
61
+ <Pressable
62
+ ref={ref}
63
+ onPress={handlePress}
64
+ disabled={disabled}
65
+ accessibilityRole="button"
66
+ accessibilityState={{ selected: pressed, disabled: !!disabled }}
67
+ className={toggleVariants({ variant, size, pressed, className })}
68
+ style={style}
69
+ {...props}
70
+ >
71
+ {typeof children === 'string' ? (
72
+ <Text className={cn(
73
+ 'text-sm font-medium',
74
+ pressed ? 'text-accent-foreground' : 'text-foreground'
75
+ )}>
76
+ {children}
77
+ </Text>
78
+ ) : (
79
+ children
80
+ )}
81
+ </Pressable>
82
+ );
83
+ }
84
+ );
85
+ Toggle.displayName = 'Toggle';
86
+
87
+ import { cn } from '../../lib/utils';
@@ -0,0 +1 @@
1
+ export * from './toggle-group';
@@ -0,0 +1,87 @@
1
+ import React, { forwardRef, createContext, useContext } from 'react';
2
+ import { View, type ViewProps } from 'react-native';
3
+ import { cva, type VariantProps } from '../../lib/variants';
4
+ import { Toggle } from '../toggle';
5
+ import { useControllableState } from '../../hooks/use-controllable';
6
+
7
+ const ToggleGroupContext = createContext<{
8
+ type: 'single' | 'multiple';
9
+ value: string | string[];
10
+ onValueChange: (value: string) => void;
11
+ variant?: 'default' | 'outline';
12
+ size?: 'default' | 'sm' | 'lg';
13
+ } | null>(null);
14
+
15
+ export interface ToggleGroupProps extends ViewProps {
16
+ type: 'single' | 'multiple';
17
+ value?: string | string[];
18
+ defaultValue?: string | string[];
19
+ onValueChange?: (value: any) => void;
20
+ variant?: 'default' | 'outline';
21
+ size?: 'default' | 'sm' | 'lg';
22
+ }
23
+
24
+ export const ToggleGroup = forwardRef<React.ElementRef<typeof View>, ToggleGroupProps>(
25
+ ({ className, type, value: valueProp, defaultValue, onValueChange, variant, size, children, ...props }, ref) => {
26
+ const [value, setValue] = useControllableState({
27
+ prop: valueProp,
28
+ defaultProp: defaultValue || (type === 'single' ? '' : []),
29
+ onChange: onValueChange,
30
+ });
31
+
32
+ const handleValueChange = (itemValue: string) => {
33
+ if (type === 'single') {
34
+ setValue(value === itemValue ? '' : itemValue);
35
+ } else {
36
+ const arr = value as string[];
37
+ setValue(
38
+ arr.includes(itemValue)
39
+ ? arr.filter((v) => v !== itemValue)
40
+ : [...arr, itemValue]
41
+ );
42
+ }
43
+ };
44
+
45
+ return (
46
+ <ToggleGroupContext.Provider value={{ type, value, onValueChange: handleValueChange, variant, size }}>
47
+ <View ref={ref} className={cn('flex flex-row items-center justify-center gap-1', className)} {...props}>
48
+ {children}
49
+ </View>
50
+ </ToggleGroupContext.Provider>
51
+ );
52
+ }
53
+ );
54
+ ToggleGroup.displayName = 'ToggleGroup';
55
+
56
+ export interface ToggleGroupItemProps extends Omit<React.ComponentPropsWithoutRef<typeof Toggle>, 'pressed' | 'onPressedChange'> {
57
+ value: string;
58
+ }
59
+
60
+ export const ToggleGroupItem = forwardRef<React.ElementRef<typeof Toggle>, ToggleGroupItemProps>(
61
+ ({ className, value, children, ...props }, ref) => {
62
+ const context = useContext(ToggleGroupContext);
63
+ if (!context) throw new Error('ToggleGroupItem must be used within a ToggleGroup');
64
+
65
+ const isPressed =
66
+ context.type === 'single'
67
+ ? context.value === value
68
+ : (context.value as string[]).includes(value);
69
+
70
+ return (
71
+ <Toggle
72
+ ref={ref}
73
+ variant={context.variant}
74
+ size={context.size}
75
+ pressed={isPressed}
76
+ onPressedChange={() => context.onValueChange(value)}
77
+ className={className}
78
+ {...props}
79
+ >
80
+ {children}
81
+ </Toggle>
82
+ );
83
+ }
84
+ );
85
+ ToggleGroupItem.displayName = 'ToggleGroupItem';
86
+
87
+ import { cn } from '../../lib/utils';
@@ -0,0 +1 @@
1
+ export * from './tooltip';