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:
@@ -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;
|
||||
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;
|
||||
|
||||
@@ -2959,3 +2959,124 @@ Each chip carries its type-colored dot + border using TicketTypeColors (bug #E87
|
||||
|
||||
## 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);
|
||||
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);
|
||||
|
||||
@@ -31,6 +31,11 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### 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
|
||||
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)
|
||||
|
||||
@@ -42,82 +42,10 @@
|
||||
"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": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 326, "top": 118, "width": 86, "height": 24,
|
||||
"left": 96, "top": 118, "width": 86, "height": 24,
|
||||
"fillColor": "#1a151f",
|
||||
"strokeColor": "#C792EA",
|
||||
"corners": [4, 4, 4, 4]
|
||||
@@ -125,19 +53,115 @@
|
||||
"chip-init-dot": {
|
||||
"type": "Ellipse",
|
||||
"parent": "chip-init",
|
||||
"left": 334, "top": 126, "width": 8, "height": 8,
|
||||
"left": 104, "top": 126, "width": 8, "height": 8,
|
||||
"fillColor": "#C792EA",
|
||||
"strokeColor": "#C792EA"
|
||||
},
|
||||
"chip-init-label": {
|
||||
"type": "Text",
|
||||
"parent": "chip-init",
|
||||
"left": 347, "top": 124,
|
||||
"left": 117, "top": 124,
|
||||
"text": "Initiative",
|
||||
"fontColor": "#e8e8ee",
|
||||
"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": {
|
||||
"type": "Line",
|
||||
"parent": "panel",
|
||||
@@ -222,7 +246,7 @@
|
||||
"type": "Text",
|
||||
"parent": "panel",
|
||||
"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",
|
||||
"fontSize": 10
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
@@ -21,6 +21,19 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
String? _focusedId;
|
||||
final _focusedKey = GlobalKey();
|
||||
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<SchedulerTick>? _schedulerSub;
|
||||
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';
|
||||
|
||||
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));
|
||||
|
||||
final lf = _filter.toLowerCase();
|
||||
final hasFilter = lf.isNotEmpty;
|
||||
final filtered = hasFilter
|
||||
? _tickets
|
||||
.where((t) => t.id.toLowerCase().contains(lf) || t.title.toLowerCase().contains(lf) || (t.status ?? '').contains(lf) || (t.type ?? '').contains(lf))
|
||||
.toList()
|
||||
: _tickets;
|
||||
final hasTextFilter = lf.isNotEmpty;
|
||||
// All-on = "no type filter" (so a full set never hides null-type tickets).
|
||||
final allTypesOn = _enabledTypes.length == _allTypes.length;
|
||||
final filtering = hasTextFilter || !allTypesOn;
|
||||
bool textMatch(_TicketEntry t) =>
|
||||
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 = [
|
||||
('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(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
@@ -183,7 +242,7 @@ class _TicketsViewState extends State<TicketsView> {
|
||||
ClideAccordion(
|
||||
label: label,
|
||||
count: items.length,
|
||||
expanded: hasFilter || _isSectionExpanded(status),
|
||||
expanded: filtering || _isSectionExpanded(status),
|
||||
onToggle: () => _toggle(status),
|
||||
children: [
|
||||
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 {
|
||||
const _TicketEntry({required this.id, required this.title, this.type, this.status, this.priority, this.parentId});
|
||||
final String id;
|
||||
|
||||
@@ -136,4 +136,67 @@ void main() {
|
||||
await pumpAsync(tester);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user