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.
Files changed (59) hide show
  1. package/lib/module/components/alert/index.js +8 -18
  2. package/lib/module/components/alert/index.js.map +1 -1
  3. package/lib/module/components/avatar/index.js +1 -1
  4. package/lib/module/components/avatar/index.js.map +1 -1
  5. package/lib/module/components/bottom-sheet/index.js +35 -31
  6. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  7. package/lib/module/components/card/index.js +5 -5
  8. package/lib/module/components/card/index.js.map +1 -1
  9. package/lib/module/components/checkbox/index.js +1 -1
  10. package/lib/module/components/checkbox/index.js.map +1 -1
  11. package/lib/module/components/dialog/index.js +21 -17
  12. package/lib/module/components/dialog/index.js.map +1 -1
  13. package/lib/module/components/frame/index.js +100 -0
  14. package/lib/module/components/frame/index.js.map +1 -0
  15. package/lib/module/components/inline-select/index.js +159 -0
  16. package/lib/module/components/inline-select/index.js.map +1 -0
  17. package/lib/module/components/input/index.js +1 -1
  18. package/lib/module/components/input/index.js.map +1 -1
  19. package/lib/module/components/select/index.js +35 -28
  20. package/lib/module/components/select/index.js.map +1 -1
  21. package/lib/module/components/skeleton/index.js +1 -1
  22. package/lib/module/components/skeleton/index.js.map +1 -1
  23. package/lib/module/components/spinner/index.js +1 -1
  24. package/lib/module/components/spinner/index.js.map +1 -1
  25. package/lib/module/index.js +2 -0
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/providers/panel-ui-provider.js +19 -8
  28. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  29. package/lib/typescript/src/components/alert/index.d.ts +0 -30
  30. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  32. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  33. package/lib/typescript/src/components/frame/index.d.ts +21 -0
  34. package/lib/typescript/src/components/frame/index.d.ts.map +1 -0
  35. package/lib/typescript/src/components/inline-select/index.d.ts +25 -0
  36. package/lib/typescript/src/components/inline-select/index.d.ts.map +1 -0
  37. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/spinner/index.d.ts +2 -2
  39. package/lib/typescript/src/components/spinner/index.d.ts.map +1 -1
  40. package/lib/typescript/src/index.d.ts +2 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -1
  42. package/lib/typescript/src/providers/panel-ui-provider.d.ts +20 -5
  43. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  44. package/package.json +7 -2
  45. package/src/components/alert/index.tsx +8 -18
  46. package/src/components/avatar/index.tsx +1 -1
  47. package/src/components/bottom-sheet/index.tsx +8 -3
  48. package/src/components/card/index.tsx +5 -5
  49. package/src/components/checkbox/index.tsx +1 -1
  50. package/src/components/dialog/index.tsx +10 -5
  51. package/src/components/frame/index.tsx +104 -0
  52. package/src/components/inline-select/index.tsx +185 -0
  53. package/src/components/input/index.tsx +1 -1
  54. package/src/components/select/index.tsx +30 -23
  55. package/src/components/skeleton/index.tsx +1 -1
  56. package/src/components/spinner/index.tsx +1 -1
  57. package/src/index.ts +6 -0
  58. package/src/providers/panel-ui-provider.tsx +32 -10
  59. 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-1 rounded-xl border p-4',
10
- title: 'font-semibold',
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-card',
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/4',
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/4',
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/4',
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/4',
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 { open, setOpen } = useBottomSheet('BottomSheet.Content');
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
- <View className="absolute inset-0 justify-end">
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
- </View>
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-xl border border-border bg-card shadow-sm', className)}
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-4', className)} {...props} />
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-4 pt-0', className)} {...props} />
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-4 pt-0', className)}
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 { open, setOpen } = useDialog('Dialog.Content');
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
- <Animated.View
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
- {children}
123
+ >
124
+ {children}
125
+ </Animated.View>
121
126
  </Animated.View>
122
- </Animated.View>
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
- 'h-11 w-full flex-row items-center justify-between rounded-lg border border-input bg-background px-3',
22
- triggerLabel: 'text-base text-foreground',
23
- placeholder: 'text-base text-muted-foreground',
24
- item: 'flex-row items-center justify-between rounded-lg px-3 py-3.5',
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', itemLabel: 'font-medium' },
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
- {selected ? (
69
- <CheckIcon
70
- size={16}
71
- color={typeof checkColor === 'string' ? checkColor : '#737373'}
72
- />
73
- ) : null}
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
- {title ? (
150
- <Text size="lg" weight="semibold" className="mb-2 px-3">
151
- {title}
152
- </Text>
153
- ) : null}
154
- <ScrollView bounces={false} className="max-h-96">
155
- <View className="gap-1 pb-2">{children}</View>
156
- </ScrollView>
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-foreground/8', className)}
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-foreground/15 border-t-primary',
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. Wraps the app with the gesture handler root and
12
- * the portal host used by overlay components (Dialog, BottomSheet, Select).
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
- * Theme switching is handled natively by Uniwind use `Uniwind.setTheme()`
15
- * or the `useTheme()` hook exported from panelui-native.
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({ children }: PanelUIProviderProps) {
33
+ export function PanelUIProvider({
34
+ children,
35
+ className,
36
+ background = true,
37
+ }: PanelUIProviderProps) {
18
38
  return (
19
39
  <GestureHandlerRootView style={styles.root}>
20
- <PortalProvider>
21
- {children}
22
- <PortalHost />
23
- </PortalProvider>
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
  }