Phase 2: render the panel and fix what looking revealed

Rendering it headlessly for the first time turned up two defects that
neither typecheck nor 112 tests could see:

- pads were labelled PAD 1, PAD 2 rather than printing their numeral.
  The hardware prints the number, and swaps in a function name only when
  a layer lights the pad up; Key now does the same.
- the LCD showed MODE:S, the context code, instead of the mode name. A
  single letter tells the reader nothing.

Both are only visible by looking, which is the point: the geometry was
arithmetically correct the whole time and still read wrong.

CLAUDE.md now records how to look. There is no Chromium here, but
Firefox screenshots headlessly with no new dependency, given three
things that otherwise waste an afternoon: -no-remote and a throwaway
profile, or it opens a tab in the running browser and captures nothing;
an HTTP server, because module scripts will not load over file://; and a
user.js pref to force dark mode.

Verified in both light and dark at 360x735. 112 tests, build clean,
www/ untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-17 01:17:16 +03:00
parent 563caf0658
commit 4961500fa7
4 changed files with 41 additions and 3 deletions

View File

@@ -192,6 +192,26 @@ src/styles/base.css tokens: palette, fonts, the vertical budget
bugs. The cost is that a mistyped class renders with no class at all rather than
failing, which is why the layout is pinned by tests instead.
**Seeing the UI without the phone.** There is no Chromium on this machine, but
Firefox can screenshot headlessly with no extra dependency:
```
npx vite build
cd dist && python3 -m http.server 8777 --bind 127.0.0.1 &
firefox -no-remote --profile "$(mktemp -d)" --headless \
--window-size=360,735 --screenshot out.png http://127.0.0.1:8777/
```
Three things that will otherwise waste time: `-no-remote` plus a throwaway
`--profile` are required or it opens a tab in your running Firefox and captures
nothing; module scripts will not load over `file://`, so it has to be served over
HTTP; and dark mode needs a `user.js` in that profile with
`layout.css.prefers-color-scheme.content-override` set to `0`.
Do this after UI changes. The first render had pads labelled "PAD 1" instead of
their numeral, and the LCD showing the context code `MODE:S` rather than the mode
name — neither visible to typecheck or the test suite.
**`panelLayout.ts` is data, and tested.** `allPanelControls()` must cover all 54
labelled controls exactly once — a typo'd id would otherwise just quietly go
missing from the panel. The bank row is pinned at six keys ending in SHIFT

View File

@@ -77,8 +77,8 @@ export function App() {
lit={lit}
held={held}
screen={{
top: `MODE:${ctx}`,
right: activeLayer === "" ? "LAYER:—" : layerLabel(activeLayer),
top: CTX[ctx].toUpperCase(),
right: activeLayer === "" ? "NO MODIFIER" : layerLabel(activeLayer),
main: `${onLayer.length} function${onLayer.length === 1 ? "" : "s"}`,
chips: onLayer.slice(0, 3).map((e) => e.short),
}}

View File

@@ -247,6 +247,14 @@
border-radius: 4px;
}
/* The pad's number, as printed on the hardware. */
.num {
font-family: var(--mono);
font-size: 16px;
font-weight: 700;
color: var(--muted);
}
.side {
color: var(--accent);
font-weight: 700;

View File

@@ -30,10 +30,14 @@ interface KeyProps extends Pick<PanelProps, "lit" | "held" | "onPress"> {
readonly className: string | undefined;
}
/** Pads print their number, not "PAD 7" — until a layer lights them up. */
const padNumber = (id: ControlId): string | undefined => /^p(\d+)$/.exec(id)?.[1];
function Key({ id, className, lit, held, onPress }: KeyProps) {
const entries = lit.get(id);
const isHeld = held.includes(id);
const isLit = entries !== undefined && entries.length > 0;
const number = padNumber(id);
return (
<button
@@ -43,7 +47,13 @@ function Key({ id, className, lit, held, onPress }: KeyProps) {
aria-label={panelLabel(id)}
onClick={onPress ? () => onPress(id) : undefined}
>
{isLit ? entries[0]!.short : panelLabel(id)}
{isLit ? (
entries[0]!.short
) : number ? (
<span className={styles.num}>{number}</span>
) : (
panelLabel(id)
)}
{entries && entries.length > 1 ? (
<span className={styles.badge}>{entries.length}</span>
) : null}