Four rulings close Redesign Contradiction 3452893f (2026-08-06): the in-app escape hatch is ratified in full, reversing 2026-07-29's palette-only rule — the combo's Other… opens the system Colors panel, a pick landing on a palette color stores the name, anything else the hex. Free-picked colors change no contrast story: they land on the same runtime ink computation hand-written hex always got (10 amended to say so; no warning surface is owed). Anchor ownership: the card sidebar's background story is the combo alone — the well grid's background half stays with the other anchors (StyleEditorView gains showsBackground beside showsSymbols; the popover's symbol half already went to its inline SymbolPicker). Quick-style recents stay palette-vocabulary — a panel pick never enters them. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
123 lines
26 KiB
Markdown
123 lines
26 KiB
Markdown
# 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, the attributes sidebar, and the comments column (2026-07-29) 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
|
||
|
||
Three **componentized panes** (re-composed 2026-07-29, comments design): a wide **body pane** (leading), the **comments pane** (middle, when shown — The comments column below), and the narrow **attributes sidebar** (trailing) — each an independent component with its own scroll, arranged by the window's layout rather than wired to each other; componentization is the rule, so the comments pane mounts beside the body or below it (the layout option, next) without either pane knowing which. 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. **Escape abandons** (settled — the board inline rename's abandon, applied here): the field reverts to the on-disk title and focus moves into the body, never a commit. The Edit-mode collision resolves by focus, 06-history-undo.md's first-responder rule: while the title field is focused, Escape is the title abandon even with the body in Edit; with the body editor focused, Escape is the Edit→Preview flip as specified. 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.
|
||
- **Body/comments layout is an option** (ruled 2026-07-29 — componentized panes make it cheap): side-by-side is the default; **View ▸ Comments Beside Body** unchecked stacks them — body pane above, comments pane below at a fixed ≈3:2 split, each keeping its own scroll — for narrow displays. App-wide, persisted. The panes are identical in both mounts, and the thread stays visible through body **Edit** in either (Edit swaps only the body pane's content — the sidebar's own precedent); the window's minimum width grows only while the column is shown side-by-side, and resize flex always goes to the body, never the fixed panes.
|
||
|
||
(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. **The pointer-free path is the system focus model** (settled): checkboxes — like Preview's links — are real controls in the keyboard-focus and accessibility tree, so Full Keyboard Access Tab-reaches them and Space toggles, and VoiceOver toggles with VO-Space (10-accessibility.md's real-accessible-checkboxes promise, honored natively). Without FKA they are not in the key loop — standard macOS content behavior, so ordinary Tab users never wade through a long checklist. In-content controls are *content*, not commands: no menu item, no chord — 04's every-function-has-a-menu-item contract covers commands, and 11-command-nexus.md scopes them accordingly. Under the read-only lock (02-architecture.md) the controls disable in place — an in-content mutation menu validation can't reach (and not the only such path: the attachment row's ⌫/Remove shares the posture — 02's every-entry-point predicate).
|
||
- 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 surface (Preview's selectable text, the Edit editor, raw source — and the comments pane, where it searches the thread's **content — every comment's body**, cross-row with wraparound, `.draft` excluded (tightened 2026-07-31: author and date lines are metadata, not find targets — a match the bar cannot highlight is worse than none); the composer and an inline comment edit are their own focused text surfaces with the editor's ordinary find). Board search is a board-window concern (04-interactions.md ▸ Search — which reaches comment bodies through its own transient index since the 2026-07-29 re-ruling, so the two finds never overlap in scope).
|
||
|
||
### 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: 06-history-undo.md ▸ Undo routing; commit granularity (one commit per Edit session — the Edit→Preview flip is the effective Save button; never per save tick): 06 ▸ Rules ▸ Auto-commit.
|
||
|
||
### 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 (including a `modified-by` stamp the user typed or kept — Apply is the one app write that doesn't clear it, 01-storage-format.md); 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. A pull landing mid-session neither blocks on the open buffer nor invalidates it (07-sync-collab.md — same-card signpost, Apply stays last-writer-wins); branch switch and undo restore instead settle it explicitly via save-or-discard (06-history-undo.md ▸ Branch switching).
|
||
|
||
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. **One carve-out by hover target** (ruled 2026-07-29 — comment attachments are authorable): a file dropped **within the comment composer's bounds** imports to the draft's `attachments/`, and within an **inline comment edit session's bounds** to that comment's — the window-wide card default covers everywhere else (The comments column below).
|
||
- 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 **Background color combo** over the **curated symbol grid** (03-board-ui.md ▸ Styling ▸ Controls, its 2026-08-06 anchor-ownership rule) — the sidebar is exactly the narrow context the combo was built for, so it stands in for the well grid's background half here, panel escape hatch included; the full grid remains the surface at the other anchors. Card styling is discoverable here without a context menu; the component family is shared with the board popover and 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 — not because identity changes (a move keeps its UUID; the import boundary remints only on collision — 01-storage-format.md) but because the destination board's repo has no commits for the arriving folder; the old board keeps the old trail either way.
|
||
- 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** — moves the card to the trash (destructive styling; the window then dismisses itself per Deletion & lifecycle below; recoverable from the board's trash lane — 03-board-ui.md).
|
||
- **Reveal in Finder** — the card's folder.
|
||
|
||
## The comments column
|
||
|
||
Designed 2026-07-29 (storage: 01-storage-format.md ▸ Enhanced schema). Ships in **every tier** — only tracker sync is tier-gated (12-editions.md). Feature lands post-2.0.
|
||
|
||
- **Visibility** (re-ruled 2026-07-29 — the pane obeys the user, not the content): **View ▸ Show Comments** is a checkmark toggle à la Show Trash, and its choice is **app-wide and persisted across restarts** (`UserDefaults.standard`, beside Comments Beside Body — the one-app collapse's scalars rule), and it **defaults ON** (ruled 2026-07-31): the trash's hidden-by-default bargain hides destructive residue, while the pane invites content — a new feature behind an unchecked menu item would never be discovered; one persisted uncheck opts out forever. One bit, no content-derived auto-show: checked, every card window carries the pane (a comment-less card shows the empty thread and the composer — the invitation is the point); unchecked, threads and drafts are out of sight until the user says otherwise, the Show Trash bargain. The checkmark reads the bit — the menu never lies. **File ▸ Add Comment** flips the bit on when it's off (the gesture *is* the user choosing to see comments — same persistence) and focuses the composer in one gesture (11-command-nexus.md). Deleting the last comment never closes the pane — nothing but the toggle does.
|
||
- **The thread**: one comment = an author line (self-reported `author`, unattributed when absent; timestamp; "· edited" when `modified` differs from `created`), the rendered Markdown body (the card-body subset), and attachment chips when its `attachments/` is non-empty (Quick Look, the sidebar section's pattern). No avatars — there is no identity system, and initials faked from self-reported strings would be decoration. The section header carries the count ("Comments · 3") and the **sort-direction control**: chronological ascending by default, flippable to newest-first (app-wide, persisted).
|
||
- **The composer edits `comments/.draft/`** (ruled 2026-07-29 — the draft is user content in the board, the `.trash` pattern applied to composition): an always-visible text area ("Add a comment…", Edit-mode Markdown highlighting) whose backing file is the card's single draft — a reserved dot-named folder under `comments/` holding ordinary comment schema, `attachments/` included, excluded from the thread listing. Restore-on-reopen falls out for free (the composer just reads its file); drafts ride git and sync across machines like any file; concurrent drafts on two machines are an ordinary file race (local-wins). **The composer sits at the thread's newest end** (bottom ascending, top descending) and the window opens scrolled to it — a thread opens where the conversation is happening. **Comment attachments author here** (ruled 2026-07-29): a file dropped within the composer's bounds imports to the draft's `attachments/` (the hover-target carve-out — Attachments above), a quiet **paperclip affordance** on the composer covers the no-drag path (the section header's add-affordance pattern; File ▸ Add Attachment… stays card-scoped), and the same pair applies within an inline comment edit session, targeting that comment's `attachments/`. Chips on an authoring surface carry remove (to the **system** Trash — the sidebar row's rule); a posted comment's chips are read-only, Quick Look only — Edit the comment to change its files.
|
||
- **Draft saves are slow-cadence, never prompted** (flow breakage minimized): the draft writes on composer blur, window close, quit, and a lazy interval (~30 s) — not the body editor's 700 ms, so a Pro user's typing never becomes a commit stream; the saves that do land compose the quiet path-shaped **"Draft comment on '⟨card⟩'"**. Close and quit just proceed — no DirtyBufferGuard on the ordinary path, nothing to lose while saves land. **The failure path gets the guard** (re-ruled 2026-07-31, narrowing "nothing to lose" to its true premise): a close-time draft flush that *fails* with typed text in the buffer raises the DirtyBufferGuard modal (retry / save a copy / discard) exactly as the body's does — the buffer is then the only home the text has, 02-architecture.md's one-modal-moment class; ordinary closes stay ceremony-free since the guard only ever fires on a failed write. A draft emptied of text with no attachments deletes its folder — no litter. **Escape moves focus out of the composer, draft untouched** (ruled 2026-07-29 — Escape never discards: the draft is a durable file, so "abandon" has no meaning here; emptying the draft is the discard gesture, and the title field's abandon-Escape stays the transient-bubble exception).
|
||
- **⌘↩ posts** (a Comment button twins it): posting renames `.draft` → a fresh lowercase UUID and **restamps `created`/`modified`** in the same write bracket — chronology is when it was posted, not when drafting began — one gesture, one commit ("Comment on '⟨card⟩'" — 06-history-undo.md's verb family per 01).
|
||
- **Edit and delete**: every comment is editable and deletable — files-first has no enforced identity. The comment's context menu (the per-item inventory — 10-accessibility.md) carries **Edit / Delete / Reveal in Finder**. Inline Edit is a **body-edit session in miniature** (no second draft mechanism): debounced saves to the comment's own file keep it crash-safe, Save (or ⌘↩) ends the session as its commit point, Cancel — or Escape, its keyboard twin (ruled 2026-07-29; 11's grammar table) — reverts to session-start bytes, window close flushes the session exactly as the body's does. Delete is immediate and undoable, no confirm (01's ruling — undo is the net: the comment moves into `comments/.trash/`, undo is the move back on the **window's own stack**; after close, board-level undo of the session restores it, and the folder purges only when undo no longer needs it — 13-native-undo.md's session-coarsening model, re-ruled 2026-07-31).
|
||
- **Live updates**: the pane reloads its thread from the same FSEvents stream (01's window-scoped rule — the board snapshot never loads comment content); foreign arrivals snap in per the motion language, and the announcer speaks them path-shaped ("New comment on '⟨card⟩'" — 10-accessibility.md).
|
||
- **Raw Source still swaps the entire content area** — all panes, comments included; the raw outlet's rule is unchanged.
|
||
|
||
## 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). **Hidden with the title, and the loss is blessed** (ruled 2026-08-06): the no-title titlebar (Toolbar below — `titleVisibility = .hidden`) collapses AppKit's title and subtitle as one field, so the pair hides together; the window is usually beside its board, and `window.title`/`window.subtitle` keep feeding the Window menu, Exposé, VoiceOver, and restoration, so only the visual rendering is lost. The computation and live-update wiring stand untouched — if a visible placement surface is ever wanted (a details-sidebar placement row is the named candidate), it is a surface away, not a rewiring; deliberately not built now.
|
||
- 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 **entering the trash counts as deleted** (resettled 2026-07-28, the materialized trash): ⌫ on the board closes the card's open window (the card left the working set; restore and reopen if it was a slip), an external move into `.trash/` observed by reload does the same, and deleting the card's *lane* deletes the card with it — the window dismisses because the card is gone. Restoring reopens nothing — reopening is the user's act, like any open. **Dismissal never eats typed work silently where a save can land** (settled): a dirty Edit buffer flushes into the card's folder at its new `.trash/` location before the window dismisses — a surgical body write, so the keystrokes survive a later restore and enter history on git boards (the composer reads it as an edit to a trashed card — accurate). An open raw-source buffer discards instead: its Apply would write a whole stale `index.md` over the trashed card — a delete is never fought by a stale buffer. A card whose lane was deleted discards both — nowhere left to write. A card hard-deleted externally (folder gone) discards both — nowhere left to write, the inline-rename rule. Cards in the shown trash lane don't open at all — restore first (03-board-ui.md). Reopening a live card focuses the existing window.
|
||
|
||
## Changes from Kanban
|
||
|
||
- **Three componentized panes** replace the pathfinder's vertical title/strip/body stack: the body pane (title atop it, created/modified line beneath), the comments pane (when shown — the 2026-07-29 re-composition), and 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.
|