T5.1 - A4/S9: a vertical stepper, ten real buttons, states in words

The step rail was ten div elements carrying onclick inside a horizontal
scroller. Not in the tab order, not operable by keyboard, and silent about
progress - the only thing on the page that said where you were was a "1 / 10"
pill in the app bar, detached from the control it described.

The rail is now a vertical column beside the form: ten <button> elements in an
<ol> inside a named <nav>, with arrow keys, Home and End on top of the Enter and
Space a button gives for free. All ten stay in the tab order; a roving tabindex
would have satisfied "arrow keys" by breaking "tab", which the done-when asks
for both of.

Four states, each carrying a word and a marker shape as well as a colour (C1):
Complete (green disc, tick), Current step (blue disc, aria-current="step"),
Locked (dashed outline) and a plain default. Locked steps keep aria-disabled
rather than disabled, so a keyboard user can reach one and be told what is in
the way instead of finding a control that has vanished from the tab order.

Reachability is the guard's own rule, deliberately not a stricter one: you may
leave the step you are on once its required fields are filled. The tempting rule
- lock everything after the first unmet gate anywhere - is not what
validateStep() enforces, and a padlock the Next button walks straight past is
the drift this change exists to remove. validateStep() and the rail now read one
STEP_GATES table, so they cannot disagree; T5.8 widens that table rather than
editing four functions.

Clicking a step you cannot reach announces why through a role="alert" region and
puts the cursor in the field that is missing. Saying "no" and leaving you where
you were, with no idea which of five inputs was empty, is what the dialog did.

Below 900px the rail collapses to a disclosure naming the step you are on -
ten vertical rows above the fields is most of a 390px screen before you reach an
input. 44px tap targets, since Field View is the gloved-hands surface.

Also: going backwards is no longer gated. previousStep() never validated, so a
rail that did would have trapped you on an incomplete step.

  html/work-package-suite.html        rail markup, counter removed
  html/work-package-suite-styles.css  #tool-sop grid, .step-rail*, 899px collapse
  html/work-package-suite-app.js      STEP_GATES, renderStepRail, keyboard, watcher
  tests/stepper_check.py              new - 70 checks

Done when
  [x] all 10 steps are <button> elements
  [x] keyboard: tab, arrow keys, Home/End, Enter and Space
  [x] aria-current on the current step, exactly one
  [x] complete / current / unavailable told apart without colour
  [x] the "1 / 10" counter is gone - no .step-counter, no N/10 in the app bar
  [x] app-wide <div onclick> 12 -> 2, down exactly 10

Verified one at a time
  stepper_check   70/70   new
  browser_check   71/71
  url_state       23/23
  a11y            22/22   sop now rings 37 focusable elements, all >= 3:1
  autosave        34/34
  aggregates      16/16
  f_items         F1-F5 FIXED, F6 REPRODUCES (T7.2)
  baseline_shots  14 shots; only the sop pair changed. The beforeunload log on
                  sop@1440 and creator@1440 is present at HEAD too - captured
                  both sides to check rather than assume.

No colour literal was added: all five page sheets and all seven inline <style>
blocks still hold zero. New spacing consumes --wp-s*; three raw font sizes were
added and three removed, so BL-010 is unchanged in kind.

Raised, not fixed
  BL-016  Back to a URL with no `step` leaves the wizard where it was. T4.2's
          popstate handler parses NaN and ignores it; its own probe never took
          that branch. stepper_check pins the current behaviour by name so the
          fix has a test waiting.
  BL-017  The native-dialog baseline counts the word `alert(` in comments. Four
          comments written here - all of them about removing a dialog - moved
          the number from 80 to 82 while two real calls were being deleted. They
          were reworded; the metric still needs a comment-stripped variant, which
          T5.8 owns.

Question for the PR, per CLAUDE.md: BL-015 leaves the creator's .step-tab
uppercase as the last forced-uppercase interactive text in the suite, on the
grounds that A5 scopes sentence case to buttons and field labels. The wizard's
rail is now buttons, so its labels are sentence case ("Sign-offs", "WP types").
The two are consistent by rule and inconsistent on screen until T7.x.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-15 23:19:58 -05:00
parent 6fbc5b9735
commit 5e1f6e75ba
6 changed files with 1035 additions and 76 deletions

View File

@@ -151,15 +151,9 @@ body {
border-color: var(--wp-appbar-border);
}
.step-counter {
background: transparent;
border: 1px solid var(--wp-appbar-border);
color: var(--wp-appbar-fg-dim);
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}
/* .step-counter — the "1 / 10" pill — is gone with A4/S9. The step rail carries
position, progress and reachability; a badge in the app bar carried one third
of that and was the only thing on the page that did. */
/* MAIN NAVIGATION — underline tabs */
.main-nav {
@@ -256,42 +250,138 @@ body.embed-full { overflow: hidden; }
display: block;
}
/* STEP NAV */
.step-nav {
margin-bottom: 2rem;
overflow-x: auto;
/* ══ STEP RAIL — vertical stepper (A4 / S9 / C1) ═══════════════════════════════
Replaces `.step-nav > .steps-container > .step-item`: ten <div onclick> chips
in a horizontal scroller. They were unreachable by keyboard, said nothing
about progress, and the "1 / 10" pill in the app bar was doing the one job
they should have been doing.
The rail is a column beside the form rather than a strip above it, so all ten
steps and their states are visible while you work in one. Below 900px there is
no room for a second column, so it collapses — see the breakpoint at the foot
of this block.
Spacing comes from --wp-s* (the suite's only real spacing scale) rather than
fresh literals; type does not, because there is no type token to consume yet.
That is BL-010 and it is unchanged in kind by this task. */
#tool-sop.active {
display: grid;
grid-template-columns: 15rem minmax(0, 1fr);
gap: var(--wp-s5);
align-items: start;
}
.steps-container {
display: flex;
gap: 0.5rem;
min-width: min-content;
padding: 0.5rem;
}
.step-item {
padding: 0.6rem 0.9rem;
border-radius: 0;
.step-rail {
grid-column: 1;
grid-row: 1 / span 2; /* beside the form AND its navigation bar */
/* Step 5 is roughly three screens tall. A rail that scrolls away is a rail you
have to hunt for, which is how the app ended up with a counter in the bar. */
position: sticky;
top: var(--wp-s4);
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-light);
cursor: pointer;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
transition: background 0.15s, color 0.15s, border-color 0.15s;
border-radius: 0;
padding: var(--wp-s3);
}
.step-item:hover { background: var(--bg); border-color: var(--border-strong); color: var(--text); }
.step-item.active { background: var(--primary); color: var(--cds-text-on-color); border-color: var(--primary); font-weight: 600; }
.step-rail-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.step-rail-item { margin: 0; }
/* Every step is a real button, in the tab order, activated by Enter and Space
for free. Arrow keys, Home and End are added in work-package-suite-app.js. */
.step-btn {
width: 100%;
display: flex;
align-items: center;
gap: var(--wp-s3);
padding: var(--wp-s2);
background: none;
border: none;
border-left: 3px solid transparent;
border-radius: 0;
text-align: left;
font: inherit;
color: var(--text-light);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.step-btn:hover { background: var(--bg); color: var(--text); }
.step-btn-marker {
flex: none;
width: var(--wp-ctl-sm);
height: var(--wp-ctl-sm);
border: 1px solid var(--border-strong);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
line-height: 1;
}
.step-btn-body { min-width: 0; display: flex; flex-direction: column; }
.step-btn-label { font-size: 13px; font-weight: 500; }
/* The state in words. C1: completed / current / unavailable must be tellable
apart without colour, so each carries a label here AND a marker shape — filled
disc, filled disc with a tick, dashed outline — not a hue alone. */
.step-btn-state { font-size: 11px; color: var(--text-light); }
.step-btn-state:empty { display: none; }
.step-btn.is-current {
background: var(--primary-light);
border-left-color: var(--primary);
color: var(--text);
font-weight: 600;
}
.step-btn.is-current .step-btn-marker {
background: var(--primary);
border-color: var(--primary);
color: var(--cds-text-on-color);
}
/* Green because this is a status, not an action — see tokens.md section 12.
Green never fills a button in this suite; it fills the badge on one. */
.step-btn.is-complete .step-btn-marker {
background: var(--success);
border-color: var(--success);
color: var(--cds-text-on-color);
}
/* Still focusable and still in the tab order: a control removed from the tab
order is a control a keyboard user cannot even discover, and "you cannot go
there yet, here is why" is worth reaching. aria-disabled, not disabled. */
.step-btn.is-locked { cursor: not-allowed; }
.step-btn.is-locked .step-btn-marker { border-style: dashed; }
.step-rail-msg {
margin: var(--wp-s2) 0 0;
font-size: 12px;
color: var(--danger);
}
.step-rail-msg:empty { display: none; }
/* Wide widths: the list IS the control, so the disclosure has nothing to do. */
.step-rail-toggle { display: none; }
/* STEP CONTENT */
.step-content {
grid-column: 2;
grid-row: 1;
background: var(--bg-card);
padding: 2rem;
border: 1px solid var(--border);
border-radius: 0;
margin-bottom: 2rem;
}
.step { display: none; }
@@ -537,6 +627,8 @@ body.embed-full { overflow: hidden; }
/* NAVIGATION */
.step-navigation {
grid-column: 2;
grid-row: 2;
display: flex;
gap: 1rem;
justify-content: space-between;
@@ -690,6 +782,51 @@ body.embed-full { overflow: hidden; }
}
/* RESPONSIVE */
/* Below 900px there is no room for a rail beside the form. Ten vertical rows
stacked above the fields is most of a 390px screen before you reach the first
input, so the rail becomes a disclosure: where you are, tap to see the rest.
It is still the same ten buttons — nothing is hidden from the keyboard that is
not also hidden from the mouse. */
@media (max-width: 899px) {
#tool-sop.active { grid-template-columns: minmax(0, 1fr); gap: var(--wp-s4); }
.step-rail,
.step-content,
.step-navigation { grid-column: 1; grid-row: auto; }
/* The disclosure button carries the box at this width; the rail itself is only
a wrapper, and two nested borders read as two controls. */
.step-rail { position: static; background: none; border: 0; padding: 0; }
.step-rail:not(.is-collapsed) .step-rail-list {
background: var(--bg-card);
border: 1px solid var(--border);
padding: var(--wp-s2);
}
.step-rail-toggle {
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
gap: var(--wp-s2);
padding: var(--wp-s3);
background: var(--bg-card);
border: 1px solid var(--border-strong);
border-radius: 0;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--text);
cursor: pointer;
}
.step-rail-toggle-chev { transition: transform 0.15s; }
.step-rail.is-collapsed .step-rail-list { display: none; }
.step-rail:not(.is-collapsed) .step-rail-toggle { margin-bottom: var(--wp-s2); }
.step-rail:not(.is-collapsed) .step-rail-toggle-chev { transform: rotate(180deg); }
/* Gloved hands on a tablet: 44px minimum, which the 26px marker plus 8px of
padding does not reach on its own. */
.step-btn { min-height: 44px; }
}
@media (max-width: 768px) {
.header { height: auto; flex-direction: column; align-items: stretch; text-align: center; gap: 0.75rem; padding: 12px 16px; }
.main-nav { flex-wrap: wrap; }