diff --git a/DESIGN/02-architecture.md b/DESIGN/02-architecture.md index cd1db01..df8a1d2 100644 --- a/DESIGN/02-architecture.md +++ b/DESIGN/02-architecture.md @@ -90,7 +90,7 @@ State that belongs to the app, not the user's files — the recents list, per-bo - **Recents counts are registry-cached.** The lane/card counts in the welcome window come from the record, stamped at last close — no directory scan at welcome time (which would be slow or hang on big/unavailable boards). Staleness until the next open is accepted. Records that can't be counted show without counts: unavailable boards per Graceful orphaning below; a board that fails to load just fails on open, fail-fast — the welcome row doesn't pre-detect it. - **Files-first stays absolute**: nothing app-private is ever written into the board folder — no frontmatter keys, no sidecar files, no xattrs. Two machines sharing a board via a remote each keep their own record (push-on-commit and window frames are genuinely per-machine choices). - **Graceful orphaning**: a record whose bookmark no longer resolves (board deleted, or moved across volumes where bookmarks can't follow) is orphaned — recents surface it as unavailable with Forget; its settings are conveniences and die with it (accepted). -- **App-wide state has the same home.** Not everything app-side is board-scoped: quick-style recents (03-board-ui.md), the SSH host-key assignment table and TOFU fingerprint store (07-sync-collab.md — host-scoped), the last-used card-window size (05-card-window.md), and peers live beside the registry in Application Support (or `UserDefaults` where a scalar fits) — no per-board record involved. Secrets are the named exception: Keychain only, never here (07). +- **App-wide state has the same home.** Not everything app-side is board-scoped: quick-style recents (03-board-ui.md), the SSH host-key assignment table and TOFU fingerprint store (07-sync-collab.md — host-scoped), the last-used card-window size (05-card-window.md), and their peers live beside the registry in Application Support (or `UserDefaults` where a scalar fits) — no per-board record involved. Secrets are the named exception: Keychain only, never here (07). ## Caching and search diff --git a/DESIGN/03-board-ui.md b/DESIGN/03-board-ui.md index c3d6b7a..f8398fb 100644 --- a/DESIGN/03-board-ui.md +++ b/DESIGN/03-board-ui.md @@ -87,7 +87,7 @@ The welcome window carries over from the pathfinder unchanged — confirmed, it | 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 picker | +| 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 | @@ -102,7 +102,7 @@ The pathfinder's animation behavior carries over as the committed motion languag - **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 sole-selected card (carousel expansion), on the search query (filter reflow), 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. What stays animation-free by construction rather than by suppression: the drag replica and the marquee rectangle (1:1 cursor tracking — animating input echo would be lag), and multi-select churn. - **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, trash) are new work. +- **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, the lane-resize rubber-band feedback, trash) are new work. ## Changes from Kanban diff --git a/DESIGN/04-interactions.md b/DESIGN/04-interactions.md index da1063f..f0255ce 100644 --- a/DESIGN/04-interactions.md +++ b/DESIGN/04-interactions.md @@ -4,7 +4,7 @@ Selection, drag & drop, keyboard, clipboard, search. This is where the old app s ## Selection -- Cards: click selects; cmd-click toggles; shift-click range-extends; click-drag rubber-bands across lanes. Lanes: cmd/shift-click multi-select. +- Cards: click selects; ⌘-click toggles; ⇧-click range-extends; click-drag rubber-bands across lanes. Lanes: ⌘/⇧-click multi-select. - Selection is **homogeneous**: cards XOR lanes. - Lane empty-space: single click selects the lane (click again to unselect); double click creates a card at the bottom, title editor focused. - **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). @@ -48,7 +48,7 @@ Selection, drag & drop, keyboard, clipboard, search. This is where the old app s 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: 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. +- **⌥⌘↑/⌥⌘↓ sort within the lane** (the move-vs-jump question, resettled: *card* moves live on the ⌥⌘ chord, joining ⌥⌘←/⌥⌘→ lane width in a "⌥⌘ modifies" family; plain ⌥-arrows stay jumps; plain ⌘↑/⌘↓ are unassigned): the selected card(s) move one position within the lane — logical `order`, across interior masonry columns (10-accessibility.md's logical-order rule). A non-contiguous multi-selection **gathers on the first press**: the cards collect into a contiguous block anchored at the first selected card (first = lowest logical order; the rest follow in preserved relative order), and subsequent presses move the block one position. **Cards never change lanes by ⌘-arrow** (settled): inter-lane movement is drag or Cut/Paste (the clipboard rules above), so ⌥⌘↑/⌥⌘↓ disable when a card selection spans lanes and ⌘←/⌘→ are inert on card selections. With a **lane** selected, ⌘←/⌘→ move the lane one slot — closing 10-accessibility.md's lane-move defect — and ⌥⌘↑/⌥⌘↓ are inert. - **⌫/⌘⌫ delete** (unchanged): tombstone into the trash quasi-lane (03-board-ui.md); lanes included, no dialog. Selection moves to the deleted item's successor sibling, Finder-style (next card in the lane, next lane on the board; the last sibling's predecessor otherwise; empty container = nothing selected) — repeated ⌫ walks down a lane. Deliberate deletes pick a successor; *external* vanishing never does (02-architecture.md's reload-survival rule: the selection just shrinks). On a **tombstoned** selection ⌘⌫ is **Put Back** instead — Finder's exact symmetry (⌘⌫ trashes and un-trashes). The dual role is carried by **twin menu items sharing the chord** — File ▸ Delete ⌘⌫ and File ▸ Put Back ⌘⌫, validation enabling exactly one by selection state; AppKit routes a shared key equivalent to the enabled item (Finder ships this exact pair as Move to Trash/Put Back; ours says Delete per 03-board-ui.md's naming constraint). Both titles stay stable (titles-are-API), and each is independently remappable — remapping one never moves the other's role. Plain ⌫ performs the same tombstone as fixed grammar (see Grammar above) — there is no Edit ▸ Delete item, so the two Delete-titled homes never collide for title-matched remapping. - **Select All**: all visible cards on the board — filter-respecting, like every surface (Search below). - **The contract's one carve-out is configuration** (settled): form-like git and board setup — add git, add/change remote, branch switching and creation, commit identity, credentials — lives in the board popover only, its committed home; its keyboard path is Board Info (⌘I) plus Tab-reachable controls (10-accessibility.md's Full Keyboard Access). Recurring remote *operations* stay under the contract: Board ▸ Pull and Board ▸ Push are menu items (no default chord, remappable; validation enables them only on remote-backed boards — 07-sync-collab.md). @@ -68,7 +68,7 @@ The trash quasi-lane (03-board-ui.md ▸ Trash) speaks the same keyboard languag ### 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: +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.) diff --git a/DESIGN/06-history-undo.md b/DESIGN/06-history-undo.md index 33ea086..6740910 100644 --- a/DESIGN/06-history-undo.md +++ b/DESIGN/06-history-undo.md @@ -25,7 +25,7 @@ Git is the undo substrate — on boards that have git. **Git is opt-in per board The pathfinder's message engine carries over as the model — it is what earns the "semantic" in semantic commit messages, and it stays a pure, testable function: - **Pure snapshot diff, no write-site tagging.** Messages compose at commit time from a structural diff of two board snapshots (last-committed vs. current) — never by intercepting operations. Items match by id across the *whole* board, so a lane change is distinguishable from delete+add and a cross-lane move reads as a move. Bookkeeping — `order` changes that preserve sibling sequence (a renumber's rescale — 01-storage-format.md), `modified`/`created` — produces no events: a diff touching only those composes nothing. Sequence is what the diff compares, not raw `order` values: an order change that *repositions* an item among its siblings composes Reorder, so a foreign writer's single-file reorder still reads as one. A midpoint-exhaustion renumber batches with the insert or move that triggered it, so its commit reads as that event. -- **Vocabulary**: Add / Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes, and the board, plus Attach / Remove for attachment files ("Move card 'Fix login' to Doing", "Rename lane 'Todo' → 'Doing'"). One commit per window: a single event is the subject (with a detail body where one helps); several events of one kind fold into a plural subject, with shared destinations preserved ("Move 3 cards to Done"); genuinely mixed windows fall back to "Update board" — always with a bulleted body naming every event, so the oneline log stays scannable and the full message stays complete. +- **Vocabulary**: Add / Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes, and the board, plus Attach / Remove for attachment files ("Move card 'Fix login' to Doing", "Rename lane 'Todo' → 'Doing'"). One commit per debounce window: a single event is the subject (with a detail body where one helps); several events of one kind fold into a plural subject, with shared destinations preserved ("Move 3 cards to Done"); genuinely mixed windows fall back to "Update board" — always with a bulleted body naming every event, so the oneline log stays scannable and the full message stays complete. - **Implied events don't steal the subject**: deleting a lane with five cards reads "Delete lane 'X'" with the card deletions as body bullets — not "Update board". - **Titles truncate in subjects only** (~40 chars, keeping `git log --oneline` sane); body lines carry full titles. An untitled item reads "(untitled)" — never a bare `""` (a pathfinder edge fixed, not carried). Undo/redo restores commit as "Undo: ⟨subject⟩" / "Redo: ⟨subject⟩"; the undo-menu labels are the *crossed* commit's subject, so labels never nest. diff --git a/DESIGN/10-accessibility.md b/DESIGN/10-accessibility.md index cfa5f34..b6a3f0d 100644 --- a/DESIGN/10-accessibility.md +++ b/DESIGN/10-accessibility.md @@ -22,7 +22,7 @@ The stance is committed in 00-vision.md: **accessibility is a requirement of "na - **Cards**: clipboard. ⌘X the selection, move selection to the destination (arrows), ⌘V — between lanes, within a lane (paste lands after the anchor card), and across boards (04-interactions.md's staged clipboard). This is the committed drag-free move story; it needs no VoiceOver-specific machinery because selection and paste targeting are already keyboard-native. - **Lanes**: the defect this doc originally named (lanes had no keyboard-move path) is closed by the keyboard map — with a lane selected, ⌘←/⌘→ move it (Board ▸ Move Left / Move Right, 04-interactions.md); cards gain ⌥⌘↑/⌥⌘↓ within-lane sorting, and cross lanes drag-free via cut/paste (04-interactions.md's clipboard rules). Lanes carry the clipboard too (resettled, 04-interactions.md ▸ Clipboard), so cross-board lane copy/move — once drag-only, the contract's last gap — is ⌘C/⌘X, then ⌘V with the destination board frontmost. -- **Lane resize**: the header context menu's width stepper (03-board-ui.md) — and its keyboard face, the Increase/Decrease Lane Width menu items (⌥⌘→/⌥⌘←, 04-interactions.md) — is the accessible path; edge drag is enhancement only. +- **Lane resize**: the header context menu's width stepper (03-board-ui.md) — and its keyboard face, the Increase/Decrease Lane Width menu items (⌥⌘→/⌥⌘←, 11-command-nexus.md) — is the accessible path; edge drag is enhancement only. - **Attachments**: the card window's sidebar items expose Open / Reveal in Finder / Remove via context menu, and the section is keyboard-navigable outright (arrows, Space-QuickLook, Return, ⌫ — 05-card-window.md); adding files drag-free is File ▸ Add Attachment… (⇧⌘A, 11-command-nexus.md) alongside Finder-drop. ## Live board announcements @@ -44,7 +44,7 @@ The stance is committed in 00-vision.md: **accessibility is a requirement of "na - **Full relative scaling** (decided): relative text styles everywhere, no fixed point sizes. Card face, lane header, and masonry metrics derive from font metrics, so layout survives the largest system text sizes; the no-horizontal-scroll invariant is untouched (lane count is the user's choice; lanes scroll vertically), and 03-board-ui.md's graceful-truncation rules apply at every scale. - **Contrast is pinned to WCAG AA.** Lane and card colors render as edge accents (03-board-ui.md's top-edge band / left-edge stripe), so text never sits on them — they are supplementary decoration, never the sole carrier of information, and carry no text-contrast obligation. The ≥ 4.5:1 automatic-contrast rule binds where text does sit on a user-chosen color: the **board** background (palette pairs verified at design time; arbitrary hex computes its text color at runtime against that threshold). An `#RRGGBBAA` background with alpha computes against the color **composited over its effective backdrop** in the active appearance (the board's over the window background; light and dark resolve differently), recomputed on appearance change. Increase Contrast strengthens borders and the selection indicator. - **State is never color-alone**: selection is a ring plus trait, cut-pending is dim plus stated value, the trash header is hatched plus labeled — all already patterned; kept as a rule. -- **Reduce Motion**: reflow-on-drag, search animate-out, the drag replica, rubber-band feedback, and trash animations all get reduced variants (crossfade or instant). **Reduce Transparency**: glass underlays (carousel page dots) go solid. +- **Reduce Motion**: reflow-on-drag, search animate-out, the drag replica, the lane-resize rubber-band feedback (03-board-ui.md ▸ Lane — the selection marquee never animates and needs no variant), and trash animations all get reduced variants (crossfade or instant). **Reduce Transparency**: glass underlays (carousel page dots) go solid. - **Full Keyboard Access** (independent of VoiceOver): the board is one tab stop with arrow-key navigation within; every control — lane buttons, popover, card window, welcome — is Tab-reachable. ## Verification @@ -58,4 +58,4 @@ The stance is committed in 00-vision.md: **accessibility is a requirement of "na ## Open questions -None currently — the lane-move gap and the Add Attachment menu path are both closed by the keyboard map (04-interactions.md). +None currently — the lane-move gap and the Add Attachment menu path are both closed by the keyboard map (04-interactions.md's contract, inventoried in 11-command-nexus.md). diff --git a/DESIGN/11-command-nexus.md b/DESIGN/11-command-nexus.md index 09f5651..7e117cf 100644 --- a/DESIGN/11-command-nexus.md +++ b/DESIGN/11-command-nexus.md @@ -31,7 +31,7 @@ The single source of truth for **every command and action the app can perform** | File | Delete | ⌘⌫ | Board window, live selection (chord twin of Put Back — validation enables exactly one). Deliberately **not** extended to the card window: an enabled ⌘⌫ key equivalent would steal delete-to-line-start from the window's text surfaces, so there the card's delete is the sidebar Actions button (05) | | File | Put Back | ⌘⌫ | Board window, tombstoned selection (chord twin of Delete) | | File | Delete Immediately | ⌥⌘⌫ | Board window, tombstoned selection; confirmed on boards without git history (mode none / repo-nested), immediate on git boards — 03 ▸ Trash | -| File | Empty Trash… (confirmed) | ⇧⌘⌫ | Board window, trash non-empty | +| File | Empty Trash… (confirmed) | ⇧⌘⌫ | Board window, trash shown and non-empty | | File | Close | ⌘W | Any window; flushes per 02 ▸ Windows | | Edit | Undo / Redo (M−) | ⌘Z / ⇧⌘Z | Focus-routed (06 ▸ Undo routing): text undo in a focused editor, git undo otherwise; git undo disabled on no-git and repo-nested boards | | Edit | Cut / Copy / Paste | ⌘X / ⌘C / ⌘V | Board window: cards and lanes (cards-XOR-lanes selections; lane paste lands after the anchor lane — 04 ▸ Clipboard; on a zero-lane board only a lane payload pastes — 04 ▸ ⌘N target rule); in the trash, ⌘C copy-out only (card and lane entries), ⌘X disabled (04 ▸ The trash); text editors: standard text clipboard |