@coinbase/cds-mcp-server 8.27.0 → 8.27.1
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/CHANGELOG.md +4 -0
- package/mcp-docs/mobile/components/Tour.txt +388 -49
- package/mcp-docs/web/components/Tour.txt +411 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ All notable changes to this project will be documented in this file.
|
|
|
8
8
|
|
|
9
9
|
<!-- template-start -->
|
|
10
10
|
|
|
11
|
+
## 8.27.1 ((12/4/2025, 06:51 AM PST))
|
|
12
|
+
|
|
13
|
+
This is an artificial version bump with no new change.
|
|
14
|
+
|
|
11
15
|
## 8.27.0 ((12/3/2025, 09:30 AM PST))
|
|
12
16
|
|
|
13
17
|
This is an artificial version bump with no new change.
|
|
@@ -10,25 +10,32 @@ import { Tour, TourStep } from '@coinbase/cds-mobile/tour'
|
|
|
10
10
|
|
|
11
11
|
## Examples
|
|
12
12
|
|
|
13
|
+
The Tour component guides users through your app with step-by-step coachmarks.
|
|
14
|
+
You define tour steps with unique IDs and wrap target elements with `TourStep` components.
|
|
15
|
+
|
|
13
16
|
### Basic usage
|
|
14
17
|
|
|
15
|
-
```
|
|
16
|
-
function
|
|
18
|
+
```jsx
|
|
19
|
+
function BasicTourExample() {
|
|
17
20
|
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
18
21
|
|
|
19
22
|
const StepOne = () => {
|
|
20
23
|
const [checked, setChecked] = useState(false);
|
|
21
|
-
|
|
22
24
|
const { goNextTourStep, stopTour } = useTourContext();
|
|
23
25
|
|
|
24
26
|
return (
|
|
25
27
|
<Coachmark
|
|
26
|
-
action={
|
|
28
|
+
action={
|
|
29
|
+
<Button compact onPress={goNextTourStep} variant="secondary">
|
|
30
|
+
Next
|
|
31
|
+
</Button>
|
|
32
|
+
}
|
|
27
33
|
checkbox={
|
|
28
34
|
<Checkbox checked={checked} onChange={setChecked}>
|
|
29
|
-
Don
|
|
35
|
+
Don't show again
|
|
30
36
|
</Checkbox>
|
|
31
37
|
}
|
|
38
|
+
closeButtonAccessibilityLabel="Close"
|
|
32
39
|
content="Add up to 3 lines of body copy. Deliver your message with clarity and impact"
|
|
33
40
|
onClose={stopTour}
|
|
34
41
|
title="My first step"
|
|
@@ -40,16 +47,21 @@ function Example() {
|
|
|
40
47
|
const { goNextTourStep, stopTour } = useTourContext();
|
|
41
48
|
return (
|
|
42
49
|
<Coachmark
|
|
43
|
-
action={
|
|
50
|
+
action={
|
|
51
|
+
<Button compact onPress={goNextTourStep} variant="secondary">
|
|
52
|
+
Next
|
|
53
|
+
</Button>
|
|
54
|
+
}
|
|
55
|
+
closeButtonAccessibilityLabel="Close"
|
|
44
56
|
content={
|
|
45
57
|
<VStack gap={2}>
|
|
46
|
-
<
|
|
58
|
+
<Text color="fgMuted" font="caption">
|
|
47
59
|
50%
|
|
48
|
-
</
|
|
60
|
+
</Text>
|
|
49
61
|
<ProgressBar progress={0.5} />
|
|
50
|
-
<
|
|
62
|
+
<Text font="body">
|
|
51
63
|
Add up to 3 lines of body copy. Deliver your message with clarity and impact
|
|
52
|
-
</
|
|
64
|
+
</Text>
|
|
53
65
|
</VStack>
|
|
54
66
|
}
|
|
55
67
|
media={<RemoteImage height={150} source={ethBackground} width="100%" />}
|
|
@@ -65,9 +77,15 @@ function Example() {
|
|
|
65
77
|
<Coachmark
|
|
66
78
|
action={
|
|
67
79
|
<HStack gap={1}>
|
|
68
|
-
<Button
|
|
69
|
-
|
|
70
|
-
|
|
80
|
+
<Button compact onPress={goPreviousTourStep} variant="secondary">
|
|
81
|
+
Back
|
|
82
|
+
</Button>
|
|
83
|
+
<Button compact onPress={goNextTourStep} variant="secondary">
|
|
84
|
+
Next
|
|
85
|
+
</Button>
|
|
86
|
+
<Button compact onPress={stopTour} variant="secondary">
|
|
87
|
+
Done
|
|
88
|
+
</Button>
|
|
71
89
|
</HStack>
|
|
72
90
|
}
|
|
73
91
|
content="Add up to 3 lines of body copy. Deliver your message with clarity and impact"
|
|
@@ -77,67 +95,388 @@ function Example() {
|
|
|
77
95
|
);
|
|
78
96
|
};
|
|
79
97
|
|
|
98
|
+
const tourSteps = [
|
|
99
|
+
{ id: 'step1', Component: StepOne },
|
|
100
|
+
{ id: 'step2', Component: StepTwo },
|
|
101
|
+
{ id: 'step3', Component: StepThree },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const TourContent = () => {
|
|
105
|
+
const { startTour } = useTourContext();
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<VStack flexGrow={1} gap={2} justifyContent="space-between">
|
|
109
|
+
<Button onPress={() => startTour()} compact>
|
|
110
|
+
Start tour
|
|
111
|
+
</Button>
|
|
112
|
+
<TourStep id="step1">
|
|
113
|
+
<Box background="bgSecondary" padding={4}>
|
|
114
|
+
<Text>This is step 1</Text>
|
|
115
|
+
</Box>
|
|
116
|
+
</TourStep>
|
|
117
|
+
<Box height={300} />
|
|
118
|
+
<TourStep id="step2">
|
|
119
|
+
<Box background="bgSecondary" padding={4} width={150}>
|
|
120
|
+
<Text>This is step 2</Text>
|
|
121
|
+
</Box>
|
|
122
|
+
</TourStep>
|
|
123
|
+
<Box height={300} />
|
|
124
|
+
<TourStep id="step3">
|
|
125
|
+
<VStack background="bgSecondary" padding={4} width={150}>
|
|
126
|
+
<Text>This is step 3</Text>
|
|
127
|
+
</VStack>
|
|
128
|
+
</TourStep>
|
|
129
|
+
</VStack>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Tour activeTourStep={activeTourStep} onChange={setActiveTourStep} steps={tourSteps}>
|
|
135
|
+
<TourContent />
|
|
136
|
+
</Tour>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Coachmarks can contain rich content including images, progress indicators, and custom layouts.
|
|
142
|
+
|
|
143
|
+
```jsx
|
|
144
|
+
function RichContentExample() {
|
|
145
|
+
const RichStep = () => {
|
|
146
|
+
const { goNextTourStep, stopTour } = useTourContext();
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Coachmark
|
|
150
|
+
action={
|
|
151
|
+
<Button compact onPress={goNextTourStep} variant="secondary">
|
|
152
|
+
Continue
|
|
153
|
+
</Button>
|
|
154
|
+
}
|
|
155
|
+
closeButtonAccessibilityLabel="Close"
|
|
156
|
+
content={
|
|
157
|
+
<VStack gap={2}>
|
|
158
|
+
<Text color="fgMuted" font="caption">
|
|
159
|
+
Step 2 of 4
|
|
160
|
+
</Text>
|
|
161
|
+
<ProgressBar progress={0.5} />
|
|
162
|
+
<Text font="body">
|
|
163
|
+
This step showcases how you can include rich content like progress bars and images.
|
|
164
|
+
</Text>
|
|
165
|
+
</VStack>
|
|
166
|
+
}
|
|
167
|
+
media={
|
|
168
|
+
<Image
|
|
169
|
+
accessibilityIgnoresInvertColors
|
|
170
|
+
source={{ uri: 'https://example.com/feature-image.png' }}
|
|
171
|
+
style={{ width: '100%', height: 150 }}
|
|
172
|
+
/>
|
|
173
|
+
}
|
|
174
|
+
onClose={stopTour}
|
|
175
|
+
title="Rich Content Example"
|
|
176
|
+
/>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
You can use TypeScript string literal types to ensure type safety for your step IDs.
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
type StepId = 'intro' | 'feature-highlight' | 'call-to-action';
|
|
186
|
+
|
|
187
|
+
function TypeSafeTourExample() {
|
|
188
|
+
const [activeTourStep, setActiveTourStep] = useState<TourStepValue<StepId> | null>(null);
|
|
189
|
+
|
|
190
|
+
const tourSteps: TourStepValue<StepId>[] = [
|
|
191
|
+
{ id: 'intro', Component: IntroStep },
|
|
192
|
+
{ id: 'feature-highlight', Component: FeatureStep },
|
|
193
|
+
{ id: 'call-to-action', Component: CTAStep },
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Tour activeTourStep={activeTourStep} onChange={setActiveTourStep} steps={tourSteps}>
|
|
198
|
+
<TourStep id="intro">
|
|
199
|
+
<IntroContent />
|
|
200
|
+
</TourStep>
|
|
201
|
+
<TourStep id="feature-highlight">
|
|
202
|
+
<FeatureContent />
|
|
203
|
+
</TourStep>
|
|
204
|
+
{/* TypeScript error if id doesn't match StepId type */}
|
|
205
|
+
<TourStep id="call-to-action">
|
|
206
|
+
<CTAContent />
|
|
207
|
+
</TourStep>
|
|
208
|
+
</Tour>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Scrolling
|
|
214
|
+
|
|
215
|
+
When tour steps are off-screen, you can use the `onBeforeActive` callback to scroll them into view.
|
|
216
|
+
This callback runs before the step becomes active and can be async.
|
|
217
|
+
|
|
218
|
+
```jsx
|
|
219
|
+
function ScrollingTourExample() {
|
|
220
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
221
|
+
const scrollViewRef = useRef(null);
|
|
222
|
+
const step2Ref = useRef(null);
|
|
223
|
+
const step3Ref = useRef(null);
|
|
224
|
+
|
|
225
|
+
// Helper function to scroll an element into view
|
|
226
|
+
const scrollIntoView = async (scrollViewRef, elementRef) => {
|
|
227
|
+
const scrollView = scrollViewRef.current;
|
|
228
|
+
if (!scrollView) return;
|
|
229
|
+
elementRef.current?.measureLayout(scrollView, (x, y) => {
|
|
230
|
+
scrollView.scrollTo({ x, y, animated: true });
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
80
234
|
const tourSteps = [
|
|
81
235
|
{
|
|
82
236
|
id: 'step1',
|
|
83
|
-
onBeforeActive: () => console.log('step1 before'),
|
|
84
237
|
Component: StepOne,
|
|
85
238
|
},
|
|
86
239
|
{
|
|
87
240
|
id: 'step2',
|
|
88
|
-
onBeforeActive: () =>
|
|
241
|
+
onBeforeActive: async () => {
|
|
242
|
+
// Scroll step 2 into view before showing the coachmark
|
|
243
|
+
await scrollIntoView(scrollViewRef, step2Ref);
|
|
244
|
+
},
|
|
89
245
|
Component: StepTwo,
|
|
90
246
|
},
|
|
91
247
|
{
|
|
92
248
|
id: 'step3',
|
|
93
|
-
onBeforeActive: () =>
|
|
249
|
+
onBeforeActive: async () => {
|
|
250
|
+
await scrollIntoView(scrollViewRef, step3Ref);
|
|
251
|
+
},
|
|
94
252
|
Component: StepThree,
|
|
95
253
|
},
|
|
96
254
|
];
|
|
97
255
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const handlePress = useCallback(() => startTour(), [startTour]);
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
<VStack flexGrow={1} gap={2} justifyContent="space-between">
|
|
105
|
-
<Button onPress={handlePress} compact label="Start tour" />
|
|
256
|
+
return (
|
|
257
|
+
<Tour activeTourStep={activeTourStep} onChange={setActiveTourStep} steps={tourSteps}>
|
|
258
|
+
<ScrollView ref={scrollViewRef} style={{ flex: 1 }}>
|
|
106
259
|
<TourStep id="step1">
|
|
107
|
-
<Box
|
|
108
|
-
<Text>
|
|
260
|
+
<Box background="bgSecondary" padding={4}>
|
|
261
|
+
<Text>Step 1 - visible on load</Text>
|
|
109
262
|
</Box>
|
|
110
263
|
</TourStep>
|
|
111
|
-
<Box height={
|
|
112
|
-
<
|
|
113
|
-
<Box
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
264
|
+
<Box height={1000} />
|
|
265
|
+
<TourStep id="step2">
|
|
266
|
+
<Box ref={step2Ref} background="bgSecondary" padding={4}>
|
|
267
|
+
<Text>Step 2 - requires scroll</Text>
|
|
268
|
+
</Box>
|
|
269
|
+
</TourStep>
|
|
270
|
+
<Box height={1000} />
|
|
271
|
+
<TourStep id="step3">
|
|
272
|
+
<Box ref={step3Ref} background="bgSecondary" padding={4}>
|
|
273
|
+
<Text>Step 3 - requires more scroll</Text>
|
|
274
|
+
</Box>
|
|
275
|
+
</TourStep>
|
|
276
|
+
</ScrollView>
|
|
277
|
+
</Tour>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Customization
|
|
283
|
+
|
|
284
|
+
#### Overlay
|
|
285
|
+
|
|
286
|
+
You can hide the dimmed overlay behind the coachmark using the `hideOverlay` prop.
|
|
287
|
+
This can be set globally on the `Tour` component or per-step.
|
|
288
|
+
|
|
289
|
+
```jsx
|
|
290
|
+
function HideOverlayExample() {
|
|
291
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
292
|
+
|
|
293
|
+
const tourSteps = [
|
|
294
|
+
{
|
|
295
|
+
id: 'step1',
|
|
296
|
+
Component: StepOne,
|
|
297
|
+
// Hide overlay for just this step
|
|
298
|
+
hideOverlay: true,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: 'step2',
|
|
302
|
+
Component: StepTwo,
|
|
303
|
+
},
|
|
304
|
+
];
|
|
132
305
|
|
|
133
306
|
return (
|
|
134
|
-
|
|
135
|
-
|
|
307
|
+
// Or hide overlay for all steps
|
|
308
|
+
<Tour
|
|
309
|
+
hideOverlay
|
|
310
|
+
activeTourStep={activeTourStep}
|
|
311
|
+
onChange={setActiveTourStep}
|
|
312
|
+
steps={tourSteps}
|
|
313
|
+
>
|
|
314
|
+
...
|
|
136
315
|
</Tour>
|
|
137
316
|
);
|
|
138
317
|
}
|
|
139
318
|
```
|
|
140
319
|
|
|
320
|
+
#### Mask
|
|
321
|
+
|
|
322
|
+
Customize the mask (cutout) around the highlighted element with padding and border radius.
|
|
323
|
+
|
|
324
|
+
```jsx
|
|
325
|
+
function MaskCustomizationExample() {
|
|
326
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
327
|
+
|
|
328
|
+
const tourSteps = [
|
|
329
|
+
{
|
|
330
|
+
id: 'step1',
|
|
331
|
+
Component: StepOne,
|
|
332
|
+
// Per-step mask customization
|
|
333
|
+
tourMaskPadding: 16,
|
|
334
|
+
tourMaskBorderRadius: 12,
|
|
335
|
+
},
|
|
336
|
+
{ id: 'step2', Component: StepTwo },
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<Tour
|
|
341
|
+
activeTourStep={activeTourStep}
|
|
342
|
+
onChange={setActiveTourStep}
|
|
343
|
+
steps={tourSteps}
|
|
344
|
+
tourMaskPadding={8}
|
|
345
|
+
tourMaskBorderRadius={8}
|
|
346
|
+
>
|
|
347
|
+
...
|
|
348
|
+
</Tour>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Positioning
|
|
354
|
+
|
|
355
|
+
The Tour component uses `@floating-ui` to position coachmarks relative to their target elements.
|
|
356
|
+
You can customize positioning with the `tourStepOffset`, `tourStepShift`, and `tourStepAutoPlacement` props.
|
|
357
|
+
|
|
358
|
+
```jsx
|
|
359
|
+
function PositioningExample() {
|
|
360
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
361
|
+
|
|
362
|
+
const tourSteps = [
|
|
363
|
+
{ id: 'step1', Component: StepOne },
|
|
364
|
+
{ id: 'step2', Component: StepTwo },
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<Tour
|
|
369
|
+
activeTourStep={activeTourStep}
|
|
370
|
+
onChange={setActiveTourStep}
|
|
371
|
+
steps={tourSteps}
|
|
372
|
+
tourStepOffset={32}
|
|
373
|
+
tourStepShift={{ padding: 16 }}
|
|
374
|
+
>
|
|
375
|
+
...
|
|
376
|
+
</Tour>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Arrow
|
|
382
|
+
|
|
383
|
+
You can customize the arrow that points to the target element by providing a custom `TourStepArrowComponent`.
|
|
384
|
+
|
|
385
|
+
```jsx
|
|
386
|
+
function CustomArrowExample() {
|
|
387
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
388
|
+
|
|
389
|
+
// Custom arrow component (must forward ref)
|
|
390
|
+
const CustomArrow = memo(
|
|
391
|
+
forwardRef((props, ref) => {
|
|
392
|
+
return <DefaultTourStepArrow {...props} ref={ref} style={{ color: 'yellow' }} />;
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const tourSteps = [
|
|
397
|
+
{
|
|
398
|
+
id: 'step1',
|
|
399
|
+
Component: StepOne,
|
|
400
|
+
// Per-step custom arrow
|
|
401
|
+
ArrowComponent: CustomArrow,
|
|
402
|
+
arrowStyle: { color: 'red' },
|
|
403
|
+
},
|
|
404
|
+
{ id: 'step2', Component: StepTwo },
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<Tour
|
|
409
|
+
activeTourStep={activeTourStep}
|
|
410
|
+
onChange={setActiveTourStep}
|
|
411
|
+
steps={tourSteps}
|
|
412
|
+
TourStepArrowComponent={CustomArrow}
|
|
413
|
+
>
|
|
414
|
+
...
|
|
415
|
+
</Tour>
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Accessibility
|
|
421
|
+
|
|
422
|
+
Always provide accessibility labels for close buttons using the `closeButtonAccessibilityLabel` prop and ensure coachmarks are navigable.
|
|
423
|
+
|
|
424
|
+
```jsx
|
|
425
|
+
function AccessibleTourExample() {
|
|
426
|
+
const AccessibleStep = () => {
|
|
427
|
+
const { goNextTourStep, stopTour } = useTourContext();
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<Coachmark
|
|
431
|
+
action={
|
|
432
|
+
<Button
|
|
433
|
+
accessibilityHint="Advances to the next step in the tour"
|
|
434
|
+
compact
|
|
435
|
+
onPress={goNextTourStep}
|
|
436
|
+
variant="secondary"
|
|
437
|
+
>
|
|
438
|
+
Next
|
|
439
|
+
</Button>
|
|
440
|
+
}
|
|
441
|
+
closeButtonAccessibilityLabel="Close tour and return to main content"
|
|
442
|
+
content="This coachmark has proper accessibility labels for screen readers."
|
|
443
|
+
onClose={stopTour}
|
|
444
|
+
title="Accessible Step"
|
|
445
|
+
/>
|
|
446
|
+
);
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Callbacks
|
|
452
|
+
|
|
453
|
+
Use the `onBeforeActive` callback to perform actions before a step becomes active,
|
|
454
|
+
such as scrolling, fetching data, or preparing the UI.
|
|
455
|
+
|
|
456
|
+
```jsx
|
|
457
|
+
function CallbacksExample() {
|
|
458
|
+
const tourSteps = [
|
|
459
|
+
{
|
|
460
|
+
id: 'step1',
|
|
461
|
+
onBeforeActive: () => {
|
|
462
|
+
console.log('Step 1 is about to become active');
|
|
463
|
+
// Perform any setup needed
|
|
464
|
+
},
|
|
465
|
+
Component: StepOne,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: 'step2',
|
|
469
|
+
onBeforeActive: async () => {
|
|
470
|
+
// Async operations are supported
|
|
471
|
+
await someAsyncSetup();
|
|
472
|
+
console.log('Step 2 setup complete');
|
|
473
|
+
},
|
|
474
|
+
Component: StepTwo,
|
|
475
|
+
},
|
|
476
|
+
];
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
141
480
|
## Props
|
|
142
481
|
|
|
143
482
|
| Prop | Type | Required | Default | Description |
|
|
@@ -10,6 +10,9 @@ import { Tour, TourStep } from '@coinbase/cds-web/tour'
|
|
|
10
10
|
|
|
11
11
|
## Examples
|
|
12
12
|
|
|
13
|
+
The Tour component guides users through your app with step-by-step coachmarks.
|
|
14
|
+
You define tour steps with unique IDs and wrap target elements with `TourStep` components.
|
|
15
|
+
|
|
13
16
|
### Basic usage
|
|
14
17
|
|
|
15
18
|
```jsx live
|
|
@@ -56,13 +59,13 @@ function Example() {
|
|
|
56
59
|
}
|
|
57
60
|
content={
|
|
58
61
|
<VStack gap={2}>
|
|
59
|
-
<
|
|
62
|
+
<Text as="p" color="fgMuted" font="caption">
|
|
60
63
|
50%
|
|
61
|
-
</
|
|
64
|
+
</Text>
|
|
62
65
|
<ProgressBar progress={0.5} />
|
|
63
|
-
<
|
|
66
|
+
<Text as="p" font="body">
|
|
64
67
|
Add up to 3 lines of body copy. Deliver your message with clarity and impact
|
|
65
|
-
</
|
|
68
|
+
</Text>
|
|
66
69
|
</VStack>
|
|
67
70
|
}
|
|
68
71
|
media={<RemoteImage height={150} source={ethBackground} width="100%" />}
|
|
@@ -73,7 +76,7 @@ function Example() {
|
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
const StepThree = () => {
|
|
76
|
-
const { stopTour,
|
|
79
|
+
const { stopTour, goPreviousTourStep } = useTourContext();
|
|
77
80
|
return (
|
|
78
81
|
<Coachmark
|
|
79
82
|
action={
|
|
@@ -122,16 +125,20 @@ function Example() {
|
|
|
122
125
|
Start tour
|
|
123
126
|
</Button>
|
|
124
127
|
<TourStep id="step1">
|
|
125
|
-
<Box background="
|
|
126
|
-
<
|
|
128
|
+
<Box background="bgSecondary" padding={4}>
|
|
129
|
+
<Text as="p" font="body">
|
|
130
|
+
This is step 1
|
|
131
|
+
</Text>
|
|
127
132
|
</Box>
|
|
128
133
|
</TourStep>
|
|
129
134
|
<Box height={spacerHeightIncrement} />
|
|
130
135
|
<HStack justifyContent="flex-end">
|
|
131
136
|
<Box flexShrink={0} width={spacerWidthIncrement} />
|
|
132
137
|
<TourStep id="step2">
|
|
133
|
-
<Box background="
|
|
134
|
-
<
|
|
138
|
+
<Box background="bgSecondary" padding={4} width={150}>
|
|
139
|
+
<Text as="p" font="body">
|
|
140
|
+
This is step 2
|
|
141
|
+
</Text>
|
|
135
142
|
</Box>
|
|
136
143
|
</TourStep>
|
|
137
144
|
</HStack>
|
|
@@ -139,8 +146,10 @@ function Example() {
|
|
|
139
146
|
<HStack>
|
|
140
147
|
<Box flexShrink={0} width={spacerWidthIncrement * 2} />
|
|
141
148
|
<TourStep id="step3">
|
|
142
|
-
<VStack background="
|
|
143
|
-
<
|
|
149
|
+
<VStack background="bgSecondary" padding={4} width={150}>
|
|
150
|
+
<Text as="p" font="body">
|
|
151
|
+
This is step 3
|
|
152
|
+
</Text>
|
|
144
153
|
</VStack>
|
|
145
154
|
</TourStep>
|
|
146
155
|
</HStack>
|
|
@@ -156,6 +165,397 @@ function Example() {
|
|
|
156
165
|
}
|
|
157
166
|
```
|
|
158
167
|
|
|
168
|
+
You can use TypeScript string literal types to ensure type safety for your step IDs.
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
type StepId = 'intro' | 'feature-highlight' | 'call-to-action';
|
|
172
|
+
|
|
173
|
+
function TypeSafeTourExample() {
|
|
174
|
+
const [activeTourStep, setActiveTourStep] = useState<TourStepValue<StepId> | null>(null);
|
|
175
|
+
|
|
176
|
+
const tourSteps: TourStepValue<StepId>[] = [
|
|
177
|
+
{ id: 'intro', Component: IntroStep },
|
|
178
|
+
{ id: 'feature-highlight', Component: FeatureStep },
|
|
179
|
+
{ id: 'call-to-action', Component: CTAStep },
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Tour activeTourStep={activeTourStep} onChange={setActiveTourStep} steps={tourSteps}>
|
|
184
|
+
<TourStep id="intro">
|
|
185
|
+
<IntroContent />
|
|
186
|
+
</TourStep>
|
|
187
|
+
<TourStep id="feature-highlight">
|
|
188
|
+
<FeatureContent />
|
|
189
|
+
</TourStep>
|
|
190
|
+
{/* TypeScript error if id doesn't match StepId type */}
|
|
191
|
+
<TourStep id="call-to-action">
|
|
192
|
+
<CTAContent />
|
|
193
|
+
</TourStep>
|
|
194
|
+
</Tour>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Scrolling
|
|
200
|
+
|
|
201
|
+
The Tour component automatically scrolls to bring off-screen targets into view.
|
|
202
|
+
You can customize this behavior with the `scrollOptions` prop or disable it entirely with `disableAutoScroll`.
|
|
203
|
+
|
|
204
|
+
```jsx
|
|
205
|
+
function ScrollingExample() {
|
|
206
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
207
|
+
|
|
208
|
+
const tourSteps = [
|
|
209
|
+
{ id: 'step1', Component: StepOne },
|
|
210
|
+
{
|
|
211
|
+
id: 'step2',
|
|
212
|
+
// Disable auto-scroll for just this step
|
|
213
|
+
disableAutoScroll: true,
|
|
214
|
+
Component: StepTwo,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'step3',
|
|
218
|
+
// Custom scroll options for this step
|
|
219
|
+
scrollOptions: {
|
|
220
|
+
behavior: 'smooth',
|
|
221
|
+
marginX: 50,
|
|
222
|
+
marginY: 150,
|
|
223
|
+
},
|
|
224
|
+
Component: StepThree,
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<Tour
|
|
230
|
+
activeTourStep={activeTourStep}
|
|
231
|
+
onChange={setActiveTourStep}
|
|
232
|
+
steps={tourSteps}
|
|
233
|
+
// Global scroll options
|
|
234
|
+
scrollOptions={{
|
|
235
|
+
behavior: 'smooth',
|
|
236
|
+
marginX: 100,
|
|
237
|
+
marginY: 100,
|
|
238
|
+
}}
|
|
239
|
+
>
|
|
240
|
+
...
|
|
241
|
+
</Tour>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Customization
|
|
247
|
+
|
|
248
|
+
#### Overlay
|
|
249
|
+
|
|
250
|
+
You can hide the dimmed overlay behind the coachmark using the `hideOverlay` prop.
|
|
251
|
+
This can be set globally on the `Tour` component or per-step.
|
|
252
|
+
|
|
253
|
+
```jsx live
|
|
254
|
+
function HideOverlayExample() {
|
|
255
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
256
|
+
const [hideOverlay, setHideOverlay] = useState(false);
|
|
257
|
+
|
|
258
|
+
const StepOne = () => {
|
|
259
|
+
const { goNextTourStep, stopTour } = useTourContext();
|
|
260
|
+
return (
|
|
261
|
+
<Coachmark
|
|
262
|
+
action={
|
|
263
|
+
<Button compact onClick={goNextTourStep}>
|
|
264
|
+
Next
|
|
265
|
+
</Button>
|
|
266
|
+
}
|
|
267
|
+
content="This step respects the global hideOverlay setting."
|
|
268
|
+
onClose={stopTour}
|
|
269
|
+
title="Step One"
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const StepTwo = () => {
|
|
275
|
+
const { stopTour, goPreviousTourStep } = useTourContext();
|
|
276
|
+
return (
|
|
277
|
+
<Coachmark
|
|
278
|
+
action={
|
|
279
|
+
<HStack gap={1}>
|
|
280
|
+
<Button compact onClick={goPreviousTourStep} variant="secondary">
|
|
281
|
+
Back
|
|
282
|
+
</Button>
|
|
283
|
+
<Button compact onClick={stopTour}>
|
|
284
|
+
Done
|
|
285
|
+
</Button>
|
|
286
|
+
</HStack>
|
|
287
|
+
}
|
|
288
|
+
content="This step also respects the global hideOverlay setting."
|
|
289
|
+
title="Step Two"
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const tourSteps = [
|
|
295
|
+
{ id: 'step1', Component: StepOne },
|
|
296
|
+
{ id: 'step2', Component: StepTwo },
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
const TourContent = () => {
|
|
300
|
+
const { startTour } = useTourContext();
|
|
301
|
+
return (
|
|
302
|
+
<VStack gap={2}>
|
|
303
|
+
<HStack gap={2} alignItems="center">
|
|
304
|
+
<Button compact onClick={() => startTour()}>
|
|
305
|
+
Start tour
|
|
306
|
+
</Button>
|
|
307
|
+
<Checkbox checked={hideOverlay} onChange={() => setHideOverlay((prev) => !prev)}>
|
|
308
|
+
Hide overlay
|
|
309
|
+
</Checkbox>
|
|
310
|
+
</HStack>
|
|
311
|
+
<HStack gap={4}>
|
|
312
|
+
<TourStep id="step1">
|
|
313
|
+
<Box background="bgSecondary" padding={4}>
|
|
314
|
+
<Text as="p" font="body">
|
|
315
|
+
Step 1
|
|
316
|
+
</Text>
|
|
317
|
+
</Box>
|
|
318
|
+
</TourStep>
|
|
319
|
+
<TourStep id="step2">
|
|
320
|
+
<Box background="bgSecondary" padding={4}>
|
|
321
|
+
<Text as="p" font="body">
|
|
322
|
+
Step 2
|
|
323
|
+
</Text>
|
|
324
|
+
</Box>
|
|
325
|
+
</TourStep>
|
|
326
|
+
</HStack>
|
|
327
|
+
</VStack>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<Tour
|
|
333
|
+
hideOverlay={hideOverlay}
|
|
334
|
+
activeTourStep={activeTourStep}
|
|
335
|
+
onChange={setActiveTourStep}
|
|
336
|
+
steps={tourSteps}
|
|
337
|
+
>
|
|
338
|
+
<TourContent />
|
|
339
|
+
</Tour>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### Mask
|
|
345
|
+
|
|
346
|
+
Customize the mask (cutout) around the highlighted element with the `tourMaskPadding` and `tourMaskBorderRadius` props.
|
|
347
|
+
|
|
348
|
+
```jsx
|
|
349
|
+
<Tour
|
|
350
|
+
activeTourStep={activeTourStep}
|
|
351
|
+
onChange={setActiveTourStep}
|
|
352
|
+
steps={tourSteps}
|
|
353
|
+
// Padding around the highlighted element (default: 8)
|
|
354
|
+
tourMaskPadding={16}
|
|
355
|
+
// Border radius of the cutout (default: 8)
|
|
356
|
+
tourMaskBorderRadius={12}
|
|
357
|
+
>
|
|
358
|
+
...
|
|
359
|
+
</Tour>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
You can provide a completely custom mask component using the `TourMaskComponent` prop.
|
|
363
|
+
|
|
364
|
+
```jsx
|
|
365
|
+
function CustomMaskExample() {
|
|
366
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
367
|
+
|
|
368
|
+
const CustomMask = ({ activeTourStepTargetRect, padding = 8, borderRadius = 8 }) => {
|
|
369
|
+
// Custom mask implementation
|
|
370
|
+
// activeTourStepTargetRect contains { x, y, width, height } of the target element
|
|
371
|
+
return (
|
|
372
|
+
<svg
|
|
373
|
+
style={{
|
|
374
|
+
position: 'fixed',
|
|
375
|
+
top: 0,
|
|
376
|
+
left: 0,
|
|
377
|
+
width: '100vw',
|
|
378
|
+
height: '100vh',
|
|
379
|
+
pointerEvents: 'none',
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
<defs>
|
|
383
|
+
<mask id="tour-mask">
|
|
384
|
+
<rect fill="white" height="100%" width="100%" />
|
|
385
|
+
<rect
|
|
386
|
+
fill="black"
|
|
387
|
+
height={activeTourStepTargetRect.height + padding * 2}
|
|
388
|
+
rx={borderRadius}
|
|
389
|
+
ry={borderRadius}
|
|
390
|
+
width={activeTourStepTargetRect.width + padding * 2}
|
|
391
|
+
x={activeTourStepTargetRect.x - padding}
|
|
392
|
+
y={activeTourStepTargetRect.y - padding}
|
|
393
|
+
/>
|
|
394
|
+
</mask>
|
|
395
|
+
</defs>
|
|
396
|
+
<rect fill="rgba(0,0,0,0.5)" height="100%" mask="url(#tour-mask)" width="100%" />
|
|
397
|
+
</svg>
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<Tour
|
|
403
|
+
activeTourStep={activeTourStep}
|
|
404
|
+
onChange={setActiveTourStep}
|
|
405
|
+
steps={tourSteps}
|
|
406
|
+
TourMaskComponent={CustomMask}
|
|
407
|
+
>
|
|
408
|
+
...
|
|
409
|
+
</Tour>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### Positioning
|
|
415
|
+
|
|
416
|
+
The Tour component uses `@floating-ui` to position coachmarks relative to their target elements.
|
|
417
|
+
You can customize positioning with the `tourStepOffset`, `tourStepShift`, and `tourStepAutoPlacement` props.
|
|
418
|
+
|
|
419
|
+
```jsx
|
|
420
|
+
function PositioningExample() {
|
|
421
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
422
|
+
|
|
423
|
+
const tourSteps = [
|
|
424
|
+
{ id: 'step1', Component: StepOne },
|
|
425
|
+
{ id: 'step2', Component: StepTwo },
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<Tour
|
|
430
|
+
activeTourStep={activeTourStep}
|
|
431
|
+
onChange={setActiveTourStep}
|
|
432
|
+
steps={tourSteps}
|
|
433
|
+
// Distance between coachmark and target (default: 24)
|
|
434
|
+
tourStepOffset={32}
|
|
435
|
+
// Padding when shifting to stay in viewport (default: { padding: 32 })
|
|
436
|
+
tourStepShift={{ padding: 16 }}
|
|
437
|
+
// Auto-placement options from @floating-ui
|
|
438
|
+
tourStepAutoPlacement={{ allowedPlacements: ['top', 'bottom'] }}
|
|
439
|
+
>
|
|
440
|
+
...
|
|
441
|
+
</Tour>
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
#### Arrow
|
|
447
|
+
|
|
448
|
+
You can customize the arrow that points to the target element by providing a custom `TourStepArrowComponent`.
|
|
449
|
+
|
|
450
|
+
```jsx
|
|
451
|
+
function CustomArrowExample() {
|
|
452
|
+
const [activeTourStep, setActiveTourStep] = useState(null);
|
|
453
|
+
|
|
454
|
+
// Custom arrow component - MUST forward ref
|
|
455
|
+
const CustomArrow = memo(
|
|
456
|
+
forwardRef((props, ref) => {
|
|
457
|
+
return <DefaultTourStepArrow {...props} ref={ref} style={{ color: 'var(--color-blue60)' }} />;
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const tourSteps = [
|
|
462
|
+
{
|
|
463
|
+
id: 'step1',
|
|
464
|
+
Component: StepOne,
|
|
465
|
+
// Per-step custom arrow
|
|
466
|
+
ArrowComponent: CustomArrow,
|
|
467
|
+
// Or just customize the style
|
|
468
|
+
arrowStyle: { color: 'var(--color-purple60)' },
|
|
469
|
+
},
|
|
470
|
+
{ id: 'step2', Component: StepTwo },
|
|
471
|
+
];
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
<Tour
|
|
475
|
+
activeTourStep={activeTourStep}
|
|
476
|
+
onChange={setActiveTourStep}
|
|
477
|
+
steps={tourSteps}
|
|
478
|
+
TourStepArrowComponent={CustomArrow}
|
|
479
|
+
>
|
|
480
|
+
...
|
|
481
|
+
</Tour>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Portal
|
|
487
|
+
|
|
488
|
+
By default, the Tour uses React portals to render outside the DOM hierarchy.
|
|
489
|
+
You can disable this behavior if needed.
|
|
490
|
+
|
|
491
|
+
```jsx
|
|
492
|
+
<Tour
|
|
493
|
+
...
|
|
494
|
+
disablePortal
|
|
495
|
+
>
|
|
496
|
+
...
|
|
497
|
+
</Tour>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Accessibility
|
|
501
|
+
|
|
502
|
+
Always provide accessibility labels for close buttons using the `closeButtonAccessibilityLabel` prop and ensure coachmarks are navigable.
|
|
503
|
+
|
|
504
|
+
```jsx
|
|
505
|
+
function AccessibleTourExample() {
|
|
506
|
+
const AccessibleStep = () => {
|
|
507
|
+
const { goNextTourStep, stopTour } = useTourContext();
|
|
508
|
+
|
|
509
|
+
return (
|
|
510
|
+
<Coachmark
|
|
511
|
+
action={
|
|
512
|
+
<Button
|
|
513
|
+
aria-label="Advances to the next step in the tour"
|
|
514
|
+
compact
|
|
515
|
+
onClick={goNextTourStep}
|
|
516
|
+
>
|
|
517
|
+
Next
|
|
518
|
+
</Button>
|
|
519
|
+
}
|
|
520
|
+
closeButtonAccessibilityLabel="Close tour and return to main content"
|
|
521
|
+
content="This coachmark has proper accessibility labels for screen readers."
|
|
522
|
+
onClose={stopTour}
|
|
523
|
+
title="Accessible Step"
|
|
524
|
+
/>
|
|
525
|
+
);
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Callbacks
|
|
531
|
+
|
|
532
|
+
Use the `onBeforeActive` callback to perform actions before a step becomes active,
|
|
533
|
+
such as fetching data, preparing the UI, or custom scrolling logic.
|
|
534
|
+
|
|
535
|
+
```jsx
|
|
536
|
+
function CallbacksExample() {
|
|
537
|
+
const tourSteps = [
|
|
538
|
+
{
|
|
539
|
+
id: 'step1',
|
|
540
|
+
onBeforeActive: () => {
|
|
541
|
+
console.log('Step 1 is about to become active');
|
|
542
|
+
// Perform any setup needed
|
|
543
|
+
},
|
|
544
|
+
Component: StepOne,
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
id: 'step2',
|
|
548
|
+
onBeforeActive: async () => {
|
|
549
|
+
// Async operations are supported
|
|
550
|
+
await fetchStepData();
|
|
551
|
+
console.log('Step 2 data loaded');
|
|
552
|
+
},
|
|
553
|
+
Component: StepTwo,
|
|
554
|
+
},
|
|
555
|
+
];
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
159
559
|
## Props
|
|
160
560
|
|
|
161
561
|
| Prop | Type | Required | Default | Description |
|