@coinbase/cds-mcp-server 8.27.0 → 8.27.2

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 CHANGED
@@ -8,6 +8,14 @@ All notable changes to this project will be documented in this file.
8
8
 
9
9
  <!-- template-start -->
10
10
 
11
+ ## 8.27.2 ((12/4/2025, 04:23 PM PST))
12
+
13
+ This is an artificial version bump with no new change.
14
+
15
+ ## 8.27.1 ((12/4/2025, 06:51 AM PST))
16
+
17
+ This is an artificial version bump with no new change.
18
+
11
19
  ## 8.27.0 ((12/3/2025, 09:30 AM PST))
12
20
 
13
21
  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
- ```tsx live
16
- function Example() {
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={<Button variant="secondary" onPress={goNextTourStep} compact label="Next" />}
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&apos;t show again
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={<Button variant="secondary" onPress={goNextTourStep} compact label="Next" />}
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
- <TextCaption as="p" color="fgMuted">
58
+ <Text color="fgMuted" font="caption">
47
59
  50%
48
- </TextCaption>
60
+ </Text>
49
61
  <ProgressBar progress={0.5} />
50
- <TextBody as="p">
62
+ <Text font="body">
51
63
  Add up to 3 lines of body copy. Deliver your message with clarity and impact
52
- </TextBody>
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 variant="secondary" onPress={goPreviousTourStep} compact label="Back" />
69
- <Button variant="secondary" onPress={goNextTourStep} compact label="Next" />
70
- <Button variant="secondary" onPress={stopTour} compact label="Done" />
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: () => console.log('step2 before'),
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: () => console.log('step3 before'),
249
+ onBeforeActive: async () => {
250
+ await scrollIntoView(scrollViewRef, step3Ref);
251
+ },
94
252
  Component: StepThree,
95
253
  },
96
254
  ];
97
255
 
98
- const TourExample = ({ spacerWidthIncrement = 0, spacerHeightIncrement = 500 }) => {
99
- const { startTour } = useTourContext();
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 backgroundColor="secondary" padding={4}>
108
- <Text>This is step 1</Text>
260
+ <Box background="bgSecondary" padding={4}>
261
+ <Text>Step 1 - visible on load</Text>
109
262
  </Box>
110
263
  </TourStep>
111
- <Box height={spacerHeightIncrement} />
112
- <HStack justifyContent="flex-end">
113
- <Box flexShrink={0} width={spacerWidthIncrement} />
114
- <TourStep id="step2">
115
- <Box backgroundColor="secondary" padding={4} width={150}>
116
- <Text>This is step 2</Text>
117
- </Box>
118
- </TourStep>
119
- </HStack>
120
- <Box height={spacerHeightIncrement} />
121
- <HStack>
122
- <Box flexShrink={0} width={spacerWidthIncrement * 2} />
123
- <TourStep id="step3">
124
- <VStack backgroundColor="secondary" padding={4} width={150}>
125
- <Text>This is step 3</Text>
126
- </VStack>
127
- </TourStep>
128
- </HStack>
129
- </VStack>
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
- <Tour activeTourStep={activeTourStep} onChange={setActiveTourStep} steps={tourSteps}>
135
- <TourExample />
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
- <TextCaption as="p" color="fgMuted">
62
+ <Text as="p" color="fgMuted" font="caption">
60
63
  50%
61
- </TextCaption>
64
+ </Text>
62
65
  <ProgressBar progress={0.5} />
63
- <TextBody as="p">
66
+ <Text as="p" font="body">
64
67
  Add up to 3 lines of body copy. Deliver your message with clarity and impact
65
- </TextBody>
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, goNextTourStep, goPreviousTourStep } = useTourContext();
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="bgAlternate" padding={4}>
126
- <TextBody as="p">This is step 1</TextBody>
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="bgAlternate" padding={4} width={150}>
134
- <TextBody as="p">This is step 2</TextBody>
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="bgAlternate" padding={4} width={150}>
143
- <TextBody as="p">This is step 3</TextBody>
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 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cds-mcp-server",
3
- "version": "8.27.0",
3
+ "version": "8.27.2",
4
4
  "description": "Coinbase Design System - MCP Server",
5
5
  "repository": {
6
6
  "type": "git",