From 9619613d4eea43bb5ded8c3a7cc5d6f6c93ece5d Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 10 Jun 2026 11:41:24 +0200 Subject: [PATCH] document the conversation-panel card model in the ui-design skill (T-305) Capture the three card categories (dialog / simple / collapsible), the ClideCollapserCard rules, and the shared card-spacing constants so future conversation-panel work follows the settled model. Closes T-305. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/ui-design/SKILL.md | 30 +++++++++++++++++++++++ .pql/changelog/ticket_history/2026-06.sql | 1 + .pql/changelog/tickets/2026-06.sql | 30 +++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/.claude/skills/ui-design/SKILL.md b/.claude/skills/ui-design/SKILL.md index 55dd842e..dee512dc 100644 --- a/.claude/skills/ui-design/SKILL.md +++ b/.claude/skills/ui-design/SKILL.md @@ -39,6 +39,36 @@ These apply across every reference and every surface: - Typography: `clideFontMono` for code/paths/IDs, `clideFontCaption` for status/section headers, body inherits from `DefaultTextStyle`. +## Conversation-panel cards (T-305) + +The Claude conversation stream has **three** card categories. They are NOT one +shared wrapper widget — each is its own widget; they only share the spacing +constants in `lib/widgets/src/clide_card_metrics.dart` (`kClideCardGap`, +`kClideCardRadius`, `kClideCardHeaderPadH/V`, `kClideCardCounterSlotWidth`) so +the stream reads as one rhythm. Change spacing there, not per-card. + +1. **Dialog cards** — `ConversationCard` with a speaker **side stripe** (you / + claude / agent). Prose/attribution; not collapsible. +2. **Simple cards** — a single item shown fully open, never collapses (e.g. the + image card). Standalone display; no chevron, no status chrome. +3. **Collapsibles** — `ClideCollapserCard` (`lib/widgets/`). Every tool use is + one, over a list of `1..N` inner item cards (a single tool = a 1-item list; + there is no separate single-card path). Rules: + - Whole card toggles; chevron hard against the **left** edge, the status tick + (spinner/check/cross) hard against the **right** edge, the count in a + fixed-width slot just inboard of it. + - `color` drives the border + chevron/label tint (per-instance fidelity). + - Collapsed ticker echoes the run's **last content line** as the title + + count + aggregate status (computed by the caller, bubbled up from items). + - Inner item cards are content + their **own** per-item status; they pass + `margin: EdgeInsets.zero`-ish (a bottom margin matching the canvas) so the + collapser pads the inner canvas **evenly on all sides** — never let an + inner card jam under the header or against a frame edge. + +Do NOT pull a dialog card's stripe or a simple card's config into the collapser, +and do NOT nest collapsers — inside a run, tools render as the bare inner content +card (`_ConversationTurn(collapseTools: false)`). + ## Anti-patterns (cross-cutting) - Borrowing another surface's token (`sidebarBackground` for hat bar) — give diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 1dd0bf03..772f0fca 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -2444,3 +2444,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BD8PXDRHBF001200', 'status', 'in_progress', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'f4a1697c1c839dc92d6bedc2cd86d997', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'status', 'ready', 'done', NULL, '2026-06-10 08:57:51', '2026-06-10 08:57:51', '2026-06-10 08:57:51', NULL, 'fe6d00feb488f12fd0931852b2e66236', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'status', 'backlog', 'in_progress', NULL, '2026-06-10 09:13:08', '2026-06-10 09:13:08', '2026-06-10 09:13:08', NULL, '56fbf20da154b3465d3d2e7dcc87dfc9', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'status', 'in_progress', 'done', NULL, '2026-06-10 09:39:52', '2026-06-10 09:39:52', '2026-06-10 09:39:52', NULL, '5f059718e9cf548a13a9d141970ccd2e', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 7ce2b48f..e0a3de05 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -1314,3 +1314,33 @@ Root cause: lib/builtin/claude/src/prompt_card.dart:125-135 defines _digitKeys a Fix: extend _digitKeys (or the match in _onKey) to also map LogicalKeyboardKey.numpad1..numpad9 to the same 1-9 selection. Keep the hasPrimaryFocus / modifier guards as-is so digits still type into note fields. Acceptance: with the prompt card focused, numpad 1-9 selects the corresponding option/button exactly like the number-row digits; numpad keys still type normally when a note field has focus.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-10 09:39:05', '2026-06-10 09:39:05', NULL, '85f9ce66179451f329a97211dbb96c86', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'task', '06FB0TNQM5TWC00GW0P3X02HZW', 'Unify collapser cards onto one ClideCollapserCard primitive', 'Unify the conversation-panel collapsible cards onto one ClideCollapserCard primitive, and share card spacing across all card categories. Supersedes ClideHolderCard (T-266) and the collapse logic in ConversationCard (T-262) for the tool path. + +## Conversation-panel card model (agreed 2026-06-10) + +### Shared spacing — constants, NOT a shared wrapper +All card categories share a small set of spacing CONSTANTS (inter-card bottom gap, inner padding, corner radius). NOT a forced common wrapper widget — each category is its own widget; they just pull the same spacing tokens so the stream reads as one consistent rhythm. + +### Three card categories +1. **Dialog cards** — carry the side stripe marking who is speaking (user / Claude / agent). Prose / attribution. Not collapsible. (Today: ConversationCard stripe variant.) +2. **Simple cards** — a single item shown fully open in the stream, never collapses (e.g. the image-show card; more to come). Standalone display: no chevron, no status chrome. +3. **Collapsibles** — the unified collapser. Covers edits, bash, task updates, runs — every tool use. Behaves like the bash card should: + - The whole card is clickable to collapse/expand. + - Collapsed: title = the echoed last content line (like bash now) + an item count + aggregate status (spinner / check / cross). + - Expanded: an inner canvas holding the nested item card(s), each item in its own inner card. + - A single item still gets its own inner card inside the collapser when open, and pushes its status / count / last-line up to the collapser header. + - Inner item cards ALSO show their own per-item status (check / cross / spinner) when expanded; the collapser header carries the aggregate. + - Chrome (per the wireframe): `color` (outer border + chevron / label / text), fixed-width counter slot, status icon hard against the right edge, chevron hard against the left edge. + +## Scope +- New `ClideCollapserCard` in lib/widgets/ (exported from widgets.dart) — the category-3 primitive: a list of 1..N inner item cards, collapsed ticker <-> expanded inner canvas, color / fixed-counter / edge-status / edge-chevron chrome, background + caret toggle (D-78 tail-follow), aggregate status + count + echoed-title computed from the items. +- Shared card-spacing constants consumed by all three categories. +- ALL tool uses render as collapsers — single ones as a 1-item list (Bash, Read, Edit, Task, edits runs, activity runs, etc.). +- Inner item cards: content + their own per-item status; no own collapse; no stripe. +- Dialog cards (1) and simple cards (2) are NOT pulled into the collapser — they only adopt the shared spacing constants (keep stripe / inner config). + +## Verify +- Goldens regenerated (holder_card, conversation_card_merged) + a11y (expanded / collapsed semantics, focusable toggle). +- Wireframe: docs/design/wireframes/cards/collapser-card.{json,png}. +- After landing: update the ui-design skill''s conversation-panel guidance to describe the three card categories + the collapser. +', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 08:15:39', '2026-06-10 09:39:52', NULL, 'd173d4e6a58bb9dba5ad279d705f083b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);