Gap #10 ruled into 04 ▸ Search's single-source-of-truth bullet: zones, shadow, and write all resolve through the same filtered list — the position the shadow shows is the landing the write performs. The slot's meaning in the full order is its visible anchor (after the visible predecessor, before the visible successor at the lane top, true end in a query-emptied lane; multi-card in proposal order beside the anchor); hidden cards keep their ranks untouched, and the gesture's meaning survives the query's clearing. The rendered space includes the new-card placeholder. Code diverges today — implementation filed on the Implementation board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
42 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 — rows of both kinds included, since trash selection went kind-blind (re-ruled 2026-07-31; 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 — on the live board. The trash's selection is kind-blind (re-ruled 2026-07-31; The trash below): cards and lane rows select together there, and the guard lives at the exits instead.
- 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) — which extends the full column height below the last row, card and lane rows alike (re-affirmed 2026-07-29; the rewrites dropped the clause, the ruling never changed): no dead zone, a band can arm from anywhere in the shown trash's column.
- 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
titlekey (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
orderfirst, then cardorder(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,
createdis 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 app-side (02-architecture.md ▸ Per-board app state's app-wide home) so paste reproduces the item byte-for-byte — cards, attachments and all — across boards. Each manifest entry embeds the full
index.md— identification metadata (menu validation, refusal wording, the plain-text flavor's source), never a materialization source since the 2026-07-29 refuse-don't-degrade ruling below (a lane entry embeds its cards' too). 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'sindex.mdland in the pasted card'sattachments/, Finder-renamed on collision — nothing the snapshot preserved is dropped on arrival. A paste whose staged snapshot is missing or unreadable refuses loudly — never degrades (re-ruled 2026-07-29, retiring the degraded embedded-index.mdfallback and its loss row; Finder's invariant adopted, and 01's leniency doctrine applied — proceed-partially-lose-a-little is never a verdict): the paste produces nothing, and a one-shot failure banner names it from the manifest's metadata ("The copied cards are no longer available" / "Couldn't paste 'Fix login' — the copied content is gone"; BannerCenter owns the phrasing). An item arrives whole — index, attachments, loose files, and comments when they ship — or not at all; a hollowed card is never materialized, so the loss-accounting problem (what didn't arrive, and whether the totals are honest) dissolves rather than being solved. The refusal is transactional — all-or-nothing for the whole paste, the copies-are-transactions posture (01). With eager staging and the shared-store sweep discipline this is a rare corner, not a flow: the refusal names it, and ⌘C again is the recovery. 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; lanes rejoined 2026-07-29): on cards and lanes, a move into the trash (
.trash/, top position — 03-board-ui.md; a lane travels subtree-intact,kind: lanestamped when absent, no dialog — recoverable now, so nothing needs confirming); on a trash selection the same chord deletes permanently (one Delete vocabulary, staged by place — confirmation per 03's recoverability rule, a lane's alert counting its cards, a mixed trash selection's alert counting both kinds). 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. In the trash the successor walk is kind-blind (ruled 2026-07-31): the next row of either kind, in the same all-rows order plain arrows walk — a successor is a fresh singleton selection, so the landing violates no grammar, and repeated ⌘⌫ empties a mixed trash without dead-ends, each delete confirm-gated per its kind. 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; containers re-ruled 2026-07-31 — the popover/sheet split): form-like configuration keeps exactly one home per control, split by weight across two committed homes. The board popover is the light surface — rename, styling, branch display and switching, ahead/behind with Pull/Push, the status badges; its keyboard path is Board Info (⌘I) plus Tab-reachable controls. Setup lives in the board settings sheet (03-board-ui.md ▸ Board settings sheet) — add git, add/change remote, credentials and the SSH surface, commit identity, branch creation, push-on-commit; its keyboard path is Board ▸ Board Settings… (11-command-nexus.md) plus Tab-reachable controls (10-accessibility.md's Full Keyboard Access). The split's reasons are mechanical, not aesthetic: setup flows fire confirmation alerts, run inline network probes, and accept drag-in key import — acts that need a surface a stray click can't dismiss. 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 cardorder, 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 and lanes moved into <root>/.trash/; lanes rejoined 2026-07-29 as opaque-unit rows) speaks the board's ordinary keyboard language when shown; hidden, it is invisible to every gesture — and hiding it clears a trash selection (nothing invisible stays selected, so the toggle-off drops the selection rather than leave commands enabled against rows nobody can see). Trash cards are ordinary cards; a trashed lane is one opaque row (title + card count) — the old liveness machinery stays retired: no ancestor walks, no entry-vs-universe split, one container boundary plus the board's own kind rule. Rules:
- Navigation: the shown trash is the last container for card navigation — arrows walk into and out of it, and ⌥→ jumps to it; inside, plain arrows walk every row, card and lane row alike (navigation crosses kinds). 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 — or lane — on the shown trash deletes it (lanes extended 2026-07-29): the drag is the pointer's delete gesture — release moves the dragged item(s) into
.trash/; a lane drag over the shown trash proposes the delete alongside its strip slots. The drop diverges from positional drops in one way: the shadow always takes the topmost position — honest, not arbitrary: every trash arrival stampsmodifiedand the trash sorts newest-first by that stamp (03 ▸ Trash), so a fresh delete genuinely lands on top. 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 — and goes kind-blind inside the trash (re-ruled 2026-07-31, superseding the lanes-rejoin pass's kind-homogeneous trash grammar): a selection never mixes trash items with board items, but within the trash cards and lane rows select together — clicks, ⇧-click ranges, ⇧-arrow extension, and the rubber band all sweep every row (the band's full-height backdrop covers both kinds), and Select All with a non-empty trash selection selects all visible trash rows. The live board keeps cards XOR lanes, and its Select All stays card-scoped, as everywhere. The guard moves to the exits (the mixed-payload drop refusal and ⌘C/⌘X validation below) — inside the trash the only verbs are Delete and the restore paths, so upstream homogeneity bought nothing the exits don't. ⇧-arrow extension still stops at the container boundary. Menu validation stays binary by container: Delete = move to trash on board selections, Delete = permanent on trash selections (03 ▸ Trash) — and Delete works on a mixed selection, the alert counting both kinds. 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 rows — 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 is the keyboard-native restore, an ordinary folder move (10-accessibility.md's drag-free contract) — a card pastes into a lane, a trashed lane pastes after the anchor lane (the lane-paste rule above, verbatim). ⌘C and ⌘X validate against mixed selections (ruled 2026-07-31, with kind-blind selection): the pasteboard's payload types are per-kind, so Cut and Copy grey out via ordinary menu validation while a trash selection mixes kinds — no failed gesture, no beep; the drag path's drop-time explanation (Drag-to-restore below) is where the rule teaches itself. An item entering the trash voids its pending cut (the deliberate-removal rule): a cut card — or lane — 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…, and lane width ops on lane rows; Finder file drops on trash rows 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 — or a trashed lane row onto its own board's strip — 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. A mixed-kind drag never leaves the trash (ruled 2026-07-31): pickup is allowed — the selection is legal — but every out-of-trash drop target refuses the mixed payload, and the release surfaces a notice explaining the rule ("Cards and lanes leave the trash separately — restore one kind at a time"); the refused drag ends like any refusal, rows staying put. Within-trash drops stay inert as above.
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
- 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 all card content the format makes meaningful (re-ruled 2026-07-29, superseding title-plus-body-only): title + body today; comment bodies join when comments ship — via a search-owned transient comment index, never the snapshot: the first live-query keystroke kicks an async sweep of
comments/*/index.mdbodies (.draftandcomments/.trash/excluded), kept fresh by the same FSEvents stream while a query is active and discarded when it clears — the board walk stays O(cards), 01's window-scoped read untouched; attributes join as they activate (title now; labels/tags and their kin are reserved, inert keys this version — nothing to search until a future version gives them life). Attachment filenames stay unsearched. Scope options (content vs attributes, either/or) are WISHLIST #10. - 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. A drop under an active filter counts in the rendered space (ruled 2026-08-06, closing the divergence found 2026-08-01): the zones, the shadow, and the write all resolve through the same filtered list — the position the shadow shows is the landing the write performs (the shipped interim — zones and write counting the unfiltered lane while the slots rendered filtered — was self-consistent but named a position the user could not see; retired). The slot's meaning in the full order is its visible anchor: the dropped card enters immediately after the slot's visible predecessor — or immediately before its visible successor when the slot has none (top of the lane) — and a lane the query emptied appends at its true end; a multi-card drop enters in proposal order beside the same anchor. Hidden cards keep their ranks untouched, never displaced by a gesture that could not see them — and the anchor is what makes the gesture's meaning survive the query's clearing: when the filter lifts, the card sits exactly beside the card it was dropped against. The rendered space means everything rendered — the new-card placeholder occupies its slot for the zones exactly as it does for layout. 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.