Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6, and rebuilds the editor compiler, language server and extension bundles that embed the version. The release carries the output delivery fix: camelCase outputs now reach parent bindings, and 18 components emit through output.* instead of hand-built CustomEvents. See the 0.8.6 migration entry for what changes for consumers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
443 lines
12 KiB
Plaintext
443 lines
12 KiB
Plaintext
// Stepper -- ordered progress through a sequence.
|
|
//
|
|
// <Stepper steps='[{"label":"Account"},{"label":"Billing"}]' active={1} />
|
|
//
|
|
// Each step can be authored by hand instead of using the built-in body, by
|
|
// passing a slot named for its index:
|
|
//
|
|
// <Stepper steps={steps} active={1}>
|
|
// <div data-slot="step-1">...anything...</div>
|
|
// </Stepper>
|
|
//
|
|
// 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 {
|
|
<div
|
|
{...attrs}
|
|
data-ui-component="Stepper"
|
|
class='wire-stepper {class}'
|
|
data-orientation='{orientation}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-clickable='{clickable}'
|
|
>
|
|
<ol
|
|
class="wire-stepper__list"
|
|
data-wrn-roving='{rovingAxis()}'
|
|
aria-label='{label}'
|
|
>
|
|
{#each stepList() as step, index}
|
|
<li
|
|
class="wire-stepper__step"
|
|
data-status='{statusFor(index)}'
|
|
aria-current='{statusFor(index) === "current" ? "step" : "false"}'
|
|
>
|
|
<button
|
|
type="button"
|
|
class="wire-stepper__button"
|
|
data-wrn-roving-item='{clickable ? "true" : "false"}'
|
|
@click='selectStep(index, step)'
|
|
>
|
|
<span class="wire-stepper__marker" aria-hidden="true">
|
|
<span class='wire-stepper__icon {step.icon}' data-show="step.icon"></span>
|
|
<span class="wire-stepper__number" data-show="!step.icon">{index + 1}</span>
|
|
</span>
|
|
<span class="wire-stepper__body">
|
|
<span class="wire-stepper__label">{step.label}</span>
|
|
<span class="wire-stepper__description" data-show="step.description">
|
|
{step.description}
|
|
</span>
|
|
</span>
|
|
</button>
|
|
<span class="wire-stepper__custom">
|
|
<slot name="step-{index}"></slot>
|
|
</span>
|
|
</li>
|
|
{/each}
|
|
</ol>
|
|
|
|
<div class="wire-stepper__panels" data-show="showPanel">
|
|
{#each stepList() as step, index}
|
|
<div
|
|
class="wire-stepper__panel"
|
|
data-show='isActiveStep(index)'
|
|
aria-hidden='{index === activeIndex() ? "false" : "true"}'
|
|
>
|
|
<h4 class="wire-stepper__panel-title" data-show="step.title">{step.title}</h4>
|
|
<p class="wire-stepper__panel-body" data-show="step.content">{step.content}</p>
|
|
<slot name="panel-{index}"></slot>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<div class="wire-stepper__controls" data-show="controls">
|
|
<button
|
|
type="button"
|
|
class="wire-stepper__button-control wire-stepper__back"
|
|
disabled='{activeIndex() === 0}'
|
|
@click='goBack()'
|
|
>
|
|
{backLabel}
|
|
</button>
|
|
<span class="wire-stepper__controls-spacer"></span>
|
|
<button
|
|
type="button"
|
|
class="wire-stepper__button-control wire-stepper__skip"
|
|
data-show="allowSkip && !onLastStep()"
|
|
@click='goSkip()'
|
|
>
|
|
{skipLabel}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="wire-stepper__button-control wire-stepper__next"
|
|
data-primary="true"
|
|
disabled='{nextDisabled}'
|
|
@click='goNext()'
|
|
>
|
|
{onLastStep() ? finishLabel : nextLabel}
|
|
</button>
|
|
</div>
|
|
|
|
<slot />
|
|
</div>
|
|
}
|
|
|
|
style {
|
|
.wire-stepper {
|
|
--stepper-accent: var(--wire-color-primary);
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wire-stepper__list {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.wire-stepper[data-color="secondary"] {
|
|
--stepper-accent: var(--wire-color-secondary);
|
|
}
|
|
|
|
.wire-stepper[data-color="success"] {
|
|
--stepper-accent: var(--wire-color-success);
|
|
}
|
|
|
|
.wire-stepper[data-color="danger"] {
|
|
--stepper-accent: var(--wire-color-danger);
|
|
}
|
|
|
|
.wire-stepper[data-color="info"] {
|
|
--stepper-accent: var(--wire-color-info);
|
|
}
|
|
|
|
.wire-stepper[data-size="sm"] {
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.wire-stepper[data-size="lg"] {
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.wire-stepper[data-orientation="vertical"] .wire-stepper__list {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.wire-stepper__step {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
gap: 0.35rem;
|
|
}
|
|
|
|
.wire-stepper__button {
|
|
appearance: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6rem;
|
|
width: 100%;
|
|
padding: 0.5rem;
|
|
border: 0;
|
|
border-radius: var(--wire-radius-sm);
|
|
background: transparent;
|
|
color: inherit;
|
|
font: inherit;
|
|
text-align: left;
|
|
cursor: default;
|
|
}
|
|
|
|
.wire-stepper[data-clickable="true"] .wire-stepper__button {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wire-stepper[data-clickable="true"] .wire-stepper__button:hover {
|
|
background: var(--wire-color-surface-soft);
|
|
}
|
|
|
|
.wire-stepper__button:focus-visible {
|
|
outline: 2px solid var(--stepper-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-stepper__marker {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex: 0 0 auto;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
border: 1px solid var(--wire-color-border);
|
|
border-radius: 999px;
|
|
background: var(--wire-color-surface);
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.wire-stepper__step[data-status="complete"] .wire-stepper__marker {
|
|
border-color: var(--stepper-accent);
|
|
background: var(--stepper-accent);
|
|
color: var(--wire-color-primary-contrast);
|
|
}
|
|
|
|
.wire-stepper__step[data-status="current"] .wire-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);
|
|
}
|
|
|
|
.wire-stepper__step[data-status="upcoming"] .wire-stepper__marker {
|
|
color: var(--wire-color-text-muted);
|
|
}
|
|
|
|
.wire-stepper__body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-width: 0;
|
|
}
|
|
|
|
.wire-stepper__label {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.wire-stepper__description {
|
|
color: var(--wire-color-text-muted);
|
|
font-size: 0.78rem;
|
|
}
|
|
|
|
.wire-stepper__step[data-status="upcoming"] .wire-stepper__label {
|
|
color: var(--wire-color-text-muted);
|
|
}
|
|
|
|
.wire-stepper__panels {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.wire-stepper__panel {
|
|
padding: 1rem;
|
|
border: 1px solid var(--wire-color-border);
|
|
border-radius: var(--wire-radius-md);
|
|
background: var(--wire-color-surface);
|
|
animation: wire-stepper-in 200ms ease both;
|
|
}
|
|
|
|
.wire-stepper__panel-title {
|
|
margin: 0 0 0.35rem;
|
|
font-size: 0.95rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.wire-stepper__panel-body {
|
|
margin: 0;
|
|
color: var(--wire-color-text-muted);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.wire-stepper__controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-top: 0.85rem;
|
|
}
|
|
|
|
.wire-stepper__controls-spacer {
|
|
flex: 1 1 auto;
|
|
}
|
|
|
|
.wire-stepper__button-control {
|
|
appearance: none;
|
|
padding: 0.45rem 0.9rem;
|
|
border: 1px solid var(--wire-color-border);
|
|
border-radius: var(--wire-radius-sm);
|
|
background: var(--wire-color-surface);
|
|
color: var(--wire-color-text);
|
|
font: inherit;
|
|
font-size: 0.86rem;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.wire-stepper__button-control:hover:not(:disabled) {
|
|
background: var(--wire-color-surface-soft);
|
|
}
|
|
|
|
.wire-stepper__button-control:focus-visible {
|
|
outline: 2px solid var(--stepper-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wire-stepper__button-control[data-primary="true"] {
|
|
border-color: var(--stepper-accent);
|
|
background: var(--stepper-accent);
|
|
color: var(--wire-color-primary-contrast);
|
|
}
|
|
|
|
/* A held step has to look held, or the button reads as broken. */
|
|
.wire-stepper__button-control:disabled {
|
|
opacity: 0.45;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
@keyframes wire-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) {
|
|
.wire-stepper__list {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
}
|
|
}
|