You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Blocked on #1004 — the tree operations have to be uniform at every depth before dragging on the graph can address a step at any depth.
The graph is the thing a rider looks at while building a workout, and it is read-only. Every change goes: click a row in the list, move to the inspector, type a number, look back at the graph to see what happened. That round trip is the "heavy" in a clunky editor — nothing is shaped where you are looking.
░░░██████░░░██████░░░ ← drag a block's TOP edge · target % FTP
│ │↔│ │ ← drag a block's SIDE · duration
└─────┴──────┴──────┘ ← drag the BLOCK · reorder
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ FTP
Top edge sets the target. On a ramp, each end drags separately — that is what a ramp is, and a form has never expressed it well.
Side edge sets duration, pushing the following blocks along rather than overwriting them.
The block body reorders, matching the list's drag so the two surfaces agree.
While dragging, the value shows on the block itself (100% FTP · 265 W, 3:00) so the number is where the hand is.
The rules it must not break
Never the only way. Every drag has a typed equivalent in the inspector, and the inspector updates live during a drag, so a rider can shape roughly and then type exactly. ux.md: nothing lives only in one affordance.
Keyboard keeps working. Blocks are already focusable with role="button" and Enter/Space (IntervalGraph.svelte:108-114). Arrow keys adjust the focused block — the same operations, without a mouse — and the list remains a complete keyboard path on its own.
Snapping: 5 s and 1 % FTP, with Alt for a free drag. Fine enough to be precise, coarse enough that a workout does not end up at 87.3 %.
Bounds come from validate.ts and docs/SPEC.md, never invented in a drag handler — a drag cannot produce a workout the validator would reject.
Validation stays inline and constant. A rider must never press Save to find out (the editor already gets this right).
Touch: this is a desk surface, not a mid-ride one, so a pointer-events implementation that happens to work on a tablet is enough. Do not add a mobile drag mode.
Structure
IntervalGraph is shared with the ride screens and the workouts list, where it must stay read-only. Editing goes behind a prop (editable / onEdit), off by default, and the drag logic lives in its own module rather than swelling a component three surfaces render. Per code-quality.md, split in the same change.
Acceptance criteria
Dragging a block's top edge changes its target; a ramp's two ends drag independently.
Dragging a side edge changes duration and shifts the rest rather than overwriting.
Dragging a block body reorders it, consistently with the list.
The dragged value is displayed on the block during the drag.
The inspector's fields update live while dragging, and typing in them still works.
Snapping is 5 s / 1 % FTP; Alt drags free.
Arrow keys adjust a focused block; the list remains a complete keyboard path.
No drag can produce a workout validateWorkout() rejects.
One drag is one undo entry.
IntervalGraph renders exactly as it does today wherever editable is not passed — verified on the ride screen and the workouts list.
Vitest: pixel→value mapping, snapping, bounds clamping, and the read-only default.
make ci green.
Verify it for real
The verify skill: shape a whole workout using only the graph, then open the same workout in the ride screen and confirm the graph there is unchanged in behaviour and appearance. A regression in the read-only rendering is the main risk in this change.
Deliberately not in scope
Drawing a workout freehand, importing a shape from a ride, curve smoothing, and any mid-ride editing — the ride screen's graph stays read-only.
Related
#1004 (blocking), #1003, #1005, #248 (zone-legible graphs — the rendering this builds on), #255, ux.md, code-quality.md.
Blocked on #1004 — the tree operations have to be uniform at every depth before dragging on the graph can address a step at any depth.
The graph is the thing a rider looks at while building a workout, and it is read-only. Every change goes: click a row in the list, move to the inspector, type a number, look back at the graph to see what happened. That round trip is the "heavy" in a clunky editor — nothing is shaped where you are looking.
Mockups: https://claude.ai/code/artifact/6c6f89dc-e3e6-4370-a690-7d792c7ee667 — the graph at the top left.
What becomes draggable
While dragging, the value shows on the block itself (
100% FTP · 265 W,3:00) so the number is where the hand is.The rules it must not break
ux.md: nothing lives only in one affordance.role="button"and Enter/Space (IntervalGraph.svelte:108-114). Arrow keys adjust the focused block — the same operations, without a mouse — and the list remains a complete keyboard path on its own.Altfor a free drag. Fine enough to be precise, coarse enough that a workout does not end up at 87.3 %.validate.tsanddocs/SPEC.md, never invented in a drag handler — a drag cannot produce a workout the validator would reject.workout, so a drag is one entry, coalesced across the drag and pushed on release, not forty entries during it.Structure
IntervalGraphis shared with the ride screens and the workouts list, where it must stay read-only. Editing goes behind a prop (editable/onEdit), off by default, and the drag logic lives in its own module rather than swelling a component three surfaces render. Percode-quality.md, split in the same change.Acceptance criteria
Altdrags free.validateWorkout()rejects.IntervalGraphrenders exactly as it does today wherevereditableis not passed — verified on the ride screen and the workouts list.make cigreen.Verify it for real
The
verifyskill: shape a whole workout using only the graph, then open the same workout in the ride screen and confirm the graph there is unchanged in behaviour and appearance. A regression in the read-only rendering is the main risk in this change.Deliberately not in scope
Drawing a workout freehand, importing a shape from a ride, curve smoothing, and any mid-ride editing — the ride screen's graph stays read-only.
Related
#1004 (blocking), #1003, #1005, #248 (zone-legible graphs — the rendering this builds on), #255,
ux.md,code-quality.md.