Panel.tsx is presentational - told what is lit, reports presses, holds no state. The arrangement lives in panelLayout.ts as data, so the component stays readable and the layout can be tested. panelLayout.test.ts asserts allPanelControls covers all 54 labelled controls exactly once; a typo'd id would otherwise quietly vanish from the panel. It also pins the bank row at six keys ending in SHIFT, because Panel.module.css widens that key with :last-child:nth-child(6), a coupling that stops matching silently if the row changes length. Geometry is the measured one: 8 + 36 + 8 + 124 + 10 + 134 + 10 + 187 + 8 = 525. The screen is the hardware's arrangement - a 124px circle behind a 168x84 LCD, showing only as caps above and below, with no ring around the display. Key labels are 7px with no tracking, which is what stops RESAMPLE overflowing its 35.9px button. cx() absorbs CSS-module lookups typing as string | undefined under noUncheckedIndexedAccess, rather than turning that flag off - it has already caught real bugs. Two fixed on the way through: panelLabel used PL without importing it, which broke typecheck and the layout test at runtime; and ten className type errors from the same CSS-module typing, in the places that needed a definite string rather than React's optional one. 112 tests, build clean, www/ untouched so the Phase 1 APK still installs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
254 lines
3.9 KiB
CSS
254 lines
3.9 KiB
CSS
/* Measured geometry: 8 + 36 + 8 + 124 + 10 + 134 + 10 + 187 + 8 = 525. */
|
|
|
|
.panel {
|
|
height: var(--panel-h);
|
|
padding: 8px;
|
|
background: var(--body);
|
|
border: var(--outline);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ---- shared key look ---- */
|
|
|
|
.key,
|
|
.pad,
|
|
.side,
|
|
.fx,
|
|
.knob,
|
|
.valueKnob {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
border: var(--outline);
|
|
background: var(--key);
|
|
color: var(--ink);
|
|
/* 7px with no tracking: a left-block key is 35.9px wide and only 27.9px of
|
|
that is usable, so 8px made SAMPLE sit flush against the border. */
|
|
font-size: 7px;
|
|
line-height: 1.05;
|
|
letter-spacing: 0;
|
|
padding: 2px 1px;
|
|
border-radius: 3px;
|
|
hyphens: manual;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.lit {
|
|
background: var(--accent);
|
|
color: var(--on-accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.held {
|
|
background: var(--ink);
|
|
color: var(--page);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.badge {
|
|
position: absolute;
|
|
top: 1px;
|
|
right: 2px;
|
|
font-family: var(--mono);
|
|
font-size: 6px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* ---- knobs ---- */
|
|
|
|
.knobs {
|
|
height: 36px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.knob {
|
|
width: 34px;
|
|
height: 34px;
|
|
flex: 0 0 auto;
|
|
border-radius: 50%;
|
|
background: var(--pad);
|
|
color: var(--pad-ink);
|
|
}
|
|
|
|
/* ---- fx and the screen ---- */
|
|
|
|
.screenRow {
|
|
height: 124px;
|
|
margin-top: 8px;
|
|
display: grid;
|
|
grid-template-columns: 62px minmax(0, 1fr) 62px;
|
|
gap: 8px;
|
|
}
|
|
|
|
.fxCol {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.fx {
|
|
flex: 1;
|
|
background: var(--pad);
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
font-size: 7.5px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.screenWrap {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.circle {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
width: 124px;
|
|
height: 124px;
|
|
margin-left: -62px;
|
|
border-radius: 50%;
|
|
background: var(--pad);
|
|
border: var(--outline);
|
|
}
|
|
|
|
.lcd {
|
|
position: relative;
|
|
width: 168px;
|
|
height: 84px;
|
|
background: var(--lcd);
|
|
border: var(--outline);
|
|
color: #111111;
|
|
padding: 4px 6px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.lcdRow {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 3px;
|
|
font-family: var(--mono);
|
|
font-size: 6.5px;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
.lcdMain {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
text-align: center;
|
|
}
|
|
|
|
.chip {
|
|
background: #111111;
|
|
color: var(--lcd);
|
|
padding: 1px 4px;
|
|
}
|
|
|
|
/* ---- control block ---- */
|
|
|
|
.ctl {
|
|
height: 134px;
|
|
margin-top: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
}
|
|
|
|
.group {
|
|
height: 40px;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 3fr) minmax(0, 5fr);
|
|
gap: 10px;
|
|
}
|
|
|
|
.col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.caption {
|
|
height: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-family: var(--mono);
|
|
font-size: 6px;
|
|
letter-spacing: 0.11em;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.rule {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--rule);
|
|
}
|
|
|
|
.btnRow {
|
|
height: 32px;
|
|
display: flex;
|
|
gap: 5px;
|
|
}
|
|
|
|
.key {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* SHIFT is the longest label in the six-button bank row, as on the hardware. */
|
|
.btnRow .key:last-child:nth-child(6) {
|
|
flex: 1.25;
|
|
}
|
|
|
|
.valueKnob {
|
|
width: 30px;
|
|
flex: 0 0 auto;
|
|
border-radius: 50%;
|
|
background: var(--pad);
|
|
color: var(--pad-ink);
|
|
font-size: 6.5px;
|
|
}
|
|
|
|
/* ---- pads ---- */
|
|
|
|
.pads {
|
|
height: 187px;
|
|
margin-top: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.padRow {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr)) 0.9fr;
|
|
gap: 5px;
|
|
}
|
|
|
|
.pad,
|
|
.side {
|
|
background: var(--pad);
|
|
color: var(--pad-ink);
|
|
font-size: 9px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.side {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
}
|