Skip to content

Commit 997c6e4

Browse files
committed
Improve MIDI Studio V2 Song Sheet and Instrument Settings responsive field layouts - PR_26146_064-midi-studio-v2-field-grid-layouts
1 parent c8a7218 commit 997c6e4

6 files changed

Lines changed: 308 additions & 7 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# PR_26146_064 MIDI Studio V2 Field Grid Layouts Validation
2+
3+
## Scope
4+
5+
- Continued from `PR_26146_063`.
6+
- Refined Song Setup > Song Sheet into responsive editable and read-only/display field grids.
7+
- Preserved Song Sheet structure-only ownership for Sections and Loop sections.
8+
- Added shared MIDI Studio field-card states for editable, read-only, and unwired controls.
9+
- Refined Instruments > Instrument Settings into a responsive bucket/field grid.
10+
- Added a read-only derived Audible preview display field in Instrument Settings.
11+
- Preserved Instruments as the SSoT owner for GM Type, GM Instrument/Patch, volume, and related configuration.
12+
- Preserved Song Details Notes full-width bottom row, Octave Timeline Instruments accordion, selectedInstrumentId sync, canvas-backed Octave Timeline, Play/Stop, manifest import, multiple songs, Export tab ownership, and launch-specific NAV.
13+
14+
## Validation
15+
16+
PASS - `node --check tools/midi-studio-v2/js/controls/InstrumentGridControl.js`
17+
18+
PASS - `node --check tools/midi-studio-v2/js/controls/SongSheetControl.js`
19+
20+
PASS - `node --check tests/playwright/tools/MidiStudioV2.spec.mjs`
21+
22+
PASS - HTML restriction check found no inline script blocks, style blocks, inline event handlers, or inline style attributes in `tools/midi-studio-v2/index.html`.
23+
24+
PASS - `npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --project=playwright -g "PR064"`
25+
26+
Result: 1 passed.
27+
28+
PASS - `npx playwright test tests/playwright/tools/MidiStudioV2.spec.mjs --project=playwright -g "PR064|PR063|PR062|PR061|PR060"`
29+
30+
Result: 5 passed.
31+
32+
PASS - `git diff --check`
33+
34+
PASS - Playwright V8 coverage report generated at `docs/dev/reports/playwright_v8_coverage_report.txt`.
35+
36+
PASS - Changed runtime JS coverage guardrail generated at `docs/dev/reports/coverage_changed_js_guardrail.txt`.
37+
38+
## Playwright Evidence
39+
40+
- Song Sheet editable fields render in a responsive grid with multiple columns when width is available.
41+
- Song Sheet editable fields use editable field-card styling and remain enabled.
42+
- Song Sheet derived fields render in a responsive read-only/display grid with no editable controls.
43+
- Song Sheet derived fields expose read-only state.
44+
- Song Sheet Sections and Loop sections continue updating the canonical song model.
45+
- Instrument Settings buckets render in a responsive grid with multiple columns when width is available.
46+
- Instrument Settings fields inside buckets render in responsive grids.
47+
- Instrument editable fields use editable styling and remain enabled.
48+
- Instrument derived Audible preview uses read-only/display styling.
49+
- Future instrument controls remain red/unwired with tooltip/status.
50+
- Instrument configuration edits continue updating canonical instrument data.
51+
- Play and Stop still transition correctly.
52+
53+
## Samples Decision
54+
55+
SKIP - Full samples smoke test was not run per PR instruction. This PR is scoped to MIDI Studio V2 form layout and targeted tool behavior.
56+

tests/playwright/tools/MidiStudioV2.spec.mjs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3051,6 +3051,134 @@ test.describe("MIDI Studio V2", () => {
30513051
}
30523052
});
30533053

3054+
test("keeps PR064 Song Sheet and Instrument Settings fields in responsive editable/read-only grids", async ({ page }) => {
3055+
await page.setViewportSize({ width: 1600, height: 900 });
3056+
const server = await openMidiStudioForImport(page);
3057+
try {
3058+
await page.locator("#toolImportManifestInput").setInputFiles(uatManifestPath);
3059+
await selectMidiStudioTab(page, "song-setup");
3060+
3061+
await page.locator("#songSheetSectionsInput").fill("intro: C F\nbridge: Dm G\nloop: F G C C");
3062+
await page.locator("#songSheetLoopSectionsInput").fill("bridge, loop");
3063+
await expect(page.locator("#songSheetSummary [data-song-sheet-summary-field='bars'] dd")).toHaveText("8");
3064+
const songSheetLayout = await page.locator("#songSheetContent").evaluate((content) => {
3065+
const columnCount = (element) => getComputedStyle(element).gridTemplateColumns.split(" ").filter(Boolean).length;
3066+
const fieldRows = (fields) => new Set(fields.map((field) => Math.round(field.getBoundingClientRect().top))).size;
3067+
const editableGrid = content.querySelector(".midi-studio-v2__song-sheet-grid");
3068+
const summary = content.querySelector("#songSheetSummary");
3069+
const editableFields = Array.from(editableGrid.querySelectorAll("[data-midi-studio-field-state='editable']"));
3070+
const readonlyFields = Array.from(summary.querySelectorAll("[data-midi-studio-field-state='readonly']"));
3071+
return {
3072+
editableBackgrounds: editableFields.map((field) => getComputedStyle(field).backgroundColor),
3073+
editableBorderStyles: editableFields.map((field) => getComputedStyle(field).borderStyle),
3074+
editableColumns: columnCount(editableGrid),
3075+
editableControls: editableGrid.querySelectorAll("textarea:not([readonly]):not([disabled])").length,
3076+
editableFieldRows: fieldRows(editableFields),
3077+
readonlyAria: readonlyFields.every((field) => field.getAttribute("aria-readonly") === "true"),
3078+
readonlyBorderStyles: readonlyFields.map((field) => getComputedStyle(field).borderStyle),
3079+
readonlyControls: summary.querySelectorAll("input, select, textarea").length,
3080+
readonlyFields: readonlyFields.length,
3081+
summaryColumns: columnCount(summary),
3082+
summaryFieldRows: fieldRows(readonlyFields)
3083+
};
3084+
});
3085+
expect(songSheetLayout.editableColumns).toBeGreaterThan(1);
3086+
expect(songSheetLayout.summaryColumns).toBeGreaterThan(1);
3087+
expect(songSheetLayout.editableFieldRows).toBe(1);
3088+
expect(songSheetLayout.summaryFieldRows).toBeLessThan(songSheetLayout.readonlyFields);
3089+
expect(songSheetLayout.editableControls).toBe(2);
3090+
expect(songSheetLayout.readonlyControls).toBe(0);
3091+
expect(songSheetLayout.readonlyFields).toBeGreaterThanOrEqual(6);
3092+
expect(songSheetLayout.readonlyAria).toBe(true);
3093+
expect(songSheetLayout.editableBorderStyles.every((style) => style === "solid")).toBe(true);
3094+
expect(songSheetLayout.readonlyBorderStyles.every((style) => style === "dashed")).toBe(true);
3095+
expect(new Set(songSheetLayout.editableBackgrounds).size).toBe(1);
3096+
3097+
const songSheetCanonical = await page.evaluate(() => window.__midiStudioV2App.selectedSong().studioArrangement.songSheet);
3098+
expect(songSheetCanonical).toEqual({
3099+
loopSections: "bridge, loop",
3100+
sections: "intro: C F\nbridge: Dm G\nloop: F G C C"
3101+
});
3102+
3103+
await selectMidiStudioTab(page, "instruments");
3104+
const editor = page.locator("#selectedInstrumentEditor");
3105+
await expect(editor).toHaveAttribute("data-selected-instrument-id", "lead");
3106+
const instrumentLayout = await editor.evaluate((instrumentEditor) => {
3107+
const columnCount = (element) => getComputedStyle(element).gridTemplateColumns.split(" ").filter(Boolean).length;
3108+
const buckets = Array.from(instrumentEditor.querySelectorAll(".midi-studio-v2__instrument-editor-bucket"));
3109+
const bucketRows = new Set(buckets.map((bucket) => Math.round(bucket.getBoundingClientRect().top))).size;
3110+
const identityBucket = instrumentEditor.querySelector("[data-instrument-editor-bucket='identity']");
3111+
const editableFields = Array.from(instrumentEditor.querySelectorAll("[data-midi-studio-field-state='editable']"));
3112+
const readonlyFields = Array.from(instrumentEditor.querySelectorAll("[data-midi-studio-field-state='readonly']"));
3113+
const unwiredFields = Array.from(instrumentEditor.querySelectorAll("[data-midi-studio-field-state='unwired']"));
3114+
const readonlyOutput = instrumentEditor.querySelector("[data-instrument-derived-field='audible-preview']");
3115+
return {
3116+
bucketRows,
3117+
buckets: buckets.length,
3118+
editorColumns: columnCount(instrumentEditor),
3119+
editableBorderStyles: editableFields.map((field) => getComputedStyle(field).borderStyle),
3120+
editableControls: editableFields.map((field) => field.querySelector("input, select, textarea")?.disabled === false),
3121+
identityColumns: columnCount(identityBucket),
3122+
readonlyBorderStyles: readonlyFields.map((field) => getComputedStyle(field).borderStyle),
3123+
readonlyOutputText: readonlyOutput?.textContent || "",
3124+
readonlyOutputs: readonlyFields.filter((field) => field.querySelector("output[aria-readonly='true']")).length,
3125+
unwiredControls: unwiredFields.map((field) => {
3126+
const control = field.querySelector("[data-midi-studio-future-control]");
3127+
return {
3128+
borderStyle: getComputedStyle(field).borderStyle,
3129+
controlClass: control?.classList.contains("midi-studio-v2__unwired-control") || false,
3130+
disabled: control?.disabled || false,
3131+
status: control?.dataset.midiStudioUnwired || "",
3132+
title: control?.title || ""
3133+
};
3134+
}),
3135+
unwiredFields: unwiredFields.length
3136+
};
3137+
});
3138+
expect(instrumentLayout.editorColumns).toBeGreaterThan(1);
3139+
expect(instrumentLayout.identityColumns).toBeGreaterThan(1);
3140+
expect(instrumentLayout.bucketRows).toBeLessThan(instrumentLayout.buckets);
3141+
expect(instrumentLayout.editableControls.every(Boolean)).toBe(true);
3142+
expect(instrumentLayout.editableBorderStyles.every((style) => style === "solid")).toBe(true);
3143+
expect(instrumentLayout.readonlyOutputs).toBeGreaterThanOrEqual(1);
3144+
expect(instrumentLayout.readonlyOutputText).toContain("Lead");
3145+
expect(instrumentLayout.readonlyBorderStyles.every((style) => style === "dashed")).toBe(true);
3146+
expect(instrumentLayout.unwiredFields).toBeGreaterThanOrEqual(8);
3147+
expect(instrumentLayout.unwiredControls.every((control) => control.borderStyle === "solid" && control.controlClass && control.disabled && control.status === "not-implemented" && control.title.includes("Not implemented:"))).toBe(true);
3148+
3149+
await page.locator("#previewDisplayNameLeadInput").fill("Lead Grid Voice");
3150+
await setInputValue(page, "#previewVolumeLeadInput", "0.75");
3151+
await instrumentTypeSelect(page, "lead").selectOption("Bass");
3152+
await instrumentSelect(page, "lead").selectOption("gm-electric-bass-finger");
3153+
await expect(editor.locator("[data-instrument-derived-field='audible-preview']")).toHaveText("Synth Bass 1");
3154+
expect(await page.evaluate(() => {
3155+
const settings = window.__midiStudioV2App.selectedSong().studioArrangement.previewLaneSettings;
3156+
return {
3157+
displayName: settings.displayNames.lead,
3158+
instrument: settings.instruments.lead,
3159+
instrumentType: settings.instrumentTypes.lead,
3160+
volume: settings.volumes.lead
3161+
};
3162+
})).toEqual({
3163+
displayName: "Lead Grid Voice",
3164+
instrument: "gm-electric-bass-finger",
3165+
instrumentType: "Bass",
3166+
volume: 0.75
3167+
});
3168+
3169+
await selectMidiStudioTab(page, "studio");
3170+
await page.locator("#playButton").click();
3171+
await expect(page.locator("#playButton")).toBeDisabled();
3172+
await expect(page.locator("#stopButton")).toBeEnabled();
3173+
await page.locator("#stopButton").click();
3174+
await expect(page.locator("#stopButton")).toBeDisabled();
3175+
await expect(page.locator("#playButton")).toBeEnabled();
3176+
} finally {
3177+
await workspaceV2CoverageReporter.stop(page);
3178+
await server.close();
3179+
}
3180+
});
3181+
30543182
test("derives primary song, instrument, grid, playback, and diagnostics views from the canonical selected song", async ({ page }) => {
30553183
const server = await openMidiStudioForImport(page);
30563184
try {

tools/midi-studio-v2/index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,11 +261,11 @@ <h3 id="exportStatusHeading">Export Status</h3>
261261
</button>
262262
<div id="songSheetContent" class="accordion-v2__content midi-studio-v2__stack midi-studio-v2__song-sheet-panel" aria-label="Song Sheet">
263263
<div class="midi-studio-v2__song-sheet-grid">
264-
<label class="tool-starter__field" for="songSheetSectionsInput">
264+
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetSectionsInput">
265265
<span>Sections</span>
266266
<textarea id="songSheetSectionsInput" rows="4" placeholder="intro: Am F&#10;loop: Am F C G"></textarea>
267267
</label>
268-
<label class="tool-starter__field" for="songSheetLoopSectionsInput">
268+
<label class="tool-starter__field midi-studio-v2__field-card" data-midi-studio-field-state="editable" for="songSheetLoopSectionsInput">
269269
<span>Loop sections</span>
270270
<textarea id="songSheetLoopSectionsInput" rows="2" placeholder="loop"></textarea>
271271
</label>

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

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,13 +1027,16 @@ export class InstrumentGridControl {
10271027
const displayName = this.createDisplayNameInput(lane);
10281028
const typeSelect = this.createInstrumentTypeSelect(lane);
10291029
const instrumentSelect = this.createInstrumentSelect(lane);
1030+
const previewMapping = this.createInstrumentDisplayOutput("audible-preview", this.previewMappingForLane(lane));
10301031
this.selectedEditorControls.displayName = displayName;
10311032
this.selectedEditorControls.instrumentType = typeSelect;
10321033
this.selectedEditorControls.instrument = instrumentSelect;
1034+
this.selectedEditorControls.previewMapping = previewMapping;
10331035
return this.createInstrumentEditorBucket("Identity", "identity", [
10341036
this.createEditorField("Instrument display name", displayName),
10351037
this.createEditorField("GM Type", typeSelect),
1036-
this.createEditorField("GM Instrument", instrumentSelect)
1038+
this.createEditorField("GM Instrument", instrumentSelect),
1039+
this.createEditorField("Audible preview", previewMapping)
10371040
]);
10381041
}
10391042

@@ -1132,12 +1135,34 @@ export class InstrumentGridControl {
11321135
createEditorField(labelText, control) {
11331136
const label = document.createElement("label");
11341137
const text = document.createElement("span");
1135-
label.className = "tool-starter__field midi-studio-v2__instrument-editor-field";
1138+
const controlElement = this.primaryEditorControl(control);
1139+
label.className = "tool-starter__field midi-studio-v2__field-card midi-studio-v2__instrument-editor-field";
1140+
label.dataset.midiStudioFieldState = this.editorFieldState(controlElement);
11361141
text.textContent = labelText;
11371142
label.append(text, control);
11381143
return label;
11391144
}
11401145

1146+
primaryEditorControl(control) {
1147+
if (control?.matches?.("input, select, textarea, output")) {
1148+
return control;
1149+
}
1150+
return control?.querySelector?.("input, select, textarea, output") || null;
1151+
}
1152+
1153+
editorFieldState(control) {
1154+
if (!control) {
1155+
return "readonly";
1156+
}
1157+
if (control.dataset?.midiStudioFutureControl !== undefined || control.dataset?.midiStudioUnwired) {
1158+
return "unwired";
1159+
}
1160+
if (control.disabled || control.readOnly || control.tagName === "OUTPUT") {
1161+
return "readonly";
1162+
}
1163+
return "editable";
1164+
}
1165+
11411166
createOctaveRangeField(lowInput, highInput) {
11421167
const group = document.createElement("span");
11431168
group.className = "midi-studio-v2__octave-range-field";
@@ -1167,6 +1192,31 @@ export class InstrumentGridControl {
11671192
return input;
11681193
}
11691194

1195+
createInstrumentDisplayOutput(kind, value) {
1196+
const output = document.createElement("output");
1197+
output.dataset.instrumentDerivedField = kind;
1198+
output.value = value;
1199+
output.textContent = value;
1200+
output.setAttribute("aria-readonly", "true");
1201+
return output;
1202+
}
1203+
1204+
previewMappingForLane(lane) {
1205+
const state = this.previewLaneState[lane] || {};
1206+
return audiblePreviewLabel(state.instrument) || "No audible preview selected";
1207+
}
1208+
1209+
updateSelectedInstrumentDisplayFields(lane) {
1210+
if (this.selectedEditorControls?.lane !== lane) {
1211+
return;
1212+
}
1213+
if (this.selectedEditorControls.previewMapping) {
1214+
const value = this.previewMappingForLane(lane);
1215+
this.selectedEditorControls.previewMapping.value = value;
1216+
this.selectedEditorControls.previewMapping.textContent = value;
1217+
}
1218+
}
1219+
11701220
createInstrumentNumberInput(lane, kind, { ariaLabel, datasetName, max, min, step, value }) {
11711221
const input = document.createElement("input");
11721222
input.id = `preview${kind.charAt(0).toUpperCase()}${kind.slice(1)}${laneId(lane)}Input`;
@@ -1981,6 +2031,7 @@ export class InstrumentGridControl {
19812031
this.populateInstrumentOptions(lane, this.selectedEditorControls.instrument);
19822032
}
19832033
this.updateLaneTitle(lane);
2034+
this.updateSelectedInstrumentDisplayFields(lane);
19842035
this.syncQuickInstrumentControls();
19852036
this.renderAuditionKeyboard();
19862037
this.renderSelectionDetails();
@@ -2018,6 +2069,7 @@ export class InstrumentGridControl {
20182069
editorControls.instrumentType.value = nextType;
20192070
}
20202071
this.updateLaneTitle(lane);
2072+
this.updateSelectedInstrumentDisplayFields(lane);
20212073
this.syncQuickInstrumentControls();
20222074
this.renderAuditionKeyboard();
20232075
this.renderSelectionDetails();
@@ -2366,6 +2418,7 @@ export class InstrumentGridControl {
23662418
this.selectedEditorControls.octaveHigh.value = String(highOctave);
23672419
}
23682420
}
2421+
this.updateSelectedInstrumentDisplayFields(lane);
23692422
}
23702423
}
23712424
this.syncQuickInstrumentControls();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ export class SongSheetControl {
158158
const term = document.createElement("dt");
159159
const description = document.createElement("dd");
160160
const token = fieldToken(label);
161+
row.className = "midi-studio-v2__field-card midi-studio-v2__song-sheet-display-field";
161162
row.dataset.songSheetSummaryField = token;
163+
row.dataset.midiStudioFieldState = "readonly";
164+
row.setAttribute("aria-readonly", "true");
162165
term.textContent = label;
163166
description.textContent = value === undefined || value === null || value === "" ? "not declared" : String(value);
164167
description.dataset.songSheetReadonly = token;

0 commit comments

Comments
 (0)