panelui-native 0.54.0 → 0.56.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 (37) hide show
  1. package/README.md +1 -0
  2. package/lib/module/components/combobox/index.js +61 -7
  3. package/lib/module/components/combobox/index.js.map +1 -1
  4. package/lib/module/components/hex-chart/index.js +765 -0
  5. package/lib/module/components/hex-chart/index.js.map +1 -0
  6. package/lib/module/components/sortable/index.js +74 -12
  7. package/lib/module/components/sortable/index.js.map +1 -1
  8. package/lib/module/components/steps/index.js +95 -12
  9. package/lib/module/components/steps/index.js.map +1 -1
  10. package/lib/module/components/tag-input/index.js +435 -0
  11. package/lib/module/components/tag-input/index.js.map +1 -0
  12. package/lib/module/components/tour/index.js +661 -0
  13. package/lib/module/components/tour/index.js.map +1 -0
  14. package/lib/module/index.js +3 -0
  15. package/lib/module/index.js.map +1 -1
  16. package/lib/module/utils/chart.js +209 -0
  17. package/lib/module/utils/chart.js.map +1 -1
  18. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  19. package/lib/typescript/src/components/hex-chart/index.d.ts +277 -0
  20. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -0
  21. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  22. package/lib/typescript/src/components/steps/index.d.ts +19 -0
  23. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  24. package/lib/typescript/src/components/tag-input/index.d.ts +240 -0
  25. package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -0
  26. package/lib/typescript/src/components/tour/index.d.ts +168 -0
  27. package/lib/typescript/src/components/tour/index.d.ts.map +1 -0
  28. package/lib/typescript/src/index.d.ts +3 -0
  29. package/lib/typescript/src/index.d.ts.map +1 -1
  30. package/lib/typescript/src/utils/chart.d.ts +98 -0
  31. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/components/hex-chart/index.tsx +1009 -0
  34. package/src/components/steps/index.tsx +103 -8
  35. package/src/components/tour/index.tsx +933 -0
  36. package/src/index.ts +22 -0
  37. package/src/utils/chart.ts +236 -0
@@ -0,0 +1,661 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Tour — the walkthrough that introduces a screen one control at a time.
5
+ *
6
+ * An empty state explains a screen before there is anything on it; a tour
7
+ * explains it once there is. It dims everything, cuts a hole around one control
8
+ * and puts a card beside it, then moves the hole to the next control. What
9
+ * makes that work is the hole: a caption alone has to describe where to look,
10
+ * and "the button at the top right" is a sentence people read twice and still
11
+ * get wrong.
12
+ *
13
+ * ```tsx
14
+ * <Tour open={onboarding} onOpenChange={setOnboarding}>
15
+ * <Tour.Step order={0} title="Your library" description="Everything you save lands here.">
16
+ * <IconButton icon={<BookmarkIcon />} onPress={openLibrary} />
17
+ * </Tour.Step>
18
+ *
19
+ * <Tour.Step order={1} title="Start writing" description="A new note, from anywhere." shape="circle">
20
+ * <Fab icon={<PlusIcon />} onPress={compose} />
21
+ * </Tour.Step>
22
+ * </Tour>
23
+ * ```
24
+ *
25
+ * A step wraps the control it is about, so the two live together in the tree
26
+ * and cannot drift apart — a step whose target has been deleted goes with it
27
+ * rather than pointing at empty space. `order` is what puts the steps in a
28
+ * sequence, and it is the author's numbering rather than the tree's, because a
29
+ * walkthrough usually crosses a header, a list and a tab bar in an order the
30
+ * layout knows nothing about.
31
+ *
32
+ * The target is measured in window coordinates each time its step becomes
33
+ * current, and again when the window changes size — a rect measured in portrait
34
+ * describes nothing after a rotation, and a spotlight in the wrong place is
35
+ * worse than none. A target that has scrolled out of view is the one case this
36
+ * cannot fix by itself: bring it back with `onStepChange`, which fires with the
37
+ * step about to be shown.
38
+ *
39
+ * The hole is one path with an even-odd fill — the screen rectangle and the
40
+ * cutout in a single `d`, animated on the UI thread — rather than four views
41
+ * arranged around a gap. Four views cannot have rounded corners between them,
42
+ * and the corner is most of what makes the hole read as *this control* instead
43
+ * of as a rectangle that happens to contain it.
44
+ */
45
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
46
+ import { StyleSheet, useWindowDimensions, View } from 'react-native';
47
+ import Animated, { FadeIn, FadeOut, useAnimatedProps, useReducedMotion, useSharedValue, withSpring } from 'react-native-reanimated';
48
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
49
+ import Svg, { Path } from 'react-native-svg';
50
+ import { ChevronLeftIcon, XIcon } from "../../icons/index.js";
51
+ import { useBackHandler } from "../../hooks/use-back-handler.js";
52
+ import { Portal } from "../../primitives/portal.js";
53
+ import { Text } from "../../primitives/text.js";
54
+ import { cn } from "../../utils/cn.js";
55
+ import { Button } from "../button/index.js";
56
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
57
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
58
+
59
+ /** Room left between the cutout and the target inside it. */
60
+ const DEFAULT_PADDING = 8;
61
+ /** Corner radius of a rectangular cutout. */
62
+ const DEFAULT_RADIUS = 12;
63
+ /** Gap between the cutout and the card. */
64
+ const CARD_OFFSET = 12;
65
+ /** Smallest gap allowed between the card and the edge of the safe area. */
66
+ const SCREEN_MARGIN = 16;
67
+ /** Ceiling on the card's width, so it does not run edge to edge on a tablet. */
68
+ const MAX_CARD_WIDTH = 420;
69
+ /** How the spotlight travels from one target to the next. */
70
+ const SPRING = {
71
+ damping: 20,
72
+ stiffness: 180,
73
+ mass: 0.6
74
+ };
75
+ /** The dim laid over everything outside the cutout. */
76
+ const DEFAULT_OVERLAY = 'rgba(0, 0, 0, 0.66)';
77
+
78
+ /** The words on the card's controls, for a tour that is not in English. */
79
+
80
+ const DEFAULT_LABELS = {
81
+ next: 'Next',
82
+ back: 'Back',
83
+ done: 'Done',
84
+ skip: 'Skip',
85
+ close: 'End tour'
86
+ };
87
+
88
+ /**
89
+ * One step as the root sees it: what to draw the hole around, and what to say
90
+ * about it. The ref rather than a measured rect, because a rect taken at
91
+ * registration is stale by the time the step comes up.
92
+ */
93
+
94
+ const TourContext = /*#__PURE__*/createContext(null);
95
+ function useTour(component) {
96
+ const context = useContext(TourContext);
97
+ if (!context) {
98
+ throw new Error(`${component} must be used within a <Tour>`);
99
+ }
100
+ return context;
101
+ }
102
+
103
+ /**
104
+ * The screen with a rounded rectangle taken out of it, as one path.
105
+ *
106
+ * Two subpaths and `fillRule="evenodd"`: the outer one covers the screen, the
107
+ * inner one falls inside it, and even-odd makes the overlap a hole regardless
108
+ * of which way either is wound. That last part is why the inner rectangle is
109
+ * written in the natural direction rather than reversed — the winding is not
110
+ * load-bearing, and a reversed path is the kind of thing that gets tidied up
111
+ * by someone who cannot see why it was backwards.
112
+ */
113
+ function cutoutPath(screenWidth, screenHeight, x, y, width, height, radius) {
114
+ 'worklet';
115
+
116
+ const r = Math.max(0, Math.min(radius, width / 2, height / 2));
117
+ const right = x + width;
118
+ const bottom = y + height;
119
+ return `M0 0H${screenWidth}V${screenHeight}H0Z ` + `M${x + r} ${y}` + `H${right - r}A${r} ${r} 0 0 1 ${right} ${y + r}` + `V${bottom - r}A${r} ${r} 0 0 1 ${right - r} ${bottom}` + `H${x + r}A${r} ${r} 0 0 1 ${x} ${bottom - r}` + `V${y + r}A${r} ${r} 0 0 1 ${x + r} ${y}Z`;
120
+ }
121
+
122
+ /**
123
+ * The target's bounds grown into the shape the hole will take.
124
+ *
125
+ * A circle is squared around the target's centre rather than drawn inside its
126
+ * bounds, because the controls that want one — an avatar, a floating action
127
+ * button — are square already, and squaring off the longer side is what keeps
128
+ * a hole round instead of letting it collapse to a slot.
129
+ */
130
+ function spotlightFor(rect, shape, padding, radius) {
131
+ if (shape === 'circle') {
132
+ const diameter = Math.max(rect.width, rect.height) + padding * 2;
133
+ return {
134
+ x: rect.x + rect.width / 2 - diameter / 2,
135
+ y: rect.y + rect.height / 2 - diameter / 2,
136
+ width: diameter,
137
+ height: diameter,
138
+ radius: diameter / 2
139
+ };
140
+ }
141
+ return {
142
+ x: rect.x - padding,
143
+ y: rect.y - padding,
144
+ width: rect.width + padding * 2,
145
+ height: rect.height + padding * 2,
146
+ radius
147
+ };
148
+ }
149
+ function TourRoot({
150
+ children,
151
+ open,
152
+ defaultOpen = false,
153
+ onOpenChange,
154
+ step,
155
+ defaultStep,
156
+ onStepChange,
157
+ onFinish,
158
+ onSkip,
159
+ padding = DEFAULT_PADDING,
160
+ radius = DEFAULT_RADIUS,
161
+ shape = 'rect',
162
+ placement = 'auto',
163
+ dismissible = true,
164
+ showProgress = true,
165
+ showSkip = true,
166
+ interactive = false,
167
+ overlayColor = DEFAULT_OVERLAY,
168
+ labels,
169
+ cardClassName
170
+ }) {
171
+ const [steps, setSteps] = useState([]);
172
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
173
+ const [internalStep, setInternalStep] = useState(defaultStep ?? null);
174
+ const isOpenControlled = open !== undefined;
175
+ const isStepControlled = step !== undefined;
176
+ const resolvedOpen = isOpenControlled ? open : internalOpen;
177
+ const words = {
178
+ ...DEFAULT_LABELS,
179
+ ...labels
180
+ };
181
+
182
+ /*
183
+ * Steps sort themselves by `order` rather than arriving in it, because the
184
+ * tree decides when each one mounts and a tour that crosses a header, a list
185
+ * and a tab bar mounts them in whatever order those render.
186
+ */
187
+ const register = useCallback(entry => {
188
+ setSteps(current => [...current.filter(other => other.order !== entry.order), entry].sort((a, b) => a.order - b.order));
189
+ }, []);
190
+ const unregister = useCallback(entry => {
191
+ setSteps(current => current.filter(other => other !== entry));
192
+ }, []);
193
+ const context = useMemo(() => ({
194
+ register,
195
+ unregister
196
+ }), [register, unregister]);
197
+ const activeOrder = isStepControlled ? step : internalStep ?? steps[0]?.order ?? null;
198
+ const index = steps.findIndex(entry => entry.order === activeOrder);
199
+ const active = index >= 0 ? steps[index] : undefined;
200
+ const isFirst = index <= 0;
201
+ const isLast = index === steps.length - 1;
202
+ const setOpen = useCallback(next => {
203
+ if (!isOpenControlled) setInternalOpen(next);
204
+ onOpenChange?.(next);
205
+ }, [isOpenControlled, onOpenChange]);
206
+ const goTo = useCallback(order => {
207
+ onStepChange?.(order);
208
+ if (!isStepControlled) setInternalStep(order);
209
+ }, [isStepControlled, onStepChange]);
210
+
211
+ // Reopening starts the tour over rather than resuming where it was ended.
212
+ // Somebody who dismissed a walkthrough and asked for it again wants it from
213
+ // the top; resuming a half-read tour is a state nobody asked to be in.
214
+ useEffect(() => {
215
+ if (resolvedOpen && !isStepControlled) setInternalStep(defaultStep ?? null);
216
+ // eslint-disable-next-line react-hooks/exhaustive-deps
217
+ }, [resolvedOpen]);
218
+ const finish = useCallback(() => {
219
+ setOpen(false);
220
+ onFinish?.();
221
+ }, [setOpen, onFinish]);
222
+ const skip = useCallback(() => {
223
+ setOpen(false);
224
+ onSkip?.();
225
+ }, [setOpen, onSkip]);
226
+ const next = useCallback(() => {
227
+ const following = steps[index + 1];
228
+ if (following) goTo(following.order);else finish();
229
+ }, [steps, index, goTo, finish]);
230
+ const back = useCallback(() => {
231
+ const previous = steps[index - 1];
232
+ if (previous) goTo(previous.order);
233
+ }, [steps, index, goTo]);
234
+ useBackHandler(resolvedOpen && dismissible, skip);
235
+ return /*#__PURE__*/_jsxs(TourContext.Provider, {
236
+ value: context,
237
+ children: [children, resolvedOpen && steps.length > 0 ? /*#__PURE__*/_jsx(Portal, {
238
+ children: /*#__PURE__*/_jsx(TourOverlay, {
239
+ active: active,
240
+ index: index,
241
+ total: steps.length,
242
+ isFirst: isFirst,
243
+ isLast: isLast,
244
+ padding: padding,
245
+ radius: radius,
246
+ shape: shape,
247
+ placement: placement,
248
+ dismissible: dismissible,
249
+ showProgress: showProgress,
250
+ showSkip: showSkip,
251
+ interactive: interactive,
252
+ overlayColor: overlayColor,
253
+ words: words,
254
+ cardClassName: cardClassName,
255
+ onNext: next,
256
+ onBack: back,
257
+ onSkip: skip
258
+ })
259
+ }) : null]
260
+ });
261
+ }
262
+ function TourOverlay({
263
+ active,
264
+ index,
265
+ total,
266
+ isFirst,
267
+ isLast,
268
+ padding,
269
+ radius,
270
+ shape,
271
+ placement,
272
+ dismissible,
273
+ showProgress,
274
+ showSkip,
275
+ interactive,
276
+ overlayColor,
277
+ words,
278
+ cardClassName,
279
+ onNext,
280
+ onBack,
281
+ onSkip
282
+ }) {
283
+ const {
284
+ width: screenWidth,
285
+ height: screenHeight
286
+ } = useWindowDimensions();
287
+ const insets = useSafeAreaInsets();
288
+ const reducedMotion = useReducedMotion();
289
+ const [spot, setSpot] = useState(null);
290
+ const [cardHeight, setCardHeight] = useState(null);
291
+ const stepPadding = active?.padding ?? padding;
292
+ const stepRadius = active?.radius ?? radius;
293
+ const stepShape = active?.shape ?? shape;
294
+
295
+ /*
296
+ * Measured when the step becomes current and again whenever the window
297
+ * changes size. The second half is the part that is easy to leave out and
298
+ * impossible to miss once it is wrong: a rect taken in portrait describes
299
+ * nothing after a rotation, and the hole ends up over the wrong half of a
300
+ * screen the target is no longer on.
301
+ */
302
+ useEffect(() => {
303
+ const target = active?.target.current;
304
+ if (!target) {
305
+ setSpot(null);
306
+ return;
307
+ }
308
+ let cancelled = false;
309
+ // A frame late on purpose: a step whose target was just scrolled back into
310
+ // view is measured where it lands, not where it was leaving.
311
+ const frame = requestAnimationFrame(() => {
312
+ target.measureInWindow((x, y, width, height) => {
313
+ if (cancelled || width === 0 && height === 0) return;
314
+ setSpot(spotlightFor({
315
+ x,
316
+ y,
317
+ width,
318
+ height
319
+ }, stepShape, stepPadding, stepRadius));
320
+ });
321
+ });
322
+ return () => {
323
+ cancelled = true;
324
+ cancelAnimationFrame(frame);
325
+ };
326
+ }, [active, stepShape, stepPadding, stepRadius, screenWidth, screenHeight]);
327
+
328
+ /*
329
+ * The hole's geometry lives on the UI thread so travelling between two
330
+ * targets is one spring rather than a state update per frame. `settled`
331
+ * distinguishes the first target — which appears where it belongs — from
332
+ * every later one, which slides there.
333
+ */
334
+ const x = useSharedValue(0);
335
+ const y = useSharedValue(0);
336
+ const width = useSharedValue(0);
337
+ const height = useSharedValue(0);
338
+ const cornerRadius = useSharedValue(0);
339
+ const settled = useSharedValue(false);
340
+ useEffect(() => {
341
+ // A step with nothing to point at collapses the hole rather than leaving
342
+ // the last one open: the previous target is no longer what is being talked
343
+ // about, and a hole over it says it is.
344
+ if (!spot) {
345
+ width.value = 0;
346
+ height.value = 0;
347
+ settled.value = false;
348
+ return;
349
+ }
350
+ const animate = settled.value && !reducedMotion;
351
+ const to = (value, next) => {
352
+ value.value = animate ? withSpring(next, SPRING) : next;
353
+ };
354
+ to(x, spot.x);
355
+ to(y, spot.y);
356
+ to(width, spot.width);
357
+ to(height, spot.height);
358
+ to(cornerRadius, spot.radius);
359
+ settled.value = true;
360
+ }, [spot, reducedMotion, x, y, width, height, cornerRadius, settled]);
361
+ const pathProps = useAnimatedProps(() => ({
362
+ d: cutoutPath(screenWidth, screenHeight, x.value, y.value, width.value, height.value, cornerRadius.value)
363
+ }));
364
+
365
+ /*
366
+ * A step with no measurable target — a welcome card, or one whose control has
367
+ * gone — gets no hole and a card in the middle of the screen. Dimming the
368
+ * whole screen and saying nothing about where to look is honest; cutting a
369
+ * hole at the origin is not.
370
+ */
371
+ const card = cardFrame({
372
+ spot,
373
+ cardHeight,
374
+ placement: active?.placement ?? placement,
375
+ screenWidth,
376
+ screenHeight,
377
+ insets
378
+ });
379
+ const onCardLayout = event => {
380
+ const measured = event.nativeEvent.layout.height;
381
+ setCardHeight(current => current !== null && Math.abs(current - measured) < 1 ? current : measured);
382
+ };
383
+ return /*#__PURE__*/_jsxs(View, {
384
+ style: StyleSheet.absoluteFill,
385
+ pointerEvents: "box-none",
386
+ children: [/*#__PURE__*/_jsx(TourBackdrop, {
387
+ interactive: interactive,
388
+ dismissible: dismissible,
389
+ spot: spot,
390
+ screenWidth: screenWidth,
391
+ screenHeight: screenHeight,
392
+ onDismiss: onSkip
393
+ }), /*#__PURE__*/_jsx(Animated.View, {
394
+ pointerEvents: "none",
395
+ style: StyleSheet.absoluteFill,
396
+ entering: reducedMotion ? undefined : FadeIn.duration(180),
397
+ exiting: reducedMotion ? undefined : FadeOut.duration(140),
398
+ children: /*#__PURE__*/_jsx(Svg, {
399
+ width: screenWidth,
400
+ height: screenHeight,
401
+ children: /*#__PURE__*/_jsx(AnimatedPath, {
402
+ animatedProps: pathProps,
403
+ fill: overlayColor,
404
+ fillRule: "evenodd"
405
+ })
406
+ })
407
+ }), /*#__PURE__*/_jsx(Animated.View, {
408
+ entering: reducedMotion ? undefined : FadeIn.duration(200),
409
+ style: {
410
+ position: 'absolute',
411
+ left: card.left,
412
+ top: card.top,
413
+ width: card.width
414
+ },
415
+ children: /*#__PURE__*/_jsxs(View, {
416
+ onLayout: onCardLayout,
417
+ accessibilityViewIsModal: true,
418
+ accessibilityLiveRegion: "polite",
419
+ style: {
420
+ // Held off the first frame's opacity rather than off the screen:
421
+ // the card has to be laid out to be measured, and its height is
422
+ // what decides whether it goes above the target or below it.
423
+ opacity: cardHeight === null ? 0 : 1
424
+ },
425
+ className: cn('gap-3 rounded-2xl border border-border bg-overlay p-4 shadow-lg', cardClassName),
426
+ children: [dismissible ? /*#__PURE__*/_jsx(Button, {
427
+ variant: "ghost",
428
+ size: "icon",
429
+ accessibilityLabel: words.close,
430
+ onPress: onSkip,
431
+ className: "absolute end-1 top-1 h-9 w-9",
432
+ children: /*#__PURE__*/_jsx(XIcon, {
433
+ size: 16
434
+ })
435
+ }) : null, /*#__PURE__*/_jsxs(View, {
436
+ className: "gap-1 pe-8",
437
+ children: [showProgress && total > 1 && index >= 0 ? /*#__PURE__*/_jsx(Text, {
438
+ size: "xs",
439
+ muted: true,
440
+ accessibilityLabel: `Step ${index + 1} of ${total}`,
441
+ children: `${index + 1} of ${total}`
442
+ }) : null, active?.title ? /*#__PURE__*/_jsx(Text, {
443
+ accessibilityRole: "header",
444
+ weight: "semibold",
445
+ className: "text-overlay-foreground",
446
+ children: active.title
447
+ }) : null, active?.description ? /*#__PURE__*/_jsx(Text, {
448
+ size: "sm",
449
+ muted: true,
450
+ children: active.description
451
+ }) : null]
452
+ }), /*#__PURE__*/_jsxs(View, {
453
+ className: "flex-row items-center justify-between gap-2",
454
+ children: [/*#__PURE__*/_jsx(View, {
455
+ className: "flex-row items-center gap-1",
456
+ children: showSkip && !isLast ? /*#__PURE__*/_jsx(Button, {
457
+ variant: "ghost",
458
+ size: "sm",
459
+ onPress: onSkip,
460
+ children: words.skip
461
+ }) : null
462
+ }), /*#__PURE__*/_jsxs(View, {
463
+ className: "flex-row items-center gap-2",
464
+ children: [!isFirst ? /*#__PURE__*/_jsx(Button, {
465
+ variant: "outline",
466
+ size: "sm",
467
+ onPress: onBack,
468
+ startContent: /*#__PURE__*/_jsx(ChevronLeftIcon, {
469
+ size: 16
470
+ }),
471
+ children: words.back
472
+ }) : null, /*#__PURE__*/_jsx(Button, {
473
+ variant: "primary",
474
+ size: "sm",
475
+ onPress: onNext,
476
+ children: isLast ? words.done : words.next
477
+ })]
478
+ })]
479
+ })]
480
+ })
481
+ }, active?.order ?? 'none')]
482
+ });
483
+ }
484
+
485
+ /**
486
+ * Where the card goes, given the hole and the card's own height.
487
+ *
488
+ * Below the target when below fits, above it when it does not, and centred on
489
+ * the screen when there is no target at all. The card is as wide as the safe
490
+ * area allows up to a ceiling, because a card narrower than that on a phone
491
+ * only means a shorter line length and one more thing to get wrong.
492
+ */
493
+ function cardFrame({
494
+ spot,
495
+ cardHeight,
496
+ placement,
497
+ screenWidth,
498
+ screenHeight,
499
+ insets
500
+ }) {
501
+ const minX = insets.left + SCREEN_MARGIN;
502
+ const maxX = screenWidth - insets.right - SCREEN_MARGIN;
503
+ const width = Math.min(maxX - minX, MAX_CARD_WIDTH);
504
+ const left = minX + (maxX - minX - width) / 2;
505
+ const minY = insets.top + SCREEN_MARGIN;
506
+ const maxY = screenHeight - insets.bottom - SCREEN_MARGIN;
507
+ const height = cardHeight ?? 0;
508
+ if (!spot) {
509
+ return {
510
+ left,
511
+ top: Math.max(minY, (screenHeight - height) / 2),
512
+ width
513
+ };
514
+ }
515
+ const below = spot.y + spot.height + CARD_OFFSET;
516
+ const above = spot.y - CARD_OFFSET - height;
517
+ const fitsBelow = below + height <= maxY;
518
+ const fitsAbove = above >= minY;
519
+ const goBelow = placement === 'bottom' ? fitsBelow || !fitsAbove : placement === 'top' ? !fitsAbove : fitsBelow;
520
+
521
+ // Neither side fits — a target taller than the room around it. Clamping keeps
522
+ // the card on screen and lets it overlap the dim rather than the other way
523
+ // round, which is the lesser of the two failures.
524
+ const top = goBelow ? Math.min(below, maxY - height) : Math.max(above, minY);
525
+ return {
526
+ left,
527
+ top: Math.max(minY, top),
528
+ width
529
+ };
530
+ }
531
+
532
+ /**
533
+ * The layer that takes the touches the dim does not.
534
+ *
535
+ * One Pressable over everything, or four around the cutout when the target has
536
+ * to stay usable. Four rather than one with a hole, because a view cannot have
537
+ * a hole — the gap between them is the hole, and it is the only construction
538
+ * that leaves a rectangle of the screen reachable.
539
+ */
540
+ function TourBackdrop({
541
+ interactive,
542
+ dismissible,
543
+ spot,
544
+ screenWidth,
545
+ screenHeight,
546
+ onDismiss
547
+ }) {
548
+ const blocking = {
549
+ onStartShouldSetResponder: () => true
550
+ };
551
+ const press = dismissible ? {
552
+ onStartShouldSetResponder: () => true,
553
+ onResponderRelease: onDismiss
554
+ } : blocking;
555
+ if (!interactive || !spot) {
556
+ return /*#__PURE__*/_jsx(View, {
557
+ style: StyleSheet.absoluteFill,
558
+ ...press
559
+ });
560
+ }
561
+ const bottom = spot.y + spot.height;
562
+ const right = spot.x + spot.width;
563
+ return /*#__PURE__*/_jsxs(_Fragment, {
564
+ children: [/*#__PURE__*/_jsx(View, {
565
+ style: {
566
+ position: 'absolute',
567
+ left: 0,
568
+ top: 0,
569
+ right: 0,
570
+ height: Math.max(0, spot.y)
571
+ },
572
+ ...press
573
+ }), /*#__PURE__*/_jsx(View, {
574
+ style: {
575
+ position: 'absolute',
576
+ left: 0,
577
+ top: bottom,
578
+ right: 0,
579
+ height: Math.max(0, screenHeight - bottom)
580
+ },
581
+ ...press
582
+ }), /*#__PURE__*/_jsx(View, {
583
+ style: {
584
+ position: 'absolute',
585
+ left: 0,
586
+ top: spot.y,
587
+ width: Math.max(0, spot.x),
588
+ height: spot.height
589
+ },
590
+ ...press
591
+ }), /*#__PURE__*/_jsx(View, {
592
+ style: {
593
+ position: 'absolute',
594
+ left: right,
595
+ top: spot.y,
596
+ width: Math.max(0, screenWidth - right),
597
+ height: spot.height
598
+ },
599
+ ...press
600
+ })]
601
+ });
602
+ }
603
+ /**
604
+ * Wraps the control a step is about, and is what gets measured.
605
+ *
606
+ * The child is wrapped in a view rather than handed a ref, because the ref has
607
+ * to survive whatever the child is — a button, a card, a tab bar — and only a
608
+ * wrapper we own is guaranteed to be measurable. That wrapper is a plain view
609
+ * with no sizing of its own, so it takes the width its parent gives it: put
610
+ * layout classes on the step rather than on the child, the way you would on any
611
+ * other view in that position.
612
+ *
613
+ * It renders its child and nothing else while the tour is closed, and stays
614
+ * mounted either way — a step is a description of a control that is already on
615
+ * the screen, not something that appears with the walkthrough.
616
+ */
617
+ function TourStep({
618
+ order,
619
+ title,
620
+ description,
621
+ shape,
622
+ padding,
623
+ radius,
624
+ placement,
625
+ className,
626
+ children,
627
+ ...props
628
+ }) {
629
+ const {
630
+ register,
631
+ unregister
632
+ } = useTour('Tour.Step');
633
+ const target = useRef(null);
634
+ const entry = useMemo(() => ({
635
+ order,
636
+ title,
637
+ description,
638
+ shape,
639
+ padding,
640
+ radius,
641
+ placement,
642
+ target
643
+ }), [order, title, description, shape, padding, radius, placement]);
644
+ useEffect(() => {
645
+ register(entry);
646
+ return () => unregister(entry);
647
+ }, [entry, register, unregister]);
648
+ return /*#__PURE__*/_jsx(View, {
649
+ ref: target,
650
+ collapsable: false,
651
+ className: className,
652
+ ...props,
653
+ children: children
654
+ });
655
+ }
656
+ TourStep.displayName = 'Tour.Step';
657
+ TourRoot.displayName = 'Tour';
658
+ export const Tour = Object.assign(TourRoot, {
659
+ Step: TourStep
660
+ });
661
+ //# sourceMappingURL=index.js.map