Add design corpus and wishlist
Twelve design docs (00-vision through 11-command-nexus) plus assets and the wishlist, authored ahead of implementation. Claude-Session: https://claude.ai/code/session_018BjQRYBR6jQja3jCRi5S3A
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# Vision
|
||||
|
||||
## What Lanework is
|
||||
|
||||
A native macOS kanban app whose storage is **plain folders + Markdown files**. Every board, lane, and card is a folder containing an `index.md` (YAML frontmatter + Markdown body). The filesystem is the source of truth; the app is a live view over it.
|
||||
|
||||
The defining consequence: **anything that can read and write files is a first-class user of a board** — text editors, Finder, shell scripts, git, and above all AI agents. Claude Code working in a repo can file, move, and annotate cards with zero API, zero sync machinery, zero app-specific tooling. The app's job is to be the best *human* surface over that substrate.
|
||||
|
||||
## Who it's for
|
||||
|
||||
- Individuals and small teams who want their planning data in files they own — greppable, diffable, versionable, portable.
|
||||
- Developers who live in git and want boards to ride along with (or next to) their repos.
|
||||
- People working *with* AI agents, who want the agent and the human looking at the same board through their respective native surfaces (files for the agent, a polished Mac app for the human).
|
||||
|
||||
## Design principles
|
||||
|
||||
1. **Files first.** Every feature must degrade gracefully to "it's just folders of Markdown." If the app vanishes, the data remains fully usable.
|
||||
2. **The app never surprises the file.** Unknown frontmatter keys survive verbatim; untouched bodies are never rewritten; writes are atomic. Hand edits and app edits coexist without ceremony.
|
||||
3. **Fail fast on malformed input.** A broken file surfaces a loud, specific error with the offending path — never silent fixing, never partial loads, never data loss by "repair."
|
||||
4. **Native to the bone.** SwiftUI, macOS conventions (Finder-style rename, ⌥-drag copy, package documents, real windows), no web tech, no JS runtime.
|
||||
5. **Agents are users, not integrations.** The schema, the agent guide, and the tolerance rules are designed for programmatic writers from day one.
|
||||
|
||||
## App identity
|
||||
|
||||
The previous version was a **pathfinder** — it never shipped. This rewrite is the app. It keeps the internal codename `Kanban` (Xcode target, scheme, bundle id `dev.rzen.indie.Kanban`) and ships under the display name **Lanework**. Because nothing shipped, there is no migration story and no compatibility obligation to pathfinder boards; the schema number stays `1`, redefined by this design (see 01-storage-format.md).
|
||||
|
||||
## Goals for the rewrite
|
||||
|
||||
- Preserve the on-disk philosophy and the interaction quality of the pathfinder while restructuring the schema and codebase with what we learned.
|
||||
- Promote **attachments** to a first-class schema citizen.
|
||||
- Split the schema into a **core schema** (this version: boards, lanes, cards, attachments) and an **enhanced schema** (comments, tracker fields) that is reserved but out of scope — see 01-storage-format.md.
|
||||
- Build the **collab integration** axis (local-only / git) into the design from the start instead of retrofitting it.
|
||||
- Keep the codebase honest about its layering: storage contract → store → views, with the cache rebuildable and the flow one-way.
|
||||
- **Keyboard navigation as first-class UX** — the board fully operable without the mouse (04-interactions.md).
|
||||
- **Accessibility is a requirement of "native to the bone," not an afterthought.** The keyboard-first contract doubles as the drag-free operation guarantee — that's the floor. VoiceOver correctness on the custom board, text scaling, and the system visual accommodations are designed in 10-accessibility.md.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No web or iOS version (design shouldn't preclude iOS forever, but nothing is compromised for it now).
|
||||
- No MDX, no embedded scripting, no plugin runtime.
|
||||
- No wiki-style cross-linking between cards (considered and punted — overkill for kanban).
|
||||
- No tracker integrations (Gitea/GitHub/GitLab/Jira boards) in this scope — the schema reserves `remote`/`remote-state` so a future connector isn't foreclosed, but nothing here is designed for them.
|
||||
- No comments functionality in this version — `comments/` is reserved in the enhanced schema for future integrations.
|
||||
- No server. Sync rides on git remotes — never on infrastructure we run.
|
||||
- No iCloud Drive boards. The app warns against placing a board in iCloud Drive (thorough explanation, git recommended instead) and makes no iCloud accommodations — see 07-sync-collab.md.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — identity and migration are settled (see App identity above).
|
||||
@@ -0,0 +1,136 @@
|
||||
# Storage Format (Schema 1)
|
||||
|
||||
The on-disk contract between the filesystem (source of truth) and every consumer — the app, hand editors, and agents. This is the successor to the pathfinder's `SCHEMA.md`; once stable it graduates into a normative `SCHEMA.md` in the new repo. The schema number **stays `1`**: the pathfinder never shipped, so this design *redefines* schema 1 rather than bumping it — there is no migration story and no compatibility obligation to pathfinder boards.
|
||||
|
||||
The schema splits in two. The **core schema** — everything specified in this document: boards, lanes, cards, attachments — is what this version implements. The **enhanced schema** — comments, tracker integration fields — is out of scope for this version; core only *reserves* its names (see Enhanced schema below) so nothing collides later.
|
||||
|
||||
## Fractal layout
|
||||
|
||||
Every level is a **folder containing an `index.md`**. Identity is the folder name; everything else lives in the file.
|
||||
|
||||
```
|
||||
MyBoard.kanban/ ← board = the document
|
||||
├── index.md ← board settings + description
|
||||
├── 3f2a9c41-8b7e-4d2a-9c1f-5e6a7b8c9d0e/ ← lane folder
|
||||
│ ├── index.md ← lane metadata + description
|
||||
│ ├── 27ca60f1-ef07-44d5-b688-73bf1f77e066/ ← card folder
|
||||
│ │ ├── index.md ← card frontmatter + body
|
||||
│ │ └── attachments/ ← card attachments
|
||||
│ │ └── sketch.png
|
||||
│ └── 8d3b1f22-…/
|
||||
│ └── index.md
|
||||
└── b1e4d7a0-…/
|
||||
└── index.md
|
||||
```
|
||||
|
||||
### Rules (carried over from the pathfinder, proven)
|
||||
|
||||
- **Level is position.** Root = board, depth 1 = lane, depth 2 = card. No `type` field.
|
||||
- **Folder names are lowercase UUIDv4, immutable, never renamed.** The folder name is the primary key; titles live in frontmatter only. Collisions are impossible by construction.
|
||||
- **Identity lifecycle: moves keep the UUID, copies mint fresh ones.** Every move (between lanes, between boards, cut+paste) is a physical folder move — identity travels with it. Every copy (⌘C/⌘V, ⌥-drag duplicate, the cross-board drag default — 04-interactions.md's locality model — and template instantiation) mints fresh UUIDs for every folder it materializes; a copy keeps `created` (it's a fork) but is a new identity. **Exception: template instantiation stamps `created`/`modified` fresh** — a board created from a template is born today, not forked from it (09-templates.md); Save as Template, by contrast, is an ordinary fork and keeps timestamps (inert anyway — instantiation restamps them). The app never produces a duplicate UUID: **within-board uniqueness is enforced at the import boundary** — a move-in (a cross-board ⌘-drag move or a cut+paste — 04-interactions.md) whose UUID already exists anywhere in the destination board is degraded to a copy (fresh UUID minted, content intact; the source is removed as usual for a move). **Degradation is per-folder, at the finest grain**: in a compound arrival (a lane with its cards, a multi-card drag or paste), only the colliding folders are reminted — everything else keeps its identity and remains a true move, and source removal is uniform regardless (the whole selection leaves the source, as for any move). A lane drag with one colliding nested card thus stays a lane *move*; just that card arrives reminted. One gesture silently yielding moved originals plus reminted copies is the intended behavior, not an accident — the collision is an identity repair, invisible in content terms. Boards are **independent identity namespaces**: the same UUID may legitimately exist in two boards (e.g. after a cross-board move-out undone at the source — 06-history-undo.md); such forks are tolerated and only ever meet at an import boundary, where the collision is neutralized. A user hand-copying a card folder in Finder can still produce sibling duplicates — the app doesn't currently detect that, and behavior with duplicate sibling-level UUIDs in one board is unspecified.
|
||||
- `index.md` = YAML frontmatter between `---` delimiters, then Markdown body.
|
||||
- **Reserved child names** at card level: `attachments/` (specified below) and `comments/` (enhanced schema — see below). Never levels themselves, never identity (identity-bearing siblings are UUIDs).
|
||||
- A folder below the root **lacking `index.md` is skipped with a log warning**, not an error — an interrupted two-step create must not brick the board. **Stray tolerance is uniform at every level**: non-reserved files and folders at board, lane, or card level are ignored by the loader, preserved verbatim, never rendered (inside a card, only `index.md` and the reserved child names mean anything — anything else is not the app's business).
|
||||
- **Atomic writes**: temp file, rename over `index.md`.
|
||||
- **Unknown frontmatter keys and their order are preserved verbatim** on every rewrite. This is load-bearing for agent overlays (`project:`, `sphere:`, custom tags) — see 08-agent-integration.md.
|
||||
- **The app never reformats a body it didn't change** (byte-identical round-trip; plain string comparison, no re-serialization).
|
||||
|
||||
## Frontmatter
|
||||
|
||||
### Common to all levels
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `schema` | integer | yes | `1` for this design |
|
||||
| `title` | string | no | Optional at every level — the display name everywhere. Lanes/cards: a missing title shows the untitled placeholder. Boards: a missing title falls back to the folder name (see Board below) |
|
||||
| `created` | ISO-8601 | no | Set at creation, with timezone |
|
||||
| `modified` | ISO-8601 | no | Updated on every app write |
|
||||
| `modified-by` | string | no | Self-reported writer identity, set by external writers only; the app clears it on every write — see below |
|
||||
| `deleted` | ISO-8601 | no | Tombstone — see Deletion |
|
||||
| `background` | string | no | Palette name or `#RRGGBB[AA]` — see 03-board-ui.md |
|
||||
| `icon` | string | no | SF Symbol name, per-level defaults |
|
||||
| `iconColor` | string | no | Palette name or hex |
|
||||
|
||||
**`modified-by` — self-reported provenance (settled).** An external writer (agent, script) may stamp a file it writes with a short free-form identity (`modified-by: claude`). The app never sets it and **clears it on every app-mediated write** — absence means "the board's user, via the app"; the file is being rewritten anyway, so clearing costs no extra write. Consequences compose from existing rules: copies arrive cleared (paste and duplicate are app writes), template instantiation strips it alongside its timestamp restamp (09-templates.md), Save as Template keeps it inertly. The key is schema-owned, not an unknown key — unknown-key preservation doesn't apply to it. Downstream surfaces: the card window's modified line renders it (05-card-window.md), and it refines foreign commit attribution (06-history-undo.md). Honest limit: it is self-reported — a hand edit that leaves an old stamp in place inherits its attribution until the next app write clears it.
|
||||
|
||||
### Board (`<root>/index.md`)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `template` | mapping | no | `{order: N}` — picker position when the board lives in a template store (09-templates.md); future subkeys possible. **Carried onto instantiated boards**, where it is ignored and preserved like any unknown key. |
|
||||
|
||||
Body: board description (free Markdown).
|
||||
|
||||
**Board naming**: the schema is uniform at every level — folder name is storage identity, `title` is display. A board's `title` is what the window title and welcome recents show; when absent, the UI falls back to the folder name (sans extension). The in-app Board rename (03-board-ui.md) edits `title` only, never the folder — so the app's display name and the Finder document name can diverge (accepted; renaming the folder is a Finder operation). Template instantiation seeds the new board's `title` with the user-chosen document name, so the two start out matching (09-templates.md).
|
||||
|
||||
### Lane (`<root>/<guid>/index.md`)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `order` | number | yes | Rank among lanes, ascending = left-to-right |
|
||||
| `width` | integer | no | Width multiplier ≥ 1 (default 1); no cap |
|
||||
|
||||
Body: lane description / WIP policy / notes.
|
||||
|
||||
### Card (`<root>/<guid>/<guid>/index.md`)
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `order` | number | yes | Rank within its lane, ascending = top-to-bottom |
|
||||
|
||||
Body: the card's content — the whole point.
|
||||
|
||||
Colors / icons are **lenient**: a malformed value is preserved verbatim and simply not displayed. **`width` is lenient too** (it styles layout; it isn't structure): anything but an integer ≥ 1 — zero, negative, fractional, non-numeric — is preserved verbatim and renders as the default 1. Fail-fast is reserved for structure (`schema`, `order`, YAML validity) — and it covers *malformed*, not just missing: an `order` that is present but non-numeric is the same loud malformed-input rejection as a missing one.
|
||||
|
||||
## Enhanced schema (reserved, out of scope)
|
||||
|
||||
Future integrations extend the core schema; this version only reserves their names so nothing else squats on them. The app treats reserved keys as ordinary unknown keys (preserved verbatim, invisible in the UI) and reserved folders as ignored non-level content:
|
||||
|
||||
- **`comments/` card child** — a future comment thread: fractal like everything else (folder-per-comment holding `index.md` + `attachments/`, `author`/`created` frontmatter, chronological). Specified fully when the enhanced schema is designed; no comments functionality in this version.
|
||||
- **Card metadata keys `labels`, `assignees`, `due`** — issue-style metadata belonging to the tracker-integration story (the pathfinder shipped them natively; the rewrite reserves them instead).
|
||||
- **`remote` key** (board, card) and **`remote-state` key** (lane) — future tracker connectors (Gitea/GitHub/GitLab/Jira), per 07-sync-collab.md's out-of-scope note.
|
||||
|
||||
## Ordering
|
||||
|
||||
Carried over unchanged — gapped fractional ranks:
|
||||
|
||||
- New items append at `max + 1024`; insert at head = `min − 1024`; insert between two = midpoint.
|
||||
- A reorder rewrites **only the moved item's** `index.md`.
|
||||
- Midpoint precision exhausted → renumber that one lane's cards (or the board's lanes) to whole multiples of 1024. Rare, local — and **the one exception to the only-the-moved-item rule**: a renumber rewrites every sibling's `index.md` in that lane. On a remote-shared board the burst is conflict-prone in principle but benign in practice: each rewritten file changes only its `order`, the local-wins rebase (07-sync-collab.md) resolves per file, orders stay valid floats, and deterministic tie-breaking keeps rendering stable even if two machines' renumbers interleave. It commits as one app-mediated "Renumber lane 'X'" (06-history-undo.md), not N separate edits.
|
||||
- Ties broken by folder name (lexicographic) for deterministic rendering.
|
||||
|
||||
## Attachments
|
||||
|
||||
- `attachments/` under a card folder holds the card's files. The app's attachment surfaces (the card window's sidebar section and import — 05-card-window.md; the face carousel — 03-board-ui.md ▸ Card face) are **flat**: top-level files only.
|
||||
- **Import collisions auto-rename, Finder-style** (`shot.png` → `shot 2.png`): an import never overwrites an existing file and never refuses the drop. Never-refuses is policy, not an I/O guarantee — a failed copy (source unreadable, destination full) surfaces per 02-architecture.md ▸ Write-failure surfacing, with any partial file removed.
|
||||
- **Subfolders are tolerated, preserved verbatim, never created by the app, and not surfaced** in those surfaces. Their contents stay reachable via Reveal in Finder, and body-embedded relative paths into them (``) still render — preview resolves against the card folder.
|
||||
|
||||
## Deletion — tombstones
|
||||
|
||||
- Deleting writes `deleted: <ISO-8601>` into frontmatter; tombstoned items (and everything beneath) are hidden from the board and surface in the **trash quasi-lane** (03-board-ui.md) — a pure view. **Tombstoned items stay exactly where they are on disk**, which is what makes restore (Put Back = removing the key) position-perfect.
|
||||
- Tombstones are **inert to ordering**: appends and renumbering operate on visible siblings only; a restored item re-enters at its recorded `order`, ties broken deterministically.
|
||||
- A folder that disappears without a tombstone (Finder deletion) is also a delete; the app reflects it (such items are gone entirely — they never enter the trash).
|
||||
- A `deleted:` key at **board level** is legal per the common frontmatter table but meaningless — a board can't tombstone itself out of its own window. The loader ignores it and logs (the lenient-values philosophy: preserved verbatim, invisible in the UI).
|
||||
- Physical removal is the trash's **Delete Immediately / Empty Trash** (03-board-ui.md). Time-based auto-purge of old tombstones: deliberate follow-up, not in the initial release.
|
||||
|
||||
## Malformed input — fail fast
|
||||
|
||||
Loud, specific error (path + what's wrong) for: unparseable YAML, missing required fields (`schema`; `order` where required), `schema` newer than the app, board root without `index.md`. No partial loads. The only tolerated absence is a missing `index.md` below the root (skip + warn, per Rules above).
|
||||
|
||||
Fail-fast is the **initial-load** contract. Once a board is open, a failed live reload does not blank the board: the window keeps the last good snapshot and surfaces the same loud specifics in a non-modal banner — see 02-architecture.md's live-reload resilience.
|
||||
|
||||
## Document packaging
|
||||
|
||||
- Boards are created as `.kanban` packages (UTI conforming to `com.apple.package`) so Finder treats a board as one document.
|
||||
- Extension-less board folders still open.
|
||||
|
||||
## Changes from the pathfinder schema
|
||||
|
||||
No migration is implied by these — pathfinder boards have no compatibility claim on the rewrite (a hand-rename of `media/` → `attachments/` etc. covers any board worth carrying over).
|
||||
|
||||
- `media/` → **`attachments/`** (clearer, one unambiguous home for card files).
|
||||
- The schema is split into **core** (this version) and **enhanced** (comments, tracker fields — reserved only, out of scope).
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — trash-bin behavior is settled: in-place tombstones with the trash as a pure view (Deletion above; UI in 03-board-ui.md).
|
||||
@@ -0,0 +1,89 @@
|
||||
# App Architecture
|
||||
|
||||
## Platform
|
||||
|
||||
- macOS 26+, Swift 6 (strict concurrency), SwiftUI.
|
||||
- Codename `Kanban` (target, scheme, bundle id `dev.rzen.indie.Kanban`); display name **Lanework**.
|
||||
- XcodeGen project (`project.yml` → `xcodegen generate`).
|
||||
- Sandboxed; security-scoped bookmarks for reopening boards across launches.
|
||||
|
||||
## Layering
|
||||
|
||||
```
|
||||
filesystem (source of truth)
|
||||
│ FSEvents
|
||||
▼
|
||||
Loader ──validates──▶ BoardModel (value types)
|
||||
▼
|
||||
BoardStore (one per open board, @Observable, MainActor)
|
||||
▼
|
||||
SwiftUI views (board window + card windows share the store)
|
||||
```
|
||||
|
||||
One-way flow: **files → watcher → loader → store → views**. User actions go through a Writer that mutates files; the change comes back around through the watcher like any external edit. The app trusts its own writes no more than anyone else's — this is what makes external editors and agents first-class.
|
||||
|
||||
The **one named exception** is transient UI state rendering things that don't exist on disk — concretely the **new-card placeholder** (04-interactions.md): the inline editor for a card being created renders as a pseudo-card overlaid on the snapshot, with no disk presence and no UUID until the title commits. Commit creates the folder through the Writer and round-trips through the watcher like any write — the placeholder stays visible until the real card arrives, then hands off. Abandoning (Escape, empty commit, click-away) discards it; disk was never touched. Watcher reloads swap the snapshot *underneath* the overlay (like selection surviving a reload); if the placeholder's lane vanished in the reload, it is discarded — consistent with card windows dismissing when their card is deleted. Everything durable still round-trips through files.
|
||||
|
||||
### Components
|
||||
|
||||
- **Frontmatter** — YAML value model: parse, serialize, atomic write, unknown-key preservation with key order. Owns the byte-identical round-trip guarantee. Pure, heavily unit-tested.
|
||||
- **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.
|
||||
- **BoardStore** — per-board `@Observable` object holding the current snapshot plus transient UI state that must be shared across that board's windows (selection, drag state, search query, pending cut, the new-card placeholder, trash visibility). Debounces watcher reloads.
|
||||
- **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). Local boards only; there is no iCloud watching path (iCloud Drive boards are unsupported — see 07-sync-collab.md).
|
||||
- **Ranks** — gapped fractional ordering math + compaction. Pure.
|
||||
- **DropSlot** — drop-geometry math: hit zones and insertion-position targeting for drags (lane/position within the masonry, cross-board, Finder file drops). Pure, like Ranks.
|
||||
- **AgentGuide** — writes/upgrades the board-root `CLAUDE.md` (see 08-agent-integration.md).
|
||||
- **HistoryStore** — git plumbing for undo/redo (see 06-history-undo.md).
|
||||
|
||||
### Live-reload resilience
|
||||
|
||||
- **A failed reload never replaces a good snapshot.** Fail-fast (01-storage-format.md) is the *initial-load* contract, where there is nothing to fall back on. Once a board is open, a watcher-triggered reload that fails (unparseable YAML, missing required fields — typically a non-atomic external write caught mid-flight) keeps the last good snapshot on screen and raises a **non-modal banner** carrying fail-fast's specifics (offending path + what's wrong). The watcher keeps watching; the next successful reload clears the banner automatically — transient breakage self-heals without the user losing the board, persistent breakage stays loudly visible. Editing is not locked out: writes go through the Writer as usual (the breakage is per-file and localized), and the reload debounce already absorbs most momentary invalid states before they surface.
|
||||
- **App-initiated git churn is bracketed.** Operations the app runs itself (pull-rebase, branch switch, undo restore — 06-history-undo.md, 07-sync-collab.md) suspend watcher reloads for their duration and finish with one full reload — half-checked-out trees are never rendered. External git activity (the user running git in a terminal) can't be bracketed: the debounce coalesces its churn, and a transiently inconsistent but parseable tree may render briefly and heals on the next event — accepted.
|
||||
- **Selection survives reloads by UUID.** Selection — and every transient state that references items (drag state, pending cut, the search filter's result set) — is a set of UUIDs over the snapshot, re-resolved when a reload swaps it: items still present stay selected; items that vanished leave the selection silently, no substitute invented — the search filter's hidden-cards-leave-the-selection rule (04-interactions.md) applied to external change. Kin rules elsewhere: card windows dismiss when their card is deleted (05-card-window.md), the placeholder is discarded when its lane vanishes (above), and VoiceOver announces a vanished focused card and recovers focus to its lane (10-accessibility.md). App-mediated deletion is deliberately different — an act, not a surprise: ⌫ selects the successor sibling (04-interactions.md ▸ The map).
|
||||
- **A failed reload after a bracketed operation locks the board read-only** — the exception to "editing is not locked out" above. Ordinary watcher breakage is per-file: the snapshot still describes the tree, so editing around the broken file is safe. But a bracketed git operation changed the tree *wholesale*: if its final reload fails, the last-good snapshot on screen describes the pre-operation state (after a branch switch, a different branch entirely — 06-history-undo.md), and writes derived from it would land nonsense on the new tree. The banner carries the same fail-fast specifics plus the read-only state; the next successful reload (typically after the offending file is fixed) clears both.
|
||||
|
||||
### Write-failure surfacing
|
||||
|
||||
The read-side rules above have a write-side mirror — one banner vocabulary for both directions:
|
||||
|
||||
- **The one-way flow makes write failures honest by construction.** Views render only what is on disk, so a failed Writer operation (disk full, permissions, volume error) never shows phantom state — the action visibly doesn't happen. The failure surfaces in the same non-modal banner as read-side breakage, naming the operation and the cause ("Couldn't move 'Fix login' — disk full"). One-shot actions (move, tombstone, style, create) fail once and wait for the user to act again; nothing is queued behind their back.
|
||||
- **The debounced body save retries on its own cadence** — keystrokes stay in the dirty buffer, so nothing is lost while the window stays open; the banner stands until a save lands. The **one modal moment on the write-failure path**: closing a window (or the board, or quitting) with a dirty buffer that cannot be written — the only state that exists nowhere but memory — raises an alert (retry / save a copy elsewhere / discard) instead of failing silently. Everything else on this path stays non-modal. (Deliberate confirmations elsewhere are their own stories: Empty Trash… and Delete Immediately on boards without git history — 03-board-ui.md, the branch-switch save-or-discard step — 06-history-undo.md, machine-key regeneration — 07-sync-collab.md, the raw-source Apply validation alert — 05-card-window.md, the once-per-board iCloud/network-volume warning on open/create — 07, and the SSH trust-on-first-use fingerprint confirmation with its mismatch hard-block — 07.)
|
||||
- **A vanished board root locks the board read-only** — the bracketed-reload vocabulary applied to a root that is gone (volume unmounted, folder Finder-deleted while open): every write would land nowhere, so the last-good snapshot stays on screen, read-only, banner up. The watcher keeps watching; if the root returns (remount, Finder undo), the next successful reload clears the lock and pending dirty buffers save normally.
|
||||
- **Auto-commit failures beyond `index.lock` contention** (06-history-undo.md covers the lock) — disk full mid-commit, repo corruption: the files are safely on disk but history stops advancing, which quietly suspends the undo trail and the flush-before-overwrite guarantee. That degradation is surfaced, not hidden: the banner states that changes aren't being recorded to history; the committer retries on the next debounce and the banner clears on the first successful commit.
|
||||
- **Attachment import copy failures** (source unreadable, destination full): the drop was accepted — "never refuses the drop" (01-storage-format.md ▸ Attachments) is policy, not an I/O guarantee — so a failed copy surfaces in the banner with the filename, and any partial file is removed; no half-copied attachment is ever left in `attachments/`.
|
||||
|
||||
## Windows
|
||||
|
||||
- **Welcome window** — Xcode-style: branding + actions left, recents right (board icon, name, location, lane/card counts, sorted by last opened).
|
||||
- **Board windows** — one per board root; multiple boards open at once; per-board frame memory (repositioned onto a live screen if the saved one is gone).
|
||||
- **Card windows** — `WindowGroup(for: CardWindowRef.self)`; at most one per card (reopen focuses); follows its card across lanes; dismisses itself if the card is deleted.
|
||||
- **Close flushes**: closing a board window (and app quit) first closes the board's card windows — each open Edit session ends with its normal session commit (06-history-undo.md's granularity) — then flushes pending debounced work, editor saves before the pending auto-commit, before the store tears down. Nothing about this is conditional: a card window cannot exist without its board window (the ownership rule above), so the close flush is always the whole story.
|
||||
|
||||
## Per-board app state
|
||||
|
||||
State that belongs to the app, not the user's files — the recents list, per-board window frames, 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.
|
||||
- **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).
|
||||
|
||||
## Caching and search
|
||||
|
||||
The old app loaded boards fast enough that the planned SwiftData cache was never built. Position for the rewrite: **same discipline** — the loader reads files directly; any cache introduced later must be rebuildable from files at any time and populated only by watcher events (never written by the UI path). Cross-board search is the feature that would force the cache into existence; until it ships, no cache.
|
||||
|
||||
## Testing
|
||||
|
||||
- Unit: Frontmatter round-trip (including hostile YAML), Ranks, DropSlot zone math, Loader fixtures (valid, malformed, interrupted-create).
|
||||
- UI: XCUITests over fixture boards via a debug-only `--open-board` launch hook (inline rename, new-card focus, drag cleanup — the flows that regress).
|
||||
|
||||
## Changes from Kanban
|
||||
|
||||
- The store's transient-state grab-bag (selection, drag, search) gets an explicit home rather than accreting — exact shape TBD during implementation planning.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — the iCloud watching question dissolved with the decision not to support iCloud Drive boards (07-sync-collab.md).
|
||||
@@ -0,0 +1,115 @@
|
||||
# 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.
|
||||
- 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.
|
||||
- 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).
|
||||
|
||||
## 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 else** — trailing, the one default item; 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. **Catalog** (available via Customize): New Card, New Lane, Undo, Redo (the pair disabled on boards without undo — no-git and repo-nested boards, matching their menu items — 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. 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.
|
||||
|
||||
## 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: **the sole selected card** shows the paged media carousel when it has attachments (the pathfinder's selection-keyed in-place expansion, minus its body blurb — no body excerpt, above). Single selection only: multi-selections and unselected cards stay compact, and the expansion animates under the selection-keyed transaction (Motion below). QuickLook thumbnails for anything previewable, Finder icon fallback, page dots on a glass underlay, paged by trackpad pan / dot click / scroll wheel.
|
||||
|
||||
## 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.
|
||||
- **`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.
|
||||
|
||||
### 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.
|
||||
|
||||
- **Palette-only in-app**: the background grid offers the 12 palette colors — every pair AA-verified at design time (10-accessibility.md) — plus a leading **None** well that removes the `background` key. Custom hex is not pickable in-app but stays fully honored from disk (runtime contrast, 10-accessibility.md): curated in-app, unlimited on disk.
|
||||
- **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 — the palette stance again. No full-browser escape hatch in-app; the raw file is the escape hatch.
|
||||
- **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.
|
||||
- **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. 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).
|
||||
- **Board styling** — the embedded style editor (Styling ▸ Controls above).
|
||||
- **Git integration** — mode-aware (06-history-undo.md, 07-sync-collab.md): on a mode-none board, the **add-git** action (opt-in init; on repo-nested boards replaced by the honest this-board-lives-inside-a-repository explanation — 06); on git boards, branch/source display, branch switching and creation, the commit-identity name/email fields (06), and **add/change remote** (a remote can be added or changed at any point — 07); for remote-backed boards additionally remote tracking (ahead/behind) with Pull/Push controls and the push-on-every-commit option. **Remote authentication surfaces inline here** (07 ▸ Remote authentication): credential fields on add/verify, the machine SSH key with Copy, and the Authentication-needed badge state.
|
||||
|
||||
## Trash
|
||||
|
||||
Deletion is a two-stage, Finder-style story: ⌫ tombstones (01-storage-format.md), and the **trash quasi-lane** is where tombstoned items live on screen. It is a **pure view** — tombstoned cards keep their `deleted:` key and stay exactly where they are on disk; nothing about the storage schema is trash-specific.
|
||||
|
||||
- **Rendering**: trailing (rightmost) position, visually distinct — dimmed/hatched header, trash SF Symbol, count badge; no new-card button; not draggable, not resizable, excluded from lane reordering and width math.
|
||||
- **Contents**: the board's tombstoned cards, sorted by `deleted` timestamp (newest first). A tombstoned *lane* appears as a single restorable entry — its cards were hidden with it, not individually tombstoned, and it restores as a whole.
|
||||
- **Visibility**: hidden by default; **View ▸ Show Trash** toggles it (⇧⌘T; stable title with checkmark state, per 04-interactions.md's configurable-bindings rules). Transient board-scoped state, held in the BoardStore (02-architecture.md; one board window per board, so board-scoped and per-window coincide today) — resets to hidden on open, not persisted (visiting the trash is an errand, not a layout choice). Hidden trash is invisible to search; shown, it participates in the filter like any lane.
|
||||
- **Put Back** (context menu, Finder vocabulary; ⌘⌫ on a tombstoned selection — Finder's own symmetry): removes `deleted:` — the item reappears in its lane at its old `order` (ties break deterministically). Putting back a card whose parent lane is tombstoned restores the lane too. Restore fidelity is perfect because nothing ever moved.
|
||||
- **Drag-to-restore**: dragging a card out of the trash into one of its own board's lanes restores it at the drop position (key removed, `order` set, folder moved only if the destination lane differs). Dropped on another board it follows the drag locality model (04-interactions.md) — a live copy by default, the tombstoned original staying put; ⌘-drag for the true restore-move.
|
||||
- **Keyboard, selection, and clipboard semantics** inside the shown trash (navigation, no mixed live/tombstoned selections, copy-out-only clipboard, inert moves) are specified in 04-interactions.md ▸ The trash, keyboard-first.
|
||||
- **No editing in the trash**: tombstoned cards don't open — double-click does nothing beyond selection; Put Back or drag out first (Finder vocabulary: the trash is for restoring or purging, not working). Tombstoning a card whose window is open dismisses that window (05-card-window.md).
|
||||
- **Delete Immediately** (per item, ⌥⌘⌫) and **Empty Trash…** (confirmed, ⇧⌘⌫) physically remove the folder(s) — Finder's trash trio throughout. **Delete Immediately confirms exactly where the loss is real** (settled): on boards without app-managed git history — mode none and repo-nested — the alert stands between one keystroke and unrecoverable deletion; on git boards it acts immediately, since the content remains reachable in history (06-history-undo.md's delete-never-forgets). A deliberate divergence from Finder's always-confirm: the prompt tracks actual recoverability, not ceremony. Empty Trash… confirms everywhere (bulk scope, not per-item recoverability, is what it guards). Time-based auto-purge remains a deferred follow-up (01-storage-format.md).
|
||||
- Every trash operation is an ordinary file write — auto-committed and undoable on git boards; on no-git boards the trash itself is the delete-recovery story (07-sync-collab.md).
|
||||
- **Naming constraint**: two "Trash" concepts coexist — attachment Remove moves the file to the *system* Trash (05-card-window.md), while card/lane deletion lands in this in-app quasi-lane. UI copy must keep them distinguishable: Finder's "Move to Trash" phrasing is reserved for the system Trash; board deletion says "Delete", and the quasi-lane is "Trash" / "Show Trash". Final strings settled in one naming pass when the trash UI copy is written.
|
||||
|
||||
## 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. 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. On a git board, the duplicate **keeps `.git` but has its remote configuration stripped**: 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 picker |
|
||||
| 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 sole-selected card, on the search query — never on broad state, so multi-select churn, marquee drags, and in-flight drags stay animation-free by construction rather than by suppression.
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,93 @@
|
||||
# Interactions
|
||||
|
||||
Selection, drag & drop, keyboard, clipboard, search. This is where the old app spent most of its polish budget; nearly everything here is carried over as settled — the details below are the contract, not suggestions.
|
||||
|
||||
## Selection
|
||||
|
||||
- Cards: click selects; cmd-click toggles; shift-click range-extends; click-drag rubber-bands across lanes. Lanes: cmd/shift-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).
|
||||
|
||||
## Drag & drop
|
||||
|
||||
- **Shadow placeholder with live reflow**: dragging shows a shadow at the exact landing spot; the board reflows to make room. The whole slot is the trigger zone; proposals are geometry-based so the shadow is stable, never jittery. Width-aware triggers for lanes (no reflow until the cursor reaches where the dragged lane would actually land); the shadow holds until a real new candidate appears. (The old repo's DRAG-REORDER.md documents the full model — port that document alongside the implementation.)
|
||||
- Cards reorder within a lane and move between lanes (folder move). Lanes reorder; a full-size replica travels under the cursor.
|
||||
- **Multi-drag**: dragging any member of a multi-selection drags the whole selection; N contiguous shadows; drop inserts contiguously in preserved relative order — defined, for any multi-selection, as lane `order` first, then card `order` (a cross-lane selection flattens left-to-right, top-to-bottom).
|
||||
- **Locality picks the default — the Finder volume model** (settled): within a board a drag is a **move** (rearranging); between boards it is a **copy** (transferring — the system copy badge shows over the foreign board). **⌥ always forces copy** and **⌘ always forces move**, Finder's exact modifier grammar; each is a no-op where its behavior is already the default. The badge tracks the effective operation live as the cursor crosses a board boundary.
|
||||
- **Within-board ⌥-drag copies**: originals stay, cursor shows the copy badge, fresh-GUID duplicates land at the drop. Lane drags never copy *within their board* — a lane duplicate inside its own board stays unsupported; ⌥ is simply ignored there (the drag stays a move and the badge never shows copy).
|
||||
- **Cross-board copy** (the default): cards and lanes (including multi-selections) drag between open boards; fresh-GUID duplicates land at the drop, originals stay, `created` is kept (a copy is a fork — 01-storage-format.md). Lanes copy cards and all — transferring workflow structure between boards is safe by default. A lane copy **strips tombstoned cards**: the copy transfers content, and trash isn't content (09-templates.md's instantiation precedent — a board isn't born with trash); the tombstoned originals stay recoverable in the source board. A ⌘-drag *move* carries them whole — the folder moves as-is, and they land in the destination's trash.
|
||||
- **Cross-board move** (⌘-drag): a real filesystem move, works across volumes — identity travels. A moved folder whose UUID already exists in the destination board arrives as a fresh-UUID copy (01-storage-format.md's import-boundary rule); in a compound move (lane with cards, multi-selection) only the colliding folders are reminted — the rest is a true move (01's per-folder degradation).
|
||||
- **Files from Finder**: dropped on a card → copied into its `attachments/` (any type, multi-file; card highlights while hovered). Dropped on lane empty space → creates a card with the file attached, titled with the filename without its extension (multi-file drop: one card per file).
|
||||
|
||||
## Clipboard
|
||||
|
||||
- ⌘X/⌘C/⌘V on cards **and lanes** (resettled — lanes joined the clipboard so cross-board structure transfer has a keyboard path under the every-function contract; the cards-XOR-lanes selection rule means the clipboard holds cards or lanes, never both). Hybrid clipboard: pasteboard carries a JSON manifest + plain text; full folder snapshots staged in Application Support so paste reproduces the item byte-for-byte — cards, attachments and all — across boards. Each manifest entry embeds the full `index.md` as a staging-less fallback (a lane entry embeds its cards' too, attachment-less).
|
||||
- **Cut is Finder-style deferred**: cut items dim in place until paste moves them; voided if another app takes the pasteboard or the source board closes; second paste materializes copies. **Deletion voids per item**: a cut item that is tombstoned or vanishes externally before paste drops out of the pending cut — 02-architecture.md's UUID-set rule; transient state never resurrects what's gone — so paste moves only the survivors, and a cut voided down to nothing is simply void (paste disabled, no error).
|
||||
- Paste lands after the anchor card (or appends to a selected lane). Copies keep `created` (a duplicate is a fork) and take fresh GUID/`order`/`modified`.
|
||||
- **Lane paste** lands after the anchor lane — the selected lane, or the selected card's lane; nothing selected = the board's right end. Semantics mirror the drag pair above exactly: a pasted *copy* takes fresh GUIDs throughout and **strips tombstoned cards**; a cut-paste is the ⌘-drag move — the folder moves whole, tombstoned cards landing in the destination's trash.
|
||||
|
||||
## Keyboard
|
||||
|
||||
**Keyboard navigation is a first-class UX concern** — the board must be fully operable without the mouse: creating, navigating, renaming, moving, deleting, and opening items all need keyboard paths, and **every board function must have a menu item + shortcut** (toolbar-hosted functions included — toolbars are pure enhancement, 03-board-ui.md ▸ Toolbar). The pathfinder covered creation, navigation, rename, and delete; the keyboard-map iteration's outcome — settled as a coherent whole — is inventoried in 11-command-nexus.md, with its behavioral rules below.
|
||||
|
||||
### Grammar (fixed keys — deliberately not remappable)
|
||||
|
||||
- **Arrows**: spatial card navigation (nearest card in the direction, across interior grid columns and lanes); with a lane selected, ←/→ move lane selection; ⇧-arrow extends; selection scrolls into view; all grammar keys inert while a title editor is focused, and menu dispatch narrows to the text domain (focused-editor rule below).
|
||||
- **⌥-arrows jump**: ⌥↑/⌥↓ to the current lane's first/last card; ⌥←/⌥→ to the first/last lane.
|
||||
- **Return** on a selected lane: creates a card at its bottom, editor focused; Return commits and re-selects the lane (next Return = next card); ⌘↩ commits and opens the card window. Abandoned placeholders (Escape, empty commit, click-away) are discarded — creating-then-abandoning never leaves an empty card behind (untitled cards exist only when made deliberately, e.g. by an external writer or by clearing an existing title). The placeholder is store-transient overlay state — the named exception to 02-architecture.md's one-way flow; nothing exists on disk until the title commits.
|
||||
- **Return** on a sole selected **card**: inline rename. Return disambiguates on card selection — sole card = rename, lane = create (above) — and is **inert on a multi-card selection**; a lane's rename path is Board ▸ Rename. **Escape** steps outward one layer per press: abandons an open editor; else clears search, returning focus to the board (Search below); else **clears the selection** — the keyboard deselect.
|
||||
- **Focused editor = text domain** (settled): while an inline title editor — rename or the new-card placeholder — is focused, board-scoped menu commands (Delete, New Card, Paste, Move, Style, …) disable via menu validation; text-domain chords route to the field as standard text ops — ⌘Z/⇧⌘Z are the editor's text undo (06-history-undo.md ▸ Undo routing), ⌘X/⌘C/⌘V/⌘A act on the text. The one board-command carve-out is **Open Card ⌘↩**, which stays enabled to commit the edit — placeholder or rename — and open the card window. Exits are otherwise unchanged: Return commits, Escape abandons; click-away splits by editor kind — a **rename commits** (focus loss = commit, matching the card window's title field in 05-card-window.md and the branch-switch parenthetical in 06-history-undo.md), while the **placeholder discards** per its rule above, the deliberate exception because nothing exists on disk yet.
|
||||
- **⌫** on a live selection: delete (tombstone) — the plain-key synonym for File ▸ Delete ⌘⌫ (see The map). Grammar, not a menu item: giving it a menu home would require a second "Delete"-titled item, which would collide for title-matched remapping (Configurable bindings). Inert while a title editor is focused, like every grammar key.
|
||||
- The card window speaks the same grammar: **Return** in Preview enters Edit, **Escape** returns to Preview (05-card-window.md) — plain keys, not menu items.
|
||||
- These plain-key behaviors are platform grammar (Finder's own Return/arrows aren't remappable either) and sit below the remapping mechanism, which handles modifier chords on menu items only — see Configurable bindings.
|
||||
|
||||
### The map — moved to the command Nexus
|
||||
|
||||
Every command is a menu item. The full inventory — every command and action, its default binding, applicable context, and customizability class — lives in **11-command-nexus.md**, the single source of truth for what the app can do; the command titles there are the stable strings the remapping mechanism keys on (Configurable bindings below). The rules below are the behavior behind those bindings and stay normative here.
|
||||
|
||||
- **⌥⌘↑/⌥⌘↓ sort within the lane** (the move-vs-jump question, resettled: 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).
|
||||
- **⌘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.
|
||||
|
||||
### The trash, keyboard-first (settled)
|
||||
|
||||
The trash quasi-lane (03-board-ui.md ▸ Trash) speaks the same keyboard language when shown; hidden, it is invisible to every gesture. Rules:
|
||||
|
||||
- **Navigation**: the shown trash is the **last container for card navigation** — arrows walk into and out of it, and ⌥→ jumps to it. The quasi-lane itself is never selectable *as a lane* (no lane op applies to it): with a lane selected, ←/→ and ⌥→ stop at the last real lane.
|
||||
- **Moves are inert across the boundary**: no move or paste ever targets the trash (deleting is ⌫/⌘⌫), and ⌥⌘↑/⌥⌘↓ are inert *on* tombstoned cards (moving out is Put Back or drag-to-restore).
|
||||
- **Selection is homogeneous by liveness** (extending the homogeneous-selection rule): a selection never mixes live and tombstoned cards. Select All selects visible live cards only; a rubber-band stays on the side of the boundary it started on. Menu validation stays binary — Delete for live selections, Put Back / Delete Immediately for tombstoned ones.
|
||||
- **Tombstoned lane entries are full keyboard citizens, homogeneous by kind**: arrows walk every trash entry in its sorted order — card and lane entries alike (a lane's single restorable entry, 03-board-ui.md ▸ Trash) — and the board's cards-XOR-lanes rule extends into the trash: a selection never mixes card entries and lane entries (on top of never mixing live and tombstoned). Put Back (⌘⌫) and Delete Immediately (⌥⌘⌫) apply to lane entries exactly as to cards — a put-back lane returns whole, cards and all. A lane entry is not draggable (its entry is a compact row, not the lane); its copy-out is ⌘C only, and its move-out is Put Back.
|
||||
- **Clipboard: copy out only.** ⌘C (cards and lane entries), ⌥-drag, and the cross-board drag default (cards) always yield *live* copies — `deleted:` is stripped on paste/duplicate/drop, like copying a file out of Finder's Trash; a lane entry's copy additionally strips its tombstoned interior cards (the lane-copy rule — copies transfer content, and trash isn't content). ⌘X is disabled: the move-out vocabulary is Put Back or drag-to-restore, nothing else. The *cross-board restore-move* (⌘-drag below) needs no command of its own — its keyboard equivalent is the composition Put Back → ⌘X → ⌘V in the destination: same folder, same identity.
|
||||
- **Everything edit-shaped is disabled** on tombstoned selections — Open Card, Rename, Style… (File ▸ Duplicate is untouched: it duplicates the board, never the selection — 11-command-nexus.md; card copies out of the trash are ⌘C or ⌥-drag, which name a live destination). Finder file drops (attachment import) on tombstoned cards are inert — 03-board-ui.md's no-editing-in-the-trash.
|
||||
- **Drag-to-restore follows the locality model**: dropping a tombstoned card into one of its own board's lanes restores it at the drop position (`deleted:` removed, `order` set). Dropped on *another* board it follows the copy default — a live copy lands there and the tombstoned original stays in the source trash (copy-out, like ⌘C); ⌘-drag forces the true cross-board restore-move (the tombstone leaves the source board; ordinary cross-board move semantics, `deleted:` cleared at the destination).
|
||||
|
||||
### Configurable bindings (settled)
|
||||
|
||||
Custom shortcuts are **system-native, with no in-app remapping UI**: macOS's App Shortcuts mechanism (System Settings ▸ Keyboard ▸ App Shortcuts, stored as `NSUserKeyEquivalents` in the app's defaults) remaps any menu item, and AppKit applies it automatically — menus always display the *effective* binding, so the menu bar is the self-documenting keyboard map. Because every board function is a menu item (the contract above), coverage is complete for all modifier-chord commands; the fixed grammar keys stay fixed by design. An in-app shortcut-recorder pane was considered and set aside as ceremony (WISHLIST.md); the Help content carries one line teaching the System Settings path. Constraints this mechanism imposes, adopted as design rules:
|
||||
|
||||
- **Menu item titles are API.** The mechanism matches on exact titles — renaming a menu item orphans users' bindings. Titles change only with the deliberateness of a schema change.
|
||||
- **Toggles keep one stable title** with a checkmark state — "Show Trash" stays "Show Trash" when checked, never becomes "Hide Trash". (Same for Edit Body and Raw Source.)
|
||||
- **Undo/Redo are effectively not remappable** — NSUndoManager rewrites their titles dynamically ("Undo Move Card…"), which defeats title matching. Accepted; nobody remaps ⌘Z.
|
||||
- **Two items may share a default chord when validation is mutually exclusive** (Delete / Put Back on ⌘⌫) — AppKit fires the enabled one. Each keeps its own stable title, so remapping stays per-item. Corollary: no two menu items share a *title* either (titles are the remap key), which is why plain-⌫ delete is grammar rather than a second Delete item.
|
||||
|
||||
## Accessibility
|
||||
|
||||
**Stance (committed, 00-vision.md): accessibility is a requirement, not an afterthought.** The keyboard-first contract above is doing double duty as the drag-free operation guarantee — every function reachable without the mouse is the accessibility floor. The lane-move defect 10-accessibility.md named is closed by the map above (Move Left/Right on a selected lane). The full design — VoiceOver tree shape and logical traversal order over the masonry, clipboard as the drag-free card-move story, live-reload announcements, text scaling, visual accommodations, and verification — is **10-accessibility.md**.
|
||||
|
||||
## Search
|
||||
|
||||
- Search field invoked with ⌘F (the board toolbar's sole default item; removed from the toolbar, ⌘F surfaces it transiently — 03-board-ui.md ▸ Toolbar; in the **card window**, Edit ▸ Find is find-in-text instead — 05-card-window.md), live filter: cards whose title *and* body both miss the query animate out; case/diacritic-insensitive substring. Scope is **title + body only** (settled) — attachment filenames are not searched.
|
||||
- The filter is the single source of truth for "what's on the board": layout, drop zones, marquee, ranges, arrow nav, and lane count badges all read it. Hidden cards leave the selection; creating a card clears the search. Escape clears, then returns focus to the board.
|
||||
- **Dispatch while the search field is focused** (settled): the field is a *control*, not a content editor — the focused-editor lockdown (Grammar above) does not apply. Text-domain keys route to the field: ⌘A/⌘X/⌘C/⌘V act on the query, plain ⌫ edits the query and never reaches the board, horizontal arrows move the caret. Board menu commands stay enabled and act on the board selection exactly as when the field is unfocused — ⌘N included (creating a card clears the search, above) — and the Delete pair stays unambiguous by construction: plain ⌫ is query editing, ⌘⌫ is File ▸ Delete on the selection.
|
||||
|
||||
## Changes from Kanban
|
||||
|
||||
- Finder file drop on lane empty space (old item 49) is in scope from the start rather than a follow-up.
|
||||
- **Clicking never edits**: the pathfinder's Finder-style slow-second-click inline rename is gone — Return (or the menu) renames, double-click only opens. One less timer, and no edit ever starts from a hesitant click.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — the keyboard map is settled (⌥⌘↑/⌥⌘↓ within-lane sort, ⌘←/⌘→ lane moves, ⌥-arrows jump, creation trio, ⌘N target rule, system-native remapping); its inventory lives in 11-command-nexus.md, its behavior above.
|
||||
@@ -0,0 +1,108 @@
|
||||
# Card Window
|
||||
|
||||
The standalone per-card window. Opened by fast double-click, ⌘↩, or the context menu; at most one window per card (reopening a live card focuses the existing window); board and card windows share one live store, so edits reflect everywhere instantly (02-architecture.md).
|
||||
|
||||
> **Status: designed** — composition, body column, and the attributes sidebar are all settled. The storage-facing rules the pathfinder settled painfully — Preview/Edit over WYSIWYG, the untouched-body byte-identical guarantee, validate-before-write on the raw outlet, dirty-buffer-wins — carry over unchanged.
|
||||
|
||||
## Composition
|
||||
|
||||
Two full-height columns: a wide **body column** (leading) and a narrow **attributes sidebar** (trailing), each scrolling independently. Visual reference: [assets/card-sidebar-reference.png](assets/card-sidebar-reference.png) — a GitLab-style issue pane; its *pattern* (stacked small-caps sections, quiet read-first rows, actions at the bottom) is what carries over, not its enhanced-schema content.
|
||||
|
||||
- **Body column, top to bottom**: the **title field** — large, borderless; edits write through to frontmatter on commit (Return or focus loss); clearing it removes the `title` key (titles are optional — the untitled placeholder shows here as on the face); Return commits and moves focus into the body. Beneath it, a **quiet created/modified line** ("Created ⟨date⟩ · Modified ⟨date⟩ · by ⟨modified-by⟩", secondary styling, omitting whichever keys are absent — the "by" segment renders only when the self-reported provenance stamp is present, 01-storage-format.md; this is provenance made visible where git history may not exist) — the read-only readout the pathfinder dropped with its inspector, back where the mockup puts it. Then the **body** (Preview/Edit, below).
|
||||
- **Attributes sidebar**: everything about the card that isn't the body — sections below. Fixed narrow width derived from font metrics (full relative scaling, 10-accessibility.md); the window's resize flex goes to the body.
|
||||
|
||||
(The pathfinder's compositions are both gone: the metadata bar — labels, assignees, due — left with those fields' move to the enhanced schema, and the horizontal attachments strip dissolves into the sidebar.)
|
||||
|
||||
## Body: Preview / Edit — not WYSIWYG
|
||||
|
||||
Settled the hard way in the pathfinder (WYSIWYG built, then reversed): the body is **read/write raw Markdown with a fully rendered preview**. `TextEditor` can't lay out tables, so rendered-reading + raw-editing is the honest, lossless pairing for a file-backed app.
|
||||
|
||||
### Mode grammar
|
||||
|
||||
- **Preview is the resting state**: a card opens in Preview — **unless its body is empty, which opens straight into Edit** with the cursor ready (a new card has nothing to preview, so ⌘↩ during creation flows title → body without a mode stop).
|
||||
- **⌘E toggles** (View ▸ Edit Body, checkmark state — 11-command-nexus.md). **Return in Preview also enters Edit** — the board's edit key applied to the body; fixed grammar like the board's Return, not a menu item. **Escape in Edit returns to Preview.**
|
||||
- **Clicking never edits** (the same pivot as the board's click grammar, 04-interactions.md): clicking the rendered body selects text — Preview is selectable and copyable — and nothing else. The one interactive exception is task-list checkboxes, below.
|
||||
- **Leaving Edit flushes the debounce** (mode flip, raw-source entry, window close) — the preview never lags the text that produced it, and neither does disk.
|
||||
|
||||
### Preview
|
||||
|
||||
- Renders headings, bold/italic/code, bullet/ordered/task lists, fenced + indented code, nested quotes, GFM tables (per-column alignment, columns sized to contents with the browser sizing rule), thematic breaks, HTML shown **verbatim as literal code-styled text** (never interpreted — no web view, per 00-vision.md's no-web-tech stance), and images resolved against the card's own folder (``).
|
||||
- **Remote images are never fetched** — Preview does no networking (sandbox-quiet, files-first). An `` renders as a quiet placeholder chip carrying the alt text (or the URL); the file-relative form above is the supported image story.
|
||||
- **Task-list checkboxes are live**: clicking a `- [ ]` / `- [x]` checkbox flips exactly that marker in the source — a single-character textual edit; every other byte of the body is untouched. This is the deliberate exception to "Preview only reads": checklists are kanban's working currency, and a mode flip to tick a box is ceremony. A toggle is an ordinary user edit — the standard atomic write, auto-committed and undoable on git boards.
|
||||
- Links: external URLs open in the browser; relative links open the target file with its default app (resolved against the card folder, like images).
|
||||
- **Edit ▸ Find (⌘F) is find-in-text here** — the standard find bar over the focused body surface (Preview's selectable text, the Edit editor, raw source); board search is a board-window concern (04-interactions.md ▸ Search).
|
||||
|
||||
### Edit
|
||||
|
||||
- A monospaced editor with **lightweight Markdown syntax highlighting** — headings emphasized, bold/italic rendered as such, code tinted, link targets and structural markers dimmed. Highlighting is presentation only: the text is the raw Markdown, character for character — no hidden transforms, no smart substitutions.
|
||||
- Saved on a ~700 ms debounce; flushed on leaving Edit, entering source mode, and window close.
|
||||
- **⌘Z here is the text view's own undo** — session-scoped, ending when the editor loses focus or the mode flips; it works on every board, git or not. Board-level undo routing and commit granularity (one commit per Edit session — the Edit→Preview flip is the effective Save button; never per save tick): 06-history-undo.md ▸ Undo routing.
|
||||
|
||||
### Write rules (settled, storage-facing)
|
||||
|
||||
- **An untouched body is never rewritten** — plain string comparison, so it stays byte-identical on disk. Three-gate write: untouched → never re-serialized; reverted → not written; echo of an external edit → not written back.
|
||||
- **Concurrent external edits — dirty buffer wins.** A dirty Edit buffer is never reloaded under the cursor: while the user has unsaved keystrokes, watcher reloads update everything else (board, Preview, other windows) but leave the buffer alone; the debounced save then writes it — deliberate last-writer-wins, the same no-merge-UI philosophy as sync (07-sync-collab.md). A clean buffer follows disk. On git boards, flush-before-overwrite (06-history-undo.md) guarantees the overwritten external version was committed first — one revert away; on no-git and repo-nested boards it is lost — the app manages no git there, so flush-before-overwrite never runs (07's accepted caveat).
|
||||
|
||||
## Raw source outlet
|
||||
|
||||
A toggle (View ▸ Raw Source, ⌥⌘E — 11-command-nexus.md) swaps the **entire content area — title, body, and sidebar —** for the literal on-disk `index.md` (frontmatter and all) in a monospaced editor with Cancel/Apply: the same frontmatter is being edited as raw text, so interactive controls over it would fight the raw edit. Entering source mode flushes any pending title/body edits first, then reads the file fresh from disk. Apply validates through the same fail-fast parse the loader uses (detailed alert on error, stays in source mode) before writing byte-for-byte; the watcher reload then refreshes every window. Cancel (and window close) discards without ceremony. This is the escape hatch that keeps *everything* — unknown keys, exotic formatting — reachable in-app.
|
||||
|
||||
Key grammar in source mode, completing the window's key story: **Escape is Cancel**, **⌘↩ is Apply**, and toggling off via ⌥⌘E (menu or toolbar) is **Apply too** — leaving-by-toggle commits, mirroring leaving-Edit-flushes; a failed validation keeps source mode open (toggle stays checked) with the alert. Return just types — it's an editor. View ▸ Edit Body (⌘E) disables while source mode is active, matching its toolbar item.
|
||||
|
||||
## The attributes sidebar
|
||||
|
||||
Stacked sections under small-caps headers, in this order; quiet rows, read-optimized, edit affordances only where a section edits. The sidebar is the card's non-body inventory — future attributes (enhanced-schema fields, if they ever ship) slot in as new sections rather than growing new bars.
|
||||
|
||||
### Attachments
|
||||
|
||||
- Shows **every top-level file of `attachments/`** — including files also embedded in the body (settled: the section is the card's complete file inventory, no reference-tracking magic; an image appearing in both places is honest, not a bug). Subfolders are tolerated but not surfaced (01-storage-format.md's attachments rules).
|
||||
- **Compact rows**: small QuickLook thumbnail (Finder-icon fallback) + middle-truncated filename, one row per file. The section header carries a quiet add affordance; empty, the section stays with a one-line hint (drop files, or File ▸ Add Attachment…, ⇧⌘A) — the drop surface remains the **whole window** (name collisions auto-rename, Finder-style — 01-storage-format.md). **Drop precedence is split by payload** (settled): file drops import as attachments anywhere in the window — Edit mode included, the text editor never intercepts a file drop; dragged *text* lands in the Edit editor at the caret within its bounds as ordinary insertion, and is inert elsewhere in the window.
|
||||
- Row interactions: double-click or Return opens; context menu Open / Reveal in Finder / Remove (moves to the **system** Trash, never hard-deletes — 03-board-ui.md's naming constraint keeps this distinct from board deletion); rows drag out their file URL.
|
||||
- **Keyboard-native, new in the rewrite** (the pathfinder's strip was pointer-only): the section is focusable; arrows move between rows, **Space QuickLooks** the selected row, Return opens it, ⌫ removes it (same system-Trash semantics).
|
||||
|
||||
### Style
|
||||
|
||||
The card-level styling home: the **embedded style editor** — background palette grid (with the leading None well) and curated symbol grid, per 03-board-ui.md ▸ Styling ▸ Controls. Card styling is discoverable here without a context menu; the same component appears in the board popover and behind Style….
|
||||
|
||||
### Details — unknown frontmatter keys
|
||||
|
||||
- **Read-only key/value rows for every unknown frontmatter key**, shown only when any exist, in file order (01-storage-format.md preserves key order verbatim — the sidebar honors it). This is the files-first payoff surfaced: an agent overlay (`project:`, `sphere:` — 08-agent-integration.md) or any hand-added key is visible to the human without opening raw source.
|
||||
- Values render as plain text, leniently — exotic YAML shapes display best-effort, never error. Reserved enhanced-schema keys (`labels`, `assignees`, `due`, `remote`, …) are ordinary unknown keys in this version and appear here like any other — no special rendering.
|
||||
- Editing is deliberately not offered: the **raw source outlet** is the write path for frontmatter the app doesn't own.
|
||||
|
||||
### History (git boards only)
|
||||
|
||||
- **The card's commit trail, read-only** (settled): every commit that touched this card's folder, newest first — semantic subject, relative date, author. The trail is where the commit-message and attribution machinery (06-history-undo.md) pays off for humans: "Move card 'X' to Doing — 2 days ago — Claude" reads as a story, agent and hand edits included (foreign authorship and `modified-by` refinement render as the author).
|
||||
- The listing **follows the card across lane moves** (path changes; the UUID folder is the identity to track). A cross-board arrival starts fresh — the import boundary remints identity (01-storage-format.md), and the old board keeps the old trail.
|
||||
- The section is **absent** on boards without app-managed git (mode none, repo-nested) — same honesty rule as the popover's git section (06-history-undo.md). Rows are focusable (arrows), but carry **no actions in v1** — restoring an old version stays a git-client task for now; a per-row forward-restore and lane history are wishlist items, deliberately.
|
||||
- Menu path (contract): **View ▸ History** in the card window focuses the section — no default chord.
|
||||
|
||||
### Actions (bottom)
|
||||
|
||||
- **Delete** — tombstones the card (destructive styling; the window then dismisses itself per Deletion & lifecycle below; recoverable from the board's trash quasi-lane).
|
||||
- **Reveal in Finder** — the card's folder.
|
||||
|
||||
## Window
|
||||
|
||||
- **The subtitle shows the card's place**: "⟨board⟩ › ⟨lane⟩" under the window title, live-updating as the card moves (the window follows its card).
|
||||
- New windows open at the last-used card-window size, cascaded; frames restore per card across relaunch where state restoration allows.
|
||||
- **Toolbar (settled — 03-board-ui.md ▸ Toolbar)**: default set Edit Body (single toggle, on-state in Edit) · Raw Source (toggle; while active, Edit Body disables) · Add Attachment; user-customizable like the board window's.
|
||||
|
||||
## Deletion & lifecycle
|
||||
|
||||
- The window follows its card across lanes (keyed by board URL + GUID) — *within its board*. A **cross-board move dismisses the window like a delete**: the card left this board — its UUID travels with the move (reminted only on an import-boundary collision, 01-storage-format.md's identity lifecycle), but the window's key is board URL + GUID, and the board half no longer names it.
|
||||
- Window dismisses itself if the card is deleted — and a **tombstone counts as deleted**: ⌫ on the board closes the card's open window (the card is gone from the board's perspective; Put Back and reopen if it was a slip). Cards in the shown trash quasi-lane don't open at all — restore first (03-board-ui.md). Reopening a live card focuses the existing window.
|
||||
|
||||
## Changes from Kanban
|
||||
|
||||
- **Two-column composition** replaces the pathfinder's vertical title/strip/body stack: body column (title atop it, created/modified line beneath) plus a full-height attributes sidebar.
|
||||
- **The sidebar revives the dropped readouts**: created/modified return under the title; unknown frontmatter keys get the read-only Details section (the pathfinder's inspector casualties, rehomed).
|
||||
- **Empty body opens in Edit**; Return in Preview enters Edit; Escape returns to Preview (the pathfinder always opened in Preview, toggle-only).
|
||||
- **Live task-list checkboxes in Preview** — the pathfinder's preview was fully inert.
|
||||
- **Syntax-highlighted editor** — the pathfinder used a plain monospaced `TextEditor`.
|
||||
- **Keyboard-navigable attachments** with Space-QuickLook — the strip was pointer-only.
|
||||
- The window gains a live board › lane subtitle.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — the embedded-attachment duplication question is closed (the sidebar shows every file), and the click-grammar pivot is recorded in 04-interactions.md.
|
||||
@@ -0,0 +1,79 @@
|
||||
# History & Undo
|
||||
|
||||
Git is the undo substrate — on boards that have git. **Git is opt-in per board (a pivot from the pathfinder, which auto-initialized every board): a board may be created without git, and git can be added later** (via the board popover; see 07-sync-collab.md's mode progression). A board without git has **no undo/redo** (board history, that is — text editors keep their standard typing undo everywhere; see Undo routing below) — consistent with the settled no-undo stance for repo-nested boards; deletes are the exception, recoverable on every board via the tombstone trash (03-board-ui.md). On git-enabled boards, every settled change auto-commits; those mechanics are carried over from the pathfinder with their hard rules intact.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Opt-in init**: adding git to a board initializes a local repo at the board root. Bundled libgit2 — no git install required. No silent auto-init, ever.
|
||||
- **Adoption**: a board whose root already contains `.git` opens **in git mode, silently** — adoption is not init. The no-silent-auto-init rule forbids *creating* a repository the user didn't ask for; recognizing one that exists is the opposite of that: the repo's presence *is* the opt-in (someone ran `git init` or `git clone`), and this is the primary way a second machine joins a shared board — clone in a terminal, open in the app (07-sync-collab.md's second entry arrow). All git-mode behavior applies from the first open: auto-commit, undo reseeded from the existing HEAD's first-parent ancestry, remote tracking if a remote is configured.
|
||||
- **Detection is nearest-`.git`-wins**, checked at every board open: `.git` at the board root → git mode (adoption above); no `.git` at the root but one at any ancestor → repo-nested (below); neither → mode none. A board can therefore change mode between opens (e.g. the user ran `git init` in a terminal) — the app just reflects what it finds. **Open-time only, deliberately**: a `git init` under an open mode-none board takes effect at the next open — the running session keeps its mode, and the watcher does not scan for `.git` appearing (no mid-session mode flips; stated here so it isn't rediscovered as a bug).
|
||||
- **Boards nested inside an existing repository are left strictly alone** — git cannot be added to them (no nested repo, no commits into the user's repo), so they get **no undo** (settled; no app-managed undo journal, which would violate self-containment). The board popover's git section must say so honestly: not a hidden "add git" but a short explanation ("this board lives inside a repository; Lanework leaves it to that repository") — the option is absent because it *can't* apply, and the UI should teach that rather than look broken.
|
||||
- **Auto-commit**: every settled change (debounced past drag/typing churn) commits with a descriptive message ("Move card 'Fix login' to Doing"). Board undo sees **Edit sessions, not save ticks** (resettled from typing-settle granularity): the body editor's ~700 ms disk saves (05-card-window.md) keep the file crash-safe throughout a session but stay **uncommitted** — the body commit lands when the session ends, the **Edit→Preview flip being the effective Save button** (raw-source entry and window close end the session too). The committer **stages around open Edit sessions**: a board change committing mid-session excludes the session card's folder from staging, so a lane move never sweeps half-typed body text into its commit. Settled-tree events that cannot wait — a pull's flush-before-overwrite — commit the session's on-disk saves as-is (a mechanical exception; 07-sync-collab.md's same-card signpost covers the visible half); branch switch instead gates on explicit save-or-discard (Branch switching below). The cadence constraint below demands batching at least this coarse. **Board-window close and app quit flush the pipeline** — any pending editor save (05-card-window.md), then the pending auto-commit — before teardown; nothing settled is ever left unsaved or uncommitted by closing.
|
||||
- **Undo routing is by focus** — the platform's first-responder rule, stated here because two undo systems coexist. While a text-editing surface is focused (card title field, body Edit mode, raw source, board inline rename), ⌘Z/⇧⌘Z are that editor's own **text undo** — standard, transient, session-scoped: leaving the editor (mode flip, focus loss, close) ends the session, and from then on that content's undo story is the git trail. Text undo works on **every** board — no-git and repo-nested included; "no undo/redo" above means board history, not typing. With focus anywhere else, Edit ▸ Undo/Redo are git undo (and are disabled on boards without it). **No fall-through**: exhausting a focused editor's stack beeps; it never reaches board history.
|
||||
- **Flush-before-overwrite**: before an app write overwrites on-disk state that differs from the last-loaded snapshot (an uncommitted external change — e.g. an agent's body rewrite racing the card editor's debounced save, 05-card-window.md), the pending auto-commit is flushed so the external version enters history first. "Both versions exist as commits" is thereby a guarantee, not a likelihood. The same flush settles the tree before a pull runs (07-sync-collab.md).
|
||||
- **Cadence constraint** (agreed): the auto-commit cadence must not make the history of a remote-shared board unbearable — one commit per drag is fine for a local undo trail but noisy as a shared log. The debounce/batching design here must serve both consumers; the push/pull side is settled in 07-sync-collab.md (optional push-on-commit, automatic fetch-rebase-push on rejected pushes).
|
||||
- **Undo never rewrites history.** Undo (⌘Z) and redo (⇧⌘Z) restore earlier states as **new forward commits** — never reset, never force. The whole trail stays inspectable in any git client. The one deliberate rewrite anywhere in the app is pull's rebase of **unpushed local** commits (07-sync-collab.md); published history is never touched.
|
||||
- **Undo survives relaunch**: the undo stack reseeds from HEAD's first-parent ancestry on load; redo starts empty. In-session it behaves as classic dual stacks; after relaunch, past restore commits reappear as ordinary undoable steps. Deliberate: no sidecar state, nothing ever lost. Interaction with pull (07-sync-collab.md): a pull rebases unpushed local commits, so the in-session stack must remap onto the rewritten commits — the pre-rebase hashes are orphaned. A pleasant consequence of the reseed rule: the fetched remote commits sit in HEAD's first-parent ancestry, so after the next relaunch remote work becomes ordinary undoable steps too.
|
||||
- **Undo is board-local.** A cross-board move-out undone at the source resurrects the card even though it lives on in the destination — per-board histories cannot and must not mutate other boards. The resulting same-UUID fork across boards is legitimate (boards are independent identity namespaces); if the two ever meet through a move-in, the import boundary remints the arrival (01-storage-format.md's identity lifecycle).
|
||||
- The git surface lives in the **board popover** (03-board-ui.md): branch/source display, branch switching and creation, and the commit-identity name/email fields (see Interaction with external writers below) — alongside board rename and styling.
|
||||
|
||||
## Commit messages
|
||||
|
||||
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` renumbering, `modified`/`created` — produces no events: a diff touching only those composes nothing.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
**The external gap, closed** (the pathfinder weakness this section exists to fix): the composer is origin-agnostic, but external writers routinely touch what the pathfinder's diff never modeled — `labels`, `assignees`, `due`, custom frontmatter keys — so their commits degraded to a generic fallback even though the attribution machinery knew plenty. The rewrite:
|
||||
|
||||
- **The diff models the full schema-1 surface**: label, assignee, and due changes compose ("Relabel card 'X'", "Assign card 'X'", "Set due date on card 'X'"); a change to any unmodeled or custom key composes a named generic ("Update card 'X'") — never a board-level shrug when the touched item is identifiable.
|
||||
- **Foreign commits speak the same vocabulary.** Origin lives in the author field (structural attribution below), not in message prose — a foreign move reads "Move card …" exactly like an app-mediated one, and any git client filters by author.
|
||||
- **The launch catch-up commit composes too**: changes found pending at board open diff HEAD's tree against the working tree through the same composer, instead of committing blind.
|
||||
|
||||
## Branch switching
|
||||
|
||||
Switching (or creating-and-switching) a branch from the board popover:
|
||||
|
||||
- **Settle the editors first — explicitly, never silently.** Branch switch neither silently commits nor silently abandons an open card-body Edit session: if any open card window has one (unsaved keystrokes, or on-disk ~700 ms saves the session hasn't committed — the mid-session state Auto-commit above deliberately leaves uncommitted), the switch presents a **save-or-discard step**: **Save All** ends every session with its normal commit (each card's Edit→Preview flip), **Discard** reverts buffers and uncommitted saves to HEAD, **Cancel** keeps the current branch and the sessions. Silently flushing the commit alone would be wrong twice over: the tree can be clean precisely because a save hasn't landed, and a later debounced save would write old-branch text onto the new branch's card. With sessions settled, the pending auto-commit flushes (flush-before-overwrite above) and checkout runs on a truly settled tree: it cannot fail dirty, and no in-flight work is lost or dragged across branches. (Inline title editors need no step of their own: reaching the popover's branch controls commits them — click-away commits, and board-scoped commands disable while one is focused — 04-interactions.md ▸ Grammar.)
|
||||
- **The undo/redo stack does not survive a switch.** It is discarded and reseeded from the new HEAD's first-parent ancestry — the relaunch rule applied at switch time; redo starts empty. (Replaying a restore commit from the previous branch onto the new one would be wrong.)
|
||||
- **Everything remote-facing tracks the current branch**: ahead/behind, Pull/Push, and push-on-commit all operate against the current branch's upstream. On a branch with no upstream yet, the first push — manual or push-on-commit — **creates it on the remote quietly** (`push -u` semantics): creating a remote branch is non-destructive, and quiet is consistent with push-failures-never-nag (07-sync-collab.md). Genuine failures queue with the badge as usual.
|
||||
- The switch itself is bracketed (02-architecture.md): watcher suspended, one full reload at the end. If that final reload fails, the board locks read-only until a successful reload — see 02's live-reload resilience; the on-screen snapshot is from the previous branch and must not be edited over the new one.
|
||||
|
||||
## Interaction with external writers
|
||||
|
||||
Agent and hand edits arrive through the watcher like any change and get auto-committed on the same debounce — so agent work is undoable, attributed, and *described* in the same trail: foreign changes compose through the same message engine as app-mediated ones (Commit messages above — the pathfinder's generic "External edit: 2 cards changed" fallback is gone), with origin carried by the author field. One attribution exception: the app's own agent-guide writes (08-agent-integration.md) are the app's own Writer operations — app-mediated by the echo machinery, carrying the guide's version-marker first line — and committed as "Update agent guide (vN)", not "External edit". Known quirk, not a bug: undoing an "Update agent guide (vN)" commit restores an older guide that the app immediately re-upgrades — a one-bounce no-op undo (restore commit + fresh upgrade commit). Harmless; the guide is app-owned and self-healing by design.
|
||||
|
||||
**Commit attribution is structural, not just a message convention.** The Writer/echo machinery lets the auto-committer classify every observed change as **app-mediated** (the user acting through the app) or **foreign** (anything else). User-driven commits carry the user's git identity; foreign changes are committed under the pinned synthetic author **`Lanework External <[email protected]>`** — so any git client can filter, log, and blame by origin. The strings are API (users script against them; the `.invalid` TLD honestly marks a non-routable synthetic identity) — they change with the deliberateness of a schema change.
|
||||
|
||||
**Where the user's git identity comes from** (no git install is assumed, and the sandbox doesn't read `~/.gitconfig` — honest limits, not bugs): **repo-local `.git/config` wins when present** — standard git semantics, readable in-sandbox because it lives under the board root, and the natural state of adopted/cloned boards. The board popover's git section exposes name/email fields that **write that repo-local config** — the setting *is* the file, portable to any git client, per-board by nature (work and personal boards can differ). Absent repo config, the **derived default** applies: the macOS account's full name plus `shortname@hostname` — git's own no-config fallback shape, zero ceremony. Commits pushed to a forge under the derived email won't link to a forge account; the popover fields are the fix when that matters. A debounce window containing both kinds is **split into two commits**, never mixed (flush-before-overwrite already orders them: foreign first, then the user's overwrite). Honest limit: the app distinguishes app-mediated from foreign, not human from agent — a hand edit in a text editor and an agent write look identical *unless the writer says otherwise via `modified-by` (below)*. Agents wanting precise attribution are encouraged (via the agent guide, 08-agent-integration.md) to commit their own changes; the app follows along.
|
||||
|
||||
**`modified-by` refines foreign attribution** (the self-reported provenance key — 01-storage-format.md): when every file changed in a foreign debounce window carries the same `modified-by: X`, that commit is authored as **X** with the synthetic email `<slug>@agents.lanework.invalid` (display name verbatim, email local part slugified; the domain marks self-reported identity, distinct from both the user and the generic external author). Any disagreement between stamps, any unstamped changed file, or any folder deletion in the window falls back to `Lanework External`. Same trust level as self-committing — it's what the writer claims, accepted as such; the stale-stamp hand-edit case (01) is the known misattribution edge. Self-committing remains the precise path; the stamp is the lightweight middle.
|
||||
|
||||
**Two writers, one repository — the designed situation, not an edge case.** Self-committing agents mean the auto-committer shares the repo with concurrent `git` processes, and it must be graceful about it:
|
||||
|
||||
- **`index.lock` contention is never an error.** If the auto-committer finds the index locked (an agent's commit in flight), it backs off briefly and retries; if the lock persists, it simply re-debounces — the pending changes are still pending, and the next quiet moment commits them. No banner, no log-worthy failure: a held lock is another writer doing its job. (Genuine commit failures — disk full, repo corruption — are different: files stay safe on disk but history stops advancing; surfaced per 02-architecture.md ▸ Write-failure surfacing, retried on the next debounce.)
|
||||
- **A clean tree is the happy path, not a malfunction.** When the debounce fires and the tree has nothing to commit — the agent already committed its own work — the auto-committer no-ops silently. The agent's commit, under the agent's own authorship, *is* the record; that is precisely what the self-commit recommendation is for.
|
||||
- **An agent's `git add -A` can sweep up the user's not-yet-committed app-mediated changes** under the agent's authorship, muddying structural attribution for that window. Accepted limit — the app cannot police another process's staging; the agent guide (08-agent-integration.md) tells agents to commit only their own paths, which keeps well-behaved agents honest.
|
||||
|
||||
## Repository hygiene
|
||||
|
||||
- **`.gitignore` seeded at init, never touched after.** Adding git to a board writes a minimal `.gitignore` (`.DS_Store`) if none exists; the app never edits an existing one and never manages the file afterward — it's the user's from then on. (Repo-nested boards have no app-managed git, so no app `.gitignore` either.)
|
||||
- **Repo growth is accepted.** Unbounded history is the price of never-rewrite, and every attachment version lives in the repo forever. The app may run safe libgit2 housekeeping (repacking loose objects) periodically — it rewrites nothing. Content-removing compaction is explicitly out (it would rewrite history); size tooling joins the wishlist if growth ever bites in practice.
|
||||
- **Deleting never forgets.** On a git board, deleting a card removes it from the board but never from history — every version of its content and attachments stays reachable in any git client, and even the future tombstone purge (01-storage-format.md) only cleans the working tree. This is part of the design; users should learn it here, not from a repo browser.
|
||||
|
||||
## Changes from Kanban
|
||||
|
||||
- **Commit granularity resettled to Edit sessions**: body commits land at the Edit→Preview flip (the effective Save button), not at typing-settle; the committer stages around open sessions (Rules ▸ Auto-commit).
|
||||
- **Branch switch gates on explicit save-or-discard** for open Edit sessions instead of silently flushing (Branch switching).
|
||||
- **Commit messages upgraded for external writers**: full schema-1 diff surface, foreign commits in the same vocabulary (no "External edit" prose), semantic launch catch-up, untitled-item rendering (Commit messages).
|
||||
- The rewrite should extract the old repo's AI-ANALYSIS-git-operations.md conclusions (forward-restore model, C3/C8) into a short normative doc rather than re-deriving them.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Board-wide history browser / timeline view** (the old C8 direction): decided — this is a tracker-integration feature, belonging to the out-of-scope integration story (07-sync-collab.md). The commit trail remains its natural substrate whenever that feature happens; nothing here needs to prepare for it beyond the existing undo-never-rewrites rule. **One deliberate carve-in**: the card window's read-only per-card History section (05-card-window.md) — a scoped log view, not a browser; per-row restore and lane history stay on the wishlist.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — the remote-cadence concern is promoted to a design constraint (see Rules above); the push/pull mechanics are settled in 07-sync-collab.md.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Sync & Collaboration
|
||||
|
||||
Every board has exactly one **collab mode** at a time, but the mode is not fixed at creation — it can evolve over the board's lifetime:
|
||||
|
||||
```
|
||||
none ──add git──▶ git ──add remote──▶ git + remote
|
||||
▲
|
||||
open a board that already has .git (clone, git init)
|
||||
```
|
||||
|
||||
A board may be created plain — **without any git repository** (a pivot from the pathfinder's auto-init) — gain git later, and later still gain a remote; the remote can be added or changed at any point. The second entry arrow is **adoption** (06-history-undo.md): opening a board whose root already contains `.git` lands directly in git mode (or git + remote, if the repo has one) — the normal path for a second machine joining a shared board: clone in a terminal, open in the app. Mode is detected at every open, nearest-`.git`-wins: root → git mode; ancestor only → repo-nested (left strictly alone, 06); neither → none. The arrows are one-way upgrades in spirit (removing git from a board is not a designed flow), but nothing about a board's mode is locked in on day one. The exception will be tracker-backed boards (a board *representing* an external tracker), whose mode is inherent to their creation — but tracker integrations (Gitea, GitHub, GitLab, Jira) are **out of scope for this design**. The schema keeps `remote`/`remote-state` reserved (see 01-storage-format.md) so a future connector isn't foreclosed, but no componentry, board mode, or UI is designed for them here.
|
||||
|
||||
## Mode: none (local-only)
|
||||
|
||||
Plain folders on local disk. **No git repository at all** — and therefore, since git is the undo substrate, no undo/redo (06-history-undo.md). FSEvents live-reload works as on any board. Adding git later initializes the repo and moves the board to git mode. Honest caveat: without git there is no commit-before-overwrite protection, so on a no-git board **real data loss is possible** (e.g. concurrent or external overwrites) — accepted; adding git is the remedy. Repo-nested boards share this caveat: a repo exists, but the app manages no git there (06-history-undo.md), so its protections never run — committing is the user's own workflow. Deletion is the exception: the tombstone trash (03-board-ui.md) makes deletes recoverable even without git — overwrites are the lossy case, and Empty Trash is deliberate.
|
||||
|
||||
## Mode: git
|
||||
|
||||
The board is a git repository (the 06-history-undo.md substrate — undo/redo, auto-commit trail), optionally with a **remote**. With a remote configured:
|
||||
|
||||
- Push/pull becomes a sharing mechanism between machines/people at file-level granularity; the fractal one-item-one-file design keeps conflicts rare and small (a reorder touches one file).
|
||||
- **Remote tracking lives in the board popover** (03-board-ui.md): ahead/behind indicator plus manual **Pull** and **Push** controls — which are also Board-menu items (no default chord, remappable — 11-command-nexus.md; the configuration carve-out is 04-interactions.md's).
|
||||
- **Which remote is the board's remote** (settled — adopted clones can carry several): resolution is git's own defaulting — the current branch's upstream remote; else `origin`; else the repo's sole remote. The popover names the remote it tracks, its change-remote control edits exactly that one, add-remote on a remote-less repo creates `origin`, and 06-history-undo.md's quiet first `push -u` targets the resolved remote (recording it as the upstream, which pins resolution thereafter). The unresolvable case — several remotes, no upstream, none named `origin` — is surfaced honestly: remote operations disable and the popover offers a one-time remote picker, whose choice becomes the branch's upstream on the next push.
|
||||
- **Optional push-on-commit**: a per-board setting (stored app-side in the board registry — 02-architecture.md's per-board app state); when enabled, every auto-commit is pushed immediately.
|
||||
- **Push failures never nag.** A push rejected as non-fast-forward (another machine pushed first) triggers an automatic **fetch → rebase → push**, with bounded retries — the same rebase machinery as Pull, so it cannot block and cannot conflict. Manual Push behaves identically. Stated plainly: enabling push-on-commit implicitly accepts that remote commits may land in the live board whenever pushes race — consistent with the board's live-reload nature, but it should be learned from the design, not discovered. All other push failures stay quiet: pushes queue, the ahead/behind indicator in the board popover carries the pending count and the last error, and pushing resumes automatically on the next commit or manual Push. No modals, no per-commit errors. The one refinement: **authentication failures pause rather than retry** — see Remote authentication below.
|
||||
- **There can be no conflicts — and no data loss.** Every edit becomes a commit before anything can overwrite it (auto-commit settles local changes; the tree is clean by the time a pull runs). A pull — manual, or the automatic fetch-rebase after a rejected push — runs only at **interaction rest**: it queues behind an in-flight drag or open inline editor (the same settled-change notion the auto-commit debounce uses), flushes the pending auto-commit (06-history-undo.md's flush-before-overwrite), then runs bracketed (02-architecture.md's live-reload resilience) — never an error dialog, never a board yanked mid-drag. An open card-body **Edit session neither blocks a pull nor is interrupted by one**: the flush commits the session's on-disk saves as-is (06-history-undo.md's mechanical exception to session-granularity commits) and the rebase runs; when the pulled commits touch the very card being edited, the card window **signposts** the remote change (a transient banner, no modal, no merge UI) while the dirty buffer stays put and wins per 05-card-window.md — the losing remote version is a commit, one revert away. A pull fetches the remote's commits and **rebases local commits on top of them**; where a rebase hits a genuinely conflicting hunk, the **local side wins** — always, with no configuration. (This is the one deliberate history rewrite in the app, and it only ever touches unpushed local commits — see 06-history-undo.md's undo-never-rewrites rule.) Crucially, resolution discards nothing: the losing remote version survives intact in the fetched commits below, so an edit that "lost" the rebase is visible in any git client and one revert away. What may *appear* as data loss is always recoverable. No interactive merge UI, no conflict markers written by the app, sync never blocks. Conflict markers encountered in files (from git activity *outside* the app) fail fast only where they break parsing — markers in or around the frontmatter make the file the malformed-input case the loader rejects loudly with the offending path. Markers wholly inside a Markdown body are, honestly, valid input: they load fine and render as body text, and the app deliberately doesn't police body content to detect them (stated stance, not an oversight). (The old repo's AI-THINKING-merge-conflicts.md explored this territory; mine it when specifying the rebase mechanics.)
|
||||
|
||||
## Remote authentication (settled)
|
||||
|
||||
Everything above assumes credentials exist; this is where they come from. Constraints first, stated as honest limits: a sandboxed app with bundled libgit2 cannot read `~/.ssh` (no entitlement grants it — silent access to every key is exactly what the sandbox exists to prevent), cannot reach `ssh-agent` (a unix socket outside the container; this also rules out 1Password/Secretive/YubiKey agents), cannot run the user's credential helpers, and gains nothing by shelling out (children inherit the sandbox). The network-client entitlement is assumed. Auth is therefore app-native, and **the Keychain is the only credential store** — credentials never live in board files or repo config. That is the deliberate inversion of files-are-truth: secrets are the one thing that must never be a file in the board.
|
||||
|
||||
- **Transports: HTTPS and SSH, both Keychain-backed.**
|
||||
- **HTTPS (primary)**: username + token (forge PATs; plain basic auth for generic hosts), stored as a Keychain internet password keyed by **host + username** — git's own scoping model, shared across boards: one GitHub token serves every board, and two accounts on one host coexist as two usernames. **Which username a board uses is the remote URL's business** (git's own answer, and the HTTPS analogue of the SSH per-host table): a username in the URL (`https://alice@host/…`) selects the Keychain item `host + alice`, and the popover's credential capture stamps the entered username into the remote URL in repo config — the URL is the assignment record, no app-side state (the secret itself stays in the Keychain). A URL naming no username resolves to the host's sole stored username; when a host has several, the popover's username field becomes a picker and saving stamps the choice into the URL, while background operations treat the ambiguity as **Authentication needed** (pause and badge, never guess — the same posture as auth failure).
|
||||
- **SSH — Keychain-resident keys, never key files.** Each Mac has a **Lanework key**: an app-generated ed25519 keypair whose private half lives as an ACL-protected Keychain item and is handed to libssh2 from memory — it never exists on disk. The board popover shows the public key with a Copy affordance; the user adds it to their forge like any machine key. An **existing key imports by paste or drag** (a one-time read under user intent): copied into the Keychain — passphrase entered once at import, stored under Keychain protection thereafter — and the original file is never referenced again. Per-machine identity, per-Mac revocable on the forge — the ssh-idiomatic shape. (Secure Enclave-backed keys — non-exportable, custom sign callback, P-256 — are a possible later hardening, not v1.)
|
||||
- **Key scope: app-level objects, per-host assignment.** Keys are never board state — the machine key plus any imports live app-wide (Keychain), and each SSH host maps to one of them: default the machine key; importing a key during a host's setup assigns it to that host. A "host" is `hostname[:port]` parsed from the remote URL — the same endpoint identity the TOFU fingerprint store uses (OpenSSH's own `[host]:port` convention); the URL's username (`git@`) disambiguates nothing and stays out of it. The assignment table holds **only overrides** — no entry means the machine key, so the default costs zero records and removing an override self-heals to it. The popover's key picker is labeled per-host ("key for github.com"), which teaches the one cross-board consequence: switching a host's key switches it for every board on that host — the same rotate-once-follow-everywhere behavior as HTTPS tokens. Housekeeping stays small: an import referenced by no host row can be removed; the machine key only regenerates (confirm-gated — it invalidates the old public half on every forge), and that is the entire rotation story. The board popover is only the surface — it shows the key for *that remote's host*, the way the commit-identity fields front repo-local config. Known limit, accepted: two accounts on the *same* host can't be told apart by key (forges bind key→account globally; git's own answer is ssh-config aliases, which live in files the sandbox can't read) — a per-remote key override joins the wishlist if it ever bites.
|
||||
- **Host verification is trust-on-first-use**: with no `~/.ssh/known_hosts` readable, the first connection to an SSH host confirms its fingerprint with the user; accepted fingerprints live app-side in Application Support (02-architecture.md's app-wide state home, host-scoped). A later mismatch **hard-blocks with an explanation** — that mismatch is the attack the check exists for.
|
||||
- **Setup verifies right there.** Adding or changing a remote (board popover — 03-board-ui.md) probes with authentication immediately (ls-remote): missing or rejected credentials surface **inline in the popover** — HTTPS shows username + token fields with a forge-appropriate hint; SSH shows the machine key to copy plus Verify. The user leaves the popover with a remote that demonstrably works, or knowingly not. Boards adopted from a terminal clone (whose auth lives outside the sandbox and can't be reused) hit the same inline flow at the first in-app operation that needs credentials.
|
||||
- **Auth failures pause; they never nag and never hammer.** A push or pull rejected for authentication (expired token, revoked key) is not retried — a dead credential cannot succeed, and hammering invites rate limits and lockouts. The push queue pauses and the popover badge switches to a distinct **Authentication needed** state carrying the error; the popover presents the same inline fields, prefilled where possible. Updating the credential (or fixing forge-side and hitting Verify) resumes the queue. Network failures keep the quiet auto-resume above — only auth pauses.
|
||||
- **Background operations never prompt.** Push-on-commit and the automatic fetch-rebase-push stay silent through auth trouble (badge only); credential capture happens exclusively in the popover, where the user already is when it matters (manual Pull/Push live there too).
|
||||
|
||||
## iCloud Drive — not supported (decided)
|
||||
|
||||
Boards should not live in iCloud Drive. The app makes **no iCloud accommodations**: no NSMetadataQuery watching, no eviction handling, no download triggering, no NSFileVersion conflict resolution. When the user opens or creates a board at a path inside iCloud Drive, the app **warns with a thorough explanation and recommends git integration instead** — it does not hard-block (the user is always right), but the warning must genuinely teach why this is a bad idea:
|
||||
|
||||
- **Git and iCloud corrupt each other.** A board with git enabled (the undo substrate) has a `.git` inside; iCloud syncs `.git` internals — thousands of small object files and constantly-rewritten refs/packs — poorly and non-atomically; partial or reordered sync can corrupt the repository. Two Macs auto-committing the same board produce divergent histories iCloud cannot merge.
|
||||
- **Eviction breaks fail-fast loading.** iCloud may evict any file's contents to free space, leaving a placeholder. An evicted `index.md` is unreadable; with no download-trigger machinery the board simply fails to load with an I/O error until the user manually re-downloads it.
|
||||
- **iCloud conflict handling silently forks files.** iCloud resolves concurrent edits per-file via hidden file versions with no semantic merge; a board edited from two Macs can silently lose ordering or content coherence with no error surfaced anywhere.
|
||||
- **The right tool exists.** Git mode with a remote is the supported multi-Mac/sharing story: atomic commits, real merges, inspectable history, and conflicts that surface loudly instead of silently.
|
||||
|
||||
Warning UX: shown on open/create of an iCloud-resident board, once per board (no nagging; the shown flag lives app-side in the board registry — 02-architecture.md's per-board app state), with the recommendation to move the board to local disk and use git integration for sync. The same warning is reasonable for other file-provider-synced locations (Dropbox, OneDrive, Google Drive) since the failure modes are identical — treat that as an implementation detail, not a separate design.
|
||||
|
||||
**Network volumes (SMB/NFS) get the same once-per-board warning with tailored reasons**: FSEvents delivery is unreliable there, so live reload — the app's central mechanism — silently degrades (external and agent edits may not appear until reopen); and two machines mounting the same volume share one `.git`, a real corruption risk that git-with-remote (each machine its own repo) doesn't have. Same stance as iCloud: **no accommodations** — no polling fallback, no reduced mode; warn honestly (flag in the board registry, 02-architecture.md), recommend local disk + git remote, and respect the user's choice.
|
||||
|
||||
## Known issues (flagged, parked)
|
||||
|
||||
- **Overridden remote edits are not in-app recoverable in-session.** A remote edit that loses the local-wins rebase doesn't enter the running app's undo stack, so recovering it mid-session requires an external git client. The gap is session-scoped only: the rebase leaves a linear history, so the fetched remote commits sit in HEAD's first-parent ancestry and reappear as ordinary undoable steps after the next relaunch (06-history-undo.md's reseed rule). Acceptable for now; revisit when there's an in-app history surface.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — sync surface and semantics are settled: manual Pull/Push in the board popover, optional push-on-commit, pull-rebase where the local side always wins conflicting hunks, automatic fetch-rebase-push on rejected pushes, quiet-badge surfacing of push failures, and Keychain-native authentication (HTTPS tokens + the machine SSH key, verify-on-add, pause-on-auth-failure).
|
||||
@@ -0,0 +1,49 @@
|
||||
# Agent Integration
|
||||
|
||||
AI agents are first-class users of Lanework boards — not through an API, but through the filesystem itself. An agent that can read and write files can read and write a board. The app's design carries several load-bearing guarantees that exist substantially *for* this use case.
|
||||
|
||||
## The load-bearing guarantees
|
||||
|
||||
1. **Unknown-key preservation** — agent overlays add custom frontmatter (`project:`, `sphere:`, `tags:`) and the app preserves hand-added keys and their order verbatim across every rewrite. The schema is safe to extend from outside.
|
||||
2. **Live external-edit reload** — an agent writing a card folder makes the card appear on the open board via FSEvents immediately. Agent write-path and human read-path are the same surface; the agent is just another external editor.
|
||||
3. **Fail-fast validation + atomic writes** — a malformed agent write surfaces loudly with the offending path instead of silently corrupting. The right failure mode for letting AI write into a planning system.
|
||||
4. **Skip-don't-fail for index-less folders** — an agent interrupted between `mkdir` and writing `index.md` cannot brick the board.
|
||||
5. **Git trail** (06-history-undo.md) — on git-enabled boards, agent changes are auto-committed, attributed, and undoable like any others. Foreign changes are committed under a synthetic external author, structurally distinct from the user's own commits; an agent can claim its work lightly by stamping `modified-by` (guarantee 6 below), or precisely by committing its changes itself — the app follows along: a held `index.lock` just makes the auto-committer retry/re-debounce, and a tree the agent already committed is a silent no-op that preserves the agent's authorship (06-history-undo.md). The guide says so, and also tells agents to **stage only their own paths** (no `git add -A`) — a sweep would commit the user's not-yet-auto-committed changes under the agent's name.
|
||||
6. **`modified-by` self-stamping** (01-storage-format.md) — the lightweight attribution path: an agent stamps files it writes (`modified-by: <your-name>`), and the stamp renders on the card window's modified line and authors the agent's foreign commits (06-history-undo.md) — no git ceremony, and it works on no-git boards, where it is the *only* attribution there is. The app clears the stamp on its own writes, so agents should re-stamp on every write, not once.
|
||||
|
||||
## The agent guide (`CLAUDE.md` at board root)
|
||||
|
||||
The app silently maintains a `CLAUDE.md` in every board — a condensed, agent-facing rendition of the schema teaching any agent how to operate on the board directly:
|
||||
|
||||
- Creating cards (mkdir UUID, write `index.md`, ordering rules).
|
||||
- Moving between lanes (folder move), reordering (gapped ranks, only touch the moved item).
|
||||
- Tombstone deletes, colors/icons.
|
||||
- New in the rewrite: **attachments** (the `attachments/` convention, importing files).
|
||||
- New in the rewrite: **`modified-by` self-stamping** — stamp files you write; re-stamp every write (the app clears it on its own writes); self-commit instead when you need exact authorship.
|
||||
- New in the rewrite: a pointer to the optional **`CLAUDE.user.md`** (see below), instructing agents to read it when present.
|
||||
|
||||
Mechanics: version marker in the first line (`lanework-agent-guide vN`); rewritten when missing or older, never downgraded, left untouched when current or newer. To the loader it's just another ignored file.
|
||||
|
||||
**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.
|
||||
|
||||
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).
|
||||
|
||||
## Agent conventions worth specifying (new in the rewrite)
|
||||
|
||||
- **The masterplan pattern** (from the old repo's AI-MASTERPLAN.md): a cross-project board with lifecycle lanes (Inbox → Triaged → Active → Done) where agent sessions file needs as cards at near-zero friction. The design requirement it imposes here: filing a card must need nothing but the schema — no app running, no registration, no index to update.
|
||||
|
||||
## Automation surfaces (beyond raw files)
|
||||
|
||||
Raw files are the primary surface. Candidates for more, all unadjudicated:
|
||||
|
||||
- CLI (`lanework add-card …`) — probably unnecessary; agents handle raw files fine, and a CLI is a second schema client to maintain.
|
||||
- URL scheme / AppleScript / Shortcuts — for human automation more than agents.
|
||||
- MCP server — same schema knowledge packaged for non-file-capable agents.
|
||||
|
||||
Lean: ship none initially; the guide + schema are the API.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — the "while you were away" digest idea moved to the project wishlist (../WISHLIST.md).
|
||||
@@ -0,0 +1,60 @@
|
||||
# Board Templates
|
||||
|
||||
> **Status: settled.** The template inventory is settled (inherit all ten from the pathfinder, more to come); the definition format and the storage story below are signed off.
|
||||
|
||||
## Inventory (settled)
|
||||
|
||||
All ten pathfinder templates carry over: Basic, Classic Kanban, Software Project, Content Pipeline, Job Hunt, Sales Pipeline, Weekly Planner, Roadmap, Event Planning, Bug Tracker — each with its existing lane titles, icons, palette icon colors, board icon, and blurb. Basic stays first and remains the plain To Do / Done scaffold. The set is expected to grow.
|
||||
|
||||
## Definition format (settled)
|
||||
|
||||
**A template is itself a board.** Each template is a schema-valid board folder bundled in the app's resources — the same fractal `folder + index.md` structure, loadable by the same `BoardLoader`, validated by the same rules:
|
||||
|
||||
```
|
||||
<app bundle>/Templates/
|
||||
├── basic.kanban/
|
||||
│ ├── index.md ← title: Basic; template: {order: 100}; body = picker blurb
|
||||
│ ├── 3f2a9c41-…/index.md ← lane "To Do" (order: 1024)
|
||||
│ └── 8d3b1f22-…/index.md ← lane "Done" (order: 2048)
|
||||
├── classic-kanban.kanban/
|
||||
│ └── …
|
||||
```
|
||||
|
||||
- The template board's `index.md` carries the picker metadata in its ordinary frontmatter: `title` = display name, `icon`/`iconColor` = the board icon shown in the picker and inherited by the new board, and the **body** = the blurb (shown in the picker *and* becoming the new board's description, as in the pathfinder).
|
||||
- One picker key: `template:` — a mapping holding picker concerns; **`order` (display position in the chooser) is its only subkey** (settled — future subkeys possible). Carried onto instantiated boards, where it is ignored and preserved like any unknown key; specified in the core schema's board table (01-storage-format.md).
|
||||
- The bundle folder name (`basic.kanban`) is the template's stable slug (tests, a11y ids).
|
||||
- Lane folders are ordinary GUID folders with ordinary `order`/`icon`/`iconColor` frontmatter. Templates *may* contain starter cards — a capability the Swift-struct format never had (e.g. a "How this board works" card).
|
||||
|
||||
### 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; adoption applies to boards the user opens, not boards the app materializes): every new board starts at mode `none`. Beyond the `template:` residue, the result is indistinguishable from a hand-built board.
|
||||
|
||||
### Why this format
|
||||
|
||||
- **Dogfood**: the template format *is* the board format — no second schema, no parallel Swift model to keep in sync. The picker's mini per-lane preview renders from a real `BoardModel` via the normal loader. **One bad template never fails the chooser** (the user store is hand-editable, so a malformed board there is one edit away): a user template the loader rejects is still listed — by folder name, marked unloadable, carrying the loader's fail-fast specifics — but can't be instantiated or previewed; fix the files and it comes back. The same honest-surfacing posture as 02-architecture.md's welcome recents, which don't pre-detect broken boards either.
|
||||
- **Authorable**: adding a template = adding a folder (a human, or an agent, can author one without touching Swift); a template diff in review shows real frontmatter, not struct literals.
|
||||
- **Testable for free**: template validity is enforced by the loader's own fail-fast rules at test time (walk `Templates/`, load each).
|
||||
- **Opens the door** to user-defined templates — see Save as Template below.
|
||||
|
||||
## Save as Template
|
||||
|
||||
A "Save as Template" function copies the current board into the user templates store; the chooser lists user templates after the bundled ones. **The copy is preceded by the close flush** (02-architecture.md ▸ Windows: editor saves, then the pending auto-commit — with the pull-style mechanical exception committing an open Edit session's on-disk saves as-is, sessions staying open), so the template never misses the last keystrokes; the same rule covers File ▸ Duplicate (03-board-ui.md), where the flush also keeps the copied `.git`'s history from lagging its tree. Because a template *is* a board, the copy is nearly literal: **`.git` is not copied** (a template is content, not history — copying it would embed the board's full repo, every attachment version included, in the template store; see 06-history-undo.md's repo-growth note), tombstoned items are dropped, a `template:` key is added — or, when the board already carries one (e.g. it was itself instantiated from a template), its stale `order` is overwritten — with an order appended after existing user templates; GUIDs are left as-is (instantiation mints fresh ones anyway), and timestamps are kept per 01-storage-format.md's copies-keep-`created` rule (equally inert — instantiation restamps them).
|
||||
|
||||
Two edges, settled:
|
||||
|
||||
- **Store collisions auto-rename, Finder-style** (`Board.kanban` → `Board 2.kanban`) — the 01 import precedent: saving never overwrites an existing template and never refuses.
|
||||
- **Strays copy through.** The copy is literal apart from the stated exclusions (`.git`, tombstoned items) — `CLAUDE.user.md`, a seeded `.gitignore`, and other non-schema files carry through Save as Template *and* instantiation alike. Deliberate: a template is the folder, and `CLAUDE.user.md` carrying a board's custom agent instructions into boards born from it is a feature. The app-owned `CLAUDE.md` copies inertly and self-heals to the current guide version when the new board is opened (08-agent-integration.md).
|
||||
|
||||
**Storage (settled): Application Support** (`…/Lanework/Templates/`, inside the app container) as the canonical store — friction-free sandbox writes, no location ceremony — kept honest by a **Reveal in Finder** affordance in the template chooser: revealed, it's plain board folders, hand-editable and agent-writable, and a board folder dropped in becomes a template — **no `template:` key required**. Chooser order: bundled templates by `template.order`, then keyed user templates by `template.order`, then keyless user boards last, sorted by display name (`title` ?? folder name — 01-storage-format.md's board naming). The app never stamps a key into store files it didn't write itself — a hand-dropped board is never touched, and adding a key by hand is how its user picks a position; the one writer of keyed files is Save as Template, whose own copies arrive keyed (above). A user-visible or user-configurable location was considered and set aside as ceremony disproportionate to a secondary feature; revisit if template sharing becomes a real workflow.
|
||||
|
||||
## Rejected alternative
|
||||
|
||||
Keeping the Swift-struct catalog (pathfinder approach). Simpler to ship, but it's a second definition of "a board" that must track schema changes by hand, can't hold starter cards naturally, and forecloses non-programmer template authoring.
|
||||
|
||||
## Settled
|
||||
|
||||
- **No dialect, one schema**: bundled (and user) templates use GUID folder names like any board — no relaxed template-only format.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — the storage location is settled (Application Support as the canonical store, kept honest by Reveal in Finder; a user-visible or configurable location was set aside as ceremony, revisit if template sharing becomes a real workflow).
|
||||
@@ -0,0 +1,61 @@
|
||||
# Accessibility
|
||||
|
||||
The stance is committed in 00-vision.md: **accessibility is a requirement of "native to the bone," not an afterthought.** This document is the design iteration that stance promised — VoiceOver correctness on the custom board, text scaling, and the system visual accommodations. The keyboard-first contract (04-interactions.md) is the floor this builds on.
|
||||
|
||||
## Principles
|
||||
|
||||
- **The keyboard path is the accessibility path.** Every function's drag-free guarantee is its keyboard/menu path (04-interactions.md's contract: every board function has a menu item + shortcut), and VoiceOver rides those same paths. Bespoke accessibility-only UI is a last resort — if a function needs a drag-free alternative, it belongs in the context menu or menu bar, where *all* users get it. Concretely: **card movement is the clipboard** (⌘X, select target, ⌘V — 04's paste-lands-after-anchor/appends-to-lane rules make cut/paste a complete move story), not a parallel set of "Move to lane" accessibility actions.
|
||||
- **Logical order over visual geometry.** Assistive traversal follows the board's semantic order (lane `order`, then card `order`), never rendering artifacts like masonry column position.
|
||||
- **Announce what the files do.** A live board that changes under external writers must say so — silence about a mutating board is a lie to a VoiceOver user.
|
||||
|
||||
## The board through VoiceOver
|
||||
|
||||
- **Tree shape**: window → lanes (accessibility containers, in lane `order`) → cards (leaf elements, in card `order`). A lane container is labeled "⟨title⟩, lane, N cards" — the count reads the search filter like the visible badge (04-interactions.md). The lane header's new-card button is a labeled child ("New card in ⟨lane⟩"). A card is **one flattened element**: label = title (or the untitled placeholder), value carries the attachment count when present, selected state via trait. Face icon and chips are decorative — folded into the element, never separately focusable.
|
||||
- **Logical order, not masonry position** (decided): within a wide lane, VoiceOver reads cards by `order` — the interior grid columns are presentation only. This deliberately diverges from on-screen geometry; the spatial arrow-key model (04-interactions.md) remains available alongside, since board keyboard navigation keeps working with VoiceOver running.
|
||||
- **VO cursor and app selection are independent** (Finder-style): moving the VoiceOver cursor never mutates selection. VO-Space on a card toggles its selection (the ⌘-click analogue — a toggle, never plain click's replace, 04-interactions.md ▸ Selection); ⌘↩ opens the card window; arrow keys and ⇧-arrows drive selection exactly as without VoiceOver. Selection state is always readable from the element (trait), and cut cards expose their dimmed pending state in the value ("cut, pending paste").
|
||||
- **Actions come from the context menu.** Context menus are the single inventory of per-item actions (Open, Rename, Delete, Put Back, Delete Immediately, width stepper, …), reachable the standard way (VO-⇧-M); where SwiftUI additionally surfaces menu items as custom accessibility actions, that's free improvement, not a separate design surface.
|
||||
- **Rotor**: lane titles are headings, so the headings rotor jumps lane-to-lane — on a one-dimensional board that *is* structural navigation; no custom rotors unless practice shows the need.
|
||||
- **Trash quasi-lane**: when shown (View ▸ Show Trash), it is the last container, labeled as Trash with its count; toggling visibility is announced. Tombstoned cards read their deletion state and expose Put Back / Delete Immediately via the context menu; there is no Open (03-board-ui.md's no-editing-in-the-trash). A tombstoned lane's single entry reads "⟨title⟩, deleted lane, N cards" and exposes the same actions; keyboard reachability follows 04-interactions.md's homogeneous-by-kind trash rules.
|
||||
- **Search**: filtered-out cards leave layout and the accessibility tree together — the filter is the single source of truth for "what's on the board" (04-interactions.md), and the tree is one of its readers. Lane counts announced reflect the filter.
|
||||
|
||||
## Moving without dragging
|
||||
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
- **Foreign changes announce, app-mediated echoes never do.** The auto-committer already classifies every observed change as app-mediated or foreign and synthesizes diff summaries for commit messages (06-history-undo.md); announcements reuse that summarizer — one polite (non-interrupting) digest per reload debounce ("Board changed: 2 cards edited, 1 card added"), never per-file chatter. On no-git boards the same classifier runs without the committer — announcements don't depend on git mode.
|
||||
- **A vanishing focus is called out specifically.** If the selected or VO-focused card disappears in a reload (deleted externally, or hidden by a lane tombstone), the announcement names it ("Card 'Fix login' was deleted externally") and focus recovers to the card's lane (mirroring selection's reload-survival rules, 02-architecture.md).
|
||||
- **Bracketed operations announce once, at completion** ("Pulled 3 commits", "Switched to branch 'redesign'") — never their internal churn (02-architecture.md's bracketing). The live-reload-resilience banner (02-architecture.md) is an accessibility element and is announced when it appears and when it clears — including the read-only lock after a failed bracketed reload.
|
||||
|
||||
## Card window, welcome, template chooser, popover
|
||||
|
||||
- **Card window**: standard controls, standard labels. The attributes sidebar is a labeled container of labeled sections; attachment rows are elements labeled by filename; the Details section's unknown-key rows read as static text ("⟨key⟩, ⟨value⟩"); the bottom actions are ordinary buttons. **Preview renders to the accessibility tree as structured text** — headings navigable by rotor, lists and tables read as such; task-list checkboxes are real accessible checkboxes, toggleable without the pointer (05-card-window.md's live checkboxes); body images use Markdown alt text when present, else the filename. Edit and the raw-source outlet are ordinary accessible text editors; the Preview/Edit toggle (⌘E) announces its state.
|
||||
- **Welcome window**: recents rows are elements labeled "⟨name⟩, ⟨location⟩, N lanes, M cards" (registry-cached counts — 02-architecture.md); row actions (Open / Reveal in Finder / Forget) via context menu; unavailable rows say so ("unavailable — board not found").
|
||||
- **Template chooser**: templates are elements labeled by title; the mini per-lane previews are decorative and hidden from the tree.
|
||||
- **Board popover**: labeled controls throughout; the ahead/behind indicator's information — counts, queued pushes, last error — must be readable as text, never conveyed by color or shape alone.
|
||||
- **Style editor** (card sidebar section, board popover, Style… popover — 03-board-ui.md ▸ Styling ▸ Controls): grids are arrow-navigable, every well Tab-reachable and labeled by name (palette color, symbol name; leading wells "None" / "Default"); the current value is stated by trait, and a batch selection's mixed state reads as "mixed", never conveyed by highlight alone.
|
||||
|
||||
## Text scaling & visual accommodations
|
||||
|
||||
- **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.
|
||||
- **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
|
||||
|
||||
- **Automated audits are test failures**: Xcode's accessibility audit (`performAccessibilityAudit`) runs in UI tests over every surface — board (trash shown and hidden), card window (Preview, Edit, raw source), welcome, template chooser, board popover.
|
||||
- **A manual VoiceOver smoke script** lives with the test plan: create lane → create card → rename → cut/paste to another lane → external edit lands (announcement heard) → delete → Put Back → Empty Trash. Run per release; it is the canonical "does the board actually work blind" check.
|
||||
|
||||
## Changes from Kanban
|
||||
|
||||
- All new ground — the pathfinder never had an accessibility pass. Nothing carries over.
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently — the lane-move gap and the Add Attachment menu path are both closed by the keyboard map (04-interactions.md).
|
||||
@@ -0,0 +1,110 @@
|
||||
# Command Nexus
|
||||
|
||||
The single source of truth for **every command and action the app can perform** — what exists, the context it applies in, its default binding, and whether it can be customized. Behavior is specified in the home docs (cross-referenced per row); this inventory is normative for *existence, context, and bindings*: a command absent here doesn't exist, and adding one means adding a row here first. 04-interactions.md's contract underwrites the tables — every board function has a menu item + shortcut, with configuration as the one carve-out (04 ▸ The map) — and 04 remains normative for the behavior behind the rows.
|
||||
|
||||
## Customizability classes
|
||||
|
||||
| Class | Meaning | Customization |
|
||||
|---|---|---|
|
||||
| **M** | Menu command | Remappable system-natively (System Settings ▸ Keyboard ▸ App Shortcuts — 04 ▸ Configurable bindings). Titles are API. |
|
||||
| **M−** | Menu command, effectively fixed | Undo/Redo only: NSUndoManager rewrites their titles dynamically, which defeats title-matched remapping (04). |
|
||||
| **G** | Fixed grammar key | Platform grammar, deliberately not remappable — Finder's own Return/arrows aren't either (04 ▸ Grammar). |
|
||||
| **P** | Pointer grammar | Clicks, drags, modifiers — not customizable. |
|
||||
| **C** | Configuration control | Form-like controls (board popover, chooser, welcome); the keyboard path is reachability (Board Info ⌘I + Tab-reachable controls), not bindings — 04's configuration carve-out. |
|
||||
|
||||
**Toolbar presence is a separate axis**, orthogonal to the classes: toolbar items mirror menu commands, their presence per window is user-customizable (Customize Toolbar — 03-board-ui.md ▸ Toolbar; defaults and catalogs live there), and a toolbar is never a function's only home. Labels match menu titles except Undo/Redo, whose toolbar labels stay static (03).
|
||||
|
||||
## Menu commands (M unless noted)
|
||||
|
||||
| Menu | Command | Default | Context |
|
||||
|---|---|---|---|
|
||||
| File | New Card | ⌘N | Board window; disabled on a zero-lane board. Target rule: 04 |
|
||||
| File | New Lane | ⇧⌘N | Board window |
|
||||
| File | New Board… (opens the template chooser) | ⌥⌘N | Everywhere |
|
||||
| File | Open… | ⌘O | Everywhere; standard open panel (boards = `.kanban` packages and extension-less board folders — 01) |
|
||||
| File | Open Recent ▸ (with Clear Menu) | — | Everywhere; reads the board registry (02) |
|
||||
| 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 | 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 | 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 | 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 | 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 |
|
||||
| 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 Next / Find Previous | ⌘G / ⇧⌘G | Card window: the find bar's stepping; disabled in the board window — board search is a live filter, not a cursor. **Use Selection for Find (⌘E) is deliberately absent**: the chord belongs to View ▸ Edit Body, which outranks the text view's binding; a user who wants it back remaps Edit Body system-natively |
|
||||
| Board | Open Card | ⌘↩ | Board window, sole selected live card; during an inline title edit (placeholder or rename), commits it and opens — the one board command enabled mid-edit (04 ▸ Grammar) |
|
||||
| Board | Rename | — (cards: Return in place) | Board window, sole selected card/lane; a lane's only rename path (Return on a lane creates); exists for completeness and remapping |
|
||||
| Board | Style… (the style editor; selection-aware) | ⌥⌘S | Board window: selected cards or lane; nothing selected = the board |
|
||||
| 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 | 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 |
|
||||
| View | Show Trash (checkmark toggle) | ⇧⌘T | Board window |
|
||||
| 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 | History | — (no default) | Card window; focuses the sidebar History section (05); git boards only — section absent, item disabled on mode none / repo-nested |
|
||||
|
||||
## Fixed grammar keys (G)
|
||||
|
||||
All board grammar keys are inert while a title editor is focused, and menu dispatch narrows to the text domain — board-scoped commands disable, text chords route to the field, Open Card ⌘↩ excepted (04 ▸ Grammar, focused-editor rule).
|
||||
|
||||
| Key | Context | Action |
|
||||
|---|---|---|
|
||||
| Arrows / ⇧-arrows | Board window | Spatial card navigation / extend selection; lane selected: ←/→ move lane selection; shown trash is the last container |
|
||||
| ⌥↑ / ⌥↓ / ⌥← / ⌥→ | Board window | Jump: lane's first/last card; first/last lane; ⌥→ reaches the shown trash |
|
||||
| Return | Board, lane selected | Create card at its bottom (placeholder; Return commits and re-selects the lane) |
|
||||
| Return | Board, sole selected card | Inline rename; committing empty removes `title`; inert on multi-card selections |
|
||||
| Escape | Board window | One layer per press: abandon editor, else clear search (focus to board), else deselect |
|
||||
| ⌫ | Board, live selection | Tombstone — plain-key synonym of File ▸ Delete, kept grammar so no second "Delete" title exists (04) |
|
||||
| Return | Card window, Preview | Enter Edit |
|
||||
| Escape | Card window, Edit | Return to Preview |
|
||||
| Return | Card window, title field | Commit title, focus into body |
|
||||
| Escape / ⌘↩ | Card window, source mode | Cancel / Apply (leaving-by-toggle is Apply too — 05) |
|
||||
| Arrows / Space / Return / ⌫ | Card window, attachments section focused | Row navigation / QuickLook / open / Remove to *system* Trash (05) |
|
||||
|
||||
## Pointer grammar (P)
|
||||
|
||||
- **Selection clicks** (board): click selects, ⌘-click toggles, ⇧-click range-extends, click-drag rubber-bands; lane empty-space click selects the lane, double-click creates a card. **Clicking never edits**; fast double-click opens the card window (04 ▸ Selection).
|
||||
- **Drag & drop — the locality model** (04): within-board move / cross-board copy; **⌥ always forces copy, ⌘ always forces move**; multi-drag; lane header is the lane drag surface; drag-to-restore from the trash.
|
||||
- **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).
|
||||
- **Preview** (05): task-list checkbox toggle (the one interactive exception), link opens, text selection; carousel paging on the card face (03).
|
||||
- **Attachment rows** drag out their file URL (05). **Welcome rows**: single click selects, double click opens (03).
|
||||
|
||||
## Context menus
|
||||
|
||||
Context menus are the per-item action inventory VoiceOver reads (10 ▸ Actions). Every entry is a twin of a menu command, a fixed grammar key, or a configuration control — no function's only home:
|
||||
|
||||
| Surface | Entries |
|
||||
|---|---|
|
||||
| Card / lane | Open (cards), Rename, Style…, quick-style recents row (03), 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) |
|
||||
| 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 |
|
||||
| Welcome recent | Open, Reveal in Finder, Forget (C — registry management, welcome-scoped) |
|
||||
|
||||
## Configuration controls (C)
|
||||
|
||||
- **Board popover** (Board Info ⌘I — 03 ▸ Board popover): board rename; embedded style editor; add-git (mode none) / repo-nested explanation (06); branch display, switch, create; commit-identity name/email (06); add/change remote, credential fields, machine SSH key Copy + Verify, Authentication-needed state (07); ahead/behind with Pull/Push buttons and the push-on-commit toggle.
|
||||
- **Style editor** (three anchors — 03 ▸ Styling ▸ Controls): grids arrow-navigable, every well Tab-reachable.
|
||||
- **Template chooser** (09): template selection; Reveal in Finder for the user store.
|
||||
- **Welcome** (03): recents list; Forget.
|
||||
|
||||
## Standard macOS furniture
|
||||
|
||||
System-provided; the app adds nothing beyond convention: App menu (About, Hide, Quit — **no Settings pane in v1**: the only app-wide preferences, quick-style recents and `NSUserKeyEquivalents`, need no UI; ⌘, unused), Window menu, Help (carries the one line teaching the System Settings remap path — 04). **No Print story in v1** (⌘P unused). Customize Toolbar… per system convention (03).
|
||||
|
||||
## Changes from Kanban
|
||||
|
||||
- New doc — no pathfinder counterpart; 04's map table moved here (04 keeps the behavioral rules).
|
||||
- The first compilation audit added two commands that existed only as context/pointer surfaces, violating 04's contract: **File ▸ Open… (⌘O)** and **File ▸ Reveal in Finder** (no default; selection-aware).
|
||||
|
||||
## Open questions
|
||||
|
||||
None currently.
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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`).
|
||||
|
||||
Each document covers one aspect of the design. Within each:
|
||||
|
||||
- **Decisions** — settled, either carried over from the old app or newly made.
|
||||
- **Changes from Kanban** — where this rewrite deliberately diverges.
|
||||
- **Open questions** — flagged for iteration; nothing here is implicitly settled.
|
||||
|
||||
## Index
|
||||
|
||||
| Doc | Aspect |
|
||||
|---|---|
|
||||
| [00-vision.md](00-vision.md) | What Lanework is, who it's for, goals and non-goals |
|
||||
| [01-storage-format.md](01-storage-format.md) | On-disk contract: folders, frontmatter, ordering, tombstones |
|
||||
| [02-architecture.md](02-architecture.md) | App structure: source of truth, stores, watchers, concurrency |
|
||||
| [03-board-ui.md](03-board-ui.md) | Board window: layout, lanes, cards, styling, templates |
|
||||
| [04-interactions.md](04-interactions.md) | Selection, drag & drop, keyboard, clipboard, search |
|
||||
| [05-card-window.md](05-card-window.md) | The card window: Markdown preview/edit, attachments |
|
||||
| [06-history-undo.md](06-history-undo.md) | Git-backed undo/redo and history |
|
||||
| [07-sync-collab.md](07-sync-collab.md) | Board modes: local-only, git; iCloud Drive warned against |
|
||||
| [08-agent-integration.md](08-agent-integration.md) | AI agents as first-class users of the board |
|
||||
| [09-templates.md](09-templates.md) | Board templates: inventory and definition format |
|
||||
| [10-accessibility.md](10-accessibility.md) | VoiceOver, text scaling, visual accommodations |
|
||||
| [11-command-nexus.md](11-command-nexus.md) | The command Nexus — every command and action: bindings, contexts, customizability |
|
||||
|
||||
## Deferred design iterations
|
||||
|
||||
None remaining — the card window (05-card-window.md), toolbar (03-board-ui.md ▸ Toolbar), and styling controls (03-board-ui.md ▸ Styling ▸ Controls) each had their focused pass and are settled.
|
||||
|
||||
## Wishlist
|
||||
|
||||
Ideas worth keeping but outside the committed design live in [../WISHLIST.md](../WISHLIST.md).
|
||||
|
||||
## Terminology
|
||||
|
||||
The rewrite adopts **lane** as the canonical term for what the old app called a column (it's in the name: Lane-work). "Column" survives only when referring to the old app or to interior card-grid columns inside a wide lane.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
+25
@@ -0,0 +1,25 @@
|
||||
# Lanework — Feature Wishlist
|
||||
|
||||
> Ideas worth developing further, deliberately outside the current design scope. Items get recorded as they come; grooming happens later. The DESIGN/ documents hold the committed design — nothing here is committed.
|
||||
|
||||
## Items
|
||||
|
||||
### 1. "While you were away" digest
|
||||
|
||||
A human-facing digest of what changed on a board since the user last looked, derived from the git trail (commit messages already describe every settled change, agent and human alike). The read-ritual half of the masterplan pattern (DESIGN/08-agent-integration.md): agents file cards at near-zero friction all week; the digest is how the human catches up and stays honest about the plan. Sketch: on opening a board (or in the board popover), "since you last opened: 4 cards added by claude-code, 2 moved to Done" — clicking through highlights the affected cards. Requires git-enabled boards; another reason the git trail is the substrate of record.
|
||||
|
||||
### 2. In-app shortcut recorder pane
|
||||
|
||||
A Settings ▸ Shortcuts pane with a per-command recorder, writing the same `NSUserKeyEquivalents` mechanism the committed system-native remapping uses (DESIGN/04-interactions.md ▸ Configurable bindings) — so menus would keep showing effective bindings either way. Set aside as ceremony: System Settings ▸ App Shortcuts already covers remapping completely because every command is a menu item. Revisit only if users demonstrably don't find the system path.
|
||||
|
||||
### 3. Move cards to another lane before deleting a lane
|
||||
|
||||
An explicit "Move cards to…" action (context menu on a lane) that relocates a lane's cards to a chosen lane in one gesture. The old lane-delete confirmation dialog offered this as a delete intercept; the trash design replaced the dialog with a plain recoverable tombstone (DESIGN/04-interactions.md), so the relocation convenience — if ever wanted — should return as a deliberate standalone action, not a prompt.
|
||||
|
||||
### 4. Restore a card version from its History section
|
||||
|
||||
The card window's History section (DESIGN/05-card-window.md) is read-only in v1. A per-row "Restore this version" would forward-restore the card folder at that commit (a new commit — "Restore card 'X'" — never a rewrite, per DESIGN/06-history-undo.md). Needs design: attachments at the old version, versions that lived in another lane, interaction with the trash and the undo stack.
|
||||
|
||||
### 5. Lane history
|
||||
|
||||
Per-lane commit trail, the lane sibling of the card History section. Undecided whether it's wanted at all (a lane's own commits — rename, restyle, resize, reorder — are sparse; most of what changes "in" a lane belongs to its cards) and where it would surface, since lanes have no window.
|
||||
Reference in New Issue
Block a user