panelui-native 0.7.0 → 0.12.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 (161) hide show
  1. package/README.md +5 -3
  2. package/lib/module/components/attachment/index.js +219 -0
  3. package/lib/module/components/attachment/index.js.map +1 -0
  4. package/lib/module/components/bottom-sheet/index.js +53 -3
  5. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  6. package/lib/module/components/button/index.js +19 -1
  7. package/lib/module/components/button/index.js.map +1 -1
  8. package/lib/module/components/dialog/index.js +6 -2
  9. package/lib/module/components/dialog/index.js.map +1 -1
  10. package/lib/module/components/empty-state/index.js +94 -17
  11. package/lib/module/components/empty-state/index.js.map +1 -1
  12. package/lib/module/components/frame/index.js +128 -15
  13. package/lib/module/components/frame/index.js.map +1 -1
  14. package/lib/module/components/input/index.js +107 -15
  15. package/lib/module/components/input/index.js.map +1 -1
  16. package/lib/module/components/line-chart/index.js +1024 -0
  17. package/lib/module/components/line-chart/index.js.map +1 -0
  18. package/lib/module/components/marker/index.js +190 -0
  19. package/lib/module/components/marker/index.js.map +1 -0
  20. package/lib/module/components/message/index.js +24 -8
  21. package/lib/module/components/message/index.js.map +1 -1
  22. package/lib/module/components/message-scroller/index.js +451 -0
  23. package/lib/module/components/message-scroller/index.js.map +1 -0
  24. package/lib/module/components/popover/index.js +511 -0
  25. package/lib/module/components/popover/index.js.map +1 -0
  26. package/lib/module/components/progress/index.js +52 -2
  27. package/lib/module/components/progress/index.js.map +1 -1
  28. package/lib/module/components/radio-group/index.js +132 -17
  29. package/lib/module/components/radio-group/index.js.map +1 -1
  30. package/lib/module/components/section-rail/index.js +355 -0
  31. package/lib/module/components/section-rail/index.js.map +1 -0
  32. package/lib/module/components/select/index.js +245 -56
  33. package/lib/module/components/select/index.js.map +1 -1
  34. package/lib/module/components/separator/index.js +91 -0
  35. package/lib/module/components/separator/index.js.map +1 -0
  36. package/lib/module/components/slider/index.js +355 -0
  37. package/lib/module/components/slider/index.js.map +1 -0
  38. package/lib/module/components/surface/index.js +21 -2
  39. package/lib/module/components/surface/index.js.map +1 -1
  40. package/lib/module/components/switch/index.js +10 -1
  41. package/lib/module/components/switch/index.js.map +1 -1
  42. package/lib/module/components/tabs/index.js +186 -16
  43. package/lib/module/components/tabs/index.js.map +1 -1
  44. package/lib/module/components/toggle-button/index.js +297 -0
  45. package/lib/module/components/toggle-button/index.js.map +1 -0
  46. package/lib/module/hooks/index.js +1 -0
  47. package/lib/module/hooks/index.js.map +1 -1
  48. package/lib/module/hooks/use-keyboard-avoidance.js +57 -8
  49. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  50. package/lib/module/hooks/use-scroll-sections.js +89 -0
  51. package/lib/module/hooks/use-scroll-sections.js.map +1 -0
  52. package/lib/module/icons/index.js +69 -0
  53. package/lib/module/icons/index.js.map +1 -1
  54. package/lib/module/index.js +11 -2
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/native/index.js.map +1 -1
  57. package/lib/module/primitives/keyboard-avoider.js +2 -0
  58. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  59. package/lib/module/primitives/scrim.js +72 -0
  60. package/lib/module/primitives/scrim.js.map +1 -0
  61. package/lib/module/utils/haptics.js +57 -0
  62. package/lib/module/utils/haptics.js.map +1 -0
  63. package/lib/typescript/src/components/attachment/index.d.ts +93 -0
  64. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -0
  65. package/lib/typescript/src/components/bottom-sheet/index.d.ts +6 -1
  66. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  67. package/lib/typescript/src/components/button/index.d.ts +9 -4
  68. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  69. package/lib/typescript/src/components/dialog/index.d.ts +7 -1
  70. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  71. package/lib/typescript/src/components/empty-state/index.d.ts +152 -2
  72. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  73. package/lib/typescript/src/components/frame/index.d.ts +47 -7
  74. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  75. package/lib/typescript/src/components/input/index.d.ts +113 -2
  76. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  77. package/lib/typescript/src/components/line-chart/index.d.ts +247 -0
  78. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -0
  79. package/lib/typescript/src/components/marker/index.d.ts +137 -0
  80. package/lib/typescript/src/components/marker/index.d.ts.map +1 -0
  81. package/lib/typescript/src/components/message/index.d.ts +8 -1
  82. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  83. package/lib/typescript/src/components/message-scroller/index.d.ts +150 -0
  84. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -0
  85. package/lib/typescript/src/components/popover/index.d.ts +136 -0
  86. package/lib/typescript/src/components/popover/index.d.ts.map +1 -0
  87. package/lib/typescript/src/components/progress/index.d.ts +33 -0
  88. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  89. package/lib/typescript/src/components/radio-group/index.d.ts +39 -0
  90. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  91. package/lib/typescript/src/components/section-rail/index.d.ts +115 -0
  92. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -0
  93. package/lib/typescript/src/components/select/index.d.ts +46 -7
  94. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/separator/index.d.ts +47 -0
  96. package/lib/typescript/src/components/separator/index.d.ts.map +1 -0
  97. package/lib/typescript/src/components/slider/index.d.ts +203 -0
  98. package/lib/typescript/src/components/slider/index.d.ts.map +1 -0
  99. package/lib/typescript/src/components/surface/index.d.ts +51 -2
  100. package/lib/typescript/src/components/surface/index.d.ts.map +1 -1
  101. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/tabs/index.d.ts +46 -4
  103. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  104. package/lib/typescript/src/components/toggle-button/index.d.ts +244 -0
  105. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -0
  106. package/lib/typescript/src/hooks/index.d.ts +1 -0
  107. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  108. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +10 -1
  109. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  110. package/lib/typescript/src/hooks/use-scroll-sections.d.ts +31 -0
  111. package/lib/typescript/src/hooks/use-scroll-sections.d.ts.map +1 -0
  112. package/lib/typescript/src/icons/index.d.ts +6 -0
  113. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  114. package/lib/typescript/src/index.d.ts +13 -4
  115. package/lib/typescript/src/index.d.ts.map +1 -1
  116. package/lib/typescript/src/native/index.d.ts +16 -1
  117. package/lib/typescript/src/native/index.d.ts.map +1 -1
  118. package/lib/typescript/src/primitives/keyboard-avoider.d.ts +8 -1
  119. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  120. package/lib/typescript/src/primitives/scrim.d.ts +21 -0
  121. package/lib/typescript/src/primitives/scrim.d.ts.map +1 -0
  122. package/lib/typescript/src/utils/haptics.d.ts +12 -0
  123. package/lib/typescript/src/utils/haptics.d.ts.map +1 -0
  124. package/package.json +9 -1
  125. package/src/components/attachment/index.tsx +279 -0
  126. package/src/components/bottom-sheet/index.tsx +58 -2
  127. package/src/components/button/index.tsx +26 -5
  128. package/src/components/dialog/index.tsx +10 -1
  129. package/src/components/empty-state/index.tsx +60 -14
  130. package/src/components/frame/index.tsx +175 -29
  131. package/src/components/input/index.tsx +122 -16
  132. package/src/components/line-chart/index.tsx +1224 -0
  133. package/src/components/marker/index.tsx +194 -0
  134. package/src/components/message/index.tsx +29 -5
  135. package/src/components/message-scroller/index.tsx +574 -0
  136. package/src/components/popover/index.tsx +624 -0
  137. package/src/components/progress/index.tsx +81 -1
  138. package/src/components/radio-group/index.tsx +156 -18
  139. package/src/components/section-rail/index.tsx +459 -0
  140. package/src/components/select/index.tsx +265 -52
  141. package/src/components/separator/index.tsx +97 -0
  142. package/src/components/slider/index.tsx +371 -0
  143. package/src/components/surface/index.tsx +29 -3
  144. package/src/components/switch/index.tsx +8 -1
  145. package/src/components/tabs/index.tsx +208 -28
  146. package/src/components/toggle-button/index.tsx +374 -0
  147. package/src/hooks/index.ts +5 -0
  148. package/src/hooks/use-keyboard-avoidance.ts +73 -7
  149. package/src/hooks/use-scroll-sections.ts +127 -0
  150. package/src/icons/index.tsx +48 -0
  151. package/src/index.ts +97 -6
  152. package/src/native/index.ts +13 -1
  153. package/src/primitives/keyboard-avoider.tsx +9 -1
  154. package/src/primitives/scrim.tsx +95 -0
  155. package/src/utils/haptics.ts +63 -0
  156. package/theme.css +64 -2
  157. package/lib/module/components/inline-select/index.js +0 -284
  158. package/lib/module/components/inline-select/index.js.map +0 -1
  159. package/lib/typescript/src/components/inline-select/index.d.ts +0 -48
  160. package/lib/typescript/src/components/inline-select/index.d.ts.map +0 -1
  161. 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
+ });