Skip to content

v0.27.0 — move a connection's end without drawing it again - #352

Merged
MerciHanrim merged 8 commits into
mainfrom
feat/move-connection-end
Oct 13, 2026
Merged

MerciHanrim merged 8 commits into
mainfrom
feat/move-connection-end

Conversation

@MerciHanrim

@MerciHanrim MerciHanrim commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

v0.27.0 — move a connection's end without drawing it again

The first line-editing unit (docs/edge-routing.md §ER18): an existing connection's source or target end moves to another side of its node or to another node, from the canvas or from the Inspector, through one shared check and one shared change. Eight commits, each verified and pushed as it was done.

1. One check, one change (822eb18)

src/model/endMove.ts (endMoveVerdict, movedEdge) and the store's moveEdgeEnds, used by every surface.

  • A move keeps the connection: id, kind, direction, handles, amount or condition, resource type, colour, line shape and the user's bends. Only the end node and its side (data.ports, canonical) change.
  • Refused, with the reason shown: an end on a node without the port this connection needs there (a Source has no input, a Drain or End no output, a Parameter or Register no ports); and, only when an end changes node, a result that repeats another connection by the drawing rule (the same two ends and stored handles), the moved connection excluded. A side change alone is never refused as a duplicate: the examples hold parallel connections of one kind between the same nodes (Early MMO a_z2_*_lo / _hi, the choreography's l_add / l_sub), and resource + state pairs. Policy closed by Lumi.
  • Drawing runs the same duplicate check before it records, so a refused drawing no longer adds an undo step or resets the run (it did before).
  • One undo step per applied move; a cancel, a refusal or the same ends record nothing. A side-only move keeps the run (ports counts as decoration on undo and redo too); a move to another node resets it.
  • User bends are never removed by a move; one the bend rule would refuse at the new ends is said before applying.

2. The Inspector's Ends group (822eb18, folded in 55ba9c6)

Folded by default to its name and a From → To summary (the design review: always open, it pushed Colour out of view). Opened: a node list and a four-side choice per end, then Apply (one change) or Revert. A node that cannot take the end is listed disabled with its reason in its own text, so a keyboard or screen-reader user reads it; the status line (reason, bend warning, result, and an end with no grip on the canvas) stays visible while folded. Not on the phone; disabled under the edit lock.

3. The canvas end grips (55ba9c6, 8ee7aec, 1f7efd4)

  • On the ONE selected connection only: never on hover, not with anything else selected (Ctrl+click of a second connection hides them), not on the phone, not under the lock. Each grip is a named button.
  • Drag a grip onto a node's face by mouse, pen or touch: React Flow's connection engine (XYHandle), started from a mouse press or a touch start. React Flow's own reconnect anchors listen to the mouse only (spike, share note 43), so they are not used, and drawing a new connection is untouched. A refused face changes nothing and says why (live region and Ends status line); a drop elsewhere, Esc, a deselection, a deletion or the lock during the drag change nothing and record nothing.
  • Target size (8ee7aec): 24 × 24 screen px at every zoom with a 10 px dot, 44 × 44 while any pointer is coarse; the side buttons at least as large. @xyflow/system 0.0.81 (the version @xyflow/react 12.11.5 pins) is now a direct dependency, since the grips import XYHandle.
  • Placement (1f7efd4, after the design review of note 46): a grip sits on its connection's own line at the nearest FREE spot near its end — the first spot past the port, then 8, 16 and 24 screen px further in (the dot's centre at most 38 px from the port with a mouse, 48 px with touch), never past the middle of the line. Free: nothing else a press could reach under the whole target (another connection's line, a port, a node, the other grip; elementsFromPoint on a 5 × 5 grid) and no label or outside name covered. No free spot: no grip — a small mark, not focusable and not a target — and the Ends group says, folded or open, to move that end from there. Measured on the cases checked, not a guarantee for every layout.
  • The side choice (1f7efd4): click, tap, Enter or Space on a grip opens a small popover naming the end and its node, the four sides in a cross, the current one marked; it opens away from the node it edits, inside the canvas, never taller or wider than the canvas allows (it scrolls rather than cut a side off); a choice moves the end, Esc or a press elsewhere closes it unchanged; focus returns to the grip. No zoom is forced for touch.
  • Reviewed screens: share notes 46, 47 (the small fixture with a mouse; the Coffee fan with touch at zoom 0.5 — no grip at either end — and 1). Lumi approved the placement and the popover; the touch feel on a real device was not part of it.

4. Release (cea6ca5, 8c639b7, e0aaeaf)

  • cea6ca5 regenerates the two state-ui Inspector snapshots (light, dark) with the folded group — the only baselines changed, approved after review (share notes 42, 45).
  • 8c639b7: 0.27.0, .changes/move-connection-end.json (user-facing), three What's new lines in 18 languages, CHANGELOG v0.27.0, README Latest. e0aaeaf (text only) brings the side-choice line, README and CHANGELOG in line with 1f7efd4: a small panel naming the node, and Ends in the Inspector where the canvas has no room for a grip.
  • 29 strings in 18 languages over the unit (26 interface, 3 release note). es-ES says móvil where es-419 says teléfono; pt-BR and pt-PT write the release lines identically and neutrally, so pt-PT's region delta stays 280. The summary's arrow is a graph-relation arrow, kept unmirrored in Arabic (scripts/arrow-units.json); the node names entering ICU arguments are declared (scripts/isolate-obligations.json, 13 rows). The translation contract tests passing is evidence of the catalogs' structure, not of how natural the lines read; no native-speaker review was done.

Verification (final head e0aaeaf)

  • Full local e2e, --workers=1 --retries=0: 2 183 passed, 0 failed, 10 skipped (53.2 min).
  • dist and pwa configs, run separately after the full run: dist 16 passed, pwa 19 passed.
  • PR CI: 10 / 10 (checks, six e2e shards, production bundle, PWA).
  • Unit: 3 555 passed (vitest, full), including graphStore.endMove.test.ts (17). tsc, lint (0 errors), check:change-declaration, check:i18n, check:i18n-surface, check:arrow-direction, check:isolate-arguments, check:icu-arguments, check:mobile-query, the direction and glyph checks, check:e2e-shards; build and check:third-party-notices on the dependency change.
  • e2e connection-ends.spec.ts: 27 (the Ends group, the fold, the grips by mouse, touch and keyboard, the free-spot placement and the no-grip end, the popover, every cancellation, the target sizes, multi-selection, the phone and the lock).

Earlier runs, kept as they were: the full local e2e on 8c639b7 (2 182 passed, 0 failed, 10 skipped) with dist 16 and pwa 19 there, and CI 10 / 10 on 6463fde; neither covers 1f7efd4 or e0aaeaf. Related e2e per step: 974 → 965 passed (two fixed, two pending baselines); 992 → 985 passed, 5 skipped, the two pending baselines (the 55ba9c6 message says "984 tests" for that run; it was 992).

Notes

  • 6463fde changes TEXT only (CHANGELOG.md, docs/edge-routing.md §ER18, two e2e test TITLES): the grips' clearance of ports is stated as a measurement, not a guarantee. The 8ee7aec subject ("clear of every port") overstates it and is not rewritten.
  • One process slip recorded: the cea6ca5 commit message file was written with printf, not the editor tool; its text is intact and the history is not rewritten.
  • The final code review and the merge approval are separate.

Records and open items

Share notes 40–47 (the proposal and decisions, two defects, the Inspector baselines, the reconnect spike, the grips, the fold, the target sizes, the free-spot placement and the popover). Recorded, not fixed here (note 41): the Inspector's Type switch drops the colour and the sides and leaves out / in on a new state connection; a Parameter or Register offers state handles a connection can be drawn from. Separate later units: turning a connection around, user curves, moving a connection's label.

…e or another node, the connection kept

docs/edge-routing.md §ER18, the first line-editing unit. One check and one
change for every surface (src/model/endMove.ts `endMoveVerdict`, the store's
`moveEdgeEnds`); the Inspector's Ends group is the first surface, the canvas
end grips come next.

- A move keeps the connection: id, kind, direction, handles and every data
  field (amount or condition, resource type, colour, line shape, the user's
  bends). Only the end node and its side (`data.ports`, canonical) change.
- Refused, with the reason shown: an end on a node without the port this
  kind needs there (a Source has no input, a Drain or End no output, a
  Parameter or Register no ports); and, only when a node changes, a result
  that repeats another connection by the drawing rule (same ends and stored
  handles, as xyflow's addEdge refuses), the moved connection excluded. A
  side change alone is never a duplicate: the examples hold parallel
  connections. Drawing runs the same duplicate check before it records, so
  a refused drawing no longer adds an undo entry or resets the run.
- One undo step per applied move; a cancel, a refusal or the same ends
  record nothing. A side-only move keeps the run (`ports` is decoration on
  undo and redo too); a node change resets it.
- The user's bends are never removed; one the bend rule would refuse at
  the new ends is said before applying.
- The Ends group: per end a node list and a four-side choice, then Apply
  (one change) or Revert. A node that cannot take the end is disabled with
  its reason in the option's own text; the reason, the bend warning and the
  result are in a status line the controls are described by. Node names are
  bidi-isolated. Not on the phone (read-only); disabled under the edit lock.
- 21 strings in 18 languages; pt-BR writes three neutral lines as pt-PT
  does, so pt-PT's region delta stays 280. The locale tests' counts and the
  Russian ё list follow. Contract tests only; no native-speaker review.

Tests: src/store/graphStore.endMove.test.ts (17, including the parallel
connections of state-verification and the Early MMO); e2e
connection-ends.spec.ts (6). Related e2e (974): 965 passed; of the four
failures, two are fixed here (an Inspector input selector met the new
radios first: the group now sits below Route; the phone showed radios: the
group is not offered there) and two are the Inspector snapshots in
state-ui.spec.ts (light, dark), which change by this group and are NOT
updated: their before / after awaits review.
…ace, or pick a side; the Ends group folds

docs/edge-routing.md §ER18. The canvas surface for moving a connection's
end, through the same check and change as the Inspector's Ends group.

- Grips on the ONE selected connection only (nothing else selected): never
  on hover, not on the phone, not under the edit lock. Each grip is a named
  button.
- Drag a grip onto a node's face: the end moves (one undo step). A drop on
  a face the check refuses changes nothing and says why (live region and
  the Ends status line); elsewhere, or after Esc, nothing changes and
  nothing is recorded. The drag is React Flow's connection engine
  (XYHandle), started from a mouse press or a touch start: React Flow's own
  reconnect anchors listen to the mouse only (spike, share note 43), so
  they are not used, and drawing a new connection is untouched.
- A drag in progress is cancelled, applying nothing, if the grips go away
  under it (deselected, deleted, the lock turned on); after Esc, moving on
  or releasing does nothing.
- Click, tap, Enter or Space on a grip: the four sides of that end's node as
  buttons around it (past its outside name where the name is on that side),
  the current one marked; a choice moves the end and closes them, Esc or a
  press elsewhere closes them unchanged; focus returns to the grip.
- The Ends group is folded by default to its name and a From → To summary
  (the design review: always open, it pushed Colour out of view). Opened, the
  pick and Apply as before; folding drops a pick not applied; the status
  line stays visible while folded.
- @xyflow/system 0.0.81 (the version @xyflow/react 12.11.5 pins) is now a
  direct dependency, since the grips import XYHandle from it; it already
  shipped and is in the third-party manifest.
- 3 strings in 18 languages (the summary, the two grip names); the summary's
  arrow is a `graph-relation` arrow, kept unmirrored in Arabic as the canvas
  it describes is (scripts/arrow-units.json); the node names entering ICU
  arguments are declared in scripts/isolate-obligations.json (13 rows) and
  counted in scripts/icu-argument-disposition.json. Locale tests' counts and
  identical-to-English lists follow. Contract tests only; no native review.

Tests: e2e connection-ends.spec.ts 25 (the Ends group, the fold, the grips
by mouse, touch and keyboard, the side choice, every cancellation, the phone
and the lock). Related e2e (984 tests): 985 passed, 5 skipped, 2 failed —
the two state-ui Inspector snapshots, which change by this group and are
NOT updated: before / after (opened and folded) await the design review
(share notes 42, 45). Unit 3 555. Build and third-party notices ok.
…4 px for touch, clear of every port

docs/edge-routing.md §ER18 (share note 46). The grips and the side buttons
live in the zoomed flow layer, so at zoom 0.5 a 24 px grip was a 12 px
target. Each is now scaled back by the zoom: a grip is a 24 x 24 screen px
target with a 10 px dot at every zoom, 44 x 44 while any pointer of the
device is coarse (`data-coarse`, from `(any-pointer: coarse)`; no second
media breakpoint, as scripts/check-mobile-query.mjs requires); the side
buttons are at least as large and placed by their own size.

Centred on its port, a grip covered that port (so a new connection could
not be drawn from it while the connection was selected, e.g. the Coffee
roastery's fan) and, at zoom 0.5 or with a touch target, the node's other
ports. A grip now sits half its target plus 2 px out along its end's stub,
as React Flow's own reconnect anchors are offset: no port meets a grip's
target on the small fixture or the Coffee fan at zoom 0.5, 1 and 2, mouse
and touch (before: one to three ports per grip).

Ctrl+click adds a second connection to the selection on this branch and on
main e7efd77 alike (Meta and Shift replace it on both); the multi-select
e2e now holds Control on the keyboard (Playwright's mouse.click has no
modifier option) instead of setting the second selection in the store.

e2e connection-ends.spec.ts: 26 (+ the 24 px / 10 px dot / no-port check
at three zooms and the 44 px / no-port check with touch).
state-ui.spec.ts Inspector snapshots, light and dark, regenerated from the
final code: the Ends group folded to one line (Ends · Feeder → Tank) above
the drawing note, Colour in view again. Reviewed and approved (share notes
42, 45); only these two baselines change. Re-run after writing: 2 passed.
…gain

- package.json / package-lock.json 0.27.0; `.changes/move-connection-end.json`
  (user-facing, `release:0.27.0`).
- What's new 0.27.0, three lines in 18 languages: move an end by dragging
  a grip onto a side of another node, or with Ends in the Inspector, the
  amount, condition, colour, line shape and bends kept; click or tap a grip
  to pick a side of the same node, the phone read-only as before; a move
  that cannot be made is not applied and says why. None claims more than
  that: turning a connection around, user curves and label moving are not
  in it.
- es-ES says `móvil` where es-419 says `teléfono` (one audited key more);
  pt-BR and pt-PT write all three lines identically and regionally neutral
  (`em uma`, `em volta`, `telefone`), so pt-PT's region delta stays 280.
  The locale tests' counts and the Russian ё list (`щёлкните`) follow. The
  e2e release list pins 0.27.0. Contract tests only; no native review.
- CHANGELOG v0.27.0; README Latest — v0.27.0 (v0.26.2 kept as a section,
  v0.24.0 moved into the CHANGELOG list).
…ses checked, not a guarantee

docs/edge-routing.md §ER18, CHANGELOG v0.27.0 and two e2e test titles said
a grip's target is clear of every port. That holds for the cases measured
(the small fixture and the Coffee fan, zoom 0.5 to 2, mouse and touch: no
port met a grip's target), not for every layout: a port of another node
close to a stub, or a very short stub, can still meet it. The wording now
says so; the `8ee7aec` subject ("clear of every port") is not rewritten.
Text only: no product code, no test logic (the two titles say "on this
fixture"); connection-ends.spec.ts 26 passed after the change.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Deploying cozy-loop-studio with  Cloudflare Pages  Cloudflare Pages

Latest commit: e0aaeaf
Status: ✅  Deploy successful!
Preview URL: https://5b81af37.cozy-loop-studio.pages.dev
Branch Preview URL: https://feat-move-connection-end.cozy-loop-studio.pages.dev

View logs

…e side choice is a popover naming its node

docs/edge-routing.md §ER18 (share notes 46, 47). The review of the grips
moved out along the stub found two handling problems on the Coffee fan
(touch, zoom 0.5): the source grip sat on the fan's shared trunk and took
presses meant for the two sibling connections, and the side buttons spread
around the node covered the neighbouring nodes' names and ports.

- A grip sits on its connection's own line at the nearest FREE spot near
  its end: from half its target plus 2 px past the port, then 8, 16 and 24
  screen px further in (24 px, one mouse target: beyond it the dot no longer
  reads as this end's), never past the middle of the line. Free = nothing
  else a press could reach under the whole target (another connection's
  line, a port, a node, the other grip; the browser's elementsFromPoint on
  a 5 x 5 grid) and no text covered (any connection label, its own too, or
  an outside name).
- No free spot: no grip. A small hollow mark (not focusable, not a pointer
  target, not announced) shows the end, and the Ends group says, folded or
  open, that the canvas has no free spot for that end's grip and that the
  end moves from there (2 strings x 18). No large transparent target takes
  presses from the lines around it.
- The side choice is a small popover instead of buttons around the node:
  its title names the end and the node, the four sides in a cross, the
  current one marked; it opens beside the grip on the side away from the
  node it edits, stays inside the canvas, is never taller or wider than the
  canvas allows (it scrolls rather than cut a side off); Esc or a press
  elsewhere closes it, a choice moves the end; focus returns to the grip.
  No zoom is forced for touch.

On the two scenes reviewed: the small fixture (mouse, zoom 1) keeps both
grips, free; on the Coffee fan with touch at zoom 0.5 neither end has a free
spot (marks and the Ends hint), at zoom 1 the target end has one.

Tests: connection-ends.spec.ts 27 (the popover's name, place in the canvas
and off its node, no side cut off; a grip's target with no port, label or
name under it at zoom 0.5 / 1 / 2; an end with no spot: the mark is not
focusable and the hint shows while folded; the 44 px touch tests at zoom
1.5). Locale tests' counts +2. Unit 3 555. Static checks pass.
… and where there is no grip

The What's new line for the side choice (18 languages), the README Latest
section and the CHANGELOG v0.27.0 entry said a side is chosen "around that
node" and that every end has a grip. Since `1f7efd4` the sides are chosen in
a small popover naming the node, and an end with no free spot near it has
no grip: the Ends group in the Inspector moves it. The line now says both.
Neutral wording kept identical in pt-BR and pt-PT; es-ES keeps `móvil`;
Arabic names the popover a small window, the canvas being its `اللوحة`.

Text only. Locale and release-note tests 713, whats-new.spec.ts and
descriptive-copy-wrapping.spec.ts 121 passed; check:i18n, arrow, ICU,
control-byte, glyph and change-declaration checks pass.
@MerciHanrim
MerciHanrim merged commit 9a72f61 into main Oct 13, 2026
11 checks passed
@MerciHanrim
MerciHanrim deleted the feat/move-connection-end branch October 13, 2026 14:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant