feat(ui): build the Stepper component

Replaces a scaffold that rendered bare anchors with ordered steps: complete,
current and upcoming status derived from the active index, horizontal or
vertical, optional icons, and indexed named slots (step-0, step-1, ...) for
authoring a step body by hand.

Also tightens the roving contract from the previous commit. A template writes
data-wrn-roving="" or data-wrn-roving-item="false" to mean not this time, but
a bare [attr] selector matches either, so a read-only stepper would still have
taken arrow-key focus. The container now requires a named axis, while a bare
item marker still counts as opted in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:44:12 +05:30
co-authored by Claude Opus 5
parent b67a5e43eb
commit 45ef035bae
5 changed files with 337 additions and 13 deletions
+27 -1
View File
@@ -3069,11 +3069,35 @@ export const REACTIVE_RUNTIME = String.raw`
var ROVING_SELECTOR = "[data-wrn-roving]";
var ROVING_ITEM_SELECTOR = "[data-wrn-roving-item]";
/*
* These attributes are written by templates, so they arrive stringified:
* data-wrn-roving="" or data-wrn-roving-item="false" is how a component
* says "not this time". A bare [attr] selector matches either, so the value
* has to be checked -- otherwise a stepper with clickable=false still takes
* arrow-key focus.
*/
// A bare data-wrn-roving-item means yes; only an explicit "false" opts out.
function rovingItemOff(value) {
return value === null || value === "false";
}
/*
* The container is stricter: it must name an axis. An empty value is what a
* template emits for {cond ? "horizontal" : ""}, so empty means off rather
* than defaulting to horizontal.
*/
function rovingOrientation(container) {
var value = container.getAttribute("data-wrn-roving");
if (value === null || value === "" || value === "false") return "";
return value;
}
function rovingItems(container) {
var found = [];
var candidates = container.querySelectorAll(ROVING_ITEM_SELECTOR);
for (var index = 0; index < candidates.length; index += 1) {
var candidate = candidates[index];
if (rovingItemOff(candidate.getAttribute("data-wrn-roving-item"))) continue;
// A nested group owns its own items; do not steal them.
if (candidate.closest(ROVING_SELECTOR) !== container) continue;
if (candidate.hasAttribute("disabled")) continue;
@@ -3107,6 +3131,7 @@ export const REACTIVE_RUNTIME = String.raw`
}
function syncRovingGroup(container) {
if (!rovingOrientation(container)) return;
var items = rovingItems(container);
if (!items.length) return;
applyRovingTabindex(items, rovingActiveIndex(items));
@@ -3129,7 +3154,8 @@ export const REACTIVE_RUNTIME = String.raw`
var index = items.indexOf(item);
if (index === -1) return;
var orientation = container.getAttribute("data-wrn-roving") || "horizontal";
var orientation = rovingOrientation(container);
if (!orientation) return;
var horizontal = orientation === "horizontal" || orientation === "both";
var vertical = orientation === "vertical" || orientation === "both";
var key = event.key;