web: the phone's bar wears icons, stays at the top and ends on EXPORT — the four words wrapped the header to 135px over a 390px screen, 88px now, and the tab pills were 28px tall under a thumb, 44px now

LIBRARY, RESET, SAVE PHOTO and EXPORT are icons on a phone from here on. The
recipe is the one the file already used for the stage's toolbar: font-size 0
takes the word out of the paint, never out of the button, so every accessible
name is exactly the word that is no longer drawn, and a ::before carries the
glyph (▤ ⟲ ⤓ ↗ — all of them already spoken in this app). The row's own content
measured 990px wide with the words and 716px with the icons, and the bar it sat
in measured 135px tall on a 390px screen before this and 88px after: three rows
to two.

RESET also gains a data-key (`reset-look`) — the CSS has to be able to name it.

EXPORT moves to the end of the <header>, after the three menus. It is the end of
the job and now the end of the row, which is the button the eye should land on
once the edit is done.

SAVE PHOTO's PRO marker goes with the word: a 9px badge inside a 34px button is
a word in a box too small for it, and the count that shares the label has no room
either. The button keeps its title, and the count is the one thing the phone
loses here — the wide screen still counts.

The header is now `position: sticky; top: 0; z-index: 5` on a phone. The page
never scrolls (the stage does), so nothing moved before this and nothing moves
now — but that is a promise the layout can keep rather than a coincidence of who
scrolls. `.adm-bar` already carried the same three lines.

The tab strip at the foot grows to the size a thumb is: pills 10px on 8x12 and
28px tall become 12px on 0x16 with a 44px floor, and the bar's own padding goes
6x8 to 8x10. Measured: a pill 28px to 44px, the bar 40px to 61px.

This commit also lands the strip work from the session before it, which was
written and probed but never committed: under 860px the rail of tabs and the
tab's chips become one bar at a time (`.workspace.strip-open`), a TABS chip
stands the rail back up, the open chip's sub-chips stand over the bar as 60px
tiles, and the ruler is drawn as ticks on a bare input. It shares app.css with
the bar above, so the phone's block lands in one piece.

Checks:
- npm run build (tsc --noEmit + vite) clean.
- The ten browser-free image checks (tone-base, highlight-knee, half,
  white-level, auto-tone, preview-match, raw-develop, roll-walk, wb-table,
  mask-wb) all pass.
- Playwright at 390x844, light and dark (scratchpad topbar-probe.mjs): header
  sticky, top 0px, z-index 5, y=0 h=88, lastElementChild data-key
  export-photo; the four buttons 34x30 with font-size 0 and a 15px ::before;
  the rail at y=783 h=61 with a 93x44 pill at 12px; the theme popover inside
  the screen (x=47, right=382); every scroller set to 400 leaves headerY at 0;
  strip-open keeps headerY 0 with the rail display:none; no page errors.
- Playwright at 1280 (scratchpad topbar-desktop.mjs): header static, one row
  50px, the same four buttons at 14px with no ::before, EXPORT last.

Skipped: no new aria-label — the words the icons replace are still the buttons'
own text, so nothing needs one. Add one only if a label leaves the DOM.
This commit is contained in:
2026-10-01 06:21:52 +07:00
parent 91aeacbe46
commit 6bbf18dc44
7 changed files with 291 additions and 39 deletions
+5
View File
@@ -17,6 +17,10 @@ export interface ChipDef {
// CSS colour drawn as a dot on the chip's leading edge — the HSL mixer's band
// chips name a colour, so they show it too.
color?: string;
// The button's face on a phone, where a sub-chip is a tile: a short mark over
// the name (see .col-sub .chip in app.css). Desktop never paints it — the
// label alone is what a chip reads as in a column.
icon?: string;
// The chip's own FILL and the text colour that fill can carry (WB's preset
// table: the swatch the pick paints, printed on the button itself). `bg` wins
// over the class's own background, so an active button keeps its swatch and
@@ -99,6 +103,7 @@ export function ChipRow({ chips, grid }: { chips: ChipDef[]; grid?: boolean }) {
}}
>
{chip.color ? <span className="chip-dot" data-color={chip.color} style={{ background: chip.color }} /> : null}
{chip.icon ? <span className="chip-icon" aria-hidden="true">{chip.icon}</span> : null}
{/* The name is a box, not the bare text node it used to be: a flex
container turns a loose text node into an anonymous item of its
own, and on iOS that item stops being painted until the column