Files
lanework/DESIGN/05-card-window.md
T
rzen 639d9839bf The subtitle hides with the title, blessed — context survives everywhere but pixels
Minor ruled into 05 ▸ Window: the no-title titlebar collapses AppKit's
title/subtitle pair as one field, so board › lane hides with it. The
window is usually beside its board; title and subtitle keep feeding the
Window menu, Exposé, VoiceOver, and restoration. The live-update wiring
stands — a sidebar placement row is the named candidate if a visible
surface is ever wanted; deliberately not built now.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-06 20:57:47 -04:00

123 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (`![](attachments/shot.png)`).
- **Remote images are never fetched** — Preview does no networking (sandbox-quiet, files-first). An `![](https://…)` 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 **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 — 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.