Files
lanework/DESIGN/04-interactions.md
T
rzen 1d7449e49a Give lane selection its keyboard entry — ⌥↑ escalates; header clicks select
Every lane command keyed off "a selected lane" with no drag-free way to
create one. Settled: ⌥↑ with the lane's first card selected selects the
lane (↓/⌥↓ descend back; ⌥↑ then inert), an empty selection seeds at the
first lane's first card, and a plain click on the lane title bar selects
with the drag surface engaging only on movement.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 20:45:10 -04:00

26 KiB

Interactions

Selection, drag & drop, keyboard, clipboard, search. This is where the old app spent most of its polish budget; nearly everything here is carried over as settled — the details below are the contract, not suggestions.

Selection

  • Cards: click selects; ⌘-click toggles; ⇧-click range-extends; click-drag rubber-bands across lanes. Lanes: ⌘/⇧-click multi-select.
  • Selection is homogeneous: cards XOR lanes.
  • Lane empty-space: single click selects the lane (click again to unselect); double click creates a card at the bottom, title editor focused. The lane header is click-to-select too (settled — a full lane has no empty space left): a plain click on the title bar selects the lane; the drag surface (03-board-ui.md ▸ Lane) engages only on movement — the click-vs-drag split cards already have.
  • Clicking never edits (pivot from the pathfinder's Finder-rename two-stage click): one click selects, and that is all a single click ever does — no slow-second-click rename, no timers, no accidental edit on a hesitant click. Inline rename is Return on a sole selected card, or Board ▸ Rename — the menu item is a lane's only rename path, since Return on a lane creates a card (Grammar below). A fast double-click opens the card window (⌘↩'s pointer twin). Committing an empty rename on an existing item removes its title key (titles are optional; the face shows the untitled placeholder).

Drag & drop

  • Shadow placeholder with live reflow: dragging shows a shadow at the exact landing spot; the board reflows to make room. The whole slot is the trigger zone; proposals are geometry-based so the shadow is stable, never jittery. Width-aware triggers for lanes (no reflow until the cursor reaches where the dragged lane would actually land); the shadow holds until a real new candidate appears. (The old repo's DRAG-REORDER.md documents the full model — port that document alongside the implementation.)
  • Cards reorder within a lane and move between lanes (folder move). Lanes reorder; a full-size replica travels under the cursor.
  • Multi-drag: dragging any member of a multi-selection drags the whole selection; N contiguous shadows; drop inserts contiguously in preserved relative order — defined, for any multi-selection, as lane order first, then card order (a cross-lane selection flattens left-to-right, top-to-bottom).
  • Locality picks the default — the Finder volume model (settled): within a board a drag is a move (rearranging); between boards it is a copy (transferring — the system copy badge shows over the foreign board). ⌥ always forces copy and ⌘ always forces move, Finder's exact modifier grammar; each is a no-op where its behavior is already the default. The badge tracks the effective operation live as the cursor crosses a board boundary.
  • Within-board ⌥-drag copies: originals stay, cursor shows the copy badge, fresh-GUID duplicates land at the drop. Lane drags never copy within their board — a within-board lane duplicate is not available by drag (⌥ is simply ignored there: the drag stays a clean reorder and the badge never shows copy); the duplicate itself is supported, via the clipboard (Lane paste below) — the usual shape: the keyboard path is the canonical one, drag the enhancement (10-accessibility.md).
  • Cross-board copy (the default): cards and lanes (including multi-selections) drag between open boards; fresh-GUID duplicates land at the drop, originals stay, created is kept (a copy is a fork — 01-storage-format.md). Lanes copy cards and all — transferring workflow structure between boards is safe by default. A lane copy strips tombstoned cards: the copy transfers content, and trash isn't content (09-templates.md's instantiation precedent — a board isn't born with trash); the tombstoned originals stay recoverable in the source board. A ⌘-drag move carries them whole — the folder moves as-is, and they land in the destination's trash.
  • Cross-board move (⌘-drag): a real filesystem move, works across volumes — identity travels. A moved folder whose UUID already exists in the destination board arrives as a fresh-UUID copy (01-storage-format.md's import-boundary rule); in a compound move (lane with cards, multi-selection) only the colliding folders are reminted — the rest is a true move (01's per-folder degradation).
  • Files from Finder: dropped on a card → copied into its attachments/ (any type, multi-file; card highlights while hovered). Dropped on lane empty space → creates a card with the file attached, titled with the filename without its extension (multi-file drop: one card per file).
  • A foreign reload mid-drag re-grounds the drag, never corrupts the drop (settled — a two-second drag racing agent edits is the designed concurrency). Three rules compose: (1) geometry re-derives — the frozen-at-drag-start inputs are the dragged items' sizes and the physical pointer only (03-board-ui.md ▸ Motion); the analytic resting zones recompute against each new snapshot, so a foreign lane-count re-divide mid-drag just moves the zones and the next proposal targets the board as it now is. (2) Proposals re-validate by liveness — a proposal whose target lane was tombstoned or vanished in the reload is invalidated (tombstoned lanes are never drop targets — The trash below); the shadow withdraws and no proposal stands until the pointer reaches a live target, and release with no valid proposal cancels — items return, nothing is written; a card is never filed under a deleted: parent. (3) An emptied drag cancels itself — drag membership is already a UUID set that vanished items leave silently (02-architecture.md); when the last dragged item vanishes the replica dissolves and release is a no-op. Partial vanishing drops the survivors, matching the pending-cut precedent.

Clipboard

  • ⌘X/⌘C/⌘V on cards and lanes (resettled — lanes joined the clipboard so cross-board structure transfer has a keyboard path under the every-function contract; the cards-XOR-lanes selection rule means the clipboard holds cards or lanes, never both). Hybrid clipboard: pasteboard carries a JSON manifest + plain text; full folder snapshots staged in Application Support so paste reproduces the item byte-for-byte — cards, attachments and all — across boards. Each manifest entry embeds the full index.md as a staging-less fallback (a lane entry embeds its cards' too, attachment-less). Staging lifecycle (settled): snapshots are staged eagerly at ⌘C/⌘X time — copy captures the source as it is at the gesture, immune to later deletion or unmount — and the store holds at most the current copy: a new Lanework copy replaces the previous snapshot, and a sweep at launch and on each copy purges entries the pasteboard no longer references (another app taking the pasteboard orphans the snapshot; the next sweep collects it). The snapshot survives relaunch exactly as long as the pasteboard still points at it — a copy made before quitting pastes whole after restart. A degraded paste is loud, never silent (the banner vocabulary — 02-architecture.md): if the staged snapshot is missing or unreadable at paste time, paste falls back to the embedded index.md — content intact, attachments absent — and a one-shot banner names exactly what was lost ("Pasted 'Fix login' without its 3 attachments"); the user never discovers an empty attachments/ later.
  • Cut is Finder-style deferred: cut items dim in place until paste moves them; voided if another app takes the pasteboard or the source board closes; second paste materializes copies. Deletion voids per item: a cut item that is tombstoned or vanishes externally before paste drops out of the pending cut — 02-architecture.md's UUID-set rule; transient state never resurrects what's gone — so paste moves only the survivors, and a cut voided down to nothing is simply void (paste disabled, no error).
  • Paste lands after the anchor card (or appends to a selected lane). Copies keep created (a duplicate is a fork) and take fresh GUID/order/modified.
  • Lane paste lands after the anchor lane — the selected lane, or the selected card's lane; nothing selected = the board's right end. Semantics mirror the drag pair above exactly: a pasted copy takes fresh GUIDs throughout and strips tombstoned cards; a cut-paste is the ⌘-drag move — the folder moves whole, tombstoned cards landing in the destination's trash. Pasting into the source board is supported and is the within-board lane duplicate (settled): fresh GUIDs and tombstone-stripping apply as anywhere else, no menu-validation special case — the drag path deliberately lacks this operation (⌥ ignored on lane drags, above), the clipboard is its one home.

Keyboard

Keyboard navigation is a first-class UX concern — the board must be fully operable without the mouse: creating, navigating, renaming, moving, deleting, and opening items all need keyboard paths, and every board function must have a menu item — the menu home is what makes a function keyboard-reachable (full keyboard access) and remappable (Configurable bindings below) — with a default chord where one earns its keyspace: 11-command-nexus.md's "— (no default)" rows are deliberate, not gaps, and remap like any other (toolbar-hosted functions included — toolbars are pure enhancement, 03-board-ui.md ▸ Toolbar). The pathfinder covered creation, navigation, rename, and delete; the keyboard-map iteration's outcome — settled as a coherent whole — is inventoried in 11-command-nexus.md, with its behavioral rules below.

Grammar (fixed keys — deliberately not remappable)

  • Arrows: spatial card navigation (nearest card in the direction, across interior grid columns and lanes); with a lane selected, ←/→ move lane selection; ⇧-arrow extends; selection scrolls into view; all grammar keys inert while a title editor is focused, and menu dispatch narrows to the text domain (focused-editor rule below).
  • ⌥-arrows jump: ⌥↑/⌥↓ to the current lane's first/last card; ⌥←/⌥→ to the first/last lane. ⌥↑ escalates into the lane domain (settled — the keyboard's one entry to lane selection): with the lane's first card already selected, ⌥↑ selects the lane itself — up in the hierarchy sense, the same key one press deeper; with a lane selected, ↓ (or ⌥↓) descends back into its cards at the first (last) card, and ⌥↑ is inert. An empty selection seeds at the first lane's first card on any plain arrow (deterministic origin; the ⌥-jumps behave as specified regardless) — two ⌥↑ presses from nothing reach the lane domain.
  • Return on a selected lane: creates a card at its bottom, editor focused; Return commits and re-selects the lane (next Return = next card); ⌘↩ commits and opens the card window. Abandoned placeholders (Escape, empty commit, click-away) are discarded — creating-then-abandoning never leaves an empty card behind (untitled cards exist only when made deliberately, e.g. by an external writer or by clearing an existing title). The placeholder is store-transient overlay state — the named exception to 02-architecture.md's one-way flow; nothing exists on disk until the title commits.
  • Inline rename tracks its target by UUID, and vanishing discards it (the placeholder and card-window kin rules — 02-architecture.md — applied to the third inline editor): a foreign move mid-rename is invisible — the editor follows the UUID and the commit writes the title wherever the card now lives; a target that is tombstoned, deleted, or gone at commit time discards the editor and its keystrokes silently (a liveness flip is a vanish; nothing is ever written into a vanished folder, and no partial index.md can resurrect deleted data). A write that fails after a valid commit is the ordinary one-shot write-failure banner. VoiceOver announces the vanished target per 10-accessibility.md's recovery rule.
  • Return on a sole selected card: inline rename. Return disambiguates on card selection — sole card = rename, lane = create (above) — and is inert on a multi-card selection; a lane's rename path is Board ▸ Rename. Escape steps outward one layer per press: abandons an open editor; else clears search, returning focus to the board (Search below); else clears the selection — the keyboard deselect.
  • Focused editor = text domain (settled): while an inline title editor — rename or the new-card placeholder — is focused, board-scoped menu commands (Delete, New Card, Paste, Move, Style, …) disable via menu validation; text-domain chords route to the field as standard text ops — ⌘Z/⇧⌘Z are the editor's text undo (06-history-undo.md ▸ Undo routing), ⌘X/⌘C/⌘V/⌘A act on the text. The one board-command carve-out is Open Card ⌘↩, which stays enabled to commit the edit — placeholder or rename — and open the card window. Exits are otherwise unchanged: Return commits, Escape abandons; click-away splits by editor kind — a rename commits (focus loss = commit, matching the card window's title field in 05-card-window.md and the branch-switch parenthetical in 06-history-undo.md), while the placeholder discards per its rule above, the deliberate exception because nothing exists on disk yet.
  • on a live selection: delete (tombstone) — the plain-key synonym for File ▸ Delete ⌘⌫ (see The map). Grammar, not a menu item: giving it a menu home would require a second "Delete"-titled item, which would collide for title-matched remapping (Configurable bindings). Inert while a title editor is focused, like every grammar key.
  • The card window speaks the same grammar: Return in Preview enters Edit, Escape returns to Preview (05-card-window.md) — plain keys, not menu items.
  • These plain-key behaviors are platform grammar (Finder's own Return/arrows aren't remappable either) and sit below the remapping mechanism, which handles modifier chords on menu items only — see Configurable bindings.

The map — moved to the command Nexus

Every command is a menu item. The full inventory — every command and action, its default binding, applicable context, and customizability class — lives in 11-command-nexus.md, the single source of truth for what the app can do; the command titles there are the stable strings the remapping mechanism keys on (Configurable bindings below). The rules below are the behavior behind those bindings and stay normative here.

  • ⌥⌘↑/⌥⌘↓ sort within the lane (the move-vs-jump question, resettled: card moves live on the ⌥⌘ chord, joining ⌥⌘←/⌥⌘→ lane width in a "⌥⌘ modifies" family; plain ⌥-arrows stay jumps; plain ⌘↑/⌘↓ are unassigned): the selected card(s) move one position within the lane — logical order, across interior masonry columns (10-accessibility.md's logical-order rule). A non-contiguous multi-selection gathers on the first press: the cards collect into a contiguous block anchored at the first selected card (first = lowest logical order; the rest follow in preserved relative order), and subsequent presses move the block one position. Cards never change lanes by ⌘-arrow (settled): inter-lane movement is drag or Cut/Paste (the clipboard rules above), so ⌥⌘↑/⌥⌘↓ disable when a card selection spans lanes and ⌘←/⌘→ are inert on card selections. With a lane selected, ⌘←/⌘→ move the lane one slot — closing 10-accessibility.md's lane-move defect — and ⌥⌘↑/⌥⌘↓ are inert.
  • ⌫/⌘⌫ delete (unchanged): tombstone into the trash quasi-lane (03-board-ui.md); lanes included, no dialog. Selection moves to the deleted item's successor sibling, Finder-style (next card in the lane, next lane on the board; the last sibling's predecessor otherwise; empty container = nothing selected) — repeated ⌫ walks down a lane. Deliberate deletes pick a successor; external vanishing never does (02-architecture.md's reload-survival rule: the selection just shrinks). On a tombstoned selection ⌘⌫ is Put Back instead — Finder's exact symmetry (⌘⌫ trashes and un-trashes). The dual role is carried by twin menu items sharing the chord — File ▸ Delete ⌘⌫ and File ▸ Put Back ⌘⌫, validation enabling exactly one by selection state; AppKit routes a shared key equivalent to the enabled item (Finder ships this exact pair as Move to Trash/Put Back; ours says Delete per 03-board-ui.md's naming constraint). Both titles stay stable (titles-are-API), and each is independently remappable — remapping one never moves the other's role. Plain ⌫ performs the same tombstone as fixed grammar (see Grammar above) — there is no Edit ▸ Delete item, so the two Delete-titled homes never collide for title-matched remapping.
  • Select All: all visible cards on the board — filter-respecting, like every surface (Search below).
  • The contract's one carve-out is configuration (settled): form-like git and board setup — add git, add/change remote, branch switching and creation, commit identity, credentials — lives in the board popover only, its committed home; its keyboard path is Board Info (⌘I) plus Tab-reachable controls (10-accessibility.md's Full Keyboard Access). Recurring remote operations stay under the contract: Board ▸ Pull and Board ▸ Push are menu items (no default chord, remappable; validation enables them only on remote-backed boards — 07-sync-collab.md).
  • ⌘N target rule (settled): with a card selected, the new card is created in that card's lane, immediately after it (paste-anchor consistency); with a lane selected, appended at its bottom (Return consistency); with nothing selected — or a tombstoned selection, which never anchors creation — the last-active lane — the lane that most recently held selection or a creation in this window session — falling back to the first lane. Title editor focused; same placeholder/abandon semantics as Return-creation. Zero-lane board (hand-made, or every lane deleted): card creation and card paste have no target — New Card, Return-creation, and Paste with a card payload disable via menu validation until a lane exists. New Lane (⇧⌘N) is one way in; Paste with a lane payload is the other — it stays enabled and lands at the board's right end (the lane-paste rule above), so cross-board structure transfer never needs a lane to exist first.

The trash, keyboard-first (settled)

The trash quasi-lane (03-board-ui.md ▸ Trash) speaks the same keyboard language when shown; hidden, it is invisible to every gesture. Rules:

  • Navigation: the shown trash is the last container for card navigation — arrows walk into and out of it, and ⌥→ jumps to it. The quasi-lane itself is never selectable as a lane (no lane op applies to it): with a lane selected, ←/→ and ⌥→ stop at the last real lane.
  • Moves are inert across the boundary: no move or paste ever targets the trash (deleting is ⌫/⌘⌫), and ⌥⌘↑/⌥⌘↓ are inert on tombstoned cards (moving out is Put Back or drag-to-restore).
  • Selection is homogeneous by liveness (extending the homogeneous-selection rule): a selection never mixes live and tombstoned cards. Select All selects visible live cards only; a rubber-band stays on the side of the boundary it started on. Menu validation stays binary — Delete for live selections, Put Back / Delete Immediately for tombstoned ones. External liveness flips can't breach the invariant: a reload that flips deleted: on a selected card ejects it from the selection (02-architecture.md's reload-survival rule — a flip is a vanish from its side of the boundary), so validation never sees a mixed selection.
  • Tombstoned lane entries are full keyboard citizens, homogeneous by kind: arrows walk every trash entry in its sorted order — card and lane entries alike (a lane's single restorable entry, 03-board-ui.md ▸ Trash) — and the board's cards-XOR-lanes rule extends into the trash: a selection never mixes card entries and lane entries (on top of never mixing live and tombstoned). Put Back (⌘⌫) and Delete Immediately (⌥⌘⌫) apply to lane entries exactly as to cards — a put-back lane returns whole, cards and all. A lane entry is not draggable (its entry is a compact row, not the lane); its copy-out is ⌘C only, and its move-out is Put Back.
  • Clipboard: copy out only. ⌘C (cards and lane entries), ⌥-drag, and the cross-board drag default (cards) always yield live copies — deleted: is stripped on paste/duplicate/drop, like copying a file out of Finder's Trash; a lane entry's copy additionally strips its tombstoned interior cards (the lane-copy rule — copies transfer content, and trash isn't content). ⌘X is disabled: the move-out vocabulary is Put Back or drag-to-restore, nothing else. The cross-board restore-move (⌘-drag below) needs no command of its own — its keyboard equivalent is the composition Put Back → ⌘X → ⌘V in the destination: same folder, same identity.
  • Everything edit-shaped is disabled on tombstoned selections — Open Card, Rename, Style… (File ▸ Duplicate is untouched: it duplicates the board, never the selection — 11-command-nexus.md; card copies out of the trash are ⌘C or ⌥-drag, which name a live destination). Finder file drops (attachment import) on tombstoned cards are inert — 03-board-ui.md's no-editing-in-the-trash.
  • Drag-to-restore follows the locality model: dropping a tombstoned card into one of its own board's lanes restores it at the drop position (deleted: removed, order set). Dropped on another board it follows the copy default — a live copy lands there and the tombstoned original stays in the source trash (copy-out, like ⌘C); ⌘-drag forces the true cross-board restore-move (the tombstone leaves the source board; ordinary cross-board move semantics, deleted: cleared at the destination).

Configurable bindings (settled)

Custom shortcuts are system-native, with no in-app remapping UI: macOS's App Shortcuts mechanism (System Settings ▸ Keyboard ▸ App Shortcuts, stored as NSUserKeyEquivalents in the app's defaults) remaps any menu item, and AppKit applies it automatically — menus always display the effective binding, so the menu bar is the self-documenting keyboard map. Because every board function is a menu item (the contract above), coverage is complete for all modifier-chord commands; the fixed grammar keys stay fixed by design. An in-app shortcut-recorder pane was considered and set aside as ceremony (../WISHLIST.md); the Help content carries one line teaching the System Settings path. Constraints this mechanism imposes, adopted as design rules:

  • Menu item titles are API. The mechanism matches on exact titles — renaming a menu item orphans users' bindings. Titles change only with the deliberateness of a schema change.
  • Toggles keep one stable title with a checkmark state — "Show Trash" stays "Show Trash" when checked, never becomes "Hide Trash". (Same for Edit Body and Raw Source.)
  • Undo/Redo are effectively not remappable — NSUndoManager rewrites their titles dynamically ("Undo Move Card…"), which defeats title matching. Accepted; nobody remaps ⌘Z.
  • Two items may share a default chord when validation is mutually exclusive (Delete / Put Back on ⌘⌫) — AppKit fires the enabled one. Each keeps its own stable title, so remapping stays per-item. Corollary: no two menu items share a title either (titles are the remap key), which is why plain-⌫ delete is grammar rather than a second Delete item.

Accessibility

Stance (committed, 00-vision.md): accessibility is a requirement, not an afterthought. The keyboard-first contract above is doing double duty as the drag-free operation guarantee — every function reachable without the mouse is the accessibility floor. The lane-move defect 10-accessibility.md named is closed by the map above (Move Left/Right on a selected lane). The full design — VoiceOver tree shape and logical traversal order over the masonry, clipboard as the drag-free card-move story, live-reload announcements, text scaling, visual accommodations, and verification — is 10-accessibility.md.

  • Search field invoked with ⌘F (the board toolbar's sole default item; removed from the toolbar, ⌘F surfaces it transiently — 03-board-ui.md ▸ Toolbar; in the card window, Edit ▸ Find is find-in-text instead — 05-card-window.md), live filter: cards whose title and body both miss the query animate out; case/diacritic-insensitive substring. Scope is title + body only (settled) — attachment filenames are not searched.
  • The filter is the single source of truth for "what's on the board": layout, drop zones, marquee, ranges, arrow nav, and lane count badges all read it. Hidden cards leave the selection; creating a card clears the search — creation's carve-out exists because a brand-new card must not be born invisible. Rename deliberately gets no carve-out: a rename committed during an active search re-runs the predicate like any edit — a title that stops matching animates the card out and drops it from the selection, exactly as an agent's edit would; the filter stays a pure predicate with one exception, not two. Escape clears, then returns focus to the board.
  • Dispatch while the search field is focused (settled): the field is a control, not a content editor — the focused-editor lockdown (Grammar above) does not apply. Text-domain keys route to the field: ⌘A/⌘X/⌘C/⌘V act on the query, plain ⌫ edits the query and never reaches the board, horizontal arrows move the caret. Board menu commands stay enabled and act on the board selection exactly as when the field is unfocused — ⌘N included (creating a card clears the search, above) — and the Delete pair stays unambiguous by construction: plain ⌫ is query editing, ⌘⌫ is File ▸ Delete on the selection.

Changes from Kanban

  • Finder file drop on lane empty space (old item 49) is in scope from the start rather than a follow-up.
  • Clicking never edits: the pathfinder's Finder-style slow-second-click inline rename is gone — Return (or the menu) renames, double-click only opens. One less timer, and no edit ever starts from a hesitant click.

Open questions

None currently — the keyboard map is settled (⌥⌘↑/⌥⌘↓ within-lane sort, ⌘←/⌘→ lane moves, ⌥-arrows jump, creation trio, ⌘N target rule, system-native remapping); its inventory lives in 11-command-nexus.md, its behavior above.