Files
lanework/DESIGN/04-interactions.md
T
rzen 16c10d61c3 Pivot the trash to a materialized .trash — cards only, no Put Back
The tombstone model is retired wholesale: deletion is a move into the reserved
<root>/.trash/, always entering at the top (rank minting — no deleted: key, no
timestamp sort), with modified stamped for the future age-based purge. Lanes
are never trashed — lane delete is physical, native undo in-session the net.
Put Back is gone: restore is drag-out or cut+paste (⌘X now enabled in the
trash as the keyboard restore path). Trash cards are ordinary cards — search,
selection, clipboard, and rendering uniform — and one container boundary
replaces the effective-liveness, ancestor-walk, and kind-homogeneity machinery
across 02 and 04. Delete stages by place (board moves, trash deletes
permanently under the recoverability confirm); legacy deleted: keys migrate
without destroying (cards relocate, lanes return live, notices). Templates
exclude .trash; Duplicate carries it; the agent guide teaches delete-as-move.
Materialized reserved lanes are named as a pattern — .archive is WISHLIST #8,
and the backed-out release-presentation question is marked open in 03.

Ruling: Redesign board Resolved card e8f1a6c2; 19 mooted tombstone-era rulings
moved to the board's new Mooted lane.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
2026-07-28 16:02:08 -04:00

36 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.
  • The range anchor (settled — standard macOS list semantics): the anchor is the last plain- or ⌘-clicked item, per board window, transient — never persisted. ⇧-click ranges from anchor to target in the flatten order (cards), lane order (lanes), or the trash's own order (no kinds there — cards only, The trash below), replacing the selection and leaving the anchor in place. A marquee and wholesale selections (Select All) set no anchor, so a following ⇧-click acts as a plain click; a reload that drops or liveness-flips the anchor clears it.
  • Selection is homogeneous: cards XOR lanes.
  • Board background — the margins around and between lanes, and below short content (settled): a plain click clears the selection — the pointer twin of Escape's deselect, Finder's behavior; modified clicks (⇧/⌘) are no-ops there — extension needs an item to extend to; the background is also a rubber-band origin surface on the live side, alongside lane empty space (live) and the trash column's empty space (trashed).
  • 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 — and toggles like empty space (settled): a click on the already-selected lane's header unselects, one lane-click behavior everywhere, so a full lane keeps a pointer path out of selection; 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 carries exactly its cards — the trash is board-level (.trash/ — 03-board-ui.md), so there is nothing lane-nested to strip or carry: copy and ⌘-drag move alike transfer the lane's folder as it is (resettled 2026-07-28; the old tombstone-stripping rule is retired with the tombstone model).
  • 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 file 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). Folders are refused at hover (settled — the attachment model is flat top-level files, and the importer refuses directories by design): a drag containing only folders never engages — no highlight, no drop proposal, the standard incompatible-payload read; a mixed drag proposes for its files only, and the drop imports the files while a loss row (02-architecture.md's warning tone) names the skipped folders ("Folders can't be attached — 2 skipped"). The create path thereby only ever fires with at least one importable file — no card is minted for an import that cannot succeed. Created cards land at the drop position (settled): resolved through the same card-grid zones an ordinary card drag uses, shadow included — drops are positional everywhere, and append-at-bottom stays the creation trio's rule, not the drop's. A multi-file drop shows one nominal-height shadow per incoming file (the multi-drag precedent; when macOS withholds item counts during hover the count floors at one shadow, the commit unaffected). A release on the lane header resolves to the topmost position (settled — forgiving beats a dead stripe: the header's chrome roles don't collide with a file payload). The landing shadow is the create path's whole feedback (settled): no lane-level highlight on top — each target gets one clear signal, and the card-attach highlight exists precisely because that target has no shadow.
  • 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 vanished in the reload is invalidated; 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 vanished 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. A cross-board lane arrival pre-divides the destination strip during hover (settled): while a foreign lane drag proposes into a board, the destination's standard width is computed with the arriving run's units included, so the shadow draws at the width the lane will actually take — without this it overflows the strip (the pathfinder's stripWidthUnits). The first entry samples the un-widened standard for one frame before hysteresis settles — accepted, imperceptible.

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 paste is an import boundary, so normalization applies (settled 2026-07-28 — 01-storage-format.md's loose-file rule): loose files the staged snapshot carries beside a card's index.md land in the pasted card's attachments/, Finder-renamed on collision — nothing the snapshot preserved is dropped on arrival. 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, files absent — and a loss row (warning tone, user-dismissed) names what was lost with true totals, attachments and loose files alike, folding plurals ("Pasted 'Fix login' without its 3 attachments"; "Pasted 2 items without their 5 files" — BannerCenter owns the phrasing, the manifest records each entry's staged file count so the totals stay honest); the user never discovers an empty attachments/ later. The pasteboard is re-read lazily, and the brief lie is accepted (settled): changeCount is checked on activation, on menu validation, and before paste — no timers; a background app taking the pasteboard while Lanework stays frontmost can leave Edit ▸ Paste enabled until the next check, and the paste itself re-validates and no-ops — nothing stale ever lands, which is the guarantee that matters.
  • 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 deleted (moved to the trash or destroyed) 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); a multi-selection anchors at its last member in flatten order — the ⌘N target rule's shared anchor (The map below). Copies keep created (a duplicate is a fork) and take fresh GUID/order/modified. A trash selection never anchors paste (settled — the ⌘N target rule's own wording, returned to the precedent it cites): ⌘V stays enabled and behaves exactly as with nothing selected — a card payload appends to the last-active lane, a lane payload lands at the board's right end; the trash is never the destination (▸ The trash), and a trashed card's live disk-lane never leaks in as "the selected card's lane".
  • Lane paste lands after the anchor lane — the selected lane, or the selected card's lane (several selected: the last, per the shared anchor rule); nothing selected = the board's right end. Semantics mirror the drag pair above exactly: a pasted copy takes fresh GUIDs throughout; a cut-paste is the ⌘-drag move — the folder moves whole (nothing lane-nested to strip or carry — the trash is board-level, resettled 2026-07-28). Pasting into the source board is supported and is the within-board lane duplicate (settled): fresh GUIDs 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 — except ⇧↑/⇧↓ in the lane domain, which are inert (settled: there is nothing above the lane domain and no vertical range within it); 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. The horizontal jumps land on a card (settled — ⌥↑ is the keyboard's one entry to lane selection, so ⌥←/⌥→ never select the lane itself): the first card of the first/last non-empty lane, scanning inward past empty lanes; ⌥→ prefers the shown non-empty trash — its first entry — per the last-container rule (The trash below). ⌥↑ 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 trashed, deleted, or gone at commit time discards the editor and its keystrokes silently (entering the trash is a vanish from the board; nothing is ever written into a vanished folder). 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.
  • Caret chords yield to any focused text control (settled): Board ▸ Move Left/Move Right ⌘←/⌘→ and the width pair ⌥⌘←/⌥⌘→ disable via menu validation whenever any text control has keyboard focus — inline title editors, the board search field, board-popover fields (rename, git identity, remote), and card-window fields — because an enabled menu key equivalent fires before the field ever sees the key, and ⌘←/⌘→ are the standard line-start/end caret chords. Caret motion always wins in text (the Safari pattern: ⌘← is Back, yet moves the caret while a field is focused); the lane commands re-enable the moment focus returns to the board. This is a narrow, per-command broadening of the focused-editor rule, not a general one: board commands whose chords carry no text meaning keep their surface-specific dispatch — in particular the search field's board-commands-stay-enabled rule (Search below) — and the search field's explicitly ruled ⌘⌫ steal (File ▸ Delete, not delete-to-line-start) stands.
  • on a selection: delete — the plain-key synonym for File ▸ Delete ⌘⌫, staged by place like the menu item (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 (resettled 2026-07-28): on cards, a move into the trash (.trash/, top position — 03-board-ui.md); on a trash selection the same chord deletes permanently (one Delete vocabulary, staged by place — confirmation per 03's recoverability rule); on lanes, a real delete — folder and contents, no dialog, native undo the net (03 ▸ Trash). 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). Put Back is retired with the tombstone model (resettled 2026-07-28): File ▸ Delete is the chord's only owner — no twin menu items, no shared-equivalent routing; restore is drag-out or ⌘X/⌘V (The trash below). Plain ⌫ performs the same delete 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). On the active trash side it selects the trash (resettled 2026-07-28): with the trash visible and a non-empty trash selection, Select All selects all visible trash cards; in every other state, all visible live cards — the container boundary decides which "all" is meant (The trash 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); a multi-selection anchors at its last member in flatten order (settled — lane order, then card order, the multi-drag order; the same anchor serves paste): creation follows the last selected card, or appends to the last selected lane; the lane header's new-card button overrides this rule — the click names its target lane, selection notwithstanding (11-command-nexus.md ▸ Pointer grammar); with nothing selected — or a trash 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 (resettled 2026-07-28 — the materialized trash)

The trash lane (03-board-ui.md ▸ Trash — cards moved into <root>/.trash/, cards only, no lane entries) speaks the board's ordinary keyboard language when shown; hidden, it is invisible to every gesture — and hiding it clears a selection of trash cards (nothing invisible stays selected, so the toggle-off drops the selection rather than leave commands enabled against rows nobody can see). Because trash cards are ordinary cards, the old kind- and liveness-boundary machinery is retired: no lane entries, no ancestor walks, no entry-vs-universe split. Rules:

  • Navigation: the shown trash is the last container for card navigation — arrows walk into and out of it, and ⌥→ jumps to it. The trash 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.
  • Dropping a live card on the shown trash deletes it (settled): the drag is the pointer's delete gesture — release moves the dragged card(s) into .trash/. The drop diverges from positional drops in one way: the shadow always takes the topmost position — honest, not arbitrary: every trash arrival mints a rank above the current top (03 ▸ Trash), so a fresh delete genuinely lands on top. Lanes are not deliverable this way (a lane drag proposes only lane slots); the trash takes no drops while hidden, like every gesture. Cross-board arrivals and ⌥-copies refuse too (a transfer-and-delete compound and a copy-into-the-trash are operations the design doesn't name), and a refusal falls through to the strip retarget rather than cancelling the held drag.
  • Selection keeps one container boundary: a selection never mixes trash cards with board cards — a single container rule replacing the old liveness law, because Delete would otherwise mean two different things in one gesture (move-to-trash vs permanent). The rubber band stays on the side it started on, ⇧-arrow extension stops at the boundary (plain arrows walk across — navigation moves, extension stops), and Select All with a non-empty trash selection selects all visible trash cards, otherwise all visible live cards. Menu validation stays binary by container: Delete = move to trash on board selections, Delete = permanent on trash selections (03 ▸ Trash). An external move observed by reload re-resolves the selection by presence, as everywhere (02-architecture.md).
  • Within-trash moves are inert: no move or paste ever targets the trash (deleting is ⌫/⌘⌫ or the drag above), and ⌥⌘↑/⌥⌘↓ are inert on trash cards — the trash's order is its arrival order, not a workspace to arrange.
  • Clipboard: the restore path. ⌘C copies a trash card (a live copy lands wherever pasted — like copying out of Finder's Trash); ⌘X works (resettled — it was disabled under the tombstone model): cut in the trash, paste into a lane is the keyboard-native restore, an ordinary folder move (10-accessibility.md's drag-free contract). A card entering the trash voids its pending cut (the deliberate-removal rule): a cut card that gets deleted drops out of the pending cut, as under the old model.
  • Everything edit-shaped is disabled on trash selections — Open Card, Rename, Style…; Finder file drops on trash cards are inert (03's no-editing-in-the-trash). Creation never anchors to the trash: ⌘N and paste with a trash selection fall back to their nothing-selected targets.
  • Drag-to-restore follows the locality model: dropping a trash card into one of its own board's lanes is an ordinary move to the drop position. Dropped on another board it follows the copy default — a live copy lands there, the original stays in the source trash; ⌘-drag forces the true cross-board restore-move.

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 (a pattern currently unused — Put Back's retirement removed its one instance) — 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, and it is stated by mechanism, not by gesture (settled): any user-initiated creation on the board clears the query — ⌘N, Return-creation, the header button, empty-space double-click, paste, and Finder file drops alike — while foreign/agent-filed cards keep riding the live filter (02-architecture.md's derived-result rule). 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 is staged (settled): in a non-empty field it clears the query, focus staying in the field; in an empty field it returns focus to the board; with board focus and an active search, one press clears the search and the full board returns — search takes Escape before its clear-selection meaning, which applies only when no search is active. A lane the query empties keeps its slot (settled): lanes are never filtered out — an all-misses lane stays on the board at its width with a 0 badge (the count reads the filter, 03-board-ui.md); the search filters cards, and the board's structure is not a search result. A leaving card stays input-reachable for its out-transition (settled): marquee and arrow targets deregister when the ~0.28 s animate-out ends, so a card mid-departure is briefly reachable while already out of the selection — accepted: it is literally on screen for that span, and closing the window would teach three input sites a predicate the layout already applied. An open inline rename survives the filter hiding its card (settled): the editor is a surface the filter doesn't reach — it stays open and focused, commits by UUID wherever the card lives, Escape abandons; keystrokes are never silently discarded for a card that still exists (the dirty-buffer courtesy), and the vanish-discard rule stays reserved for true liveness flips. The typed-query path can't even occur — focusing the search field is focus loss, which commits the rename first — so the rule covers foreign edits that stop the card matching.
  • 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. Every key with the field focused acts on the field — stock NSSearchField behavior, no pass-throughs (settled): vertical arrows are caret movement, ⇧-arrows select query text, and Return is a swallowed no-op (the filter is live, there is nothing to submit — it never reaches the board's rename/create grammar). Tab is the keep-filter path: plain key-view traversal moves focus to the board with the query intact, and the whole board grammar (arrows, ⌥↑ escalation, Return, ⌘↩) then applies over the filtered board; ⌘F returns to the field. 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) — except the caret-chord commands: Move Left/Right ⌘←/⌘→ and the width pair ⌥⌘←/⌥⌘→ disable while the field is focused (Grammar above, caret-chords rule), so ⌘←/⌘→ stay line-start/end in the query even with a lane selected — and the Delete pair stays unambiguous by construction: plain ⌫ is query editing, ⌘⌫ is File ▸ Delete on the selection, and ⌘Z/⇧⌘Z are the field's own text undo, never git undo (06-history-undo.md ▸ Undo routing's control-class rule).

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.