<!-- GENERATED by scripts/build-llms.mjs from llms/utility.md — do not edit this file. -->

# `lr-tour`

- **Import** `import '@aceshooting/lyra-ui/components/lr-tour.js';` (stable tag alias; registers the tag)
- **Class** `LyraTour`, also available unregistered from `@aceshooting/lyra-ui/components/utility/tour/tour.class.js`
- **Family** `components/utility/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** none
- **Themeable via** 12 parts, 7 custom properties — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-tour`

A spotlight-and-step guided walkthrough for first-run onboarding: each step anchors a popover to a
target element elsewhere in the page via the shared Floating UI positioner, over a dimmed
full-viewport backdrop with a cutout/ring around the current target, with Next/Previous/Skip
controls and a step-progress indicator. Controlled component — `steps` is never mutated; only
`activeIndex` and `open` are self-managed.

**Properties:**

- `open: boolean = false` (reflected) — no separate `show()`/`hide()`; set this or call
  `start()`/`end()`
- `steps: readonly Readonly<LyraTourStep>[] = []` (attribute: false) — assignment takes a shallow
  frozen snapshot; empty renders nothing
- `activeIndex: number = 0` (attribute `active-index`, reflected) — clamped to
  `[0, steps.length - 1]` as a finite integer, including for a direct property/attribute write that
  bypasses `goToStep()`; fractions floor and non-finite values fall back to `0`
- `placement: Placement = 'bottom'` (reflected) — tour-level Floating UI default, overridable per
  step; resolved through `rtlAwarePlacement()`
- `distance: number = 12` — px offset between target and popover. Tour-level only (no per-step
  override); may be negative for overlap
- `spotlightPadding: number = 4` (attribute `spotlight-padding`) — extra px between the target's box
  and the cutout/ring; overridable per step
- `lightDismiss: boolean = false` (attribute `light-dismiss`) — a deliberate inversion of
  `lr-dialog`'s `lightDismiss`: a backdrop click does **nothing** by default so a stray click
  can't discard onboarding progress. Set it to make a backdrop click `end('skip')`
- `showProgress: boolean = true` (attribute `show-progress`) — renders the "Step X of Y" text + dots
- `aria-label` (a plain host attribute, not a public JS property) — names **every** step's popover,
  overriding each step's own `heading` as the `aria-labelledby` source

**Exported types:** `LyraTourStep { stepId: string; target: LyraTourTarget; heading: string; content?: string;
placement?: Placement; spotlightPadding?: number; interactiveTarget?: boolean; hidePrevious?: boolean }`;
`LyraTourTarget = string | HTMLElement | (() => HTMLElement | null)` — a string resolves via
`ownerDocument.querySelector` (top-level light DOM only). Every form resolves exactly once when the
step becomes active and is kept as one connected snapshot for that activation, then resolves again
on a later activation/reconnect. Invalid selectors, throwing resolvers, non-`HTMLElement` results,
and detached elements use the normal missing-target path instead of throwing. `heading` normally
becomes the panel's accessible name; a defensive blank/whitespace heading uses the localized
"Step X of Y" string instead, including when progress is visually hidden or the target is missing.
`content` renders as plain text (no HTML/markdown parsing).
Assigning `steps` clone-normalizes at most 256 own-data plain records into an immutable snapshot;
accessor/inherited/malformed rows and invalid optional fields are omitted while later valid siblings
remain usable. Step ids and headings are bounded to 256 and 4,096 characters, target selector
strings to 8,192, and body content to 65,536. Per-step spotlight padding is finite, non-negative,
and capped at 10,000px. Provider mutation after assignment cannot change rendering or event detail.
`LyraTourEndReason = 'completed' | 'skip' | 'escape' | 'api' | 'unmount' | (string & {})`.

**Methods:** `start(index = 0)` (clamps, opens, emits `lr-tour-start`), `next()` (on the last step
ends with `'completed'` instead), `back()` (no-op on the first step), `goToStep(index)` (clamped),
`skip()` (sugar for `end('skip')`), `end(reason: LyraTourEndReason = 'api')`.

**Events:** `lr-tour-start` (`detail: { index }`, not cancelable); `lr-tour-step-change`
(`detail: { index, previousIndex, step, via: 'next'|'back'|'goto' }`, **cancelable** — fires before
`activeIndex` changes, so `preventDefault()` gates advancement on a real action; a deliberate
departure from `lr-carousel`'s non-cancelable `lr-slide-change`); `lr-tour-end`
(`detail: LyraTourEndReason`, cancelable except in practice for `'unmount'`, which is emitted when the
element is removed while still open by something other than its own `end()`);
`lr-tour-target-missing` (`detail: { index, step }`, informational — the tour does **not** auto-end,
it renders that step viewport-centered with no spotlight).

**Slots:** default — rich content replacing the active step's plain-text `content`. Not scoped per
step: a consumer needing different rich content per step swaps the slotted children itself (e.g. on
`lr-tour-step-change`).

**CSS parts:** `backdrop` (the full-viewport `<svg>` scrim with the cutout, `aria-hidden`),
`spotlight` (the decorative ring around the padded target rect, `pointer-events: none`),
`popover` (the step panel, `role="dialog"`, `aria-modal="true"` for default steps and `"false"` for
an `interactiveTarget` step, `data-unanchored` when the target didn't resolve), `heading` (the
`aria-labelledby` target), `body` (slot or `step.content`),
`progress` (wrapper), `progress-text` (the "Step X of Y" text — an `aria-describedby` target),
`progress-dot` (one decorative dot per step, `data-current` on the active one, `aria-hidden`),
`footer` (the control row), `previous-button`, `skip-button`, `next-button` (the Next control's
label switches to Done on the last step).

**Themeable custom properties:** `--lr-tour-backdrop-color` (default `--lr-color-overlay`),
`--lr-tour-spotlight-radius` (default `--lr-radius` — shared by the cutout and the ring),
`--lr-tour-spotlight-ring-color` (default `--lr-color-brand`), `--lr-tour-spotlight-ring-width`
(default `--lr-border-width-medium`), `--lr-tour-popover-max-width` (default `--lr-size-22rem`,
further capped by `--lr-popover-viewport-clamp` and the positioner's available space),
`--lr-tour-progress-dot-current-bg` (default `var(--lr-color-brand)`) — background of
`progress-dot` for the current step. Component-scoped indirection over the shared `--lr-color-brand`
token, so a consumer can retheme just the current-step dot without repainting every other component
that reuses the same shared brand token.

`--lr-popover-viewport-clamp` (default `92vw`, from `--lr-theme-popover-viewport-clamp`) is the
shared ceiling that keeps any floating surface inside a narrow viewport. `lr-tour`,
`lr-mention-popover`, and `lr-export-button` all `min()` their own max-inline-size against it, so
retuning `--lr-theme-popover-viewport-clamp` once at `:root` narrows or widens all three together
rather than per component.

`--lr-positioning-strategy` (16.0.0) — the step popover reads this same cascading `absolute`/`fixed`
override documented on `<lr-popover>` when a step is (re)positioned, falling back to its own `fixed`
default when nothing is set. There is no per-instance `positioning-strategy` property on `<lr-tour>`;
set the custom property on `:root`, a theme, or one clipping ancestor to change every unset tour
beneath it.

**Known gotchas:**

- By default the spotlighted target is **non-interactive**: it stays visible and announceable (not
  `inert`, not `aria-hidden`) but every pointer event over the viewport is captured by the backdrop.
  A default step uses a modal overlay and traps focus in the panel. Set `step.interactiveTarget` to
  clip the backdrop around the target, switch the panel to nonmodal semantics, and install an
  explicit bounded Tab scope over the panel and the live composed target descendants, so nested,
  forwarded, dynamically disabled, and inert controls cannot leak focus to unrelated page content.
- A live transition between modal and interactive-target steps preserves the trigger captured when
  the tour opened. Intermediate overlay ownership changes never restore focus; a genuine end returns
  to that original trigger when it is still connected.
- Each step transition mounts a genuinely new popover node keyed on occurrence index plus
  `step.stepId`, so repeated business ids cannot collapse distinct occurrences. Every step-related
  event exposes the occurrence index, which is the authoritative collection identity; focus
  reliably re-enters the panel — don't cache a reference to `[part="popover"]` across steps.
- No `Home`/`End` shortcut and no click-to-jump progress dots (unlike `lr-stepper`): later steps may
  depend on an earlier step's side effect having run. `goToStep()` is available for a caller that
  knows better. ArrowRight/ArrowLeft do move between steps (swapped under RTL), except while focus
  is in an `input`/`textarea`/`contenteditable` inside slotted content.
- A direct `HTMLElement` target must be connected at activation and is treated as missing after a
  remount; prefer a selector string or resolver function for targets that can be replaced between
  activations.
- The active step's target is `scrollIntoView({ block: 'center' })`'d on activation, smoothly unless
  `prefers-reduced-motion: reduce`.
- Changing `placement`, `distance`, or tour-level `spotlightPadding` while open repositions/repaints
  the current step live without scrolling again or emitting a duplicate `lr-tour-target-missing`.
