Batch of eleven Minor design-review fixes across the corpus

- 02: welcome counts are live items only; failed first opens still record;
  duplicate-identity registry records merge silently (newest wins);
  BoardStore's FolderWatcher pointer reads "below"
- 11: lane-header new-card button inventoried (click names its target lane,
  overriding 04's ⌘N rule — cross-referenced there); attachments quiet-add
  affordance listed as a twin; Undo/Redo and Pull/Push rows gain the
  abnormal-state pause, read-only lock, and unresolvable-remote conditions;
  welcome scope added to Reveal in Finder
- 03/11: one lane context menu (header or empty space) with the full
  inventory, replacing the split rows
- 09: instantiated boards are never in git mode — actual mode follows
  detection at the destination (repo-nested possible)
- 08: the app never writes an existing CLAUDE.user.md; one-time rescue
  creation stated as the exception
- README: pathfinder path corrected to ../../Kanban

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
This commit is contained in:
2026-07-26 21:09:23 -04:00
parent 23e761120f
commit adfe87fdec
7 changed files with 13 additions and 12 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ The **one named exception** is transient UI state rendering things that don't ex
- **BoardLoader** — walks the folder tree, applies the fail-fast/skip rules, produces an immutable `BoardModel` snapshot. Pure function of the tree. - **BoardLoader** — walks the folder tree, applies the fail-fast/skip rules, produces an immutable `BoardModel` snapshot. Pure function of the tree.
- **BoardWriter** — every mutation (create, move, reorder, tombstone, style) as an explicit filesystem operation. No hidden state; a write is done when the file is on disk. (The EchoLedger's receipts are not this bullet's "hidden state": a receipt describes a *completed* write, and the ledger lives beside the Writer, not in it — no write is ever pending in memory.) - **BoardWriter** — every mutation (create, move, reorder, tombstone, style) as an explicit filesystem operation. No hidden state; a write is done when the file is on disk. (The EchoLedger's receipts are not this bullet's "hidden state": a receipt describes a *completed* write, and the ledger lives beside the Writer, not in it — no write is ever pending in memory.)
- **EchoLedger** — the write-provenance ledger (the "Writer/echo machinery" that 06-history-undo.md ▸ Commit attribution and 10-accessibility.md ▸ Live board announcements consume; settled). Every BoardWriter operation drops a receipt of its expected on-disk outcome before returning: path → content hash for writes (attachment imports hash during the copy — the bytes stream through the app anyway), an absence marker for deletes, an old→new pair for folder moves; a newer app write to the same path supersedes the receipt. Classification runs per observed changed file in a debounce window: current on-disk content matches the receipt → **app-mediated**, receipt consumed; no receipt, or mismatch → **foreign**. Final content deciding is what settles the races: an agent writing byte-identical bytes over a fresh app write matches and classifies app-mediated — with identical bytes the misattribution is unobservable in the tree, accepted; a foreign edit landing on an app-written path inside the same window misses the hash and the file classifies foreign — last writer wins the file, the app's subsumed intermediate never separately recorded (the diff compares snapshots, not a journal — 06-history-undo.md). Consumers: the auto-committer's author field and two-commit split (06), and the announcement filter (10) — on no-git boards the ledger runs identically with the announcer as its only consumer. **In-memory, per-store, dies with the session** — losing it costs attribution and nothing else, so the launch catch-up commit (06) classifies everything foreign: the app never vouches for changes it didn't witness. Bracketed operations don't consult it (they commit themselves and announce once at completion), and the reload-granularity origin tag (Live-reload resilience below) is orthogonal: it classifies *reloads*, the ledger classifies *files*. Feeds attribution and announcements only — never the render path (Layering above). - **EchoLedger** — the write-provenance ledger (the "Writer/echo machinery" that 06-history-undo.md ▸ Commit attribution and 10-accessibility.md ▸ Live board announcements consume; settled). Every BoardWriter operation drops a receipt of its expected on-disk outcome before returning: path → content hash for writes (attachment imports hash during the copy — the bytes stream through the app anyway), an absence marker for deletes, an old→new pair for folder moves; a newer app write to the same path supersedes the receipt. Classification runs per observed changed file in a debounce window: current on-disk content matches the receipt → **app-mediated**, receipt consumed; no receipt, or mismatch → **foreign**. Final content deciding is what settles the races: an agent writing byte-identical bytes over a fresh app write matches and classifies app-mediated — with identical bytes the misattribution is unobservable in the tree, accepted; a foreign edit landing on an app-written path inside the same window misses the hash and the file classifies foreign — last writer wins the file, the app's subsumed intermediate never separately recorded (the diff compares snapshots, not a journal — 06-history-undo.md). Consumers: the auto-committer's author field and two-commit split (06), and the announcement filter (10) — on no-git boards the ledger runs identically with the announcer as its only consumer. **In-memory, per-store, dies with the session** — losing it costs attribution and nothing else, so the launch catch-up commit (06) classifies everything foreign: the app never vouches for changes it didn't witness. Bracketed operations don't consult it (they commit themselves and announce once at completion), and the reload-granularity origin tag (Live-reload resilience below) is orthogonal: it classifies *reloads*, the ledger classifies *files*. Feeds attribution and announcements only — never the render path (Layering above).
- **BoardStore** — per-board `@Observable` object holding the current snapshot plus transient UI state that must be shared across that board's windows (TransientBoardState — see Changes from Kanban). Coalesces watcher reloads — at most one tree walk in flight, signals landing mid-walk fold into one follow-up; the debounce itself lives in FolderWatcher (above). - **BoardStore** — per-board `@Observable` object holding the current snapshot plus transient UI state that must be shared across that board's windows (TransientBoardState — see Changes from Kanban). Coalesces watcher reloads — at most one tree walk in flight, signals landing mid-walk fold into one follow-up; the debounce itself lives in FolderWatcher (below).
- **BoardStoreRegistry** — refcounted registry so a board window and its card windows share one live store and one watcher. **The board window owns the board** (settled): card windows never outlive it — closing the board window closes its card windows too, so the last-window teardown and board-window close coincide. (The refcount still earns its keep ordering teardown while multiple windows close.) - **BoardStoreRegistry** — refcounted registry so a board window and its card windows share one live store and one watcher. **The board window owns the board** (settled): card windows never outlive it — closing the board window closes its card windows too, so the last-window teardown and board-window close coincide. (The refcount still earns its keep ordering teardown while multiple windows close.)
- **FolderWatcher** — FSEvents (debounced: **200 ms trailing**, the timer restarting per event so a burst yields one reload after quiet, over 50 ms FSEvents latency — settled numbers), attached best-effort to whatever path the board lives at. **Events under any `.git` path component are filtered out** (settled): the board's own root-level repo (a worktree-link `.git` file included) is the app's auto-commit churn, and a repo nested deeper — a card folder containing a clone, a submodule — is a stray (01-storage-format.md) whose internals never render; neither can alter the rendered tree, so neither drives reloads. (A nested repo's *working files* still fire events like any stray's — those reloads are value-equal and quiet.) There is only this one watching path: no NSMetadataQuery for iCloud Drive, no polling fallback for network volumes — on those warned-against locations (07-sync-collab.md) FSEvents delivery is unreliable and live reload silently degrades, accepted per 07's no-accommodations stance. - **FolderWatcher** — FSEvents (debounced: **200 ms trailing**, the timer restarting per event so a burst yields one reload after quiet, over 50 ms FSEvents latency — settled numbers), attached best-effort to whatever path the board lives at. **Events under any `.git` path component are filtered out** (settled): the board's own root-level repo (a worktree-link `.git` file included) is the app's auto-commit churn, and a repo nested deeper — a card folder containing a clone, a submodule — is a stray (01-storage-format.md) whose internals never render; neither can alter the rendered tree, so neither drives reloads. (A nested repo's *working files* still fire events like any stray's — those reloads are value-equal and quiet.) There is only this one watching path: no NSMetadataQuery for iCloud Drive, no polling fallback for network volumes — on those warned-against locations (07-sync-collab.md) FSEvents delivery is unreliable and live reload silently degrades, accepted per 07's no-accommodations stance.
- **Ranks** — gapped fractional ordering math + compaction. Pure. - **Ranks** — gapped fractional ordering math + compaction. Pure.
@@ -90,7 +90,7 @@ The non-modal banner named throughout the read- and write-side rules above is on
State that belongs to the app, not the user's files — the recents list, per-board window frames, the open-now restoration flag (Launch and window lifecycle above), the push-on-commit setting and the once-per-board iCloud warning flag (07-sync-collab.md), and whatever accumulates later — lives in a **board registry in Application Support**: one record per known board, anchored by the **security-scoped bookmark** the sandboxed app keeps anyway for reopening boards. State that belongs to the app, not the user's files — the recents list, per-board window frames, the open-now restoration flag (Launch and window lifecycle above), the push-on-commit setting and the once-per-board iCloud warning flag (07-sync-collab.md), and whatever accumulates later — lives in a **board registry in Application Support**: one record per known board, anchored by the **security-scoped bookmark** the sandboxed app keeps anyway for reopening boards.
- **Keyed by file identity, never by path.** Bookmarks track renames and moves on the same volume; an opened URL is matched to its record by bookmark resolution / file identity, so a moved board keeps its settings. The recents list *is* this registry sorted by last-opened. - **Keyed by file identity, never by path.** Bookmarks track renames and moves on the same volume; an opened URL is matched to its record by bookmark resolution / file identity, so a moved board keeps its settings. The recents list *is* this registry sorted by last-opened.
- **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. - **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. **The counts are live items only** (settled): tombstoned lanes and cards — and cards hidden beneath a tombstoned lane — don't count; the row advertises the board's working size, and the trash is an errand, not inventory. 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. **A first open that fails fail-fast still records** (settled): the registry record is created before loading, so the failed board lands in recents carrying fail-fast's specifics — retry after fixing the file is one click, uniform with the restored-board failure row, and Forget is the eraser for a genuinely mistaken open. **Records that collapse onto one file identity merge silently** (settled — a restored registry file, or an orphan whose bookmark re-resolves onto a recreated board): on detection, the record with the newest `lastOpened` wins wholesale and the others retire — recents never shows one board twice, and per-board settings are conveniences that don't earn a merge UI.
- **The row's title and icon are registry-cached too — with live write-through** (settled): the record carries the board's `title`, `icon`, and `iconColor` beside the counts, and the welcome row reads only the record — it never opens any board's `index.md` (the same hang-avoidance that motivated the counts rule). Unlike the counts' at-close stamp, these three refresh **whenever an open board's reload changes them**: the store already holds the new snapshot, so an in-app Board rename (03-board-ui.md) lands in the record instantly — never a welcome row showing a name the user just changed away from — and a foreign rename of an *open* board rides the same path for free. The honest residual: renaming a board that isn't open (an agent editing its root `index.md`) stays stale until the next open — accepted, the counts' staleness class. The title falls back to the folder name per 01-storage-format.md, cached at the same moments. - **The row's title and icon are registry-cached too — with live write-through** (settled): the record carries the board's `title`, `icon`, and `iconColor` beside the counts, and the welcome row reads only the record — it never opens any board's `index.md` (the same hang-avoidance that motivated the counts rule). Unlike the counts' at-close stamp, these three refresh **whenever an open board's reload changes them**: the store already holds the new snapshot, so an in-app Board rename (03-board-ui.md) lands in the record instantly — never a welcome row showing a name the user just changed away from — and a foreign rename of an *open* board rides the same path for free. The honest residual: renaming a board that isn't open (an agent editing its root `index.md`) stays stale until the next open — accepted, the counts' staleness class. The title falls back to the folder name per 01-storage-format.md, cached at the same moments.
- **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). - **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). - **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).
+1 -1
View File
@@ -17,7 +17,7 @@ Toolbars are **pure enhancement**: every function they host already has a menu i
## Lane ## 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 count reads the search filter like every other surface (04-interactions.md): during a search it shows the visible count, not the total. - 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. - 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. - 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.
+1 -1
View File
@@ -52,7 +52,7 @@ Every command is a menu item. The full inventory — every command and action, i
- **⌫/⌘⌫ 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. - **⌫/⌘⌫ 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). - **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). - **The contract's one carve-out is configuration** (settled): form-like git and board setup — add git, add/change remote, branch switching and creation, commit identity, credentials — lives in the board popover only, its committed home; its keyboard path is Board Info (⌘I) plus Tab-reachable controls (10-accessibility.md's Full Keyboard Access). Recurring remote *operations* stay under the contract: Board ▸ Pull and Board ▸ Push are menu items (no default chord, remappable; validation enables them only on remote-backed boards — 07-sync-collab.md).
- **⌘N target rule** (settled): with a card selected, the new card is created in that card's lane, immediately after it (paste-anchor consistency); with a lane selected, appended at its bottom (Return consistency); with nothing selected — or a **tombstoned** selection, which never anchors creation — the **last-active lane** — the lane that most recently held selection or a creation in this window session — falling back to the first lane. Title editor focused; same placeholder/abandon semantics as Return-creation. **Zero-lane board** (hand-made, or every lane deleted): card creation and card paste have no target — New Card, Return-creation, and Paste with a *card* payload disable via menu validation until a lane exists. New Lane (⇧⌘N) is one way in; Paste with a **lane** payload is the other — it stays enabled and lands at the board's right end (the lane-paste rule above), so cross-board structure transfer never needs a lane to exist first. - **⌘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); 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 **tombstoned** selection, which never anchors creation — the **last-active lane** — the lane that most recently held selection or a creation in this window session — falling back to the first lane. Title editor focused; same placeholder/abandon semantics as Return-creation. **Zero-lane board** (hand-made, or every lane deleted): card creation and card paste have no target — New Card, Return-creation, and Paste with a *card* payload disable via menu validation until a lane exists. New Lane (⇧⌘N) is one way in; Paste with a **lane** payload is the other — it stays enabled and lands at the board's right end (the lane-paste rule above), so cross-board structure transfer never needs a lane to exist first.
### The trash, keyboard-first (settled) ### The trash, keyboard-first (settled)
+1 -1
View File
@@ -26,7 +26,7 @@ Mechanics: version marker in the first line (`lanework-agent-guide vN`); rewritt
**Ownership**: the board-root `CLAUDE.md` is **app-owned and not available for user editing** — its header says so, and user edits do not survive upgrades. This holds on every board, including repo-nested ones: the guide is auto-written there too (the untracked file in the user's repository is accepted — a board is agent-facing wherever it lives). A board-root `CLAUDE.md` found *without* the marker is displaced user content, not clobbered: its content is moved to `CLAUDE.user.md` if that name is free (otherwise the guide write is skipped with a log — user content is never destroyed), and the guide is then written. **Ownership**: the board-root `CLAUDE.md` is **app-owned and not available for user editing** — its header says so, and user edits do not survive upgrades. This holds on every board, including repo-nested ones: the guide is auto-written there too (the untracked file in the user's repository is accepted — a board is agent-facing wherever it lives). A board-root `CLAUDE.md` found *without* the marker is displaced user content, not clobbered: its content is moved to `CLAUDE.user.md` if that name is free (otherwise the guide write is skipped with a log — user content is never destroyed), and the guide is then written.
**`CLAUDE.user.md`** is the user's extension point: an optional, user-authored file at board root carrying board-specific agent instructions. The generated guide tells agents to read it when present, so it lands in agent context without the app ever touching it — the app never writes, upgrades, or validates it. **`CLAUDE.user.md`** is the user's extension point: an optional, user-authored file at board root carrying board-specific agent instructions. The generated guide tells agents to read it when present, so it lands in agent context without the app ever touching it — the app never writes, upgrades, or validates an *existing* `CLAUDE.user.md`; the one exception is its creation, once, to rescue displaced content (the markerless-`CLAUDE.md` relocation above, which only runs when the name is free).
On git boards, a guide write rides the normal watcher → auto-commit path with an honest message ("Update agent guide (v3)"), not "External edit" (06-history-undo.md ▸ Commit messages — the non-snapshot-files rule: the committer stages the whole root, and the composer reads the subject's version from the guide's marker line, a pure function of file content). On git boards, a guide write rides the normal watcher → auto-commit path with an honest message ("Update agent guide (v3)"), not "External edit" (06-history-undo.md ▸ Commit messages — the non-snapshot-files rule: the committer stages the whole root, and the composer reads the subject's version from the guide's marker line, a pure function of file content).
+1 -1
View File
@@ -27,7 +27,7 @@ All ten pathfinder templates carry over: Basic, Classic Kanban, Software Project
### Instantiation ### Instantiation
Creating a board from a template: copy the tree — **skipping tombstoned items** (Save as Template already strips them, but hand-dropped user templates can carry them; a new board isn't born with trash) — **mint fresh GUIDs** for every lane/card folder, stamp `created`/`modified` fresh (the stated exception to 01-storage-format.md's copies-keep-`created` rule — a new board is born today, not forked from the template), seed the save panel's suggested name from the template title, and **set the new board's `title` to the user-chosen document name** (per 01-storage-format.md's board-naming rule, so display name and folder name start out matching). The `template:` key is kept — inert on an ordinary board. **`.git` is never copied** — a template is content, not history, and a hand-dropped user template that carries one must not produce boards that are silently in git mode (06-history-undo.md's no-silent-auto-init — the principle is *never give the user a repo they didn't ask for*, and it scopes to instantiation: File ▸ Duplicate deliberately carries `.git`, because a duplicate of a git board is a fork of its history — 03-board-ui.md): every instantiated board starts at mode `none`. Beyond the `template:` residue, the result is indistinguishable from a hand-built board. Creating a board from a template: copy the tree — **skipping tombstoned items** (Save as Template already strips them, but hand-dropped user templates can carry them; a new board isn't born with trash) — **mint fresh GUIDs** for every lane/card folder, stamp `created`/`modified` fresh (the stated exception to 01-storage-format.md's copies-keep-`created` rule — a new board is born today, not forked from the template), seed the save panel's suggested name from the template title, and **set the new board's `title` to the user-chosen document name** (per 01-storage-format.md's board-naming rule, so display name and folder name start out matching). The `template:` key is kept — inert on an ordinary board. **`.git` is never copied** — a template is content, not history, and a hand-dropped user template that carries one must not produce boards that are silently in git mode (06-history-undo.md's no-silent-auto-init — the principle is *never give the user a repo they didn't ask for*, and it scopes to instantiation: File ▸ Duplicate deliberately carries `.git`, because a duplicate of a git board is a fork of its history — 03-board-ui.md): an instantiated board is never in *git* mode — its actual mode follows 06's nearest-`.git`-wins detection at the destination the save panel chose: mode `none` in a plain folder, repo-nested when saved inside an existing repository (no undo, no add-git — the popover explains, 06-history-undo.md). Beyond the `template:` residue, the result is indistinguishable from a hand-built board.
### Why this format ### Why this format
+6 -5
View File
@@ -27,14 +27,14 @@ The single source of truth for **every command and action the app can perform**
| File | Board Info (opens the board popover) | ⌘I | Board window | | File | Board Info (opens the board popover) | ⌘I | Board window |
| File | Duplicate (the board — a Finder-style "copy" sibling, 03 ▸ Welcome; never the selection) | ⇧⌘S | Board window | | File | Duplicate (the board — a Finder-style "copy" sibling, 03 ▸ Welcome; never the selection) | ⇧⌘S | Board window |
| File | Save as Template | — (no default) | Board window; 09-templates.md | | File | Save as Template | — (no default) | Board window; 09-templates.md |
| File | Reveal in Finder | — (no default) | Board window: the selection's folder(s), or the board root with nothing selected; card window: the card's folder — the selected attachment's file instead when the attachments section is focused | | File | Reveal in Finder | — (no default) | Board window: the selection's folder(s), or the board root with nothing selected; card window: the card's folder — the selected attachment's file instead when the attachments section is focused; welcome: the selected recent's folder (disabled on unavailable rows) — the context-menu entry's required twin |
| File | Add Attachment… | ⇧⌘A | Card window | | File | Add Attachment… | ⇧⌘A | Card window |
| 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 | 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 | 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 | 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 shown and non-empty | | File | Empty Trash… (confirmed) | ⇧⌘⌫ | Board window, trash shown and non-empty |
| File | Close | ⌘W | Any window; flushes per 02 ▸ Windows | | 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 | 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, during 06's abnormal-state pause (detached HEAD, in-progress merge/rebase), and under the read-only lock (02) |
| 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 | | 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 |
| Edit | Select All | ⌘A | Board: all visible live cards (filter-respecting); text editors: the text | | Edit | Select All | ⌘A | Board: all visible live cards (filter-respecting); text editors: the text |
| Edit | Find | ⌘F | Board window: board search (04 ▸ Search); card window: find-in-text (05) | | Edit | Find | ⌘F | Board window: board search (04 ▸ Search); card window: find-in-text (05) |
@@ -45,7 +45,7 @@ The single source of truth for **every command and action the app can perform**
| Board | Move Up / Move Down | ⌥⌘↑ / ⌥⌘↓ | Card selection within one lane (within-lane sort, logical order; non-contiguous selections gather behind their first card on the first press); disabled when the selection spans lanes; inert on lanes and on tombstoned cards | | Board | Move Up / Move Down | ⌥⌘↑ / ⌥⌘↓ | Card selection within one lane (within-lane sort, logical order; non-contiguous selections gather behind their first card on the first press); disabled when the selection spans lanes; inert on lanes and on tombstoned cards |
| Board | Move Left / Move Right | ⌘← / ⌘→ | Lane selection only (one slot; never into the trash) — cards cross lanes by drag or Cut/Paste, not ⌘-arrows | | Board | Move Left / Move Right | ⌘← / ⌘→ | Lane selection only (one slot; never into the trash) — cards cross lanes by drag or Cut/Paste, not ⌘-arrows |
| Board | Increase Lane Width / Decrease Lane Width (the stepper's re-divide semantics, never the window's size — 03 ▸ Lane) | ⌥⌘→ / ⌥⌘← | Selected lane | | Board | Increase Lane Width / Decrease Lane Width (the stepper's re-divide semantics, never the window's size — 03 ▸ Lane) | ⌥⌘→ / ⌥⌘← | Selected lane |
| Board | Pull / Push | — (no default) | Remote-backed boards only (07); popover twins exist | | Board | Pull / Push | — (no default) | Remote-backed boards only (07); disabled during 06's abnormal-state pause (the whole git surface holds) and on an unresolvable remote (07's one-time remote picker case); popover twins exist |
| View | Show Trash (checkmark toggle) | — (no default) | Board window — ⇧⌘T is deliberately left to the system's Show Tab Bar: window tabbing stays enabled (settled; see Standard macOS furniture), so the chord is the system's; assign one via the remapping mechanism if wanted (04 ▸ Configurable bindings) | | View | Show Trash (checkmark toggle) | — (no default) | Board window — ⇧⌘T is deliberately left to the system's Show Tab Bar: window tabbing stays enabled (settled; see Standard macOS furniture), so the chord is the system's; assign one via the remapping mechanism if wanted (04 ▸ Configurable bindings) |
| View | Edit Body (checkmark toggle) | ⌘E | Card window; disabled while Raw Source is active | | View | Edit Body (checkmark toggle) | ⌘E | Card window; disabled while Raw Source is active |
| View | Raw Source (checkmark toggle; toggling off = Apply) | ⌥⌘E | Card window | | View | Raw Source (checkmark toggle; toggling off = Apply) | ⌥⌘E | Card window |
@@ -78,6 +78,7 @@ All board grammar keys are inert while a title editor is focused, and menu dispa
- **Lane edge drag** (03 ▸ Lane): window-growing resize between integer widths — the one width control that moves the window. - **Lane edge drag** (03 ▸ Lane): window-growing resize between integer widths — the one width control that moves the window.
- **Finder file drops** (04): onto a card = attach; onto lane empty space = one card per file; anywhere on the card window = attach (05's payload-split precedence). - **Finder file drops** (04): onto a card = attach; onto lane empty space = one card per file; anywhere on the card window = attach (05's payload-split precedence).
- **Preview** (05): task-list checkbox toggle (the one interactive exception), link opens, text selection; carousel paging on the card face (03). **In-content controls aren't P-only** (settled): checkboxes and links are real controls in the focus/accessibility tree, so Full Keyboard Access + Space and VO-Space reach them (05 ▸ Task-list checkboxes) — content rides the system focus model rather than earning command rows; "a command absent here doesn't exist" scopes to commands, not content. - **Preview** (05): task-list checkbox toggle (the one interactive exception), link opens, text selection; carousel paging on the card face (03). **In-content controls aren't P-only** (settled): checkboxes and links are real controls in the focus/accessibility tree, so Full Keyboard Access + Space and VO-Space reach them (05 ▸ Task-list checkboxes) — content rides the system focus model rather than earning command rows; "a command absent here doesn't exist" scopes to commands, not content.
- **Lane header new-card button** (03, labeled "New card in ⟨lane⟩" — 10): creates in **the button's lane**, appended at the bottom — the click names its target, overriding 04's selection-derived ⌘N target rule (settled); placeholder and abandon semantics exactly as ⌘N. The card window's attachments-section **quiet add affordance** (05) is the same class: a pointer twin of File ▸ Add Attachment…, no separate behavior.
- **Attachment rows** drag out their file URL (05). **Welcome rows**: single click selects, double click opens (03). - **Attachment rows** drag out their file URL (05). **Welcome rows**: single click selects, double click opens (03).
## Context menus ## Context menus
@@ -86,9 +87,9 @@ Context menus are the per-item action inventory VoiceOver reads (10 ▸ The boar
| Surface | Entries | | Surface | Entries |
|---|---| |---|---|
| Card / lane | Open (cards), Rename, Style…, quick-style recents row (03), Delete | | Card | Open, Rename, Style…, quick-style recents row (03), Delete |
| Lane | One menu, invoked on the header or lane empty space (settled — a full lane still has its header): Rename, Style…, quick-style recents row (03), Width control (stepper — menu twins Increase/Decrease Lane Width), Delete |
| Trash entries | Put Back, Delete Immediately, Reveal in Finder (inspection before a purge; twin of File ▸ Reveal in Finder, which is not edit-shaped and stays enabled on tombstoned selections — 04 ▸ The trash) | | Trash entries | Put Back, Delete Immediately, Reveal in Finder (inspection before a purge; twin of File ▸ Reveal in Finder, which is not edit-shaped and stays enabled on tombstoned selections — 04 ▸ The trash) |
| Lane header | Width control (stepper — menu twins Increase/Decrease Lane Width) |
| Attachment row | Open, Remove (system Trash) — twins of the focused section's grammar keys (Return / ⌫ — 05); Reveal in Finder — twin of File ▸ Reveal in Finder in its attachments-focused context | | Attachment row | Open, Remove (system Trash) — twins of the focused section's grammar keys (Return / ⌫ — 05); Reveal in Finder — twin of File ▸ Reveal in Finder in its attachments-focused context |
| Welcome recent | Open, Reveal in Finder, Forget (C — registry management, welcome-scoped) | | Welcome recent | Open, Reveal in Finder, Forget (C — registry management, welcome-scoped) |
+1 -1
View File
@@ -1,6 +1,6 @@
# Lanework — Design Documents # Lanework — Design Documents
A ground-up rewrite of the Kanban app, to ship as **Lanework** on the Mac App Store. The old repo (`../Kanban`) was a pathfinder — it never shipped, but it is the reference implementation and the source of hard-won decisions. This design starts from a clean slate and keeps only what earned its place. The rewrite keeps the internal codename `Kanban` (bundle id `dev.rzen.indie.Kanban`). A ground-up rewrite of the Kanban app, to ship as **Lanework** on the Mac App Store. The old repo (`../../Kanban`, one level above this repository — not the rewrite's own `Kanban/` source folder, which shares the codename) was a pathfinder — it never shipped, but it is the reference implementation and the source of hard-won decisions. This design starts from a clean slate and keeps only what earned its place. The rewrite keeps the internal codename `Kanban` (bundle id `dev.rzen.indie.Kanban`).
Each document covers one aspect of the design. Within each: Each document covers one aspect of the design. Within each: