{"version":3,"file":"stepper.cjs","names":[],"sources":["../src/content/stepper/stepper.ts"],"sourcesContent":["import {\n  bind,\n  createContext,\n  define,\n  getHost,\n  html,\n  onCleanup,\n  onMounted,\n  prop,\n  provide,\n  useEmit,\n  useSlots,\n} from '@vielzeug/ore';\nimport { computed, type Readable, signal, watch } from '@vielzeug/ripple';\nimport { createInteraction, createListControl, elementDirection, lifecycleSignal } from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport { isStepNavigable } from './_is-step-navigable';\nimport componentStyles from './stepper.css?inline';\n\n// ── Context ─────────────────────────────────────────────────────────────────\n//\n// ore-step derives all of its parent-relative state (current/completed/navigable/\n// index/total/color/size/orientation) from this context instead of ore-stepper\n// pushing 8 attributes onto every child on every change. Matches the\n// provide()/inject() coordination already used by ore-tabs/ore-tab-item,\n// ore-radio-group/ore-radio, and ore-list/ore-list-item — ore-stepper was\n// previously the one outlier doing manual `querySelectorAll` + `setAttribute`\n// fan-out, which meant every reactive change re-walked and re-wrote every step\n// regardless of whether that step's own state actually changed.\n\n/** Context provided by ore-stepper to its ore-step children. */\nexport type StepperContext = {\n  clickable: Readable<boolean>;\n  color: Readable<ThemeColor | undefined>;\n  currentValue: Readable<string | undefined>;\n  linear: Readable<boolean>;\n  orientation: Readable<'horizontal' | 'vertical'>;\n  size: Readable<ComponentSize | undefined>;\n  /** Ordered `value`s of every sibling `ore-step`, used to derive index/total/completed. */\n  stepValues: Readable<string[]>;\n};\n/** Injection key for the stepper context. */\nexport const STEPPER_CTX = createContext<StepperContext>('StepperContext');\n\nexport type OreStepperEvents = {\n  change: { value: string };\n};\n\nexport type OreStepperProps = {\n  /** When true, steps are clickable/keyboard-focusable for navigation. Default: display-only progress. */\n  clickable?: boolean;\n  /** Theme color for the current/completed step indicators */\n  color?: ThemeColor;\n  /** Disables the whole stepper — no step is navigable regardless of `clickable` */\n  disabled?: boolean;\n  /** Accessible label for the nav landmark */\n  label?: string;\n  /**\n   * Restricts navigation to completed steps and the current step — steps ahead of the\n   * current one cannot be clicked or focused, even when `clickable` is set.\n   */\n  linear?: boolean;\n  /** Layout orientation — 'horizontal' (default, desktop) or 'vertical' (compact/mobile-friendly) */\n  orientation?: 'horizontal' | 'vertical';\n  /** Component size */\n  size?: ComponentSize;\n  /** The `value` of the currently active `ore-step` */\n  value?: string;\n};\n\n/**\n * Displays progress through a sequence of numbered steps. Manages step selection and provides\n * shared state (current value, clickability, theming) to its `ore-step` children via context.\n * Can be purely informational (progress display) or interactive navigation.\n *\n * @element ore-stepper\n * @element ore-step - Child element for each step (auto-discovered)\n *\n * @attr {string} value - The value of the currently active step\n * @attr {boolean} clickable - Allow clicking/focusing steps to navigate\n * @attr {boolean} linear - Restrict navigation to completed + current steps only\n * @attr {boolean} disabled - Disables the whole stepper\n * @attr {string} orientation - 'horizontal' (default) | 'vertical'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} size - Size: 'sm' | 'md' | 'lg'\n * @attr {string} label - Accessible nav landmark label (default: 'Progress')\n *\n * @fires change - Emitted when the active step changes via click/keyboard. detail: { value: string }\n *\n * @slot - `ore-step` elements\n *\n * @cssprop --stepper-connector-color - Color of the connector line between steps\n * @cssprop --stepper-connector-size - Thickness of the connector line\n * @cssprop --stepper-gap - Gap between steps\n *\n * @part nav - Navigation landmark element\n * @part list - Ordered list container holding the slotted steps\n *\n * @example\n * ```html\n * <!-- Display-only progress -->\n * <ore-stepper value=\"shipping\" color=\"primary\">\n *   <ore-step value=\"cart\">Cart</ore-step>\n *   <ore-step value=\"shipping\">Shipping</ore-step>\n *   <ore-step value=\"payment\">Payment</ore-step>\n * </ore-stepper>\n *\n * <!-- Clickable navigation, mobile-friendly vertical layout -->\n * <ore-stepper value=\"shipping\" clickable linear orientation=\"vertical\">\n *   <ore-step value=\"cart\">Cart</ore-step>\n *   <ore-step value=\"shipping\">Shipping</ore-step>\n *   <ore-step value=\"payment\" disabled>Payment</ore-step>\n * </ore-stepper>\n * ```\n */\nexport const STEPPER_TAG = 'ore-stepper' as const;\ndefine<OreStepperProps>(STEPPER_TAG, {\n  props: {\n    ...themableBundle,\n    ...sizableBundle,\n    ...disablableBundle,\n    clickable: prop.bool(false),\n    label: prop.string('Progress'),\n    linear: prop.bool(false),\n    orientation: prop.oneOf(['horizontal', 'vertical'] as const, 'horizontal'),\n    value: prop.string(),\n  },\n  setup(props) {\n    const el = getHost();\n    const emit = useEmit<OreStepperEvents>();\n    const slots = useSlots();\n\n    const getSteps = (): HTMLElement[] => Array.from(el.querySelectorAll(':scope > ore-step')) as HTMLElement[];\n\n    const focusStep = (step: HTMLElement | undefined) => {\n      step?.shadowRoot?.querySelector<HTMLElement>('button.control')?.focus();\n    };\n\n    // ────────────────────────────────────────────────────────────────\n    // Selection State — mirrors ore-tabs' `selectedValue` signal + `ensureSelection()` pattern\n    // ────────────────────────────────────────────────────────────────\n\n    const currentValue = signal<string | undefined>(props.value.value);\n\n    bind({ attr: { value: () => currentValue.value ?? null } });\n\n    const setSelection = (value: string | undefined, shouldEmit = false) => {\n      if (!value || value === currentValue.value) return;\n\n      currentValue.value = value;\n\n      if (shouldEmit) emit('change', { value });\n    };\n\n    const ensureSelection = () => {\n      const steps = getSteps();\n\n      if (steps.length === 0) return;\n\n      const current = currentValue.value;\n      const hasCurrent = current ? steps.some((s) => s.getAttribute('value') === current) : false;\n\n      if (hasCurrent) return;\n\n      const firstEnabled = steps.find((s) => !s.hasAttribute('disabled'))?.getAttribute('value') ?? undefined;\n\n      if (firstEnabled) setSelection(firstEnabled);\n    };\n\n    watch(props.value, (value) => {\n      currentValue.value = value;\n      ensureSelection();\n    });\n\n    // ────────────────────────────────────────────────────────────────\n    // Context provided to ore-step children\n    // ────────────────────────────────────────────────────────────────\n\n    const stepValues = computed(() => {\n      void slots.elements().value;\n\n      return getSteps().map((s) => s.getAttribute('value') ?? '');\n    });\n\n    provide(STEPPER_CTX, {\n      clickable: computed(() => Boolean(props.clickable.value) && !props.disabled.value),\n      color: props.color,\n      currentValue,\n      linear: computed(() => Boolean(props.linear.value)),\n      orientation: computed(() => props.orientation.value ?? 'horizontal'),\n      size: props.size,\n      stepValues,\n    });\n\n    // Deferred to `onMounted()` (mirrors ore-tabs' `ensureSelection()` timing) rather than an\n    // immediate `watch(stepValues, ...)` — `getSteps()` walks the *live* light DOM, and while the\n    // browser is still parsing this element's `ore-step` children (synchronously upgrading each\n    // one as its own tag is reached, which happens whenever `customElements.define()` already\n    // ran before this markup was parsed — exactly what a sandboxed live-preview iframe does by\n    // design), `stepValues` observes that child list mid-populate. Reading it eagerly here used\n    // to call `ensureSelection()` — and therefore write `currentValue` — once per step as each\n    // one was discovered, interleaved with that *same* step's own first render effect further\n    // down the reactive graph. That reentrant write during a child's not-yet-finished initial\n    // render corrupted its rendered output (the step's clickable/static control silently failed\n    // to mount at all). Onmounted's callback runs on a microtask, strictly after the whole\n    // synchronous parse (and thus every child) has completed, so `getSteps()` sees the final list.\n    onMounted(() => {\n      ensureSelection();\n      return undefined;\n    });\n\n    // ────────────────────────────────────────────────────────────────\n    // Keyboard Navigation (roving tabindex over navigable steps)\n    // ────────────────────────────────────────────────────────────────\n\n    const getNavigableSteps = (): HTMLElement[] => {\n      const steps = getSteps();\n      const values = steps.map((s) => s.getAttribute('value') ?? '');\n      const currentIndex = values.indexOf(currentValue.value ?? '');\n      const clickable = Boolean(props.clickable.value) && !props.disabled.value;\n      const linear = Boolean(props.linear.value);\n\n      return steps.filter((step, index) =>\n        isStepNavigable({\n          disabled: step.hasAttribute('disabled'),\n          index,\n          linear,\n          stepperClickable: clickable,\n          stepperCurrentIndex: currentIndex,\n        }),\n      );\n    };\n\n    const listControl = createListControl({\n      direction: () => elementDirection(el),\n      getItems: getNavigableSteps,\n      loop: false,\n      onNavigate: ({ item }) => {\n        // Select BEFORE focusing: changing `currentValue` re-renders each step's\n        // control (the step's navigable/button node-slot depends on the current\n        // index), which destroys the previously focused button. Ripple flushes\n        // effects synchronously, so by the time focusStep() runs the replacement\n        // button exists.\n        const value = item.getAttribute('value');\n\n        if (value) setSelection(value, true);\n\n        focusStep(item);\n      },\n      orientation: () => (props.orientation.value === 'vertical' ? 'vertical' : 'horizontal'),\n      signal: lifecycleSignal(onCleanup),\n    });\n\n    const handleStepClick = (e: Event) => {\n      const step = e\n        .composedPath()\n        .find((node): node is HTMLElement => node instanceof HTMLElement && node.localName === 'ore-step');\n\n      if (!step || step.closest(STEPPER_TAG) !== el || !getNavigableSteps().includes(step)) return;\n\n      const value = step.getAttribute('value');\n\n      setSelection(value ?? undefined, true);\n      // The click focused the step's old control; the selection re-render replaced\n      // it — restore focus onto the new one (see onNavigate's ordering note).\n      focusStep(step);\n    };\n\n    const activateFocusedStep = (): void => {\n      const steps = getNavigableSteps();\n      const focusedStep = steps.find(\n        (step) => step === document.activeElement || step.shadowRoot?.activeElement === document.activeElement,\n      );\n      const value = focusedStep?.getAttribute('value');\n\n      if (value) {\n        setSelection(value, true);\n        focusStep(focusedStep);\n      }\n    };\n\n    const activationPress = createInteraction({ onPress: activateFocusedStep });\n\n    const handleKeydown = (e: KeyboardEvent) => {\n      const steps = getNavigableSteps();\n\n      if (steps.length === 0) return;\n\n      const path = e.composedPath();\n      const stepFromEvent = path.find(\n        (node): node is HTMLElement => node instanceof HTMLElement && node.localName === 'ore-step',\n      );\n      const focused = stepFromEvent ? steps.indexOf(stepFromEvent) : -1;\n\n      if (focused >= 0) listControl.set(focused);\n\n      if (listControl.handleKeydown(e)) return;\n\n      activationPress.handleKeydown(e);\n    };\n\n    bind({\n      on: {\n        click: handleStepClick,\n        keydown: handleKeydown,\n      },\n    });\n\n    return html`\n      <nav part=\"nav\" aria-label=\"${props.label}\">\n        <ol class=\"steps\" role=\"list\" part=\"list\">\n          <slot></slot>\n        </ol>\n      </nav>\n    `;\n  },\n  styles: [componentStyles],\n});\n"],"mappings":"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"}