Files
lanework/DESIGN/03-board-ui.md
T
rzen 190a8e36f1 Search takes the center — the titlebar placement grammar ratified
Redesign card 6d5a97fd resolved off the 2026-08-01 live try-out: leading is
board identity, center is view controls, trailing stays the user's catalog.
centeredItemIdentifiers, not a flexible-space sandwich — centers against the
window, holds as catalog items install, reaches machines with a saved
arrangement; defaultItems and its pinned tests untouched. 03 records the
grammar and the accepted tensions (widget-vs-field budget in narrow windows,
HIG's trailing convention yielding).

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-06 22:13:55 -04:00

42 KiB
Raw Blame History

Board UI

The board window: layout, lanes, cards, and styling. Interaction mechanics (selection, drag, keyboard) are in 04-interactions.md.

Layout — full visibility

  • Every lane is always on screen. The window width divides across the lanes' width units — no horizontal scroll, no enforced minimum lane width. Resizing the window is the width control. (Settled emphatically in the old app: horizontal scroll strays from what kanban is for.) The degenerate case is accepted, not floored: enough lanes/units in a small window compress every lane, titles and cards truncate gracefully, and the remedy is the user's (fewer units, bigger window). A minimum-width setting that reintroduces scroll was considered in the pathfinder and deliberately rejected. Lane-count changes re-divide, never resize: new lane (⇧⌘N), lane paste or cross-board lane drop, lane delete (and its undo), and Show/Hide Trash (the trash lane's fixed unit joins and leaves the division — Trash below) all re-divide the existing window width across the new unit total — window-growing behavior belongs to the right-edge drag alone (Lane below).
  • A lane spans a whole number of width units (width frontmatter, ≥ 1, no cap). Cards stay standard width; a wide lane flows them into as many interior masonry columns as it has units.
  • Zoom scales the ruler, never the strip (settled 2026-08-02 — View ▸ Zoom In / Zoom Out / Actual Size, 11-command-nexus.md). The board's whole geometry is already derived from the body font's point size (10-accessibility.md ▸ Full relative scaling — every figure an em multiple, no fixed point sizes), and macOS supplies no text-size control to move it, so the zoom commands are that control: a rung on the level ladder raises the effective body size, and type, card chrome, lane chrome and card heights grow together off it. Full visibility above is untouched, and that is the whole design constraint: a canvas magnification would have to widen the strip and reintroduce the horizontal scroll this section rejects, so zoom does not do that — lane width stays the window's division at every rung. Zoom does move the inter-lane gap (an em multiple like everything else), so lanes narrow by a few percent across the ladder's whole range; the felt effect is the intended one — zoom in for bigger, more legible cards and fewer per screen, out for a denser board. The level is app-wide, persisted, and never a property of a board: it lives beside Show Comments in the app's preferences, not in any lane's or board's frontmatter, because it describes how a user likes to read rather than what a board is. Zoom never moves the window — the minimum content size stays pinned to the system body size, since window-growing behavior belongs to the right-edge drag alone (Lane below).
  • New lanes are created via a File-menu item (the one committed surface; ⇧⌘N — 11-command-nexus.md); new cards from the lane (see 04-interactions.md for creation flows). The created lane becomes the sole selection (settled): ⇧⌘N → Board ▸ Rename is a pure keyboard path — the create-then-act texture (Return-creation re-selects its lane, ⌫ picks a successor).

Toolbar (board + card windows)

Toolbars are pure enhancement: every function they host already has a menu item + shortcut (04-interactions.md's contract), so nothing below is anyone's only path. Both windows' toolbars are user-customizable, macOS-native (right-click ▸ Customize Toolbar…, drag to rearrange, system overflow and icon/text display options) — the sets below are shipped defaults, not verdicts. Toolbar item labels match their menu-item titles exactly (Show Trash, Edit Body, Raw Source, …), minus any trailing ellipsis (macOS convention: "Add Attachment…" labels as Add Attachment) — one vocabulary everywhere, and the customize palette self-documents against the menus. One exception: the Undo/Redo toolbar items keep static labels — NSUndoManager rewrites their menu titles dynamically ("Undo Move Card…", 04-interactions.md ▸ Configurable bindings), which a toolbar label doesn't track.

  • Board window default: the search field, nothing elsecentered (ratified 2026-08-06 off the 2026-08-01 live try-out, reversing the earlier trailing ruling): the titlebar reads as a placement grammar — leading is board identity (the title widget), center is view controls (search today, the filter family if one ever grows), trailing remains the user's catalog space. The mechanism is NSToolbar.centeredItemIdentifiers, deliberately not a flexible-space sandwich: it centers against the window rather than leftover space, holds as catalog items install, and sits outside the autosaved configuration, so it takes effect on machines with a saved arrangement — and defaultItems is unchanged, keeping the pinned default-set tests standing. The known tensions were weighed and accepted at ratification: the 400pt leading widget and a centered field share the titlebar's budget in narrow windows (the squeezed field's expand-in-place answer below is the relief), and HIG's trailing-edge convention yields to the grammar. The titlebar stays clean. ⌘F always summons search: with the field removed from the toolbar, invoking it surfaces the field transiently until the search clears. A squeezed field expands in place; the strip answers only genuine unreachability (blessed 2026-08-06): a space-constrained NSSearchToolbarItem collapses to a magnifying-glass button still in the window, and ⌘F expands and focuses it — the toolbar's own field, a better surface than the fallback — so the transient strip fires only for a truly windowless field (true overflow, or the item removed from the toolbar). The item's overflow row is the platform's own second answer, and the duality is blessed (2026-08-06): AppKit's overflow menu row carries a live action that widens the window until the field is usable, and suppressing it would destroy the honest overflow presentation — so a squeezed toolbar reaches search two ways with two honest resolutions: ⌘F expands the item or surfaces the strip (ours), the overflow row grows the window (the platform's) — different gestures, reasonable respective outcomes, no contradiction to resolve. The field is the platform's own search toolbar item and grows on focus (2026-08-01): the em-derived width is the focused width — applied when the field takes the keyboard, expanding via the item's own animation — and the resting width is AppKit's natural one, not the app's to set. The two-homes width rule is focused-width parity (ruled 2026-08-01): the width the user types in is the same em-derived figure whichever home the field is in — the toolbar item focused, or the transient strip; the toolbar field's resting width is outside the invariant (the strip never rests — it exists only while a search is live or focused, so it has no collapsed state to mirror). Catalog (available via Customize): New Card, New Lane, Zoom In, Zoom Out (the zoom pair is catalog-only by the same logic as everything else here — the titlebar's default stays the search field alone; each disables at its end of the ladder, and both disable mid-drag like their menu rows — Layout ▸ zoom above), Undo, Redo (the pair disabled only under locks and on empty stacks — re-ruled 2026-07-31, twice: the provider follows the board, so boards without app-managed git — repo-nested included — bind 13-native-undo.md's native stack in every tier and Pro git boards bind the git provider — 06-history-undo.md), Show Trash (toggle state matching the View menu checkmark). The board popover deliberately has no toolbar item — the window-title widget is its committed home (below), and a second entry would muddy it.
  • Card window default: Edit Body · Raw Source · Add Attachment — the window's three committed functions, all discoverable from its toolbar; the catalog is the same trio. Edit Body is a single toggle button (on-state in Edit — mirroring the View ▸ Edit Body checkmark and the ⌘E/Return/Escape grammar; the pathfinder's segmented Preview|Edit is retired). Raw Source is likewise a toggle showing on-state; while source mode is active, Edit Body disables (Cancel/Apply own the exits — 05-card-window.md). Add Attachment stays enabled in every mode — attachment operations never touch index.md, so they're safe alongside a raw edit (the sidebar's feedback returns on exit).

Lane

  • Title bar: leading SF Symbol (the lane's icon), title, card-count badge (quiet, secondary styling), new-card button. The whole title bar is the lane's drag surface — no separate grip; a plain click (no movement) on it selects the lane (04-interactions.md ▸ Selection). The lane has one context menu (settled), invoked on the header or on lane empty space alike — Rename, Style…, the quick-style recents row, the Width stepper, Delete (inventory normative in 11-command-nexus.md ▸ Context menus); a full lane still has its header, so the menu is always reachable. The count reads the search filter like every other surface (04-interactions.md): during a search it shows the visible count, not the total.
  • Body: vertical card stack (masonry grid when wide — settled, the pathfinder's masonry works), scrolls vertically.
  • Right-edge drag-to-resize between integer widths (1×, 2×, 3×, … — no cap): shadow snaps at the inter-column gap with 10pt release hysteresis; the window grows/shrinks by one standard width per snap so other lanes keep their exact size. Growth hard-stops at the screen's visible frame, with rubber-band feedback (the dragged edge gives a fraction of the overshoot and snaps back, signalling the bound — pathfinder behavior, proven): the drag never compresses siblings and the window never overflows the screen. The header context menu's Width control (stepper, uncapped) is the precise control — and deliberately the opposite mechanism: it never touches the window, it re-divides the existing width across the new unit total (siblings compress). Widths beyond the screen's capacity stay reachable through it. The Increase/Decrease Lane Width menu items (⌥⌘→/⌥⌘← — 11-command-nexus.md) are this stepper's keyboard face — same re-divide semantics, never the window's size; window-growing behavior belongs to the drag alone — and they batch over a multi-lane selection (settled, the styling precedent): each selected lane steps one unit, one gesture, one commit; the context-menu stepper itself stays single-lane by nature. A width write landing on 1 removes the width key (settled — the remove-at-default family: the empty rename removes title, the None well removes background): a default lane's frontmatter stays clean, drag, stepper, and menu items alike; a hand-written width: 1 is legal and preserved until the app itself next edits width. A failed width commit at drag release rolls the window back (settled): the failure surfaces as the ordinary one-shot banner and the window animates back by the uncommitted delta — 02-architecture.md's write-failure honesty (the action visibly doesn't happen) applied to the one control that moves the window.

Card face

  • Leading icon + title. The only face chip in scope is attachments (a quiet indicator when the card has files — the title dominates). Metadata chips (labels/assignees/due) went to the enhanced schema with their fields — out of scope.
  • No body excerpt (settled): the face stays title-only — the old "iterate on the card face later" item is closed with no growth.
  • Titles are optional at every level. On cards and lanes, a missing title renders as a quiet placeholder ("Untitled", secondary styling) wherever the title would appear. On boards, the fallback is the folder name (sans extension), never "Untitled" — see 01-storage-format.md's board-naming rule; window title and welcome recents show title when present, folder name otherwise.
  • Attachments on the face: the chip only — there is no face carousel (resettled 2026-07-28, reversing the carry-over): the pathfinder's selection-keyed in-place expansion — compact unselected, media carousel when sole-selected — proved undesirable and does not carry over. A card has one presentation: selection changes styling (the selection treatment), never geometry, so the masonry never reflows on click and a card face is the same object whatever the selection state. The attachment chip above is the face's whole attachment story; viewing media is the card window's job (⌘↩ / double-click — the attachments section and QuickLook, 05-card-window.md). The earlier 2026-07-28 carousel settlements (metrics, tick paging, clamp, dots, marquee suppression, trash exclusion) are superseded with it — recorded on their Resolved cards.

Styling

Capabilities (settled)

  • background on board / lane / card: palette name (kebab-case, hand-editable) or #RRGGBB[AA] hex. Board color paints the board window's content background (the surface behind and between lanes). Lane and card color are edge accents, not fills (settled in the pathfinder's treatment shootout — its settings matrix of C-series lane / K-series card variants landed on C7 · full-column top edge and K1 · left edge stripe): a lane's color paints a full-width band along its top edge, a card's a stripe along its left edge; the surfaces themselves keep the standard chrome, so colored title text never sits on a colored fill.
  • The standard chrome is the pathfinder's surface stack (settled 2026-08-06): the window keeps the neutral system background; every lane wears a quiet quaternary-wash plate (the trash plate's own figure — translucent, so a board-chosen color shows through and the board-level ink rule keeps its premise; opaque under Reduce Transparency, the trash precedent); every card sits on an opaque controlBackgroundColor plate — white over the washed lane in light appearance, a step darker than the window in dark. One plate value for every face a card draws (resting, replicas, placeholder, arriving), so a card is the same object wherever it renders.
  • icon: SF Symbol per item with per-level defaults (board rectangle.split.3x1, lane square.stack, card doc.text).
  • iconColor: resolved — schema yes, control no. The field renders when hand-written (tint palette name or hex); the app offers no control for it (Controls below).
  • The pathfinder's palettes (12 icon tints, 12 backgrounds) carry over as the starting point. The 12+12 split is a picker split, not a name split (settled): color resolution searches foregrounds then backgrounds, so a hand-written background: carnation (an icon-tint name) resolves and paints — the split governs what the grids offer, never what a name means.

Controls (settled)

One style editor component — a background palette grid and a curated symbol grid — presented from three anchors: embedded in the card window sidebar's Style section (05-card-window.md) and in the board popover's styling area, and as a popover opened by Style… from a card/lane context menu or the menu bar (Board ▸ Style…, ⌥⌘S — 11-command-nexus.md; selection-aware: it styles the selected cards or lane, and with nothing selected, the board). One component, one behavior, three anchors — replacing the pathfinder's swatch-row-plus-Style-popover split, whose functions were right and whose form wasn't. The anchors compose the halves they need (2026-08-06): the card sidebar shows the symbol grid with the color combo (below) standing in for the background half — the narrow context the combo was built for; the board popover shows the background half only, its symbol picker beside the rename field owning the board glyph (two surfaces writing one key in one popover would read as two settings); the Style… popover carries both grids in full.

  • Curated-first, panel-backed (re-ratified 2026-08-06, reversing 2026-07-29's palette-only ruling): the background grid offers the 12 palette colors — every one AA-verified through one code path (ratified 2026-07-29: palette names route through the same runtime ink-selection seam as hand-written hex — the appearance flip picks the readable label vocabulary — and PaletteContrastTests pins that the chosen ink meets AA in both appearances for all 12 backgrounds, so palette drift can never silently break it) — plus a leading None well that removes the background key. Beside the grid the vocabulary now has a second, compact form: the color combo — a swatch-faced popup listing None, the role's twelve, the current off-palette value verbatim when there is one, and Other…, which opens the system Colors panel. The panel is the in-app escape hatch the 2026-07-29 ruling withheld: a pick landing exactly on a palette color stores the name (so a re-pick never drifts to a hex spelling), anything else stores the hex — the same unlimited vocabulary hand-editing always had, now pickable. An arbitrary pick changes no contrast story (it lands on the identical runtime ink computation hand-written hex already gets — 10-accessibility.md), and the quick-style recents stay palette-vocabulary: a panel pick never enters them.
  • Curated symbol grid: a hand-picked set (roughly five dozen kanban-relevant SF Symbols); its leading well is the level's default symbol and removes the icon key. Any other SF Symbol name works written by hand — named symbols the running OS knows, that is: inventories grow per macOS release, so a newer-OS name renders the level default on an older Mac, value preserved on disk — the palette stance again. No full-browser escape hatch in-app; the raw file is the escape hatch. (Symbols keep this stance deliberately — the 2026-08-06 color-panel reversal above is colors only: the system offers a Colors panel worth deferring to, and no symbol browser of equal standing.)
  • Off-palette values display leniently: a hand-written hex background or uncurated symbol shows as the current value in the editor (labeled verbatim, outside the grids); choosing any well replaces it.
  • Batch edits: a multi-selection shows per-dimension mixed state (no well selected, "—" where a value would read); choosing a well applies to the whole selection — one gesture, one commit on git boards.
  • The Style… popover tracks its target set live and dismisses when it empties (settled): its target is the selection, re-resolved across reloads by 02-architecture.md's UUID rule — a member that vanishes or flips liveness leaves the set and the mixed-state display recomputes; a set emptied by a foreign reload dismisses the popover (the inline-rename discard applied here) — it never silently retargets to the board, and nothing writes into a vanished folder (a member moved to the trash leaves the set like any other departure). The read-only lock instead disables its wells in place (settled): a popover open when the lock lands stays open, content disabled — the banner names why, and the lock never yanks a surface (the Edit buffer's keeps-its-place posture). The embedded anchors need no rule of their own: the card sidebar dismisses with its card's window, and the board popover's target is the board itself.
  • Quick-style row, recents only: card and lane context menus carry one compact row of recently used backgrounds plus the Style… item — one-click recolor for the common case; the pathfinder's second full-palette tier is gone. Before any background has ever been applied, the row is omitted entirely — never an empty strip. Recents are app-wide and persist app-side (user preference, never board data).
  • Keyboard path: Style… is a menu item with a shortcut (04-interactions.md's contract); inside the editor the grids are arrow-navigable and every well Tab-reachable (10-accessibility.md).

Board popover

The window-title widget opens the board popover — the one board-level surface, hosting:

  • Board rename (settled: this function stays in-app, unlike the pathfinder which dropped it with the inspector). Rename edits the board's frontmatter title only — the folder is never renamed by the app; the Finder document name is Finder's to change (01-storage-format.md's board-naming rule). A foreign rename landing while the popover is open resyncs the field from the snapshot only while the field is unfocused — a focused field keeps the user's keystrokes, the dirty-buffer courtesy applied here (settled).
  • Board styling — the embedded style editor (Styling ▸ Controls above).
  • Git at a glance — Pro tier surface, mode-aware, display and daily operations only (re-ruled 2026-07-31 — setup moved to the board settings sheet below; in the free tier this section is absent on ordinary boards and reduces to the contextual one-line Pro pointer on boards carrying an inert .git — 12-editions.md): the posture lines (repo-nested explanation, unreadable-repo and paused states — 06), branch display with the switch picker, and on remote-backed boards remote tracking (ahead/behind) with Pull/Push controls and the status badges (Authentication needed, queued pushes, last error — the badge points at the sheet, capture happens there). A single-branch board's picker opens onto a disabled explanatory row (ruled 2026-08-06): with creation relocated to the sheet, the menu holds only the other local branches, and an empty menu reads as broken — a disabled "No other branches" row teaches both why the menu is empty and where creation went. A Board Settings… row opens the sheet — the popover's one setup affordance. The Pro mode-none posture is header plus that door (blessed 2026-08-06): with no repository the section stays — "Git" and the Board Settings… row alone — rather than vanishing, so the board's git story keeps its named place in the popover and the door stands exactly where a user looking for git will look.

Board settings sheet

The setup home (ruled 2026-07-31 — the popover/sheet split, 04-interactions.md's configuration carve-out): a board-scoped, titled, sectioned sheet on the board window, opened from the popover's Board Settings… row and from Board ▸ Board Settings… (11-command-nexus.md). It hosts everything setup-shaped: add-git (mode none; opt-in init — 06), add/change remote with the inline verify probe (07 ▸ Setup verifies right there), credentials — HTTPS username/token fields and the whole SSH surface (machine key Copy + Verify, key import by paste or drag — the sheet's stable frame is part of why it exists — the per-host key picker, unreferenced-import removal, confirm-gated machine-key regeneration), the TOFU first-connect confirm and mismatch block, commit identity name/email (06 — the visibility-scoped 2 s config re-read rides with the fields), branch creation (switching stays in the popover; create-and-switch runs 06's identical settle sequence from here), and push-on-commit. The mechanics that forced the split live comfortably here: confirmation alerts present over the sheet without dismissing the flow that owns them, network probes and their spinners survive focus changes, and typed-but-unverified credentials are never discarded by a stray click. Under the read-only lock the sheet's mutating controls disable in place (the Style-popover rule); every control is Tab-reachable and labeled (10-accessibility.md). Each control has exactly one home — the popover never duplicates a sheet control, the sheet never hosts the daily surface.

Trash

Resettled 2026-07-28 — the materialized trash. The tombstone model (a deleted: flag on items left in place, rendered by a pure-view quasi-lane) is retired: it generated a standing tax of nesting rules — ancestor walks, effective liveness, entry-vs-universe splits, kind-homogeneous selection — that this design replaces wholesale. Deletion is now a move: deleting a card moves its folder into <board-root>/.trash/, a reserved, materialized container (01-storage-format.md). A trashed card is an ordinary card in a special place — search, selection, rendering, styling, and clipboard all treat it exactly like any other card, and .trash/ is self-describing in Finder and to agents.

  • Lanes trash too (re-ruled 2026-07-29, retiring "cards only" and with it the design's sole destructive delete): deleting a lane moves its folder — subtree intact — into .trash/, exactly as a card moves; kind: lane in its frontmatter is what tells a trashed lane from a card in the flat container (01-storage-format.md ▸ Deletion), stamped on the way in when absent. The no-dialog posture survives for a better reason: the move is recoverable, so nothing needs confirming. Native undo's inverse is the ordinary move back (13-native-undo.md — the capture/recreate machinery retires). A trashed lane is an opaque unit: one distinct dimmed row showing its title and held-card count ("Doing — 5 cards"), no styling accents, never expandable; its cards are invisible to search and not individually addressable — it restores whole or purges whole. The row matches the search filter by lane title only. The held count is a load-time disk fact (pinned 2026-07-31): counted from a subtree the snapshot deliberately does not hold — the model's one number not derivable from the model — refreshed by reload like every snapshot field (its changes speak in the digest, 10-accessibility.md); the freight confirm's "…and its 5 cards" is thereby the design's one confirmation counting unrendered content, honest as of the latest reload — the opaque-unit trade, deliberate. Lane rows and cards interleave in the one trash column by modified descending (the trash's sort — Entry below).
  • Entry is always at the top — the trash sorts by modified descending (re-ruled 2026-07-31, retiring the arrival rank mint): every arrival, card or lane — ⌫/⌘⌫ delete and drag-to-trash alike — lands at the top because the move stamps modified (a container-changing move — 01's modified scope; deletion is an edit to the card's story), and that stamp is the position: newest-first with no order rewrite, no rank minting, the item's order key riding along untouched for its eventual restore. Ties break by title (case-insensitive), then folder name. Undated entries sort after every dated one (blessed 2026-07-31 — the comments rule's rung applied here: malformed sorts after dated siblings), then fall to the same tail. Whole-second stamps make batch deletes tie deliberately (blessed 2026-07-31): modified serializes at second granularity, so a multi-item delete bracket — and any two deletes inside one wall-clock second — carries identical stamps and orders by the tail; "newest first" reading as title-order within a second is the accepted consequence, not worth a schema-wide move to fractional stamps. The merged order is one derivation (both kinds interleaved — the column, the keyboard grammar, and the path resolver all read the same sequence; a second implementation of "the row below this one" is a bug by definition). The stamp is also what a future age-based auto-purge will read (deferred, 01-storage-format.md).
  • Rendering: trailing (rightmost) position when shown, visually distinct — dimmed/hatched header, trash SF Symbol, count badge — the badge counts rows (blessed 2026-07-31): its one invariant is matching what the column draws, so a trashed lane counts as one whatever its freight; card-level totals surface where consequences are decided — the freight confirm and the spoken accessibility value; no new-card button; not draggable, not resizable, excluded from lane reordering. Fixed one width unit, consumed only while shown; Show/Hide Trash is a re-divide trigger (Layout above). Visibility: hidden by default; View ▸ Show Trash toggles (no default chord — ⇧⌘T belongs to the system's Show Tab Bar, 11-command-nexus.md); per-open transient state, resets to hidden, never persisted. Hidden, the trash is invisible to every gesture and to search; shown, its cards participate in the filter exactly like any other card — the point of the pivot.
  • No Put Back (settled): the valuable item is the card (or lane); where it goes on the way out is the user's cheap decision. Restoring is an ordinary move out: drag a trash card into any lane at any position — or a trashed lane row to a lane-strip slot — or ⌘X in the trash, ⌘V — into a lane for cards, after the anchor lane for a trashed lane (04-interactions.md's lane-paste rule verbatim); the clipboard works on trash items like on any item, which is also the keyboard-native restore path (10-accessibility.md). Dropped on another board it follows the drag locality model (04-interactions.md).
  • No editing in the trash: trash cards don't open — double-click stops at selection; move it out first — and a trashed lane row never expands. Moving a card to the trash dismisses its open card window (05-card-window.md), and an external move-in observed by reload does the same; a lane entering the trash dismisses the open card windows of every card it carries (they entered the trash with it).
  • Permanent deletion: on a trash selection, Delete (⌫/⌘⌫) is permanent — one delete vocabulary, staged by place: on the board it moves to the trash, in the trash it removes the folder. Delete Immediately is deliberately absent (removed 2026-07-30): Finder's ⌥⌘⌫ answers disk-space pressure boards don't have, and it was the one gesture reaching unrecoverable straight from the board — permanence is only reachable inside the trash, where the staging makes the loss visible. The trash-side Delete confirms exactly where the loss is real (carried over): on boards without app-managed git history the alert stands between one keystroke and unrecoverable deletion; on git boards it acts immediately (delete-never-forgets). Confirms name the freight honestly — a trashed lane's alert counts its cards ("Permanently delete lane 'Doing' and its 5 cards"). Empty Trash… (⇧⌘⌫) confirms everywhere and purges the whole .trash/, lane subtrees walked, search-independent, the confirmation naming the full count ("Permanently delete 41 cards", "… 41 cards and 2 lanes containing 9 more cards" — 06-history-undo.md's plural folding); menu validation's "non-empty" reads .trash/, not the filtered view.
  • Every trash operation is an ordinary file operation — auto-committed and undoable on git boards; native undo restores a delete-to-trash in-session on any board (a trash move undoes as a move back — durable across relaunch, since it is just a move), while a permanent delete registers no step (13-native-undo.md: the confirm is the safety).
  • Naming constraint (carried over): attachment Remove moves files to the system Trash (05-card-window.md); board deletion says "Delete" and this container is "Trash" / "Show Trash" — Finder's "Move to Trash" phrasing stays reserved for the system Trash.
  • Materialized reserved lanes are a pattern, not a one-off: .trash is the first; an archive lane (<root>/.archive, intake criteria to be designed) is the anticipated second (WISHLIST) — same mechanics, ordinary cards in a reserved dot-named container, different entry semantics. Nothing beyond .trash is committed yet.

Welcome screen & templates

The welcome window carries over from the pathfinder unchanged — confirmed, it works well.

  • Welcome: resizable, no title bar (background drag); recents list with board icon, name, location, counts; single click selects, double click opens; context menu Open / Reveal in Finder / Forget.
  • Templates: New Board (⌥⌘N — ⌘N is new card; 11-command-nexus.md) opens a Pages-style chooser with a mini per-lane preview per template. Inventory and definition format: 09-templates.md.
  • File menu: Open Recent (with Clear Menu; available everywhere), and Duplicate (⇧⌘S) — board window only (11-command-nexus.md), duplicating the frontmost open board to a Finder-style "copy" sibling; it never acts on a welcome-selected recent. A sandbox refusal of the sibling write falls back to a save panel (settled — the board's security-scoped bookmark grants its subtree, not its parent, so the sibling destination may be unwritable): the silent Finder-style sibling is attempted first; on a permission refusal a save panel opens pre-filled with the parent folder and the "copy" name — the panel's grant is the sandbox's own answer, and it doubles as a choose-another-location affordance. Cancelling the panel cancels the duplicate quietly (no banner — the user declined, nothing failed); non-permission failures (disk full, …) keep the ordinary one-shot banner. The copy itself is cancellable (settled — 02-architecture.md's in-progress banner promises Cancel on copy-shaped work, and Duplicate honors it): the copy runs as a per-item file walk that checks cancellation between items — never one monolithic copyItem — and Cancel removes the partial sibling before dismissing the banner (the attachment partial-cleanup precedent): a cancelled duplicate never happened. The copy is preceded by the close flush (02-architecture.md ▸ Windows; the rule and its Edit-session exception are stated at 09-templates.md ▸ Save as Template), so neither the tree nor the copied history misses pending work; under the read-only lock Duplicate disables in every state (02-architecture.md — the flush can't run and the sibling destination shares the board's fate). The duplicate opens in its own board window once copied — macOS Duplicate convention; the original stays open too. On a git board, the duplicate keeps .git but has its remote configuration stripped — remotes only: the repo-local user.name/user.email (06-history-undo.md's identity home) survives, so the fork keeps its commit identity. The copy keeps every GUID — a whole-board copy is 01-storage-format.md's explicit carve-out from the copies-remint rule (a new identity namespace, no collision possible), and keeping them is what keeps the copied history true: its commits name paths that still exist. The trash is carried too (settled, re-grounded 2026-07-28): Duplicate is a full fork, .trash/ included — dropping it would leave the copy's working tree disagreeing with its own copied HEAD (the trash folders are tracked), where keeping it means the duplicate is born exactly matching its history; Empty Trash in the copy is one command away. Save as Template makes the opposite choice — a template isn't a fork (09-templates.md). A fork of the board keeps its history (undo trail, delete-never-forgets — and it opens straight in git mode via 06-history-undo.md's adoption rule), but it must not silently push into the original's remote — sharing stays a deliberate per-board opt-in. (Push-on-commit lives app-side in the board registry and never carries to a new board path anyway.)

Editing surfaces summary

What Where
Card title Inline rename on the face; card window title field
Card body Card window (05-card-window.md)
Lane title Inline rename on the header
Colors / icons The style editor — card sidebar Style section (05-card-window.md), board popover, or Style… (context menu / Board ▸ Style…)
Lane width Edge drag + header context-menu stepper + Increase/Decrease Lane Width
Board title, board styling Board popover
Board/lane descriptions (bodies) File-only — hand-edit index.md; live-reload reflects it

Motion

The pathfinder's animation behavior carries over as the committed motion language — it grew call-site by call-site but is disciplined enough to state as rules. The rewrite gives the vocabulary the one thing the pathfinder lacked: a single named home (one animation-constants surface), so curves and durations stop being per-site literals.

  • Two curves, semantically split. Snappy springs are the structural/positional voice: drag reflow (~0.18 s), drop commit, paste, and delete (~0.25 s), keyboard nudges, scroll-into-view, and lane resize (~0.2 s). A smooth spring (~0.28 s) is the content-reflow voice: search filtering and undo/redo restore, deliberately paired so a restore reads like the search filter — leavers and arrivers run their transition, survivors reflow under one gentle spring. Named system presets only; no hand-tuned spring parameters.
  • User-initiated structural changes animate; foreign changes snap. Everything the user does through the app — drag, delete, paste, nudge, resize, search — lands in an animated transaction regardless of entry point (the pathfinder enforced this by routing every deletion/move through the same animated store methods; keep that shape). Changes arriving through the watcher — agent edits, hand edits, sync, external git — apply instantly with no transition: live-reload is the board becoming what's on disk, not an event to perform. The one deliberate crossover is app-initiated undo/redo restore — a git checkout, but ours, so it animates in the search-filter language (06-history-undo.md).
  • Equivalent operations share one dialect. Paste animates exactly like a drop commit (same curve, same duration) so the clipboard's move story feels like drag landing; keyboard one-slot moves slide for the same reason a drop does — an item that teleports is harder to follow than one that slides; cut dims the card in place, Finder-style, until paste moves it (04-interactions.md).
  • Appear/disappear is scale + fade (cards scale from ~0.8, lanes ~0.9, combined with opacity). A restore that moves a card across lanes flies it from old frame to new via matched geometry. Search-hiding rides the same structural transition — hiding is removal, not a special fade.
  • Some things deliberately never animate: the rubber-band marquee tracks the cursor 1:1 (an eased band visibly lags the mouse), and the selection highlight rides whatever transaction is active rather than easing on its own.
  • Animated transactions are keyed narrowly — on the search query (filter reflow) and on the drag's drop proposal (the reflow-to-make-room above animates under it, ~0.18 s) — never on broad state like the selection set (selection changes styling only, never geometry — Card face above, the no-carousel resettlement). What stays animation-free by construction rather than by suppression: the drag replica's tracking and the marquee rectangle (1:1 cursor following — animating input echo would be lag), and multi-select churn. The replica's bracketing transitions do animate (settled): the pickup lift (scale + shadow as it detaches from the card) and the cancel fly-back are the system drag session's own behaviors and match the spec verbatim; only the tracking between them is verbatim input echo. The drop settle is the board's, not the replica's (resettled 2026-07-28 — drags are system NSItemProvider sessions, required for cross-board transfer and the copy badge, and a successful drop's drag image has no fly-to-slot hook, only AppKit's brief fade): at release the held overlay (below) renders the dropped arrangement instantly while the system fade dissolves the drag image over it — the item is in its slot the moment the mouse releases, which is the promise that matters. A custom fly-to-slot animator (shadow-window replica, masked system fade) remains a deliberate later upgrade, not a commitment (WISHLIST). The settle holds the drop proposal until the echo lands (settled — the one-way flow means the write is still in flight at release, and a snapshot-order re-render would glide the dragged item back before the reload animates it forward again): the proposal survives release as overlay state in the app-wide DragSession (02-architecture.md — the placeholder's kin in semantics; app-wide in home because a drag crosses boards), the board keeps rendering the proposed arrangement under the system fade (the drop settle above) — the release presentation is an open question (reopened 2026-07-28): the first treatment — swapping the shadow for the dropped card(s) drawn in place immediately at release — was implemented and backed out on user review; the pause between release and the card's appearance still wants a designed answer, revisited separately (Redesign board ▸ Issues to Resolve). Until then the shadow holds through the gap and the card appears at the echo — and the proposal discards itself when the bracket's echo reload lands — positions already match, so the handoff moves nothing. A failed write discards the proposal and the board animates back to snapshot order with the ordinary one-shot banner — the width-drag rollback posture (the action visibly doesn't happen); a foreign reload that vanishes the dragged item discards it too (02-architecture.md's constraint rule).
  • Motion never feeds back into logic (the pathfinder's animation-proof-inputs rule, kept as a hard constraint): drop-proposal math reads analytically computed resting zones, the physical mouse position, and item sizes frozen at drag start — never mid-flight measured frames, which are garbage precisely during the ~0.2 s reflow they trigger.
  • Reduce Motion is a rewrite obligation, not an inheritance: the pathfinder ships zero reduced variants; 10-accessibility.md's commitments (crossfade or instant for reflow, search animate-out, the drag replica's lift and settle, the lane-resize rubber-band feedback, trash) are new work.

Changes from Kanban

  • Terminology: lane throughout the UI (menus, dialogs, agent guide).
  • Lane and card colors render as edge accents (C7 top edge / K1 left stripe — Styling above), fixed rather than the pathfinder's configurable treatment matrix; the pathfinder's full-surface painting is gone.
  • The motion language is named and centralized (Motion above); Reduce Motion variants are new work the pathfinder never had.

Open questions

  • Board rename is settled (board popover, above). Board/lane descriptions (bodies) remain file-only — keep that stance, or does the card window pattern (a "lane window"?) deserve to exist for them? Current lean: keep file-only; descriptions are edited rarely.