{"version":3,"file":"Tour.cjs","names":[],"sources":["../../../src/components/Tour/Tour.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — a product tour is a\n * controlled sequence (steps, index, onIndexChange, open, onClose, onFinish) drawn\n * over someone else's DOM (spotlightPadding, locale, className). The body measures\n * the target element, positions the spotlight and the card against it, and re-\n * measures on scroll and resize — one geometry, three triggers.\n */\nimport {\n    useCallback,\n    useEffect,\n    useId,\n    useLayoutEffect,\n    useRef,\n    useState,\n    type KeyboardEvent,\n    type ReactNode,\n} from \"react\";\n\nimport { useFocusTrap } from \"@/hooks/use-focus-trap\";\nimport { cn } from \"@/utils/cn\";\n\nimport { backdropRects, placeCard, type TourPlacement, type TourRect } from \"./tour-position\";\nimport styles from \"./Tour.module.css\";\n\n/** One stop of the tour. */\nexport interface TourStep {\n    /**\n     * CSS selector of the element to point at.\n     *\n     * A selector rather than a ref, so a tour can be declared as data — in a config\n     * file, from the backend, next to the copy — without every screen having to\n     * thread refs up to whoever renders the tour.\n     */\n    target?: string;\n    /** Heading of the card. */\n    title?: ReactNode;\n    /** The explanation. */\n    body: ReactNode;\n    /** Preferred side. Default `\"bottom\"`, flipped when it does not fit. */\n    placement?: TourPlacement;\n}\n\n/** Labels, per locale. */\ninterface TourStrings {\n    next: string;\n    back: string;\n    finish: string;\n    skip: string;\n    close: string;\n    progress: (current: number, total: number) => string;\n}\n\nconst PT_BR: TourStrings = {\n    next: \"Próximo\",\n    back: \"Voltar\",\n    finish: \"Concluir\",\n    skip: \"Pular\",\n    close: \"Fechar tour\",\n    progress: (current, total) => `Passo ${current} de ${total}`,\n};\n\nconst EN: TourStrings = {\n    next: \"Next\",\n    back: \"Back\",\n    finish: \"Done\",\n    skip: \"Skip\",\n    close: \"Close tour\",\n    progress: (current, total) => `Step ${current} of ${total}`,\n};\n\nexport interface TourProps {\n    /** The stops, in order. */\n    steps: readonly TourStep[];\n    /** Whether the tour is showing. Controlled. */\n    open: boolean;\n    /** Closed by `Esc`, by the close button or by \"skip\". */\n    onClose: () => void;\n    /** Called after the last step is confirmed. `onClose` follows it. */\n    onFinish?: () => void;\n    /** Current step index, when the app wants to drive it. */\n    index?: number;\n    /** Step changed — required only when `index` is given. */\n    onIndexChange?: (index: number) => void;\n    /** Locale for the labels. Default `\"pt-BR\"`. */\n    locale?: \"pt-BR\" | \"en\";\n    /** Space kept clear around the highlighted element, in px. Default 4. */\n    spotlightPadding?: number;\n    /** Extra class on the card. */\n    className?: string;\n}\n\n/**\n * A guided tour: dim the page, highlight one element at a time, explain it.\n *\n * The highlighted element stays **clickable** while everything else is blocked,\n * because the useful kind of coachmark says \"press this\" and lets you press it. That\n * is why the backdrop is four rectangles around the target rather than one overlay\n * with a `box-shadow` hole — a shadow is not hit-testable, so a hole made that way\n * would not block anything.\n *\n * Whether a user has seen the tour is the app's business: this component takes\n * `open` and emits `onClose`/`onFinish`. Persisting a flag in `localStorage` is one\n * line in the app and a wrong default here.\n *\n * @example\n * const [open, setOpen] = useState(!storage.get(\"tour-v1\"));\n *\n * <Tour\n *     open={open}\n *     steps={[\n *         { target: \"#novo-pedido\", title: \"Comece aqui\", body: \"Todo pedido nasce deste botão.\" },\n *         { target: \"[data-tour='filtros']\", body: \"E filtre por período aqui.\" },\n *     ]}\n *     onClose={() => setOpen(false)}\n *     onFinish={() => storage.set(\"tour-v1\", true)}\n * />\n */\nexport function Tour({\n    steps,\n    open,\n    onClose,\n    onFinish,\n    index,\n    onIndexChange,\n    locale = \"pt-BR\",\n    spotlightPadding = 4,\n    className,\n}: TourProps) {\n    const strings = locale === \"en\" ? EN : PT_BR;\n    const [internal, setInternal] = useState(0);\n    const current = Math.min(index ?? internal, Math.max(0, steps.length - 1));\n    const step = steps[current];\n\n    const card = useRef<HTMLDivElement | null>(null);\n    const [target, setTarget] = useState<TourRect | null>(null);\n    const [position, setPosition] = useState<{\n        placement: TourPlacement;\n        top: number;\n        left: number;\n    }>({ placement: \"center\", top: 0, left: 0 });\n    const [viewport, setViewport] = useState({ width: 0, height: 0 });\n    const titleId = useId();\n    const bodyId = useId();\n\n    useFocusTrap(card, open);\n\n    const goTo = useCallback(\n        (next: number) => {\n            if (index === undefined) setInternal(next);\n            onIndexChange?.(next);\n        },\n        [index, onIndexChange],\n    );\n\n    /** Reset to the first step whenever the tour opens again. */\n    useEffect(() => {\n        if (open && index === undefined) setInternal(0);\n    }, [open, index]);\n\n    /**\n     * Find the step's element, bring it into view, and measure everything.\n     *\n     * The scroll happens before measuring, and the measurement is deferred to the\n     * next frame: reading a rect in the same tick as `scrollIntoView` gives the\n     * position the element had *before* the scroll, which lands the card over the\n     * wrong part of the page.\n     */\n    const measure = useCallback(() => {\n        if (!open || !step) return;\n        const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n        const rect = element?.getBoundingClientRect() ?? null;\n        setTarget(\n            rect\n                ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n                : null,\n        );\n\n        const size = {\n            width: card.current?.offsetWidth ?? 320,\n            height: card.current?.offsetHeight ?? 160,\n        };\n        const view = { width: window.innerWidth, height: window.innerHeight };\n        setViewport(view);\n        setPosition(\n            placeCard({\n                target: rect\n                    ? { top: rect.top, left: rect.left, width: rect.width, height: rect.height }\n                    : null,\n                card: size,\n                viewport: view,\n                preferred: step.placement,\n            }),\n        );\n    }, [open, step]);\n\n    useLayoutEffect(() => {\n        if (!open || !step) return;\n        const element = step.target ? document.querySelector<HTMLElement>(step.target) : null;\n        element?.scrollIntoView({ block: \"center\", inline: \"nearest\", behavior: \"smooth\" });\n        const frame = requestAnimationFrame(measure);\n        return () => cancelAnimationFrame(frame);\n    }, [open, step, measure]);\n\n    useEffect(() => {\n        if (!open) return;\n        window.addEventListener(\"resize\", measure);\n        window.addEventListener(\"scroll\", measure, true);\n        return () => {\n            window.removeEventListener(\"resize\", measure);\n            window.removeEventListener(\"scroll\", measure, true);\n        };\n    }, [open, measure]);\n\n    useEffect(() => {\n        if (open) card.current?.focus();\n    }, [open, current]);\n\n    if (!open || !step) return null;\n\n    const last = current === steps.length - 1;\n\n    const finish = (): void => {\n        onFinish?.();\n        onClose();\n    };\n\n    /**\n     * Arrow keys walk the tour and `Esc` leaves it.\n     *\n     * `Esc` is handled here rather than on `window` so a tour opened over a modal\n     * does not close both: the innermost handler that sees the key wins, and the\n     * card has focus.\n     */\n    const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {\n        if (event.key === \"Escape\") {\n            event.stopPropagation();\n            onClose();\n            return;\n        }\n        if (event.key === \"ArrowRight\" && !last) {\n            event.preventDefault();\n            goTo(current + 1);\n        }\n        if (event.key === \"ArrowLeft\" && current > 0) {\n            event.preventDefault();\n            goTo(current - 1);\n        }\n    };\n\n    return (\n        <div className={styles.root} data-placement={position.placement}>\n            {backdropRects(target, viewport, spotlightPadding).map((rect, i) => (\n                <div\n                    key={i}\n                    className={styles.backdrop}\n                    style={{\n                        top: rect.top,\n                        left: rect.left,\n                        width: rect.width,\n                        height: rect.height,\n                    }}\n                    onClick={onClose}\n                />\n            ))}\n\n            {target && (\n                <div\n                    className={styles.spotlight}\n                    style={{\n                        top: target.top - spotlightPadding,\n                        left: target.left - spotlightPadding,\n                        width: target.width + spotlightPadding * 2,\n                        height: target.height + spotlightPadding * 2,\n                    }}\n                />\n            )}\n\n            <div\n                ref={card}\n                className={cn(styles.card, className)}\n                style={{ top: position.top, left: position.left }}\n                role=\"dialog\"\n                aria-modal=\"true\"\n                aria-labelledby={step.title ? titleId : undefined}\n                aria-describedby={bodyId}\n                tabIndex={-1}\n                onKeyDown={handleKeyDown}\n            >\n                <div className={styles.header}>\n                    <span className={styles.progress}>\n                        {strings.progress(current + 1, steps.length)}\n                    </span>\n                    <button\n                        type=\"button\"\n                        className={styles.close}\n                        aria-label={strings.close}\n                        onClick={onClose}\n                    >\n                        ×\n                    </button>\n                </div>\n\n                {step.title && (\n                    <h2 className={styles.title} id={titleId}>\n                        {step.title}\n                    </h2>\n                )}\n                <div className={styles.body} id={bodyId}>\n                    {step.body}\n                </div>\n\n                <div className={styles.footer}>\n                    {!last && (\n                        <button type=\"button\" className={styles.skip} onClick={onClose}>\n                            {strings.skip}\n                        </button>\n                    )}\n                    <div className={styles.actions}>\n                        {current > 0 && (\n                            <button\n                                type=\"button\"\n                                className={styles.secondary}\n                                onClick={() => goTo(current - 1)}\n                            >\n                                {strings.back}\n                            </button>\n                        )}\n                        <button\n                            type=\"button\"\n                            className={styles.primary}\n                            onClick={() => (last ? finish() : goTo(current + 1))}\n                        >\n                            {last ? strings.finish : strings.next}\n                        </button>\n                    </div>\n                </div>\n            </div>\n        </div>\n    );\n}\n"],"mappings":"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"}