panelui-native 0.1.0 → 0.2.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 +8 -18
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/avatar/index.js +1 -1
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +35 -31
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/card/index.js +5 -5
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +1 -1
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/dialog/index.js +21 -17
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/frame/index.js +100 -0
- package/lib/module/components/frame/index.js.map +1 -0
- package/lib/module/components/inline-select/index.js +159 -0
- package/lib/module/components/inline-select/index.js.map +1 -0
- package/lib/module/components/input/index.js +1 -1
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/select/index.js +35 -28
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/skeleton/index.js +1 -1
- package/lib/module/components/skeleton/index.js.map +1 -1
- package/lib/module/components/spinner/index.js +1 -1
- package/lib/module/components/spinner/index.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +19 -8
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/typescript/src/components/alert/index.d.ts +0 -30
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +21 -0
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -0
- package/lib/typescript/src/components/inline-select/index.d.ts +25 -0
- package/lib/typescript/src/components/inline-select/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/spinner/index.d.ts +2 -2
- package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts +20 -5
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/package.json +7 -2
- package/src/components/alert/index.tsx +8 -18
- package/src/components/avatar/index.tsx +1 -1
- package/src/components/bottom-sheet/index.tsx +8 -3
- package/src/components/card/index.tsx +5 -5
- package/src/components/checkbox/index.tsx +1 -1
- package/src/components/dialog/index.tsx +10 -5
- package/src/components/frame/index.tsx +104 -0
- package/src/components/inline-select/index.tsx +185 -0
- package/src/components/input/index.tsx +1 -1
- package/src/components/select/index.tsx +30 -23
- package/src/components/skeleton/index.tsx +1 -1
- package/src/components/spinner/index.tsx +1 -1
- package/src/index.ts +6 -0
- package/src/providers/panel-ui-provider.tsx +32 -10
- package/theme.css +122 -107
|
@@ -6,36 +6,26 @@ import { Text, type TextProps } from '../../primitives/text';
|
|
|
6
6
|
|
|
7
7
|
const alertVariants = tv({
|
|
8
8
|
slots: {
|
|
9
|
-
root: 'w-full gap-
|
|
10
|
-
title: 'font-
|
|
11
|
-
description: 'text-sm',
|
|
9
|
+
root: 'w-full gap-0.5 rounded-xl border px-3.5 py-3',
|
|
10
|
+
title: 'text-sm font-medium text-card-foreground',
|
|
11
|
+
description: 'text-sm text-muted-foreground',
|
|
12
12
|
},
|
|
13
13
|
variants: {
|
|
14
14
|
variant: {
|
|
15
15
|
default: {
|
|
16
|
-
root: 'border-border bg-
|
|
17
|
-
title: 'text-foreground',
|
|
18
|
-
description: 'text-muted-foreground',
|
|
16
|
+
root: 'border-border bg-surface',
|
|
19
17
|
},
|
|
20
18
|
info: {
|
|
21
|
-
root: 'border-info/32 bg-info/
|
|
22
|
-
title: 'text-foreground',
|
|
23
|
-
description: 'text-muted-foreground',
|
|
19
|
+
root: 'border-info/32 bg-info/10 dark:bg-info/[0.06]',
|
|
24
20
|
},
|
|
25
21
|
success: {
|
|
26
|
-
root: 'border-success/32 bg-success/
|
|
27
|
-
title: 'text-foreground',
|
|
28
|
-
description: 'text-muted-foreground',
|
|
22
|
+
root: 'border-success/32 bg-success/10 dark:bg-success/[0.06]',
|
|
29
23
|
},
|
|
30
24
|
warning: {
|
|
31
|
-
root: 'border-warning/32 bg-warning/
|
|
32
|
-
title: 'text-foreground',
|
|
33
|
-
description: 'text-muted-foreground',
|
|
25
|
+
root: 'border-warning/32 bg-warning/10 dark:bg-warning/[0.06]',
|
|
34
26
|
},
|
|
35
27
|
destructive: {
|
|
36
|
-
root: 'border-destructive/32 bg-destructive/
|
|
37
|
-
title: 'text-foreground',
|
|
38
|
-
description: 'text-muted-foreground',
|
|
28
|
+
root: 'border-destructive/32 bg-destructive/10 dark:bg-destructive/[0.06]',
|
|
39
29
|
},
|
|
40
30
|
},
|
|
41
31
|
},
|
|
@@ -12,7 +12,7 @@ import { Text } from '../../primitives/text';
|
|
|
12
12
|
const avatarVariants = tv({
|
|
13
13
|
slots: {
|
|
14
14
|
root: 'items-center justify-center overflow-hidden rounded-full border border-border bg-muted',
|
|
15
|
-
image: 'h-full w-full',
|
|
15
|
+
image: 'absolute inset-0 h-full w-full',
|
|
16
16
|
fallback: 'font-medium text-muted-foreground',
|
|
17
17
|
},
|
|
18
18
|
variants: {
|
|
@@ -111,7 +111,8 @@ function BottomSheetContent({
|
|
|
111
111
|
children,
|
|
112
112
|
...props
|
|
113
113
|
}: BottomSheetContentProps) {
|
|
114
|
-
const
|
|
114
|
+
const context = useBottomSheet('BottomSheet.Content');
|
|
115
|
+
const { open, setOpen } = context;
|
|
115
116
|
const { height: screenHeight } = useWindowDimensions();
|
|
116
117
|
const insets = useSafeAreaInsets();
|
|
117
118
|
const translateY = useSharedValue(0);
|
|
@@ -145,7 +146,10 @@ function BottomSheetContent({
|
|
|
145
146
|
|
|
146
147
|
return (
|
|
147
148
|
<Portal>
|
|
148
|
-
|
|
149
|
+
{/* Portal content mounts under PortalHost, outside this provider's
|
|
150
|
+
subtree — re-provide the context so nested consumers keep working. */}
|
|
151
|
+
<BottomSheetContext.Provider value={context}>
|
|
152
|
+
<View className="absolute inset-0 justify-end">
|
|
149
153
|
<Animated.View
|
|
150
154
|
entering={FadeIn.duration(180)}
|
|
151
155
|
exiting={FadeOut.duration(180)}
|
|
@@ -175,7 +179,8 @@ function BottomSheetContent({
|
|
|
175
179
|
{children}
|
|
176
180
|
</Animated.View>
|
|
177
181
|
</GestureDetector>
|
|
178
|
-
|
|
182
|
+
</View>
|
|
183
|
+
</BottomSheetContext.Provider>
|
|
179
184
|
</Portal>
|
|
180
185
|
);
|
|
181
186
|
}
|
|
@@ -10,14 +10,14 @@ export interface CardProps extends ViewProps {
|
|
|
10
10
|
const CardRoot = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
11
11
|
<View
|
|
12
12
|
ref={ref}
|
|
13
|
-
className={cn('rounded-
|
|
13
|
+
className={cn('rounded-2xl border border-border bg-card shadow-sm', className)}
|
|
14
14
|
{...props}
|
|
15
15
|
/>
|
|
16
16
|
));
|
|
17
17
|
CardRoot.displayName = 'Card';
|
|
18
18
|
|
|
19
19
|
const CardHeader = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
20
|
-
<View ref={ref} className={cn('gap-1.5 p-
|
|
20
|
+
<View ref={ref} className={cn('gap-1.5 p-6', className)} {...props} />
|
|
21
21
|
));
|
|
22
22
|
CardHeader.displayName = 'Card.Header';
|
|
23
23
|
|
|
@@ -27,7 +27,7 @@ const CardTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
|
27
27
|
ref={ref}
|
|
28
28
|
size="lg"
|
|
29
29
|
weight="semibold"
|
|
30
|
-
className={cn('text-card-foreground', className)}
|
|
30
|
+
className={cn('leading-none text-card-foreground', className)}
|
|
31
31
|
{...props}
|
|
32
32
|
/>
|
|
33
33
|
)
|
|
@@ -42,14 +42,14 @@ const CardDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
|
42
42
|
CardDescription.displayName = 'Card.Description';
|
|
43
43
|
|
|
44
44
|
const CardContent = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
45
|
-
<View ref={ref} className={cn('p-
|
|
45
|
+
<View ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
|
46
46
|
));
|
|
47
47
|
CardContent.displayName = 'Card.Content';
|
|
48
48
|
|
|
49
49
|
const CardFooter = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
|
|
50
50
|
<View
|
|
51
51
|
ref={ref}
|
|
52
|
-
className={cn('flex-row items-center gap-2 p-
|
|
52
|
+
className={cn('flex-row items-center gap-2 p-6 pt-0', className)}
|
|
53
53
|
{...props}
|
|
54
54
|
/>
|
|
55
55
|
));
|
|
@@ -38,7 +38,7 @@ export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
|
38
38
|
({ className, checked, onCheckedChange, disabled, label }, ref) => {
|
|
39
39
|
const progress = useSharedValue(checked ? 1 : 0);
|
|
40
40
|
const slots = checkboxVariants({ disabled: !!disabled });
|
|
41
|
-
const checkColor = useCSSVariable('--primary-foreground');
|
|
41
|
+
const checkColor = useCSSVariable('--color-primary-foreground');
|
|
42
42
|
|
|
43
43
|
useEffect(() => {
|
|
44
44
|
progress.value = checked
|
|
@@ -91,13 +91,17 @@ function DialogContent({
|
|
|
91
91
|
children,
|
|
92
92
|
...props
|
|
93
93
|
}: DialogContentProps) {
|
|
94
|
-
const
|
|
94
|
+
const context = useDialog('Dialog.Content');
|
|
95
|
+
const { open, setOpen } = context;
|
|
95
96
|
|
|
96
97
|
if (!open) return null;
|
|
97
98
|
|
|
98
99
|
return (
|
|
99
100
|
<Portal>
|
|
100
|
-
|
|
101
|
+
{/* Portal content mounts under PortalHost, outside this provider's
|
|
102
|
+
subtree — re-provide the context so Dialog.Close etc. keep working. */}
|
|
103
|
+
<DialogContext.Provider value={context}>
|
|
104
|
+
<Animated.View
|
|
101
105
|
entering={FadeIn.duration(150)}
|
|
102
106
|
exiting={FadeOut.duration(150)}
|
|
103
107
|
className="absolute inset-0 items-center justify-center p-6"
|
|
@@ -116,10 +120,11 @@ function DialogContent({
|
|
|
116
120
|
className
|
|
117
121
|
)}
|
|
118
122
|
{...props}
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
>
|
|
124
|
+
{children}
|
|
125
|
+
</Animated.View>
|
|
121
126
|
</Animated.View>
|
|
122
|
-
</
|
|
127
|
+
</DialogContext.Provider>
|
|
123
128
|
</Portal>
|
|
124
129
|
);
|
|
125
130
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { View, type ViewProps } from 'react-native';
|
|
3
|
+
import { Text, type TextProps } from '../../primitives/text';
|
|
4
|
+
import { cn } from '../../utils/cn';
|
|
5
|
+
|
|
6
|
+
export interface FrameProps extends ViewProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Grouping container (Coss UI's CardFrame): a tinted, rounded surface that
|
|
12
|
+
* holds a header, one or more inset panels, and a footer. Use it to group
|
|
13
|
+
* related settings or list sections under a single title.
|
|
14
|
+
*/
|
|
15
|
+
const FrameRoot = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
|
|
16
|
+
<View
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn('gap-2 rounded-2xl border border-border bg-surface py-2', className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
));
|
|
22
|
+
FrameRoot.displayName = 'Frame';
|
|
23
|
+
|
|
24
|
+
const FrameHeader = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
|
|
25
|
+
<View ref={ref} className={cn('gap-1 px-5 py-2', className)} {...props} />
|
|
26
|
+
));
|
|
27
|
+
FrameHeader.displayName = 'Frame.Header';
|
|
28
|
+
|
|
29
|
+
const FrameTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
30
|
+
({ className, ...props }, ref) => (
|
|
31
|
+
<Text
|
|
32
|
+
ref={ref}
|
|
33
|
+
size="base"
|
|
34
|
+
weight="semibold"
|
|
35
|
+
className={cn('text-foreground', className)}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
FrameTitle.displayName = 'Frame.Title';
|
|
41
|
+
|
|
42
|
+
const FrameDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
43
|
+
({ className, ...props }, ref) => (
|
|
44
|
+
<Text ref={ref} size="sm" muted className={className} {...props} />
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
FrameDescription.displayName = 'Frame.Description';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* An inset card surface inside a Frame — the raised panel that holds the
|
|
51
|
+
* frame's actual content, sitting slightly in from the frame's edges.
|
|
52
|
+
*/
|
|
53
|
+
const FramePanel = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
|
|
54
|
+
<View
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn(
|
|
57
|
+
'mx-1.5 overflow-hidden rounded-xl border border-border bg-card',
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
));
|
|
63
|
+
FramePanel.displayName = 'Frame.Panel';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A row inside a Frame.Panel. Pass `divided` on every row after the first to
|
|
67
|
+
* draw a hairline separator above it (React Native has no :first-child).
|
|
68
|
+
*/
|
|
69
|
+
export interface FrameRowProps extends FrameProps {
|
|
70
|
+
divided?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const FrameRow = forwardRef<View, FrameRowProps>(
|
|
74
|
+
({ className, divided, ...props }, ref) => (
|
|
75
|
+
<View
|
|
76
|
+
ref={ref}
|
|
77
|
+
className={cn(
|
|
78
|
+
'flex-row items-center gap-3 px-4 py-3.5',
|
|
79
|
+
divided && 'border-t border-border',
|
|
80
|
+
className
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
FrameRow.displayName = 'Frame.Row';
|
|
87
|
+
|
|
88
|
+
const FrameFooter = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
|
|
89
|
+
<View
|
|
90
|
+
ref={ref}
|
|
91
|
+
className={cn('flex-row items-center gap-2 px-5 py-2', className)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
));
|
|
95
|
+
FrameFooter.displayName = 'Frame.Footer';
|
|
96
|
+
|
|
97
|
+
export const Frame = Object.assign(FrameRoot, {
|
|
98
|
+
Header: FrameHeader,
|
|
99
|
+
Title: FrameTitle,
|
|
100
|
+
Description: FrameDescription,
|
|
101
|
+
Panel: FramePanel,
|
|
102
|
+
Row: FrameRow,
|
|
103
|
+
Footer: FrameFooter,
|
|
104
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createContext,
|
|
4
|
+
isValidElement,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { Pressable, View } from 'react-native';
|
|
12
|
+
import Animated, {
|
|
13
|
+
FadeIn,
|
|
14
|
+
FadeOut,
|
|
15
|
+
useAnimatedStyle,
|
|
16
|
+
useSharedValue,
|
|
17
|
+
withTiming,
|
|
18
|
+
} from 'react-native-reanimated';
|
|
19
|
+
import { tv } from 'tailwind-variants';
|
|
20
|
+
import { useCSSVariable } from 'uniwind';
|
|
21
|
+
import { CheckIcon, ChevronDownIcon } from '../../icons';
|
|
22
|
+
import { Text } from '../../primitives/text';
|
|
23
|
+
|
|
24
|
+
const inlineSelectVariants = tv({
|
|
25
|
+
slots: {
|
|
26
|
+
trigger:
|
|
27
|
+
'w-full flex-row items-center justify-between gap-3 rounded-2xl border border-input bg-background px-4 py-3.5',
|
|
28
|
+
triggerLabel: 'flex-1 text-base font-medium text-foreground',
|
|
29
|
+
placeholder: 'flex-1 text-base text-muted-foreground',
|
|
30
|
+
list: 'mt-2 overflow-hidden rounded-2xl border border-border bg-popover p-2 shadow-sm',
|
|
31
|
+
item: 'flex-row items-center gap-2 rounded-xl px-3 py-3',
|
|
32
|
+
itemLabel: 'flex-1 text-base font-medium text-foreground',
|
|
33
|
+
itemIndicator: 'h-5 w-5 items-center justify-center',
|
|
34
|
+
},
|
|
35
|
+
variants: {
|
|
36
|
+
selected: {
|
|
37
|
+
true: { item: 'bg-accent' },
|
|
38
|
+
},
|
|
39
|
+
disabled: {
|
|
40
|
+
true: { trigger: 'opacity-[0.64]' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
interface InlineSelectContextValue {
|
|
46
|
+
value: string | undefined;
|
|
47
|
+
onSelect: (value: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const InlineSelectContext = createContext<InlineSelectContextValue | null>(null);
|
|
51
|
+
|
|
52
|
+
export interface InlineSelectItemProps {
|
|
53
|
+
value: string;
|
|
54
|
+
label: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Option row. Rendered inline beneath the trigger — no overlay involved. */
|
|
58
|
+
function InlineSelectItem({ value, label }: InlineSelectItemProps) {
|
|
59
|
+
const context = useContext(InlineSelectContext);
|
|
60
|
+
if (!context) {
|
|
61
|
+
throw new Error('InlineSelect.Item must be used within an <InlineSelect>');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const selected = context.value === value;
|
|
65
|
+
const { item, itemLabel, itemIndicator } = inlineSelectVariants({ selected });
|
|
66
|
+
const checkColor = useCSSVariable('--color-muted-foreground');
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Pressable
|
|
70
|
+
accessibilityRole="menuitem"
|
|
71
|
+
accessibilityState={{ selected }}
|
|
72
|
+
onPress={() => context.onSelect(value)}
|
|
73
|
+
className={item()}
|
|
74
|
+
>
|
|
75
|
+
<Text className={itemLabel()}>{label}</Text>
|
|
76
|
+
<View className={itemIndicator()}>
|
|
77
|
+
{selected ? (
|
|
78
|
+
<CheckIcon
|
|
79
|
+
size={16}
|
|
80
|
+
color={typeof checkColor === 'string' ? checkColor : '#737373'}
|
|
81
|
+
/>
|
|
82
|
+
) : null}
|
|
83
|
+
</View>
|
|
84
|
+
</Pressable>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface InlineSelectProps {
|
|
89
|
+
className?: string;
|
|
90
|
+
value?: string;
|
|
91
|
+
onValueChange: (value: string) => void;
|
|
92
|
+
placeholder?: string;
|
|
93
|
+
disabled?: boolean;
|
|
94
|
+
children: ReactNode;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* A select that expands its options inline instead of opening a bottom sheet.
|
|
99
|
+
* Useful inside forms and scroll views where an overlay would be disruptive.
|
|
100
|
+
*/
|
|
101
|
+
function InlineSelectRoot({
|
|
102
|
+
className,
|
|
103
|
+
value,
|
|
104
|
+
onValueChange,
|
|
105
|
+
placeholder = 'Select an option',
|
|
106
|
+
disabled,
|
|
107
|
+
children,
|
|
108
|
+
}: InlineSelectProps) {
|
|
109
|
+
const [open, setOpen] = useState(false);
|
|
110
|
+
const chevron = useSharedValue(0);
|
|
111
|
+
|
|
112
|
+
const selectedLabel = useMemo(() => {
|
|
113
|
+
let label: string | undefined;
|
|
114
|
+
Children.forEach(children, (child) => {
|
|
115
|
+
if (isValidElement<InlineSelectItemProps>(child) && child.props.value === value) {
|
|
116
|
+
label = child.props.label;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
return label;
|
|
120
|
+
}, [children, value]);
|
|
121
|
+
|
|
122
|
+
const toggle = useCallback(() => {
|
|
123
|
+
setOpen((current) => {
|
|
124
|
+
chevron.value = withTiming(current ? 0 : 1, { duration: 160 });
|
|
125
|
+
return !current;
|
|
126
|
+
});
|
|
127
|
+
}, [chevron]);
|
|
128
|
+
|
|
129
|
+
const context = useMemo<InlineSelectContextValue>(
|
|
130
|
+
() => ({
|
|
131
|
+
value,
|
|
132
|
+
onSelect: (next) => {
|
|
133
|
+
onValueChange(next);
|
|
134
|
+
chevron.value = withTiming(0, { duration: 160 });
|
|
135
|
+
setOpen(false);
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
[value, onValueChange, chevron]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const slots = inlineSelectVariants({ disabled: !!disabled });
|
|
142
|
+
const chevronColor = useCSSVariable('--color-muted-foreground');
|
|
143
|
+
|
|
144
|
+
const chevronStyle = useAnimatedStyle(() => ({
|
|
145
|
+
transform: [{ rotate: `${chevron.value * 180}deg` }],
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<InlineSelectContext.Provider value={context}>
|
|
150
|
+
<View className={className}>
|
|
151
|
+
<Pressable
|
|
152
|
+
accessibilityRole="button"
|
|
153
|
+
accessibilityState={{ disabled: !!disabled, expanded: open }}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
onPress={toggle}
|
|
156
|
+
className={slots.trigger()}
|
|
157
|
+
>
|
|
158
|
+
{selectedLabel ? (
|
|
159
|
+
<Text className={slots.triggerLabel()}>{selectedLabel}</Text>
|
|
160
|
+
) : (
|
|
161
|
+
<Text className={slots.placeholder()}>{placeholder}</Text>
|
|
162
|
+
)}
|
|
163
|
+
<Animated.View style={chevronStyle}>
|
|
164
|
+
<ChevronDownIcon
|
|
165
|
+
color={typeof chevronColor === 'string' ? chevronColor : '#737373'}
|
|
166
|
+
/>
|
|
167
|
+
</Animated.View>
|
|
168
|
+
</Pressable>
|
|
169
|
+
{open ? (
|
|
170
|
+
<Animated.View
|
|
171
|
+
entering={FadeIn.duration(140)}
|
|
172
|
+
exiting={FadeOut.duration(120)}
|
|
173
|
+
className={slots.list()}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</Animated.View>
|
|
177
|
+
) : null}
|
|
178
|
+
</View>
|
|
179
|
+
</InlineSelectContext.Provider>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const InlineSelect = Object.assign(InlineSelectRoot, {
|
|
184
|
+
Item: InlineSelectItem,
|
|
185
|
+
});
|
|
@@ -52,7 +52,7 @@ export const Input = forwardRef<TextInput, InputProps>(
|
|
|
52
52
|
ref
|
|
53
53
|
) => {
|
|
54
54
|
const [focused, setFocused] = useState(false);
|
|
55
|
-
const placeholderColor = useCSSVariable('--muted-foreground');
|
|
55
|
+
const placeholderColor = useCSSVariable('--color-muted-foreground');
|
|
56
56
|
const slots = inputVariants({
|
|
57
57
|
focused,
|
|
58
58
|
invalid: !!errorMessage,
|
|
@@ -18,15 +18,16 @@ import { BottomSheet } from '../bottom-sheet';
|
|
|
18
18
|
const selectVariants = tv({
|
|
19
19
|
slots: {
|
|
20
20
|
trigger:
|
|
21
|
-
'
|
|
22
|
-
triggerLabel: 'text-base text-foreground',
|
|
23
|
-
placeholder: 'text-base text-muted-foreground',
|
|
24
|
-
item: 'flex-row items-center
|
|
25
|
-
itemLabel: 'text-base text-foreground',
|
|
21
|
+
'w-full flex-row items-center justify-between gap-3 rounded-2xl border border-input bg-background px-4 py-3.5',
|
|
22
|
+
triggerLabel: 'flex-1 text-base font-medium text-foreground',
|
|
23
|
+
placeholder: 'flex-1 text-base text-muted-foreground',
|
|
24
|
+
item: 'flex-row items-center gap-2 rounded-xl px-3 py-3',
|
|
25
|
+
itemLabel: 'flex-1 text-base font-medium text-foreground',
|
|
26
|
+
itemIndicator: 'h-5 w-5 items-center justify-center',
|
|
26
27
|
},
|
|
27
28
|
variants: {
|
|
28
29
|
selected: {
|
|
29
|
-
true: { item: 'bg-accent'
|
|
30
|
+
true: { item: 'bg-accent' },
|
|
30
31
|
},
|
|
31
32
|
disabled: {
|
|
32
33
|
true: { trigger: 'opacity-50' },
|
|
@@ -54,8 +55,8 @@ function SelectItem({ value, label }: SelectItemProps) {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
const selected = context.value === value;
|
|
57
|
-
const { item, itemLabel } = selectVariants({ selected });
|
|
58
|
-
const checkColor = useCSSVariable('--muted-foreground');
|
|
58
|
+
const { item, itemLabel, itemIndicator } = selectVariants({ selected });
|
|
59
|
+
const checkColor = useCSSVariable('--color-muted-foreground');
|
|
59
60
|
|
|
60
61
|
return (
|
|
61
62
|
<Pressable
|
|
@@ -65,12 +66,14 @@ function SelectItem({ value, label }: SelectItemProps) {
|
|
|
65
66
|
className={item()}
|
|
66
67
|
>
|
|
67
68
|
<Text className={itemLabel()}>{label}</Text>
|
|
68
|
-
{
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
<View className={itemIndicator()}>
|
|
70
|
+
{selected ? (
|
|
71
|
+
<CheckIcon
|
|
72
|
+
size={16}
|
|
73
|
+
color={typeof checkColor === 'string' ? checkColor : '#737373'}
|
|
74
|
+
/>
|
|
75
|
+
) : null}
|
|
76
|
+
</View>
|
|
74
77
|
</Pressable>
|
|
75
78
|
);
|
|
76
79
|
}
|
|
@@ -124,7 +127,7 @@ function SelectRoot({
|
|
|
124
127
|
const { trigger, triggerLabel, placeholder: placeholderSlot } = selectVariants({
|
|
125
128
|
disabled: !!disabled,
|
|
126
129
|
});
|
|
127
|
-
const chevronColor = useCSSVariable('--muted-foreground');
|
|
130
|
+
const chevronColor = useCSSVariable('--color-muted-foreground');
|
|
128
131
|
|
|
129
132
|
return (
|
|
130
133
|
<SelectContext.Provider value={context}>
|
|
@@ -146,14 +149,18 @@ function SelectRoot({
|
|
|
146
149
|
</Pressable>
|
|
147
150
|
<BottomSheet open={open} onOpenChange={setOpen}>
|
|
148
151
|
<BottomSheet.Content>
|
|
149
|
-
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
{/* BottomSheet.Content portals its children out of this subtree —
|
|
153
|
+
re-provide the select context so Select.Item keeps working. */}
|
|
154
|
+
<SelectContext.Provider value={context}>
|
|
155
|
+
{title ? (
|
|
156
|
+
<Text size="lg" weight="semibold" className="mb-2 px-3">
|
|
157
|
+
{title}
|
|
158
|
+
</Text>
|
|
159
|
+
) : null}
|
|
160
|
+
<ScrollView bounces={false} className="max-h-96">
|
|
161
|
+
<View className="gap-1 pb-2">{children}</View>
|
|
162
|
+
</ScrollView>
|
|
163
|
+
</SelectContext.Provider>
|
|
157
164
|
</BottomSheet.Content>
|
|
158
165
|
</BottomSheet>
|
|
159
166
|
</SelectContext.Provider>
|
|
@@ -28,7 +28,7 @@ export const Skeleton = memo(function Skeleton({ className }: SkeletonProps) {
|
|
|
28
28
|
return (
|
|
29
29
|
<Animated.View
|
|
30
30
|
style={animatedStyle}
|
|
31
|
-
className={cn('rounded-md bg-
|
|
31
|
+
className={cn('rounded-md bg-skeleton', className)}
|
|
32
32
|
/>
|
|
33
33
|
);
|
|
34
34
|
});
|
|
@@ -10,7 +10,7 @@ import Animated, {
|
|
|
10
10
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
11
11
|
|
|
12
12
|
const spinnerVariants = tv({
|
|
13
|
-
base: 'rounded-full border-2 border-
|
|
13
|
+
base: 'rounded-full border-2 border-muted border-t-primary',
|
|
14
14
|
variants: {
|
|
15
15
|
size: {
|
|
16
16
|
sm: 'h-4 w-4',
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,12 @@ export {
|
|
|
27
27
|
type DialogProps,
|
|
28
28
|
type DialogContentProps,
|
|
29
29
|
} from './components/dialog';
|
|
30
|
+
export { Frame, type FrameProps } from './components/frame';
|
|
31
|
+
export {
|
|
32
|
+
InlineSelect,
|
|
33
|
+
type InlineSelectProps,
|
|
34
|
+
type InlineSelectItemProps,
|
|
35
|
+
} from './components/inline-select';
|
|
30
36
|
export { Select, type SelectProps, type SelectItemProps } from './components/select';
|
|
31
37
|
export {
|
|
32
38
|
Tabs,
|
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { StyleSheet } from 'react-native';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
3
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
4
4
|
import { PortalHost, PortalProvider } from '../primitives/portal';
|
|
5
|
+
import { cn } from '../utils/cn';
|
|
5
6
|
|
|
6
7
|
export interface PanelUIProviderProps {
|
|
7
8
|
children: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Classes for the themed page surface. Defaults to `bg-background`, which
|
|
11
|
+
* follows the active theme. Pass your own to change the page colour.
|
|
12
|
+
*/
|
|
13
|
+
className?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Set to false to render without the themed page background — do this only
|
|
16
|
+
* if you paint the app background yourself.
|
|
17
|
+
*/
|
|
18
|
+
background?: boolean;
|
|
8
19
|
}
|
|
9
20
|
|
|
10
21
|
/**
|
|
11
|
-
* Root provider for PanelUI.
|
|
12
|
-
* the
|
|
22
|
+
* Root provider for PanelUI. Owns three things:
|
|
23
|
+
* the gesture handler root, the themed page background, and the portal host
|
|
24
|
+
* used by overlay components (Dialog, BottomSheet, Select).
|
|
13
25
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
26
|
+
* The background lives here because native wrappers like SafeAreaView do not
|
|
27
|
+
* accept `className` — putting `bg-background` on one silently does nothing,
|
|
28
|
+
* leaving the page unthemed while its children follow the theme.
|
|
29
|
+
*
|
|
30
|
+
* Theme switching is handled natively by Uniwind — use the `useTheme()` hook
|
|
31
|
+
* exported from panelui-native.
|
|
16
32
|
*/
|
|
17
|
-
export function PanelUIProvider({
|
|
33
|
+
export function PanelUIProvider({
|
|
34
|
+
children,
|
|
35
|
+
className,
|
|
36
|
+
background = true,
|
|
37
|
+
}: PanelUIProviderProps) {
|
|
18
38
|
return (
|
|
19
39
|
<GestureHandlerRootView style={styles.root}>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
<View className={cn('flex-1', background && 'bg-background', className)}>
|
|
41
|
+
<PortalProvider>
|
|
42
|
+
{children}
|
|
43
|
+
<PortalHost />
|
|
44
|
+
</PortalProvider>
|
|
45
|
+
</View>
|
|
24
46
|
</GestureHandlerRootView>
|
|
25
47
|
);
|
|
26
48
|
}
|