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.
- package/README.md +34 -0
- package/babel.config.js +6 -0
- package/jest.config.js +22 -0
- package/jest.init.js +5 -0
- package/jest.setup.js +173 -0
- package/package.json +87 -0
- package/src/__tests__/a11y/accessibility.test.tsx +33 -0
- package/src/__tests__/components/badge.test.tsx +25 -0
- package/src/__tests__/components/button.test.tsx +53 -0
- package/src/__tests__/components/card.test.tsx +28 -0
- package/src/__tests__/components/input.test.tsx +33 -0
- package/src/__tests__/hooks/use-controllable.test.ts +58 -0
- package/src/__tests__/integration.test.tsx +35 -0
- package/src/__tests__/lib/utils.test.ts +23 -0
- package/src/__tests__/mocks/handlers.ts +19 -0
- package/src/components/accordion/accordion.tsx +143 -0
- package/src/components/accordion/index.ts +1 -0
- package/src/components/alert/alert.tsx +65 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/alert-dialog/alert-dialog.tsx +145 -0
- package/src/components/alert-dialog/index.ts +1 -0
- package/src/components/aspect-ratio/aspect-ratio.tsx +18 -0
- package/src/components/aspect-ratio/index.ts +1 -0
- package/src/components/avatar/avatar.tsx +93 -0
- package/src/components/avatar/index.ts +1 -0
- package/src/components/badge/badge.tsx +64 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/breadcrumb/breadcrumb.tsx +75 -0
- package/src/components/breadcrumb/index.ts +1 -0
- package/src/components/button/button.tsx +119 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card.tsx +40 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/checkbox/checkbox.tsx +87 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/collapsible/collapsible.tsx +92 -0
- package/src/components/collapsible/index.ts +1 -0
- package/src/components/context-menu/context-menu.tsx +121 -0
- package/src/components/context-menu/index.ts +1 -0
- package/src/components/dialog/dialog.tsx +124 -0
- package/src/components/dialog/index.ts +1 -0
- package/src/components/dropdown-menu/dropdown-menu.tsx +145 -0
- package/src/components/dropdown-menu/index.ts +1 -0
- package/src/components/form/form.tsx +84 -0
- package/src/components/form/index.ts +1 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.tsx +115 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.tsx +13 -0
- package/src/components/navigation-menu/index.ts +1 -0
- package/src/components/navigation-menu/navigation-menu.tsx +68 -0
- package/src/components/pagination/index.ts +1 -0
- package/src/components/pagination/pagination.tsx +70 -0
- package/src/components/progress/index.ts +1 -0
- package/src/components/progress/progress.tsx +66 -0
- package/src/components/radio-group/index.ts +1 -0
- package/src/components/radio-group/radio-group.tsx +90 -0
- package/src/components/scroll-area/index.ts +1 -0
- package/src/components/scroll-area/scroll-area.tsx +27 -0
- package/src/components/select/index.ts +1 -0
- package/src/components/select/select.tsx +154 -0
- package/src/components/separator/index.ts +1 -0
- package/src/components/separator/separator.tsx +37 -0
- package/src/components/sheet/index.ts +1 -0
- package/src/components/sheet/sheet.tsx +128 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/skeleton/skeleton.tsx +84 -0
- package/src/components/slider/index.ts +1 -0
- package/src/components/slider/slider.tsx +145 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/switch/switch.tsx +78 -0
- package/src/components/table/index.ts +1 -0
- package/src/components/table/table.tsx +71 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/tabs/tabs.tsx +124 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/textarea/textarea.tsx +83 -0
- package/src/components/toast/index.ts +1 -0
- package/src/components/toast/toast.tsx +124 -0
- package/src/components/toggle/index.ts +1 -0
- package/src/components/toggle/toggle.tsx +87 -0
- package/src/components/toggle-group/index.ts +1 -0
- package/src/components/toggle-group/toggle-group.tsx +87 -0
- package/src/components/tooltip/index.ts +1 -0
- package/src/components/tooltip/tooltip.tsx +103 -0
- package/src/components/typography/index.ts +1 -0
- package/src/components/typography/typography.tsx +57 -0
- package/src/context/index.ts +3 -0
- package/src/context/provider.tsx +35 -0
- package/src/context/theme-context.tsx +81 -0
- package/src/context/toast-context.tsx +63 -0
- package/src/env.d.ts +2 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/use-biometric.ts +27 -0
- package/src/hooks/use-color-scheme.ts +10 -0
- package/src/hooks/use-controllable.ts +40 -0
- package/src/hooks/use-countdown.ts +33 -0
- package/src/hooks/use-debounce.ts +18 -0
- package/src/hooks/use-disclosure.ts +14 -0
- package/src/hooks/use-haptics.ts +47 -0
- package/src/hooks/use-keyboard.ts +35 -0
- package/src/hooks/use-media-query.ts +27 -0
- package/src/hooks/use-press-animation.ts +45 -0
- package/src/hooks/use-previous.ts +14 -0
- package/src/hooks/use-scroll-header.ts +42 -0
- package/src/hooks/use-spring.ts +18 -0
- package/src/hooks/use-theme.ts +6 -0
- package/src/hooks/use-toast.ts +6 -0
- package/src/index.ts +53 -0
- package/src/lib/create-animated.tsx +25 -0
- package/src/lib/create-component.tsx +56 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/platform.ts +25 -0
- package/src/lib/types.ts +28 -0
- package/src/lib/utils.ts +35 -0
- package/src/lib/variants.ts +7 -0
- package/src/premium/ai/chat-bubble.tsx +58 -0
- package/src/premium/ai/typing-indicator.tsx +59 -0
- package/src/premium/charts/bar-chart.tsx +66 -0
- package/src/premium/charts/progress-ring.tsx +63 -0
- package/src/premium/glass/glass-bottom-sheet.tsx +50 -0
- package/src/premium/glass/glass-card.tsx +51 -0
- package/src/premium/glass/glass-header.tsx +61 -0
- package/src/premium/glass/glass-panel.tsx +32 -0
- package/src/premium/glass/glass-sidebar.tsx +56 -0
- package/src/premium/index.ts +44 -0
- package/src/premium/index2.ts +13 -0
- package/src/premium/index3.ts +1 -0
- package/src/premium/inputs/color-picker.tsx +92 -0
- package/src/premium/inputs/currency-input.tsx +50 -0
- package/src/premium/inputs/otp-input.tsx +92 -0
- package/src/premium/inputs/phone-input.tsx +58 -0
- package/src/premium/inputs/rating.tsx +51 -0
- package/src/premium/layout/carousel.tsx +57 -0
- package/src/premium/layout/floating-dock.tsx +63 -0
- package/src/premium/layout/masonry-grid.tsx +41 -0
- package/src/premium/layout/parallax-scroll.tsx +81 -0
- package/src/premium/magic/animated-number.tsx +104 -0
- package/src/premium/magic/bento-grid.tsx +55 -0
- package/src/premium/magic/border-beam.tsx +68 -0
- package/src/premium/magic/confetti.tsx +88 -0
- package/src/premium/magic/magic-card.tsx +65 -0
- package/src/premium/magic/meteors.tsx +95 -0
- package/src/premium/magic/ripple.tsx +70 -0
- package/src/premium/magic/shimmer.tsx +58 -0
- package/src/premium/magic/shiny-button.tsx +70 -0
- package/src/premium/mobile/biometric-button.tsx +82 -0
- package/src/premium/mobile/bottom-tab-bar.tsx +81 -0
- package/src/premium/mobile/fab.tsx +74 -0
- package/src/premium/mobile/haptic-pressable.tsx +53 -0
- package/src/premium/mobile/notification-badge.tsx +61 -0
- package/src/premium/mobile/pull-to-refresh.tsx +84 -0
- package/src/premium/mobile/scroll-header.tsx +57 -0
- package/src/premium/mobile/swipe-row.tsx +128 -0
- package/src/premium/mobile/swipeable-card-stack.tsx +121 -0
- package/src/premium/motion/blur-fade.tsx +51 -0
- package/src/premium/motion/fade-up.tsx +34 -0
- package/src/premium/motion/marquee.tsx +67 -0
- package/src/premium/motion/pulsating-button.tsx +95 -0
- package/src/premium/motion/slide-in.tsx +38 -0
- package/src/premium/motion/stagger-children.tsx +28 -0
- package/src/premium/motion/typing-text.tsx +55 -0
- package/src/premium/motion/word-pull-up.tsx +34 -0
- package/src/premium/onboarding/step-indicator.tsx +65 -0
- package/src/tokens/colors.ts +83 -0
- package/src/tokens/global.css +83 -0
- package/src/tokens/index.ts +10 -0
- package/src/tokens/layout.ts +121 -0
- package/src/tokens/motion.ts +94 -0
- package/src/tokens/themes/dark.ts +7 -0
- package/src/tokens/themes/default.ts +8 -0
- package/src/tokens/themes/ocean.ts +28 -0
- package/src/tokens/themes/rose.ts +29 -0
- package/src/tokens/typography.ts +127 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './scroll-area';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ScrollView, type ScrollViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
|
|
5
|
+
export interface ScrollAreaProps extends ScrollViewProps {
|
|
6
|
+
orientation?: 'vertical' | 'horizontal';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const ScrollArea = React.forwardRef<React.ElementRef<typeof ScrollView>, ScrollAreaProps>(
|
|
10
|
+
({ className, orientation = 'vertical', children, ...props }, ref) => {
|
|
11
|
+
const isHorizontal = orientation === 'horizontal';
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<ScrollView
|
|
15
|
+
ref={ref}
|
|
16
|
+
horizontal={isHorizontal}
|
|
17
|
+
showsVerticalScrollIndicator={!isHorizontal}
|
|
18
|
+
showsHorizontalScrollIndicator={isHorizontal}
|
|
19
|
+
className={cn('flex shrink-0', className)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</ScrollView>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
ScrollArea.displayName = 'ScrollArea';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './select';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from 'react';
|
|
2
|
+
import { View, Pressable, Modal, type ViewProps, type LayoutRectangle } from 'react-native';
|
|
3
|
+
import Animated, { FadeIn, FadeOut, ZoomIn, ZoomOut } from 'react-native-reanimated';
|
|
4
|
+
import { ChevronDown, Check } from 'lucide-react-native';
|
|
5
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
6
|
+
import { cn } from '../../lib/utils';
|
|
7
|
+
import { Text } from '../typography';
|
|
8
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
9
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
10
|
+
|
|
11
|
+
const SelectContext = React.createContext<{
|
|
12
|
+
open: boolean;
|
|
13
|
+
onOpenChange: (open: boolean) => void;
|
|
14
|
+
value: string;
|
|
15
|
+
onValueChange: (value: string) => void;
|
|
16
|
+
triggerLayout: LayoutRectangle | null;
|
|
17
|
+
setTriggerLayout: (layout: LayoutRectangle | null) => void;
|
|
18
|
+
} | null>(null);
|
|
19
|
+
|
|
20
|
+
export const Select = ({ open: openProp, defaultOpen, onOpenChange, value: valueProp, defaultValue, onValueChange, children }: any) => {
|
|
21
|
+
const [open, setOpen] = useControllableState({
|
|
22
|
+
prop: openProp,
|
|
23
|
+
defaultProp: defaultOpen || false,
|
|
24
|
+
onChange: onOpenChange,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const [value, setValue] = useControllableState({
|
|
28
|
+
prop: valueProp,
|
|
29
|
+
defaultProp: defaultValue || '',
|
|
30
|
+
onChange: onValueChange,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<SelectContext.Provider value={{ open, onOpenChange: setOpen, value, onValueChange: setValue, triggerLayout, setTriggerLayout }}>
|
|
37
|
+
{children}
|
|
38
|
+
</SelectContext.Provider>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const SelectTrigger = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
43
|
+
({ className, children, ...props }, ref) => {
|
|
44
|
+
const context = React.useContext(SelectContext);
|
|
45
|
+
if (!context) throw new Error('SelectTrigger must be used within Select');
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View
|
|
49
|
+
ref={ref}
|
|
50
|
+
collapsable={false}
|
|
51
|
+
onLayout={(e) => {
|
|
52
|
+
(e.target as any).measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
|
|
53
|
+
context.setTriggerLayout({ x: pageX, y: pageY, width, height });
|
|
54
|
+
});
|
|
55
|
+
}}
|
|
56
|
+
className={cn(
|
|
57
|
+
'flex h-10 w-full flex-row items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<Pressable className="flex-1 flex-row justify-between items-center" onPress={() => context.onOpenChange(true)} {...props}>
|
|
62
|
+
{children}
|
|
63
|
+
<ChevronDown size={16} className="text-muted-foreground opacity-50" />
|
|
64
|
+
</Pressable>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
SelectTrigger.displayName = 'SelectTrigger';
|
|
70
|
+
|
|
71
|
+
export const SelectValue = ({ placeholder }: { placeholder?: string }) => {
|
|
72
|
+
const context = React.useContext(SelectContext);
|
|
73
|
+
return (
|
|
74
|
+
<Text className={cn('text-sm', !context?.value ? 'text-muted-foreground' : 'text-foreground')} numberOfLines={1}>
|
|
75
|
+
{context?.value || placeholder}
|
|
76
|
+
</Text>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const SelectContent = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
81
|
+
({ className, children, ...props }, ref) => {
|
|
82
|
+
const context = React.useContext(SelectContext);
|
|
83
|
+
if (!context) throw new Error('SelectContent must be used within Select');
|
|
84
|
+
|
|
85
|
+
if (!context.triggerLayout) return null;
|
|
86
|
+
|
|
87
|
+
const { x, y, width, height } = context.triggerLayout;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Modal
|
|
91
|
+
visible={context.open}
|
|
92
|
+
transparent
|
|
93
|
+
animationType="none"
|
|
94
|
+
onRequestClose={() => context.onOpenChange(false)}
|
|
95
|
+
>
|
|
96
|
+
<View className="flex-1">
|
|
97
|
+
<Pressable className="absolute inset-0" onPress={() => context.onOpenChange(false)} />
|
|
98
|
+
<Animated.View
|
|
99
|
+
entering={ZoomIn.duration(150)}
|
|
100
|
+
exiting={FadeOut.duration(100)}
|
|
101
|
+
style={{
|
|
102
|
+
position: 'absolute',
|
|
103
|
+
top: y + height + 4,
|
|
104
|
+
left: x,
|
|
105
|
+
width: width,
|
|
106
|
+
}}
|
|
107
|
+
className={cn(
|
|
108
|
+
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md',
|
|
109
|
+
className
|
|
110
|
+
)}
|
|
111
|
+
{...props}
|
|
112
|
+
>
|
|
113
|
+
<View className="p-1">{children}</View>
|
|
114
|
+
</Animated.View>
|
|
115
|
+
</View>
|
|
116
|
+
</Modal>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
SelectContent.displayName = 'SelectContent';
|
|
121
|
+
|
|
122
|
+
export const SelectItem = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable> & { value: string; label: string }>(
|
|
123
|
+
({ className, value, label, onPress, ...props }, ref) => {
|
|
124
|
+
const context = React.useContext(SelectContext);
|
|
125
|
+
const triggerHaptic = useHaptics();
|
|
126
|
+
const { theme } = useThemeContext();
|
|
127
|
+
|
|
128
|
+
const isSelected = context?.value === value;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Pressable
|
|
132
|
+
ref={ref}
|
|
133
|
+
onPress={(e) => {
|
|
134
|
+
triggerHaptic('selection');
|
|
135
|
+
context?.onValueChange(value);
|
|
136
|
+
context?.onOpenChange(false);
|
|
137
|
+
onPress?.(e);
|
|
138
|
+
}}
|
|
139
|
+
className={cn(
|
|
140
|
+
'relative flex w-full flex-row cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground',
|
|
141
|
+
isSelected ? 'bg-accent' : '',
|
|
142
|
+
className
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
>
|
|
146
|
+
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
147
|
+
{isSelected && <Check size={16} color={theme.foreground as string} />}
|
|
148
|
+
</View>
|
|
149
|
+
<Text className="text-sm font-medium text-foreground">{label}</Text>
|
|
150
|
+
</Pressable>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
SelectItem.displayName = 'SelectItem';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './separator';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cva, type VariantProps } from '../../lib/variants';
|
|
4
|
+
import { createComponent } from '../../lib/create-component';
|
|
5
|
+
|
|
6
|
+
const separatorVariants = cva('shrink-0 bg-border', {
|
|
7
|
+
variants: {
|
|
8
|
+
orientation: {
|
|
9
|
+
horizontal: 'h-[1px] w-full',
|
|
10
|
+
vertical: 'h-full w-[1px]',
|
|
11
|
+
},
|
|
12
|
+
variant: {
|
|
13
|
+
default: 'bg-border',
|
|
14
|
+
gradient: 'bg-transparent', // Handled by Layer 3 Gradient wrapper
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
orientation: 'horizontal',
|
|
19
|
+
variant: 'default',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export interface SeparatorProps extends ViewProps, VariantProps<typeof separatorVariants> {}
|
|
24
|
+
|
|
25
|
+
export const Separator = React.forwardRef<React.ElementRef<typeof View>, SeparatorProps>(
|
|
26
|
+
({ className, orientation, variant, ...props }, ref) => {
|
|
27
|
+
return (
|
|
28
|
+
<View
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={separatorVariants({ orientation, variant, className })}
|
|
31
|
+
role={props.role ?? 'separator'}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
Separator.displayName = 'Separator';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sheet';
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { View, Modal, Pressable, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { FadeIn, FadeOut, SlideInDown, SlideOutDown } from 'react-native-reanimated';
|
|
4
|
+
import { X } from 'lucide-react-native';
|
|
5
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
6
|
+
import { cn } from '../../lib/utils';
|
|
7
|
+
import { Text } from '../typography';
|
|
8
|
+
|
|
9
|
+
const SheetContext = React.createContext<{
|
|
10
|
+
open: boolean;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
} | null>(null);
|
|
13
|
+
|
|
14
|
+
export interface SheetProps {
|
|
15
|
+
open?: boolean;
|
|
16
|
+
defaultOpen?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Sheet = ({ open: openProp, defaultOpen, onOpenChange, children }: SheetProps) => {
|
|
22
|
+
const [open, setOpen] = useControllableState({
|
|
23
|
+
prop: openProp,
|
|
24
|
+
defaultProp: defaultOpen || false,
|
|
25
|
+
onChange: onOpenChange,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<SheetContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
30
|
+
{children}
|
|
31
|
+
</SheetContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const SheetTrigger = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable>>(
|
|
36
|
+
({ children, onPress, ...props }, ref) => {
|
|
37
|
+
const context = React.useContext(SheetContext);
|
|
38
|
+
if (!context) throw new Error('SheetTrigger must be used within Sheet');
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Pressable ref={ref} onPress={(e) => { context.onOpenChange(true); onPress?.(e); }} {...props}>
|
|
42
|
+
{children}
|
|
43
|
+
</Pressable>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
SheetTrigger.displayName = 'SheetTrigger';
|
|
48
|
+
|
|
49
|
+
export interface SheetContentProps extends ViewProps {
|
|
50
|
+
overlayClassName?: string;
|
|
51
|
+
hideClose?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const SheetContent = forwardRef<React.ElementRef<typeof View>, SheetContentProps>(
|
|
55
|
+
({ className, overlayClassName, children, hideClose = false, ...props }, ref) => {
|
|
56
|
+
const context = React.useContext(SheetContext);
|
|
57
|
+
if (!context) throw new Error('SheetContent must be used within Sheet');
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Modal
|
|
61
|
+
visible={context.open}
|
|
62
|
+
transparent
|
|
63
|
+
animationType="none"
|
|
64
|
+
onRequestClose={() => context.onOpenChange(false)}
|
|
65
|
+
>
|
|
66
|
+
<View className={cn('flex-1 justify-end', overlayClassName)}>
|
|
67
|
+
<Animated.View
|
|
68
|
+
entering={FadeIn.duration(300)}
|
|
69
|
+
exiting={FadeOut.duration(300)}
|
|
70
|
+
className="absolute inset-0 bg-black/80"
|
|
71
|
+
>
|
|
72
|
+
<Pressable className="flex-1" onPress={() => context.onOpenChange(false)} />
|
|
73
|
+
</Animated.View>
|
|
74
|
+
|
|
75
|
+
<Animated.View
|
|
76
|
+
entering={SlideInDown.springify().damping(20).stiffness(200)}
|
|
77
|
+
exiting={SlideOutDown.duration(300)}
|
|
78
|
+
ref={ref}
|
|
79
|
+
className={cn(
|
|
80
|
+
'z-50 w-full gap-4 bg-background p-6 shadow-lg rounded-t-3xl border-t border-border mt-10',
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{/* Grab bar for aesthetics */}
|
|
86
|
+
<View className="w-10 h-1 bg-muted rounded-full self-center mb-4" />
|
|
87
|
+
|
|
88
|
+
{children}
|
|
89
|
+
|
|
90
|
+
{!hideClose && (
|
|
91
|
+
<Pressable
|
|
92
|
+
onPress={() => context.onOpenChange(false)}
|
|
93
|
+
className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
|
|
94
|
+
>
|
|
95
|
+
<X size={20} className="text-muted-foreground" />
|
|
96
|
+
</Pressable>
|
|
97
|
+
)}
|
|
98
|
+
</Animated.View>
|
|
99
|
+
</View>
|
|
100
|
+
</Modal>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
SheetContent.displayName = 'SheetContent';
|
|
105
|
+
|
|
106
|
+
export const SheetHeader = ({ className, ...props }: ViewProps) => (
|
|
107
|
+
<View className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
|
108
|
+
);
|
|
109
|
+
SheetHeader.displayName = 'SheetHeader';
|
|
110
|
+
|
|
111
|
+
export const SheetFooter = ({ className, ...props }: ViewProps) => (
|
|
112
|
+
<View className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-2 mt-auto pb-8', className)} {...props} />
|
|
113
|
+
);
|
|
114
|
+
SheetFooter.displayName = 'SheetFooter';
|
|
115
|
+
|
|
116
|
+
export const SheetTitle = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
117
|
+
({ className, ...props }, ref) => (
|
|
118
|
+
<Text ref={ref} className={cn('text-lg font-semibold text-foreground', className)} {...props} />
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
SheetTitle.displayName = 'SheetTitle';
|
|
122
|
+
|
|
123
|
+
export const SheetDescription = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
124
|
+
({ className, ...props }, ref) => (
|
|
125
|
+
<Text ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
|
126
|
+
)
|
|
127
|
+
);
|
|
128
|
+
SheetDescription.displayName = 'SheetDescription';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './skeleton';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withRepeat,
|
|
7
|
+
withTiming,
|
|
8
|
+
interpolate,
|
|
9
|
+
Extrapolate,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { cn } from '../../lib/utils';
|
|
12
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
13
|
+
|
|
14
|
+
export interface SkeletonProps extends ViewProps {
|
|
15
|
+
/** Uses true wave-like shimmer animation instead of opacity pulse */
|
|
16
|
+
shimmer?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Skeleton = React.forwardRef<React.ElementRef<typeof View>, SkeletonProps>(
|
|
20
|
+
({ className, shimmer = true, style, ...props }, ref) => {
|
|
21
|
+
const { theme } = useThemeContext();
|
|
22
|
+
const progress = useSharedValue(0);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
progress.value = withRepeat(
|
|
26
|
+
withTiming(1, { duration: 1200 }),
|
|
27
|
+
-1,
|
|
28
|
+
false
|
|
29
|
+
);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
33
|
+
if (!shimmer) {
|
|
34
|
+
// Fallback to simple opacity pulse if shimmer is false
|
|
35
|
+
return {
|
|
36
|
+
opacity: interpolate(progress.value, [0, 0.5, 1], [0.5, 1, 0.5]),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// We translate the shimmer layer across the background
|
|
41
|
+
return {};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn('animate-pulse rounded-md bg-muted overflow-hidden relative', className)}
|
|
48
|
+
style={style}
|
|
49
|
+
{...props}
|
|
50
|
+
>
|
|
51
|
+
{shimmer && (
|
|
52
|
+
<Animated.View
|
|
53
|
+
style={[
|
|
54
|
+
{
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
top: 0,
|
|
57
|
+
left: 0,
|
|
58
|
+
right: 0,
|
|
59
|
+
bottom: 0,
|
|
60
|
+
backgroundColor: theme.shimmer,
|
|
61
|
+
width: '200%', // wider to sweep across
|
|
62
|
+
},
|
|
63
|
+
useAnimatedStyle(() => ({
|
|
64
|
+
transform: [
|
|
65
|
+
{
|
|
66
|
+
translateX: interpolate(
|
|
67
|
+
progress.value,
|
|
68
|
+
[0, 1],
|
|
69
|
+
[-200, 200], // Translate across the screen
|
|
70
|
+
Extrapolate.CLAMP
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
})),
|
|
75
|
+
]}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
Skeleton.displayName = 'Skeleton';
|
|
83
|
+
|
|
84
|
+
import { StyleSheet } from 'react-native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './slider';
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from 'react';
|
|
2
|
+
import { View, type ViewProps, PanResponder } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withSpring,
|
|
7
|
+
runOnJS,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
10
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
11
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
12
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
13
|
+
import { cn } from '../../lib/utils';
|
|
14
|
+
|
|
15
|
+
export interface SliderProps extends Omit<ViewProps, 'value'> {
|
|
16
|
+
value?: number;
|
|
17
|
+
defaultValue?: number;
|
|
18
|
+
onValueChange?: (value: number) => void;
|
|
19
|
+
min?: number;
|
|
20
|
+
max?: number;
|
|
21
|
+
step?: number;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Slider = forwardRef<React.ElementRef<typeof View>, SliderProps>(
|
|
26
|
+
(
|
|
27
|
+
{
|
|
28
|
+
className,
|
|
29
|
+
value: valueProp,
|
|
30
|
+
defaultValue = 0,
|
|
31
|
+
onValueChange,
|
|
32
|
+
min = 0,
|
|
33
|
+
max = 100,
|
|
34
|
+
step = 1,
|
|
35
|
+
disabled = false,
|
|
36
|
+
style,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
ref
|
|
40
|
+
) => {
|
|
41
|
+
const triggerHaptic = useHaptics();
|
|
42
|
+
const springConfig = useSpring('snappy');
|
|
43
|
+
const { theme } = useThemeContext();
|
|
44
|
+
|
|
45
|
+
const [value, setValue] = useControllableState({
|
|
46
|
+
prop: valueProp,
|
|
47
|
+
defaultProp: defaultValue,
|
|
48
|
+
onChange: onValueChange,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const [width, setWidth] = useState(0);
|
|
52
|
+
const isInteracting = useSharedValue(false);
|
|
53
|
+
|
|
54
|
+
// Ensure value is within bounds
|
|
55
|
+
const clampedValue = Math.min(Math.max(value, min), max);
|
|
56
|
+
const percentage = ((clampedValue - min) / (max - min)) * 100;
|
|
57
|
+
|
|
58
|
+
const handleValueChange = (newPercentage: number) => {
|
|
59
|
+
const rawValue = (newPercentage / 100) * (max - min) + min;
|
|
60
|
+
const steppedValue = Math.round(rawValue / step) * step;
|
|
61
|
+
const finalValue = Math.min(Math.max(steppedValue, min), max);
|
|
62
|
+
|
|
63
|
+
if (finalValue !== value) {
|
|
64
|
+
// Trigger haptic if we crossed a step
|
|
65
|
+
if (step > 0 && finalValue % step === 0) {
|
|
66
|
+
triggerHaptic('selection');
|
|
67
|
+
}
|
|
68
|
+
setValue(finalValue);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const panResponder = PanResponder.create({
|
|
73
|
+
onStartShouldSetPanResponder: () => !disabled,
|
|
74
|
+
onMoveShouldSetPanResponder: () => !disabled,
|
|
75
|
+
onPanResponderGrant: (evt) => {
|
|
76
|
+
isInteracting.value = true;
|
|
77
|
+
triggerHaptic('light');
|
|
78
|
+
if (width > 0) {
|
|
79
|
+
const newPercentage = (evt.nativeEvent.locationX / width) * 100;
|
|
80
|
+
handleValueChange(newPercentage);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
onPanResponderMove: (evt, gestureState) => {
|
|
84
|
+
if (width > 0) {
|
|
85
|
+
// Adjust based on the initial touch + drag distance
|
|
86
|
+
const newPercentage = ((evt.nativeEvent.locationX) / width) * 100;
|
|
87
|
+
handleValueChange(newPercentage);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
onPanResponderRelease: () => {
|
|
91
|
+
isInteracting.value = false;
|
|
92
|
+
triggerHaptic('light');
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const trackAnimatedStyle = useAnimatedStyle(() => {
|
|
97
|
+
return {
|
|
98
|
+
width: withSpring(`${percentage}%`, springConfig),
|
|
99
|
+
};
|
|
100
|
+
}, [percentage, springConfig]);
|
|
101
|
+
|
|
102
|
+
const thumbAnimatedStyle = useAnimatedStyle(() => {
|
|
103
|
+
return {
|
|
104
|
+
left: withSpring(`${percentage}%`, springConfig),
|
|
105
|
+
transform: [
|
|
106
|
+
{ translateX: -10 }, // Half of thumb width
|
|
107
|
+
{ scale: withSpring(isInteracting.value ? 1.2 : 1, springConfig) }
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
}, [percentage, isInteracting.value, springConfig]);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<View
|
|
114
|
+
ref={ref}
|
|
115
|
+
className={cn(
|
|
116
|
+
'relative flex w-full touch-none select-none items-center justify-center py-4',
|
|
117
|
+
disabled && 'opacity-50',
|
|
118
|
+
className
|
|
119
|
+
)}
|
|
120
|
+
style={style}
|
|
121
|
+
accessibilityRole="adjustable"
|
|
122
|
+
accessibilityValue={{ min, max, now: clampedValue }}
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
<View
|
|
126
|
+
className="relative h-2 w-full overflow-hidden rounded-full bg-secondary"
|
|
127
|
+
onLayout={(e) => setWidth(e.nativeEvent.layout.width)}
|
|
128
|
+
{...panResponder.panHandlers}
|
|
129
|
+
>
|
|
130
|
+
<Animated.View
|
|
131
|
+
className="absolute h-full bg-primary"
|
|
132
|
+
style={trackAnimatedStyle}
|
|
133
|
+
/>
|
|
134
|
+
</View>
|
|
135
|
+
|
|
136
|
+
<Animated.View
|
|
137
|
+
className="absolute h-5 w-5 rounded-full border-2 border-primary bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
138
|
+
style={thumbAnimatedStyle}
|
|
139
|
+
pointerEvents="none"
|
|
140
|
+
/>
|
|
141
|
+
</View>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
Slider.displayName = 'Slider';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './switch';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { Pressable, View, type PressableProps } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
withSpring,
|
|
6
|
+
interpolateColor,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
9
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
10
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
11
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
12
|
+
import { cn } from '../../lib/utils';
|
|
13
|
+
|
|
14
|
+
export interface SwitchProps extends Omit<PressableProps, 'value'> {
|
|
15
|
+
checked?: boolean;
|
|
16
|
+
defaultChecked?: boolean;
|
|
17
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
22
|
+
|
|
23
|
+
export const Switch = forwardRef<React.ElementRef<typeof Pressable>, SwitchProps>(
|
|
24
|
+
({ className, checked: checkedProp, defaultChecked, onCheckedChange, disabled, style, ...props }, ref) => {
|
|
25
|
+
const triggerHaptic = useHaptics();
|
|
26
|
+
const springConfig = useSpring('snappy');
|
|
27
|
+
const { theme } = useThemeContext();
|
|
28
|
+
|
|
29
|
+
const [checked, setChecked] = useControllableState({
|
|
30
|
+
prop: checkedProp,
|
|
31
|
+
defaultProp: defaultChecked || false,
|
|
32
|
+
onChange: onCheckedChange,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const handlePress = () => {
|
|
36
|
+
if (disabled) return;
|
|
37
|
+
triggerHaptic('light');
|
|
38
|
+
setChecked(!checked);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const trackStyle = useAnimatedStyle(() => {
|
|
42
|
+
const backgroundColor = interpolateColor(
|
|
43
|
+
checked ? 1 : 0,
|
|
44
|
+
[0, 1],
|
|
45
|
+
[theme.input, theme.primary.DEFAULT]
|
|
46
|
+
);
|
|
47
|
+
return { backgroundColor };
|
|
48
|
+
}, [checked, theme]);
|
|
49
|
+
|
|
50
|
+
const thumbStyle = useAnimatedStyle(() => {
|
|
51
|
+
const translateX = withSpring(checked ? 20 : 0, springConfig);
|
|
52
|
+
return { transform: [{ translateX }] };
|
|
53
|
+
}, [checked, springConfig]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<AnimatedPressable
|
|
57
|
+
ref={ref}
|
|
58
|
+
onPress={handlePress}
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
accessibilityRole="switch"
|
|
61
|
+
accessibilityState={{ checked, disabled }}
|
|
62
|
+
className={cn(
|
|
63
|
+
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
|
|
64
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
style={[trackStyle, style]}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
<Animated.View
|
|
71
|
+
className="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform"
|
|
72
|
+
style={thumbStyle}
|
|
73
|
+
/>
|
|
74
|
+
</AnimatedPressable>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
Switch.displayName = 'Switch';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './table';
|