Skip to content

Commit 621e96f

Browse files
committed
Make MIDI Studio V2 octave grid use piano-style keys and 50 percent viewport height - PR_26146_040-midi-studio-v2-piano-key-grid-height
1 parent 3b6bc3d commit 621e96f

4 files changed

Lines changed: 182 additions & 2 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# PR_26146_040 MIDI Studio V2 Piano Key Grid Height Validation
2+
3+
Status: PASS
4+
5+
## Scope
6+
7+
- Refined MIDI Studio V2 Octave Timeline left note labels into piano-style key rows.
8+
- Set the octave spreadsheet viewport to 50% of screen height with vertical scrolling.
9+
- Preserved square cells, zoom controls, synced horizontal scrolling, active note highlights, editing, and Play/Stop behavior.
10+
- Roadmap was not changed because this PR did not provide an execution-backed roadmap status transition request.
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_040-midi-studio-v2-piano-key-grid-height_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'); 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'); }"
36+
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" --config=codex_playwright_system_chrome.config.cjs --reporter=list --workers=1 --timeout=60000
37+
git diff --check
38+
```
39+
40+
Playwright result:
41+
42+
```text
43+
Running 2 tests using 1 worker
44+
45+
ok 1 MIDI Studio V2 - fast octave note editing supports drag painting keyboard shortcuts selection and timeline scroll sync
46+
ok 2 MIDI Studio V2 - octave timeline freezes compact headers and note labels while active cells stay textless
47+
48+
2 passed
49+
```
50+
51+
Notes:
52+
53+
- `git diff --check` exited 0. Git emitted line-ending warnings for existing LF/CRLF handling in touched files; no whitespace errors were reported.
54+
- An initial expanded Playwright command also included `updates play and stop control state without requiring real audio output`; that standalone legacy test timed out during V8 coverage cleanup. The final focused passing command covers Play/Stop through the fast-edit keyboard workflow and direct Play/Stop button assertions added to the PR040 grid test.
55+
56+
## Test Evidence
57+
58+
- Left key/chord column has no independent fixed row height: Playwright verifies piano row label height tracks square note cell height and changes with zoom.
59+
- Natural and sharp/flat piano rows are visually distinct: Playwright verifies `white` and `black` key kinds, CSS classes, colors, and labels.
60+
- Row alignment: Playwright verifies C5 and C#5 labels align with matching timeline cells by top and height.
61+
- Grid viewport height: Playwright verifies `#instrumentGridOutput` renders at 50% of `window.innerHeight`.
62+
- Vertical scrolling: Playwright verifies the grid can scroll vertically and header rows stay frozen beneath the top horizontal scrollbar.
63+
- Horizontal scrolling: Playwright verifies top scrollbar, native bottom scroll, and header/body positions remain synchronized.
64+
- Square cells and zoom controls: Playwright verifies width equals height and zoom in/out changes square cell size without breaking header/body alignment.
65+
- Editing: Playwright verifies active notes remain highlighted without printed note text and cells remain editable after zoom changes.
66+
- Play/Stop: Playwright verifies Space keyboard Play/Stop and button Play/Stop state transitions.
67+
68+
## Samples
69+
70+
Full samples smoke test: SKIP.
71+
72+
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.
73+
74+
## Manual UAT
75+
76+
1. Open MIDI Studio V2 and import the UAT manifest.
77+
2. Confirm the Octave Timeline viewport is about half the screen height and vertical scrolling reveals additional notes.
78+
3. Scroll horizontally and confirm left piano-style labels stay frozen and aligned with rows.
79+
4. Use zoom in/out and confirm cells remain square while key labels stay aligned.
80+
5. Toggle notes/chords and confirm highlighted notes stay textless.
81+
6. Start and stop playback and confirm the playhead/header behavior remains aligned.

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,14 +1004,45 @@ test.describe("MIDI Studio V2", () => {
10041004
const topScrollbar = element.querySelector(".midi-studio-v2__timeline-scroll-proxy");
10051005
const grid = element.querySelector(".midi-studio-v2__octave-timeline");
10061006
const firstLabel = element.querySelector(".midi-studio-v2__octave-row-label");
1007+
const whiteLabel = element.querySelector('.midi-studio-v2__octave-row-label[data-octave-row="C5"]');
1008+
const blackLabel = element.querySelector('.midi-studio-v2__octave-row-label[data-octave-row="C#5"]');
1009+
const whiteCell = element.querySelector('.midi-studio-v2__octave-note-cell[data-row-token="C5"][data-step-index="0"]');
1010+
const blackCell = element.querySelector('.midi-studio-v2__octave-note-cell[data-row-token="C#5"][data-step-index="0"]');
1011+
const outputStyle = getComputedStyle(element);
1012+
const whiteLabelStyle = getComputedStyle(whiteLabel);
1013+
const blackLabelStyle = getComputedStyle(blackLabel);
1014+
const whiteLabelRect = whiteLabel.getBoundingClientRect();
1015+
const blackLabelRect = blackLabel.getBoundingClientRect();
1016+
const whiteCellRect = whiteCell.getBoundingClientRect();
1017+
const blackCellRect = blackCell.getBoundingClientRect();
10071018
return {
10081019
alternatingRowsDiffer: getComputedStyle(firstRow).backgroundColor !== getComputedStyle(secondRow).backgroundColor,
1020+
blackBackground: blackLabelStyle.backgroundImage || blackLabelStyle.backgroundColor,
1021+
blackClass: blackLabel.className,
1022+
blackColor: blackLabelStyle.color,
1023+
blackKind: blackLabel.dataset.keyKind,
1024+
blackLabelHeight: blackLabelRect.height,
1025+
blackLabelText: blackLabel.textContent,
10091026
borderRightWidth: getComputedStyle(firstNote).borderRightWidth,
1027+
canScrollVertically: element.scrollHeight > element.clientHeight,
10101028
cellCssWidth: getComputedStyle(firstNote).width,
10111029
cellHeight: firstNote.getBoundingClientRect().height,
10121030
columnWidth: firstNote.getBoundingClientRect().width,
10131031
columnTemplate: getComputedStyle(grid).gridTemplateColumns,
10141032
containsGrid: grid?.parentElement === element,
1033+
expectedViewportHeight: window.innerHeight * 0.5,
1034+
overflowY: outputStyle.overflowY,
1035+
viewportHeight: element.getBoundingClientRect().height,
1036+
whiteBackground: whiteLabelStyle.backgroundImage || whiteLabelStyle.backgroundColor,
1037+
whiteClass: whiteLabel.className,
1038+
whiteColor: whiteLabelStyle.color,
1039+
whiteKind: whiteLabel.dataset.keyKind,
1040+
whiteLabelHeight: whiteLabelRect.height,
1041+
whiteLabelText: whiteLabel.textContent,
1042+
whiteRowDelta: Math.abs(whiteLabelRect.top - whiteCellRect.top),
1043+
whiteRowHeightDelta: Math.abs(whiteLabelRect.height - whiteCellRect.height),
1044+
blackRowDelta: Math.abs(blackLabelRect.top - blackCellRect.top),
1045+
blackRowHeightDelta: Math.abs(blackLabelRect.height - blackCellRect.height),
10151046
labelWidth: firstLabel.getBoundingClientRect().width,
10161047
topScrollbarHeight: topScrollbar.getBoundingClientRect().height
10171048
};
@@ -1026,20 +1057,39 @@ test.describe("MIDI Studio V2", () => {
10261057
expect(gridLayout.columnTemplate).toContain("22px");
10271058
expect(gridLayout.labelWidth).toBeGreaterThan(40);
10281059
expect(gridLayout.topScrollbarHeight).toBeGreaterThanOrEqual(12);
1060+
expect(Math.abs(gridLayout.viewportHeight - gridLayout.expectedViewportHeight)).toBeLessThanOrEqual(2);
1061+
expect(gridLayout.overflowY).toMatch(/auto|scroll/);
1062+
expect(gridLayout.canScrollVertically).toBe(true);
1063+
expect(gridLayout.whiteKind).toBe("white");
1064+
expect(gridLayout.blackKind).toBe("black");
1065+
expect(gridLayout.whiteClass).toContain("midi-studio-v2__octave-row-label--white-key");
1066+
expect(gridLayout.blackClass).toContain("midi-studio-v2__octave-row-label--black-key");
1067+
expect(gridLayout.whiteLabelText).toBe("C5");
1068+
expect(gridLayout.blackLabelText).toBe("C#5");
1069+
expect(gridLayout.whiteBackground).not.toBe(gridLayout.blackBackground);
1070+
expect(gridLayout.whiteColor).not.toBe(gridLayout.blackColor);
1071+
expect(gridLayout.whiteRowDelta).toBeLessThanOrEqual(1);
1072+
expect(gridLayout.blackRowDelta).toBeLessThanOrEqual(1);
1073+
expect(gridLayout.whiteRowHeightDelta).toBeLessThanOrEqual(1);
1074+
expect(gridLayout.blackRowHeightDelta).toBeLessThanOrEqual(1);
10291075

10301076
await page.locator("#instrumentGridZoomInButton").click();
10311077
const zoomedInLayout = await output.evaluate((element) => {
10321078
const firstNote = element.querySelector('.midi-studio-v2__octave-note-cell[data-step-index="0"]');
10331079
const header = element.querySelector('.midi-studio-v2__grid-cell--beat-header[data-step-index="6"]').getBoundingClientRect();
10341080
const bodyCell = element.querySelector('.midi-studio-v2__octave-note-cell[data-row-token="C5"][data-step-index="6"]').getBoundingClientRect();
1081+
const label = element.querySelector('.midi-studio-v2__octave-row-label[data-octave-row="C5"]').getBoundingClientRect();
10351082
return {
10361083
bodyHeaderDelta: Math.abs(header.left - bodyCell.left),
10371084
height: firstNote.getBoundingClientRect().height,
1085+
labelHeight: label.height,
10381086
width: firstNote.getBoundingClientRect().width
10391087
};
10401088
});
10411089
expect(zoomedInLayout.width).toBeGreaterThan(gridLayout.columnWidth);
10421090
expect(Math.abs(zoomedInLayout.width - zoomedInLayout.height)).toBeLessThanOrEqual(1);
1091+
expect(zoomedInLayout.labelHeight).toBeGreaterThan(gridLayout.whiteLabelHeight);
1092+
expect(Math.abs(zoomedInLayout.labelHeight - zoomedInLayout.height)).toBeLessThanOrEqual(1);
10431093
expect(zoomedInLayout.bodyHeaderDelta).toBeLessThanOrEqual(1);
10441094

10451095
await page.locator("#instrumentGridZoomOutButton").click();
@@ -1048,15 +1098,19 @@ test.describe("MIDI Studio V2", () => {
10481098
const firstNote = element.querySelector('.midi-studio-v2__octave-note-cell[data-step-index="0"]');
10491099
const header = element.querySelector('.midi-studio-v2__grid-cell--beat-header[data-step-index="6"]').getBoundingClientRect();
10501100
const bodyCell = element.querySelector('.midi-studio-v2__octave-note-cell[data-row-token="C5"][data-step-index="6"]').getBoundingClientRect();
1101+
const label = element.querySelector('.midi-studio-v2__octave-row-label[data-octave-row="C5"]').getBoundingClientRect();
10511102
return {
10521103
bodyHeaderDelta: Math.abs(header.left - bodyCell.left),
10531104
height: firstNote.getBoundingClientRect().height,
1105+
labelHeight: label.height,
10541106
width: firstNote.getBoundingClientRect().width
10551107
};
10561108
});
10571109
expect(zoomedOutLayout.width).toBeLessThan(zoomedInLayout.width);
10581110
expect(zoomedOutLayout.width).toBeGreaterThan(1);
10591111
expect(Math.abs(zoomedOutLayout.width - zoomedOutLayout.height)).toBeLessThanOrEqual(1);
1112+
expect(zoomedOutLayout.labelHeight).toBeLessThan(zoomedInLayout.labelHeight);
1113+
expect(Math.abs(zoomedOutLayout.labelHeight - zoomedOutLayout.height)).toBeLessThanOrEqual(1);
10601114
expect(zoomedOutLayout.bodyHeaderDelta).toBeLessThanOrEqual(1);
10611115

10621116
await output.evaluate((element) => {
@@ -1161,6 +1215,13 @@ test.describe("MIDI Studio V2", () => {
11611215
});
11621216
expect(activeHighlight.content).not.toBe("none");
11631217
expect(Number.parseFloat(activeHighlight.width)).toBeGreaterThan(1);
1218+
1219+
await page.locator("#playButton").click();
1220+
await expect(page.locator("#stopButton")).toBeEnabled();
1221+
await expect(page.locator("#playButton")).toBeDisabled();
1222+
await page.locator("#stopButton").click();
1223+
await expect(page.locator("#stopButton")).toBeDisabled();
1224+
await expect(page.locator("#playButton")).toBeEnabled();
11641225
} finally {
11651226
await workspaceV2CoverageReporter.stop(page);
11661227
await server.close();

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,22 +811,32 @@ export class InstrumentGridControl {
811811
}
812812

813813
createOctaveRowHeader(row, rowIndex) {
814+
const keyKind = this.octaveRowKeyKind(row);
814815
const header = document.createElement("div");
815816
header.className = [
816817
"midi-studio-v2__grid-cell",
817818
"midi-studio-v2__grid-cell--label",
818819
"midi-studio-v2__grid-cell--instrument-column",
819820
"midi-studio-v2__octave-row-label",
821+
`midi-studio-v2__octave-row-label--${keyKind}-key`,
820822
rowIndex % 2 === 1 ? "midi-studio-v2__octave-row-label--alternate" : ""
821823
].filter(Boolean).join(" ");
822824
header.setAttribute("role", "rowheader");
823825
header.dataset.octaveRow = row.value;
824826
header.dataset.octave = row.octave;
827+
header.dataset.keyKind = keyKind;
825828
header.dataset.octaveRowIndex = String(rowIndex);
826829
header.textContent = row.label;
827830
return header;
828831
}
829832

833+
octaveRowKeyKind(row) {
834+
if (row.octave === "percussion") {
835+
return "percussion";
836+
}
837+
return /#|b/.test(row.value) ? "black" : "white";
838+
}
839+
830840
createOctaveTimelineCell({ result, row, rowIndex, stepIndex }) {
831841
const events = this.orderedEventsForCell(this.visibleEventsForCell({ result, row, stepIndex }));
832842
const cell = document.createElement("div");

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,9 @@
326326
}
327327

328328
.midi-studio-v2__instrument-grid-output {
329+
height: 50vh;
329330
max-width: 100%;
331+
max-height: 50vh;
330332
overflow: auto;
331333
--midi-studio-v2-octave-cell-size: 22px;
332334
--midi-studio-v2-top-scrollbar-height: 16px;
@@ -421,11 +423,15 @@ body[data-tool-id="midi-studio-v2"] #instrumentGridContent {
421423
}
422424

423425
.midi-studio-v2__octave-row-label {
426+
align-content: center;
427+
height: var(--midi-studio-v2-octave-cell-size);
424428
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
425429
font-size: 0.7rem;
426430
justify-items: start;
427-
min-height: 1.55rem;
431+
min-height: var(--midi-studio-v2-octave-cell-size);
432+
padding: 0 0.45rem;
428433
text-align: left;
434+
text-transform: none;
429435
}
430436

431437
.midi-studio-v2__timeline-scroll-proxy {
@@ -730,6 +736,27 @@ body[data-tool-id="midi-studio-v2"] #instrumentGridContent {
730736
background: color-mix(in srgb, var(--tool-starter-panel-strong) 88%, #ffffff 12%);
731737
}
732738

739+
.midi-studio-v2__octave-row-label--white-key {
740+
background: linear-gradient(90deg, #f8fafc 0%, #e2e8f0 100%);
741+
border-right-color: #94a3b8;
742+
color: #0f172a;
743+
box-shadow: inset -0.35rem 0 0 rgba(148, 163, 184, 0.36);
744+
}
745+
746+
.midi-studio-v2__octave-row-label--black-key {
747+
background: linear-gradient(90deg, #020617 0%, #1e293b 100%);
748+
border-right-color: #020617;
749+
color: #f8fafc;
750+
justify-items: end;
751+
padding-right: 0.62rem;
752+
box-shadow: inset -1.05rem 0 0 rgba(2, 6, 23, 0.94);
753+
}
754+
755+
.midi-studio-v2__octave-row-label--percussion-key {
756+
background: color-mix(in srgb, var(--tool-starter-surface) 76%, #f59e0b 24%);
757+
color: var(--tool-starter-text);
758+
}
759+
733760
.midi-studio-v2__grid-cell--bar,
734761
.midi-studio-v2__grid-cell--ruler,
735762
.midi-studio-v2__grid-cell--beat {
@@ -980,5 +1007,6 @@ html:fullscreen body[data-tool-id="midi-studio-v2"] .tool-shell-common__fullscre
9801007

9811008
body.tools-platform-fullscreen-active .midi-studio-v2__instrument-grid-output,
9821009
html:fullscreen body[data-tool-id="midi-studio-v2"] .midi-studio-v2__instrument-grid-output {
983-
max-height: calc(100vh - 19rem);
1010+
height: 50vh;
1011+
max-height: 50vh;
9841012
}

0 commit comments

Comments
 (0)