panelui-native 0.1.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/lib/module/components/alert/index.js +122 -0
- package/lib/module/components/alert/index.js.map +1 -0
- package/lib/module/components/avatar/index.js +74 -0
- package/lib/module/components/avatar/index.js.map +1 -0
- package/lib/module/components/badge/index.js +77 -0
- package/lib/module/components/badge/index.js.map +1 -0
- package/lib/module/components/bottom-sheet/index.js +137 -0
- package/lib/module/components/bottom-sheet/index.js.map +1 -0
- package/lib/module/components/button/index.js +101 -0
- package/lib/module/components/button/index.js.map +1 -0
- package/lib/module/components/card/index.js +73 -0
- package/lib/module/components/card/index.js.map +1 -0
- package/lib/module/components/checkbox/index.js +84 -0
- package/lib/module/components/checkbox/index.js.map +1 -0
- package/lib/module/components/dialog/index.js +140 -0
- package/lib/module/components/dialog/index.js.map +1 -0
- package/lib/module/components/input/index.js +92 -0
- package/lib/module/components/input/index.js.map +1 -0
- package/lib/module/components/radio-group/index.js +92 -0
- package/lib/module/components/radio-group/index.js.map +1 -0
- package/lib/module/components/select/index.js +147 -0
- package/lib/module/components/select/index.js.map +1 -0
- package/lib/module/components/skeleton/index.js +26 -0
- package/lib/module/components/skeleton/index.js.map +1 -0
- package/lib/module/components/spinner/index.js +47 -0
- package/lib/module/components/spinner/index.js.map +1 -0
- package/lib/module/components/switch/index.js +96 -0
- package/lib/module/components/switch/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +161 -0
- package/lib/module/components/tabs/index.js.map +1 -0
- package/lib/module/icons/index.js +65 -0
- package/lib/module/icons/index.js.map +1 -0
- package/lib/module/index.js +36 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/primitives/animated-pressable.js +51 -0
- package/lib/module/primitives/animated-pressable.js.map +1 -0
- package/lib/module/primitives/portal.js +76 -0
- package/lib/module/primitives/portal.js.map +1 -0
- package/lib/module/primitives/text.js +51 -0
- package/lib/module/primitives/text.js.map +1 -0
- package/lib/module/providers/panel-ui-provider.js +29 -0
- package/lib/module/providers/panel-ui-provider.js.map +1 -0
- package/lib/module/theme/use-theme.js +19 -0
- package/lib/module/theme/use-theme.js.map +1 -0
- package/lib/module/uniwind-env.d.js +4 -0
- package/lib/module/uniwind-env.d.js.map +1 -0
- package/lib/module/utils/cn.js +8 -0
- package/lib/module/utils/cn.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/alert/index.d.ts +113 -0
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -0
- package/lib/typescript/src/components/avatar/index.d.ts +83 -0
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -0
- package/lib/typescript/src/components/badge/index.d.ts +114 -0
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +30 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -0
- package/lib/typescript/src/components/button/index.d.ts +159 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -0
- package/lib/typescript/src/components/card/index.d.ts +13 -0
- package/lib/typescript/src/components/card/index.d.ts.map +1 -0
- package/lib/typescript/src/components/checkbox/index.d.ts +47 -0
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -0
- package/lib/typescript/src/components/dialog/index.d.ts +50 -0
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -0
- package/lib/typescript/src/components/input/index.d.ts +12 -0
- package/lib/typescript/src/components/input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radio-group/index.d.ts +20 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +23 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -0
- package/lib/typescript/src/components/skeleton/index.d.ts +6 -0
- package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -0
- package/lib/typescript/src/components/spinner/index.d.ts +27 -0
- package/lib/typescript/src/components/spinner/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts +76 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts +34 -0
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +9 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +23 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/primitives/animated-pressable.d.ts +14 -0
- package/lib/typescript/src/primitives/animated-pressable.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +11 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -0
- package/lib/typescript/src/primitives/text.d.ts +66 -0
- package/lib/typescript/src/primitives/text.d.ts.map +1 -0
- package/lib/typescript/src/providers/panel-ui-provider.d.ts +13 -0
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -0
- package/lib/typescript/src/theme/use-theme.d.ts +12 -0
- package/lib/typescript/src/theme/use-theme.d.ts.map +1 -0
- package/lib/typescript/src/utils/cn.d.ts +3 -0
- package/lib/typescript/src/utils/cn.d.ts.map +1 -0
- package/package.json +73 -0
- package/src/components/alert/index.tsx +106 -0
- package/src/components/avatar/index.tsx +68 -0
- package/src/components/badge/index.tsx +51 -0
- package/src/components/bottom-sheet/index.tsx +186 -0
- package/src/components/button/index.tsx +93 -0
- package/src/components/card/index.tsx +64 -0
- package/src/components/checkbox/index.tsx +79 -0
- package/src/components/dialog/index.tsx +180 -0
- package/src/components/input/index.tsx +104 -0
- package/src/components/radio-group/index.tsx +118 -0
- package/src/components/select/index.tsx +165 -0
- package/src/components/skeleton/index.tsx +34 -0
- package/src/components/spinner/index.tsx +53 -0
- package/src/components/switch/index.tsx +83 -0
- package/src/components/tabs/index.tsx +208 -0
- package/src/icons/index.tsx +48 -0
- package/src/index.ts +54 -0
- package/src/primitives/animated-pressable.tsx +71 -0
- package/src/primitives/portal.tsx +90 -0
- package/src/primitives/text.tsx +47 -0
- package/src/providers/panel-ui-provider.tsx +30 -0
- package/src/theme/use-theme.ts +18 -0
- package/src/uniwind-env.d.ts +1 -0
- package/src/utils/cn.ts +6 -0
- package/theme.css +143 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createContext, forwardRef, useContext, type ReactNode } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
import { Text, type TextProps } from '../../primitives/text';
|
|
6
|
+
|
|
7
|
+
const alertVariants = tv({
|
|
8
|
+
slots: {
|
|
9
|
+
root: 'w-full gap-1 rounded-xl border p-4',
|
|
10
|
+
title: 'font-semibold',
|
|
11
|
+
description: 'text-sm',
|
|
12
|
+
},
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
default: {
|
|
16
|
+
root: 'border-border bg-card',
|
|
17
|
+
title: 'text-foreground',
|
|
18
|
+
description: 'text-muted-foreground',
|
|
19
|
+
},
|
|
20
|
+
info: {
|
|
21
|
+
root: 'border-info/32 bg-info/4',
|
|
22
|
+
title: 'text-foreground',
|
|
23
|
+
description: 'text-muted-foreground',
|
|
24
|
+
},
|
|
25
|
+
success: {
|
|
26
|
+
root: 'border-success/32 bg-success/4',
|
|
27
|
+
title: 'text-foreground',
|
|
28
|
+
description: 'text-muted-foreground',
|
|
29
|
+
},
|
|
30
|
+
warning: {
|
|
31
|
+
root: 'border-warning/32 bg-warning/4',
|
|
32
|
+
title: 'text-foreground',
|
|
33
|
+
description: 'text-muted-foreground',
|
|
34
|
+
},
|
|
35
|
+
destructive: {
|
|
36
|
+
root: 'border-destructive/32 bg-destructive/4',
|
|
37
|
+
title: 'text-foreground',
|
|
38
|
+
description: 'text-muted-foreground',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: {
|
|
43
|
+
variant: 'default',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type AlertVariant = VariantProps<typeof alertVariants>['variant'];
|
|
48
|
+
|
|
49
|
+
const AlertContext = createContext<AlertVariant>('default');
|
|
50
|
+
|
|
51
|
+
export interface AlertProps extends ViewProps, VariantProps<typeof alertVariants> {
|
|
52
|
+
className?: string;
|
|
53
|
+
/** Optional leading icon rendered before the content. */
|
|
54
|
+
icon?: ReactNode;
|
|
55
|
+
children?: ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const AlertRoot = forwardRef<View, AlertProps>(
|
|
59
|
+
({ className, variant = 'default', icon, children, ...props }, ref) => {
|
|
60
|
+
const { root } = alertVariants({ variant });
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<AlertContext.Provider value={variant}>
|
|
64
|
+
<View
|
|
65
|
+
ref={ref}
|
|
66
|
+
accessibilityRole="alert"
|
|
67
|
+
className={root({ className })}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{icon ? (
|
|
71
|
+
<View className="flex-row items-start gap-3">
|
|
72
|
+
<View className="mt-0.5">{icon}</View>
|
|
73
|
+
<View className="flex-1 gap-1">{children}</View>
|
|
74
|
+
</View>
|
|
75
|
+
) : (
|
|
76
|
+
children
|
|
77
|
+
)}
|
|
78
|
+
</View>
|
|
79
|
+
</AlertContext.Provider>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
AlertRoot.displayName = 'Alert';
|
|
84
|
+
|
|
85
|
+
const AlertTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
86
|
+
({ className, ...props }, ref) => {
|
|
87
|
+
const variant = useContext(AlertContext);
|
|
88
|
+
const { title } = alertVariants({ variant });
|
|
89
|
+
return <Text ref={ref} className={cn(title(), className)} {...props} />;
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
AlertTitle.displayName = 'Alert.Title';
|
|
93
|
+
|
|
94
|
+
const AlertDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
95
|
+
({ className, ...props }, ref) => {
|
|
96
|
+
const variant = useContext(AlertContext);
|
|
97
|
+
const { description } = alertVariants({ variant });
|
|
98
|
+
return <Text ref={ref} className={cn(description(), className)} {...props} />;
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
AlertDescription.displayName = 'Alert.Description';
|
|
102
|
+
|
|
103
|
+
export const Alert = Object.assign(AlertRoot, {
|
|
104
|
+
Title: AlertTitle,
|
|
105
|
+
Description: AlertDescription,
|
|
106
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { forwardRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Image,
|
|
4
|
+
View,
|
|
5
|
+
type ImageProps,
|
|
6
|
+
type ImageSourcePropType,
|
|
7
|
+
type ViewProps,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
10
|
+
import { Text } from '../../primitives/text';
|
|
11
|
+
|
|
12
|
+
const avatarVariants = tv({
|
|
13
|
+
slots: {
|
|
14
|
+
root: 'items-center justify-center overflow-hidden rounded-full border border-border bg-muted',
|
|
15
|
+
image: 'h-full w-full',
|
|
16
|
+
fallback: 'font-medium text-muted-foreground',
|
|
17
|
+
},
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
sm: { root: 'h-8 w-8', fallback: 'text-xs' },
|
|
21
|
+
md: { root: 'h-10 w-10', fallback: 'text-sm' },
|
|
22
|
+
lg: { root: 'h-14 w-14', fallback: 'text-lg' },
|
|
23
|
+
xl: { root: 'h-20 w-20', fallback: 'text-2xl' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: {
|
|
27
|
+
size: 'md',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export interface AvatarProps extends ViewProps, VariantProps<typeof avatarVariants> {
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Image source; falls back to initials when missing or on load error. */
|
|
34
|
+
source?: ImageSourcePropType;
|
|
35
|
+
/** Fallback text, e.g. initials ("KA"). */
|
|
36
|
+
fallback?: string;
|
|
37
|
+
imageProps?: Omit<ImageProps, 'source'>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Avatar = forwardRef<View, AvatarProps>(
|
|
41
|
+
({ className, size, source, fallback, imageProps, ...props }, ref) => {
|
|
42
|
+
const [errored, setErrored] = useState(false);
|
|
43
|
+
const { root, image, fallback: fallbackSlot } = avatarVariants({ size });
|
|
44
|
+
const showImage = !!source && !errored;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View
|
|
48
|
+
ref={ref}
|
|
49
|
+
accessibilityRole="image"
|
|
50
|
+
className={root({ className })}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
{showImage ? (
|
|
54
|
+
<Image
|
|
55
|
+
source={source}
|
|
56
|
+
onError={() => setErrored(true)}
|
|
57
|
+
className={image()}
|
|
58
|
+
{...imageProps}
|
|
59
|
+
/>
|
|
60
|
+
) : (
|
|
61
|
+
<Text className={fallbackSlot()}>{fallback ?? '?'}</Text>
|
|
62
|
+
)}
|
|
63
|
+
</View>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
Avatar.displayName = 'Avatar';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { forwardRef, type ReactNode } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
4
|
+
import { Text } from '../../primitives/text';
|
|
5
|
+
|
|
6
|
+
const badgeVariants = tv({
|
|
7
|
+
slots: {
|
|
8
|
+
root: 'flex-row items-center justify-center gap-1 self-start rounded-sm border border-transparent px-2 py-0.5',
|
|
9
|
+
label: 'text-xs font-medium',
|
|
10
|
+
},
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default: { root: 'bg-primary', label: 'text-primary-foreground' },
|
|
14
|
+
secondary: { root: 'bg-secondary', label: 'text-secondary-foreground' },
|
|
15
|
+
outline: { root: 'border-border bg-transparent', label: 'text-foreground' },
|
|
16
|
+
destructive: { root: 'bg-destructive', label: 'text-white' },
|
|
17
|
+
success: { root: 'bg-success/8 dark:bg-success/16', label: 'text-success-foreground' },
|
|
18
|
+
warning: { root: 'bg-warning/8 dark:bg-warning/16', label: 'text-warning-foreground' },
|
|
19
|
+
info: { root: 'bg-info/8 dark:bg-info/16', label: 'text-info-foreground' },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export interface BadgeProps
|
|
28
|
+
extends ViewProps,
|
|
29
|
+
VariantProps<typeof badgeVariants> {
|
|
30
|
+
children?: ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
labelClassName?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Badge = forwardRef<View, BadgeProps>(
|
|
36
|
+
({ children, className, labelClassName, variant, ...props }, ref) => {
|
|
37
|
+
const { root, label } = badgeVariants({ variant });
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View ref={ref} className={root({ className })} {...props}>
|
|
41
|
+
{typeof children === 'string' ? (
|
|
42
|
+
<Text className={label({ className: labelClassName })}>{children}</Text>
|
|
43
|
+
) : (
|
|
44
|
+
children
|
|
45
|
+
)}
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
Badge.displayName = 'Badge';
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cloneElement,
|
|
3
|
+
createContext,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactElement,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { Pressable, useWindowDimensions, View, type ViewProps } from 'react-native';
|
|
13
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
14
|
+
import Animated, {
|
|
15
|
+
FadeIn,
|
|
16
|
+
FadeOut,
|
|
17
|
+
SlideInDown,
|
|
18
|
+
SlideOutDown,
|
|
19
|
+
runOnJS,
|
|
20
|
+
useAnimatedStyle,
|
|
21
|
+
useSharedValue,
|
|
22
|
+
withSpring,
|
|
23
|
+
withTiming,
|
|
24
|
+
} from 'react-native-reanimated';
|
|
25
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
26
|
+
import { Portal } from '../../primitives/portal';
|
|
27
|
+
import { cn } from '../../utils/cn';
|
|
28
|
+
|
|
29
|
+
const SPRING = { damping: 22, stiffness: 280, mass: 0.7 } as const;
|
|
30
|
+
const DISMISS_DISTANCE = 120;
|
|
31
|
+
const DISMISS_VELOCITY = 800;
|
|
32
|
+
|
|
33
|
+
interface BottomSheetContextValue {
|
|
34
|
+
open: boolean;
|
|
35
|
+
setOpen: (open: boolean) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const BottomSheetContext = createContext<BottomSheetContextValue | null>(null);
|
|
39
|
+
|
|
40
|
+
function useBottomSheet(component: string): BottomSheetContextValue {
|
|
41
|
+
const context = useContext(BottomSheetContext);
|
|
42
|
+
if (!context) {
|
|
43
|
+
throw new Error(`${component} must be used within a <BottomSheet>`);
|
|
44
|
+
}
|
|
45
|
+
return context;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BottomSheetProps {
|
|
49
|
+
children: ReactNode;
|
|
50
|
+
open?: boolean;
|
|
51
|
+
onOpenChange?: (open: boolean) => void;
|
|
52
|
+
defaultOpen?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function BottomSheetRoot({
|
|
56
|
+
children,
|
|
57
|
+
open,
|
|
58
|
+
onOpenChange,
|
|
59
|
+
defaultOpen = false,
|
|
60
|
+
}: BottomSheetProps) {
|
|
61
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
62
|
+
const isControlled = open !== undefined;
|
|
63
|
+
const resolvedOpen = isControlled ? open : internalOpen;
|
|
64
|
+
|
|
65
|
+
const setOpen = useCallback(
|
|
66
|
+
(next: boolean) => {
|
|
67
|
+
if (!isControlled) setInternalOpen(next);
|
|
68
|
+
onOpenChange?.(next);
|
|
69
|
+
},
|
|
70
|
+
[isControlled, onOpenChange]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const context = useMemo(
|
|
74
|
+
() => ({ open: resolvedOpen, setOpen }),
|
|
75
|
+
[resolvedOpen, setOpen]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<BottomSheetContext.Provider value={context}>
|
|
80
|
+
{children}
|
|
81
|
+
</BottomSheetContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface BottomSheetTriggerProps {
|
|
86
|
+
children: ReactElement<{ onPress?: (...args: unknown[]) => void }>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function BottomSheetTrigger({ children }: BottomSheetTriggerProps) {
|
|
90
|
+
const { setOpen } = useBottomSheet('BottomSheet.Trigger');
|
|
91
|
+
if (!isValidElement(children)) return children;
|
|
92
|
+
|
|
93
|
+
return cloneElement(children, {
|
|
94
|
+
onPress: (...args: unknown[]) => {
|
|
95
|
+
children.props.onPress?.(...args);
|
|
96
|
+
setOpen(true);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface BottomSheetContentProps extends ViewProps {
|
|
102
|
+
className?: string;
|
|
103
|
+
/** Tap on the backdrop closes the sheet. Default true. */
|
|
104
|
+
dismissible?: boolean;
|
|
105
|
+
children?: ReactNode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function BottomSheetContent({
|
|
109
|
+
className,
|
|
110
|
+
dismissible = true,
|
|
111
|
+
children,
|
|
112
|
+
...props
|
|
113
|
+
}: BottomSheetContentProps) {
|
|
114
|
+
const { open, setOpen } = useBottomSheet('BottomSheet.Content');
|
|
115
|
+
const { height: screenHeight } = useWindowDimensions();
|
|
116
|
+
const insets = useSafeAreaInsets();
|
|
117
|
+
const translateY = useSharedValue(0);
|
|
118
|
+
|
|
119
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
120
|
+
|
|
121
|
+
const pan = Gesture.Pan()
|
|
122
|
+
.onChange((event) => {
|
|
123
|
+
// Rubber-band when dragging upward, follow the finger downward.
|
|
124
|
+
const next = translateY.value + event.changeY;
|
|
125
|
+
translateY.value = next > 0 ? next : next / 3;
|
|
126
|
+
})
|
|
127
|
+
.onEnd((event) => {
|
|
128
|
+
if (
|
|
129
|
+
translateY.value > DISMISS_DISTANCE ||
|
|
130
|
+
event.velocityY > DISMISS_VELOCITY
|
|
131
|
+
) {
|
|
132
|
+
translateY.value = withTiming(screenHeight, { duration: 200 }, () => {
|
|
133
|
+
runOnJS(close)();
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
translateY.value = withSpring(0, SPRING);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const sheetStyle = useAnimatedStyle(() => ({
|
|
141
|
+
transform: [{ translateY: translateY.value }],
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
if (!open) return null;
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Portal>
|
|
148
|
+
<View className="absolute inset-0 justify-end">
|
|
149
|
+
<Animated.View
|
|
150
|
+
entering={FadeIn.duration(180)}
|
|
151
|
+
exiting={FadeOut.duration(180)}
|
|
152
|
+
className="absolute inset-0"
|
|
153
|
+
>
|
|
154
|
+
<Pressable
|
|
155
|
+
accessibilityLabel="Close sheet"
|
|
156
|
+
className="flex-1 bg-black/50"
|
|
157
|
+
onPress={dismissible ? close : undefined}
|
|
158
|
+
/>
|
|
159
|
+
</Animated.View>
|
|
160
|
+
<GestureDetector gesture={pan}>
|
|
161
|
+
<Animated.View
|
|
162
|
+
entering={SlideInDown.springify().damping(22).stiffness(240).mass(0.8)}
|
|
163
|
+
exiting={SlideOutDown.duration(200)}
|
|
164
|
+
style={[sheetStyle, { paddingBottom: Math.max(insets.bottom, 16) }]}
|
|
165
|
+
accessibilityViewIsModal
|
|
166
|
+
className={cn(
|
|
167
|
+
'rounded-t-3xl border border-b-0 border-border bg-popover px-5 pt-2 shadow-lg',
|
|
168
|
+
className
|
|
169
|
+
)}
|
|
170
|
+
{...props}
|
|
171
|
+
>
|
|
172
|
+
<View className="mb-3 self-center">
|
|
173
|
+
<View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
|
|
174
|
+
</View>
|
|
175
|
+
{children}
|
|
176
|
+
</Animated.View>
|
|
177
|
+
</GestureDetector>
|
|
178
|
+
</View>
|
|
179
|
+
</Portal>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const BottomSheet = Object.assign(BottomSheetRoot, {
|
|
184
|
+
Trigger: BottomSheetTrigger,
|
|
185
|
+
Content: BottomSheetContent,
|
|
186
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { forwardRef, isValidElement, type ReactNode } from 'react';
|
|
2
|
+
import type { View } from 'react-native';
|
|
3
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
4
|
+
import {
|
|
5
|
+
AnimatedPressable,
|
|
6
|
+
type AnimatedPressableProps,
|
|
7
|
+
} from '../../primitives/animated-pressable';
|
|
8
|
+
import { Text } from '../../primitives/text';
|
|
9
|
+
|
|
10
|
+
const buttonVariants = tv({
|
|
11
|
+
slots: {
|
|
12
|
+
root: 'flex-row items-center justify-center gap-2 rounded-lg border border-transparent',
|
|
13
|
+
label: 'font-medium',
|
|
14
|
+
},
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
primary: {
|
|
18
|
+
root: 'border-primary bg-primary shadow-sm',
|
|
19
|
+
label: 'text-primary-foreground',
|
|
20
|
+
},
|
|
21
|
+
secondary: {
|
|
22
|
+
root: 'bg-secondary',
|
|
23
|
+
label: 'text-secondary-foreground',
|
|
24
|
+
},
|
|
25
|
+
outline: {
|
|
26
|
+
root: 'border-input bg-popover shadow-sm',
|
|
27
|
+
label: 'text-foreground',
|
|
28
|
+
},
|
|
29
|
+
ghost: {
|
|
30
|
+
root: 'bg-transparent',
|
|
31
|
+
label: 'text-foreground',
|
|
32
|
+
},
|
|
33
|
+
destructive: {
|
|
34
|
+
root: 'border-destructive bg-destructive shadow-sm',
|
|
35
|
+
label: 'text-white',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
size: {
|
|
39
|
+
sm: { root: 'h-9 gap-1.5 px-2.5', label: 'text-sm' },
|
|
40
|
+
md: { root: 'h-11 px-4', label: 'text-base' },
|
|
41
|
+
lg: { root: 'h-12 px-6', label: 'text-lg' },
|
|
42
|
+
icon: { root: 'h-11 w-11 px-0' },
|
|
43
|
+
},
|
|
44
|
+
disabled: {
|
|
45
|
+
true: { root: 'opacity-[0.64]' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
defaultVariants: {
|
|
49
|
+
variant: 'primary',
|
|
50
|
+
size: 'md',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
type ButtonVariantProps = VariantProps<typeof buttonVariants>;
|
|
55
|
+
|
|
56
|
+
export interface ButtonProps
|
|
57
|
+
extends Omit<AnimatedPressableProps, 'children' | 'disabled'>,
|
|
58
|
+
Omit<ButtonVariantProps, 'disabled'> {
|
|
59
|
+
children?: ReactNode;
|
|
60
|
+
disabled?: boolean;
|
|
61
|
+
/** Extra classes for the label when children is a string. */
|
|
62
|
+
labelClassName?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const Button = forwardRef<View, ButtonProps>(
|
|
66
|
+
(
|
|
67
|
+
{ children, className, labelClassName, variant, size, disabled, ...props },
|
|
68
|
+
ref
|
|
69
|
+
) => {
|
|
70
|
+
const { root, label } = buttonVariants({ variant, size, disabled: !!disabled });
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<AnimatedPressable
|
|
74
|
+
ref={ref}
|
|
75
|
+
accessibilityRole="button"
|
|
76
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
className={root({ className })}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
{typeof children === 'string' ? (
|
|
82
|
+
<Text className={label({ className: labelClassName })}>{children}</Text>
|
|
83
|
+
) : isValidElement(children) ? (
|
|
84
|
+
children
|
|
85
|
+
) : (
|
|
86
|
+
children
|
|
87
|
+
)}
|
|
88
|
+
</AnimatedPressable>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
Button.displayName = 'Button';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { cn } from '../../utils/cn';
|
|
4
|
+
import { Text, type TextProps } from '../../primitives/text';
|
|
5
|
+
|
|
6
|
+
export interface CardProps extends ViewProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CardRoot = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
11
|
+
<View
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn('rounded-xl border border-border bg-card shadow-sm', className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
CardRoot.displayName = 'Card';
|
|
18
|
+
|
|
19
|
+
const CardHeader = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
20
|
+
<View ref={ref} className={cn('gap-1.5 p-4', className)} {...props} />
|
|
21
|
+
));
|
|
22
|
+
CardHeader.displayName = 'Card.Header';
|
|
23
|
+
|
|
24
|
+
const CardTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
25
|
+
({ className, ...props }, ref) => (
|
|
26
|
+
<Text
|
|
27
|
+
ref={ref}
|
|
28
|
+
size="lg"
|
|
29
|
+
weight="semibold"
|
|
30
|
+
className={cn('text-card-foreground', className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
CardTitle.displayName = 'Card.Title';
|
|
36
|
+
|
|
37
|
+
const CardDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
38
|
+
({ className, ...props }, ref) => (
|
|
39
|
+
<Text ref={ref} size="sm" muted className={className} {...props} />
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
CardDescription.displayName = 'Card.Description';
|
|
43
|
+
|
|
44
|
+
const CardContent = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
45
|
+
<View ref={ref} className={cn('p-4 pt-0', className)} {...props} />
|
|
46
|
+
));
|
|
47
|
+
CardContent.displayName = 'Card.Content';
|
|
48
|
+
|
|
49
|
+
const CardFooter = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
50
|
+
<View
|
|
51
|
+
ref={ref}
|
|
52
|
+
className={cn('flex-row items-center gap-2 p-4 pt-0', className)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
));
|
|
56
|
+
CardFooter.displayName = 'Card.Footer';
|
|
57
|
+
|
|
58
|
+
export const Card = Object.assign(CardRoot, {
|
|
59
|
+
Header: CardHeader,
|
|
60
|
+
Title: CardTitle,
|
|
61
|
+
Description: CardDescription,
|
|
62
|
+
Content: CardContent,
|
|
63
|
+
Footer: CardFooter,
|
|
64
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { forwardRef, useEffect } from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withSpring,
|
|
7
|
+
withTiming,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
10
|
+
import { useCSSVariable } from 'uniwind';
|
|
11
|
+
import { CheckIcon } from '../../icons';
|
|
12
|
+
import { Text } from '../../primitives/text';
|
|
13
|
+
|
|
14
|
+
const checkboxVariants = tv({
|
|
15
|
+
slots: {
|
|
16
|
+
row: 'flex-row items-center gap-2.5',
|
|
17
|
+
box: 'h-5 w-5 items-center justify-center rounded-md border border-input bg-background',
|
|
18
|
+
fill: 'absolute inset-0 items-center justify-center rounded-md bg-primary',
|
|
19
|
+
label: 'text-sm text-foreground',
|
|
20
|
+
},
|
|
21
|
+
variants: {
|
|
22
|
+
disabled: {
|
|
23
|
+
true: { row: 'opacity-50' },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export interface CheckboxProps extends VariantProps<typeof checkboxVariants> {
|
|
29
|
+
className?: string;
|
|
30
|
+
checked: boolean;
|
|
31
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
32
|
+
disabled?: boolean;
|
|
33
|
+
/** Optional label rendered next to the box; pressing it also toggles. */
|
|
34
|
+
label?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
38
|
+
({ className, checked, onCheckedChange, disabled, label }, ref) => {
|
|
39
|
+
const progress = useSharedValue(checked ? 1 : 0);
|
|
40
|
+
const slots = checkboxVariants({ disabled: !!disabled });
|
|
41
|
+
const checkColor = useCSSVariable('--primary-foreground');
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
progress.value = checked
|
|
45
|
+
? withSpring(1, { damping: 15, stiffness: 300, mass: 0.5 })
|
|
46
|
+
: withTiming(0, { duration: 120 });
|
|
47
|
+
}, [checked, progress]);
|
|
48
|
+
|
|
49
|
+
const fillStyle = useAnimatedStyle(() => ({
|
|
50
|
+
opacity: progress.value,
|
|
51
|
+
transform: [{ scale: 0.6 + progress.value * 0.4 }],
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Pressable
|
|
56
|
+
ref={ref}
|
|
57
|
+
accessibilityRole="checkbox"
|
|
58
|
+
accessibilityState={{ checked, disabled: !!disabled }}
|
|
59
|
+
accessibilityLabel={label}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
onPress={() => onCheckedChange?.(!checked)}
|
|
62
|
+
hitSlop={8}
|
|
63
|
+
className={slots.row({ className })}
|
|
64
|
+
>
|
|
65
|
+
<View className={slots.box()}>
|
|
66
|
+
<Animated.View style={fillStyle} className={slots.fill()}>
|
|
67
|
+
<CheckIcon
|
|
68
|
+
size={12}
|
|
69
|
+
color={typeof checkColor === 'string' ? checkColor : '#fff'}
|
|
70
|
+
/>
|
|
71
|
+
</Animated.View>
|
|
72
|
+
</View>
|
|
73
|
+
{label ? <Text className={slots.label()}>{label}</Text> : null}
|
|
74
|
+
</Pressable>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
Checkbox.displayName = 'Checkbox';
|