@astryxdesign/lab 0.3.0-canary.6f9bc84 → 0.3.0-canary.b68ee61

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.
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @file Tour.tsx
3
+ * @input Uses React state/refs, TourContext
4
+ * @output Exports Tour controller component and TourProps
5
+ * @position Lab experiment (facebook/astryx#4239); controller consumed by index.ts
6
+ *
7
+ * Tour is the controller for a product-tour / NUX walkthrough. It renders no
8
+ * visible chrome of its own — it owns the tour state (active step, advance /
9
+ * retreat / complete / dismiss) and shares it with declaratively-nested
10
+ * `<TourStep>` children through context. Steps register on mount, so the step
11
+ * ORDER is taken from the children in document order (no step array to keep in
12
+ * sync with the markup).
13
+ *
14
+ * The behavior (a controller plus spotlight feature steps, an `isActive`
15
+ * switch, dismiss-with-reason, and step progress) is composed from Astryx
16
+ * primitives. Each step anchors its callout via Popover and draws its own
17
+ * highlight/backdrop (see TourStep) — the controller owns no chrome.
18
+ *
19
+ * SYNC: When modified, update these files to stay in sync:
20
+ * - /packages/lab/src/Tour/TourStep.tsx
21
+ * - /packages/lab/src/Tour/TourContext.ts
22
+ * - /packages/lab/src/Tour/useTour.ts
23
+ * - /packages/lab/src/Tour/Tour.doc.mjs (props table, features)
24
+ * - /packages/lab/src/Tour/Tour.test.tsx (tests for new/changed behavior)
25
+ * - /packages/lab/src/Tour/index.ts (exports if types change)
26
+ */
27
+ import { type ReactNode } from 'react';
28
+ import { type TourDismissSource } from './TourContext';
29
+ export interface TourProps {
30
+ /**
31
+ * Whether the tour is running. When false, nothing renders and step state
32
+ * resets — the tour restarts from the first step next time it becomes active.
33
+ * (Controlled: the consumer owns "has this user seen the tour?".)
34
+ */
35
+ isActive: boolean;
36
+ /**
37
+ * The tour's steps — `<TourStep>` elements. Step order is taken from their
38
+ * order here; only the active step renders its callout.
39
+ */
40
+ children?: ReactNode;
41
+ /**
42
+ * Called when the tour is dismissed, with the reason. Fires for every exit,
43
+ * including completing the last step (`source === 'complete'`). The consumer
44
+ * flips `isActive` to false in response, and can branch on the source to tell
45
+ * a successful finish apart from an early skip/backdrop/escape.
46
+ */
47
+ onDismiss: (source: TourDismissSource) => void;
48
+ /**
49
+ * Dim the page around the active step's target (a spotlight cutout — the
50
+ * target stays lit, everything else darkens). Use for modal-style steps that
51
+ * demand focus; leave off for a lightweight coachmark that only rings the
52
+ * target.
53
+ * @default false
54
+ */
55
+ hasBackdrop?: boolean;
56
+ /**
57
+ * Show the step count ("2 of 5") in each step.
58
+ * @default false
59
+ */
60
+ isStepCountShown?: boolean;
61
+ }
62
+ /**
63
+ * Controller for a guided product tour. Renders no chrome; coordinates the
64
+ * active step among its `<TourStep>` children.
65
+ *
66
+ * @example
67
+ * ```
68
+ * const [isActive, setIsActive] = useState(true);
69
+ * const saveRef = useRef(null);
70
+ * const shareRef = useRef(null);
71
+ * <Tour isActive={isActive} hasBackdrop isStepCountShown onDismiss={() => setIsActive(false)}>
72
+ * <TourStep targetRef={saveRef} heading="Save your work">
73
+ * Changes save automatically to the cloud.
74
+ * </TourStep>
75
+ * <TourStep targetRef={shareRef} heading="Share it">
76
+ * Invite teammates from here.
77
+ * </TourStep>
78
+ * </Tour>
79
+ * ```
80
+ */
81
+ export declare function Tour({ isActive, children, onDismiss, hasBackdrop, isStepCountShown, }: TourProps): import("react").JSX.Element;
82
+ export declare namespace Tour {
83
+ var displayName: string;
84
+ }
85
+ //# sourceMappingURL=Tour.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tour.d.ts","sourceRoot":"","sources":["../../src/Tour/Tour.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAA4C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAChF,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;;OAKG;IACH,SAAS,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IAC/C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,EACnB,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,WAAmB,EACnB,gBAAwB,GACzB,EAAE,SAAS,+BAuEX;yBA7Ee,IAAI"}
@@ -0,0 +1,112 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file Tour.tsx
7
+ * @input Uses React state/refs, TourContext
8
+ * @output Exports Tour controller component and TourProps
9
+ * @position Lab experiment (facebook/astryx#4239); controller consumed by index.ts
10
+ *
11
+ * Tour is the controller for a product-tour / NUX walkthrough. It renders no
12
+ * visible chrome of its own — it owns the tour state (active step, advance /
13
+ * retreat / complete / dismiss) and shares it with declaratively-nested
14
+ * `<TourStep>` children through context. Steps register on mount, so the step
15
+ * ORDER is taken from the children in document order (no step array to keep in
16
+ * sync with the markup).
17
+ *
18
+ * The behavior (a controller plus spotlight feature steps, an `isActive`
19
+ * switch, dismiss-with-reason, and step progress) is composed from Astryx
20
+ * primitives. Each step anchors its callout via Popover and draws its own
21
+ * highlight/backdrop (see TourStep) — the controller owns no chrome.
22
+ *
23
+ * SYNC: When modified, update these files to stay in sync:
24
+ * - /packages/lab/src/Tour/TourStep.tsx
25
+ * - /packages/lab/src/Tour/TourContext.ts
26
+ * - /packages/lab/src/Tour/useTour.ts
27
+ * - /packages/lab/src/Tour/Tour.doc.mjs (props table, features)
28
+ * - /packages/lab/src/Tour/Tour.test.tsx (tests for new/changed behavior)
29
+ * - /packages/lab/src/Tour/index.ts (exports if types change)
30
+ */
31
+ import { useCallback, useEffect, useMemo, useState } from 'react';
32
+ import { TourContext } from "./TourContext.js";
33
+ import { jsx as _jsx } from "react/jsx-runtime";
34
+ /**
35
+ * Controller for a guided product tour. Renders no chrome; coordinates the
36
+ * active step among its `<TourStep>` children.
37
+ *
38
+ * @example
39
+ * ```
40
+ * const [isActive, setIsActive] = useState(true);
41
+ * const saveRef = useRef(null);
42
+ * const shareRef = useRef(null);
43
+ * <Tour isActive={isActive} hasBackdrop isStepCountShown onDismiss={() => setIsActive(false)}>
44
+ * <TourStep targetRef={saveRef} heading="Save your work">
45
+ * Changes save automatically to the cloud.
46
+ * </TourStep>
47
+ * <TourStep targetRef={shareRef} heading="Share it">
48
+ * Invite teammates from here.
49
+ * </TourStep>
50
+ * </Tour>
51
+ * ```
52
+ */
53
+ export function Tour({
54
+ isActive,
55
+ children,
56
+ onDismiss,
57
+ hasBackdrop = false,
58
+ isStepCountShown = false
59
+ }) {
60
+ // Steps register on mount; insertion order (document order) defines the
61
+ // sequence. A plain array keeps registration order deterministic.
62
+ const [stepIds, setStepIds] = useState([]);
63
+ const [activeStepIndex, setActiveStepIndex] = useState(0);
64
+ const registerStep = useCallback(id => {
65
+ setStepIds(prev => prev.includes(id) ? prev : [...prev, id]);
66
+ return () => {
67
+ setStepIds(prev => prev.filter(stepId => stepId !== id));
68
+ };
69
+ }, []);
70
+
71
+ // Reset to the first step whenever the tour is (re)started, so a dismissed
72
+ // tour begins from the top next time isActive flips back on.
73
+ useEffect(() => {
74
+ if (!isActive) {
75
+ setActiveStepIndex(0);
76
+ }
77
+ }, [isActive]);
78
+ const stepCount = stepIds.length;
79
+ const onNext = useCallback(() => {
80
+ // Runs from a click/imperative call (never during render), so branch on the
81
+ // current index directly rather than inside a state updater — calling
82
+ // onDismiss from within setState triggers a "setState while rendering"
83
+ // warning.
84
+ if (activeStepIndex < stepCount - 1) {
85
+ setActiveStepIndex(activeStepIndex + 1);
86
+ return;
87
+ }
88
+ // Past the last step → complete. onDismiss carries the reason so a
89
+ // consumer can branch on 'complete' vs an early exit.
90
+ onDismiss('complete');
91
+ }, [activeStepIndex, stepCount, onDismiss]);
92
+ const onPrevious = useCallback(() => {
93
+ setActiveStepIndex(prev => prev > 0 ? prev - 1 : prev);
94
+ }, []);
95
+ const activeStepId = isActive ? stepIds[activeStepIndex] ?? null : null;
96
+ const contextValue = useMemo(() => ({
97
+ registerStep,
98
+ activeStepId,
99
+ activeStepIndex,
100
+ stepCount,
101
+ isStepCountShown,
102
+ hasBackdrop,
103
+ onNext,
104
+ onPrevious,
105
+ onDismiss
106
+ }), [registerStep, activeStepId, activeStepIndex, stepCount, isStepCountShown, hasBackdrop, onNext, onPrevious, onDismiss]);
107
+ return /*#__PURE__*/_jsx(TourContext.Provider, {
108
+ value: contextValue,
109
+ children: children
110
+ });
111
+ }
112
+ Tour.displayName = 'Tour';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Why a tour was dismissed. Lets a consumer distinguish "finished the whole
3
+ * tour" from the various early exits, so it can persist "has seen this tour"
4
+ * appropriately.
5
+ * - `backdrop`: clicked the dimmed background
6
+ * - `escape`: pressed Escape at any step
7
+ * - `close`: pressed the step's close (X) control
8
+ * - `skip`: chose "skip"/"maybe later" on the first step
9
+ * - `complete`: advanced past the final step
10
+ */
11
+ export type TourDismissSource = 'backdrop' | 'escape' | 'close' | 'skip' | 'complete';
12
+ /**
13
+ * Value shared from the Tour controller to its steps. Steps register
14
+ * themselves on mount (so the controller learns the step order from the
15
+ * children, in document order) and read whether they are the active step.
16
+ */
17
+ export interface TourContextValue {
18
+ /** Register a step by its stable id; returns an unregister cleanup. */
19
+ registerStep: (id: string) => () => void;
20
+ /** The id of the step that is currently active, or null when none. */
21
+ activeStepId: string | null;
22
+ /** Zero-based index of the active step among registered steps. */
23
+ activeStepIndex: number;
24
+ /** Total number of registered steps. */
25
+ stepCount: number;
26
+ /** Whether the tour's step count should be shown in each step. */
27
+ isStepCountShown: boolean;
28
+ /** Whether a dimmed background is shown behind the active step. */
29
+ hasBackdrop: boolean;
30
+ /** Advance to the next step (or complete on the last step). */
31
+ onNext: () => void;
32
+ /** Return to the previous step (no-op on the first step). */
33
+ onPrevious: () => void;
34
+ /** Dismiss the tour with a reason. */
35
+ onDismiss: (source: TourDismissSource) => void;
36
+ }
37
+ export declare const TourContext: import("react").Context<TourContextValue | null>;
38
+ //# sourceMappingURL=TourContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TourContext.d.ts","sourceRoot":"","sources":["../../src/Tour/TourContext.ts"],"names":[],"mappings":"AAmBA;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAC3B,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,IAAI,CAAC;IACzC,sEAAsE;IACtE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kEAAkE;IAClE,eAAe,EAAE,MAAM,CAAC;IACxB,wCAAwC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mEAAmE;IACnE,WAAW,EAAE,OAAO,CAAC;IACrB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,sCAAsC;IACtC,SAAS,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAChD;AAED,eAAO,MAAM,WAAW,kDAA+C,CAAC"}
@@ -0,0 +1,36 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file TourContext.ts
7
+ * @input Uses React createContext
8
+ * @output Exports TourContext, TourContextValue, TourDismissSource
9
+ * @position Internal context; provided by Tour, consumed by TourStep + useTour
10
+ *
11
+ * SYNC: When modified, update these files to stay in sync:
12
+ * - /packages/lab/src/Tour/Tour.tsx
13
+ * - /packages/lab/src/Tour/TourStep.tsx
14
+ * - /packages/lab/src/Tour/useTour.ts
15
+ * - /packages/lab/src/Tour/Tour.doc.mjs
16
+ */
17
+ import { createContext } from 'react';
18
+
19
+ /**
20
+ * Why a tour was dismissed. Lets a consumer distinguish "finished the whole
21
+ * tour" from the various early exits, so it can persist "has seen this tour"
22
+ * appropriately.
23
+ * - `backdrop`: clicked the dimmed background
24
+ * - `escape`: pressed Escape at any step
25
+ * - `close`: pressed the step's close (X) control
26
+ * - `skip`: chose "skip"/"maybe later" on the first step
27
+ * - `complete`: advanced past the final step
28
+ */
29
+
30
+ /**
31
+ * Value shared from the Tour controller to its steps. Steps register
32
+ * themselves on mount (so the controller learns the step order from the
33
+ * children, in document order) and read whether they are the active step.
34
+ */
35
+
36
+ export const TourContext = /*#__PURE__*/createContext(null);
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @file TourStep.tsx
3
+ * @input Uses React, Popover/Button/Text/Heading/Stack from @astryxdesign/core, TourContext
4
+ * @output Exports TourStep component and TourStepProps
5
+ * @position Lab experiment (facebook/astryx#4239); a single spotlight step in a Tour
6
+ *
7
+ * A TourStep highlights a target element and renders a callout anchored to it,
8
+ * with a heading, body, optional step progress ("2 of 5"), and back / next /
9
+ * close controls. It registers with the parent `<Tour>` on mount (so step
10
+ * order follows the children) and only renders its callout while it is the
11
+ * active step. Anchoring + the callout surface reuse the core `Popover`
12
+ * (`anchorRef` → the step's target), so positioning, top-layer rendering, and
13
+ * dismiss semantics come from the existing layer system rather than a bespoke
14
+ * implementation.
15
+ *
16
+ * The highlight is drawn as a separate overlay tracking the target's box — the
17
+ * consumer's element is never restyled, so there is no cascade fight with the
18
+ * target's own styles. That overlay is promoted into the browser TOP LAYER
19
+ * (via the popover API, like the rest of the layer system) so it sits above
20
+ * page content without any hardcoded z-index; the callout is promoted after
21
+ * it, so the callout stays above the highlight. Because it is promoted in
22
+ * place (not portaled out of the tree), it stays inside the consumer's Theme
23
+ * subtree and inherits theme tokens — including a scoped/nested theme's accent
24
+ * for the ring. Dimming is an opt-in spotlight cutout (dims around the target,
25
+ * not over it) rather than a flat scrim.
26
+ *
27
+ * SYNC: When modified, update these files to stay in sync:
28
+ * - /packages/lab/src/Tour/Tour.tsx
29
+ * - /packages/lab/src/Tour/TourContext.ts
30
+ * - /packages/lab/src/Tour/Tour.doc.mjs (props table, features)
31
+ * - /packages/lab/src/Tour/Tour.test.tsx (tests for new/changed behavior)
32
+ * - /packages/lab/src/Tour/index.ts (exports if types change)
33
+ */
34
+ import { type ReactNode } from 'react';
35
+ import type { LayerAlignment, LayerPlacement } from '@astryxdesign/core/Layer';
36
+ export interface TourStepProps {
37
+ /**
38
+ * Ref to the element this step points at. The callout anchors to it (like a
39
+ * Popover trigger); it must be a `<button>` or `[role="button"]` element,
40
+ * matching Popover's `anchorRef` contract. Accepts a ref to any HTMLElement
41
+ * subtype (e.g. `useRef<HTMLButtonElement>(null)`).
42
+ */
43
+ targetRef: React.RefObject<HTMLElement | null>;
44
+ /** Step heading. */
45
+ heading: ReactNode;
46
+ /** Step body content. */
47
+ children?: ReactNode;
48
+ /**
49
+ * Which side of the target the callout sits on.
50
+ * @default 'below'
51
+ */
52
+ placement?: LayerPlacement;
53
+ /**
54
+ * How the callout aligns along the placement side — e.g. with `placement="below"`,
55
+ * `start` left-aligns it under the target, `center` centers it, `end` right-aligns it.
56
+ * @default 'start'
57
+ */
58
+ alignment?: LayerAlignment;
59
+ /** Test id applied to the callout content. */
60
+ 'data-testid'?: string;
61
+ }
62
+ /**
63
+ * A single spotlight step within a `<Tour>`. Renders its callout only while
64
+ * active.
65
+ *
66
+ * @example
67
+ * ```
68
+ * <TourStep targetRef={saveRef} heading="Save your work">
69
+ * Changes save automatically.
70
+ * </TourStep>
71
+ * ```
72
+ */
73
+ export declare function TourStep({ targetRef, heading, children, placement, alignment, 'data-testid': testId, }: TourStepProps): import("react").JSX.Element | null;
74
+ export declare namespace TourStep {
75
+ var displayName: string;
76
+ }
77
+ //# sourceMappingURL=TourStep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TourStep.d.ts","sourceRoot":"","sources":["../../src/Tour/TourStep.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,0BAA0B,CAAC;AAmK7E,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC/C,oBAAoB;IACpB,OAAO,EAAE,SAAS,CAAC;IACnB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;OAGG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B;;;;OAIG;IACH,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,8CAA8C;IAC9C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,EACvB,SAAS,EACT,OAAO,EACP,QAAQ,EACR,SAAmB,EACnB,SAAmB,EACnB,aAAa,EAAE,MAAM,GACtB,EAAE,aAAa,sCA0If;yBAjJe,QAAQ"}
@@ -0,0 +1,307 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file TourStep.tsx
7
+ * @input Uses React, Popover/Button/Text/Heading/Stack from @astryxdesign/core, TourContext
8
+ * @output Exports TourStep component and TourStepProps
9
+ * @position Lab experiment (facebook/astryx#4239); a single spotlight step in a Tour
10
+ *
11
+ * A TourStep highlights a target element and renders a callout anchored to it,
12
+ * with a heading, body, optional step progress ("2 of 5"), and back / next /
13
+ * close controls. It registers with the parent `<Tour>` on mount (so step
14
+ * order follows the children) and only renders its callout while it is the
15
+ * active step. Anchoring + the callout surface reuse the core `Popover`
16
+ * (`anchorRef` → the step's target), so positioning, top-layer rendering, and
17
+ * dismiss semantics come from the existing layer system rather than a bespoke
18
+ * implementation.
19
+ *
20
+ * The highlight is drawn as a separate overlay tracking the target's box — the
21
+ * consumer's element is never restyled, so there is no cascade fight with the
22
+ * target's own styles. That overlay is promoted into the browser TOP LAYER
23
+ * (via the popover API, like the rest of the layer system) so it sits above
24
+ * page content without any hardcoded z-index; the callout is promoted after
25
+ * it, so the callout stays above the highlight. Because it is promoted in
26
+ * place (not portaled out of the tree), it stays inside the consumer's Theme
27
+ * subtree and inherits theme tokens — including a scoped/nested theme's accent
28
+ * for the ring. Dimming is an opt-in spotlight cutout (dims around the target,
29
+ * not over it) rather than a flat scrim.
30
+ *
31
+ * SYNC: When modified, update these files to stay in sync:
32
+ * - /packages/lab/src/Tour/Tour.tsx
33
+ * - /packages/lab/src/Tour/TourContext.ts
34
+ * - /packages/lab/src/Tour/Tour.doc.mjs (props table, features)
35
+ * - /packages/lab/src/Tour/Tour.test.tsx (tests for new/changed behavior)
36
+ * - /packages/lab/src/Tour/index.ts (exports if types change)
37
+ */
38
+ import { useContext, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
39
+ import * as stylex from '@stylexjs/stylex';
40
+ import { Popover } from '@astryxdesign/core/Popover';
41
+ import { Button } from '@astryxdesign/core/Button';
42
+ import { Text } from '@astryxdesign/core/Text';
43
+ import { Heading } from '@astryxdesign/core/Heading';
44
+ import { VStack, HStack } from '@astryxdesign/core/Layout';
45
+ import '@astryxdesign/core/theme/tokens.stylex';
46
+ import { colorVars, spacingVars, durationVars, easeVars } from '@astryxdesign/core/theme/tokens.stylex';
47
+ import { TourContext } from "./TourContext.js";
48
+
49
+ // Client-only layout effect (SSR renders no overlay, so this only runs on the
50
+ // client). Kept local since core does not export its isomorphic variant.
51
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
52
+ const useClientLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
53
+
54
+ // Gap between the target's edge and the highlight ring, in px.
55
+ const HIGHLIGHT_PADDING = 4;
56
+ const styles = {
57
+ footer: {
58
+ kGNEyG: "x6s0dn4",
59
+ kjj79g: "x1qughib",
60
+ $$css: true
61
+ },
62
+ calloutGapBelow: {
63
+ keoZOQ: "xtbrsbv",
64
+ $$css: true
65
+ },
66
+ calloutGapAbove: {
67
+ k1K539: "x1p37lm5",
68
+ $$css: true
69
+ },
70
+ calloutGapStart: {
71
+ k71WvV: "x1pezmd8",
72
+ $$css: true
73
+ },
74
+ calloutGapEnd: {
75
+ keTefX: "x1cmpsy9",
76
+ $$css: true
77
+ }
78
+ };
79
+ const calloutGapStyles = {
80
+ below: styles.calloutGapBelow,
81
+ above: styles.calloutGapAbove,
82
+ start: styles.calloutGapStart,
83
+ end: styles.calloutGapEnd
84
+ };
85
+
86
+ /**
87
+ * The highlight overlay: a top-layer box sized to the target, plus an optional
88
+ * spotlight-cutout dim. Rendered inline (not portaled) so it stays inside the
89
+ * consumer's Theme subtree and inherits theme tokens — the popover API promotes
90
+ * it into the top layer in place. Rendered before the callout so the callout
91
+ * promotes on top of it; promoted once (never re-promoted) so that ordering
92
+ * holds across re-measures.
93
+ */
94
+ function TourHighlight({
95
+ rect,
96
+ hasBackdrop,
97
+ onBackdropClick
98
+ }) {
99
+ const ref = useRef(null);
100
+ useClientLayoutEffect(() => {
101
+ const el = ref.current;
102
+ if (el == null || typeof el.showPopover !== 'function') {
103
+ return;
104
+ }
105
+ el.showPopover();
106
+ return () => {
107
+ if (el.isConnected && typeof el.hidePopover === 'function') {
108
+ el.hidePopover();
109
+ }
110
+ };
111
+ }, []);
112
+ return /*#__PURE__*/_jsx("div", {
113
+ ref: ref,
114
+ popover: "manual",
115
+ "data-testid": hasBackdrop ? 'tour-backdrop' : undefined,
116
+ "aria-hidden": "true",
117
+ onClick: hasBackdrop ? onBackdropClick : undefined,
118
+ ...{
119
+ 0: {
120
+ className: "xixxii4 x10a8y8t x1ghz6dp x1717udv xc342km xng3xce xjbqb8w x1rea2x4 x47corl"
121
+ },
122
+ 1: {
123
+ className: "xixxii4 x10a8y8t x1ghz6dp x1717udv xc342km xng3xce xjbqb8w x1rea2x4 x67bb7w"
124
+ }
125
+ }[!!hasBackdrop << 0],
126
+ children: /*#__PURE__*/_jsx("div", {
127
+ "data-testid": "tour-highlight",
128
+ ...{
129
+ 0: {
130
+ className: "x10l6tqk x47corl xr5mita x1gfj591 xuedmi6 xlr8y92"
131
+ },
132
+ 2: {
133
+ className: "x10l6tqk x47corl x1gfj591 xuedmi6 xlr8y92 xefcaua"
134
+ },
135
+ 1: {
136
+ className: "x10l6tqk x47corl xr5mita x1gfj591 xuedmi6 xlr8y92 xg01cxk"
137
+ },
138
+ 3: {
139
+ className: "x10l6tqk x47corl x1gfj591 xuedmi6 xlr8y92 xefcaua xg01cxk"
140
+ }
141
+ }[!!hasBackdrop << 1 | !!(rect == null) << 0],
142
+ style: rect != null ? {
143
+ top: rect.top - HIGHLIGHT_PADDING,
144
+ left: rect.left - HIGHLIGHT_PADDING,
145
+ width: rect.width + HIGHLIGHT_PADDING * 2,
146
+ height: rect.height + HIGHLIGHT_PADDING * 2,
147
+ borderRadius: rect.radius
148
+ } : undefined
149
+ })
150
+ });
151
+ }
152
+ /**
153
+ * A single spotlight step within a `<Tour>`. Renders its callout only while
154
+ * active.
155
+ *
156
+ * @example
157
+ * ```
158
+ * <TourStep targetRef={saveRef} heading="Save your work">
159
+ * Changes save automatically.
160
+ * </TourStep>
161
+ * ```
162
+ */
163
+ export function TourStep({
164
+ targetRef,
165
+ heading,
166
+ children,
167
+ placement = 'below',
168
+ alignment = 'start',
169
+ 'data-testid': testId
170
+ }) {
171
+ const tour = useContext(TourContext);
172
+ const id = useId();
173
+ const isActiveStep = tour != null && tour.activeStepId === id;
174
+
175
+ // The active target's viewport box, tracked so the highlight overlay can sit
176
+ // exactly over it. null until measured (or when inactive).
177
+ const [rect, setRect] = useState(null);
178
+
179
+ // Register with the controller on mount so the tour learns this step (and
180
+ // its position among siblings). Unregister on unmount.
181
+ useEffect(() => {
182
+ if (tour == null) {
183
+ return;
184
+ }
185
+ return tour.registerStep(id);
186
+ }, [tour, id]);
187
+
188
+ // Track the target's box while this step is active. Re-measure on
189
+ // scroll/resize so the highlight stays glued to the target.
190
+ useEffect(() => {
191
+ const el = targetRef.current;
192
+ if (el == null || !isActiveStep) {
193
+ setRect(null);
194
+ return;
195
+ }
196
+ const measure = () => {
197
+ const box = el.getBoundingClientRect();
198
+ setRect({
199
+ top: box.top,
200
+ left: box.left,
201
+ width: box.width,
202
+ height: box.height,
203
+ radius: getComputedStyle(el).borderRadius || '0px'
204
+ });
205
+ };
206
+ measure();
207
+ window.addEventListener('scroll', measure, true);
208
+ window.addEventListener('resize', measure);
209
+ return () => {
210
+ window.removeEventListener('scroll', measure, true);
211
+ window.removeEventListener('resize', measure);
212
+ setRect(null);
213
+ };
214
+ }, [targetRef, isActiveStep]);
215
+
216
+ // Outside a <Tour>, or when this isn't the active step, render nothing.
217
+ if (tour == null || !isActiveStep) {
218
+ return null;
219
+ }
220
+ const {
221
+ activeStepIndex,
222
+ stepCount,
223
+ isStepCountShown,
224
+ hasBackdrop,
225
+ onNext,
226
+ onPrevious,
227
+ onDismiss
228
+ } = tour;
229
+ const isFirstStep = activeStepIndex <= 0;
230
+ const isLastStep = stepCount > 0 && activeStepIndex === stepCount - 1;
231
+ const content = /*#__PURE__*/_jsxs("div", {
232
+ ...{
233
+ className: "x78zum5 xdt5ytf xjcht0a x1va8c73"
234
+ },
235
+ "data-testid": testId,
236
+ children: [/*#__PURE__*/_jsxs(VStack, {
237
+ gap: 1,
238
+ children: [/*#__PURE__*/_jsx(Heading, {
239
+ level: 4,
240
+ children: heading
241
+ }), children != null && /*#__PURE__*/_jsx(Text, {
242
+ type: "body",
243
+ children: children
244
+ })]
245
+ }), /*#__PURE__*/_jsxs(HStack, {
246
+ gap: 2,
247
+ xstyle: styles.footer,
248
+ children: [isStepCountShown && stepCount > 0 ? /*#__PURE__*/_jsx(Text, {
249
+ type: "supporting",
250
+ color: "secondary",
251
+ children: `${activeStepIndex + 1} of ${stepCount}`
252
+ }) : /*#__PURE__*/_jsx("span", {}), /*#__PURE__*/_jsxs(HStack, {
253
+ gap: 2,
254
+ children: [!isFirstStep && /*#__PURE__*/_jsx(Button, {
255
+ variant: "ghost",
256
+ size: "sm",
257
+ label: "Back",
258
+ onClick: onPrevious
259
+ }), /*#__PURE__*/_jsx(Button, {
260
+ variant: "primary",
261
+ size: "sm",
262
+ label: isLastStep ? 'Done' : 'Next',
263
+ onClick: onNext
264
+ })]
265
+ })]
266
+ })]
267
+ });
268
+ return /*#__PURE__*/_jsxs(_Fragment, {
269
+ children: [/*#__PURE__*/_jsx(TourHighlight, {
270
+ rect: rect,
271
+ hasBackdrop: hasBackdrop,
272
+ onBackdropClick: () => onDismiss('backdrop')
273
+ }), /*#__PURE__*/_jsx(Popover
274
+ // Popover types anchorRef as RefObject<HTMLElement>; TourStep accepts a
275
+ // nullable ref for ergonomics (useRef<HTMLButtonElement>(null)). Popover
276
+ // guards a null `.current` internally, so this widening is safe.
277
+ , {
278
+ anchorRef: targetRef,
279
+ isOpen: true,
280
+ onOpenChange: open => {
281
+ // Popover reports close from light-dismiss (backdrop) or Escape.
282
+ // Route it to the tour as a dismissal so the whole tour ends, not
283
+ // just this step's popover. Escape and outside-click both surface
284
+ // here.
285
+ if (!open) {
286
+ onDismiss('close');
287
+ }
288
+ },
289
+ placement: placement,
290
+ alignment: alignment
291
+ // Size the callout to its content instead of matching the target's
292
+ // width (Popover's default minWidth: anchor-size(width) makes it span a
293
+ // wide target); the content's own maxWidth caps it.
294
+ ,
295
+ width: "fit-content"
296
+ // Push the callout clear of the highlight ring so the ring is never
297
+ // covered (placement-aware; adds to Popover's own anchor gap).
298
+ ,
299
+ xstyle: calloutGapStyles[placement],
300
+ label: typeof heading === 'string' ? heading : 'Tour step',
301
+ hasCloseButton: true,
302
+ closeButtonLabel: "Close tour",
303
+ content: content
304
+ })]
305
+ });
306
+ }
307
+ TourStep.displayName = 'TourStep';