tickets panel: per-type filter chips (T-343)

A row of toggle chips below the filter box — one per pql ticket type,
ordered large→small (Initiative, Epic, Story, Task, Bug), each with its
TicketTypeColors dot + border. Single-click toggles a type; double-click
isolates it (chart-legend solo, fully reversible); disabling the last
enabled type snaps all back on so the list is never blank. ANDed with the
text filter; all on by default, nothing persisted.

One GestureDetector owns both onTap + onDoubleTap so Flutter disambiguates
single vs double. Wireframe updated + approved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 18:13:04 +02:00
co-authored by Claude Opus 4.8
parent 91b7dbef4d
commit 75cf14f1cb
7 changed files with 477 additions and 83 deletions
+64
View File
@@ -3436,3 +3436,67 @@ SCOPE CLARIFICATION (2026-06-10, from user):
Test additions: (a) two concurrent agents whose sidechain items interleave -> each agent''s run items land under its own card, none cross-attributed; (b) an unattributable sidechain item (no parent_tool_use_id, broken chain) is NOT swept into the nearest agent''s card; (c) regression: consecutive Bash/Read calls still form one Activity cluster.', NULL, '2026-06-10 15:28:52', '2026-06-10 15:28:52', '2026-06-10 15:28:52', NULL, '0352b24d88a4bf24ede628880fe9b502', 2) ON CONFLICT(hash) DO NOTHING; Test additions: (a) two concurrent agents whose sidechain items interleave -> each agent''s run items land under its own card, none cross-attributed; (b) an unattributable sidechain item (no parent_tool_use_id, broken chain) is NOT swept into the nearest agent''s card; (c) regression: consecutive Bash/Read calls still form one Activity cluster.', NULL, '2026-06-10 15:28:52', '2026-06-10 15:28:52', '2026-06-10 15:28:52', NULL, '0352b24d88a4bf24ede628880fe9b502', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'status', 'backlog', 'in_progress', NULL, '2026-06-10 15:54:23', '2026-06-10 15:54:23', '2026-06-10 15:54:23', NULL, '8061a1d449acdb1e467239de0dce5d1d', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'status', 'backlog', 'in_progress', NULL, '2026-06-10 15:54:23', '2026-06-10 15:54:23', '2026-06-10 15:54:23', NULL, '8061a1d449acdb1e467239de0dce5d1d', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'status', 'in_progress', 'in_progress', NULL, '2026-06-10 16:02:55', '2026-06-10 16:02:55', '2026-06-10 16:02:55', NULL, '9c00a4d5fefdc3cf8f227b040bc78acb', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'description', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default.
## Behaviour
- Single-click a chip → toggle that type in/out of the list.
- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible.
- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank.
- Tooltip per chip: "Click to toggle · double-click to isolate".
## Type mapping (pql → chip)
pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as:
- Bug → bug
- Ticket → story + task (leaf work items)
- Epic → epic
- Initiative → initiative
Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA).
## Filtering
- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter.
- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today).
## Implementation notes
- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable.
- Persist nothing across sessions for v1 (always all-on on load); revisit if requested.
- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle.
## Wireframe
docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default.
## Behaviour
- Single-click a chip → toggle that type in/out of the list.
- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible.
- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank.
- Tooltip per chip: "Click to toggle · double-click to isolate".
## Type mapping (pql → chip)
pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as:
- Bug → bug
- Ticket → story + task (leaf work items)
- Epic → epic
- Initiative → initiative
Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA).
## Filtering
- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter.
- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today).
## Implementation notes
- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable.
- Persist nothing across sessions for v1 (always all-on on load); revisit if requested.
- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle.
## Wireframe
docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)
Design revision (2026-06-10, supersedes the chip set/order above): FIVE chips, one per pql type — no story+task grouping. Ordered LARGE→SMALL left to right: Initiative, Epic, Story, Task, Bug. Each maps 1:1 to its pql type (initiative/epic/story/task/bug) with its TicketTypeColors dot+border (initiative #C792EA, epic #78A0F8, story #7DD3A8, task #9AA0AA grey, bug #E87D7D). All five ON by default. Toggle/solo(double-click)/last-off-reset behaviour unchanged. Wireframe updated + approved.', NULL, '2026-06-10 16:06:25', '2026-06-10 16:06:25', '2026-06-10 16:06:25', NULL, '26b5b4d5cf2a48fe6aeacd7b83968848', 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 ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'backlog', 'ready', NULL, '2026-06-10 16:09:21', '2026-06-10 16:09:21', '2026-06-10 16:09:21', NULL, 'c89b4ddcbdeea95483c53b1b76220842', 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 ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'ready', 'in_progress', NULL, '2026-06-10 16:09:26', '2026-06-10 16:09:26', '2026-06-10 16:09:26', NULL, 'c36e6343d44f59ff4af0eea3178b5a94', 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 ('06FB0TNQM5TWC00GW0P3X02HZW', 'status', 'in_progress', 'backlog', NULL, '2026-06-10 16:09:57', '2026-06-10 16:09:57', '2026-06-10 16:09:57', NULL, 'be22c7ff64f106d600b4a5bd70ced1d2', 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 ('06FB4J5E6W983P1S7BE0FDPSMM', 'status', 'backlog', 'ready', NULL, '2026-06-10 16:10:20', '2026-06-10 16:10:20', '2026-06-10 16:10:20', NULL, '6f4b3ce9e2167200ebdde0c1511d4a91', 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 ('06FB4J5E6W983P1S7BE0FDPSMM', 'status', 'ready', 'in_progress', NULL, '2026-06-10 16:10:22', '2026-06-10 16:10:22', '2026-06-10 16:10:22', NULL, 'cce7ce3f17ae087f753f4d6d072a702e', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'status', 'in_progress', 'done', NULL, '2026-06-10 16:12:15', '2026-06-10 16:12:15', '2026-06-10 16:12:15', NULL, '4cb2daa831a115888a9456461bf53bdf', 2) ON CONFLICT(hash) DO NOTHING;
+121
View File
@@ -2959,3 +2959,124 @@ Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87
## Wireframe ## Wireframe
docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 15:54:23', NULL, 'f5614424c00196aabab868db75483bf2', 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); docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 15:54:23', NULL, 'f5614424c00196aabab868db75483bf2', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket panel: per-type filter chips (Bug / Ticket / Epic / Initiative)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default.
## Behaviour
- Single-click a chip → toggle that type in/out of the list.
- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible.
- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank.
- Tooltip per chip: "Click to toggle · double-click to isolate".
## Type mapping (pql → chip)
pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as:
- Bug → bug
- Ticket → story + task (leaf work items)
- Epic → epic
- Initiative → initiative
Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA).
## Filtering
- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter.
- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today).
## Implementation notes
- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable.
- Persist nothing across sessions for v1 (always all-on on load); revisit if requested.
- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle.
## Wireframe
docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 16:02:55', NULL, '83c5d82ad411e2827733928cdb81c6e0', 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 ('06FB4J5E6W983P1S7BE0FDPSMM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Conversation card font sizes inconsistent: ClideCollapserCard vs ConversationCard', 'Two collapsible-card primitives sit adjacent in the Claude conversation stream but use different font-size tokens for the same visual roles, so labels/summaries are visibly 1-2px off between neighbouring cards.
- ClideCollapserCard (lib/widgets/src/clide_collapser_card.dart) — the ''Activity'' / agent-run / edit-run cards: header label, collapsedSummary, and counter all use clideFontCaption (14).
- ConversationCard (lib/builtin/claude/src/conversation_card.dart) — tool-use / result / ''denied'' cards: header label uses clideFontSmall (12), collapsedSummary uses clideFontMeta (13).
Result: an ''Activity Bash …'' collapser and a neighbouring ''Bash · denied'' tool card render their label + collapsed summary at different sizes in the same list. Spotted in a screenshot of three stacked cards (collapser Bash, denied tool result, collapser Read).
Fix: pick one set of tokens for the shared header/label/summary role across both primitives (likely align ConversationCard''s label to clideFontCaption and standardise the collapsed-summary size), or extract a shared card-header style. Tokens: clideFontCaption=14, clideFontMeta=13, clideFontSmall=12 (lib/widgets/src/typography.dart).', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 16:06:08', '2026-06-10 16:06:08', NULL, 'c858a9b7c474697611c3ff8240c61a2d', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket panel: per-type filter chips (Bug / Ticket / Epic / Initiative)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default.
## Behaviour
- Single-click a chip → toggle that type in/out of the list.
- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible.
- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank.
- Tooltip per chip: "Click to toggle · double-click to isolate".
## Type mapping (pql → chip)
pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as:
- Bug → bug
- Ticket → story + task (leaf work items)
- Epic → epic
- Initiative → initiative
Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA).
## Filtering
- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter.
- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today).
## Implementation notes
- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable.
- Persist nothing across sessions for v1 (always all-on on load); revisit if requested.
- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle.
## Wireframe
docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)
Design revision (2026-06-10, supersedes the chip set/order above): FIVE chips, one per pql type — no story+task grouping. Ordered LARGE→SMALL left to right: Initiative, Epic, Story, Task, Bug. Each maps 1:1 to its pql type (initiative/epic/story/task/bug) with its TicketTypeColors dot+border (initiative #C792EA, epic #78A0F8, story #7DD3A8, task #9AA0AA grey, bug #E87D7D). All five ON by default. Toggle/solo(double-click)/last-off-reset behaviour unchanged. Wireframe updated + approved.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 16:06:25', NULL, '93554cd5afe5bd881b12bef2892e25b8', 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 ('06FB0TNQM5TWC00GW0P3X02HZW', 'epic', NULL, 'UI tweaks & fixes', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work.
**PERMANENT — never close.** This is a standing rolling tracker for loose UI/UX work and bugs, not a deliverable epic. It stays open indefinitely; only its children are completed/closed. Do not mark T-276 done even when all current children are closed — new tweaks/fixes get filed here on an ongoing basis.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-08 07:46:23', '2026-06-10 16:09:21', NULL, 'd9e663378cc6deec9bc85f95f5552db8', 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 ('06FB0TNQM5TWC00GW0P3X02HZW', 'epic', NULL, 'UI tweaks & fixes', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work.
**PERMANENT — never close.** This is a standing rolling tracker for loose UI/UX work and bugs, not a deliverable epic. It stays open indefinitely; only its children are completed/closed. Do not mark T-276 done even when all current children are closed — new tweaks/fixes get filed here on an ongoing basis.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-08 07:46:23', '2026-06-10 16:09:26', NULL, 'fc45344f2d71e9b71573100825c2bce7', 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 ('06FB0TNQM5TWC00GW0P3X02HZW', 'epic', NULL, 'UI tweaks & fixes', 'Ongoing umbrella for small, standalone UI polish, cosmetic tweaks, and visual/interaction bug fixes that don''t belong to a feature epic — color/token corrections, control placement, status surfaces, micro-interactions, and the wireframes that frame them. Children are independently shippable; the epic stays open as a rolling home for this class of work.
**PERMANENT — never close.** This is a standing rolling tracker for loose UI/UX work and bugs, not a deliverable epic. It stays open indefinitely; only its children are completed/closed. Do not mark T-276 done even when all current children are closed — new tweaks/fixes get filed here on an ongoing basis.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-08 07:46:23', '2026-06-10 16:09:57', NULL, 'a1ab5ae76714ede6d6a473db9b5d8aa9', 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 ('06FB4J5E6W983P1S7BE0FDPSMM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Conversation card font sizes inconsistent: ClideCollapserCard vs ConversationCard', 'Two collapsible-card primitives sit adjacent in the Claude conversation stream but use different font-size tokens for the same visual roles, so labels/summaries are visibly 1-2px off between neighbouring cards.
- ClideCollapserCard (lib/widgets/src/clide_collapser_card.dart) — the ''Activity'' / agent-run / edit-run cards: header label, collapsedSummary, and counter all use clideFontCaption (14).
- ConversationCard (lib/builtin/claude/src/conversation_card.dart) — tool-use / result / ''denied'' cards: header label uses clideFontSmall (12), collapsedSummary uses clideFontMeta (13).
Result: an ''Activity Bash …'' collapser and a neighbouring ''Bash · denied'' tool card render their label + collapsed summary at different sizes in the same list. Spotted in a screenshot of three stacked cards (collapser Bash, denied tool result, collapser Read).
Fix: pick one set of tokens for the shared header/label/summary role across both primitives (likely align ConversationCard''s label to clideFontCaption and standardise the collapsed-summary size), or extract a shared card-header style. Tokens: clideFontCaption=14, clideFontMeta=13, clideFontSmall=12 (lib/widgets/src/typography.dart).', 'ready', 'medium', NULL, NULL, NULL, '2026-06-10 16:06:08', '2026-06-10 16:10:20', NULL, '140190d844d98929ecc83e30cdcc0745', 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 ('06FB4J5E6W983P1S7BE0FDPSMM', 'bug', '06FB0TNQM5TWC00GW0P3X02HZW', 'Conversation card font sizes inconsistent: ClideCollapserCard vs ConversationCard', 'Two collapsible-card primitives sit adjacent in the Claude conversation stream but use different font-size tokens for the same visual roles, so labels/summaries are visibly 1-2px off between neighbouring cards.
- ClideCollapserCard (lib/widgets/src/clide_collapser_card.dart) — the ''Activity'' / agent-run / edit-run cards: header label, collapsedSummary, and counter all use clideFontCaption (14).
- ConversationCard (lib/builtin/claude/src/conversation_card.dart) — tool-use / result / ''denied'' cards: header label uses clideFontSmall (12), collapsedSummary uses clideFontMeta (13).
Result: an ''Activity Bash …'' collapser and a neighbouring ''Bash · denied'' tool card render their label + collapsed summary at different sizes in the same list. Spotted in a screenshot of three stacked cards (collapser Bash, denied tool result, collapser Read).
Fix: pick one set of tokens for the shared header/label/summary role across both primitives (likely align ConversationCard''s label to clideFontCaption and standardise the collapsed-summary size), or extract a shared card-header style. Tokens: clideFontCaption=14, clideFontMeta=13, clideFontSmall=12 (lib/widgets/src/typography.dart).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 16:06:08', '2026-06-10 16:10:22', NULL, 'f02a03592e970322a82bf1be7002b7fa', 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 ('06FB4FDREHRYRR7B9ER72KCQKC', 'story', '06FB0TNQM5TWC00GW0P3X02HZW', 'Ticket panel: per-type filter chips (Bug / Ticket / Epic / Initiative)', 'Add a row of type-filter toggle chips at the top of the tickets panel, directly below the "Filter tickets…" box (no section header — the chips read on their own). One chip per ticket type the user thinks in: Bug, Ticket, Epic, Initiative. All four ON by default.
## Behaviour
- Single-click a chip → toggle that type in/out of the list.
- Double-click a chip → isolate (solo) that type: turns it ON and all others OFF. Double-click the same chip again → restore all to ON. This is the chart-legend solo pattern (Plotly/Tableau/Grafana) — learnable and fully reversible.
- Last-off resets to all-on: disabling the final remaining type snaps all chips back ON. An empty type filter means "no filter", so the list is never mysteriously blank.
- Tooltip per chip: "Click to toggle · double-click to isolate".
## Type mapping (pql → chip)
pql ticket types are initiative, epic, story, task, bug (see lib/builtin/tickets/src/ticket_colors.dart). The four chips map as:
- Bug → bug
- Ticket → story + task (leaf work items)
- Epic → epic
- Initiative → initiative
Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87D7D, story/task green/grey, epic #78A0F8, initiative #C792EA).
## Filtering
- Type filter is ANDed with the existing text filter in _TicketsViewState (lib/builtin/tickets/src/tickets_view.dart): a ticket shows only if its type is enabled AND it matches the text filter.
- When the type filter hides all items in a status section, that section collapses out (same as text-filter behaviour today).
## Implementation notes
- Active-chip visual: filled tint + type-colored border (active) vs muted/no border (inactive) — reuse the _Toggle pattern from lib/builtin/search/src/search_panel_view.dart and ClideTappable.
- Persist nothing across sessions for v1 (always all-on on load); revisit if requested.
- a11y: Semantics(button, toggled) per chip, mirroring the search-panel toggle.
## Wireframe
docs/design/wireframes/tickets/ticket-type-filters.json (+ .png export)
Design revision (2026-06-10, supersedes the chip set/order above): FIVE chips, one per pql type — no story+task grouping. Ordered LARGE→SMALL left to right: Initiative, Epic, Story, Task, Bug. Each maps 1:1 to its pql type (initiative/epic/story/task/bug) with its TicketTypeColors dot+border (initiative #C792EA, epic #78A0F8, story #7DD3A8, task #9AA0AA grey, bug #E87D7D). All five ON by default. Toggle/solo(double-click)/last-off-reset behaviour unchanged. Wireframe updated + approved.', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 15:54:09', '2026-06-10 16:12:15', NULL, '3dd40475245455459d9f09d26ef34316', 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);
+5
View File
@@ -31,6 +31,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
### Added ### Added
- **Per-type filter chips on the tickets panel.** A row of toggle chips
(Initiative · Epic · Story · Task · Bug, large→small) below the filter box.
Click a chip to toggle that type; double-click to isolate it (chart-legend
solo); disabling the last one snaps all back on. ANDed with the text filter.
All on by default. (T-343)
- **VS Code keybinding preset.** A `vscode` keymap mapping VS Code's default - **VS Code keybinding preset.** A `vscode` keymap mapping VS Code's default
shortcuts (Ctrl+P, Ctrl+Shift+P, Ctrl+B, Ctrl+J, Ctrl+`, zoom, …) to clide. shortcuts (Ctrl+P, Ctrl+Shift+P, Ctrl+B, Ctrl+J, Ctrl+`, zoom, …) to clide.
Activate via the "Keymap: VS Code" command or `app.keymap.preset = vscode`. (T-64) Activate via the "Keymap: VS Code" command or `app.keymap.preset = vscode`. (T-64)
@@ -42,82 +42,10 @@
"fontSize": 13 "fontSize": 13
}, },
"chip-bug": {
"type": "Rectangle",
"parent": "panel",
"left": 96, "top": 118, "width": 66, "height": 24,
"fillColor": "#211519",
"strokeColor": "#E87D7D",
"corners": [4, 4, 4, 4]
},
"chip-bug-dot": {
"type": "Ellipse",
"parent": "chip-bug",
"left": 104, "top": 126, "width": 8, "height": 8,
"fillColor": "#E87D7D",
"strokeColor": "#E87D7D"
},
"chip-bug-label": {
"type": "Text",
"parent": "chip-bug",
"left": 117, "top": 124,
"text": "Bug",
"fontColor": "#e8e8ee",
"fontSize": 11
},
"chip-ticket": {
"type": "Rectangle",
"parent": "panel",
"left": 168, "top": 118, "width": 78, "height": 24,
"fillColor": "#15191d",
"strokeColor": "#7DD3A8",
"corners": [4, 4, 4, 4]
},
"chip-ticket-dot": {
"type": "Ellipse",
"parent": "chip-ticket",
"left": 176, "top": 126, "width": 8, "height": 8,
"fillColor": "#7DD3A8",
"strokeColor": "#7DD3A8"
},
"chip-ticket-label": {
"type": "Text",
"parent": "chip-ticket",
"left": 189, "top": 124,
"text": "Ticket",
"fontColor": "#e8e8ee",
"fontSize": 11
},
"chip-epic": {
"type": "Rectangle",
"parent": "panel",
"left": 252, "top": 118, "width": 68, "height": 24,
"fillColor": "#14171f",
"strokeColor": "#78A0F8",
"corners": [4, 4, 4, 4]
},
"chip-epic-dot": {
"type": "Ellipse",
"parent": "chip-epic",
"left": 260, "top": 126, "width": 8, "height": 8,
"fillColor": "#78A0F8",
"strokeColor": "#78A0F8"
},
"chip-epic-label": {
"type": "Text",
"parent": "chip-epic",
"left": 273, "top": 124,
"text": "Epic",
"fontColor": "#e8e8ee",
"fontSize": 11
},
"chip-init": { "chip-init": {
"type": "Rectangle", "type": "Rectangle",
"parent": "panel", "parent": "panel",
"left": 326, "top": 118, "width": 86, "height": 24, "left": 96, "top": 118, "width": 86, "height": 24,
"fillColor": "#1a151f", "fillColor": "#1a151f",
"strokeColor": "#C792EA", "strokeColor": "#C792EA",
"corners": [4, 4, 4, 4] "corners": [4, 4, 4, 4]
@@ -125,19 +53,115 @@
"chip-init-dot": { "chip-init-dot": {
"type": "Ellipse", "type": "Ellipse",
"parent": "chip-init", "parent": "chip-init",
"left": 334, "top": 126, "width": 8, "height": 8, "left": 104, "top": 126, "width": 8, "height": 8,
"fillColor": "#C792EA", "fillColor": "#C792EA",
"strokeColor": "#C792EA" "strokeColor": "#C792EA"
}, },
"chip-init-label": { "chip-init-label": {
"type": "Text", "type": "Text",
"parent": "chip-init", "parent": "chip-init",
"left": 347, "top": 124, "left": 117, "top": 124,
"text": "Initiative", "text": "Initiative",
"fontColor": "#e8e8ee", "fontColor": "#e8e8ee",
"fontSize": 11 "fontSize": 11
}, },
"chip-epic": {
"type": "Rectangle",
"parent": "panel",
"left": 188, "top": 118, "width": 58, "height": 24,
"fillColor": "#14171f",
"strokeColor": "#78A0F8",
"corners": [4, 4, 4, 4]
},
"chip-epic-dot": {
"type": "Ellipse",
"parent": "chip-epic",
"left": 196, "top": 126, "width": 8, "height": 8,
"fillColor": "#78A0F8",
"strokeColor": "#78A0F8"
},
"chip-epic-label": {
"type": "Text",
"parent": "chip-epic",
"left": 209, "top": 124,
"text": "Epic",
"fontColor": "#e8e8ee",
"fontSize": 11
},
"chip-story": {
"type": "Rectangle",
"parent": "panel",
"left": 252, "top": 118, "width": 62, "height": 24,
"fillColor": "#15191d",
"strokeColor": "#7DD3A8",
"corners": [4, 4, 4, 4]
},
"chip-story-dot": {
"type": "Ellipse",
"parent": "chip-story",
"left": 260, "top": 126, "width": 8, "height": 8,
"fillColor": "#7DD3A8",
"strokeColor": "#7DD3A8"
},
"chip-story-label": {
"type": "Text",
"parent": "chip-story",
"left": 273, "top": 124,
"text": "Story",
"fontColor": "#e8e8ee",
"fontSize": 11
},
"chip-task": {
"type": "Rectangle",
"parent": "panel",
"left": 320, "top": 118, "width": 56, "height": 24,
"fillColor": "#17181a",
"strokeColor": "#9AA0AA",
"corners": [4, 4, 4, 4]
},
"chip-task-dot": {
"type": "Ellipse",
"parent": "chip-task",
"left": 328, "top": 126, "width": 8, "height": 8,
"fillColor": "#9AA0AA",
"strokeColor": "#9AA0AA"
},
"chip-task-label": {
"type": "Text",
"parent": "chip-task",
"left": 341, "top": 124,
"text": "Task",
"fontColor": "#e8e8ee",
"fontSize": 11
},
"chip-bug": {
"type": "Rectangle",
"parent": "panel",
"left": 382, "top": 118, "width": 54, "height": 24,
"fillColor": "#211519",
"strokeColor": "#E87D7D",
"corners": [4, 4, 4, 4]
},
"chip-bug-dot": {
"type": "Ellipse",
"parent": "chip-bug",
"left": 390, "top": 126, "width": 8, "height": 8,
"fillColor": "#E87D7D",
"strokeColor": "#E87D7D"
},
"chip-bug-label": {
"type": "Text",
"parent": "chip-bug",
"left": 403, "top": 124,
"text": "Bug",
"fontColor": "#e8e8ee",
"fontSize": 11
},
"divider": { "divider": {
"type": "Line", "type": "Line",
"parent": "panel", "parent": "panel",
@@ -222,7 +246,7 @@
"type": "Text", "type": "Text",
"parent": "panel", "parent": "panel",
"left": 96, "top": 540, "left": 96, "top": 540,
"text": "All four type filters ON by default — tap a chip to toggle that type out.", "text": "All five type filters ON by default — click a chip to toggle, double-click to isolate.",
"fontColor": "#56565f", "fontColor": "#56565f",
"fontSize": 10 "fontSize": 10
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

+124 -7
View File
@@ -21,6 +21,19 @@ class _TicketsViewState extends State<TicketsView> {
String? _focusedId; String? _focusedId;
final _focusedKey = GlobalKey(); final _focusedKey = GlobalKey();
final Set<String> _pinned = {'in_progress', 'ready', 'backlog'}; final Set<String> _pinned = {'in_progress', 'ready', 'backlog'};
/// Type-filter chips (T-343), ordered large→small. Each maps 1:1 to a pql
/// ticket type; all on by default. An empty set never persists — toggling off
/// the last one snaps all back on, so the list is never mysteriously blank.
static const _allTypes = {'initiative', 'epic', 'story', 'task', 'bug'};
static const _typeOrder = [
('initiative', 'Initiative'),
('epic', 'Epic'),
('story', 'Story'),
('task', 'Task'),
('bug', 'Bug'),
];
final Set<String> _enabledTypes = {..._allTypes};
StreamSubscription<Message>? _focusSub; StreamSubscription<Message>? _focusSub;
StreamSubscription<SchedulerTick>? _schedulerSub; StreamSubscription<SchedulerTick>? _schedulerSub;
StreamSubscription<Message>? _changedSub; StreamSubscription<Message>? _changedSub;
@@ -44,6 +57,31 @@ class _TicketsViewState extends State<TicketsView> {
}); });
} }
/// Single-click a chip: toggle that type in/out. Removing the last enabled
/// type resets all back on (T-343).
void _toggleType(String type) {
setState(() {
if (_enabledTypes.contains(type)) {
_enabledTypes.remove(type);
if (_enabledTypes.isEmpty) _enabledTypes.addAll(_allTypes);
} else {
_enabledTypes.add(type);
}
});
}
/// Double-click a chip: isolate (solo) that type — it on, all others off.
/// Double-clicking the already-soloed chip restores all-on (chart-legend
/// solo pattern, T-343).
void _soloType(String type) {
setState(() {
final soloed = _enabledTypes.length == 1 && _enabledTypes.contains(type);
_enabledTypes
..clear()
..addAll(soloed ? _allTypes : {type});
});
}
static String _sectionForStatus(String? status) => status ?? 'backlog'; static String _sectionForStatus(String? status) => status ?? 'backlog';
void _onFocus(Message msg) { void _onFocus(Message msg) {
@@ -132,12 +170,14 @@ class _TicketsViewState extends State<TicketsView> {
if (_tickets.isEmpty) return const Padding(padding: EdgeInsets.all(12), child: ClideText('No tickets.\nRun `pql ticket new` to create one.', muted: true)); if (_tickets.isEmpty) return const Padding(padding: EdgeInsets.all(12), child: ClideText('No tickets.\nRun `pql ticket new` to create one.', muted: true));
final lf = _filter.toLowerCase(); final lf = _filter.toLowerCase();
final hasFilter = lf.isNotEmpty; final hasTextFilter = lf.isNotEmpty;
final filtered = hasFilter // All-on = "no type filter" (so a full set never hides null-type tickets).
? _tickets final allTypesOn = _enabledTypes.length == _allTypes.length;
.where((t) => t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf)) final filtering = hasTextFilter || !allTypesOn;
.toList() bool textMatch(_TicketEntry t) =>
: _tickets; t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf);
bool typeMatch(_TicketEntry t) => allTypesOn || _enabledTypes.contains(t.type);
final filtered = _tickets.where((t) => (!hasTextFilter || textMatch(t)) && typeMatch(t)).toList();
const sections = [ const sections = [
('in_progress', 'IN PROGRESS'), ('in_progress', 'IN PROGRESS'),
@@ -172,6 +212,25 @@ class _TicketsViewState extends State<TicketsView> {
), ),
], ],
), ),
// Per-type filter chips (T-343): large→small, all on by default.
Padding(
padding: const EdgeInsets.fromLTRB(8, 2, 8, 6),
child: Wrap(
spacing: 6,
runSpacing: 4,
children: [
for (final (type, label) in _typeOrder)
_TypeChip(
label: label,
color: typeColors.forType(type),
active: _enabledTypes.contains(type),
onToggle: () => _toggleType(type),
onSolo: () => _soloType(type),
tokens: tokens,
),
],
),
),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
@@ -183,7 +242,7 @@ class _TicketsViewState extends State<TicketsView> {
ClideAccordion( ClideAccordion(
label: label, label: label,
count: items.length, count: items.length,
expanded: hasFilter || _isSectionExpanded(status), expanded: filtering || _isSectionExpanded(status),
onToggle: () => _toggle(status), onToggle: () => _toggle(status),
children: [ children: [
for (final t in items) for (final t in items)
@@ -205,6 +264,64 @@ class _TicketsViewState extends State<TicketsView> {
} }
} }
/// A type-filter chip (T-343): a type-colored dot + label. Active = filled
/// tint + colored border; inactive = muted, no fill. Single-click toggles the
/// type; double-click isolates it (chart-legend solo). One [GestureDetector]
/// owns both so Flutter disambiguates single vs double.
class _TypeChip extends StatelessWidget {
const _TypeChip({
required this.label,
required this.color,
required this.active,
required this.onToggle,
required this.onSolo,
required this.tokens,
});
final String label;
final Color color;
final bool active;
final VoidCallback onToggle;
final VoidCallback onSolo;
final SurfaceTokens tokens;
@override
Widget build(BuildContext context) {
final dotColor = active ? color : tokens.globalTextMuted;
return Semantics(
button: true,
toggled: active,
label: '$label type filter',
child: ClideTooltip(
message: 'Click to toggle · double-click to isolate',
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onToggle,
onDoubleTap: onSolo,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
color: active ? color.withAlpha(0x22) : null,
border: Border.all(color: active ? color : tokens.buttonBorder),
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(width: 8, height: 8, decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle)),
const SizedBox(width: 6),
ClideText(label, fontSize: clideFontSmall, fontFamily: clideMonoFamily, color: active ? tokens.globalForeground : tokens.globalTextMuted),
],
),
),
),
),
),
);
}
}
class _TicketEntry { class _TicketEntry {
const _TicketEntry({required this.id, required this.title, this.type, this.status, this.priority, this.parentId}); const _TicketEntry({required this.id, required this.title, this.type, this.status, this.priority, this.parentId});
final String id; final String id;
@@ -136,4 +136,67 @@ void main() {
await pumpAsync(tester); await pumpAsync(tester);
expect(calls, greaterThan(before)); expect(calls, greaterThan(before));
}); });
// -- Type-filter chips (T-343) ------------------------------------------
// The chips own one GestureDetector for both onTap (toggle) and onDoubleTap
// (solo), so a single tap's onTap only fires after the ~300ms double-tap
// window — hence the 350ms pumps below.
Future<void> loadTwoTypes(WidgetTester tester) async {
f.ipc.stub(
'pql.tickets.list',
(_) async => _list([
_t('T-1', 'a bug item', 'backlog', type: 'bug'),
_t('T-2', 'a task item', 'backlog', type: 'task'),
]));
await pumpView(tester);
}
testWidgets('renders a chip per type, large→small (T-343)', (tester) async {
f.ipc.stub('pql.tickets.list', (_) async => _list([_t('T-1', 'x', 'backlog')]));
await pumpView(tester);
for (final label in ['Initiative', 'Epic', 'Story', 'Task', 'Bug']) {
expect(find.text(label), findsOneWidget);
}
});
testWidgets('single-click toggles a type out, others stay (T-343)', (tester) async {
await loadTwoTypes(tester);
expect(find.text('a bug item'), findsOneWidget);
await tester.tap(find.text('Bug'));
await tester.pump(const Duration(milliseconds: 350));
expect(find.text('a bug item'), findsNothing);
expect(find.text('a task item'), findsOneWidget);
});
testWidgets('double-click isolates a type (solo) (T-343)', (tester) async {
await loadTwoTypes(tester);
await tester.tap(find.text('Bug'));
await tester.pump(const Duration(milliseconds: 50));
await tester.tap(find.text('Bug'));
await tester.pump(const Duration(milliseconds: 350));
expect(find.text('a bug item'), findsOneWidget);
expect(find.text('a task item'), findsNothing);
});
testWidgets('toggling off the last enabled type resets all on (T-343)', (tester) async {
await loadTwoTypes(tester);
// Solo Bug → task hidden.
await tester.tap(find.text('Bug'));
await tester.pump(const Duration(milliseconds: 50));
await tester.tap(find.text('Bug'));
await tester.pump(const Duration(milliseconds: 350));
expect(find.text('a task item'), findsNothing);
// Toggle the only-remaining Bug off → snaps all back on → task returns.
await tester.tap(find.text('Bug'));
await tester.pump(const Duration(milliseconds: 350));
expect(find.text('a task item'), findsOneWidget);
expect(find.text('a bug item'), findsOneWidget);
});
} }