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 LARGESMALL 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 LARGESMALL 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 LARGESMALL 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);
});
} }