feat: Smart Guides — alignment guides + magnetic snapping for floating groups#1381
Merged
Conversation
… groups (phase 1) Add a SmartGuides module that snaps a dragged floating group's edge to another float's edge (within snapDistance) and renders one alignment guide, torn down on drop. The float drag loop stays owned by Overlay; the component composes the module into the existing per-frame transformFloatingGroupDrag seam so an app-provided transform and the module can both nudge a single drag. Additive: with the module/option absent the loop is byte-for-byte unchanged. - core: SmartGuidesOptions + `smartGuides` option; ISmartGuidesHost / ISmartGuidesService contracts + service slot; getFloatingContainer() host method; onDidEndFloatingGroupDrag drag-end signal; composed drag transform - modules: SmartGuidesService (single-axis edge snap + guide overlay layer), registered in the Modules bundle (auto-removability covered) - tests: jsdom service spec + Playwright real-geometry e2e + fixture handle Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pass `smartGuides: { snapDistance: 8 }` to the demo DockviewReact so floating
groups snap + show alignment guides when dragged near each other.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…argets, hysteresis (phase 2) Extend the float snapping from a single edge to the full candidate set: - centers as well as edges; container edges + center (and optional inset margin lines) alongside other floats, via `snapTargets` - X and Y resolved independently, so a float can snap horizontally to one neighbour and vertically to another; every alignment the snapped box lands on is drawn (e.g. left-edges + tops = two guides) - contested snaps ranked edge-over-center, then source (floats > container), then nearest - asymmetric engage/release hysteresis (`releaseDistance`) so a snap doesn't oscillate at the threshold - guide lines pooled in the overlay layer (style-only writes per frame) Options gain `releaseDistance` + `snapTargets` (SmartGuidesSnapTargets). Unit specs cover centers/container/inset/independent-axes/hysteresis; the e2e adds a two-axis corner-snap (float-only fixture for deterministic geometry). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…se 3) When a dragged float comes flush against another float, suggest docking or merging it and commit on drop: - edge-adjacency (a dragged edge meets a target's opposite edge with >= 50% perpendicular overlap) suggests docking beside the target; overlapping tab strips (tops flush + stacked) suggest a center/tabset merge - a drop-preview rectangle shows where the float will land; on drop the move goes through the existing `moveGroupOrPanel` primitive so events + undo cover it (self-drop guarded) - gated by `snapTogether` (default on) Adds `ISmartGuidesHost.getFloatingGroupSnapshots` (float identity + box) and `mergeFloatInto`, plus the `SmartGuidesSnapPosition` type and the `snapTogether` option. Unit specs cover adjacency/center detection, overlap threshold, pending clears on move-away, gating + commit-on-drop; e2e drops a float onto another and asserts the real merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ events (phase 4) Round out the floating-group snapping: - modifier gate: holding a configurable modifier (default Alt) while dragging suspends snapping + guides and the float follows the pointer freely; releasing re-engages. Plumbed via a new `modifiers` field on the drag context (carried from the pointer event through the overlay loop), so the public `transformFloatingGroupDrag` option sees it too - `snapTargets.splitters` (default off): align to the underlying grid's sash positions, read via a new `getGridSplitterRects` host method - per-float opt-out: `disableSmartGuides` on the floating-group options excludes a window from snapping (the component skips composing the module for it) - runtime control: `DockviewApi.setSmartGuidesEnabled` / `updateSmartGuidesOptions` (+ `smartGuidesEnabled`) and `onDidSnapFloat` / `onDidSnapTogether` events, backed by service-side overrides + emitters Adds the `SnapModifier`, `DragModifiers`, `SmartGuidesSnapEvent` and `SmartGuidesSnapTogetherEvent` types. Unit specs cover the gate (default + configurable + off), splitter snapping + default-off, runtime enable/options, and both events; e2e holds Alt and asserts the float is not pulled to the edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 5 polish) - move guide-line + drop-preview cosmetics (colour, border) out of inline JS into the stylesheet (`.dv-smart-guide` / `.dv-smart-guide-preview` in overlay.scss), themable via `--dv-smart-guides-color` / `--dv-smart-guides-preview-color`; the module now only sets geometry inline - dedup the per-frame guide DOM writes: a frame whose guides are unchanged skips the write entirely (the render path has no interleaved layout reads, so what does change still coalesces into a single reflow at paint). `clear()` invalidates the cache so a re-acquired snap always redraws Popout correctness needs no change: every coordinate is container-relative (from the drag context) and the guide layer lives in the float container, so there is no cross-document geometry to get wrong. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ds, perf Addresses findings from a code review of the branch: - redock long-press aborts a float drag via `cancelPendingDrag` (no end event), which left the guide session + DOM layer leaked and reused stale candidates on the next drag. Add an `onDidStartFloatingGroupDrag` host signal (from the overlay's `onDidStartMoving`) and reset the session on each drag start. - `mergeFloatInto` now bails if the captured target was removed mid-drag (it could be closed async before the drop), instead of moving into a dead group. - tab-strip (center) merge now requires the dragged float's centre to sit over the target, not merely top-alignment + edge overlap — so aligning two overlapping floats no longer reads as a merge (§11). - the dock/merge drop-preview now shows even when `showGuides:false` — a merge that commits on drop must never be silent (showGuides governs alignment lines only). - skip zero-area (hidden/collapsed) sashes so they can't become a phantom splitter candidate at coord 0. - correct the `_buildFloatingDragTransform` doc to be precise about the present-but-inert case, and cache `_opts` by reference so the inert per-frame path allocates nothing. - cleanup: `_gatherFloatingGroupBoxes` delegates to `getFloatingGroupSnapshots` (one geometry source); alias the module's `Rect` to the core `Box`. Regression tests added for each behavioural fix. core+modules 1241 green; e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge, option precedence Address the deferred review findings: - hysteresis now tracks WHICH probe (leading edge / centre / trailing edge) latched onto a line and keeps gluing that one, instead of re-deriving the nearest of all three each frame — so a sub-24px float no longer silently re-snaps its centre onto an edge line and jumps. - snap-together evaluates every candidate target and picks the best (a centre stack outranks an adjacency; within a kind the nearest wins) rather than the first float in iteration order, so clustered floats dock into the closest. - a fresh app-level `smartGuides` option update (a new option reference) now drops earlier `setSmartGuidesEnabled` / `updateSmartGuidesOptions` runtime overrides, so the two no longer fight as permanent competing sources of truth. Left as-is: snap-together targets the floating window's anchor group with its whole-window box — intentional per the spec's "use the outer overlay rect for multi-group floats" guidance, not a defect. Regression tests added for each. core+modules 1244 green; e2e green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s-phase2 # Conflicts: # e2e/fixtures/index.html # packages/dockview-core/src/dockview/dockviewComponent.ts # packages/dockview-core/src/dockview/moduleContracts.ts # packages/dockview-core/src/dockview/modules.ts # packages/dockview-core/src/dockview/options.ts # packages/dockview-core/src/index.ts # packages/dockview-modules/src/index.ts
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Figma-style alignment guides + magnetic snapping for floating groups. While a floating window is dragged, it snaps into alignment with other floats, the container, or the grid behind it, drawing crisp guide lines; bringing one flush over another suggests docking/merging them.
Built additively on the existing per-frame float drag-position hook — with the option unset the float drag loop is byte-for-byte unchanged.
What's included (by phase)
moveGroupOrPanelprimitive (so events + undo cover it).disableSmartGuides), runtime API (setSmartGuidesEnabled/updateSmartGuidesOptions+onDidSnapFloat/onDidSnapTogether).showGuides:false, sticky-probe jumps on tiny floats, nearest-target selection, runtime-vs-option precedence, and cleanups.API surface
DockviewOptions.smartGuides(option types in core);DockviewApi.setSmartGuidesEnabled/updateSmartGuidesOptions/smartGuidesEnabled+onDidSnapFloat/onDidSnapTogether; per-floatdisableSmartGuides. The module lives indockview-modules; core holds only the contracts + the composed drag-transform seam.Tests
toJSON).Notes
format:checkmay flag a pre-existingpaneview.scssdrift onv8-branch— unrelated to this change.🤖 Generated with Claude Code