Files
404-Layers/src/ui/Panel.tsx
ncode 4961500fa7 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>
2026-09-17 01:17:16 +03:00

155 lines
5.0 KiB
TypeScript

/**
* The SP-404MK2 panel. Presentational: it is told what is lit and reports
* presses, holding no state of its own.
*/
import { panelLabel } from "../domain/format";
import type { ControlId, Entry } from "../domain/types";
import { cx } from "./cx";
import { CONTROL_GROUPS, FX_LEFT, FX_RIGHT, KNOBS, PAD_ROWS } from "./panelLayout";
import styles from "./Panel.module.css";
export interface ScreenText {
readonly top: string;
readonly right: string;
readonly main: string;
/** Inverted chips along the bottom of the LCD — the layer's functions. */
readonly chips: readonly string[];
}
export interface PanelProps {
/** Controls carrying a function on the current layer, and which entries. */
readonly lit: ReadonlyMap<ControlId, Entry[]>;
readonly held: readonly ControlId[];
readonly screen: ScreenText;
readonly onPress?: (id: ControlId) => void;
}
interface KeyProps extends Pick<PanelProps, "lit" | "held" | "onPress"> {
readonly id: ControlId;
/** Comes from a CSS module, so possibly undefined. See cx. */
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
type="button"
className={cx(className, isHeld && styles.held, !isHeld && isLit && styles.lit)}
data-control={id}
aria-label={panelLabel(id)}
onClick={onPress ? () => onPress(id) : undefined}
>
{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}
</button>
);
}
export function Panel({ lit, held, screen, onPress }: PanelProps) {
const key = { lit, held, ...(onPress ? { onPress } : {}) };
return (
<div className={styles.panel}>
<div className={styles.knobs}>
{KNOBS.map((id) => (
<Key key={id} id={id} className={styles.knob} {...key} />
))}
</div>
<div className={styles.screenRow}>
<div className={styles.fxCol}>
{FX_LEFT.map((id) => (
<Key key={id} id={id} className={styles.fx} {...key} />
))}
</div>
{/* The circle sits behind the display and shows only as caps above and
below it — no ring around the rectangle. */}
<div className={styles.screenWrap}>
<div className={styles.circle} aria-hidden="true" />
<div className={styles.lcd}>
<div className={styles.lcdRow}>
<span>{screen.top}</span>
<span>{screen.right}</span>
</div>
<div className={styles.lcdMain}>{screen.main}</div>
<div className={styles.lcdRow}>
{screen.chips.map((chip) => (
<span key={chip} className={styles.chip}>
{chip}
</span>
))}
</div>
</div>
</div>
<div className={styles.fxCol}>
{FX_RIGHT.map((id) => (
<Key key={id} id={id} className={styles.fx} {...key} />
))}
</div>
</div>
<div className={styles.ctl}>
{CONTROL_GROUPS.map((group) => (
<div className={styles.group} key={group.left.join()}>
<div className={styles.col}>
<div className={styles.caption}>
{group.leftCaption ? <span>{group.leftCaption}</span> : null}
<i className={styles.rule} />
</div>
<div className={styles.btnRow}>
{group.left.map((id) => (
<Key key={id} id={id} className={styles.key} {...key} />
))}
</div>
</div>
<div className={styles.col}>
<div className={styles.caption}>
{group.rightCaption ? <span>{group.rightCaption}</span> : null}
<i className={styles.rule} />
</div>
<div className={styles.btnRow}>
{group.right.map((id) => (
<Key key={id} id={id} className={styles.key} {...key} />
))}
{group.rightKnob ? (
<Key id={group.rightKnob} className={styles.valueKnob} {...key} />
) : null}
</div>
</div>
</div>
))}
</div>
<div className={styles.pads}>
{PAD_ROWS.map((row) => (
<div className={styles.padRow} key={row.side}>
{row.pads.map((id) => (
<Key key={id} id={id} className={styles.pad} {...key} />
))}
<Key id={row.side} className={styles.side} {...key} />
</div>
))}
</div>
</div>
);
}