panelui-native 0.30.0 → 0.34.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 (81) hide show
  1. package/README.md +4 -2
  2. package/lib/module/components/button/index.js +106 -11
  3. package/lib/module/components/button/index.js.map +1 -1
  4. package/lib/module/components/calendar/index.js +107 -24
  5. package/lib/module/components/calendar/index.js.map +1 -1
  6. package/lib/module/components/drawer/index.js +477 -0
  7. package/lib/module/components/drawer/index.js.map +1 -0
  8. package/lib/module/components/heatmap-chart/index.js +9 -8
  9. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  10. package/lib/module/components/map/index.js +50 -4
  11. package/lib/module/components/map/index.js.map +1 -1
  12. package/lib/module/components/panelside/index.js +1024 -0
  13. package/lib/module/components/panelside/index.js.map +1 -0
  14. package/lib/module/components/plan/index.js +307 -21
  15. package/lib/module/components/plan/index.js.map +1 -1
  16. package/lib/module/components/post/index.js +992 -0
  17. package/lib/module/components/post/index.js.map +1 -0
  18. package/lib/module/components/response/index.js +296 -0
  19. package/lib/module/components/response/index.js.map +1 -0
  20. package/lib/module/components/response/markdown.js +463 -0
  21. package/lib/module/components/response/markdown.js.map +1 -0
  22. package/lib/module/components/select/index.js +111 -18
  23. package/lib/module/components/select/index.js.map +1 -1
  24. package/lib/module/components/table/index.js +632 -0
  25. package/lib/module/components/table/index.js.map +1 -0
  26. package/lib/module/icons/index.js +220 -0
  27. package/lib/module/icons/index.js.map +1 -1
  28. package/lib/module/index.js +6 -1
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/native/index.js +30 -0
  31. package/lib/module/native/index.js.map +1 -1
  32. package/lib/module/utils/date.js +129 -22
  33. package/lib/module/utils/date.js.map +1 -1
  34. package/lib/typescript/src/components/button/index.d.ts +22 -1
  35. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  36. package/lib/typescript/src/components/calendar/index.d.ts +12 -3
  37. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
  38. package/lib/typescript/src/components/drawer/index.d.ts +146 -0
  39. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -0
  40. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/map/index.d.ts +8 -1
  42. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  43. package/lib/typescript/src/components/panelside/index.d.ts +354 -0
  44. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -0
  45. package/lib/typescript/src/components/plan/index.d.ts +121 -10
  46. package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
  47. package/lib/typescript/src/components/post/index.d.ts +358 -0
  48. package/lib/typescript/src/components/post/index.d.ts.map +1 -0
  49. package/lib/typescript/src/components/response/index.d.ts +91 -0
  50. package/lib/typescript/src/components/response/index.d.ts.map +1 -0
  51. package/lib/typescript/src/components/response/markdown.d.ts +88 -0
  52. package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
  53. package/lib/typescript/src/components/select/index.d.ts +27 -2
  54. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  55. package/lib/typescript/src/components/table/index.d.ts +274 -0
  56. package/lib/typescript/src/components/table/index.d.ts.map +1 -0
  57. package/lib/typescript/src/icons/index.d.ts +41 -1
  58. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  59. package/lib/typescript/src/index.d.ts +7 -2
  60. package/lib/typescript/src/index.d.ts.map +1 -1
  61. package/lib/typescript/src/native/index.d.ts +20 -0
  62. package/lib/typescript/src/native/index.d.ts.map +1 -1
  63. package/lib/typescript/src/utils/date.d.ts +49 -5
  64. package/lib/typescript/src/utils/date.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/components/button/index.tsx +140 -8
  67. package/src/components/calendar/index.tsx +136 -23
  68. package/src/components/drawer/index.tsx +662 -0
  69. package/src/components/heatmap-chart/index.tsx +9 -9
  70. package/src/components/map/index.tsx +53 -4
  71. package/src/components/panelside/index.tsx +1358 -0
  72. package/src/components/plan/index.tsx +344 -21
  73. package/src/components/post/index.tsx +1116 -0
  74. package/src/components/response/index.tsx +390 -0
  75. package/src/components/response/markdown.ts +489 -0
  76. package/src/components/select/index.tsx +155 -15
  77. package/src/components/table/index.tsx +747 -0
  78. package/src/icons/index.tsx +176 -1
  79. package/src/index.ts +93 -0
  80. package/src/native/index.ts +51 -0
  81. package/src/utils/date.ts +146 -22
@@ -0,0 +1,1024 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Panelside — a navigation panel that moves the app aside instead of covering
5
+ * it.
6
+ *
7
+ * ```tsx
8
+ * <Panelside>
9
+ * <Panelside.Panel>
10
+ * <Panelside.Header title="Assistant">
11
+ * <Panelside.Search value={query} onChangeText={setQuery} />
12
+ * </Panelside.Header>
13
+ * <Panelside.Content>
14
+ * <Panelside.Group>
15
+ * <Panelside.GroupLabel>Recents</Panelside.GroupLabel>
16
+ * <Panelside.Item label="Launch thread" />
17
+ * </Panelside.Group>
18
+ * </Panelside.Content>
19
+ * <Panelside.Footer>
20
+ * <Panelside.Cta label="New chat" onPress={compose} />
21
+ * </Panelside.Footer>
22
+ * </Panelside.Panel>
23
+ *
24
+ * <Panelside.Scene>
25
+ * <Panelside.Trigger />
26
+ * <Conversation />
27
+ * </Panelside.Scene>
28
+ * </Panelside>
29
+ * ```
30
+ *
31
+ * ## Why it is not a Drawer
32
+ *
33
+ * A drawer is an overlay: it mounts through a portal, lands above everything
34
+ * and dims what it hid. That is the wrong shape here, because the whole point
35
+ * of this pattern is that the app *stays legible* — it slides across, shrinks,
36
+ * rounds its corners and waits, so the panel reads as a layer behind the app
37
+ * rather than a sheet on top of it. A portal cannot do that: its content is
38
+ * above the app content by construction, and the app content is somewhere else
39
+ * in the tree entirely, unreachable.
40
+ *
41
+ * So Panelside owns both halves. It renders inline, keeps the panel and the
42
+ * app screen as siblings under one clipping container, and gives them a single
43
+ * `progress` value to move against. `Panelside.Scene` is the wrapper you put
44
+ * around your own screen; without it there is nothing to push, which is why it
45
+ * is explicit rather than inferred.
46
+ *
47
+ * ## The scene maths
48
+ *
49
+ * React Native scales a view about its centre, so a scene scaled to `s` has
50
+ * already pulled its left edge `W * (1 - s) / 2` inward before any translation
51
+ * is applied. Translating by the panel width alone would therefore leave a gap
52
+ * that grows with the scale, and the panel would look mis-measured. Subtracting
53
+ * that inset is what puts the scene's *visible* edge exactly where the panel
54
+ * ends:
55
+ *
56
+ * scale = 1 - (1 - s) * p
57
+ * translateX = p * (width + gap) - W * (1 - scale) / 2
58
+ *
59
+ * Both are driven from one shared value, so a half-finished drag is a real
60
+ * halfway state rather than an interpolation between two snapshots.
61
+ *
62
+ * ## One gesture, both directions
63
+ *
64
+ * A single pan opens and closes. By default it listens across the whole
65
+ * surface, because that is the behaviour this pattern is known for: a sideways
66
+ * drag anywhere on the app brings the panel in, from wherever your thumb
67
+ * already was. What keeps a list usable underneath it is the pair of
68
+ * thresholds — the drag gives itself up on twelve points of vertical travel
69
+ * and only claims the touch at fourteen horizontal, so anything even slightly
70
+ * vertical resolves as a scroll.
71
+ *
72
+ * `swipeFrom="edge"` narrows the closed-state hit area to a strip at the
73
+ * leading screen edge instead. That is for a scene with its own use for a
74
+ * horizontal drag — a carousel, a wide table, a pannable chart — which would
75
+ * otherwise fight the panel and lose.
76
+ *
77
+ * Reanimated's default `ReduceMotion.System` applies throughout: with the
78
+ * accessibility setting on, every spring here resolves instantly to its target
79
+ * rather than travelling.
80
+ */
81
+ import { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useMemo, useState } from 'react';
82
+ import { Platform, Pressable, ScrollView, StyleSheet, TextInput, useWindowDimensions, View } from 'react-native';
83
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
84
+ import Animated, { runOnJS, useAnimatedProps, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring } from 'react-native-reanimated';
85
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
86
+ import { tv } from 'tailwind-variants';
87
+ import { useCSSVariable } from 'uniwind';
88
+ import { LinearGradient } from 'expo-linear-gradient';
89
+ import { EllipsisIcon, IconColorProvider, MenuIcon, SearchIcon } from "../../icons/index.js";
90
+ import { Button } from "../button/index.js";
91
+ import { AnimatedPressable } from "../../primitives/animated-pressable.js";
92
+ import { Text, textChildren } from "../../primitives/text.js";
93
+ import { useBackHandler } from "../../hooks/use-back-handler.js";
94
+ import { useDirectionSign } from "../../hooks/use-direction.js";
95
+ import { cn } from "../../utils/cn.js";
96
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
97
+ const SPRING = {
98
+ damping: 24,
99
+ stiffness: 300,
100
+ mass: 0.7
101
+ };
102
+
103
+ /**
104
+ * How wide the leading-edge strip that starts a swipe is, in points.
105
+ *
106
+ * Wider than the system's own edge gestures, because this one has to be found
107
+ * without a bezel to feel for: a thumb reaching for the side of a phone lands
108
+ * anywhere in the first 40-odd points, and a strip narrower than that reads as
109
+ * a gesture that does not work rather than one that was missed.
110
+ */
111
+ const EDGE_WIDTH = 48;
112
+ /** A drag has to clear this before releasing it changes the open state. */
113
+ const COMMIT_DISTANCE = 60;
114
+ /** A flick this fast commits regardless of how far it got. */
115
+ const COMMIT_VELOCITY = 500;
116
+ /**
117
+ * How much travel claims the drag, and how much gives it up — different on
118
+ * each axis, and different again depending on where the swipe may start.
119
+ *
120
+ * From the edge strip the horizontal claim is small and the vertical give-up
121
+ * generous: the target is narrow, so the gesture has to win early, and nobody
122
+ * swipes in a straight line from the side of a phone.
123
+ *
124
+ * From anywhere the numbers invert, because now the whole screen is competing.
125
+ * Giving up sooner than it claims is what makes a lazy diagonal resolve as a
126
+ * scroll rather than as the panel: a list keeps every drag that is even
127
+ * slightly vertical, and only a deliberate sideways one opens the panel.
128
+ */
129
+ const OFFSETS = {
130
+ edge: {
131
+ activate: 6,
132
+ fail: 16
133
+ },
134
+ anywhere: {
135
+ activate: 14,
136
+ fail: 12
137
+ }
138
+ };
139
+ /** Below this the drag is a tap that wobbled, and velocity is not consulted. */
140
+ const MIN_OFFSET = 5;
141
+
142
+ /** Gap left between the panel's edge and the pushed scene. */
143
+ const GAP = 12;
144
+ /**
145
+ * How small the scene gets at full travel. One, by default: it does not shrink.
146
+ *
147
+ * Scaling is the obvious way to make the pushed screen read as a card, and it
148
+ * is the wrong one. A scale is applied about the centre, so it insets the
149
+ * scene at the top and the bottom as well as the side — the screen lifts away
150
+ * from the status bar and the home indicator, and the two strips of panel that
151
+ * appear above and below it are strips of nothing. What the apps this pattern
152
+ * comes from do instead is keep the screen full height, running behind the
153
+ * status bar exactly as it did before, and let the corner radius and the dim
154
+ * carry the whole effect. Only the *content* respects the safe area, which it
155
+ * was already doing.
156
+ *
157
+ * Set it below one for the shrinking version; nothing else has to change.
158
+ */
159
+ const SCENE_SCALE = 1;
160
+ /** The corner radius the scene picks up at full travel. */
161
+ const SCENE_RADIUS = 44;
162
+ /** How far the scene is dimmed at full travel. */
163
+ const SCENE_DIM = 0.45;
164
+ /**
165
+ * How strongly the line along the scene's edge reads at full travel.
166
+ *
167
+ * One, because the token it is drawn in already carries its own alpha — 6%
168
+ * white in a dark theme, 8% black in a light one. Holding it back further would
169
+ * be dimming a colour that is already almost entirely transparent.
170
+ */
171
+ const EDGE_OPACITY = 1;
172
+ /**
173
+ * How thick that line is.
174
+ *
175
+ * A point rather than `StyleSheet.hairlineWidth`. A hairline is one physical
176
+ * pixel, which is right for a divider on a flat surface and too little on a
177
+ * corner this round — most of the line is curve, and a third of a point of
178
+ * curve antialiases away to nothing.
179
+ */
180
+ const EDGE_WIDTH_PT = 1;
181
+
182
+ /**
183
+ * How far behind the scene the panel starts.
184
+ *
185
+ * The panel is never actually off-screen in push mode — it is simply covered.
186
+ * Moving it a little anyway is what stops the reveal reading as a photograph
187
+ * sliding off a poster: the two layers travel at different rates, so the panel
188
+ * settles into place rather than having been there all along.
189
+ */
190
+ const PARALLAX = 0.18;
191
+
192
+ /**
193
+ * Fraction of the container the panel takes, and the cap it never passes.
194
+ *
195
+ * Wide on purpose. The sliver of app left showing is not a preview of it — it
196
+ * is a handle and a reminder, and the moment it is wide enough to read as a
197
+ * column the screen turns into a two-pane layout that neither pane fits. The
198
+ * cap keeps that true on a tablet, where the fraction alone would produce a
199
+ * navigation list with a field of whitespace beside it.
200
+ */
201
+ const WIDTH_FRACTION = 0.8;
202
+ const WIDTH_MAX = 360;
203
+
204
+ /**
205
+ * The same, for a docked panel — and nothing like it, because the job changed.
206
+ *
207
+ * An overlay panel can take most of the width, since the app is behind it and
208
+ * gets it all back on close. A docked panel keeps what it takes: every point
209
+ * of it is a point the app does not have, and 80% of the container leaves a
210
+ * column too narrow to put anything in. A third, capped, is a sidebar.
211
+ */
212
+ const DOCK_WIDTH_FRACTION = 0.32;
213
+ const DOCK_WIDTH_MAX = 320;
214
+
215
+ /** How far above the floating footer the list starts dissolving into it. */
216
+ const FOOTER_FADE = 28;
217
+
218
+ /** Progress past which a layer is treated as fully hidden for accessibility. */
219
+ const HIDDEN_EPSILON = 0.05;
220
+ const clamp = (value, min, max) => {
221
+ 'worklet';
222
+
223
+ return Math.min(Math.max(value, min), max);
224
+ };
225
+ const itemVariants = tv({
226
+ // No width: in a group it stretches on its own, and pinning it to full width
227
+ // would stop it sharing a footer row with anything else. `shrink` because
228
+ // React Native defaults `flexShrink` to 0 — in a footer beside a button, on a
229
+ // panel narrow enough for the two not to fit, nothing would give way and
230
+ // both would simply hang off the edge.
231
+ base: 'shrink flex-row items-center gap-3 rounded-xl px-3 py-2.5',
232
+ variants: {
233
+ active: {
234
+ true: 'bg-secondary'
235
+ },
236
+ disabled: {
237
+ true: 'opacity-40'
238
+ }
239
+ }
240
+ });
241
+ const ctaVariants = tv({
242
+ // Taller and wider than a list row's control, and set a step up. It is the
243
+ // one thing in the panel you are meant to reach for without reading, so it
244
+ // should not be the same size as the eight chat titles above it.
245
+ base: 'h-12 shrink flex-row items-center justify-center gap-2 rounded-full px-6',
246
+ variants: {
247
+ variant: {
248
+ primary: 'bg-primary',
249
+ secondary: 'bg-secondary'
250
+ }
251
+ },
252
+ defaultVariants: {
253
+ variant: 'primary'
254
+ }
255
+ });
256
+ const PanelsideContext = /*#__PURE__*/createContext(null);
257
+ function usePanelsideContext(component) {
258
+ const context = useContext(PanelsideContext);
259
+ if (!context) {
260
+ throw new Error(`${component} must be used within a <Panelside>`);
261
+ }
262
+ return context;
263
+ }
264
+ /**
265
+ * The panel's state, from anywhere inside a `<Panelside>` — including your own
266
+ * screen inside `Panelside.Scene`, which is where a custom open button usually
267
+ * lives.
268
+ */
269
+ export function usePanelside() {
270
+ const {
271
+ open,
272
+ setOpen,
273
+ toggle,
274
+ progress,
275
+ docked
276
+ } = usePanelsideContext('usePanelside');
277
+ return {
278
+ open,
279
+ setOpen,
280
+ toggle,
281
+ progress,
282
+ docked
283
+ };
284
+ }
285
+
286
+ /**
287
+ * What the panel's own parts share: the floating footer's height, so the
288
+ * scroller can leave room for it. The footer overlays the list rather than
289
+ * taking a row of its own, which means nothing else can know how tall it is
290
+ * until it has laid itself out.
291
+ */
292
+
293
+ const PanelsideSurfaceContext = /*#__PURE__*/createContext(null);
294
+ function PanelsideRoot({
295
+ children,
296
+ open: controlledOpen,
297
+ onOpenChange,
298
+ defaultOpen = false,
299
+ mode = 'push',
300
+ width: widthProp,
301
+ dock = false,
302
+ swipeEnabled = true,
303
+ swipeFrom = 'anywhere',
304
+ edgeWidth = EDGE_WIDTH,
305
+ dismissible = true,
306
+ className
307
+ }) {
308
+ const {
309
+ width: windowWidth
310
+ } = useWindowDimensions();
311
+ const sign = useDirectionSign();
312
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
313
+ const controlled = controlledOpen !== undefined;
314
+ const open = controlled ? controlledOpen : uncontrolledOpen;
315
+
316
+ /*
317
+ * Measured rather than taken from the window, because Panelside does not
318
+ * have to be the whole screen — it can be one tab of a larger layout, and
319
+ * the push distance is a fraction of whatever it actually got. The window
320
+ * width is only the value to use until the first layout arrives.
321
+ */
322
+ const [containerWidth, setContainerWidth] = useState(windowWidth);
323
+ const onLayout = useCallback(event => {
324
+ setContainerWidth(event.nativeEvent.layout.width);
325
+ }, []);
326
+ const docked = dock !== false && containerWidth >= dock;
327
+ const width = widthProp ?? (docked ? Math.min(containerWidth * DOCK_WIDTH_FRACTION, DOCK_WIDTH_MAX) : Math.min(containerWidth * WIDTH_FRACTION, WIDTH_MAX));
328
+
329
+ /**
330
+ * How far the drag runs. In push mode the scene clears the gap too.
331
+ *
332
+ * Floored at 1 because it is a divisor: a container measured at zero — one
333
+ * frame during a collapsed layout is enough — would otherwise make progress
334
+ * infinite and park both layers somewhere off the screen for good.
335
+ */
336
+ const extent = Math.max(1, mode === 'push' ? width + GAP : width);
337
+ const translation = useSharedValue(open ? extent : 0);
338
+ const progress = useDerivedValue(() => translation.value / extent, [extent]);
339
+
340
+ /*
341
+ * Which end state a spring is already heading for, so the effect below does
342
+ * not restart an animation the gesture just launched with velocity. Without
343
+ * it, committing a flick re-springs from mid-flight at zero velocity, which
344
+ * is visible as a stutter right where the motion should feel fastest.
345
+ */
346
+ const animatingTo = useSharedValue(null);
347
+ const settle = useCallback((next, velocity) => {
348
+ 'worklet';
349
+
350
+ const target = next ? extent : 0;
351
+ if (translation.value === target) return;
352
+ if (animatingTo.value === (next ? 'open' : 'close')) return;
353
+
354
+ /*
355
+ * A velocity pointing away from the target would fight the spring, and
356
+ * the spring wins — so the only thing it contributes is a hitch at the
357
+ * start. Drop it and let the spring do the whole trip.
358
+ */
359
+ const aligned = velocity !== undefined && (target > translation.value && velocity > 0 || target < translation.value && velocity < 0);
360
+ animatingTo.value = next ? 'open' : 'close';
361
+ translation.value = withSpring(target, {
362
+ ...SPRING,
363
+ velocity: aligned ? velocity : 0
364
+ }, () => {
365
+ animatingTo.value = null;
366
+ });
367
+ }, [animatingTo, extent, translation]);
368
+ const setOpen = useCallback(next => {
369
+ if (!controlled) setUncontrolledOpen(next);
370
+ onOpenChange?.(next);
371
+ }, [controlled, onOpenChange]);
372
+ const toggle = useCallback(() => setOpen(!open), [open, setOpen]);
373
+ const close = useCallback(() => setOpen(false), [setOpen]);
374
+ useBackHandler(open && dismissible && !docked, close);
375
+
376
+ /*
377
+ * A docked panel is open by definition, and it must not animate there — on
378
+ * a rotation into a docked layout the panel is already beside the scene, so
379
+ * a spring would slide furniture that never moved.
380
+ */
381
+ useEffect(() => {
382
+ if (docked) {
383
+ translation.value = extent;
384
+ return;
385
+ }
386
+ settle(open);
387
+ }, [docked, extent, open, settle, translation]);
388
+ const offsets = OFFSETS[swipeFrom];
389
+ const pan = useMemo(() => {
390
+ let gesture = Gesture.Pan().enabled(swipeEnabled && !docked).activeOffsetX([-offsets.activate, offsets.activate])
391
+ // Real vertical intent hands the touch back, so a list inside the panel
392
+ // and a scroller inside the app both keep their own drags.
393
+ .failOffsetY([-offsets.fail, offsets.fail]).onChange(event => {
394
+ translation.value = clamp(translation.value + event.changeX * sign, 0, extent);
395
+ }).onEnd(event => {
396
+ const distance = event.translationX * sign;
397
+ const velocity = event.velocityX * sign;
398
+ const decisive = Math.abs(distance) > MIN_OFFSET && Math.abs(velocity) > COMMIT_VELOCITY || Math.abs(distance) > COMMIT_DISTANCE;
399
+ // Below the threshold the drag was not an instruction: go back to
400
+ // wherever the panel already was.
401
+ const next = decisive ? (velocity === 0 ? distance : velocity) > 0 : open;
402
+
403
+ // `velocity` is already in travel space; the spring runs on the same
404
+ // axis, so it must not be converted back to screen space here.
405
+ settle(next, velocity);
406
+ runOnJS(setOpen)(next);
407
+ });
408
+
409
+ /*
410
+ * The strip is only ever a closed-state restriction. Open, it is dropped
411
+ * whatever `swipeFrom` says: the panel is already out, so there is no app
412
+ * underneath left to compete for the same horizontal swipe.
413
+ */
414
+ if (!open && swipeFrom === 'edge') {
415
+ gesture = gesture.hitSlop(sign === 1 ? {
416
+ left: 0,
417
+ width: edgeWidth
418
+ } : {
419
+ right: 0,
420
+ width: edgeWidth
421
+ });
422
+ }
423
+ return gesture;
424
+ }, [docked, edgeWidth, extent, offsets, open, setOpen, settle, sign, swipeEnabled, swipeFrom, translation]);
425
+ const context = useMemo(() => ({
426
+ open,
427
+ setOpen,
428
+ toggle,
429
+ progress,
430
+ width,
431
+ mode,
432
+ docked,
433
+ dismissible
434
+ }), [dismissible, docked, mode, open, progress, setOpen, toggle, width]);
435
+ return /*#__PURE__*/_jsx(PanelsideContext.Provider, {
436
+ value: context,
437
+ children: /*#__PURE__*/_jsx(View, {
438
+ onLayout: onLayout
439
+ // Clipping is what lets the panel sit at the edge with a parallax
440
+ // offset without a sliver of it hanging outside the container.
441
+ ,
442
+ className: cn('flex-1 overflow-hidden', docked && 'flex-row', className),
443
+ children: /*#__PURE__*/_jsx(GestureDetector, {
444
+ gesture: pan,
445
+ children: /*#__PURE__*/_jsx(Animated.View, {
446
+ className: cn('flex-1', docked && 'flex-row'),
447
+ children: children
448
+ })
449
+ })
450
+ })
451
+ });
452
+ }
453
+ function PanelsidePanel({
454
+ className,
455
+ children,
456
+ style,
457
+ ...props
458
+ }) {
459
+ const {
460
+ progress,
461
+ width,
462
+ mode,
463
+ docked
464
+ } = usePanelsideContext('Panelside.Panel');
465
+ const [footerHeight, setFooterHeight] = useState(0);
466
+ const animatedStyle = useAnimatedStyle(() => {
467
+ const p = docked ? 1 : progress.value;
468
+ const distance = mode === 'overlay' ? width : width * PARALLAX;
469
+ return {
470
+ transform: [{
471
+ translateX: -(1 - p) * distance
472
+ }]
473
+ };
474
+ }, [docked, mode, width]);
475
+
476
+ /*
477
+ * Out of the accessibility tree until it is nearly all the way in. A panel
478
+ * at rest is behind the app, fully covered and not a thing you can reach —
479
+ * but nothing about being covered says that to a screen reader, which will
480
+ * happily read out a navigation list nobody can see.
481
+ */
482
+ const animatedProps = useAnimatedProps(() => {
483
+ const hidden = !docked && progress.value < 1 - HIDDEN_EPSILON;
484
+ return Platform.OS === 'android' ? {
485
+ importantForAccessibility: hidden ? 'no-hide-descendants' : 'auto'
486
+ } : {
487
+ accessibilityElementsHidden: hidden
488
+ };
489
+ }, [docked]);
490
+ const surface = useMemo(() => ({
491
+ footerHeight,
492
+ setFooterHeight
493
+ }), [footerHeight]);
494
+ return /*#__PURE__*/_jsx(PanelsideSurfaceContext.Provider, {
495
+ value: surface,
496
+ children: /*#__PURE__*/_jsx(Animated.View, {
497
+ animatedProps: animatedProps,
498
+ className: cn('bg-background', docked ? 'h-full border-e border-border' : 'absolute bottom-0 start-0 top-0',
499
+ // In overlay mode the panel comes in *over* the app, but it is the
500
+ // first child and the scene is the second — so without this it slides
501
+ // in behind the very thing it is supposed to cover, and all you see
502
+ // is the scrim.
503
+ !docked && mode === 'overlay' && 'z-10 shadow-lg', className)
504
+ // Animated style before the caller's, so a className or style cannot
505
+ // silently drop the transform the panel is being moved by.
506
+ ,
507
+ style: [{
508
+ width
509
+ }, animatedStyle, style],
510
+ ...props,
511
+ children: children
512
+ })
513
+ });
514
+ }
515
+ function PanelsideHeader({
516
+ className,
517
+ title,
518
+ action,
519
+ children,
520
+ style,
521
+ ...props
522
+ }) {
523
+ const insets = useSafeAreaInsets();
524
+ return /*#__PURE__*/_jsxs(View
525
+ // The panel draws behind the status bar on purpose, so it reads as a
526
+ // full-height surface rather than a card. That makes the inset the
527
+ // header's to clear, and it stacks with its own padding rather than
528
+ // being maxed against it.
529
+ , {
530
+ style: [{
531
+ paddingTop: insets.top + 12
532
+ }, style]
533
+ // `px-3` matches the scroller below it, so the search field and the rows
534
+ // share one edge. A header inset further would leave the field floating
535
+ // a few points inside the list it filters.
536
+ ,
537
+ className: cn('gap-3 px-3 pb-3', className),
538
+ ...props,
539
+ children: [(title || action) && /*#__PURE__*/_jsxs(View, {
540
+ className: "h-9 flex-row items-center justify-between gap-2",
541
+ children: [title ? /*#__PURE__*/_jsx(Text, {
542
+ size: "xl",
543
+ weight: "semibold",
544
+ numberOfLines: 1,
545
+ className: "flex-1",
546
+ children: title
547
+ }) : /*#__PURE__*/_jsx(View, {
548
+ className: "flex-1"
549
+ }), action]
550
+ }), textChildren(children)]
551
+ });
552
+ }
553
+ /**
554
+ * A compact filter field for the panel.
555
+ *
556
+ * Deliberately not the library's `Input`: that field carries a label, a
557
+ * description, an error slot and keyboard avoidance, none of which a panel
558
+ * search row wants, and all of which would have to be switched off at every
559
+ * call site.
560
+ */
561
+ function PanelsideSearch({
562
+ className,
563
+ containerClassName,
564
+ placeholder = 'Search',
565
+ ...props
566
+ }) {
567
+ const placeholderTint = useCSSVariable('--color-muted-foreground');
568
+ const textTint = useCSSVariable('--color-foreground');
569
+ const muted = typeof placeholderTint === 'string' ? placeholderTint : undefined;
570
+ return /*#__PURE__*/_jsxs(View, {
571
+ className: cn('h-10 flex-row items-center gap-2 rounded-xl bg-secondary px-3', containerClassName),
572
+ children: [/*#__PURE__*/_jsx(SearchIcon, {
573
+ size: 16,
574
+ color: muted
575
+ }), /*#__PURE__*/_jsx(TextInput, {
576
+ placeholder: placeholder,
577
+ placeholderTextColor: muted
578
+ /*
579
+ * `text-[16px]`, not `text-base`. A `text-*` step sets a size and a
580
+ * line height together — 16px glyphs in a 24px line box — and in a
581
+ * field of fixed height the extra leading lands above them, so the
582
+ * text and the placeholder sit below the middle of the row. A length
583
+ * sets the size alone and leaves the line box the font's own.
584
+ */,
585
+ className: cn('h-full flex-1 text-[16px] text-foreground', className),
586
+ style: typeof textTint === 'string' ? {
587
+ color: textTint
588
+ } : undefined,
589
+ accessibilityRole: "search",
590
+ returnKeyType: "search",
591
+ clearButtonMode: "while-editing",
592
+ ...props
593
+ })]
594
+ });
595
+ }
596
+ function PanelsideContent({
597
+ className,
598
+ contentContainerClassName,
599
+ contentContainerStyle,
600
+ children,
601
+ ...props
602
+ }) {
603
+ const surface = useContext(PanelsideSurfaceContext);
604
+ return /*#__PURE__*/_jsx(ScrollView, {
605
+ className: cn('flex-1', className),
606
+ contentContainerClassName: cn('gap-1 px-3 pb-3', contentContainerClassName)
607
+ // Room for the footer, which floats over this list rather than taking a
608
+ // row below it — so the last item can be scrolled clear of the pill
609
+ // instead of living permanently underneath it.
610
+ ,
611
+ contentContainerStyle: [{
612
+ paddingBottom: (surface?.footerHeight ?? 0) + 12
613
+ }, contentContainerStyle],
614
+ showsVerticalScrollIndicator: false,
615
+ keyboardShouldPersistTaps: "handled",
616
+ ...props,
617
+ children: textChildren(children)
618
+ });
619
+ }
620
+ function PanelsideGroup({
621
+ className,
622
+ children,
623
+ ...props
624
+ }) {
625
+ return /*#__PURE__*/_jsx(View, {
626
+ className: cn('gap-0.5 pb-2', className),
627
+ ...props,
628
+ children: textChildren(children)
629
+ });
630
+ }
631
+ function PanelsideGroupLabel({
632
+ className,
633
+ children,
634
+ ...props
635
+ }) {
636
+ return /*#__PURE__*/_jsx(View, {
637
+ className: cn('px-3 pb-1 pt-3', className),
638
+ accessibilityRole: "header",
639
+ ...props,
640
+ children: textChildren(children, text => /*#__PURE__*/_jsx(Text, {
641
+ size: "xs",
642
+ weight: "medium",
643
+ muted: true,
644
+ children: text
645
+ }))
646
+ });
647
+ }
648
+ function PanelsideItem({
649
+ className,
650
+ icon,
651
+ label,
652
+ active = false,
653
+ badge,
654
+ disabled = false,
655
+ children,
656
+ ...props
657
+ }) {
658
+ const restTint = useCSSVariable('--color-muted-foreground');
659
+ const activeTint = useCSSVariable('--color-foreground');
660
+ const tint = active ? typeof activeTint === 'string' ? activeTint : undefined : typeof restTint === 'string' ? restTint : undefined;
661
+ return /*#__PURE__*/_jsxs(AnimatedPressable, {
662
+ className: itemVariants({
663
+ active,
664
+ disabled,
665
+ className
666
+ }),
667
+ disabled: disabled,
668
+ accessibilityRole: "button",
669
+ accessibilityState: {
670
+ selected: active,
671
+ disabled
672
+ },
673
+ accessibilityLabel: label,
674
+ pressScale: 0.985,
675
+ ...props,
676
+ children: [icon ? /*#__PURE__*/_jsx(IconColorProvider, {
677
+ color: tint,
678
+ children: icon
679
+ }) : null, label ? /*#__PURE__*/_jsx(Text, {
680
+ size: "base",
681
+ weight: active ? 'medium' : 'normal',
682
+ muted: !active,
683
+ numberOfLines: 1,
684
+ className: "flex-1",
685
+ children: label
686
+ }) : /*#__PURE__*/_jsx(View, {
687
+ className: "flex-1"
688
+ }), typeof badge === 'string' || typeof badge === 'number' ? /*#__PURE__*/_jsx(View, {
689
+ className: "rounded-full bg-secondary px-2 py-0.5",
690
+ children: /*#__PURE__*/_jsx(Text, {
691
+ size: "xs",
692
+ muted: true,
693
+ children: badge
694
+ })
695
+ }) : badge, children]
696
+ });
697
+ }
698
+ function PanelsideAction({
699
+ className,
700
+ label = 'More options',
701
+ children,
702
+ ...props
703
+ }) {
704
+ const tint = useCSSVariable('--color-muted-foreground');
705
+ const color = typeof tint === 'string' ? tint : undefined;
706
+ return /*#__PURE__*/_jsx(AnimatedPressable, {
707
+ className: cn('h-7 w-7 items-center justify-center rounded-lg', className),
708
+ accessibilityRole: "button",
709
+ accessibilityLabel: label
710
+ // The glyph is small and sits next to a row-sized target, so it takes
711
+ // the difference back as slop rather than as layout.
712
+ ,
713
+ hitSlop: 8,
714
+ ...props,
715
+ children: children ?? /*#__PURE__*/_jsx(EllipsisIcon, {
716
+ size: 18,
717
+ color: color
718
+ })
719
+ });
720
+ }
721
+ function PanelsideFooter({
722
+ className,
723
+ floating = true,
724
+ children,
725
+ style,
726
+ ...props
727
+ }) {
728
+ const insets = useSafeAreaInsets();
729
+ const surface = useContext(PanelsideSurfaceContext);
730
+ const setFooterHeight = surface?.setFooterHeight;
731
+ const background = useCSSVariable('--color-background');
732
+ const solid = typeof background === 'string' ? background : '#000000';
733
+ const onLayout = useCallback(event => {
734
+ setFooterHeight?.(event.nativeEvent.layout.height);
735
+ }, [setFooterHeight]);
736
+ return /*#__PURE__*/_jsxs(View, {
737
+ onLayout: floating ? onLayout : undefined,
738
+ style: [{
739
+ paddingBottom: Math.max(insets.bottom, 12)
740
+ }, floating ? {
741
+ paddingTop: FOOTER_FADE
742
+ } : null, style],
743
+ className: cn(
744
+ // `gap-3` and a wider inset: the compose control is the one thing in
745
+ // the panel that is not a list row, and a native one brings its own
746
+ // metrics — it needs room around it rather than the row spacing the
747
+ // list uses.
748
+ 'flex-row items-center gap-3 px-4', floating ? 'absolute bottom-0 end-0 start-0' : 'border-t border-border bg-background pt-2', className),
749
+ ...props,
750
+ children: [floating ? /*#__PURE__*/_jsxs(_Fragment, {
751
+ children: [/*#__PURE__*/_jsx(LinearGradient, {
752
+ colors: [`${solid}00`, solid],
753
+ start: {
754
+ x: 0,
755
+ y: 0
756
+ },
757
+ end: {
758
+ x: 0,
759
+ y: 1
760
+ },
761
+ pointerEvents: "none",
762
+ style: [styles.fade, {
763
+ height: FOOTER_FADE
764
+ }]
765
+ }), /*#__PURE__*/_jsx(View, {
766
+ pointerEvents: "none",
767
+ className: "absolute bottom-0 end-0 start-0 bg-background",
768
+ style: {
769
+ top: FOOTER_FADE
770
+ }
771
+ })]
772
+ }) : null, textChildren(children)]
773
+ });
774
+ }
775
+ const styles = StyleSheet.create({
776
+ fade: {
777
+ position: 'absolute',
778
+ top: 0,
779
+ left: 0,
780
+ right: 0
781
+ }
782
+ });
783
+ function PanelsideCta({
784
+ className,
785
+ label,
786
+ icon,
787
+ variant = 'primary',
788
+ native = false,
789
+ glass = false,
790
+ disabled,
791
+ children,
792
+ ...props
793
+ }) {
794
+ const primaryTint = useCSSVariable('--color-primary-foreground');
795
+ const secondaryTint = useCSSVariable('--color-secondary-foreground');
796
+ const raw = variant === 'primary' ? primaryTint : secondaryTint;
797
+ const tint = typeof raw === 'string' ? raw : undefined;
798
+
799
+ /*
800
+ * Delegated to Button rather than reaching for the native bridge here.
801
+ * Button already resolves the package lazily, maps the variant onto the
802
+ * platform's own style and falls back when it is missing — reimplementing
803
+ * that would be a second copy to keep in step with the first.
804
+ */
805
+ if (native) {
806
+ return /*#__PURE__*/_jsx(Button, {
807
+ native: true,
808
+ glass: glass
809
+ // The platform sizes a native button from its label, so the step up
810
+ // has to be asked for rather than styled on.
811
+ ,
812
+ size: "lg",
813
+ variant: variant,
814
+ accessibilityLabel: label
815
+ // Pressable allows `null` for disabled; Button does not.
816
+ ,
817
+ disabled: disabled ?? undefined,
818
+ ...props,
819
+ children: label ?? children
820
+ });
821
+ }
822
+ return /*#__PURE__*/_jsx(AnimatedPressable, {
823
+ className: ctaVariants({
824
+ variant,
825
+ className
826
+ }),
827
+ accessibilityRole: "button",
828
+ accessibilityLabel: label,
829
+ disabled: disabled,
830
+ ...props,
831
+ children: /*#__PURE__*/_jsxs(IconColorProvider, {
832
+ color: tint,
833
+ children: [icon, label ? /*#__PURE__*/_jsx(Text, {
834
+ size: "lg",
835
+ weight: "medium"
836
+ // The pill gives way before the panel does, so the label has to be
837
+ // able to end somewhere rather than pushing the button off the edge.
838
+ ,
839
+ numberOfLines: 1,
840
+ className: cn('shrink', variant === 'primary' ? 'text-primary-foreground' : 'text-secondary-foreground'),
841
+ children: label
842
+ }) : null, textChildren(children)]
843
+ })
844
+ });
845
+ }
846
+ function PanelsideScene({
847
+ className,
848
+ scale = SCENE_SCALE,
849
+ radius = SCENE_RADIUS,
850
+ dim = SCENE_DIM,
851
+ children,
852
+ style,
853
+ ...props
854
+ }) {
855
+ const {
856
+ progress,
857
+ width,
858
+ mode,
859
+ docked,
860
+ dismissible,
861
+ open,
862
+ setOpen
863
+ } = usePanelsideContext('Panelside.Scene');
864
+ const [sceneWidth, setSceneWidth] = useState(0);
865
+ const sign = useDirectionSign();
866
+ /*
867
+ * The same border token every other edge in the library is drawn in, so this
868
+ * one belongs to the same set rather than being a line of its own invention.
869
+ * It already inverts with the theme — white at 6% in a dark one, black at 8%
870
+ * in a light one — which is what makes it read on both sides of a boundary
871
+ * between two surfaces of the same colour.
872
+ *
873
+ * It only failed to show before because it was drawn *under* the scrim. Above
874
+ * it, at a full point, the token is enough on its own.
875
+ */
876
+ const edge = useCSSVariable('--color-border');
877
+ const edgeColor = typeof edge === 'string' ? edge : undefined;
878
+ const onLayout = useCallback(event => {
879
+ setSceneWidth(event.nativeEvent.layout.width);
880
+ }, []);
881
+ const close = useCallback(() => setOpen(false), [setOpen]);
882
+
883
+ /*
884
+ * `pushes` rather than a branch inside the worklet, so the style always
885
+ * returns the same set of properties. Reanimated keeps a property it has
886
+ * seen once; dropping it from a later frame leaves the last value applied
887
+ * instead of resetting it.
888
+ */
889
+ const pushes = mode === 'push' && !docked;
890
+ const animatedStyle = useAnimatedStyle(() => {
891
+ const p = pushes ? progress.value : 0;
892
+ const s = 1 - (1 - scale) * p;
893
+ return {
894
+ transform: [
895
+ // Subtracting the inset a centre-origin scale already applied is what
896
+ // lands the scene's visible edge on the panel's, rather than near it.
897
+ {
898
+ translateX: sign * (p * (width + GAP) - sceneWidth * (1 - s) / 2)
899
+ }, {
900
+ scale: s
901
+ }],
902
+ borderRadius: p * radius
903
+ };
904
+ }, [pushes, radius, scale, sceneWidth, sign, width]);
905
+ const scrimStyle = useAnimatedStyle(() => {
906
+ const p = docked ? 0 : progress.value;
907
+ return {
908
+ opacity: p * dim
909
+ };
910
+ }, [dim, docked]);
911
+
912
+ /*
913
+ * The edge is drawn as its own layer rather than as a border on the scene.
914
+ * A border is a layout property: put one on the scene itself and it insets
915
+ * everything inside by its width for the whole life of the screen, open or
916
+ * shut, to show a line that is only wanted while the panel is out. A ring
917
+ * over the top costs nothing when it is invisible.
918
+ */
919
+ const ringStyle = useAnimatedStyle(() => {
920
+ const p = docked ? 0 : progress.value;
921
+ return {
922
+ opacity: p * EDGE_OPACITY,
923
+ borderRadius: p * radius
924
+ };
925
+ }, [docked, radius]);
926
+ const animatedProps = useAnimatedProps(() => {
927
+ const hidden = !docked && progress.value > 1 - HIDDEN_EPSILON;
928
+ return Platform.OS === 'android' ? {
929
+ importantForAccessibility: hidden ? 'no-hide-descendants' : 'auto'
930
+ } : {
931
+ accessibilityElementsHidden: hidden
932
+ };
933
+ }, [docked]);
934
+ return /*#__PURE__*/_jsxs(Animated.View, {
935
+ onLayout: onLayout,
936
+ className: cn('flex-1 overflow-hidden bg-background', className),
937
+ style: [animatedStyle, style],
938
+ ...props,
939
+ children: [/*#__PURE__*/_jsx(Animated.View, {
940
+ animatedProps: animatedProps,
941
+ className: "flex-1",
942
+ children: children
943
+ }), /*#__PURE__*/_jsx(Animated.View, {
944
+ pointerEvents: open && !docked ? 'auto' : 'none',
945
+ className: "absolute bottom-0 end-0 start-0 top-0 bg-black",
946
+ style: scrimStyle,
947
+ children: dismissible ? /*#__PURE__*/_jsx(Pressable, {
948
+ onPress: close,
949
+ className: "flex-1",
950
+ accessibilityRole: "button",
951
+ accessibilityLabel: "Close navigation panel"
952
+ }) : null
953
+ }), edgeColor ? /*#__PURE__*/_jsx(Animated.View, {
954
+ pointerEvents: "none",
955
+ style: [StyleSheet.absoluteFill, {
956
+ borderWidth: EDGE_WIDTH_PT,
957
+ borderColor: edgeColor
958
+ }, ringStyle]
959
+ }) : null]
960
+ });
961
+ }
962
+ function PanelsideTrigger({
963
+ className,
964
+ label = 'Open navigation panel',
965
+ children,
966
+ ...props
967
+ }) {
968
+ const {
969
+ toggle,
970
+ docked
971
+ } = usePanelsideContext('Panelside.Trigger');
972
+ const tint = useCSSVariable('--color-foreground');
973
+ const color = typeof tint === 'string' ? tint : undefined;
974
+
975
+ // A docked panel is already open and cannot be closed, so a control for it
976
+ // would be a button that does nothing.
977
+ if (docked) return null;
978
+ if (children && /*#__PURE__*/isValidElement(children)) {
979
+ return /*#__PURE__*/cloneElement(children, {
980
+ onPress: (...args) => {
981
+ children.props.onPress?.(...args);
982
+ toggle();
983
+ }
984
+ });
985
+ }
986
+ return /*#__PURE__*/_jsx(AnimatedPressable, {
987
+ onPress: toggle,
988
+ className: cn('h-10 w-10 items-center justify-center rounded-full', className),
989
+ accessibilityRole: "button",
990
+ accessibilityLabel: label,
991
+ ...props,
992
+ children: /*#__PURE__*/_jsx(MenuIcon, {
993
+ size: 20,
994
+ color: color
995
+ })
996
+ });
997
+ }
998
+ PanelsidePanel.displayName = 'Panelside.Panel';
999
+ PanelsideHeader.displayName = 'Panelside.Header';
1000
+ PanelsideSearch.displayName = 'Panelside.Search';
1001
+ PanelsideContent.displayName = 'Panelside.Content';
1002
+ PanelsideGroup.displayName = 'Panelside.Group';
1003
+ PanelsideGroupLabel.displayName = 'Panelside.GroupLabel';
1004
+ PanelsideItem.displayName = 'Panelside.Item';
1005
+ PanelsideAction.displayName = 'Panelside.Action';
1006
+ PanelsideFooter.displayName = 'Panelside.Footer';
1007
+ PanelsideCta.displayName = 'Panelside.Cta';
1008
+ PanelsideScene.displayName = 'Panelside.Scene';
1009
+ PanelsideTrigger.displayName = 'Panelside.Trigger';
1010
+ export const Panelside = Object.assign(PanelsideRoot, {
1011
+ Panel: PanelsidePanel,
1012
+ Header: PanelsideHeader,
1013
+ Search: PanelsideSearch,
1014
+ Content: PanelsideContent,
1015
+ Group: PanelsideGroup,
1016
+ GroupLabel: PanelsideGroupLabel,
1017
+ Item: PanelsideItem,
1018
+ Action: PanelsideAction,
1019
+ Footer: PanelsideFooter,
1020
+ Cta: PanelsideCta,
1021
+ Scene: PanelsideScene,
1022
+ Trigger: PanelsideTrigger
1023
+ });
1024
+ //# sourceMappingURL=index.js.map