panelui-native 0.9.0 → 0.13.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/README.md +5 -3
- package/lib/module/components/bottom-sheet/index.js +38 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/frame/index.js +128 -15
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/input/index.js +107 -15
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/popover/index.js +22 -11
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/progress/index.js +52 -2
- package/lib/module/components/progress/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +34 -7
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +355 -0
- package/lib/module/components/section-rail/index.js.map +1 -0
- package/lib/module/components/select/index.js +245 -56
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/slider/index.js +355 -0
- package/lib/module/components/slider/index.js.map +1 -0
- package/lib/module/components/switch/index.js +10 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +186 -16
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/toggle-button/index.js +297 -0
- package/lib/module/components/toggle-button/index.js.map +1 -0
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-keyboard-avoidance.js +57 -8
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
- package/lib/module/hooks/use-scroll-sections.js +89 -0
- package/lib/module/hooks/use-scroll-sections.js.map +1 -0
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +2 -0
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/utils/haptics.js +57 -0
- package/lib/module/utils/haptics.js.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +9 -4
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +47 -7
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +113 -2
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts +7 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress/index.d.ts +33 -0
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts +7 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts +115 -0
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +46 -7
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts +203 -0
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts +46 -4
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toggle-button/index.d.ts +244 -0
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +10 -1
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts +31 -0
- package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +16 -1
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +8 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/utils/haptics.d.ts +12 -0
- package/lib/typescript/src/utils/haptics.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/components/bottom-sheet/index.tsx +38 -2
- package/src/components/button/index.tsx +26 -5
- package/src/components/chip/index.tsx +333 -0
- package/src/components/frame/index.tsx +175 -29
- package/src/components/input/index.tsx +122 -16
- package/src/components/popover/index.tsx +49 -24
- package/src/components/progress/index.tsx +81 -1
- package/src/components/radio-group/index.tsx +55 -8
- package/src/components/section-rail/index.tsx +459 -0
- package/src/components/select/index.tsx +265 -52
- package/src/components/slider/index.tsx +371 -0
- package/src/components/switch/index.tsx +8 -1
- package/src/components/tabs/index.tsx +208 -28
- package/src/components/toggle-button/index.tsx +396 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/use-keyboard-avoidance.ts +73 -7
- package/src/hooks/use-scroll-sections.ts +127 -0
- package/src/index.ts +45 -6
- package/src/native/index.ts +13 -1
- package/src/primitives/keyboard-avoider.tsx +9 -1
- package/src/utils/haptics.ts +63 -0
- package/theme.css +6 -2
- package/lib/module/components/inline-select/index.js +0 -284
- package/lib/module/components/inline-select/index.js.map +0 -1
- package/lib/typescript/src/components/inline-select/index.d.ts +0 -48
- package/lib/typescript/src/components/inline-select/index.d.ts.map +0 -1
- package/src/components/inline-select/index.tsx +0 -332
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* InlineSelect — a select that reveals its options next to the trigger instead
|
|
3
|
-
* of taking over the screen with a sheet.
|
|
4
|
-
*
|
|
5
|
-
* Two presentations, because the right one depends on what surrounds it:
|
|
6
|
-
*
|
|
7
|
-
* - Inline (default) expands the list in normal layout flow. Everything below
|
|
8
|
-
* moves down. Fine inside a settings list where that reads as the row
|
|
9
|
-
* growing, wrong anywhere the shift is jarring.
|
|
10
|
-
* - `overlay` floats the list above the page through a portal, anchored to the
|
|
11
|
-
* trigger and flipped above it when there is no room below. Nothing else on
|
|
12
|
-
* the screen moves.
|
|
13
|
-
*/
|
|
14
|
-
import {
|
|
15
|
-
Children,
|
|
16
|
-
createContext,
|
|
17
|
-
isValidElement,
|
|
18
|
-
useCallback,
|
|
19
|
-
useContext,
|
|
20
|
-
useMemo,
|
|
21
|
-
useRef,
|
|
22
|
-
useState,
|
|
23
|
-
type ReactNode,
|
|
24
|
-
} from 'react';
|
|
25
|
-
import {
|
|
26
|
-
Pressable,
|
|
27
|
-
ScrollView,
|
|
28
|
-
useWindowDimensions,
|
|
29
|
-
View,
|
|
30
|
-
type LayoutChangeEvent,
|
|
31
|
-
} from 'react-native';
|
|
32
|
-
import Animated, {
|
|
33
|
-
FadeIn,
|
|
34
|
-
FadeOut,
|
|
35
|
-
useAnimatedStyle,
|
|
36
|
-
useSharedValue,
|
|
37
|
-
withTiming,
|
|
38
|
-
} from 'react-native-reanimated';
|
|
39
|
-
import { tv } from 'tailwind-variants';
|
|
40
|
-
import { useCSSVariable } from 'uniwind';
|
|
41
|
-
import { CheckIcon, ChevronDownIcon } from '../../icons';
|
|
42
|
-
import { Portal } from '../../primitives/portal';
|
|
43
|
-
import { Text } from '../../primitives/text';
|
|
44
|
-
|
|
45
|
-
const inlineSelectVariants = tv({
|
|
46
|
-
slots: {
|
|
47
|
-
trigger:
|
|
48
|
-
'w-full flex-row items-center justify-between gap-3 rounded-2xl border border-input bg-background px-4 py-3.5',
|
|
49
|
-
triggerLabel: 'flex-1 text-base font-medium text-foreground',
|
|
50
|
-
placeholder: 'flex-1 text-base text-muted-foreground',
|
|
51
|
-
list: 'overflow-hidden rounded-2xl border border-border bg-popover p-2 shadow-sm',
|
|
52
|
-
item: 'flex-row items-center gap-2 rounded-xl px-3 py-3',
|
|
53
|
-
itemLabel: 'flex-1 text-base font-medium text-foreground',
|
|
54
|
-
itemIndicator: 'h-5 w-5 items-center justify-center',
|
|
55
|
-
},
|
|
56
|
-
variants: {
|
|
57
|
-
selected: {
|
|
58
|
-
true: { item: 'bg-accent' },
|
|
59
|
-
},
|
|
60
|
-
disabled: {
|
|
61
|
-
true: { trigger: 'opacity-[0.64]' },
|
|
62
|
-
},
|
|
63
|
-
presentation: {
|
|
64
|
-
inline: { list: 'mt-2' },
|
|
65
|
-
overlay: { list: 'shadow-lg' },
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
defaultVariants: {
|
|
69
|
-
presentation: 'inline',
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
interface InlineSelectContextValue {
|
|
74
|
-
value: string | undefined;
|
|
75
|
-
onSelect: (value: string) => void;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const InlineSelectContext = createContext<InlineSelectContextValue | null>(null);
|
|
79
|
-
|
|
80
|
-
export interface InlineSelectItemProps {
|
|
81
|
-
value: string;
|
|
82
|
-
label: string;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Option row. */
|
|
86
|
-
function InlineSelectItem({ value, label }: InlineSelectItemProps) {
|
|
87
|
-
const context = useContext(InlineSelectContext);
|
|
88
|
-
if (!context) {
|
|
89
|
-
throw new Error('InlineSelect.Item must be used within an <InlineSelect>');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const selected = context.value === value;
|
|
93
|
-
const { item, itemLabel, itemIndicator } = inlineSelectVariants({ selected });
|
|
94
|
-
const checkColor = useCSSVariable('--color-muted-foreground');
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<Pressable
|
|
98
|
-
accessibilityRole="menuitem"
|
|
99
|
-
accessibilityState={{ selected }}
|
|
100
|
-
onPress={() => context.onSelect(value)}
|
|
101
|
-
className={item()}
|
|
102
|
-
>
|
|
103
|
-
<Text className={itemLabel()}>{label}</Text>
|
|
104
|
-
<View className={itemIndicator()}>
|
|
105
|
-
{selected ? (
|
|
106
|
-
<CheckIcon
|
|
107
|
-
size={16}
|
|
108
|
-
color={typeof checkColor === 'string' ? checkColor : '#737373'}
|
|
109
|
-
/>
|
|
110
|
-
) : null}
|
|
111
|
-
</View>
|
|
112
|
-
</Pressable>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Trigger frame in window coordinates, measured when the list opens. */
|
|
117
|
-
interface Anchor {
|
|
118
|
-
x: number;
|
|
119
|
-
y: number;
|
|
120
|
-
width: number;
|
|
121
|
-
height: number;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface InlineSelectProps {
|
|
125
|
-
className?: string;
|
|
126
|
-
value?: string;
|
|
127
|
-
onValueChange: (value: string) => void;
|
|
128
|
-
placeholder?: string;
|
|
129
|
-
disabled?: boolean;
|
|
130
|
-
/**
|
|
131
|
-
* Float the list above the page instead of expanding it in layout flow, so
|
|
132
|
-
* opening the select does not push the content below it down.
|
|
133
|
-
*/
|
|
134
|
-
overlay?: boolean;
|
|
135
|
-
/**
|
|
136
|
-
* Width of the floating list. `trigger` matches the trigger, `content` sizes
|
|
137
|
-
* to the longest option, or pass a pixel value. Ignored without `overlay`.
|
|
138
|
-
*/
|
|
139
|
-
overlayWidth?: 'trigger' | 'content' | number;
|
|
140
|
-
/** Gap between the trigger and the floating list. Ignored without `overlay`. */
|
|
141
|
-
overlayOffset?: number;
|
|
142
|
-
/** Called when the list opens or closes. */
|
|
143
|
-
onOpenChange?: (open: boolean) => void;
|
|
144
|
-
children: ReactNode;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function InlineSelectRoot({
|
|
148
|
-
className,
|
|
149
|
-
value,
|
|
150
|
-
onValueChange,
|
|
151
|
-
placeholder = 'Select an option',
|
|
152
|
-
disabled,
|
|
153
|
-
overlay = false,
|
|
154
|
-
overlayWidth = 'trigger',
|
|
155
|
-
overlayOffset = 8,
|
|
156
|
-
onOpenChange,
|
|
157
|
-
children,
|
|
158
|
-
}: InlineSelectProps) {
|
|
159
|
-
const [open, setOpen] = useState(false);
|
|
160
|
-
const [anchor, setAnchor] = useState<Anchor | null>(null);
|
|
161
|
-
const [listHeight, setListHeight] = useState(0);
|
|
162
|
-
const triggerRef = useRef<View>(null);
|
|
163
|
-
const chevron = useSharedValue(0);
|
|
164
|
-
const { height: screenHeight } = useWindowDimensions();
|
|
165
|
-
|
|
166
|
-
const selectedLabel = useMemo(() => {
|
|
167
|
-
let label: string | undefined;
|
|
168
|
-
Children.forEach(children, (child) => {
|
|
169
|
-
if (isValidElement<InlineSelectItemProps>(child) && child.props.value === value) {
|
|
170
|
-
label = child.props.label;
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
return label;
|
|
174
|
-
}, [children, value]);
|
|
175
|
-
|
|
176
|
-
const close = useCallback(() => {
|
|
177
|
-
chevron.value = withTiming(0, { duration: 160 });
|
|
178
|
-
setOpen(false);
|
|
179
|
-
onOpenChange?.(false);
|
|
180
|
-
}, [chevron, onOpenChange]);
|
|
181
|
-
|
|
182
|
-
const toggle = useCallback(() => {
|
|
183
|
-
if (open) {
|
|
184
|
-
close();
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const show = () => {
|
|
189
|
-
chevron.value = withTiming(1, { duration: 160 });
|
|
190
|
-
setOpen(true);
|
|
191
|
-
onOpenChange?.(true);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
if (!overlay) {
|
|
195
|
-
show();
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// The list is positioned in window coordinates, so it has to know where
|
|
200
|
-
// the trigger actually landed — not where layout said it would.
|
|
201
|
-
triggerRef.current?.measureInWindow((x, y, width, height) => {
|
|
202
|
-
setAnchor({ x, y, width, height });
|
|
203
|
-
show();
|
|
204
|
-
});
|
|
205
|
-
}, [chevron, close, onOpenChange, open, overlay]);
|
|
206
|
-
|
|
207
|
-
const context = useMemo<InlineSelectContextValue>(
|
|
208
|
-
() => ({
|
|
209
|
-
value,
|
|
210
|
-
onSelect: (next) => {
|
|
211
|
-
onValueChange(next);
|
|
212
|
-
close();
|
|
213
|
-
},
|
|
214
|
-
}),
|
|
215
|
-
[value, onValueChange, close]
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
const slots = inlineSelectVariants({
|
|
219
|
-
disabled: !!disabled,
|
|
220
|
-
presentation: overlay ? 'overlay' : 'inline',
|
|
221
|
-
});
|
|
222
|
-
const chevronColor = useCSSVariable('--color-muted-foreground');
|
|
223
|
-
|
|
224
|
-
const chevronStyle = useAnimatedStyle(() => ({
|
|
225
|
-
transform: [{ rotate: `${chevron.value * 180}deg` }],
|
|
226
|
-
}));
|
|
227
|
-
|
|
228
|
-
const trigger = (
|
|
229
|
-
<Pressable
|
|
230
|
-
ref={triggerRef}
|
|
231
|
-
accessibilityRole="button"
|
|
232
|
-
accessibilityState={{ disabled: !!disabled, expanded: open }}
|
|
233
|
-
disabled={disabled}
|
|
234
|
-
onPress={toggle}
|
|
235
|
-
className={slots.trigger()}
|
|
236
|
-
>
|
|
237
|
-
{selectedLabel ? (
|
|
238
|
-
<Text className={slots.triggerLabel()}>{selectedLabel}</Text>
|
|
239
|
-
) : (
|
|
240
|
-
<Text className={slots.placeholder()}>{placeholder}</Text>
|
|
241
|
-
)}
|
|
242
|
-
<Animated.View style={chevronStyle}>
|
|
243
|
-
<ChevronDownIcon
|
|
244
|
-
color={typeof chevronColor === 'string' ? chevronColor : '#737373'}
|
|
245
|
-
/>
|
|
246
|
-
</Animated.View>
|
|
247
|
-
</Pressable>
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
if (!overlay) {
|
|
251
|
-
return (
|
|
252
|
-
<InlineSelectContext.Provider value={context}>
|
|
253
|
-
<View className={className}>
|
|
254
|
-
{trigger}
|
|
255
|
-
{open ? (
|
|
256
|
-
<Animated.View
|
|
257
|
-
entering={FadeIn.duration(140)}
|
|
258
|
-
exiting={FadeOut.duration(120)}
|
|
259
|
-
className={slots.list()}
|
|
260
|
-
>
|
|
261
|
-
{children}
|
|
262
|
-
</Animated.View>
|
|
263
|
-
) : null}
|
|
264
|
-
</View>
|
|
265
|
-
</InlineSelectContext.Provider>
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Flip above the trigger when the list would run off the bottom. listHeight
|
|
270
|
-
// is 0 on the first frame, so the list opens downwards and corrects itself
|
|
271
|
-
// once measured — which is invisible inside the 140ms fade.
|
|
272
|
-
const spaceBelow = anchor
|
|
273
|
-
? screenHeight - (anchor.y + anchor.height) - overlayOffset
|
|
274
|
-
: 0;
|
|
275
|
-
const flip = !!anchor && listHeight > 0 && listHeight > spaceBelow;
|
|
276
|
-
|
|
277
|
-
const overlayPosition = anchor
|
|
278
|
-
? {
|
|
279
|
-
position: 'absolute' as const,
|
|
280
|
-
left: anchor.x,
|
|
281
|
-
...(flip
|
|
282
|
-
? { bottom: screenHeight - anchor.y + overlayOffset }
|
|
283
|
-
: { top: anchor.y + anchor.height + overlayOffset }),
|
|
284
|
-
...(overlayWidth === 'trigger'
|
|
285
|
-
? { width: anchor.width }
|
|
286
|
-
: typeof overlayWidth === 'number'
|
|
287
|
-
? { width: overlayWidth }
|
|
288
|
-
: { minWidth: anchor.width }),
|
|
289
|
-
// Never collapse to nothing in a cramped viewport — the list scrolls.
|
|
290
|
-
maxHeight: Math.max((flip ? anchor.y : spaceBelow) - overlayOffset, 160),
|
|
291
|
-
}
|
|
292
|
-
: null;
|
|
293
|
-
|
|
294
|
-
const onListLayout = (event: LayoutChangeEvent) => {
|
|
295
|
-
setListHeight(event.nativeEvent.layout.height);
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
return (
|
|
299
|
-
<InlineSelectContext.Provider value={context}>
|
|
300
|
-
<View className={className}>{trigger}</View>
|
|
301
|
-
|
|
302
|
-
{open && overlayPosition ? (
|
|
303
|
-
<Portal>
|
|
304
|
-
{/* Full-screen catcher so a press anywhere else dismisses the list. */}
|
|
305
|
-
<Pressable
|
|
306
|
-
accessibilityRole="button"
|
|
307
|
-
accessibilityLabel="Close"
|
|
308
|
-
onPress={close}
|
|
309
|
-
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
|
|
310
|
-
/>
|
|
311
|
-
<InlineSelectContext.Provider value={context}>
|
|
312
|
-
<Animated.View
|
|
313
|
-
entering={FadeIn.duration(140)}
|
|
314
|
-
exiting={FadeOut.duration(120)}
|
|
315
|
-
onLayout={onListLayout}
|
|
316
|
-
style={overlayPosition}
|
|
317
|
-
className={slots.list()}
|
|
318
|
-
>
|
|
319
|
-
<ScrollView bounces={false} showsVerticalScrollIndicator={false}>
|
|
320
|
-
{children}
|
|
321
|
-
</ScrollView>
|
|
322
|
-
</Animated.View>
|
|
323
|
-
</InlineSelectContext.Provider>
|
|
324
|
-
</Portal>
|
|
325
|
-
) : null}
|
|
326
|
-
</InlineSelectContext.Provider>
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export const InlineSelect = Object.assign(InlineSelectRoot, {
|
|
331
|
-
Item: InlineSelectItem,
|
|
332
|
-
});
|