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,119 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View, 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 { usePressAnimation } from '../../hooks/use-press-animation';
|
|
7
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
8
|
+
import Animated from 'react-native-reanimated';
|
|
9
|
+
|
|
10
|
+
const buttonVariants = cva(
|
|
11
|
+
'flex-row items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
16
|
+
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
17
|
+
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
18
|
+
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
19
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
20
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
21
|
+
// Nativecn extensions
|
|
22
|
+
gradient: 'bg-transparent', // Handled by a Gradient wrapper in Layer 3
|
|
23
|
+
glass: 'bg-glass border border-glass-border',
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
default: 'h-10 px-4 py-2',
|
|
27
|
+
sm: 'h-9 rounded-md px-3',
|
|
28
|
+
lg: 'h-11 rounded-md px-8',
|
|
29
|
+
icon: 'h-10 w-10',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultVariants: {
|
|
33
|
+
variant: 'default',
|
|
34
|
+
size: 'default',
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Map variants to specific text styles to override inheritance issues in React Native
|
|
40
|
+
const textVariants = cva('', {
|
|
41
|
+
variants: {
|
|
42
|
+
variant: {
|
|
43
|
+
default: 'text-primary-foreground',
|
|
44
|
+
destructive: 'text-destructive-foreground',
|
|
45
|
+
outline: 'text-foreground',
|
|
46
|
+
secondary: 'text-secondary-foreground',
|
|
47
|
+
ghost: 'text-foreground',
|
|
48
|
+
link: 'text-primary underline',
|
|
49
|
+
gradient: 'text-primary-foreground',
|
|
50
|
+
glass: 'text-foreground',
|
|
51
|
+
},
|
|
52
|
+
size: {
|
|
53
|
+
default: 'text-sm font-medium',
|
|
54
|
+
sm: 'text-xs font-medium',
|
|
55
|
+
lg: 'text-base font-medium',
|
|
56
|
+
icon: 'text-sm',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
defaultVariants: {
|
|
60
|
+
variant: 'default',
|
|
61
|
+
size: 'default',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export interface ButtonProps
|
|
66
|
+
extends React.ComponentPropsWithoutRef<typeof Pressable>,
|
|
67
|
+
VariantProps<typeof buttonVariants> {
|
|
68
|
+
haptic?: 'none' | 'selection' | 'light' | 'medium' | 'heavy';
|
|
69
|
+
animated?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
73
|
+
|
|
74
|
+
export const Button = React.forwardRef<React.ElementRef<typeof Pressable>, ButtonProps>(
|
|
75
|
+
({ className, variant, size, haptic = 'selection', animated = true, children, onPressIn, onPressOut, onPress, ...props }, ref) => {
|
|
76
|
+
const triggerHaptic = useHaptics();
|
|
77
|
+
const { handlePressIn, handlePressOut, animatedStyle } = usePressAnimation({
|
|
78
|
+
scale: 'normal',
|
|
79
|
+
springConfig: 'snappy',
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const onInternalPressIn = (e: any) => {
|
|
83
|
+
if (animated) handlePressIn();
|
|
84
|
+
onPressIn?.(e);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const onInternalPressOut = (e: any) => {
|
|
88
|
+
if (animated) handlePressOut();
|
|
89
|
+
onPressOut?.(e);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onInternalPress = (e: any) => {
|
|
93
|
+
if (haptic !== 'none') triggerHaptic(haptic);
|
|
94
|
+
onPress?.(e);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const Container = animated ? AnimatedPressable : Pressable;
|
|
98
|
+
const style = animated ? animatedStyle : undefined;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Container
|
|
102
|
+
ref={ref}
|
|
103
|
+
className={buttonVariants({ variant, size, className })}
|
|
104
|
+
onPressIn={onInternalPressIn}
|
|
105
|
+
onPressOut={onInternalPressOut}
|
|
106
|
+
onPress={onInternalPress}
|
|
107
|
+
style={style}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
{typeof children === 'string' ? (
|
|
111
|
+
<Text className={textVariants({ variant, size })}>{children}</Text>
|
|
112
|
+
) : (
|
|
113
|
+
children
|
|
114
|
+
)}
|
|
115
|
+
</Container>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
Button.displayName = 'Button';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './button';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { createComponent } from '../../lib/create-component';
|
|
4
|
+
import { Text } from '../typography';
|
|
5
|
+
|
|
6
|
+
const Card = createComponent<View, ViewProps>({
|
|
7
|
+
Component: View,
|
|
8
|
+
baseClassName: 'rounded-lg border border-border bg-card shadow-sm',
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const CardHeader = createComponent<View, ViewProps>({
|
|
12
|
+
Component: View,
|
|
13
|
+
baseClassName: 'flex flex-col space-y-1.5 p-6',
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const CardTitle = React.forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
17
|
+
({ className, ...props }, ref) => (
|
|
18
|
+
<Text ref={ref} className={`text-2xl font-semibold leading-none tracking-tight ${className}`} {...props} />
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
CardTitle.displayName = 'CardTitle';
|
|
22
|
+
|
|
23
|
+
const CardDescription = React.forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
24
|
+
({ className, ...props }, ref) => (
|
|
25
|
+
<Text ref={ref} className={`text-sm text-muted-foreground ${className}`} {...props} />
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
CardDescription.displayName = 'CardDescription';
|
|
29
|
+
|
|
30
|
+
const CardContent = createComponent<View, ViewProps>({
|
|
31
|
+
Component: View,
|
|
32
|
+
baseClassName: 'p-6 pt-0',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const CardFooter = createComponent<View, ViewProps>({
|
|
36
|
+
Component: View,
|
|
37
|
+
baseClassName: 'flex flex-row items-center p-6 pt-0',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './card';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { Pressable, type PressableProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, withSpring, interpolate } from 'react-native-reanimated';
|
|
4
|
+
import { Check as CheckIcon } from 'lucide-react-native';
|
|
5
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
6
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
7
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
8
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
9
|
+
import { cn } from '../../lib/utils';
|
|
10
|
+
|
|
11
|
+
export interface CheckboxProps extends Omit<PressableProps, 'value'> {
|
|
12
|
+
checked?: boolean | 'indeterminate';
|
|
13
|
+
defaultChecked?: boolean | 'indeterminate';
|
|
14
|
+
onCheckedChange?: (checked: boolean | 'indeterminate') => void;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
19
|
+
|
|
20
|
+
export const Checkbox = forwardRef<React.ElementRef<typeof Pressable>, CheckboxProps>(
|
|
21
|
+
({ className, checked: checkedProp, defaultChecked, onCheckedChange, disabled, style, ...props }, ref) => {
|
|
22
|
+
const triggerHaptic = useHaptics();
|
|
23
|
+
const springConfig = useSpring('snappy');
|
|
24
|
+
const { theme } = useThemeContext();
|
|
25
|
+
|
|
26
|
+
const [checked, setChecked] = useControllableState({
|
|
27
|
+
prop: checkedProp,
|
|
28
|
+
defaultProp: defaultChecked || false,
|
|
29
|
+
onChange: onCheckedChange,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const isChecked = checked === true;
|
|
33
|
+
const isIndeterminate = checked === 'indeterminate';
|
|
34
|
+
|
|
35
|
+
const handlePress = () => {
|
|
36
|
+
if (disabled) return;
|
|
37
|
+
triggerHaptic('selection');
|
|
38
|
+
if (isIndeterminate) {
|
|
39
|
+
setChecked(true);
|
|
40
|
+
} else {
|
|
41
|
+
setChecked(!checked);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const containerStyle = useAnimatedStyle(() => {
|
|
46
|
+
return {
|
|
47
|
+
backgroundColor: (isChecked || isIndeterminate) ? theme.primary.DEFAULT : 'transparent',
|
|
48
|
+
borderColor: (isChecked || isIndeterminate) ? theme.primary.DEFAULT : theme.primary.DEFAULT,
|
|
49
|
+
};
|
|
50
|
+
}, [isChecked, isIndeterminate, theme]);
|
|
51
|
+
|
|
52
|
+
const iconStyle = useAnimatedStyle(() => {
|
|
53
|
+
const scale = withSpring(isChecked || isIndeterminate ? 1 : 0, springConfig);
|
|
54
|
+
const opacity = withSpring(isChecked || isIndeterminate ? 1 : 0, springConfig);
|
|
55
|
+
return {
|
|
56
|
+
transform: [{ scale }],
|
|
57
|
+
opacity,
|
|
58
|
+
};
|
|
59
|
+
}, [isChecked, isIndeterminate, springConfig]);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<AnimatedPressable
|
|
63
|
+
ref={ref}
|
|
64
|
+
onPress={handlePress}
|
|
65
|
+
disabled={disabled}
|
|
66
|
+
accessibilityRole="checkbox"
|
|
67
|
+
accessibilityState={{ checked: isChecked, disabled }}
|
|
68
|
+
className={cn(
|
|
69
|
+
'peer h-5 w-5 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 justify-center items-center',
|
|
70
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
71
|
+
className
|
|
72
|
+
)}
|
|
73
|
+
style={[containerStyle, style]}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
<Animated.View style={iconStyle} className="flex items-center justify-center text-current">
|
|
77
|
+
{isIndeterminate ? (
|
|
78
|
+
<Animated.View className="h-0.5 w-2.5 bg-primary-foreground rounded-full" />
|
|
79
|
+
) : (
|
|
80
|
+
<CheckIcon size={14} color={theme.primary.foreground as string} strokeWidth={3} />
|
|
81
|
+
)}
|
|
82
|
+
</Animated.View>
|
|
83
|
+
</AnimatedPressable>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
Checkbox.displayName = 'Checkbox';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './checkbox';
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { useSpring } from '../../hooks/use-spring';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
7
|
+
|
|
8
|
+
const CollapsibleContext = React.createContext<{
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
toggle: () => void;
|
|
11
|
+
} | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface CollapsibleProps extends ViewProps {
|
|
14
|
+
open?: boolean;
|
|
15
|
+
defaultOpen?: boolean;
|
|
16
|
+
onOpenChange?: (open: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const Collapsible = forwardRef<React.ElementRef<typeof View>, CollapsibleProps>(
|
|
20
|
+
({ className, open: openProp, defaultOpen, onOpenChange, children, ...props }, ref) => {
|
|
21
|
+
const [isOpen, setIsOpen] = useControllableState({
|
|
22
|
+
prop: openProp,
|
|
23
|
+
defaultProp: defaultOpen || false,
|
|
24
|
+
onChange: onOpenChange,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<CollapsibleContext.Provider value={{ isOpen, toggle: () => setIsOpen(!isOpen) }}>
|
|
29
|
+
<View ref={ref} className={cn('', className)} {...props}>
|
|
30
|
+
{children}
|
|
31
|
+
</View>
|
|
32
|
+
</CollapsibleContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
Collapsible.displayName = 'Collapsible';
|
|
37
|
+
|
|
38
|
+
export const CollapsibleTrigger = forwardRef<React.ElementRef<typeof Pressable>, PressableProps>(
|
|
39
|
+
({ className, children, ...props }, ref) => {
|
|
40
|
+
const context = React.useContext(CollapsibleContext);
|
|
41
|
+
if (!context) throw new Error('CollapsibleTrigger must be used within Collapsible');
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Pressable ref={ref} onPress={context.toggle} className={className} {...props}>
|
|
45
|
+
{children}
|
|
46
|
+
</Pressable>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
CollapsibleTrigger.displayName = 'CollapsibleTrigger';
|
|
51
|
+
|
|
52
|
+
export const CollapsibleContent = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
53
|
+
({ className, children, ...props }, ref) => {
|
|
54
|
+
const context = React.useContext(CollapsibleContext);
|
|
55
|
+
if (!context) throw new Error('CollapsibleContent must be used within Collapsible');
|
|
56
|
+
|
|
57
|
+
const [contentHeight, setContentHeight] = useState(0);
|
|
58
|
+
const height = useSharedValue(context.isOpen ? contentHeight : 0);
|
|
59
|
+
const opacity = useSharedValue(context.isOpen ? 1 : 0);
|
|
60
|
+
const springConfig = useSpring('snappy');
|
|
61
|
+
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
height.value = withSpring(context.isOpen ? contentHeight : 0, springConfig);
|
|
64
|
+
opacity.value = withSpring(context.isOpen ? 1 : 0, springConfig);
|
|
65
|
+
}, [context.isOpen, contentHeight, springConfig]);
|
|
66
|
+
|
|
67
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
68
|
+
height: height.value,
|
|
69
|
+
opacity: opacity.value,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Animated.View style={[animatedStyle, { overflow: 'hidden' }]}>
|
|
74
|
+
<View
|
|
75
|
+
ref={ref}
|
|
76
|
+
onLayout={(e) => {
|
|
77
|
+
const h = e.nativeEvent.layout.height;
|
|
78
|
+
if (h > 0 && contentHeight !== h) {
|
|
79
|
+
setContentHeight(h);
|
|
80
|
+
if (context.isOpen) height.value = h;
|
|
81
|
+
}
|
|
82
|
+
}}
|
|
83
|
+
className={className}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</View>
|
|
88
|
+
</Animated.View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
CollapsibleContent.displayName = 'CollapsibleContent';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './collapsible';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from 'react';
|
|
2
|
+
import { View, Pressable, Modal, type ViewProps, type LayoutRectangle, type GestureResponderEvent } from 'react-native';
|
|
3
|
+
import Animated, { FadeIn, FadeOut, ZoomIn, ZoomOut } from 'react-native-reanimated';
|
|
4
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { Text } from '../typography';
|
|
7
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
8
|
+
|
|
9
|
+
const ContextMenuContext = React.createContext<{
|
|
10
|
+
open: boolean;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
triggerPosition: { x: number; y: number } | null;
|
|
13
|
+
setTriggerPosition: (pos: { x: number; y: number } | null) => void;
|
|
14
|
+
} | null>(null);
|
|
15
|
+
|
|
16
|
+
export const ContextMenu = ({ open: openProp, defaultOpen, onOpenChange, children }: any) => {
|
|
17
|
+
const [open, setOpen] = useControllableState({
|
|
18
|
+
prop: openProp,
|
|
19
|
+
defaultProp: defaultOpen || false,
|
|
20
|
+
onChange: onOpenChange,
|
|
21
|
+
});
|
|
22
|
+
const [triggerPosition, setTriggerPosition] = useState<{ x: number; y: number } | null>(null);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ContextMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerPosition, setTriggerPosition }}>
|
|
26
|
+
{children}
|
|
27
|
+
</ContextMenuContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const ContextMenuTrigger = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
32
|
+
({ children, ...props }, ref) => {
|
|
33
|
+
const context = React.useContext(ContextMenuContext);
|
|
34
|
+
if (!context) throw new Error('ContextMenuTrigger must be used within ContextMenu');
|
|
35
|
+
const triggerHaptic = useHaptics();
|
|
36
|
+
|
|
37
|
+
const handleLongPress = (e: GestureResponderEvent) => {
|
|
38
|
+
triggerHaptic('heavy');
|
|
39
|
+
const { pageX, pageY } = e.nativeEvent;
|
|
40
|
+
context.setTriggerPosition({ x: pageX, y: pageY });
|
|
41
|
+
context.onOpenChange(true);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View ref={ref} collapsable={false}>
|
|
46
|
+
<Pressable onLongPress={handleLongPress} delayLongPress={500} {...props}>
|
|
47
|
+
{children}
|
|
48
|
+
</Pressable>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
ContextMenuTrigger.displayName = 'ContextMenuTrigger';
|
|
54
|
+
|
|
55
|
+
export const ContextMenuContent = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
56
|
+
({ className, children, ...props }, ref) => {
|
|
57
|
+
const context = React.useContext(ContextMenuContext);
|
|
58
|
+
if (!context) throw new Error('ContextMenuContent must be used within ContextMenu');
|
|
59
|
+
|
|
60
|
+
if (!context.triggerPosition) return null;
|
|
61
|
+
|
|
62
|
+
const { x, y } = context.triggerPosition;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Modal
|
|
66
|
+
visible={context.open}
|
|
67
|
+
transparent
|
|
68
|
+
animationType="none"
|
|
69
|
+
onRequestClose={() => context.onOpenChange(false)}
|
|
70
|
+
>
|
|
71
|
+
<View className="flex-1">
|
|
72
|
+
<Pressable className="absolute inset-0" onPress={() => context.onOpenChange(false)} />
|
|
73
|
+
<Animated.View
|
|
74
|
+
entering={ZoomIn.duration(150)}
|
|
75
|
+
exiting={FadeOut.duration(100)}
|
|
76
|
+
style={{
|
|
77
|
+
position: 'absolute',
|
|
78
|
+
top: y,
|
|
79
|
+
left: x,
|
|
80
|
+
}}
|
|
81
|
+
className={cn(
|
|
82
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</Animated.View>
|
|
89
|
+
</View>
|
|
90
|
+
</Modal>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
ContextMenuContent.displayName = 'ContextMenuContent';
|
|
95
|
+
|
|
96
|
+
export const ContextMenuItem = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable> & { inset?: boolean }>(
|
|
97
|
+
({ className, inset, onPress, children, ...props }, ref) => {
|
|
98
|
+
const context = React.useContext(ContextMenuContext);
|
|
99
|
+
const triggerHaptic = useHaptics();
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<Pressable
|
|
103
|
+
ref={ref}
|
|
104
|
+
onPress={(e) => {
|
|
105
|
+
triggerHaptic('selection');
|
|
106
|
+
onPress?.(e);
|
|
107
|
+
context?.onOpenChange(false);
|
|
108
|
+
}}
|
|
109
|
+
className={cn(
|
|
110
|
+
'relative flex flex-row cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground',
|
|
111
|
+
inset && 'pl-8',
|
|
112
|
+
className
|
|
113
|
+
)}
|
|
114
|
+
{...props}
|
|
115
|
+
>
|
|
116
|
+
<Text className="text-sm font-medium text-popover-foreground">{children as any}</Text>
|
|
117
|
+
</Pressable>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
ContextMenuItem.displayName = 'ContextMenuItem';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './context-menu';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { View, Modal, Pressable, type ViewProps, type ModalProps } from 'react-native';
|
|
3
|
+
import Animated, { FadeIn, FadeOut, ZoomIn, ZoomOut } 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 DialogContext = React.createContext<{
|
|
10
|
+
open: boolean;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
} | null>(null);
|
|
13
|
+
|
|
14
|
+
export interface DialogProps {
|
|
15
|
+
open?: boolean;
|
|
16
|
+
defaultOpen?: boolean;
|
|
17
|
+
onOpenChange?: (open: boolean) => void;
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Dialog = ({ open: openProp, defaultOpen, onOpenChange, children }: DialogProps) => {
|
|
22
|
+
const [open, setOpen] = useControllableState({
|
|
23
|
+
prop: openProp,
|
|
24
|
+
defaultProp: defaultOpen || false,
|
|
25
|
+
onChange: onOpenChange,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<DialogContext.Provider value={{ open, onOpenChange: setOpen }}>
|
|
30
|
+
{children}
|
|
31
|
+
</DialogContext.Provider>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const DialogTrigger = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable>>(
|
|
36
|
+
({ children, onPress, ...props }, ref) => {
|
|
37
|
+
const context = React.useContext(DialogContext);
|
|
38
|
+
if (!context) throw new Error('DialogTrigger must be used within Dialog');
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Pressable ref={ref} onPress={(e) => { context.onOpenChange(true); onPress?.(e); }} {...props}>
|
|
42
|
+
{children}
|
|
43
|
+
</Pressable>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
DialogTrigger.displayName = 'DialogTrigger';
|
|
48
|
+
|
|
49
|
+
export interface DialogContentProps extends ViewProps {
|
|
50
|
+
overlayClassName?: string;
|
|
51
|
+
hideClose?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const DialogContent = forwardRef<React.ElementRef<typeof View>, DialogContentProps>(
|
|
55
|
+
({ className, overlayClassName, children, hideClose = false, ...props }, ref) => {
|
|
56
|
+
const context = React.useContext(DialogContext);
|
|
57
|
+
if (!context) throw new Error('DialogContent must be used within Dialog');
|
|
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 items-center justify-center', overlayClassName)}>
|
|
67
|
+
<Animated.View
|
|
68
|
+
entering={FadeIn.duration(200)}
|
|
69
|
+
exiting={FadeOut.duration(200)}
|
|
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={ZoomIn.duration(200).springify().damping(20).stiffness(200)}
|
|
77
|
+
exiting={ZoomOut.duration(200)}
|
|
78
|
+
ref={ref}
|
|
79
|
+
className={cn(
|
|
80
|
+
'z-50 grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg sm:rounded-lg md:w-full sm:w-[90%] w-[90%] rounded-xl mx-4',
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
{!hideClose && (
|
|
87
|
+
<Pressable
|
|
88
|
+
onPress={() => context.onOpenChange(false)}
|
|
89
|
+
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"
|
|
90
|
+
>
|
|
91
|
+
<X size={20} className="text-muted-foreground" />
|
|
92
|
+
</Pressable>
|
|
93
|
+
)}
|
|
94
|
+
</Animated.View>
|
|
95
|
+
</View>
|
|
96
|
+
</Modal>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
DialogContent.displayName = 'DialogContent';
|
|
101
|
+
|
|
102
|
+
export const DialogHeader = ({ className, ...props }: ViewProps) => (
|
|
103
|
+
<View className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
|
|
104
|
+
);
|
|
105
|
+
DialogHeader.displayName = 'DialogHeader';
|
|
106
|
+
|
|
107
|
+
export const DialogFooter = ({ className, ...props }: ViewProps) => (
|
|
108
|
+
<View className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 gap-2 sm:gap-0 mt-4', className)} {...props} />
|
|
109
|
+
);
|
|
110
|
+
DialogFooter.displayName = 'DialogFooter';
|
|
111
|
+
|
|
112
|
+
export const DialogTitle = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
113
|
+
({ className, ...props }, ref) => (
|
|
114
|
+
<Text ref={ref} className={cn('text-lg font-semibold leading-none tracking-tight', className)} {...props} />
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
DialogTitle.displayName = 'DialogTitle';
|
|
118
|
+
|
|
119
|
+
export const DialogDescription = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
120
|
+
({ className, ...props }, ref) => (
|
|
121
|
+
<Text ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
DialogDescription.displayName = 'DialogDescription';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dialog';
|