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,145 @@
|
|
|
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 { 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 DropdownMenuContext = React.createContext<{
|
|
10
|
+
open: boolean;
|
|
11
|
+
onOpenChange: (open: boolean) => void;
|
|
12
|
+
triggerLayout: LayoutRectangle | null;
|
|
13
|
+
setTriggerLayout: (layout: LayoutRectangle | null) => void;
|
|
14
|
+
} | null>(null);
|
|
15
|
+
|
|
16
|
+
export const DropdownMenu = ({ open: openProp, defaultOpen, onOpenChange, children }: any) => {
|
|
17
|
+
const [open, setOpen] = useControllableState({
|
|
18
|
+
prop: openProp,
|
|
19
|
+
defaultProp: defaultOpen || false,
|
|
20
|
+
onChange: onOpenChange,
|
|
21
|
+
});
|
|
22
|
+
const [triggerLayout, setTriggerLayout] = useState<LayoutRectangle | null>(null);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<DropdownMenuContext.Provider value={{ open, onOpenChange: setOpen, triggerLayout, setTriggerLayout }}>
|
|
26
|
+
{children}
|
|
27
|
+
</DropdownMenuContext.Provider>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const DropdownMenuTrigger = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
32
|
+
({ children, ...props }, ref) => {
|
|
33
|
+
const context = React.useContext(DropdownMenuContext);
|
|
34
|
+
if (!context) throw new Error('DropdownMenuTrigger must be used within DropdownMenu');
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View
|
|
38
|
+
ref={ref}
|
|
39
|
+
collapsable={false}
|
|
40
|
+
onLayout={(e) => {
|
|
41
|
+
(e.target as any).measure((x: number, y: number, width: number, height: number, pageX: number, pageY: number) => {
|
|
42
|
+
context.setTriggerLayout({ x: pageX, y: pageY, width, height });
|
|
43
|
+
});
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
<Pressable onPress={() => context.onOpenChange(true)} {...props}>
|
|
47
|
+
{children}
|
|
48
|
+
</Pressable>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
DropdownMenuTrigger.displayName = 'DropdownMenuTrigger';
|
|
54
|
+
|
|
55
|
+
export const DropdownMenuContent = forwardRef<React.ElementRef<typeof View>, ViewProps & { align?: 'start' | 'center' | 'end' }>(
|
|
56
|
+
({ className, children, align = 'center', ...props }, ref) => {
|
|
57
|
+
const context = React.useContext(DropdownMenuContext);
|
|
58
|
+
if (!context) throw new Error('DropdownMenuContent must be used within DropdownMenu');
|
|
59
|
+
|
|
60
|
+
if (!context.triggerLayout) return null;
|
|
61
|
+
|
|
62
|
+
const { x, y, width, height } = context.triggerLayout;
|
|
63
|
+
|
|
64
|
+
// Very basic positioning relative to the trigger.
|
|
65
|
+
let leftPosition = x;
|
|
66
|
+
if (align === 'center') leftPosition = x + width / 2;
|
|
67
|
+
if (align === 'end') leftPosition = x + width;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Modal
|
|
71
|
+
visible={context.open}
|
|
72
|
+
transparent
|
|
73
|
+
animationType="none"
|
|
74
|
+
onRequestClose={() => context.onOpenChange(false)}
|
|
75
|
+
>
|
|
76
|
+
<View className="flex-1">
|
|
77
|
+
<Pressable className="absolute inset-0" onPress={() => context.onOpenChange(false)} />
|
|
78
|
+
<Animated.View
|
|
79
|
+
entering={ZoomIn.duration(200)}
|
|
80
|
+
exiting={FadeOut.duration(150)}
|
|
81
|
+
style={{
|
|
82
|
+
position: 'absolute',
|
|
83
|
+
top: y + height + 8,
|
|
84
|
+
left: leftPosition,
|
|
85
|
+
transform: align === 'center' ? [{ translateX: '-50%' }] : align === 'end' ? [{ translateX: '-100%' }] : [],
|
|
86
|
+
}}
|
|
87
|
+
className={cn(
|
|
88
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
{children}
|
|
94
|
+
</Animated.View>
|
|
95
|
+
</View>
|
|
96
|
+
</Modal>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
DropdownMenuContent.displayName = 'DropdownMenuContent';
|
|
101
|
+
|
|
102
|
+
export const DropdownMenuItem = forwardRef<React.ElementRef<typeof Pressable>, React.ComponentPropsWithoutRef<typeof Pressable> & { inset?: boolean }>(
|
|
103
|
+
({ className, inset, onPress, children, ...props }, ref) => {
|
|
104
|
+
const context = React.useContext(DropdownMenuContext);
|
|
105
|
+
const triggerHaptic = useHaptics();
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Pressable
|
|
109
|
+
ref={ref}
|
|
110
|
+
onPress={(e) => {
|
|
111
|
+
triggerHaptic('selection');
|
|
112
|
+
onPress?.(e);
|
|
113
|
+
context?.onOpenChange(false);
|
|
114
|
+
}}
|
|
115
|
+
className={cn(
|
|
116
|
+
'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 focus:bg-accent focus:text-accent-foreground',
|
|
117
|
+
inset && 'pl-8',
|
|
118
|
+
className
|
|
119
|
+
)}
|
|
120
|
+
{...props}
|
|
121
|
+
>
|
|
122
|
+
<Text className="text-sm font-medium text-popover-foreground">{children as any}</Text>
|
|
123
|
+
</Pressable>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
DropdownMenuItem.displayName = 'DropdownMenuItem';
|
|
128
|
+
|
|
129
|
+
export const DropdownMenuSeparator = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
130
|
+
({ className, ...props }, ref) => (
|
|
131
|
+
<View ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
|
|
135
|
+
|
|
136
|
+
export const DropdownMenuLabel = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text> & { inset?: boolean }>(
|
|
137
|
+
({ className, inset, ...props }, ref) => (
|
|
138
|
+
<Text
|
|
139
|
+
ref={ref}
|
|
140
|
+
className={cn('px-2 py-1.5 text-sm font-semibold text-popover-foreground', inset && 'pl-8', className)}
|
|
141
|
+
{...props}
|
|
142
|
+
/>
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
DropdownMenuLabel.displayName = 'DropdownMenuLabel';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dropdown-menu';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { forwardRef, createContext, useContext } from 'react';
|
|
2
|
+
import { View, type ViewProps, TextInput, type TextInputProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Text } from '../typography';
|
|
5
|
+
|
|
6
|
+
const FormItemContext = createContext<{ id: string } | null>(null);
|
|
7
|
+
|
|
8
|
+
export const Form = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
9
|
+
({ className, ...props }, ref) => (
|
|
10
|
+
<View ref={ref} className={cn('space-y-6', className)} {...props} />
|
|
11
|
+
)
|
|
12
|
+
);
|
|
13
|
+
Form.displayName = 'Form';
|
|
14
|
+
|
|
15
|
+
export const FormItem = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
16
|
+
({ className, ...props }, ref) => {
|
|
17
|
+
const id = React.useId();
|
|
18
|
+
return (
|
|
19
|
+
<FormItemContext.Provider value={{ id }}>
|
|
20
|
+
<View ref={ref} className={cn('space-y-2', className)} {...props} />
|
|
21
|
+
</FormItemContext.Provider>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
FormItem.displayName = 'FormItem';
|
|
26
|
+
|
|
27
|
+
export const FormLabel = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text> & { error?: boolean }>(
|
|
28
|
+
({ className, error, ...props }, ref) => {
|
|
29
|
+
return (
|
|
30
|
+
<Text
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn(
|
|
33
|
+
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
|
|
34
|
+
error && 'text-destructive',
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
FormLabel.displayName = 'FormLabel';
|
|
43
|
+
|
|
44
|
+
export const FormControl = forwardRef<React.ElementRef<typeof View>, ViewProps & { error?: boolean }>(
|
|
45
|
+
({ className, error, ...props }, ref) => {
|
|
46
|
+
return (
|
|
47
|
+
<View
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn('w-full', className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
FormControl.displayName = 'FormControl';
|
|
56
|
+
|
|
57
|
+
export const FormDescription = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
58
|
+
({ className, ...props }, ref) => {
|
|
59
|
+
return (
|
|
60
|
+
<Text
|
|
61
|
+
ref={ref}
|
|
62
|
+
className={cn('text-[0.8rem] text-muted-foreground', className)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
FormDescription.displayName = 'FormDescription';
|
|
69
|
+
|
|
70
|
+
export const FormMessage = forwardRef<React.ElementRef<typeof Text>, React.ComponentPropsWithoutRef<typeof Text>>(
|
|
71
|
+
({ className, children, ...props }, ref) => {
|
|
72
|
+
if (!children) return null;
|
|
73
|
+
return (
|
|
74
|
+
<Text
|
|
75
|
+
ref={ref}
|
|
76
|
+
className={cn('text-[0.8rem] font-medium text-destructive', className)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
{children}
|
|
80
|
+
</Text>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
FormMessage.displayName = 'FormMessage';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './form';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './input';
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { TextInput, View, type TextInputProps } from 'react-native';
|
|
3
|
+
import { cva, type VariantProps } from '../../lib/variants';
|
|
4
|
+
import { Text } from '../typography';
|
|
5
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
6
|
+
import Animated, { useAnimatedStyle, withSpring, useSharedValue, interpolateColor } from 'react-native-reanimated';
|
|
7
|
+
|
|
8
|
+
const inputVariants = cva(
|
|
9
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium 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 InputProps extends TextInputProps, VariantProps<typeof inputVariants> {
|
|
24
|
+
label?: string;
|
|
25
|
+
floatingLabel?: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
helperText?: string;
|
|
28
|
+
leftIcon?: React.ReactNode;
|
|
29
|
+
rightIcon?: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
|
|
33
|
+
|
|
34
|
+
export const Input = React.forwardRef<React.ElementRef<typeof TextInput>, InputProps>(
|
|
35
|
+
({ className, variant, label, floatingLabel, error, helperText, leftIcon, rightIcon, onFocus, onBlur, value, ...props }, ref) => {
|
|
36
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
37
|
+
const { theme } = useThemeContext();
|
|
38
|
+
|
|
39
|
+
// Floating label animation state
|
|
40
|
+
const floatingAnim = useSharedValue(value ? 1 : 0);
|
|
41
|
+
|
|
42
|
+
const handleFocus = (e: any) => {
|
|
43
|
+
setIsFocused(true);
|
|
44
|
+
if (floatingLabel) floatingAnim.value = withSpring(1, { damping: 20, stiffness: 300 });
|
|
45
|
+
onFocus?.(e);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const handleBlur = (e: any) => {
|
|
49
|
+
setIsFocused(false);
|
|
50
|
+
if (floatingLabel && !value) floatingAnim.value = withSpring(0, { damping: 20, stiffness: 300 });
|
|
51
|
+
onBlur?.(e);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const labelStyle = useAnimatedStyle(() => {
|
|
55
|
+
return {
|
|
56
|
+
transform: [
|
|
57
|
+
{ translateY: floatingAnim.value === 1 ? -24 : 0 },
|
|
58
|
+
{ scale: floatingAnim.value === 1 ? 0.85 : 1 },
|
|
59
|
+
],
|
|
60
|
+
opacity: floatingAnim.value === 1 ? 1 : 0.7,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View className="flex flex-col space-y-1.5 w-full relative">
|
|
66
|
+
{label && !floatingLabel && (
|
|
67
|
+
<Text className="text-sm font-medium text-foreground mb-1">{label}</Text>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
<View className="relative w-full justify-center">
|
|
71
|
+
{leftIcon && (
|
|
72
|
+
<View className="absolute left-3 z-10 justify-center h-full">
|
|
73
|
+
{leftIcon}
|
|
74
|
+
</View>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{label && floatingLabel && (
|
|
78
|
+
<Animated.Text
|
|
79
|
+
style={[
|
|
80
|
+
labelStyle,
|
|
81
|
+
{ position: 'absolute', left: leftIcon ? 36 : 12, top: 10 },
|
|
82
|
+
]}
|
|
83
|
+
className="text-sm font-medium text-muted-foreground pointer-events-none z-10"
|
|
84
|
+
>
|
|
85
|
+
{label}
|
|
86
|
+
</Animated.Text>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<AnimatedTextInput
|
|
90
|
+
ref={ref as any}
|
|
91
|
+
className={inputVariants({ variant, className: [className, leftIcon && 'pl-10', rightIcon && 'pr-10', error && 'border-destructive focus-visible:ring-destructive'].filter(Boolean).join(' ') })}
|
|
92
|
+
onFocus={handleFocus}
|
|
93
|
+
onBlur={handleBlur}
|
|
94
|
+
value={value}
|
|
95
|
+
placeholderTextColor={theme.muted.foreground}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
{rightIcon && (
|
|
100
|
+
<View className="absolute right-3 z-10 justify-center h-full">
|
|
101
|
+
{rightIcon}
|
|
102
|
+
</View>
|
|
103
|
+
)}
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
{(helperText || error) && (
|
|
107
|
+
<Text className={`text-sm ${error ? 'text-destructive' : 'text-muted-foreground'}`}>
|
|
108
|
+
{error || helperText}
|
|
109
|
+
</Text>
|
|
110
|
+
)}
|
|
111
|
+
</View>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
Input.displayName = 'Input';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './label';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { createComponent } from '../../lib/create-component';
|
|
3
|
+
import { Text, type TextProps } from '../typography';
|
|
4
|
+
|
|
5
|
+
const labelVariants = 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70';
|
|
6
|
+
|
|
7
|
+
export interface LabelProps extends TextProps {}
|
|
8
|
+
|
|
9
|
+
export const Label = createComponent<React.ElementRef<typeof Text>, LabelProps>({
|
|
10
|
+
Component: Text,
|
|
11
|
+
baseClassName: labelVariants,
|
|
12
|
+
});
|
|
13
|
+
Label.displayName = 'Label';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './navigation-menu';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { View, Pressable, type ViewProps, type PressableProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Text } from '../typography';
|
|
5
|
+
|
|
6
|
+
export const NavigationMenu = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
7
|
+
({ className, children, ...props }, ref) => (
|
|
8
|
+
<View
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn('relative z-10 flex max-w-max flex-1 items-center justify-center', className)}
|
|
11
|
+
{...props}
|
|
12
|
+
>
|
|
13
|
+
{children}
|
|
14
|
+
</View>
|
|
15
|
+
)
|
|
16
|
+
);
|
|
17
|
+
NavigationMenu.displayName = 'NavigationMenu';
|
|
18
|
+
|
|
19
|
+
export const NavigationMenuList = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
20
|
+
({ className, ...props }, ref) => (
|
|
21
|
+
<View
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn('group flex flex-1 flex-row list-none items-center justify-center space-x-1', className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
NavigationMenuList.displayName = 'NavigationMenuList';
|
|
29
|
+
|
|
30
|
+
export const NavigationMenuItem = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
31
|
+
({ className, ...props }, ref) => (
|
|
32
|
+
<View ref={ref} className={className} {...props} />
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
NavigationMenuItem.displayName = 'NavigationMenuItem';
|
|
36
|
+
|
|
37
|
+
export const NavigationMenuTrigger = forwardRef<React.ElementRef<typeof Pressable>, PressableProps>(
|
|
38
|
+
({ className, children, ...props }, ref) => (
|
|
39
|
+
<Pressable
|
|
40
|
+
ref={ref}
|
|
41
|
+
className={cn(
|
|
42
|
+
'group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50',
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
{typeof children === 'string' ? <Text className="font-medium">{children}</Text> : children}
|
|
48
|
+
</Pressable>
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
NavigationMenuTrigger.displayName = 'NavigationMenuTrigger';
|
|
52
|
+
|
|
53
|
+
export const NavigationMenuLink = forwardRef<React.ElementRef<typeof Pressable>, PressableProps & { active?: boolean }>(
|
|
54
|
+
({ className, active, children, ...props }, ref) => (
|
|
55
|
+
<Pressable
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn(
|
|
58
|
+
'block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground',
|
|
59
|
+
active && 'bg-accent/50',
|
|
60
|
+
className
|
|
61
|
+
)}
|
|
62
|
+
{...props}
|
|
63
|
+
>
|
|
64
|
+
{typeof children === 'string' ? <Text>{children}</Text> : children}
|
|
65
|
+
</Pressable>
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
NavigationMenuLink.displayName = 'NavigationMenuLink';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './pagination';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react-native';
|
|
4
|
+
import { cn } from '../../lib/utils';
|
|
5
|
+
import { Button, type ButtonProps } from '../button';
|
|
6
|
+
import { Text } from '../typography';
|
|
7
|
+
|
|
8
|
+
export const Pagination = ({ className, ...props }: ViewProps) => (
|
|
9
|
+
<View
|
|
10
|
+
role="navigation"
|
|
11
|
+
aria-label="pagination"
|
|
12
|
+
className={cn('mx-auto flex w-full justify-center flex-row', className)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
Pagination.displayName = 'Pagination';
|
|
17
|
+
|
|
18
|
+
export const PaginationContent = React.forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
19
|
+
({ className, ...props }, ref) => (
|
|
20
|
+
<View ref={ref} className={cn('flex flex-row items-center gap-1', className)} {...props} />
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
PaginationContent.displayName = 'PaginationContent';
|
|
24
|
+
|
|
25
|
+
export const PaginationItem = React.forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
26
|
+
({ className, ...props }, ref) => <View ref={ref} className={cn('', className)} {...props} />
|
|
27
|
+
);
|
|
28
|
+
PaginationItem.displayName = 'PaginationItem';
|
|
29
|
+
|
|
30
|
+
type PaginationLinkProps = {
|
|
31
|
+
isActive?: boolean;
|
|
32
|
+
} & ButtonProps;
|
|
33
|
+
|
|
34
|
+
export const PaginationLink = ({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) => (
|
|
35
|
+
<Button
|
|
36
|
+
aria-current={isActive ? 'page' : undefined}
|
|
37
|
+
variant={isActive ? 'outline' : 'ghost'}
|
|
38
|
+
size={size}
|
|
39
|
+
className={cn('w-9 h-9', className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
PaginationLink.displayName = 'PaginationLink';
|
|
44
|
+
|
|
45
|
+
export const PaginationPrevious = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
|
46
|
+
<PaginationLink aria-label="Go to previous page" size="default" className={cn('gap-1 pl-2.5', className)} {...props}>
|
|
47
|
+
<View className="flex flex-row items-center gap-1">
|
|
48
|
+
<ChevronLeft size={16} className="text-foreground" />
|
|
49
|
+
<Text className="text-sm font-medium">Previous</Text>
|
|
50
|
+
</View>
|
|
51
|
+
</PaginationLink>
|
|
52
|
+
);
|
|
53
|
+
PaginationPrevious.displayName = 'PaginationPrevious';
|
|
54
|
+
|
|
55
|
+
export const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
|
|
56
|
+
<PaginationLink aria-label="Go to next page" size="default" className={cn('gap-1 pr-2.5', className)} {...props}>
|
|
57
|
+
<View className="flex flex-row items-center gap-1">
|
|
58
|
+
<Text className="text-sm font-medium">Next</Text>
|
|
59
|
+
<ChevronRight size={16} className="text-foreground" />
|
|
60
|
+
</View>
|
|
61
|
+
</PaginationLink>
|
|
62
|
+
);
|
|
63
|
+
PaginationNext.displayName = 'PaginationNext';
|
|
64
|
+
|
|
65
|
+
export const PaginationEllipsis = ({ className, ...props }: ViewProps) => (
|
|
66
|
+
<View aria-hidden className={cn('flex h-9 w-9 items-center justify-center', className)} {...props}>
|
|
67
|
+
<MoreHorizontal size={16} className="text-foreground" />
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
PaginationEllipsis.displayName = 'PaginationEllipsis';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './progress';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, withSpring, useSharedValue } from 'react-native-reanimated';
|
|
4
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
7
|
+
|
|
8
|
+
export interface ProgressProps extends ViewProps {
|
|
9
|
+
/** Value between 0 and 100 */
|
|
10
|
+
value?: number;
|
|
11
|
+
/** Max value, defaults to 100 */
|
|
12
|
+
max?: number;
|
|
13
|
+
/** If true, the progress bar will animate continuously */
|
|
14
|
+
indeterminate?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Progress = React.forwardRef<React.ElementRef<typeof View>, ProgressProps>(
|
|
18
|
+
({ className, value = 0, max = 100, indeterminate = false, style, ...props }, ref) => {
|
|
19
|
+
const springConfig = useSpring('gentle');
|
|
20
|
+
const { theme } = useThemeContext();
|
|
21
|
+
const progressWidth = useSharedValue(0);
|
|
22
|
+
const translateX = useSharedValue(-100);
|
|
23
|
+
|
|
24
|
+
// Calculate percentage (0-100)
|
|
25
|
+
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!indeterminate) {
|
|
29
|
+
progressWidth.value = withSpring(percentage, springConfig);
|
|
30
|
+
}
|
|
31
|
+
}, [percentage, indeterminate, springConfig]);
|
|
32
|
+
|
|
33
|
+
const indicatorStyle = useAnimatedStyle(() => {
|
|
34
|
+
if (indeterminate) {
|
|
35
|
+
// We'll handle indeterminate animation via CSS/Tailwind if possible
|
|
36
|
+
// but for Native, it's better to do a repeating animation loop.
|
|
37
|
+
// For simplicity right now, we'll let it fill or just do a standard fill.
|
|
38
|
+
return { width: '50%' };
|
|
39
|
+
}
|
|
40
|
+
return { width: `${progressWidth.value}%` };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<View
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-secondary', className)}
|
|
47
|
+
style={style}
|
|
48
|
+
accessibilityRole="progressbar"
|
|
49
|
+
accessibilityValue={{ min: 0, max, now: value }}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
<Animated.View
|
|
53
|
+
className={cn(
|
|
54
|
+
'h-full w-full flex-1 bg-primary transition-all',
|
|
55
|
+
indeterminate && 'animate-pulse' // Nativewind supports animate-pulse out of the box
|
|
56
|
+
)}
|
|
57
|
+
style={[
|
|
58
|
+
indicatorStyle,
|
|
59
|
+
{ backgroundColor: theme.primary.DEFAULT as string }
|
|
60
|
+
]}
|
|
61
|
+
/>
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
Progress.displayName = 'Progress';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './radio-group';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import React, { forwardRef, createContext, useContext } from 'react';
|
|
2
|
+
import { View, Pressable, type ViewProps, type PressableProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, 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 RadioGroupContext = createContext<{
|
|
11
|
+
value: string;
|
|
12
|
+
onValueChange: (value: string) => void;
|
|
13
|
+
} | null>(null);
|
|
14
|
+
|
|
15
|
+
export interface RadioGroupProps extends ViewProps {
|
|
16
|
+
value?: string;
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
onValueChange?: (value: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const RadioGroup = forwardRef<React.ElementRef<typeof View>, RadioGroupProps>(
|
|
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
|
+
<RadioGroupContext.Provider value={{ value, onValueChange: setValue }}>
|
|
31
|
+
<View ref={ref} className={cn('flex flex-col gap-2', className)} {...props}>
|
|
32
|
+
{children}
|
|
33
|
+
</View>
|
|
34
|
+
</RadioGroupContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
RadioGroup.displayName = 'RadioGroup';
|
|
39
|
+
|
|
40
|
+
export interface RadioGroupItemProps extends PressableProps {
|
|
41
|
+
value: string;
|
|
42
|
+
label?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const AnimatedView = Animated.createAnimatedComponent(View);
|
|
46
|
+
|
|
47
|
+
export const RadioGroupItem = forwardRef<React.ElementRef<typeof Pressable>, RadioGroupItemProps>(
|
|
48
|
+
({ className, value, label, disabled, style, ...props }, ref) => {
|
|
49
|
+
const context = useContext(RadioGroupContext);
|
|
50
|
+
if (!context) throw new Error('RadioGroupItem must be used within a RadioGroup');
|
|
51
|
+
|
|
52
|
+
const triggerHaptic = useHaptics();
|
|
53
|
+
const springConfig = useSpring('snappy');
|
|
54
|
+
const checked = context.value === value;
|
|
55
|
+
|
|
56
|
+
const handlePress = () => {
|
|
57
|
+
if (disabled) return;
|
|
58
|
+
if (!checked) {
|
|
59
|
+
triggerHaptic('selection');
|
|
60
|
+
context.onValueChange(value);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const indicatorStyle = useAnimatedStyle(() => {
|
|
65
|
+
const scale = withSpring(checked ? 1 : 0, springConfig);
|
|
66
|
+
return { transform: [{ scale }] };
|
|
67
|
+
}, [checked, springConfig]);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<Pressable
|
|
71
|
+
ref={ref}
|
|
72
|
+
onPress={handlePress}
|
|
73
|
+
disabled={disabled}
|
|
74
|
+
accessibilityRole="radio"
|
|
75
|
+
accessibilityState={{ checked, disabled: !!disabled }}
|
|
76
|
+
className={cn('flex flex-row items-center space-x-2', disabled && 'opacity-50', className)}
|
|
77
|
+
{...props}
|
|
78
|
+
>
|
|
79
|
+
<View className="aspect-square h-5 w-5 items-center justify-center rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2">
|
|
80
|
+
<AnimatedView
|
|
81
|
+
style={indicatorStyle}
|
|
82
|
+
className="h-2.5 w-2.5 rounded-full bg-primary"
|
|
83
|
+
/>
|
|
84
|
+
</View>
|
|
85
|
+
{label && <Text className="text-sm font-medium leading-none">{label}</Text>}
|
|
86
|
+
</Pressable>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
RadioGroupItem.displayName = 'RadioGroupItem';
|