fix: make a "+" mean an offer, and a direction change mean direction (#549) - #552
Conversation
…rappe#549) A flowchart node marked every side with a "+" whether or not that side already carried a connector, so a finished connection was framed by one mark at each end and read as unfinished. A decision crowded both of its branch previews onto the bottom edge, ~40px apart, where the "Yes" pill covered the "No" handle and then the branch it had just created. And a mind-map node hid its handles entirely while it was being named, so reaching for the "+" with the caret still blinking found nothing. Handles now describe what is actually available: - A side that carries a connector offers no "+". The side comes from chooseSides, the same function the renderer routes with, so it matches the drawn connector and re-derives itself after a drag instead of trusting an anchor written at creation time. - A decision gives each untaken branch a side of its own — Yes down, No right — and an extended branch offers nothing. A side is also skipped when the branch PILL, not just the mark, would land on another node, and a blocked side moves the branch along rather than costing it its handle. - Naming a node no longer suppresses its handles; it only stops them appearing on selection alone, which is what frappe#510 was protecting. - The handle under the pointer darkens and grows, so a hit radius far wider than the mark still says which direction a press would take. Changing the flow direction moved the whole chart. positionByLevels lays every chart out from PAD, so a re-flow teleported the diagram to the top-left of the canvas — over whatever else was there — and each further toggle moved it again. It also re-read crossCenter inside its own loop, off nodes that same pass had already moved, so the layout was not even a pure function of its input and a second run landed pixels off the first. The centre is now measured once and the result anchored back onto the chart's own bounding-box centre: L→R→T→B→L→R cycles between two states and stays put. Tidy up and Number the steps are gone from flowcharts, along with the numbering plumbing behind them (autoNumberFlow, isFlowNumbered, the stepPrefix tags). Both asked the user to re-arrange a chart they had already arranged by hand. The mind map keeps its Tidy up. The legacy FlowchartLayer, which renders only for pre-unification single-type documents, is left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Letting hover reveal a mind-map node's handles while it is being named (the previous commit) went one step too far: clicking a "+" leaves the pointer parked exactly where the handle was, and the child is born under it, so the new node offered a "+" of its own before it even had a name. That is the complaint frappe#510 fixed, and e2e caught it. Opening an editor now drops the hover. Since hover only re-arms on a pointermove, a node born under a stationary pointer offers nothing, while reaching back toward it still produces a handle — because that is the user actually asking. Both frappe#549 item 1 and frappe#510 hold, and neither depends on where the pointer happened to be when the editor opened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vibhavkatre
left a comment
There was a problem hiding this comment.
Reviewed against #549, all seven items.
Correctness
Occupancy is derived, not stored. occupancy() re-runs chooseSides — the same function useFlowchartRoutes routes with, and with the same three guards (role, both endpoints known, no self-loop) — so a "+" disappears from exactly the side the drawn connector uses, and it follows the connector after a drag. This is the right call: an anchor written at creation time would have gone stale.
positionByLevels reading crossCenter inside its own loop was a real defect, not just drift: the layout was not a pure function of its input. Hoisting the measurement out and anchoring the result on the chart's own bounding-box centre fixes both the teleport and the idempotency, and the toggle-cycle test pins it.
buildContext gained a parameter, and the mind-map's same-named buildContext is a different function that already defaulted connectors. No caller is left passing the wrong arity.
Notes, none blocking
- A decision with more open branches than free sides silently drops the surplus —
branches.slice(0, sides.length). A three-branch decision hemmed in on two sides offers two handles and the third outcome has no affordance. Rare, and the fallback inofferableSideskeeps it from ever showing nothing. - A chart numbered before this keeps "1. " baked into its labels, and the toggle that removed them is gone. The stored
flowchart.stepPrefixis now orphaned. Removal is what #549 item 7 asked for, so this is a note for the record rather than a change request — those labels are editable by hand. - The two new assertions in
mindmapDragLayer.test.jsmatch source text. That file already works this way, so it is consistent, but a harmless refactor (watch(editingNodeId, () => clearHover())) breaks them with no behaviour change.
All three checks green. Merging.
Closes #549.
What was wrong
A
+did not mean "you can add something here." Every flowchart node marked all four sides whether or not a side already carried a connector, so one finished connection was framed by a mark at each end and read as an unfinished target. A decision crowded both branch previews onto the bottom edge about 40px apart, where theYespill covered theNohandle — and, onceYesexisted, covered the branch it had just created. A mind-map node hid its handles outright while it was being named, so reaching for the+with the caret still blinking found nothing there.A direction change moved the diagram instead of changing its direction.
positionByLevelslays every chart out fromPAD, so a re-flow teleported the chart to the top-left of the canvas — on top of whatever else was there — and each further toggle moved it again. It also re-readcrossCenterinside its own loop, off nodes that same pass had already moved, so the layout was not a pure function of its input: running it twice landed a few pixels off.Measured on the same two-node chart, before and after:
What changed
Handles describe available actions (#549 items 1–4)
chooseSides— the same function the renderer routes with — so an occupied side is exactly the side the drawn connector uses, and it re-derives itself after a drag rather than trusting an anchor written at creation time.Direction change stays put (item 6) — the cross centre is measured once, and the re-flow is anchored back onto the chart's own bounding-box centre. The toggle is idempotent and cycles between two stable states.
Tidy up and Number the steps removed from flowcharts (items 5, 7) — including the numbering plumbing behind them (
autoNumberFlow,isFlowNumbered,seedStepNumbers, thestepPrefixtags). The mind map keeps its Tidy up;stripStepNumberstays, since the outline conversions still use it.Deliberately out of scope
FlowchartLayer.vuestill draws its own+per node. It renders only for pre-unification single-type documents, and every case in the issue is the unified canvas. Happy to extend the same rules there if wanted.Testing
yarn test— 1692 passing. New coverage for side/branch occupancy, preview-footprint avoidance, layout anchoring, idempotency, and the full toggle cycle. Verified by hand in the running app: handles reveal on hover while a node is being edited and stay quiet when the pointer leaves; a connected pair shows no marks at either end of the connection; the toggle cycles between two positions without drift.🤖 Generated with Claude Code