// Stepper -- ordered progress through a sequence. // // // // Each step can be authored by hand instead of using the built-in body, by // passing a slot named for its index: // // //
...anything...
//
// // NOTE: the style block uses /* */ comments only -- // is not a CSS comment // and silently swallows the rule that follows it. component Stepper { outputs { change(payload: { index: number; step: object }) back(payload: { index: number; step: object }) next(payload: { index: number; step: object }) skip(payload: { index: number; step: object }) finish(payload: { index: number; step: object }) } props { color: string = "primary" size: string = "default" steps: unknown[] = [] active: number = 0 orientation: string = "horizontal" clickable: boolean = false label: string = "Progress" // Render each step body, showing only the active one -- same contract as // Tabs, so a wizard does not have to hand-roll the panel switching. showPanel: boolean = false controls: boolean = false allowSkip: boolean = false // Lets a form hold the step. The component never validates anything // itself: the page owns the form, so it sets this while the step is // incomplete and clears it once the step passes. nextDisabled: boolean = false backLabel: string = "Back" nextLabel: string = "Next" skipLabel: string = "Skip" finishLabel: string = "Finish" class: string = "" } functions { shared function stepList() { return Array.isArray(steps) ? steps : [] } shared function activeIndex() { var count = stepList().length if (count < 1) { return 0 } var value = Number(active) if (!value || value < 0) { return 0 } return Math.min(value, count - 1) } shared function statusFor(index) { if (index < activeIndex()) { return "complete" } if (index === activeIndex()) { return "current" } return "upcoming" } // An empty orientation is how the roving runtime is told to stay out of // the way, so a read-only stepper never takes arrow-key focus. shared function rovingAxis() { if (!clickable) { return "" } return orientation === "vertical" ? "vertical" : "horizontal" } shared function isActiveStep(index) { return index === activeIndex() } shared function lastIndex() { return Math.max(0, stepList().length - 1) } shared function onLastStep() { return activeIndex() >= lastIndex() } shared function activeStep() { var list = stepList() return list.length ? list[activeIndex()] : {} } client function goBack() { var target = Math.max(0, activeIndex() - 1) output.back({ index: target, step: stepList()[target] || {} }) output.change({ index: target, step: stepList()[target] || {} }) } client function goNext() { if (nextDisabled) { return } if (onLastStep()) { output.finish({ index: activeIndex(), step: activeStep() }) return } var target = Math.min(lastIndex(), activeIndex() + 1) output.next({ index: target, step: stepList()[target] || {} }) output.change({ index: target, step: stepList()[target] || {} }) } client function goSkip() { var target = Math.min(lastIndex(), activeIndex() + 1) output.skip({ index: target, step: stepList()[target] || {} }) output.change({ index: target, step: stepList()[target] || {} }) } client function selectStep(index, step) { if (!clickable) { return } output.change({ index: index, step: step }) } } view {
    {#each stepList() as step, index}
  1. {/each}
{#each stepList() as step, index}

{step.title}

{step.content}

{/each}
} style { .wrn-stepper { --stepper-accent: var(--wrn-color-primary); max-width: 100%; } .wrn-stepper__list { display: flex; gap: 0.5rem; margin: 0; padding: 0; list-style: none; max-width: 100%; } .wrn-stepper[data-color="secondary"] { --stepper-accent: var(--wrn-color-secondary); } .wrn-stepper[data-color="success"] { --stepper-accent: var(--wrn-color-success); } .wrn-stepper[data-color="danger"] { --stepper-accent: var(--wrn-color-danger); } .wrn-stepper[data-color="info"] { --stepper-accent: var(--wrn-color-info); } .wrn-stepper[data-size="sm"] { font-size: 0.82rem; } .wrn-stepper[data-size="lg"] { font-size: 1rem; } .wrn-stepper[data-orientation="vertical"] .wrn-stepper__list { flex-direction: column; } .wrn-stepper__step { display: flex; flex-direction: column; flex: 1 1 0; min-width: 0; gap: 0.35rem; } .wrn-stepper__button { appearance: none; display: flex; align-items: center; gap: 0.6rem; width: 100%; padding: 0.5rem; border: 0; border-radius: var(--wrn-radius-sm); background: transparent; color: inherit; font: inherit; text-align: left; cursor: default; } .wrn-stepper[data-clickable="true"] .wrn-stepper__button { cursor: pointer; } .wrn-stepper[data-clickable="true"] .wrn-stepper__button:hover { background: var(--wrn-color-surface-soft); } .wrn-stepper__button:focus-visible { outline: 2px solid var(--stepper-accent); outline-offset: 2px; } .wrn-stepper__marker { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; width: 2rem; height: 2rem; border: 1px solid var(--wrn-color-border); border-radius: 999px; background: var(--wrn-color-surface); font-size: 0.85rem; font-weight: 700; } .wrn-stepper__step[data-status="complete"] .wrn-stepper__marker { border-color: var(--stepper-accent); background: var(--stepper-accent); color: var(--wrn-color-primary-contrast); } .wrn-stepper__step[data-status="current"] .wrn-stepper__marker { border-color: var(--stepper-accent); color: var(--stepper-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--stepper-accent) 22%, transparent); } .wrn-stepper__step[data-status="upcoming"] .wrn-stepper__marker { color: var(--wrn-color-text-muted); } .wrn-stepper__body { display: flex; flex-direction: column; min-width: 0; } .wrn-stepper__label { font-size: 0.9rem; font-weight: 600; } .wrn-stepper__description { color: var(--wrn-color-text-muted); font-size: 0.78rem; } .wrn-stepper__step[data-status="upcoming"] .wrn-stepper__label { color: var(--wrn-color-text-muted); } .wrn-stepper__panels { margin-top: 1rem; } .wrn-stepper__panel { padding: 1rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-md); background: var(--wrn-color-surface); animation: wrn-stepper-in 200ms ease both; } .wrn-stepper__panel-title { margin: 0 0 0.35rem; font-size: 0.95rem; font-weight: 700; } .wrn-stepper__panel-body { margin: 0; color: var(--wrn-color-text-muted); line-height: 1.6; } .wrn-stepper__controls { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.85rem; } .wrn-stepper__controls-spacer { flex: 1 1 auto; } .wrn-stepper__button-control { appearance: none; padding: 0.45rem 0.9rem; border: 1px solid var(--wrn-color-border); border-radius: var(--wrn-radius-sm); background: var(--wrn-color-surface); color: var(--wrn-color-text); font: inherit; font-size: 0.86rem; font-weight: 600; cursor: pointer; } .wrn-stepper__button-control:hover:not(:disabled) { background: var(--wrn-color-surface-soft); } .wrn-stepper__button-control:focus-visible { outline: 2px solid var(--stepper-accent); outline-offset: 2px; } .wrn-stepper__button-control[data-primary="true"] { border-color: var(--stepper-accent); background: var(--stepper-accent); color: var(--wrn-color-primary-contrast); } /* A held step has to look held, or the button reads as broken. */ .wrn-stepper__button-control:disabled { opacity: 0.45; cursor: not-allowed; } @keyframes wrn-stepper-in { from { opacity: 0; transform: translateX(0.75rem); } to { opacity: 1; transform: none; } } /* A horizontal stepper cannot stay side by side on a phone. */ @media (max-width: 639px) { .wrn-stepper__list { flex-direction: column; } } } }