Skip to content

Commit f702767

Browse files
committed
Make MIDI Studio V2 octave timeline use a real piano-style frozen keyboard axis - PR_26146_043-midi-studio-v2-piano-roll-keyboard-grid
1 parent 7e7a12e commit f702767

4 files changed

Lines changed: 155 additions & 18 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# PR_26146_043 MIDI Studio V2 Piano Roll Keyboard Grid Validation
2+
3+
Status: PASS
4+
5+
## Scope
6+
7+
- Updated the Studio tab Octave Timeline note axis so it reads as a piano-roll keyboard instead of a plain table label column.
8+
- Preserved the single active Octave Timeline editor from PR042: one rendered editor, Studio tab only.
9+
- Preserved active timeline notes as highlighted cells without printed note text.
10+
- Preserved frozen Bar/Beat header alignment, square cells, zoom controls, vertical scrolling, horizontal scrolling, multi-note/chord editing, section/loop feedback, and Play/Stop.
11+
12+
## Files Changed
13+
14+
- `tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
15+
- `tools/midi-studio-v2/styles/midiStudioV2.css`
16+
- `tests/playwright/tools/MidiStudioV2.spec.mjs`
17+
- `docs/dev/reports/PR_26146_043-midi-studio-v2-piano-roll-keyboard-grid_validation.md`
18+
- `docs/dev/reports/coverage_changed_js_guardrail.txt`
19+
- `docs/dev/codex_commands.md`
20+
- `docs/dev/commit_comment.txt`
21+
22+
Review artifacts are generated separately:
23+
24+
- `docs/dev/reports/codex_review.diff`
25+
- `docs/dev/reports/codex_changed_files.txt`
26+
- `docs/dev/reports/playwright_v8_coverage_report.txt`
27+
28+
## Validation Commands
29+
30+
PASS:
31+
32+
```powershell
33+
node --check tools/midi-studio-v2/js/controls/InstrumentGridControl.js
34+
node --check tests/playwright/tools/MidiStudioV2.spec.mjs
35+
node -e "const fs=require('fs'); const p='tools/midi-studio-v2/index.html'; const html=fs.readFileSync(p,'utf8'); if(/<script(?![^>]*\ssrc=)[^>]*>/i.test(html)) throw new Error('Inline script block found'); if(/<style\b/i.test(html)) throw new Error('Inline style block found'); if(/\son[a-z]+\s*=/i.test(html)) throw new Error('Inline event handler found'); console.log('HTML external-only guard passed:', p);"
36+
node -e "const fs=require('fs'); for (const file of ['tools/midi-studio-v2/styles/midiStudioV2.css']) { const text=fs.readFileSync(file,'utf8'); let depth=0; for (const ch of text) { if (ch==='{') depth++; if (ch==='}') depth--; if (depth<0) throw new Error(file+': unexpected }'); } if (depth!==0) throw new Error(file+': unbalanced braces'); console.log(file+': CSS brace check OK'); }"
37+
npx.cmd playwright test tests/playwright/tools/MidiStudioV2.spec.mjs -g "octave timeline freezes compact headers and note labels while active cells stay textless|fast octave note editing supports drag painting keyboard shortcuts selection and timeline scroll sync|renders timing ruler, section navigation, and loop region visualization" --config=codex_playwright_system_chrome.config.cjs --reporter=list --workers=1 --timeout=60000
38+
git diff --check
39+
```
40+
41+
Playwright result:
42+
43+
```text
44+
Running 3 tests using 1 worker
45+
46+
ok 1 MIDI Studio V2 - fast octave note editing supports drag painting keyboard shortcuts selection and timeline scroll sync
47+
ok 2 MIDI Studio V2 - octave timeline freezes compact headers and note labels while active cells stay textless
48+
ok 3 MIDI Studio V2 - renders timing ruler, section navigation, and loop region visualization
49+
50+
3 passed
51+
```
52+
53+
Notes:
54+
55+
- An initial targeted run failed because a test still compared the plain row bed backgrounds; the piano key surfaces now live on the key-shape pseudo-elements. The assertion was corrected and the final targeted run passed.
56+
- `git diff --check` exited 0. Git emitted line-ending warnings for touched files; no whitespace errors were reported.
57+
- Full samples smoke test was not run.
58+
59+
## Test Evidence
60+
61+
- Piano-style keyboard keys: Playwright verifies the frozen note axis uses white and black key classes with different rendered key-surface colors.
62+
- White keys are full-width: Playwright verifies the white key shape starts at the left edge and matches the keyboard axis width.
63+
- Black keys are narrower/inset: Playwright verifies the black key shape starts inset, is narrower than the white key, and rises above its row bounds.
64+
- Note letters are visible: Playwright verifies visible `C5` and `C#5` text layers with readable white-key and black-key colors.
65+
- Vertical row alignment: Playwright verifies label/cell top and height deltas remain within 1px before and after zoom.
66+
- Horizontal frozen behavior: Playwright scrolls horizontally and verifies the keyboard axis remains visible while header/body columns stay aligned.
67+
- Active note cells remain textless: Playwright toggles an octave cell and verifies the cell text is empty while note metadata and highlight marker remain present.
68+
- Single editor: Playwright verifies one `#instrumentGridOutput`, one `#instrumentGridHeading`, and one `.midi-studio-v2__octave-timeline`, visible only on Studio.
69+
- Play and Stop: targeted Playwright verifies Play enables Stop and Stop re-enables Play.
70+
71+
## Coverage
72+
73+
- `docs/dev/reports/playwright_v8_coverage_report.txt` reports changed runtime JS coverage.
74+
- Changed runtime JS covered: `(80%) tools/midi-studio-v2/js/controls/InstrumentGridControl.js`.
75+
- Changed non-runtime test JS is listed separately as not collected by browser V8 coverage.
76+
77+
## Samples
78+
79+
Full samples smoke test: SKIP.
80+
81+
Reason: PR scope is limited to MIDI Studio V2 tool UI/runtime and targeted Playwright coverage; sample JSON alignment and full samples smoke are explicitly out of scope.
82+
83+
## Manual UAT
84+
85+
1. Open MIDI Studio V2 and import the UAT manifest.
86+
2. Confirm only the Studio tab shows the Octave Timeline editor.
87+
3. Confirm the frozen left note axis visually reads as piano keys, with full-width white keys and inset black keys.
88+
4. Click and drag a few notes, zoom in/out, scroll vertically and horizontally, then confirm Play and Stop still work.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,10 @@ test.describe("MIDI Studio V2", () => {
10271027
const outputStyle = getComputedStyle(element);
10281028
const whiteLabelStyle = getComputedStyle(whiteLabel);
10291029
const blackLabelStyle = getComputedStyle(blackLabel);
1030+
const whiteText = whiteLabel.querySelector(".midi-studio-v2__octave-row-label-text");
1031+
const blackText = blackLabel.querySelector(".midi-studio-v2__octave-row-label-text");
1032+
const whiteTextStyle = getComputedStyle(whiteText);
1033+
const blackTextStyle = getComputedStyle(blackText);
10301034
const barHeaderStyle = getComputedStyle(element.querySelector('.midi-studio-v2__timing-header-row-1.midi-studio-v2__note-table-column-header[data-step-index="1"]'));
10311035
const beatHeaderStyle = getComputedStyle(element.querySelector('.midi-studio-v2__timing-header-row-2.midi-studio-v2__note-table-column-header[data-step-index="1"]'));
10321036
const barAxisStyle = getComputedStyle(element.querySelector(".midi-studio-v2__timing-header-row-1.midi-studio-v2__timing-axis-header"));
@@ -1039,15 +1043,19 @@ test.describe("MIDI Studio V2", () => {
10391043
const blackCellRect = blackCell.getBoundingClientRect();
10401044
return {
10411045
alternatingRowsDiffer: getComputedStyle(firstRow).backgroundColor !== getComputedStyle(secondRow).backgroundColor,
1042-
blackBackground: blackLabelStyle.backgroundImage || blackLabelStyle.backgroundColor,
1046+
blackBackground: blackKeyStyle.backgroundImage || blackKeyStyle.backgroundColor,
10431047
blackClass: blackLabel.className,
10441048
blackColor: blackLabelStyle.color,
10451049
blackJustifyItems: blackLabelStyle.justifyItems,
10461050
blackKeyLeft: Number.parseFloat(blackKeyStyle.left),
1051+
blackKeyTop: Number.parseFloat(blackKeyStyle.top),
10471052
blackKeyWidth: Number.parseFloat(blackKeyStyle.width),
10481053
blackKind: blackLabel.dataset.keyKind,
10491054
blackLabelHeight: blackLabelRect.height,
10501055
blackLabelText: blackLabel.textContent,
1056+
blackTextColor: blackTextStyle.color,
1057+
blackTextLeft: blackText.getBoundingClientRect().left,
1058+
blackTextVisible: blackText.getBoundingClientRect().width > 0,
10511059
blackTextAlign: blackLabelStyle.textAlign,
10521060
borderRightWidth: getComputedStyle(firstNote).borderRightWidth,
10531061
canScrollVertically: element.scrollHeight > element.clientHeight,
@@ -1065,15 +1073,19 @@ test.describe("MIDI Studio V2", () => {
10651073
],
10661074
overflowY: outputStyle.overflowY,
10671075
viewportHeight: element.getBoundingClientRect().height,
1068-
whiteBackground: whiteLabelStyle.backgroundImage || whiteLabelStyle.backgroundColor,
1076+
whiteBackground: whiteKeyStyle.backgroundImage || whiteKeyStyle.backgroundColor,
10691077
whiteClass: whiteLabel.className,
10701078
whiteColor: whiteLabelStyle.color,
10711079
whiteJustifyItems: whiteLabelStyle.justifyItems,
10721080
whiteKeyLeft: Number.parseFloat(whiteKeyStyle.left),
1081+
whiteKeyTop: Number.parseFloat(whiteKeyStyle.top),
10731082
whiteKeyWidth: Number.parseFloat(whiteKeyStyle.width),
10741083
whiteKind: whiteLabel.dataset.keyKind,
10751084
whiteLabelHeight: whiteLabelRect.height,
10761085
whiteLabelText: whiteLabel.textContent,
1086+
whiteTextColor: whiteTextStyle.color,
1087+
whiteTextLeft: whiteText.getBoundingClientRect().left,
1088+
whiteTextVisible: whiteText.getBoundingClientRect().width > 0,
10771089
whiteTextAlign: whiteLabelStyle.textAlign,
10781090
whiteRowDelta: Math.abs(whiteLabelRect.top - whiteCellRect.top),
10791091
whiteRowHeightDelta: Math.abs(whiteLabelRect.height - whiteCellRect.height),
@@ -1104,6 +1116,10 @@ test.describe("MIDI Studio V2", () => {
11041116
expect(gridLayout.blackLabelText).toBe("C#5");
11051117
expect(gridLayout.whiteBackground).not.toBe(gridLayout.blackBackground);
11061118
expect(gridLayout.whiteColor).not.toBe(gridLayout.blackColor);
1119+
expect(gridLayout.whiteTextVisible).toBe(true);
1120+
expect(gridLayout.blackTextVisible).toBe(true);
1121+
expect(gridLayout.whiteTextColor).toBe("rgb(15, 23, 42)");
1122+
expect(gridLayout.blackTextColor).toBe("rgb(248, 250, 252)");
11071123
expect(gridLayout.headerBackgrounds).toEqual([
11081124
"rgb(54, 0, 175)",
11091125
"rgb(54, 0, 175)",
@@ -1115,9 +1131,13 @@ test.describe("MIDI Studio V2", () => {
11151131
expect(gridLayout.whiteJustifyItems).toBe("start");
11161132
expect(gridLayout.blackJustifyItems).toBe("start");
11171133
expect(gridLayout.whiteKeyLeft).toBe(0);
1134+
expect(gridLayout.whiteKeyTop).toBe(0);
11181135
expect(Math.abs(gridLayout.whiteKeyWidth - gridLayout.labelWidth)).toBeLessThanOrEqual(1);
11191136
expect(gridLayout.blackKeyLeft).toBeGreaterThan(0);
1137+
expect(gridLayout.blackKeyTop).toBeLessThan(0);
11201138
expect(gridLayout.blackKeyWidth).toBeLessThan(gridLayout.whiteKeyWidth);
1139+
expect(gridLayout.whiteTextLeft).toBeGreaterThanOrEqual(gridLayout.whiteKeyLeft);
1140+
expect(gridLayout.blackTextLeft).toBeGreaterThan(gridLayout.blackKeyLeft);
11211141
expect(gridLayout.whiteRowDelta).toBeLessThanOrEqual(1);
11221142
expect(gridLayout.blackRowDelta).toBeLessThanOrEqual(1);
11231143
expect(gridLayout.whiteRowHeightDelta).toBeLessThanOrEqual(1);

tools/midi-studio-v2/js/controls/InstrumentGridControl.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ export class InstrumentGridControl {
577577
grid.className = "midi-studio-v2__instrument-grid midi-studio-v2__note-table midi-studio-v2__octave-timeline";
578578
grid.setAttribute("role", "table");
579579
grid.setAttribute("aria-label", "Octave timeline editor");
580-
grid.style.gridTemplateColumns = `4.25rem repeat(${result.totalSteps}, var(--midi-studio-v2-octave-cell-size))`;
580+
grid.style.gridTemplateColumns = `5.25rem repeat(${result.totalSteps}, var(--midi-studio-v2-octave-cell-size))`;
581581
this.renderNoteTableHeader(grid, result);
582582
octaveRows.forEach((row, rowIndex) => {
583583
grid.append(this.createOctaveRowHeader(row, rowIndex));
@@ -828,7 +828,10 @@ export class InstrumentGridControl {
828828
header.dataset.octave = row.octave;
829829
header.dataset.keyKind = keyKind;
830830
header.dataset.octaveRowIndex = String(rowIndex);
831-
header.textContent = row.label;
831+
const label = document.createElement("span");
832+
label.className = "midi-studio-v2__octave-row-label-text";
833+
label.textContent = row.label;
834+
header.append(label);
832835
return header;
833836
}
834837

tools/midi-studio-v2/styles/midiStudioV2.css

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -439,29 +439,43 @@ body[data-tool-id="midi-studio-v2"] #instrumentGridContent {
439439

440440
.midi-studio-v2__octave-row-label {
441441
align-content: center;
442-
background: var(--tool-starter-surface);
442+
background: #f8fafc;
443443
height: var(--midi-studio-v2-octave-cell-size);
444444
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
445-
font-size: 0.7rem;
445+
font-size: 0.72rem;
446+
font-weight: 900;
446447
isolation: isolate;
447448
justify-items: start;
449+
line-height: 1;
448450
min-height: var(--midi-studio-v2-octave-cell-size);
449-
overflow: hidden;
450-
padding: 0 0.45rem;
451+
overflow: visible;
452+
padding: 0;
451453
text-align: left;
452454
text-transform: none;
453455
}
454456

455457
.midi-studio-v2__octave-row-label::before {
456458
content: "";
457-
inset-block: 0;
459+
bottom: 0;
458460
left: 0;
459461
pointer-events: none;
460462
position: absolute;
463+
top: 0;
461464
width: 100%;
462465
z-index: 0;
463466
}
464467

468+
.midi-studio-v2__octave-row-label-text {
469+
display: block;
470+
overflow: hidden;
471+
padding-left: 0.5rem;
472+
padding-right: 0.25rem;
473+
pointer-events: none;
474+
position: relative;
475+
text-overflow: ellipsis;
476+
z-index: 2;
477+
}
478+
465479
.midi-studio-v2__timeline-scroll-proxy {
466480
background: var(--tool-starter-surface);
467481
border-bottom: 1px solid var(--tool-starter-line);
@@ -765,32 +779,44 @@ body[data-tool-id="midi-studio-v2"] #instrumentGridContent {
765779
}
766780

767781
.midi-studio-v2__octave-row-label--white-key {
768-
background: linear-gradient(90deg, #ffffff 0%, #f8fafc 72%, #dbeafe 100%);
782+
background: #f8fafc;
769783
border-right-color: #94a3b8;
770784
color: #0f172a;
771-
box-shadow: inset -0.28rem 0 0 rgba(148, 163, 184, 0.28), inset 0 -1px 0 rgba(15, 23, 42, 0.16);
785+
box-shadow: none;
772786
}
773787

774788
.midi-studio-v2__octave-row-label--white-key::before {
775-
background: linear-gradient(90deg, #ffffff 0%, #f8fafc 72%, #dbeafe 100%);
776-
box-shadow: inset -0.28rem 0 0 rgba(148, 163, 184, 0.28), inset 0 -1px 0 rgba(15, 23, 42, 0.16);
789+
background: linear-gradient(90deg, #ffffff 0%, #f8fafc 78%, #dbeafe 100%);
790+
border: 1px solid #cbd5e1;
791+
border-left: 0;
792+
border-radius: 0 4px 4px 0;
793+
box-shadow: inset -0.34rem 0 0 rgba(148, 163, 184, 0.24), inset 0 -1px 0 rgba(15, 23, 42, 0.16);
777794
width: 100%;
778795
}
779796

780797
.midi-studio-v2__octave-row-label--black-key {
781-
background: color-mix(in srgb, var(--tool-starter-surface) 78%, #020617 22%);
798+
background: #f8fafc;
782799
border-right-color: #020617;
783800
color: #f8fafc;
784801
justify-items: start;
802+
z-index: 6;
803+
}
804+
805+
.midi-studio-v2__octave-row-label--black-key .midi-studio-v2__octave-row-label-text {
785806
padding-left: 0.95rem;
807+
text-shadow: 0 1px 1px rgba(2, 6, 23, 0.9);
786808
}
787809

788810
.midi-studio-v2__octave-row-label--black-key::before {
789811
background: linear-gradient(90deg, #020617 0%, #020617 68%, #1e293b 100%);
790-
border-radius: 0 0 4px 0;
791-
box-shadow: inset -1.15rem 0 0 rgba(2, 6, 23, 0.96), inset 0 -1px 0 rgba(248, 250, 252, 0.18);
792-
left: 0.65rem;
793-
width: 68%;
812+
border: 1px solid #020617;
813+
border-left: 0;
814+
border-radius: 0 4px 4px 0;
815+
box-shadow: 0 2px 4px rgba(2, 6, 23, 0.38), inset -1.15rem 0 0 rgba(2, 6, 23, 0.96), inset 0 -1px 0 rgba(248, 250, 252, 0.18);
816+
bottom: -12%;
817+
left: 0.58rem;
818+
top: -12%;
819+
width: 64%;
794820
}
795821

796822
.midi-studio-v2__octave-row-label--percussion-key {

0 commit comments

Comments
 (0)