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.
Files changed (122) hide show
  1. package/lib/module/components/alert/index.js +122 -0
  2. package/lib/module/components/alert/index.js.map +1 -0
  3. package/lib/module/components/avatar/index.js +74 -0
  4. package/lib/module/components/avatar/index.js.map +1 -0
  5. package/lib/module/components/badge/index.js +77 -0
  6. package/lib/module/components/badge/index.js.map +1 -0
  7. package/lib/module/components/bottom-sheet/index.js +137 -0
  8. package/lib/module/components/bottom-sheet/index.js.map +1 -0
  9. package/lib/module/components/button/index.js +101 -0
  10. package/lib/module/components/button/index.js.map +1 -0
  11. package/lib/module/components/card/index.js +73 -0
  12. package/lib/module/components/card/index.js.map +1 -0
  13. package/lib/module/components/checkbox/index.js +84 -0
  14. package/lib/module/components/checkbox/index.js.map +1 -0
  15. package/lib/module/components/dialog/index.js +140 -0
  16. package/lib/module/components/dialog/index.js.map +1 -0
  17. package/lib/module/components/input/index.js +92 -0
  18. package/lib/module/components/input/index.js.map +1 -0
  19. package/lib/module/components/radio-group/index.js +92 -0
  20. package/lib/module/components/radio-group/index.js.map +1 -0
  21. package/lib/module/components/select/index.js +147 -0
  22. package/lib/module/components/select/index.js.map +1 -0
  23. package/lib/module/components/skeleton/index.js +26 -0
  24. package/lib/module/components/skeleton/index.js.map +1 -0
  25. package/lib/module/components/spinner/index.js +47 -0
  26. package/lib/module/components/spinner/index.js.map +1 -0
  27. package/lib/module/components/switch/index.js +96 -0
  28. package/lib/module/components/switch/index.js.map +1 -0
  29. package/lib/module/components/tabs/index.js +161 -0
  30. package/lib/module/components/tabs/index.js.map +1 -0
  31. package/lib/module/icons/index.js +65 -0
  32. package/lib/module/icons/index.js.map +1 -0
  33. package/lib/module/index.js +36 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/module/primitives/animated-pressable.js +51 -0
  37. package/lib/module/primitives/animated-pressable.js.map +1 -0
  38. package/lib/module/primitives/portal.js +76 -0
  39. package/lib/module/primitives/portal.js.map +1 -0
  40. package/lib/module/primitives/text.js +51 -0
  41. package/lib/module/primitives/text.js.map +1 -0
  42. package/lib/module/providers/panel-ui-provider.js +29 -0
  43. package/lib/module/providers/panel-ui-provider.js.map +1 -0
  44. package/lib/module/theme/use-theme.js +19 -0
  45. package/lib/module/theme/use-theme.js.map +1 -0
  46. package/lib/module/uniwind-env.d.js +4 -0
  47. package/lib/module/uniwind-env.d.js.map +1 -0
  48. package/lib/module/utils/cn.js +8 -0
  49. package/lib/module/utils/cn.js.map +1 -0
  50. package/lib/typescript/package.json +1 -0
  51. package/lib/typescript/src/components/alert/index.d.ts +113 -0
  52. package/lib/typescript/src/components/alert/index.d.ts.map +1 -0
  53. package/lib/typescript/src/components/avatar/index.d.ts +83 -0
  54. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -0
  55. package/lib/typescript/src/components/badge/index.d.ts +114 -0
  56. package/lib/typescript/src/components/badge/index.d.ts.map +1 -0
  57. package/lib/typescript/src/components/bottom-sheet/index.d.ts +30 -0
  58. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -0
  59. package/lib/typescript/src/components/button/index.d.ts +159 -0
  60. package/lib/typescript/src/components/button/index.d.ts.map +1 -0
  61. package/lib/typescript/src/components/card/index.d.ts +13 -0
  62. package/lib/typescript/src/components/card/index.d.ts.map +1 -0
  63. package/lib/typescript/src/components/checkbox/index.d.ts +47 -0
  64. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -0
  65. package/lib/typescript/src/components/dialog/index.d.ts +50 -0
  66. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -0
  67. package/lib/typescript/src/components/input/index.d.ts +12 -0
  68. package/lib/typescript/src/components/input/index.d.ts.map +1 -0
  69. package/lib/typescript/src/components/radio-group/index.d.ts +20 -0
  70. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -0
  71. package/lib/typescript/src/components/select/index.d.ts +23 -0
  72. package/lib/typescript/src/components/select/index.d.ts.map +1 -0
  73. package/lib/typescript/src/components/skeleton/index.d.ts +6 -0
  74. package/lib/typescript/src/components/skeleton/index.d.ts.map +1 -0
  75. package/lib/typescript/src/components/spinner/index.d.ts +27 -0
  76. package/lib/typescript/src/components/spinner/index.d.ts.map +1 -0
  77. package/lib/typescript/src/components/switch/index.d.ts +76 -0
  78. package/lib/typescript/src/components/switch/index.d.ts.map +1 -0
  79. package/lib/typescript/src/components/tabs/index.d.ts +34 -0
  80. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -0
  81. package/lib/typescript/src/icons/index.d.ts +9 -0
  82. package/lib/typescript/src/icons/index.d.ts.map +1 -0
  83. package/lib/typescript/src/index.d.ts +23 -0
  84. package/lib/typescript/src/index.d.ts.map +1 -0
  85. package/lib/typescript/src/primitives/animated-pressable.d.ts +14 -0
  86. package/lib/typescript/src/primitives/animated-pressable.d.ts.map +1 -0
  87. package/lib/typescript/src/primitives/portal.d.ts +11 -0
  88. package/lib/typescript/src/primitives/portal.d.ts.map +1 -0
  89. package/lib/typescript/src/primitives/text.d.ts +66 -0
  90. package/lib/typescript/src/primitives/text.d.ts.map +1 -0
  91. package/lib/typescript/src/providers/panel-ui-provider.d.ts +13 -0
  92. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -0
  93. package/lib/typescript/src/theme/use-theme.d.ts +12 -0
  94. package/lib/typescript/src/theme/use-theme.d.ts.map +1 -0
  95. package/lib/typescript/src/utils/cn.d.ts +3 -0
  96. package/lib/typescript/src/utils/cn.d.ts.map +1 -0
  97. package/package.json +73 -0
  98. package/src/components/alert/index.tsx +106 -0
  99. package/src/components/avatar/index.tsx +68 -0
  100. package/src/components/badge/index.tsx +51 -0
  101. package/src/components/bottom-sheet/index.tsx +186 -0
  102. package/src/components/button/index.tsx +93 -0
  103. package/src/components/card/index.tsx +64 -0
  104. package/src/components/checkbox/index.tsx +79 -0
  105. package/src/components/dialog/index.tsx +180 -0
  106. package/src/components/input/index.tsx +104 -0
  107. package/src/components/radio-group/index.tsx +118 -0
  108. package/src/components/select/index.tsx +165 -0
  109. package/src/components/skeleton/index.tsx +34 -0
  110. package/src/components/spinner/index.tsx +53 -0
  111. package/src/components/switch/index.tsx +83 -0
  112. package/src/components/tabs/index.tsx +208 -0
  113. package/src/icons/index.tsx +48 -0
  114. package/src/index.ts +54 -0
  115. package/src/primitives/animated-pressable.tsx +71 -0
  116. package/src/primitives/portal.tsx +90 -0
  117. package/src/primitives/text.tsx +47 -0
  118. package/src/providers/panel-ui-provider.tsx +30 -0
  119. package/src/theme/use-theme.ts +18 -0
  120. package/src/uniwind-env.d.ts +1 -0
  121. package/src/utils/cn.ts +6 -0
  122. 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';