Stacked on #31, which is stacked on #30. Merge those first.
The sheet listed its controls in one run-on line under it ("Drag a note to move it… · ←→ move · ↑↓ pitch (Shift: octave) · …"). That's hard to scan, and it grows with every feature. This replaces it with a grouped shortcut panel, like an editor's reference card.
The panel
Groups: Select, Move, Length, Notes, Kind, Syllable, Line, each a list of keys and what they do. Keys are drawn as key caps; mouse actions such as "click", "drag" and "double-click" are plain text.
Opening and closing: a KEYS ? button in the sheet's top bar or ? opens it; Esc closes it (a second Esc clears the selection, as before). It's inline rather than a pop-up, so it can stay open while you edit.
Remembered: whether it's open is kept in localStorage across lines, songs and visits. While it's closed, a one-line hint, "Press ? for keyboard shortcuts.", takes the old help line's place.
Code
The list is data: SHORTCUTS in sheet.js, where "+" joins keys pressed together and " / " separates alternatives. A new Node test reads the sheet's key handler and checks that every letter key the list names (S, M, G, F, R, P, Z, Y) is one it actually handles, so the list can't fall out of date. Another test covers how combos are split into key caps and mouse words.
Testing
In Chromium, on the seeded server: the button opens the panel, Esc closes it, ? reopens it, it's still open after a reload, all seven groups render, and there were no script errors.
From the screenshots, I widened the groups' minimum width from 210px to 320px. At the sheet's width that gives two columns instead of three cramped ones, so every action fits on one line.
20 Node tests pass, through pytest too. Full suite passes, except test_version (needs the package installed).
**Summary generated by LLM agent**
Stacked on #31, which is stacked on #30. Merge those first.
The sheet listed its controls in one run-on line under it ("Drag a note to move it… · ←→ move · ↑↓ pitch (Shift: octave) · …"). That's hard to scan, and it grows with every feature. This replaces it with a grouped shortcut panel, like an editor's reference card.
## The panel
- **Groups:** Select, Move, Length, Notes, Kind, Syllable, Line, each a list of keys and what they do. Keys are drawn as key caps; mouse actions such as "click", "drag" and "double-click" are plain text.
- **Opening and closing:** a KEYS ? button in the sheet's top bar or `?` opens it; Esc closes it (a second Esc clears the selection, as before). It's inline rather than a pop-up, so it can stay open while you edit.
- **Remembered:** whether it's open is kept in `localStorage` across lines, songs and visits. While it's closed, a one-line hint, "Press ? for keyboard shortcuts.", takes the old help line's place.
## Code
The list is data: `SHORTCUTS` in `sheet.js`, where "+" joins keys pressed together and " / " separates alternatives. A new Node test reads the sheet's key handler and checks that every letter key the list names (S, M, G, F, R, P, Z, Y) is one it actually handles, so the list can't fall out of date. Another test covers how combos are split into key caps and mouse words.
## Testing
- In Chromium, on the seeded server: the button opens the panel, Esc closes it, `?` reopens it, it's still open after a reload, all seven groups render, and there were no script errors.
- From the screenshots, I widened the groups' minimum width from 210px to 320px. At the sheet's width that gives two columns instead of three cramped ones, so every action fits on one line.
- 20 Node tests pass, through pytest too. Full suite passes, except `test_version` (needs the package installed).
The sheet's controls were listed in one run-on line under it, which was
hard to scan and grew with every feature. Replace it with a panel grouped
the way an editor's reference card is (select, move, length, notes, kind,
syllable, line) with keys drawn as key caps, opened by a KEYS button or ?,
closed by Esc, and remembered open or closed across visits.
The list is data (SHORTCUTS), and a test checks every letter key it names
is one the sheet's key handler handles, so the two cannot drift apart.
The beat grid came from the detected tempo (or the BPM override) and a GAP
pinned to the first note, so it did not always line up with the music,
and snapping did not land on its beats. A GRID bar on the sheet now sets
both, redrawing the grid live as values are tried, with x2 and /2 for a
tempo detected at double or half speed and ALIGN TO NOTE to move GAP the
least that puts the selected note on a beat. Note editing pauses while a
trial grid is unapplied, so notes never snap to a grid that is then
cancelled.
The grid is stored with the song's edits, so every rebuild keeps it, and
grid charts with it in place of its own choice; it records that choice in
tempo.json for RESET to return to. A GAP after the first note moves back
by whole beats, as notes on negative beats are rejected by players. PUT
and DELETE /api/jobs/{id}/grid set and reset it, rewriting the charts in
place.
Line breaks were always placed by songtrace, midway between lines and
recomputed around edited ones, so a break could not be put where a singer
breathes. The sheet now draws the breaks either side of a line as markers
that drag along the beat grid, kept between the lines they separate, and
double-click back to automatic. Break moves share the line's undo history
and SAVE LINE, which saves the notes only if they changed, so moving a
break never locks either line's notes.
Moved breaks are stored in edits.json keyed by the line each comes before,
follow their lines when the lyrics change, and win over the computed break
on every rebuild, clamped so a break never lands inside a line's notes.
The review lists each break with that line and whether it was moved, and
PUT /api/jobs/{id}/breaks takes {line: seconds | null}. merge now passes
untouched notes through as they are rather than rebuilding them, which
added float noise to their durations.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary generated by LLM agent
Stacked on #31, which is stacked on #30. Merge those first.
The sheet listed its controls in one run-on line under it ("Drag a note to move it… · ←→ move · ↑↓ pitch (Shift: octave) · …"). That's hard to scan, and it grows with every feature. This replaces it with a grouped shortcut panel, like an editor's reference card.
The panel
?opens it; Esc closes it (a second Esc clears the selection, as before). It's inline rather than a pop-up, so it can stay open while you edit.localStorageacross lines, songs and visits. While it's closed, a one-line hint, "Press ? for keyboard shortcuts.", takes the old help line's place.Code
The list is data:
SHORTCUTSinsheet.js, where "+" joins keys pressed together and " / " separates alternatives. A new Node test reads the sheet's key handler and checks that every letter key the list names (S, M, G, F, R, P, Z, Y) is one it actually handles, so the list can't fall out of date. Another test covers how combos are split into key caps and mouse words.Testing
?reopens it, it's still open after a reload, all seven groups render, and there were no script errors.test_version(needs the package installed).The beat grid came from the detected tempo (or the BPM override) and a GAP pinned to the first note, so it did not always line up with the music, and snapping did not land on its beats. A GRID bar on the sheet now sets both, redrawing the grid live as values are tried, with x2 and /2 for a tempo detected at double or half speed and ALIGN TO NOTE to move GAP the least that puts the selected note on a beat. Note editing pauses while a trial grid is unapplied, so notes never snap to a grid that is then cancelled. The grid is stored with the song's edits, so every rebuild keeps it, and grid charts with it in place of its own choice; it records that choice in tempo.json for RESET to return to. A GAP after the first note moves back by whole beats, as notes on negative beats are rejected by players. PUT and DELETE /api/jobs/{id}/grid set and reset it, rewriting the charts in place.Line breaks were always placed by songtrace, midway between lines and recomputed around edited ones, so a break could not be put where a singer breathes. The sheet now draws the breaks either side of a line as markers that drag along the beat grid, kept between the lines they separate, and double-click back to automatic. Break moves share the line's undo history and SAVE LINE, which saves the notes only if they changed, so moving a break never locks either line's notes. Moved breaks are stored in edits.json keyed by the line each comes before, follow their lines when the lyrics change, and win over the computed break on every rebuild, clamped so a break never lands inside a line's notes. The review lists each break with that line and whether it was moved, and PUT /api/jobs/{id}/breaks takes {line: seconds | null}. merge now passes untouched notes through as they are rather than rebuilding them, which added float noise to their durations.