feat: Atlas interaction overhaul -- unified click model, flip retirement, keyboard nav (goals 0102/0104/0106 slice A) - #227
Merged
Conversation
…ent, keyboard nav (goal 0102/0104/0106 slice A) Retires the flip gesture across every node type (note card, region frame, region chip): plain click now selects/replaces per the researched gesture table, a click on the already-selected node commits (leaf opens its page, a place zooms), shift-click toggles, cmd-click commits instantly, and a real double-click reproduces the same outcome via two plain clicks -- no dedicated dblclick handler needed. The retired back face's typed-link slot-drag relocates onto the note card's own hover-revealed right-edge link handle, dragging the board's default (first-declared) link kind; kind changed afterward via the existing edge menu. Keyboard: arrows nudge the selection (1px, 10px with Shift) and persist via a keyup-batched SetPosition/SetNotePosition, or pan the camera with no selection; Tab/Shift+Tab walk top-level nodes in reading order; Option+Arrow jumps to the nearest node in a direction (atlasKeyboardNavGeometry.ts, unit-tested); Enter commits the sole selected card. Escape's ladder (clear-selection, then go up one level) now shares atlasFocusContainment.ts's own guard with the keyboard-nav hook, so it never fires on top of a portaled Dialog or an editable field that's already consuming the same keypress -- regression: an Escape meant to close the Lens dialog was also drilling the board up a level. Fixes a real click-model bug surfaced by the new "already-selected commits" rule: releasing a slot-drag back onto its own origin card synthesizes a native click on their common DOM ancestor (the card itself), which would otherwise reopen its page uninvited -- a one-shot capture-phase listener swallows exactly the click that follows a drag's release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
… contract item 6) Every flip-dependent helper/assertion across the atlas e2e suite migrates to the click-select-then-commit model: fixtures/atlasBoard. ts's openViaFlip becomes openCard (plays both clicks, retrying past the same micro-drag race the old flip click already guarded against) plus a new closeCard helper that waits for Primer's own Dialog backdrop to fully clear, not just its content -- a tight open/close/ interact cycle can outrun that animation otherwise. Every `aria-label="Flip …"` locator becomes `"Open …"`; every data-flipped/atlas-*-card-back assertion is replaced with a selection-wrapper check or a page-content check, since that content now lives on the card's own page exclusively. Reaching a place's own page (previously a body-click flip then Open) is now a ⌘-click on the frame body, the pointer twin of ⌘↵. New coverage per the gesture/key tables: click-replaces-selection, click-an-already-selected-node-commits, a real double-click producing the identical outcome, ⌘-click's instant commit, and click-empty deselect (atlas-gestures.spec.ts); the hover link-handle's slot-drag relocation (atlas-slots.spec.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
"Add 1 cards" times out only under 4-worker parallel local load (own dedicated server, so not a cross-spec data race) -- passes clean in isolation and on retry every time observed, same signature layout.spec.ts:163 already carries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
…cape/Enter/Arrow handling React Flow ships its own per-node keyboard accessibility (Escape unselects the FOCUSED node, Enter/Space toggles it, arrow keys nudge it) as a second, uncoordinated keyboard system layered on the exact same DOM nodes this board's own hooks already own. Reproduced live: selecting a card then pressing Escape once both cleared the selection AND climbed a level in the same press, because React Flow's own unselect ran first (attached directly to the focused node) and had already cleared the selection by the time this board's own Escape ladder read it -- true whether that read came from the selectedCards state mirror or a live DOM query, since React Flow's own handling had already mutated both. `disableKeyboardA11y` on the board's `<ReactFlow>` makes this board's own hooks (useAtlasKeyboardNav, useAtlasSelectionTray's Escape ladder, each card's own onKeyDown) the sole keyboard authority, matching the goal 0102/0104 gesture and key tables exactly rather than a blend of two systems. Also swaps useAtlasKeyboardNav's own Tab/Enter/Arrow reads and the Escape ladder's own clear-selection check from the selectedIDsRef/ selectedCards state mirror to a direct DOM read (atlasFocusContainment.ts's new readSelectedNodeIDs) -- that mirror updates off React Flow's own onSelectionChange callback, a render behind the `.selected` class itself, a gap a keydown handler firing right after the click that just selected something can still hit even with the accessibility layer disabled. New coverage: atlas-gestures.spec.ts's Escape ladder test pins the two-rung sequence (clear, then up-a-level) that the race broke. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
… focus reset beat the inner-element box-shadow
Live-reproduced, screenshot-confirmed: a plain click on a single card
selected it with zero visible change. Primer's global focus reset,
[role="button"]:focus:not(:focus-visible):not(.focus-visible) {
box-shadow: none } (specificity 0,4,0), beats every ring rule of the
shape :global(.react-flow__node.selected) .card (0,3,0) whenever the
clicked node's inner div is :focus without :focus-visible -- exactly
the state every mouse click leaves it in. The ring only ever survived
on non-focused members of a multi-selection and under keyboard
:focus-visible, which is why the prior computed-style e2e assertions
passed while the live gesture was broken. role="button"/tabIndex
pre-existed on main, so the collision itself is pre-existing, but the
new click model makes plain-click-select the PRIMARY gesture, so it
cannot ship this way.
Moves the ring from the inner element to the React Flow node WRAPPER
for all four node types, keyed off the type-scoped class React Flow
already stamps per nodeTypes key (.react-flow__node-<type>.selected)
-- the wrapper carries no role, so Primer's reset structurally cannot
match it, rather than out-specificity-ing a reset that could always
be bumped again. The matching border-radius moves with it so the ring
follows each shape. The sticky's border-color flip stays on the inner
element -- Primer's reset only touches box-shadow, so that half
already survived.
New e2e coverage pins the exact broken state: a plain click on a
single card, then the wrapper's own computed box-shadow while that
card still holds DOM focus (confirmed failing against the pre-fix
selector, passing after). Existing multi-select ring assertions
(atlas-select-group.spec.ts) now measure the wrapper instead of the
inner card, for the same reason.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
alicoding
enabled auto-merge (squash)
August 18, 2026 05:43
… fixture The spec landed on main (PR #225) after this branch's e2e migration pass, so the rebase brought it in still importing openViaFlip and matching the old 'Flip <title>' aria-label; module-load failure took down all four CI shards. Now uses the shared noteCard/openCard fixtures (the local helper duplicated what atlasBoard.ts already exports). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
alicoding
added a commit
that referenced
this pull request
Aug 18, 2026
…lip, ring reads the node wrapper PR #227 retired the flip and moved every selection ring to the React Flow node wrapper; the smoke registry's note-card check now drives click-select then click-commit (page opens, Escape ladder restores an unselected board), and readRing/readStickyStyle measure box-shadow on the wrapper where the ring actually lives. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd
alicoding
added a commit
that referenced
this pull request
Aug 18, 2026
…lip, ring reads the node wrapper (#228) PR #227 retired the flip and moved every selection ring to the React Flow node wrapper; the smoke registry's note-card check now drives click-select then click-commit (page opens, Escape ladder restores an unselected board), and readRing/readStickyStyle measure box-shadow on the wrapper where the ring actually lives. Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
One click model, uniform across every node type (card, sticky, frame body, region chip):
Three bugs found live and pinned by tests along the way:
Full e2e migration off the flip gesture rides in the same PR; QUARANTINE.md gains atlas-folder-import's contention-only entry. Vitest 546/546, atlas e2e sweep green, all hooks green. Screenshot-reviewed against the design contract (ring visible on click, commit opens page, Escape returns).
Goals: docs/goals/0102-selection-click-model.md, 0104-atlas-keyboard-navigation.md, 0106-frictionless-card.md (slice A).
🤖 Generated with Claude Code
https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd