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.
- package/README.md +1 -0
- package/lib/module/components/combobox/index.js +61 -7
- package/lib/module/components/combobox/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +765 -0
- package/lib/module/components/hex-chart/index.js.map +1 -0
- package/lib/module/components/sortable/index.js +74 -12
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/steps/index.js +95 -12
- package/lib/module/components/steps/index.js.map +1 -1
- package/lib/module/components/tag-input/index.js +435 -0
- package/lib/module/components/tag-input/index.js.map +1 -0
- package/lib/module/components/tour/index.js +661 -0
- package/lib/module/components/tour/index.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +209 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts +277 -0
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/steps/index.d.ts +19 -0
- package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tag-input/index.d.ts +240 -0
- package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tour/index.d.ts +168 -0
- package/lib/typescript/src/components/tour/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +98 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/hex-chart/index.tsx +1009 -0
- package/src/components/steps/index.tsx +103 -8
- package/src/components/tour/index.tsx +933 -0
- package/src/index.ts +22 -0
- 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
|