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

26 KiB
Raw Blame History

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 — 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.