Skip to content

feat(web): shape a workout on its graph — drag a block's edges instead of typing in a side panel #1006

Description

@janlauber

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

        ░░░██████░░░██████░░░        ← 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).
  • Undo covers itfeat(web): undo and redo in the workout editor #1005's history wraps workout, so a drag is one entry, coalesced across the drag and pushed on release, not forty entries during it.
  • 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.
  • Works on a repeat's children, not only top-level blocks (this is why feat(web): a step inside a repeat cannot be dragged, duplicated, or added as anything but steady #1004 comes first).
  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

designDesign related stuffenhancementNew feature or requestworkoutsworkout engine & player

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions