Every bundled component must expose color and size; the rewrites dropped them, which the library-wide invariant test caught. Rather than re-adding them as dead props, each component now maps color onto an accent variable that its active, current and focus affordances actually use, and size onto the root font scale. Regenerates the component reference, showcase and visual contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
252 lines
6.4 KiB
Plaintext
252 lines
6.4 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 })
|
|
}
|
|
|
|
props {
|
|
color: string = "primary"
|
|
size: string = "default"
|
|
steps: unknown[] = []
|
|
active: number = 0
|
|
orientation: string = "horizontal"
|
|
clickable: boolean = false
|
|
label: string = "Progress"
|
|
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"
|
|
}
|
|
|
|
client function selectStep(index, step) {
|
|
if (!clickable) {
|
|
return
|
|
}
|
|
output.change({ index: index, step: step })
|
|
}
|
|
}
|
|
|
|
view {
|
|
<ol
|
|
{...attrs}
|
|
data-ui-component="Stepper"
|
|
class='wire-stepper {class}'
|
|
data-orientation='{orientation}'
|
|
data-color='{color}'
|
|
data-size='{size}'
|
|
data-clickable='{clickable}'
|
|
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}
|
|
<slot />
|
|
</ol>
|
|
}
|
|
|
|
style {
|
|
.wire-stepper {
|
|
--stepper-accent: var(--wire-color-primary);
|
|
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"] {
|
|
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);
|
|
}
|
|
|
|
/* A horizontal stepper cannot stay side by side on a phone. */
|
|
@media (max-width: 639px) {
|
|
.wire-stepper {
|
|
flex-direction: column;
|
|
}
|
|
}
|
|
}
|
|
}
|