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
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SectionRail — a floating navigator for a long screen.
|
|
3
|
+
*
|
|
4
|
+
* A stack of short bars pinned to one edge, one per section, that expands into
|
|
5
|
+
* a labelled panel when you touch it. Collapsed it is a position indicator you
|
|
6
|
+
* can read at a glance without giving up any content width; expanded it is a
|
|
7
|
+
* list you can jump from.
|
|
8
|
+
*
|
|
9
|
+
* The bars are deliberately unlabelled. A permanent list of section titles
|
|
10
|
+
* down the side of a phone screen is either too small to read or too wide to
|
|
11
|
+
* keep — the bars carry only the two things that survive at that size, which
|
|
12
|
+
* section you are in and roughly how deep it sits.
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <SectionRail value={active} onValueChange={scrollTo}>
|
|
16
|
+
* <SectionRail.Trigger>
|
|
17
|
+
* <SectionRail.Bar value="intro" />
|
|
18
|
+
* <SectionRail.Bar value="setup" level={1} />
|
|
19
|
+
* </SectionRail.Trigger>
|
|
20
|
+
* <SectionRail.Content>
|
|
21
|
+
* <SectionRail.Item value="intro">Introduction</SectionRail.Item>
|
|
22
|
+
* <SectionRail.Item value="setup" level={1}>Setup</SectionRail.Item>
|
|
23
|
+
* </SectionRail.Content>
|
|
24
|
+
* </SectionRail>
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* It floats: the root is absolutely positioned and lets touches through
|
|
28
|
+
* everywhere it is not drawing, so the content underneath still scrolls.
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
createContext,
|
|
32
|
+
useCallback,
|
|
33
|
+
useContext,
|
|
34
|
+
useEffect,
|
|
35
|
+
useMemo,
|
|
36
|
+
useRef,
|
|
37
|
+
useState,
|
|
38
|
+
type ReactNode,
|
|
39
|
+
} from 'react';
|
|
40
|
+
import { Pressable, ScrollView, View, type ViewProps } from 'react-native';
|
|
41
|
+
import Animated, {
|
|
42
|
+
FadeIn,
|
|
43
|
+
FadeOut,
|
|
44
|
+
interpolateColor,
|
|
45
|
+
useAnimatedStyle,
|
|
46
|
+
useSharedValue,
|
|
47
|
+
withSpring,
|
|
48
|
+
withTiming,
|
|
49
|
+
} from 'react-native-reanimated';
|
|
50
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
51
|
+
import { useCSSVariable } from 'uniwind';
|
|
52
|
+
import { Portal } from '../../primitives/portal';
|
|
53
|
+
import { Text } from '../../primitives/text';
|
|
54
|
+
import { cn } from '../../utils/cn';
|
|
55
|
+
import { selectionTick } from '../../utils/haptics';
|
|
56
|
+
|
|
57
|
+
const SPRING = { damping: 20, stiffness: 260, mass: 0.6 } as const;
|
|
58
|
+
/** Bar width at the top level, and how much each nested level takes off it. */
|
|
59
|
+
const BAR_WIDTH = 16;
|
|
60
|
+
const BAR_LEVEL_STEP = 4;
|
|
61
|
+
/** How much wider the active bar gets, so position is readable at a glance. */
|
|
62
|
+
const BAR_ACTIVE_EXTRA = 8;
|
|
63
|
+
/** Indent per level in the expanded panel. */
|
|
64
|
+
const ITEM_INDENT = 12;
|
|
65
|
+
|
|
66
|
+
export type SectionRailPlacement = 'left' | 'right';
|
|
67
|
+
export type SectionRailAlign = 'center' | 'top' | 'bottom';
|
|
68
|
+
|
|
69
|
+
/** Vertical position of the rail, and of the panel that opens from it. */
|
|
70
|
+
const ALIGNMENT: Record<SectionRailAlign, string> = {
|
|
71
|
+
center: 'justify-center',
|
|
72
|
+
top: 'justify-start',
|
|
73
|
+
bottom: 'justify-end',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
interface SectionRailContextValue {
|
|
77
|
+
value: string | undefined;
|
|
78
|
+
onValueChange?: (value: string) => void;
|
|
79
|
+
open: boolean;
|
|
80
|
+
setOpen: (open: boolean) => void;
|
|
81
|
+
close: () => void;
|
|
82
|
+
placement: SectionRailPlacement;
|
|
83
|
+
align: SectionRailAlign;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const SectionRailContext = createContext<SectionRailContextValue | null>(null);
|
|
87
|
+
|
|
88
|
+
function useSectionRail(component: string): SectionRailContextValue {
|
|
89
|
+
const context = useContext(SectionRailContext);
|
|
90
|
+
if (!context) {
|
|
91
|
+
throw new Error(`${component} must be used within a <SectionRail>`);
|
|
92
|
+
}
|
|
93
|
+
return context;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SectionRailProps extends ViewProps {
|
|
97
|
+
className?: string;
|
|
98
|
+
/** Which edge the rail sits against. */
|
|
99
|
+
placement?: SectionRailPlacement;
|
|
100
|
+
/**
|
|
101
|
+
* Where along that edge it sits. `bottom` puts it in a corner, out of the
|
|
102
|
+
* way of the text — the panel then opens upward from the rail rather than
|
|
103
|
+
* centred on the screen.
|
|
104
|
+
*/
|
|
105
|
+
align?: SectionRailAlign;
|
|
106
|
+
/**
|
|
107
|
+
* Tick under the finger on every change of section, however it was made —
|
|
108
|
+
* tapped in the panel, or scrolled past. Needs the optional `expo-haptics`
|
|
109
|
+
* package; without it this does nothing.
|
|
110
|
+
*/
|
|
111
|
+
haptics?: boolean;
|
|
112
|
+
/** Active section id. Controlled — usually driven by a scroll handler. */
|
|
113
|
+
value?: string;
|
|
114
|
+
/** Starting section when uncontrolled. */
|
|
115
|
+
defaultValue?: string;
|
|
116
|
+
/** Fires when a section is chosen from the expanded panel. */
|
|
117
|
+
onValueChange?: (value: string) => void;
|
|
118
|
+
/** Controlled expansion. */
|
|
119
|
+
open?: boolean;
|
|
120
|
+
defaultOpen?: boolean;
|
|
121
|
+
onOpenChange?: (open: boolean) => void;
|
|
122
|
+
/**
|
|
123
|
+
* How long the panel stays up after a choice, so a mis-tap can be corrected
|
|
124
|
+
* without opening it again. Set 0 to close immediately.
|
|
125
|
+
*/
|
|
126
|
+
closeDelay?: number;
|
|
127
|
+
/** Gap between the rail and the edge of the safe area. */
|
|
128
|
+
offset?: number;
|
|
129
|
+
children: ReactNode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function SectionRailRoot({
|
|
133
|
+
className,
|
|
134
|
+
placement = 'right',
|
|
135
|
+
align = 'center',
|
|
136
|
+
haptics = false,
|
|
137
|
+
value: valueProp,
|
|
138
|
+
defaultValue,
|
|
139
|
+
onValueChange,
|
|
140
|
+
open: openProp,
|
|
141
|
+
defaultOpen = false,
|
|
142
|
+
onOpenChange,
|
|
143
|
+
closeDelay = 300,
|
|
144
|
+
offset = 12,
|
|
145
|
+
children,
|
|
146
|
+
...props
|
|
147
|
+
}: SectionRailProps) {
|
|
148
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
149
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
|
150
|
+
const insets = useSafeAreaInsets();
|
|
151
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
152
|
+
|
|
153
|
+
const isControlled = valueProp !== undefined;
|
|
154
|
+
const value = isControlled ? valueProp : internalValue;
|
|
155
|
+
const isOpenControlled = openProp !== undefined;
|
|
156
|
+
const open = isOpenControlled ? openProp : internalOpen;
|
|
157
|
+
|
|
158
|
+
const setOpen = useCallback(
|
|
159
|
+
(next: boolean) => {
|
|
160
|
+
if (timer.current) {
|
|
161
|
+
clearTimeout(timer.current);
|
|
162
|
+
timer.current = null;
|
|
163
|
+
}
|
|
164
|
+
if (!isOpenControlled) setInternalOpen(next);
|
|
165
|
+
onOpenChange?.(next);
|
|
166
|
+
},
|
|
167
|
+
[isOpenControlled, onOpenChange]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
171
|
+
|
|
172
|
+
// A pending close must not fire after the rail has gone.
|
|
173
|
+
useEffect(
|
|
174
|
+
() => () => {
|
|
175
|
+
if (timer.current) clearTimeout(timer.current);
|
|
176
|
+
},
|
|
177
|
+
[]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const handleValueChange = useCallback(
|
|
181
|
+
(next: string) => {
|
|
182
|
+
if (!isControlled) setInternalValue(next);
|
|
183
|
+
onValueChange?.(next);
|
|
184
|
+
|
|
185
|
+
if (closeDelay <= 0) {
|
|
186
|
+
setOpen(false);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (timer.current) clearTimeout(timer.current);
|
|
190
|
+
timer.current = setTimeout(() => setOpen(false), closeDelay);
|
|
191
|
+
},
|
|
192
|
+
[isControlled, onValueChange, closeDelay, setOpen]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
/*
|
|
196
|
+
* Fired from the resolved value rather than from the change handler, so a
|
|
197
|
+
* section arrived at by scrolling ticks as well as one that was tapped —
|
|
198
|
+
* "every change" means every change. The ref skips the first run, since
|
|
199
|
+
* mounting is not a change of section.
|
|
200
|
+
*/
|
|
201
|
+
const ticked = useRef(false);
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (!haptics) return;
|
|
204
|
+
if (!ticked.current) {
|
|
205
|
+
ticked.current = true;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
selectionTick();
|
|
209
|
+
}, [value, haptics]);
|
|
210
|
+
|
|
211
|
+
const context = useMemo(
|
|
212
|
+
() => ({
|
|
213
|
+
value,
|
|
214
|
+
onValueChange: handleValueChange,
|
|
215
|
+
open,
|
|
216
|
+
setOpen,
|
|
217
|
+
close,
|
|
218
|
+
placement,
|
|
219
|
+
align,
|
|
220
|
+
}),
|
|
221
|
+
[value, handleValueChange, open, setOpen, close, placement, align]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<SectionRailContext.Provider value={context}>
|
|
226
|
+
<View
|
|
227
|
+
// `box-none` and not `none`: the rail's own children still take
|
|
228
|
+
// touches, but the empty column around them does not — otherwise a
|
|
229
|
+
// strip down the side of the screen would swallow every scroll.
|
|
230
|
+
pointerEvents="box-none"
|
|
231
|
+
className={cn(
|
|
232
|
+
'absolute',
|
|
233
|
+
ALIGNMENT[align],
|
|
234
|
+
placement === 'right' ? 'right-0' : 'left-0',
|
|
235
|
+
className
|
|
236
|
+
)}
|
|
237
|
+
style={{
|
|
238
|
+
top: insets.top,
|
|
239
|
+
bottom: insets.bottom,
|
|
240
|
+
[placement === 'right' ? 'right' : 'left']:
|
|
241
|
+
(placement === 'right' ? insets.right : insets.left) + offset,
|
|
242
|
+
}}
|
|
243
|
+
{...props}
|
|
244
|
+
>
|
|
245
|
+
{children}
|
|
246
|
+
</View>
|
|
247
|
+
</SectionRailContext.Provider>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface SectionRailTriggerProps extends ViewProps {
|
|
252
|
+
className?: string;
|
|
253
|
+
children: ReactNode;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The collapsed rail. Wraps the bars and opens the panel on press — one target
|
|
258
|
+
* over the whole stack rather than one per bar, because a 3px bar is not
|
|
259
|
+
* something anyone can hit.
|
|
260
|
+
*/
|
|
261
|
+
function SectionRailTrigger({ className, children, ...props }: SectionRailTriggerProps) {
|
|
262
|
+
const { open, setOpen, placement } = useSectionRail('SectionRail.Trigger');
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<Pressable
|
|
266
|
+
accessibilityRole="button"
|
|
267
|
+
accessibilityLabel="Sections"
|
|
268
|
+
accessibilityState={{ expanded: open }}
|
|
269
|
+
onPress={() => setOpen(!open)}
|
|
270
|
+
// Generous padding is the hit target; the bars themselves are hairlines.
|
|
271
|
+
hitSlop={8}
|
|
272
|
+
className={cn(
|
|
273
|
+
'gap-2 py-3',
|
|
274
|
+
placement === 'right' ? 'items-end pl-6 pr-2' : 'items-start pl-2 pr-6',
|
|
275
|
+
className
|
|
276
|
+
)}
|
|
277
|
+
{...props}
|
|
278
|
+
>
|
|
279
|
+
{children}
|
|
280
|
+
</Pressable>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface SectionRailBarProps {
|
|
285
|
+
className?: string;
|
|
286
|
+
/** Section this bar stands for. Matches the root's `value`. */
|
|
287
|
+
value: string;
|
|
288
|
+
/** Nesting depth. Deeper levels draw a shorter bar. */
|
|
289
|
+
level?: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** One section, drawn as a bar. Widens and brightens when it is the active one. */
|
|
293
|
+
function SectionRailBar({ className, value, level = 0 }: SectionRailBarProps) {
|
|
294
|
+
const { value: active } = useSectionRail('SectionRail.Bar');
|
|
295
|
+
const selected = active === value;
|
|
296
|
+
|
|
297
|
+
const restColor = useCSSVariable('--color-muted-foreground');
|
|
298
|
+
const activeColor = useCSSVariable('--color-foreground');
|
|
299
|
+
|
|
300
|
+
const base = Math.max(BAR_WIDTH - level * BAR_LEVEL_STEP, 6);
|
|
301
|
+
const progress = useSharedValue(selected ? 1 : 0);
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
progress.value = withSpring(selected ? 1 : 0, SPRING);
|
|
305
|
+
}, [selected, progress]);
|
|
306
|
+
|
|
307
|
+
const idle = typeof restColor === 'string' ? restColor : '#818181';
|
|
308
|
+
const on = typeof activeColor === 'string' ? activeColor : '#f5f5f5';
|
|
309
|
+
|
|
310
|
+
const style = useAnimatedStyle(() => ({
|
|
311
|
+
width: base + progress.value * BAR_ACTIVE_EXTRA,
|
|
312
|
+
// The inactive bars are dim on purpose — the rail is a position
|
|
313
|
+
// indicator, so only one of them is meant to be read.
|
|
314
|
+
opacity: 0.4 + progress.value * 0.6,
|
|
315
|
+
backgroundColor: interpolateColor(progress.value, [0, 1], [idle, on]),
|
|
316
|
+
}));
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<Animated.View
|
|
320
|
+
accessibilityElementsHidden
|
|
321
|
+
importantForAccessibility="no-hide-descendants"
|
|
322
|
+
style={style}
|
|
323
|
+
className={cn('h-0.5 rounded-full', className)}
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface SectionRailContentProps extends ViewProps {
|
|
329
|
+
className?: string;
|
|
330
|
+
children: ReactNode;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The expanded panel. Mounted through a portal so it floats over everything,
|
|
335
|
+
* and unmounted after it fades out rather than left behind hidden.
|
|
336
|
+
*/
|
|
337
|
+
function SectionRailContent({ className, children, ...props }: SectionRailContentProps) {
|
|
338
|
+
const context = useSectionRail('SectionRail.Content');
|
|
339
|
+
const { open, close, placement, align } = context;
|
|
340
|
+
const insets = useSafeAreaInsets();
|
|
341
|
+
|
|
342
|
+
if (!open) return null;
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<Portal>
|
|
346
|
+
{/* A press anywhere else puts it away. */}
|
|
347
|
+
<Pressable
|
|
348
|
+
accessibilityRole="button"
|
|
349
|
+
accessibilityLabel="Close"
|
|
350
|
+
onPress={close}
|
|
351
|
+
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
|
|
352
|
+
/>
|
|
353
|
+
<SectionRailContext.Provider value={context}>
|
|
354
|
+
<Animated.View
|
|
355
|
+
entering={FadeIn.duration(140)}
|
|
356
|
+
exiting={FadeOut.duration(120)}
|
|
357
|
+
// Aligned the same way the rail is, so a panel opened from a corner
|
|
358
|
+
// unfolds out of that corner instead of appearing across the middle
|
|
359
|
+
// of the screen away from the thing that was pressed.
|
|
360
|
+
className={cn('absolute', ALIGNMENT[align])}
|
|
361
|
+
style={{
|
|
362
|
+
top: insets.top,
|
|
363
|
+
bottom: insets.bottom,
|
|
364
|
+
[placement === 'right' ? 'right' : 'left']:
|
|
365
|
+
(placement === 'right' ? insets.right : insets.left) + 12,
|
|
366
|
+
}}
|
|
367
|
+
pointerEvents="box-none"
|
|
368
|
+
>
|
|
369
|
+
<SectionRailPanel className={className} {...props}>
|
|
370
|
+
{children}
|
|
371
|
+
</SectionRailPanel>
|
|
372
|
+
</Animated.View>
|
|
373
|
+
</SectionRailContext.Provider>
|
|
374
|
+
</Portal>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Split out from Content so the slide-in animated style is not on the same
|
|
380
|
+
* view as the entering/exiting fade — Reanimated will let a layout animation
|
|
381
|
+
* overwrite an animated style that touches the same property.
|
|
382
|
+
*/
|
|
383
|
+
function SectionRailPanel({ className, children, ...props }: SectionRailContentProps) {
|
|
384
|
+
const { placement } = useSectionRail('SectionRail.Content');
|
|
385
|
+
const slide = useSharedValue(0);
|
|
386
|
+
|
|
387
|
+
useEffect(() => {
|
|
388
|
+
slide.value = withTiming(1, { duration: 180 });
|
|
389
|
+
}, [slide]);
|
|
390
|
+
|
|
391
|
+
// Arrives from the edge it is anchored to, so it reads as the rail
|
|
392
|
+
// unfolding rather than as a panel appearing over it.
|
|
393
|
+
const style = useAnimatedStyle(() => ({
|
|
394
|
+
transform: [
|
|
395
|
+
{ translateX: (1 - slide.value) * (placement === 'right' ? 16 : -16) },
|
|
396
|
+
],
|
|
397
|
+
}));
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<Animated.View
|
|
401
|
+
style={style}
|
|
402
|
+
accessibilityRole="menu"
|
|
403
|
+
className={cn(
|
|
404
|
+
'max-w-[60%] gap-0.5 rounded-2xl border border-border bg-popover p-2 shadow-lg',
|
|
405
|
+
className
|
|
406
|
+
)}
|
|
407
|
+
{...props}
|
|
408
|
+
>
|
|
409
|
+
<ScrollView bounces={false} showsVerticalScrollIndicator={false}>
|
|
410
|
+
{children}
|
|
411
|
+
</ScrollView>
|
|
412
|
+
</Animated.View>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface SectionRailItemProps {
|
|
417
|
+
className?: string;
|
|
418
|
+
/** Section this row jumps to. Matches the root's `value`. */
|
|
419
|
+
value: string;
|
|
420
|
+
/** Nesting depth. Indents the row to match its bar. */
|
|
421
|
+
level?: number;
|
|
422
|
+
children: ReactNode;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** A labelled row in the expanded panel. */
|
|
426
|
+
function SectionRailItem({ className, value, level = 0, children }: SectionRailItemProps) {
|
|
427
|
+
const { value: active, onValueChange } = useSectionRail('SectionRail.Item');
|
|
428
|
+
const selected = active === value;
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<Pressable
|
|
432
|
+
accessibilityRole="menuitem"
|
|
433
|
+
accessibilityState={{ selected }}
|
|
434
|
+
onPress={() => onValueChange?.(value)}
|
|
435
|
+
style={{ paddingLeft: 12 + level * ITEM_INDENT }}
|
|
436
|
+
className={cn(
|
|
437
|
+
'rounded-lg py-2 pr-3 active:bg-accent',
|
|
438
|
+
selected && 'bg-accent',
|
|
439
|
+
className
|
|
440
|
+
)}
|
|
441
|
+
>
|
|
442
|
+
<Text
|
|
443
|
+
size="sm"
|
|
444
|
+
weight={selected ? 'medium' : 'normal'}
|
|
445
|
+
className={selected ? 'text-foreground' : 'text-muted-foreground'}
|
|
446
|
+
numberOfLines={1}
|
|
447
|
+
>
|
|
448
|
+
{children}
|
|
449
|
+
</Text>
|
|
450
|
+
</Pressable>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export const SectionRail = Object.assign(SectionRailRoot, {
|
|
455
|
+
Trigger: SectionRailTrigger,
|
|
456
|
+
Bar: SectionRailBar,
|
|
457
|
+
Content: SectionRailContent,
|
|
458
|
+
Item: SectionRailItem,
|
|
459
|
+
});
|