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,71 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Text } from '../typography';
|
|
5
|
+
|
|
6
|
+
export const Table = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
7
|
+
({ className, ...props }, ref) => (
|
|
8
|
+
<View className="w-full">
|
|
9
|
+
<View
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn('w-full max-w-full flex-col text-sm', className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
</View>
|
|
15
|
+
)
|
|
16
|
+
);
|
|
17
|
+
Table.displayName = 'Table';
|
|
18
|
+
|
|
19
|
+
export const TableHeader = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
20
|
+
({ className, ...props }, ref) => (
|
|
21
|
+
<View ref={ref} className={cn('flex-row border-b border-border bg-muted/50', className)} {...props} />
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
TableHeader.displayName = 'TableHeader';
|
|
25
|
+
|
|
26
|
+
export const TableBody = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
27
|
+
({ className, ...props }, ref) => (
|
|
28
|
+
<View ref={ref} className={cn('flex-col', className)} {...props} />
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
TableBody.displayName = 'TableBody';
|
|
32
|
+
|
|
33
|
+
export const TableFooter = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
34
|
+
({ className, ...props }, ref) => (
|
|
35
|
+
<View ref={ref} className={cn('flex-row border-t border-border bg-muted/50 font-medium', className)} {...props} />
|
|
36
|
+
)
|
|
37
|
+
);
|
|
38
|
+
TableFooter.displayName = 'TableFooter';
|
|
39
|
+
|
|
40
|
+
export const TableRow = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
41
|
+
({ className, ...props }, ref) => (
|
|
42
|
+
<View
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn('flex-row border-b border-border transition-colors hover:bg-muted/50', className)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
)
|
|
48
|
+
);
|
|
49
|
+
TableRow.displayName = 'TableRow';
|
|
50
|
+
|
|
51
|
+
export const TableHead = forwardRef<React.ElementRef<typeof View>, ViewProps & { title: string }>(
|
|
52
|
+
({ className, title, ...props }, ref) => (
|
|
53
|
+
<View
|
|
54
|
+
ref={ref}
|
|
55
|
+
className={cn('h-12 flex-1 justify-center px-4 align-middle font-medium', className)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
<Text className="text-muted-foreground font-semibold">{title}</Text>
|
|
59
|
+
</View>
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
TableHead.displayName = 'TableHead';
|
|
63
|
+
|
|
64
|
+
export const TableCell = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
65
|
+
({ className, children, ...props }, ref) => (
|
|
66
|
+
<View ref={ref} className={cn('flex-1 justify-center p-4 align-middle', className)} {...props}>
|
|
67
|
+
{typeof children === 'string' ? <Text>{children}</Text> : children}
|
|
68
|
+
</View>
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
TableCell.displayName = 'TableCell';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './tabs';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { forwardRef, useState } from 'react';
|
|
2
|
+
import { View, Pressable, type ViewProps, type PressableProps } from 'react-native';
|
|
3
|
+
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
|
|
4
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
5
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
6
|
+
import { useSpring } from '../../hooks/use-spring';
|
|
7
|
+
import { cn } from '../../lib/utils';
|
|
8
|
+
import { Text } from '../typography';
|
|
9
|
+
|
|
10
|
+
const TabsContext = React.createContext<{
|
|
11
|
+
value: string;
|
|
12
|
+
onValueChange: (value: string) => void;
|
|
13
|
+
} | null>(null);
|
|
14
|
+
|
|
15
|
+
export interface TabsProps extends ViewProps {
|
|
16
|
+
value?: string;
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
onValueChange?: (value: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const Tabs = forwardRef<React.ElementRef<typeof View>, TabsProps>(
|
|
22
|
+
({ className, value: valueProp, defaultValue, onValueChange, children, ...props }, ref) => {
|
|
23
|
+
const [value, setValue] = useControllableState({
|
|
24
|
+
prop: valueProp,
|
|
25
|
+
defaultProp: defaultValue || '',
|
|
26
|
+
onChange: onValueChange,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<TabsContext.Provider value={{ value, onValueChange: setValue }}>
|
|
31
|
+
<View ref={ref} className={cn('w-full', className)} {...props}>
|
|
32
|
+
{children}
|
|
33
|
+
</View>
|
|
34
|
+
</TabsContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
Tabs.displayName = 'Tabs';
|
|
39
|
+
|
|
40
|
+
export const TabsList = forwardRef<React.ElementRef<typeof View>, ViewProps>(
|
|
41
|
+
({ className, children, ...props }, ref) => (
|
|
42
|
+
<View
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn(
|
|
45
|
+
'inline-flex h-10 flex-row items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</View>
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
TabsList.displayName = 'TabsList';
|
|
55
|
+
|
|
56
|
+
export interface TabsTriggerProps extends PressableProps {
|
|
57
|
+
value: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const TabsTrigger = forwardRef<React.ElementRef<typeof Pressable>, TabsTriggerProps>(
|
|
61
|
+
({ className, value, disabled, children, ...props }, ref) => {
|
|
62
|
+
const context = React.useContext(TabsContext);
|
|
63
|
+
if (!context) throw new Error('TabsTrigger must be used within Tabs');
|
|
64
|
+
const triggerHaptic = useHaptics();
|
|
65
|
+
|
|
66
|
+
const isActive = context.value === value;
|
|
67
|
+
|
|
68
|
+
const handlePress = () => {
|
|
69
|
+
if (disabled) return;
|
|
70
|
+
if (!isActive) {
|
|
71
|
+
triggerHaptic('selection');
|
|
72
|
+
context.onValueChange(value);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Pressable
|
|
78
|
+
ref={ref}
|
|
79
|
+
onPress={handlePress}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
className={cn(
|
|
82
|
+
'inline-flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all',
|
|
83
|
+
isActive ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground',
|
|
84
|
+
disabled && 'opacity-50',
|
|
85
|
+
className
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
{typeof children === 'string' ? (
|
|
90
|
+
<Text className={cn('text-sm font-medium', isActive ? 'text-foreground' : 'text-muted-foreground')}>
|
|
91
|
+
{children}
|
|
92
|
+
</Text>
|
|
93
|
+
) : (
|
|
94
|
+
children
|
|
95
|
+
)}
|
|
96
|
+
</Pressable>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
TabsTrigger.displayName = 'TabsTrigger';
|
|
101
|
+
|
|
102
|
+
export interface TabsContentProps extends ViewProps {
|
|
103
|
+
value: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const TabsContent = forwardRef<React.ElementRef<typeof View>, TabsContentProps>(
|
|
107
|
+
({ className, value, children, ...props }, ref) => {
|
|
108
|
+
const context = React.useContext(TabsContext);
|
|
109
|
+
if (!context) throw new Error('TabsContent must be used within Tabs');
|
|
110
|
+
|
|
111
|
+
if (context.value !== value) return null;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<View
|
|
115
|
+
ref={ref}
|
|
116
|
+
className={cn('mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', className)}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
TabsContent.displayName = 'TabsContent';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './textarea';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { TextInput, View, type TextInputProps, type NativeSyntheticEvent, type TextInputContentSizeChangeEventData } from 'react-native';
|
|
3
|
+
import { cva, type VariantProps } from '../../lib/variants';
|
|
4
|
+
import { useThemeContext } from '../../context/theme-context';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { Text } from '../typography';
|
|
7
|
+
|
|
8
|
+
const textareaVariants = cva(
|
|
9
|
+
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: '',
|
|
14
|
+
glass: 'bg-glass border-glass-border',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: 'default',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export interface TextareaProps extends TextInputProps, VariantProps<typeof textareaVariants> {
|
|
24
|
+
error?: string;
|
|
25
|
+
helperText?: string;
|
|
26
|
+
autoGrow?: boolean;
|
|
27
|
+
maxHeight?: number;
|
|
28
|
+
showCharacterCount?: boolean;
|
|
29
|
+
maxLength?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const Textarea = React.forwardRef<React.ElementRef<typeof TextInput>, TextareaProps>(
|
|
33
|
+
({ className, variant, error, helperText, autoGrow = true, maxHeight = 200, showCharacterCount, maxLength, value, onChangeText, style, ...props }, ref) => {
|
|
34
|
+
const { theme } = useThemeContext();
|
|
35
|
+
const [height, setHeight] = useState(80);
|
|
36
|
+
|
|
37
|
+
const handleContentSizeChange = (e: NativeSyntheticEvent<TextInputContentSizeChangeEventData>) => {
|
|
38
|
+
if (autoGrow) {
|
|
39
|
+
setHeight(Math.min(Math.max(80, e.nativeEvent.contentSize.height), maxHeight));
|
|
40
|
+
}
|
|
41
|
+
props.onContentSizeChange?.(e);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const count = value?.length || 0;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View className="flex flex-col space-y-1.5 w-full">
|
|
48
|
+
<TextInput
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn(
|
|
51
|
+
textareaVariants({ variant }),
|
|
52
|
+
error && 'border-destructive focus-visible:ring-destructive',
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
style={[style, autoGrow && { height }]}
|
|
56
|
+
placeholderTextColor={theme.muted.foreground as string}
|
|
57
|
+
multiline
|
|
58
|
+
textAlignVertical="top"
|
|
59
|
+
onContentSizeChange={handleContentSizeChange}
|
|
60
|
+
value={value}
|
|
61
|
+
onChangeText={onChangeText}
|
|
62
|
+
maxLength={maxLength}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
|
|
66
|
+
<View className="flex flex-row justify-between w-full">
|
|
67
|
+
{(helperText || error) ? (
|
|
68
|
+
<Text className={cn('text-sm flex-1', error ? 'text-destructive' : 'text-muted-foreground')}>
|
|
69
|
+
{error || helperText}
|
|
70
|
+
</Text>
|
|
71
|
+
) : <View />}
|
|
72
|
+
|
|
73
|
+
{showCharacterCount && maxLength && (
|
|
74
|
+
<Text className="text-sm text-muted-foreground text-right pl-2">
|
|
75
|
+
{count}/{maxLength}
|
|
76
|
+
</Text>
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
Textarea.displayName = 'Textarea';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './toast';
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { View, Pressable, type ViewProps, SafeAreaView } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withSpring,
|
|
7
|
+
withTiming,
|
|
8
|
+
withDelay,
|
|
9
|
+
runOnJS,
|
|
10
|
+
} from 'react-native-reanimated';
|
|
11
|
+
import { X } from 'lucide-react-native';
|
|
12
|
+
import { useToastContext, type ToastProps } from '../../context/toast-context';
|
|
13
|
+
import { cn } from '../../lib/utils';
|
|
14
|
+
import { Text } from '../typography';
|
|
15
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
16
|
+
|
|
17
|
+
const ToastItem = ({ toast, onDismiss }: { toast: ToastProps; onDismiss: (id: string) => void }) => {
|
|
18
|
+
const translateY = useSharedValue(-100);
|
|
19
|
+
const opacity = useSharedValue(0);
|
|
20
|
+
const triggerHaptic = useHaptics();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
// Entrance animation
|
|
24
|
+
translateY.value = withSpring(0, { damping: 15, stiffness: 200 });
|
|
25
|
+
opacity.value = withTiming(1, { duration: 300 });
|
|
26
|
+
|
|
27
|
+
// Haptic feedback based on type
|
|
28
|
+
if (toast.type === 'error') triggerHaptic('error');
|
|
29
|
+
else if (toast.type === 'success') triggerHaptic('success');
|
|
30
|
+
else if (toast.type === 'warning') triggerHaptic('warning');
|
|
31
|
+
else triggerHaptic('light');
|
|
32
|
+
|
|
33
|
+
// Setup exit animation manually if it wasn't auto-dismissed early
|
|
34
|
+
return () => {
|
|
35
|
+
// Cleanup happens via the dismiss function animating out
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
const handleDismiss = () => {
|
|
40
|
+
translateY.value = withTiming(-100, { duration: 300 });
|
|
41
|
+
opacity.value = withTiming(0, { duration: 300 }, () => {
|
|
42
|
+
runOnJS(onDismiss)(toast.id);
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
47
|
+
transform: [{ translateY: translateY.value }],
|
|
48
|
+
opacity: opacity.value,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const getVariantStyles = () => {
|
|
52
|
+
switch (toast.type) {
|
|
53
|
+
case 'destructive':
|
|
54
|
+
case 'error':
|
|
55
|
+
return 'bg-destructive border-destructive text-destructive-foreground';
|
|
56
|
+
case 'success':
|
|
57
|
+
return 'bg-success border-success text-success-foreground';
|
|
58
|
+
case 'warning':
|
|
59
|
+
return 'bg-warning border-warning text-warning-foreground';
|
|
60
|
+
default:
|
|
61
|
+
return 'bg-background border-border text-foreground';
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Animated.View
|
|
67
|
+
style={animatedStyle}
|
|
68
|
+
className={cn(
|
|
69
|
+
'pointer-events-auto relative w-full flex-row items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all',
|
|
70
|
+
getVariantStyles()
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<View className="flex-1 flex-col gap-1">
|
|
74
|
+
<Text className={cn('text-sm font-semibold', toast.type && toast.type !== 'default' ? 'text-white' : 'text-foreground')}>
|
|
75
|
+
{toast.title}
|
|
76
|
+
</Text>
|
|
77
|
+
{toast.description && (
|
|
78
|
+
<Text className={cn('text-sm opacity-90', toast.type && toast.type !== 'default' ? 'text-white' : 'text-muted-foreground')}>
|
|
79
|
+
{toast.description}
|
|
80
|
+
</Text>
|
|
81
|
+
)}
|
|
82
|
+
</View>
|
|
83
|
+
|
|
84
|
+
{toast.action && (
|
|
85
|
+
<Pressable
|
|
86
|
+
onPress={toast.action.onPress}
|
|
87
|
+
className="inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
88
|
+
>
|
|
89
|
+
<Text className={toast.type && toast.type !== 'default' ? 'text-white' : 'text-foreground'}>
|
|
90
|
+
{toast.action.label}
|
|
91
|
+
</Text>
|
|
92
|
+
</Pressable>
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
<Pressable
|
|
96
|
+
onPress={handleDismiss}
|
|
97
|
+
className="absolute right-2 top-2 rounded-md p-1 opacity-70 transition-opacity hover:opacity-100"
|
|
98
|
+
>
|
|
99
|
+
<X size={16} color={toast.type && toast.type !== 'default' ? 'white' : 'gray'} />
|
|
100
|
+
</Pressable>
|
|
101
|
+
</Animated.View>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const ToastViewport = ({ className, ...props }: ViewProps) => {
|
|
106
|
+
const { toasts, dismiss } = useToastContext();
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<SafeAreaView pointerEvents="box-none" className="absolute inset-0 z-50">
|
|
110
|
+
<View
|
|
111
|
+
pointerEvents="box-none"
|
|
112
|
+
className={cn(
|
|
113
|
+
'flex-col items-center justify-start gap-2 p-4 pt-10 sm:justify-end sm:p-6 w-full max-w-[420px] self-center',
|
|
114
|
+
className
|
|
115
|
+
)}
|
|
116
|
+
{...props}
|
|
117
|
+
>
|
|
118
|
+
{toasts.map((toast) => (
|
|
119
|
+
<ToastItem key={toast.id} toast={toast} onDismiss={dismiss} />
|
|
120
|
+
))}
|
|
121
|
+
</View>
|
|
122
|
+
</SafeAreaView>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './toggle';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { Pressable, type PressableProps } from 'react-native';
|
|
3
|
+
import { cva, type VariantProps } from '../../lib/variants';
|
|
4
|
+
import { createComponent } from '../../lib/create-component';
|
|
5
|
+
import { Text } from '../typography';
|
|
6
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
7
|
+
import { useHaptics } from '../../hooks/use-haptics';
|
|
8
|
+
|
|
9
|
+
const toggleVariants = cva(
|
|
10
|
+
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
variant: {
|
|
14
|
+
default: 'bg-transparent',
|
|
15
|
+
outline: 'border border-input bg-transparent hover:bg-accent hover:text-accent-foreground',
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
default: 'h-10 px-3',
|
|
19
|
+
sm: 'h-9 px-2.5',
|
|
20
|
+
lg: 'h-11 px-5',
|
|
21
|
+
},
|
|
22
|
+
pressed: {
|
|
23
|
+
true: '',
|
|
24
|
+
false: '',
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
compoundVariants: [
|
|
28
|
+
{ variant: 'default', pressed: true, className: 'bg-accent text-accent-foreground' },
|
|
29
|
+
{ variant: 'outline', pressed: true, className: 'bg-accent text-accent-foreground' },
|
|
30
|
+
],
|
|
31
|
+
defaultVariants: {
|
|
32
|
+
variant: 'default',
|
|
33
|
+
size: 'default',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
export interface ToggleProps extends Omit<PressableProps, 'value'>, VariantProps<typeof toggleVariants> {
|
|
39
|
+
pressed?: boolean;
|
|
40
|
+
defaultPressed?: boolean;
|
|
41
|
+
onPressedChange?: (pressed: boolean) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const Toggle = forwardRef<React.ElementRef<typeof Pressable>, ToggleProps>(
|
|
45
|
+
({ className, variant, size, pressed: pressedProp, defaultPressed, onPressedChange, disabled, children, style, ...props }, ref) => {
|
|
46
|
+
const triggerHaptic = useHaptics();
|
|
47
|
+
|
|
48
|
+
const [pressed, setPressed] = useControllableState({
|
|
49
|
+
prop: pressedProp,
|
|
50
|
+
defaultProp: defaultPressed || false,
|
|
51
|
+
onChange: onPressedChange,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const handlePress = () => {
|
|
55
|
+
if (disabled) return;
|
|
56
|
+
triggerHaptic('light');
|
|
57
|
+
setPressed(!pressed);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<Pressable
|
|
62
|
+
ref={ref}
|
|
63
|
+
onPress={handlePress}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
accessibilityRole="button"
|
|
66
|
+
accessibilityState={{ selected: pressed, disabled: !!disabled }}
|
|
67
|
+
className={toggleVariants({ variant, size, pressed, className })}
|
|
68
|
+
style={style}
|
|
69
|
+
{...props}
|
|
70
|
+
>
|
|
71
|
+
{typeof children === 'string' ? (
|
|
72
|
+
<Text className={cn(
|
|
73
|
+
'text-sm font-medium',
|
|
74
|
+
pressed ? 'text-accent-foreground' : 'text-foreground'
|
|
75
|
+
)}>
|
|
76
|
+
{children}
|
|
77
|
+
</Text>
|
|
78
|
+
) : (
|
|
79
|
+
children
|
|
80
|
+
)}
|
|
81
|
+
</Pressable>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
Toggle.displayName = 'Toggle';
|
|
86
|
+
|
|
87
|
+
import { cn } from '../../lib/utils';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './toggle-group';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { forwardRef, createContext, useContext } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cva, type VariantProps } from '../../lib/variants';
|
|
4
|
+
import { Toggle } from '../toggle';
|
|
5
|
+
import { useControllableState } from '../../hooks/use-controllable';
|
|
6
|
+
|
|
7
|
+
const ToggleGroupContext = createContext<{
|
|
8
|
+
type: 'single' | 'multiple';
|
|
9
|
+
value: string | string[];
|
|
10
|
+
onValueChange: (value: string) => void;
|
|
11
|
+
variant?: 'default' | 'outline';
|
|
12
|
+
size?: 'default' | 'sm' | 'lg';
|
|
13
|
+
} | null>(null);
|
|
14
|
+
|
|
15
|
+
export interface ToggleGroupProps extends ViewProps {
|
|
16
|
+
type: 'single' | 'multiple';
|
|
17
|
+
value?: string | string[];
|
|
18
|
+
defaultValue?: string | string[];
|
|
19
|
+
onValueChange?: (value: any) => void;
|
|
20
|
+
variant?: 'default' | 'outline';
|
|
21
|
+
size?: 'default' | 'sm' | 'lg';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ToggleGroup = forwardRef<React.ElementRef<typeof View>, ToggleGroupProps>(
|
|
25
|
+
({ className, type, value: valueProp, defaultValue, onValueChange, variant, size, children, ...props }, ref) => {
|
|
26
|
+
const [value, setValue] = useControllableState({
|
|
27
|
+
prop: valueProp,
|
|
28
|
+
defaultProp: defaultValue || (type === 'single' ? '' : []),
|
|
29
|
+
onChange: onValueChange,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const handleValueChange = (itemValue: string) => {
|
|
33
|
+
if (type === 'single') {
|
|
34
|
+
setValue(value === itemValue ? '' : itemValue);
|
|
35
|
+
} else {
|
|
36
|
+
const arr = value as string[];
|
|
37
|
+
setValue(
|
|
38
|
+
arr.includes(itemValue)
|
|
39
|
+
? arr.filter((v) => v !== itemValue)
|
|
40
|
+
: [...arr, itemValue]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<ToggleGroupContext.Provider value={{ type, value, onValueChange: handleValueChange, variant, size }}>
|
|
47
|
+
<View ref={ref} className={cn('flex flex-row items-center justify-center gap-1', className)} {...props}>
|
|
48
|
+
{children}
|
|
49
|
+
</View>
|
|
50
|
+
</ToggleGroupContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
ToggleGroup.displayName = 'ToggleGroup';
|
|
55
|
+
|
|
56
|
+
export interface ToggleGroupItemProps extends Omit<React.ComponentPropsWithoutRef<typeof Toggle>, 'pressed' | 'onPressedChange'> {
|
|
57
|
+
value: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const ToggleGroupItem = forwardRef<React.ElementRef<typeof Toggle>, ToggleGroupItemProps>(
|
|
61
|
+
({ className, value, children, ...props }, ref) => {
|
|
62
|
+
const context = useContext(ToggleGroupContext);
|
|
63
|
+
if (!context) throw new Error('ToggleGroupItem must be used within a ToggleGroup');
|
|
64
|
+
|
|
65
|
+
const isPressed =
|
|
66
|
+
context.type === 'single'
|
|
67
|
+
? context.value === value
|
|
68
|
+
: (context.value as string[]).includes(value);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Toggle
|
|
72
|
+
ref={ref}
|
|
73
|
+
variant={context.variant}
|
|
74
|
+
size={context.size}
|
|
75
|
+
pressed={isPressed}
|
|
76
|
+
onPressedChange={() => context.onValueChange(value)}
|
|
77
|
+
className={className}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
{children}
|
|
81
|
+
</Toggle>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
ToggleGroupItem.displayName = 'ToggleGroupItem';
|
|
86
|
+
|
|
87
|
+
import { cn } from '../../lib/utils';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './tooltip';
|