Skip to content

Pin a section, and pair sections with the loop - #624

Merged
thcp merged 2 commits into
mainfrom
feat/section-lock-and-loop
Sep 13, 2026
Merged

thcp merged 2 commits into
mainfrom
feat/section-lock-and-loop

Conversation

@thcp

@thcp thcp commented Sep 13, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #621

Closes #622

Closes #623

Three changes to the sections ribbon, two asked for and one found on the way.

Pin a section against editing

A padlock sits beside the delete cross. While it is set, drag, resize and rename all refuse, and the padlock turns red so a locked section is obvious at rest rather than only once a drag fails.

Looping over a locked section still works. That reads the section without changing it, and it is the gesture most likely to be wanted on one pinned precisely because it matters. Delete stays available: the cross is an explicit press on one named target, not something a pointer does on the way past.

The flag had to be declared on SectionItem to exist at all. That model takes Pydantic's default extra="ignore", so an undeclared locked was dropped on save and answered 200. Locking would have looked right until the next load, with nothing in the response, the logs or metadata.json saying otherwise. Two backend tests cover the round trip and both fail without the field.

A loop becomes a section, and a section becomes a loop

Add now builds the section on the armed loop instead of hunting for the first gap. With no loop armed the old gap search is unchanged.

Clicking a section arms the loop over it. A press that never moved was already doing nothing, so the gesture was free. Double-click stays rename.

A loop drawn across an existing section is refused, and says so beside the button that was pressed. Trimming it to fit or displacing the neighbours both hand back a different region than the one selected, and displacing could move a section just locked.

Renaming was broken on main

Double-clicking a label did nothing, for every section on every track.

The drag calls setPointerCapture, and capture retargets the rest of the gesture to the capturing element. So the dblclick ending a real double-click was delivered to the block while the listener sat on the child label, and never ran. Dispatching the events by hand works perfectly, which is why a console check would have cleared it. The listener moves to the block.

Confirmed pre-existing by checking out main's sections.js and watching the same test fail.

A note on the module boundary

sections.js deliberately does not import transport.js. The transport reaches the DOM at import time through state.js, and pulling it in broke this module's Node unit test outright. main.js owns both and hands the two functions over instead, the same reason loopRegion.js is its own module.

Testing

Seventeen browser tests, across lock, loop and rename. Lock and rename had none at all before this.

Each was confirmed to fail without the change it covers, including the three that exist to stop a passing test proving nothing:

  • an unlocked section still drags, without which the locked case is vacuous
  • a drag does not also arm a loop, since a drag ends in a pointerup too
  • the rename input is still there a moment later, since a stolen focus commits and re-renders, and the field vanishes as fast as it appeared

Full suite: 170 passed, 0 failed. uv.lock untouched, i18n clean at 524 keys across 10 locales.

Addresses discussion #573 in full, and the loop pairing in #474.

Thales added 2 commits September 13, 2026 10:39
A padlock beside the delete cross locks a section's position: drag and
resize both refuse while it is set.

The flag had to be declared on SectionItem to exist at all. That model
takes Pydantic's default extra="ignore", so an undeclared `locked` sent
by the editor was dropped on save and answered 200: locking would have
looked right until the next load, with nothing in the response, the logs
or metadata.json saying otherwise. Both new tests fail without the field.

A locked block shows its padlock without being hovered. Hiding it would
leave locked and unlocked identical at rest, and the only way to find a
locked one would be to try to drag it.

Addresses part of discussion #573.
Add now builds the section on the armed loop instead of hunting for the
first gap. That was the complaint: you select a region, press Add, and a
section appears at the start of the track with nothing saying why. With
no loop armed the old gap search is unchanged.

Clicking a section arms the loop over it. A press that never moved was
already doing nothing, so the gesture was free. It is tracked apart from
the drag so a locked section can still be looped: that reads the section
without changing it.

Sections cannot overlap, so a loop drawn across one is refused and says
so beside the button that was pressed, rather than being trimmed to fit
or displacing a neighbour. Either would hand back a different region than
the one selected, and displacing could move a section just locked.

Renaming was already broken on main, and this is where it surfaced. The
drag calls setPointerCapture, which retargets the rest of the gesture to
the capturing element, so the dblclick ending a real double-click was
delivered to the block while the listener sat on the child label and
never ran. Synthetic events do not capture, which is why dispatching the
sequence by hand worked perfectly and every actual double-click did not.
The listener moves to the block. Rename had no browser test at all.

Lock now covers editing rather than position alone: drag, resize and
rename all refuse, and the padlock turns red, using the same --danger the
rest of the app uses to say no. Delete stays available, because the cross
is an explicit press on one named target.

sections.js deliberately does not import transport.js. transport reaches
the DOM at import time through state.js, and pulling it in broke this
module's Node test outright. main.js owns both and hands the two
functions over instead, the same reason loopRegion.js is separate.

Seventeen browser tests, where there were none for lock or rename. Each
was confirmed to fail without the change it covers, including the pair
that would otherwise let a drag retarget the loop on every move, and the
one asserting an unlocked section still drags, without which the locked
case proves nothing.

Addresses discussion #573, and #474 asked for the loop pairing too.
@thcp
thcp merged commit e59eda1 into main Sep 13, 2026
10 checks passed
@thcp
thcp deleted the feat/section-lock-and-loop branch September 13, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant