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.
Files changed (106) hide show
  1. package/README.md +5 -3
  2. package/lib/module/components/bottom-sheet/index.js +38 -2
  3. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  4. package/lib/module/components/button/index.js +19 -1
  5. package/lib/module/components/button/index.js.map +1 -1
  6. package/lib/module/components/frame/index.js +128 -15
  7. package/lib/module/components/frame/index.js.map +1 -1
  8. package/lib/module/components/input/index.js +107 -15
  9. package/lib/module/components/input/index.js.map +1 -1
  10. package/lib/module/components/popover/index.js +22 -11
  11. package/lib/module/components/popover/index.js.map +1 -1
  12. package/lib/module/components/progress/index.js +52 -2
  13. package/lib/module/components/progress/index.js.map +1 -1
  14. package/lib/module/components/radio-group/index.js +34 -7
  15. package/lib/module/components/radio-group/index.js.map +1 -1
  16. package/lib/module/components/section-rail/index.js +355 -0
  17. package/lib/module/components/section-rail/index.js.map +1 -0
  18. package/lib/module/components/select/index.js +245 -56
  19. package/lib/module/components/select/index.js.map +1 -1
  20. package/lib/module/components/slider/index.js +355 -0
  21. package/lib/module/components/slider/index.js.map +1 -0
  22. package/lib/module/components/switch/index.js +10 -1
  23. package/lib/module/components/switch/index.js.map +1 -1
  24. package/lib/module/components/tabs/index.js +186 -16
  25. package/lib/module/components/tabs/index.js.map +1 -1
  26. package/lib/module/components/toggle-button/index.js +297 -0
  27. package/lib/module/components/toggle-button/index.js.map +1 -0
  28. package/lib/module/hooks/index.js +1 -0
  29. package/lib/module/hooks/index.js.map +1 -1
  30. package/lib/module/hooks/use-keyboard-avoidance.js +57 -8
  31. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  32. package/lib/module/hooks/use-scroll-sections.js +89 -0
  33. package/lib/module/hooks/use-scroll-sections.js.map +1 -0
  34. package/lib/module/index.js +4 -1
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/native/index.js.map +1 -1
  37. package/lib/module/primitives/keyboard-avoider.js +2 -0
  38. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  39. package/lib/module/utils/haptics.js +57 -0
  40. package/lib/module/utils/haptics.js.map +1 -0
  41. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  42. package/lib/typescript/src/components/button/index.d.ts +9 -4
  43. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  44. package/lib/typescript/src/components/frame/index.d.ts +47 -7
  45. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  46. package/lib/typescript/src/components/input/index.d.ts +113 -2
  47. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  48. package/lib/typescript/src/components/popover/index.d.ts +7 -1
  49. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/progress/index.d.ts +33 -0
  51. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/radio-group/index.d.ts +7 -0
  53. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/section-rail/index.d.ts +115 -0
  55. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -0
  56. package/lib/typescript/src/components/select/index.d.ts +46 -7
  57. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/slider/index.d.ts +203 -0
  59. package/lib/typescript/src/components/slider/index.d.ts.map +1 -0
  60. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  61. package/lib/typescript/src/components/tabs/index.d.ts +46 -4
  62. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  63. package/lib/typescript/src/components/toggle-button/index.d.ts +244 -0
  64. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -0
  65. package/lib/typescript/src/hooks/index.d.ts +1 -0
  66. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  67. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +10 -1
  68. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  69. package/lib/typescript/src/hooks/use-scroll-sections.d.ts +31 -0
  70. package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -0
  71. package/lib/typescript/src/index.d.ts +6 -3
  72. package/lib/typescript/src/index.d.ts.map +1 -1
  73. package/lib/typescript/src/native/index.d.ts +16 -1
  74. package/lib/typescript/src/native/index.d.ts.map +1 -1
  75. package/lib/typescript/src/primitives/keyboard-avoider.d.ts +8 -1
  76. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  77. package/lib/typescript/src/utils/haptics.d.ts +12 -0
  78. package/lib/typescript/src/utils/haptics.d.ts.map +1 -0
  79. package/package.json +5 -1
  80. package/src/components/bottom-sheet/index.tsx +38 -2
  81. package/src/components/button/index.tsx +26 -5
  82. package/src/components/chip/index.tsx +333 -0
  83. package/src/components/frame/index.tsx +175 -29
  84. package/src/components/input/index.tsx +122 -16
  85. package/src/components/popover/index.tsx +49 -24
  86. package/src/components/progress/index.tsx +81 -1
  87. package/src/components/radio-group/index.tsx +55 -8
  88. package/src/components/section-rail/index.tsx +459 -0
  89. package/src/components/select/index.tsx +265 -52
  90. package/src/components/slider/index.tsx +371 -0
  91. package/src/components/switch/index.tsx +8 -1
  92. package/src/components/tabs/index.tsx +208 -28
  93. package/src/components/toggle-button/index.tsx +396 -0
  94. package/src/hooks/index.ts +5 -0
  95. package/src/hooks/use-keyboard-avoidance.ts +73 -7
  96. package/src/hooks/use-scroll-sections.ts +127 -0
  97. package/src/index.ts +45 -6
  98. package/src/native/index.ts +13 -1
  99. package/src/primitives/keyboard-avoider.tsx +9 -1
  100. package/src/utils/haptics.ts +63 -0
  101. package/theme.css +6 -2
  102. package/lib/module/components/inline-select/index.js +0 -284
  103. package/lib/module/components/inline-select/index.js.map +0 -1
  104. package/lib/typescript/src/components/inline-select/index.d.ts +0 -48
  105. package/lib/typescript/src/components/inline-select/index.d.ts.map +0 -1
  106. 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
- });