Skip to content

selection-reorder: integration#176

Merged
lklyne merged 12 commits into
mainfrom
claude/feat-selection-reorder
May 31, 2026
Merged

selection-reorder: integration#176
lklyne merged 12 commits into
mainfrom
claude/feat-selection-reorder

Conversation

@lklyne
Copy link
Copy Markdown
Owner

@lklyne lklyne commented May 29, 2026

Drag-to-reorder for any evenly-spaced multi-selection. Select 2+ entities that form an equal-gap row, a center dot appears on each, drag a dot to reorder, siblings reflow to keep the row even. No Cmd+G, no managed group required — reorder is now a property of having an aligned selection, not of being a managed group. Nothing persists but the new positions.

Plan: docs/plans/selection-reorder.md. Built AFK as five step PRs into this branch.

Parts

Phase What Step PR
0 Lock the model in docs — ADR 0015 D7 (geometry-is-truth, position-only, ephemeral; FigJam-parity eligibility), reframe D1 (managed group = opt-in persistence); CONTEXT.md "Reorderable row" glossary entry #171
A Pure kernel src/shared/reorder-row.tsdetectReorderableRow (equal-gap gate), dropIndexForCursor, reorderRowPositions (cross-axis preserved) #172
B reorderSelection commit path (main runtime) — batched multi-entity position write, one undo step, no entityOrder/managedLayout/commitAsOneTransaction #173
C Gating + gesture rewire — shared reorderableDots selector (union of selection + managed doors), mode rename reordering-childreordering-row, door-resolving begin-reorder-drag #174
D Live reflow preview (renderer) — siblings shift to make room mid-drag via reorderPreview.ts; insertion line removed #175

Architecture

Two doors, one gesture. The gesture, dots, and reordering-row mode are shared; only eligibility and commit branch:

  • Selection door (new, primary): detectReorderableRow(selectedBoxes) ≠ null → reorderSelection, positions only, ephemeral.
  • Managed group door (M1, unchanged): a managed-row group selected → reorderManagedChild, rewrites entityOrder, persists.

make-auto-layout is demoted from "the way to get reorder" to "crystallize this arrangement into a persistent managed row." Zero new persisted state on the selection path.

Tests (all green except pre-existing environmental pages > takes a screenshot)

  • Unit (pnpm test:unit, 650 pass) — every reorder-row predicate branch + reorderable-dots union + selection-door hit-test cases.
  • Smoke (pnpm test:smoke) — Phase B: permute-to-sequence in one transaction, one-undo restore, entityOrder unchanged, no managedLayout group on disk, cross-axis preserved, unequal-gap no-op. Phase C: full begin/move/commit/cancel-{escape,blur,undo,tab-switch} matrix for the selection door, managed-door regression, unequal-gap "no gesture armed" gate.
  • Typecheck (pnpm typecheck) clean.

Needs human verification (Phase D)

Phase D's live-reflow feel is a visual check the headless loop couldn't do. Before merging, please /verify in-app:

  • dots appear on an equal-gap selection; vanish when spacing is uneven
  • drag reorders with siblings reflowing live to make room
  • one undo restores
  • reload shows only moved positions (no group, no managedLayout on disk)

🤖 Generated with Claude Code

lklyne and others added 12 commits May 29, 2026 09:40
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lock the selection-reorder model in canonical docs ahead of implementation.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce src/shared/reorder-row.ts: detectReorderableRow (equal-gap
eligibility gate + slot definition, FigJam parity), dropIndexForCursor,
and reorderRowPositions (axis repack preserving per-item cross-axis).
Pure, side-effect-free; unit-tested across every predicate branch.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase B of selection reorder (ADR 0015 D7). Adds reorderSelection in the
main runtime: the position-only sibling of reorderManagedChild. Reads the
equal-gap row off current geometry via detectReorderableRow, repacks with
reorderRowPositions, and writes the changed origins through each entity's
per-kind mutator inside one beginBatch/endBatch + markUndoBoundary — one
undo step, no entityOrder write, no managedLayout group, no
commitAsOneTransaction. Nothing persists but the new positions.

Adds a test-only /test/canvas-reorder-selection/commit route, an AppClient
helper, and a smoke test covering the permute + one-undo round-trip,
entityOrder-unchanged, no-group-on-disk, and the unequal-gap no-op.

Mutation-verified by (1) making reorderSelection a no-op and (2) swapping
the written x/y — both fail the smoke assertions; restored to green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ure (#174)

Phase C of selection reorder (ADR 0015 D7). Add one shared `reorderableDots`
selector returning the union of both doors — eligible loose equal-gap selection
(`detectReorderableRow`) plus a selected managed-row group's children — and
consume it from both `hit-test.ts` and `ReorderDotsLayer`, deleting the
copy-pasted predicate.

Rename the interaction mode `reordering-child {groupId, childId, dropIndex}` →
`reordering-row {ids, movingId, dropIndex, axis}` across interaction-types,
the controller, interaction-state, and the broadcast snapshot, so the renderer
draws the insertion line door-agnostically.

`begin-reorder-drag` now carries only `movingId`; `reorder-gesture.ts` resolves
which door armed the gesture (managed-row child → managed door, else selection
door), freezes the row at start, and branches commit: selection →
`reorderSelection` (positions only), managed → `reorderManagedChild`
(unchanged).

Covered by a full begin/move/commit/cancel-{escape,blur,undo,tab-switch} smoke
matrix for the selection door, a managed-door regression, and unit coverage of
the union selector + the unequal-gap "no hit target" gate.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the insertion line with a live reflow: while a reordering-row
drag is in flight, repack the frozen row at the current drop index via
the shared reorderRowPositions kernel and shift each row entity to its
previewed slot, so siblings visibly make room as the dragged dot crosses
boundaries. Pure renderer ephemera — the broadcast layout is untouched;
only the geometry layers (canvas items, selection outlines, dots) see
the override, and release commits to exactly the previewed arrangement.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…older

Upgrade selection-reorder Phase D from the live-reflow preview to dnd-kit /
FigJam-style drag feedback. The dragged item lifts as a 50%-opacity ghost that
floats under the cursor (grab offset preserved); its destination slot paints a
grayscale placeholder that snaps between slots at the neighbour midpoint (50%
crossing). The multi-select bounding box wraps both the placeholder slot and the
lifted ghost, so the selection resizes during the drag.

Renderer ephemera only: the gesture publishes the live canvas-space cursor delta
through useCanvasPointerRouter, and App rebuilds the ghost/placeholder from the
broadcast layout and the shared reorder-row kernel. No new IPC, no doc mutation
until release. Page rows show the gap/placeholder but no floating body, since
page bodies are native WebContentsViews rather than React.

Also respecs Phase D in docs/plans/selection-reorder.md to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lklyne lklyne merged commit 96099ac into main May 31, 2026
1 of 2 checks passed
@lklyne lklyne deleted the claude/feat-selection-reorder branch May 31, 2026 20:00
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