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,83 @@
|
|
|
1
|
+
import { forwardRef, useEffect } from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
interpolate,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withSpring,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
10
|
+
|
|
11
|
+
const SPRING = { damping: 18, stiffness: 250, mass: 0.5 } as const;
|
|
12
|
+
|
|
13
|
+
const switchVariants = tv({
|
|
14
|
+
slots: {
|
|
15
|
+
track: 'justify-center rounded-full border border-transparent bg-input p-[3px]',
|
|
16
|
+
activeTrack: 'absolute inset-0 rounded-full bg-primary',
|
|
17
|
+
thumb: 'rounded-full bg-white shadow-sm',
|
|
18
|
+
},
|
|
19
|
+
variants: {
|
|
20
|
+
size: {
|
|
21
|
+
sm: { track: 'h-6 w-10', thumb: 'h-4 w-4' },
|
|
22
|
+
md: { track: 'h-7 w-12', thumb: 'h-5 w-5' },
|
|
23
|
+
},
|
|
24
|
+
disabled: {
|
|
25
|
+
true: { track: 'opacity-50' },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
size: 'md',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const TRAVEL: Record<'sm' | 'md', number> = { sm: 16, md: 20 };
|
|
34
|
+
|
|
35
|
+
export interface SwitchProps extends VariantProps<typeof switchVariants> {
|
|
36
|
+
className?: string;
|
|
37
|
+
value: boolean;
|
|
38
|
+
onValueChange?: (value: boolean) => void;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Animated switch. Thumb position and active-track opacity are driven on the
|
|
44
|
+
* UI thread; toggling never re-renders beyond the value change itself.
|
|
45
|
+
*/
|
|
46
|
+
export const Switch = forwardRef<View, SwitchProps>(
|
|
47
|
+
({ className, value, onValueChange, disabled, size = 'md' }, ref) => {
|
|
48
|
+
const progress = useSharedValue(value ? 1 : 0);
|
|
49
|
+
const slots = switchVariants({ size, disabled: !!disabled });
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
progress.value = withSpring(value ? 1 : 0, SPRING);
|
|
53
|
+
}, [value, progress]);
|
|
54
|
+
|
|
55
|
+
const thumbStyle = useAnimatedStyle(() => ({
|
|
56
|
+
transform: [
|
|
57
|
+
{ translateX: interpolate(progress.value, [0, 1], [0, TRAVEL[size]]) },
|
|
58
|
+
],
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const activeTrackStyle = useAnimatedStyle(() => ({
|
|
62
|
+
opacity: progress.value,
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Pressable
|
|
67
|
+
ref={ref}
|
|
68
|
+
accessibilityRole="switch"
|
|
69
|
+
accessibilityState={{ checked: value, disabled: !!disabled }}
|
|
70
|
+
disabled={disabled}
|
|
71
|
+
onPress={() => onValueChange?.(!value)}
|
|
72
|
+
hitSlop={8}
|
|
73
|
+
>
|
|
74
|
+
<View className={slots.track({ className })}>
|
|
75
|
+
<Animated.View style={activeTrackStyle} className={slots.activeTrack()} />
|
|
76
|
+
<Animated.View style={thumbStyle} className={slots.thumb()} />
|
|
77
|
+
</View>
|
|
78
|
+
</Pressable>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
Switch.displayName = 'Switch';
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { Pressable, View, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
10
|
+
import Animated, {
|
|
11
|
+
FadeIn,
|
|
12
|
+
useAnimatedStyle,
|
|
13
|
+
useSharedValue,
|
|
14
|
+
withSpring,
|
|
15
|
+
} from 'react-native-reanimated';
|
|
16
|
+
import { Text } from '../../primitives/text';
|
|
17
|
+
import { cn } from '../../utils/cn';
|
|
18
|
+
|
|
19
|
+
const SPRING = { damping: 24, stiffness: 300, mass: 0.7 } as const;
|
|
20
|
+
|
|
21
|
+
interface TabLayout {
|
|
22
|
+
x: number;
|
|
23
|
+
width: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TabsContextValue {
|
|
27
|
+
value: string;
|
|
28
|
+
setValue: (value: string) => void;
|
|
29
|
+
registerLayout: (value: string, layout: TabLayout) => void;
|
|
30
|
+
layouts: Record<string, TabLayout>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TabsContext = createContext<TabsContextValue | null>(null);
|
|
34
|
+
|
|
35
|
+
function useTabs(component: string): TabsContextValue {
|
|
36
|
+
const context = useContext(TabsContext);
|
|
37
|
+
if (!context) {
|
|
38
|
+
throw new Error(`${component} must be used within a <Tabs>`);
|
|
39
|
+
}
|
|
40
|
+
return context;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TabsProps extends ViewProps {
|
|
44
|
+
className?: string;
|
|
45
|
+
value?: string;
|
|
46
|
+
onValueChange?: (value: string) => void;
|
|
47
|
+
defaultValue: string;
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function TabsRoot({
|
|
52
|
+
className,
|
|
53
|
+
value,
|
|
54
|
+
onValueChange,
|
|
55
|
+
defaultValue,
|
|
56
|
+
children,
|
|
57
|
+
...props
|
|
58
|
+
}: TabsProps) {
|
|
59
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
60
|
+
const [layouts, setLayouts] = useState<Record<string, TabLayout>>({});
|
|
61
|
+
const isControlled = value !== undefined;
|
|
62
|
+
const resolvedValue = isControlled ? value : internalValue;
|
|
63
|
+
|
|
64
|
+
const setValue = useCallback(
|
|
65
|
+
(next: string) => {
|
|
66
|
+
if (!isControlled) setInternalValue(next);
|
|
67
|
+
onValueChange?.(next);
|
|
68
|
+
},
|
|
69
|
+
[isControlled, onValueChange]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const registerLayout = useCallback((tab: string, layout: TabLayout) => {
|
|
73
|
+
setLayouts((current) => {
|
|
74
|
+
const existing = current[tab];
|
|
75
|
+
if (existing && existing.x === layout.x && existing.width === layout.width) {
|
|
76
|
+
return current;
|
|
77
|
+
}
|
|
78
|
+
return { ...current, [tab]: layout };
|
|
79
|
+
});
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const context = useMemo(
|
|
83
|
+
() => ({ value: resolvedValue, setValue, registerLayout, layouts }),
|
|
84
|
+
[resolvedValue, setValue, registerLayout, layouts]
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<TabsContext.Provider value={context}>
|
|
89
|
+
<View className={cn('gap-3', className)} {...props}>
|
|
90
|
+
{children}
|
|
91
|
+
</View>
|
|
92
|
+
</TabsContext.Provider>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function TabsIndicator() {
|
|
97
|
+
const { value, layouts } = useTabs('Tabs.List');
|
|
98
|
+
const x = useSharedValue(0);
|
|
99
|
+
const width = useSharedValue(0);
|
|
100
|
+
const initialized = useSharedValue(0);
|
|
101
|
+
|
|
102
|
+
const layout = layouts[value];
|
|
103
|
+
if (layout) {
|
|
104
|
+
if (initialized.value === 0) {
|
|
105
|
+
x.value = layout.x;
|
|
106
|
+
width.value = layout.width;
|
|
107
|
+
initialized.value = 1;
|
|
108
|
+
} else {
|
|
109
|
+
x.value = withSpring(layout.x, SPRING);
|
|
110
|
+
width.value = withSpring(layout.width, SPRING);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const style = useAnimatedStyle(() => ({
|
|
115
|
+
opacity: initialized.value,
|
|
116
|
+
transform: [{ translateX: x.value }],
|
|
117
|
+
width: width.value,
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<Animated.View
|
|
122
|
+
style={style}
|
|
123
|
+
className="absolute bottom-1 top-1 left-0 rounded-md bg-popover shadow-sm"
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface TabsListProps extends ViewProps {
|
|
129
|
+
className?: string;
|
|
130
|
+
children: ReactNode;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function TabsList({ className, children, ...props }: TabsListProps) {
|
|
134
|
+
return (
|
|
135
|
+
<View
|
|
136
|
+
accessibilityRole="tablist"
|
|
137
|
+
className={cn('flex-row rounded-lg bg-muted p-1', className)}
|
|
138
|
+
{...props}
|
|
139
|
+
>
|
|
140
|
+
<TabsIndicator />
|
|
141
|
+
{children}
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface TabsTriggerProps {
|
|
147
|
+
className?: string;
|
|
148
|
+
value: string;
|
|
149
|
+
children: ReactNode;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function TabsTrigger({ className, value, children }: TabsTriggerProps) {
|
|
153
|
+
const context = useTabs('Tabs.Trigger');
|
|
154
|
+
const active = context.value === value;
|
|
155
|
+
|
|
156
|
+
const handleLayout = useCallback(
|
|
157
|
+
(event: LayoutChangeEvent) => {
|
|
158
|
+
const { x, width } = event.nativeEvent.layout;
|
|
159
|
+
context.registerLayout(value, { x, width });
|
|
160
|
+
},
|
|
161
|
+
[context, value]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<Pressable
|
|
166
|
+
accessibilityRole="tab"
|
|
167
|
+
accessibilityState={{ selected: active }}
|
|
168
|
+
onPress={() => context.setValue(value)}
|
|
169
|
+
onLayout={handleLayout}
|
|
170
|
+
className={cn('flex-1 items-center justify-center rounded-md py-1.5', className)}
|
|
171
|
+
>
|
|
172
|
+
{typeof children === 'string' ? (
|
|
173
|
+
<Text
|
|
174
|
+
size="sm"
|
|
175
|
+
weight="medium"
|
|
176
|
+
className={active ? 'text-foreground' : 'text-muted-foreground'}
|
|
177
|
+
>
|
|
178
|
+
{children}
|
|
179
|
+
</Text>
|
|
180
|
+
) : (
|
|
181
|
+
children
|
|
182
|
+
)}
|
|
183
|
+
</Pressable>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface TabsContentProps extends ViewProps {
|
|
188
|
+
className?: string;
|
|
189
|
+
value: string;
|
|
190
|
+
children: ReactNode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function TabsContent({ className, value, children, ...props }: TabsContentProps) {
|
|
194
|
+
const context = useTabs('Tabs.Content');
|
|
195
|
+
if (context.value !== value) return null;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<Animated.View entering={FadeIn.duration(150)} className={className} {...props}>
|
|
199
|
+
{children}
|
|
200
|
+
</Animated.View>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const Tabs = Object.assign(TabsRoot, {
|
|
205
|
+
List: TabsList,
|
|
206
|
+
Trigger: TabsTrigger,
|
|
207
|
+
Content: TabsContent,
|
|
208
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Svg, { Path, type SvgProps } from 'react-native-svg';
|
|
2
|
+
|
|
3
|
+
export interface IconProps extends SvgProps {
|
|
4
|
+
size?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function CheckIcon({ size = 14, color = '#fff', ...props }: IconProps) {
|
|
9
|
+
return (
|
|
10
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
11
|
+
<Path
|
|
12
|
+
d="M20 6 9 17l-5-5"
|
|
13
|
+
stroke={color}
|
|
14
|
+
strokeWidth={3}
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
strokeLinejoin="round"
|
|
17
|
+
/>
|
|
18
|
+
</Svg>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ChevronDownIcon({ size = 16, color = '#737373', ...props }: IconProps) {
|
|
23
|
+
return (
|
|
24
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
25
|
+
<Path
|
|
26
|
+
d="m6 9 6 6 6-6"
|
|
27
|
+
stroke={color}
|
|
28
|
+
strokeWidth={2}
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
/>
|
|
32
|
+
</Svg>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function XIcon({ size = 16, color = '#737373', ...props }: IconProps) {
|
|
37
|
+
return (
|
|
38
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
39
|
+
<Path
|
|
40
|
+
d="M18 6 6 18M6 6l12 12"
|
|
41
|
+
stroke={color}
|
|
42
|
+
strokeWidth={2}
|
|
43
|
+
strokeLinecap="round"
|
|
44
|
+
strokeLinejoin="round"
|
|
45
|
+
/>
|
|
46
|
+
</Svg>
|
|
47
|
+
);
|
|
48
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Providers
|
|
2
|
+
export { PanelUIProvider, type PanelUIProviderProps } from './providers/panel-ui-provider';
|
|
3
|
+
|
|
4
|
+
// Theme
|
|
5
|
+
export { useTheme, type ThemeName } from './theme/use-theme';
|
|
6
|
+
|
|
7
|
+
// Primitives
|
|
8
|
+
export { Portal, PortalHost, PortalProvider } from './primitives/portal';
|
|
9
|
+
export { Text, type TextProps } from './primitives/text';
|
|
10
|
+
export {
|
|
11
|
+
AnimatedPressable,
|
|
12
|
+
type AnimatedPressableProps,
|
|
13
|
+
} from './primitives/animated-pressable';
|
|
14
|
+
|
|
15
|
+
// Components
|
|
16
|
+
export { Alert, type AlertProps } from './components/alert';
|
|
17
|
+
export { Avatar, type AvatarProps } from './components/avatar';
|
|
18
|
+
export { Badge, type BadgeProps } from './components/badge';
|
|
19
|
+
export {
|
|
20
|
+
BottomSheet,
|
|
21
|
+
type BottomSheetProps,
|
|
22
|
+
type BottomSheetContentProps,
|
|
23
|
+
} from './components/bottom-sheet';
|
|
24
|
+
export { Button, type ButtonProps } from './components/button';
|
|
25
|
+
export {
|
|
26
|
+
Dialog,
|
|
27
|
+
type DialogProps,
|
|
28
|
+
type DialogContentProps,
|
|
29
|
+
} from './components/dialog';
|
|
30
|
+
export { Select, type SelectProps, type SelectItemProps } from './components/select';
|
|
31
|
+
export {
|
|
32
|
+
Tabs,
|
|
33
|
+
type TabsProps,
|
|
34
|
+
type TabsListProps,
|
|
35
|
+
type TabsTriggerProps,
|
|
36
|
+
type TabsContentProps,
|
|
37
|
+
} from './components/tabs';
|
|
38
|
+
export { Card, type CardProps } from './components/card';
|
|
39
|
+
export { Checkbox, type CheckboxProps } from './components/checkbox';
|
|
40
|
+
export { Input, type InputProps } from './components/input';
|
|
41
|
+
export {
|
|
42
|
+
RadioGroup,
|
|
43
|
+
type RadioGroupProps,
|
|
44
|
+
type RadioGroupItemProps,
|
|
45
|
+
} from './components/radio-group';
|
|
46
|
+
export { Skeleton, type SkeletonProps } from './components/skeleton';
|
|
47
|
+
export { Spinner, type SpinnerProps } from './components/spinner';
|
|
48
|
+
export { Switch, type SwitchProps } from './components/switch';
|
|
49
|
+
|
|
50
|
+
// Icons
|
|
51
|
+
export { CheckIcon, ChevronDownIcon, XIcon, type IconProps } from './icons';
|
|
52
|
+
|
|
53
|
+
// Utils
|
|
54
|
+
export { cn } from './utils/cn';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { forwardRef, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
type GestureResponderEvent,
|
|
5
|
+
type PressableProps,
|
|
6
|
+
type View,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
import Animated, {
|
|
9
|
+
useAnimatedStyle,
|
|
10
|
+
useSharedValue,
|
|
11
|
+
withSpring,
|
|
12
|
+
withTiming,
|
|
13
|
+
} from 'react-native-reanimated';
|
|
14
|
+
|
|
15
|
+
const AnimatedPressableBase = Animated.createAnimatedComponent(Pressable);
|
|
16
|
+
|
|
17
|
+
const SPRING_CONFIG = { damping: 20, stiffness: 300, mass: 0.4 } as const;
|
|
18
|
+
|
|
19
|
+
export interface AnimatedPressableProps extends PressableProps {
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Scale applied while pressed. Set to 1 to disable. Default 0.97. */
|
|
22
|
+
pressScale?: number;
|
|
23
|
+
/** Opacity applied while pressed. Set to 1 to disable. Default 1. */
|
|
24
|
+
pressOpacity?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pressable with UI-thread press feedback (scale/opacity via Reanimated).
|
|
29
|
+
* The shared base for every interactive PanelUI component.
|
|
30
|
+
*/
|
|
31
|
+
export const AnimatedPressable = forwardRef<View, AnimatedPressableProps>(
|
|
32
|
+
(
|
|
33
|
+
{ pressScale = 0.97, pressOpacity = 1, onPressIn, onPressOut, style, ...props },
|
|
34
|
+
ref
|
|
35
|
+
) => {
|
|
36
|
+
const pressed = useSharedValue(0);
|
|
37
|
+
|
|
38
|
+
const handlePressIn = useCallback(
|
|
39
|
+
(event: GestureResponderEvent) => {
|
|
40
|
+
pressed.value = withTiming(1, { duration: 80 });
|
|
41
|
+
onPressIn?.(event);
|
|
42
|
+
},
|
|
43
|
+
[onPressIn, pressed]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const handlePressOut = useCallback(
|
|
47
|
+
(event: GestureResponderEvent) => {
|
|
48
|
+
pressed.value = withSpring(0, SPRING_CONFIG);
|
|
49
|
+
onPressOut?.(event);
|
|
50
|
+
},
|
|
51
|
+
[onPressOut, pressed]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
55
|
+
transform: [{ scale: 1 + pressed.value * (pressScale - 1) }],
|
|
56
|
+
opacity: 1 + pressed.value * (pressOpacity - 1),
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<AnimatedPressableBase
|
|
61
|
+
ref={ref}
|
|
62
|
+
style={[animatedStyle, style as never]}
|
|
63
|
+
onPressIn={handlePressIn}
|
|
64
|
+
onPressOut={handlePressOut}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
AnimatedPressable.displayName = 'AnimatedPressable';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useEffect,
|
|
5
|
+
useId,
|
|
6
|
+
useRef,
|
|
7
|
+
useSyncExternalStore,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from 'react';
|
|
10
|
+
|
|
11
|
+
type PortalMap = ReadonlyMap<string, ReactNode>;
|
|
12
|
+
|
|
13
|
+
class PortalStore {
|
|
14
|
+
private portals = new Map<string, ReactNode>();
|
|
15
|
+
private snapshot: PortalMap = this.portals;
|
|
16
|
+
private listeners = new Set<() => void>();
|
|
17
|
+
|
|
18
|
+
mount = (key: string, node: ReactNode) => {
|
|
19
|
+
this.portals.set(key, node);
|
|
20
|
+
this.emit();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
unmount = (key: string) => {
|
|
24
|
+
this.portals.delete(key);
|
|
25
|
+
this.emit();
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
subscribe = (listener: () => void) => {
|
|
29
|
+
this.listeners.add(listener);
|
|
30
|
+
return () => this.listeners.delete(listener);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
getSnapshot = (): PortalMap => this.snapshot;
|
|
34
|
+
|
|
35
|
+
private emit() {
|
|
36
|
+
this.snapshot = new Map(this.portals);
|
|
37
|
+
this.listeners.forEach((listener) => listener());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PortalContext = createContext<PortalStore | null>(null);
|
|
42
|
+
|
|
43
|
+
export function PortalProvider({ children }: { children: ReactNode }) {
|
|
44
|
+
const storeRef = useRef<PortalStore | null>(null);
|
|
45
|
+
storeRef.current ??= new PortalStore();
|
|
46
|
+
return (
|
|
47
|
+
<PortalContext.Provider value={storeRef.current}>
|
|
48
|
+
{children}
|
|
49
|
+
</PortalContext.Provider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function usePortalStore(component: string): PortalStore {
|
|
54
|
+
const store = useContext(PortalContext);
|
|
55
|
+
if (!store) {
|
|
56
|
+
throw new Error(`${component} must be used within a <PanelUIProvider>`);
|
|
57
|
+
}
|
|
58
|
+
return store;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Renders children into the nearest PortalHost (above everything else). */
|
|
62
|
+
export function Portal({ children }: { children: ReactNode }) {
|
|
63
|
+
const store = usePortalStore('Portal');
|
|
64
|
+
const key = useId();
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
store.mount(key, children);
|
|
68
|
+
return () => store.unmount(key);
|
|
69
|
+
}, [store, key, children]);
|
|
70
|
+
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Mount point for portaled content. PanelUIProvider renders one automatically. */
|
|
75
|
+
export function PortalHost() {
|
|
76
|
+
const store = usePortalStore('PortalHost');
|
|
77
|
+
const portals = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
{Array.from(portals.entries(), ([key, node]) => (
|
|
82
|
+
<PortalFragment key={key}>{node}</PortalFragment>
|
|
83
|
+
))}
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function PortalFragment({ children }: { children: ReactNode }) {
|
|
89
|
+
return <>{children}</>;
|
|
90
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { Text as RNText, type Text as RNTextType, type TextProps as RNTextProps } from 'react-native';
|
|
3
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
4
|
+
|
|
5
|
+
const textVariants = tv({
|
|
6
|
+
base: 'text-foreground',
|
|
7
|
+
variants: {
|
|
8
|
+
size: {
|
|
9
|
+
xs: 'text-xs',
|
|
10
|
+
sm: 'text-sm',
|
|
11
|
+
base: 'text-base',
|
|
12
|
+
lg: 'text-lg',
|
|
13
|
+
xl: 'text-xl',
|
|
14
|
+
'2xl': 'text-2xl',
|
|
15
|
+
'3xl': 'text-3xl',
|
|
16
|
+
},
|
|
17
|
+
weight: {
|
|
18
|
+
normal: 'font-normal',
|
|
19
|
+
medium: 'font-medium',
|
|
20
|
+
semibold: 'font-semibold',
|
|
21
|
+
bold: 'font-bold',
|
|
22
|
+
},
|
|
23
|
+
muted: {
|
|
24
|
+
true: 'text-muted-foreground',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
size: 'base',
|
|
29
|
+
weight: 'normal',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export interface TextProps extends RNTextProps, VariantProps<typeof textVariants> {
|
|
34
|
+
className?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Text = forwardRef<RNTextType, TextProps>(
|
|
38
|
+
({ className, size, weight, muted, ...props }, ref) => (
|
|
39
|
+
<RNText
|
|
40
|
+
ref={ref}
|
|
41
|
+
className={textVariants({ size, weight, muted, className })}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
Text.displayName = 'Text';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
4
|
+
import { PortalHost, PortalProvider } from '../primitives/portal';
|
|
5
|
+
|
|
6
|
+
export interface PanelUIProviderProps {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Root provider for PanelUI. Wraps the app with the gesture handler root and
|
|
12
|
+
* the portal host used by overlay components (Dialog, BottomSheet, Select).
|
|
13
|
+
*
|
|
14
|
+
* Theme switching is handled natively by Uniwind — use `Uniwind.setTheme()`
|
|
15
|
+
* or the `useTheme()` hook exported from panelui-native.
|
|
16
|
+
*/
|
|
17
|
+
export function PanelUIProvider({ children }: PanelUIProviderProps) {
|
|
18
|
+
return (
|
|
19
|
+
<GestureHandlerRootView style={styles.root}>
|
|
20
|
+
<PortalProvider>
|
|
21
|
+
{children}
|
|
22
|
+
<PortalHost />
|
|
23
|
+
</PortalProvider>
|
|
24
|
+
</GestureHandlerRootView>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const styles = StyleSheet.create({
|
|
29
|
+
root: { flex: 1 },
|
|
30
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Uniwind, useUniwind } from 'uniwind';
|
|
2
|
+
|
|
3
|
+
export type ThemeName = 'light' | 'dark' | 'system';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reactive access to the current Uniwind theme plus a setter.
|
|
7
|
+
* Theme changes are applied natively by Uniwind without re-rendering the tree.
|
|
8
|
+
*/
|
|
9
|
+
export function useTheme() {
|
|
10
|
+
const { theme } = useUniwind();
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
/** Currently resolved theme ('light' or 'dark'). */
|
|
14
|
+
theme,
|
|
15
|
+
/** Switch theme: 'light', 'dark', or 'system' (follows device). */
|
|
16
|
+
setTheme: (name: ThemeName) => Uniwind.setTheme(name),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import 'uniwind/types';
|