diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index 6a4f1085..ceb6a664 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -4348,3 +4348,8 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang - Separator `·` styling (claude_pane.dart:136) should be a shared affordance if other items adopt segmented content. Acceptance: one font family/size/token scheme across all five status items; all items visually centered in the bar''s vertical space; no `globalForeground` borrows; goldens updated.', NULL, '2026-06-12 11:23:21', '2026-06-12 11:23:21', '2026-06-12 11:23:21', NULL, 'dd7198e29cdc41d0837ce0ff068fa26f', 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 ('06FBP3P8YERJ5R7ENSD675BX00', 'status', 'in_progress', 'done', NULL, '2026-06-12 11:26:14', '2026-06-12 11:26:14', '2026-06-12 11:26:14', NULL, '6033e2a267f3dc8e1102fcb2ef515a2e', 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 ('06FBP3EZC7AJANXZVF3D91QYWM', 'status', 'backlog', 'ready', NULL, '2026-06-12 11:28:23', '2026-06-12 11:28:23', '2026-06-12 11:28:23', NULL, '8152841064ed2ff151449541e0b405d3', 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 ('06FBP3P91QQQDT5J50F52FPCKM', 'status', 'backlog', 'ready', NULL, '2026-06-12 11:28:28', '2026-06-12 11:28:28', '2026-06-12 11:28:28', NULL, 'ccb4c4193c34ef5e2bd9e20d407ccda3', 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 ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'status', 'backlog', 'ready', NULL, '2026-06-12 11:28:32', '2026-06-12 11:28:32', '2026-06-12 11:28:32', NULL, '93622dd3b0a88598d1823685a0c2d3e6', 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 ('06FBQ595H08JFTRFSR90GSZQ0G', 'status', 'backlog', 'ready', NULL, '2026-06-12 11:28:38', '2026-06-12 11:28:38', '2026-06-12 11:28:38', NULL, '5729470fe53b18812c0338fa4f23a3c8', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/ticket_idmap/2026-06.sql b/.pql/changelog/ticket_idmap/2026-06.sql index 67c5a4c0..27635261 100644 --- a/.pql/changelog/ticket_idmap/2026-06.sql +++ b/.pql/changelog/ticket_idmap/2026-06.sql @@ -242,3 +242,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P91QQQDT5J50F52FPCKM', 'T-415', '2026-06-12 08:59:28', '2026-06-12 08:59:28', NULL, 'a2dea9268d44b9e8a1fd746bbb947c92', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'T-416', '2026-06-12 10:25:00', '2026-06-12 10:25:00', NULL, 'f8c2a125e661607d5dd0c73cd2c3f2ab', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ4BYD4STCKCY8JNKF23Q4W', 'T-417', '2026-06-12 11:22:15', '2026-06-12 11:22:15', NULL, '15aa9b25417162126cbcde174d3537da', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); +INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ595H08JFTRFSR90GSZQ0G', 'T-418', '2026-06-12 11:26:14', '2026-06-12 11:26:14', NULL, '000e07ae64b08273a2d2d9f8a77d193f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index b53bc78b..65a10768 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -4749,3 +4749,31 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat - Separator `·` styling (claude_pane.dart:136) should be a shared affordance if other items adopt segmented content. Acceptance: one font family/size/token scheme across all five status items; all items visually centered in the bar''s vertical space; no `globalForeground` borrows; goldens updated.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-12 11:22:15', '2026-06-12 11:23:21', NULL, 'a558e4c5edebc1aa89665bc1c35a6b38', 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 ('06FBP3P8YERJ5R7ENSD675BX00', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Config tab → interactive controls (model / effort / permission inline)', 'Turn the Claude sidebar Config tab''s read-only rows (T-183, split in T-395) into live controls — the heart of the power-panel epic. Inline: model picker (reuse the T-408 picker, anchored popover per D-ui primitive), permission-mode control (reuse T-275''s permission_mode_control), effort selector (lands with T-412; row shows ''n/a'' with a hint until supported). Each control reads live SessionStatus and writes through the same session APIs the slash commands use — one implementation, two surfaces (D-6). + +Per-session scoping: controls target the active/primary session; the Team tab''s per-member badges (T-157) stay as-is. Keep read-only rows for facts (version, transcript path, skills count). A11y: every control keyboard-reachable, semantics labels per the a11y contract; run make test-a11y. Golden for the new rows if visual. + +STYLING PASS (user, 2026-06-12): the Claude sidepanel looks bland and the font is +small. While making the Config tab interactive, also do a visual polish pass over +the whole Claude sidebar (Activity/Team/Config): bump the row/label typography to +the panel scale used elsewhere, give sections clearer hierarchy (headers, spacing, +accent marks per ui-design tokens), and make the controls feel like controls. +Treat ui-design skill as the reference for token/type choices.', 'done', 'medium', NULL, NULL, NULL, '2026-06-12 08:59:28', '2026-06-12 11:26:14', NULL, '069271bbde02f9624f80d8703c09a183', 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 ('06FBQ595H08JFTRFSR90GSZQ0G', 'task', '06FBP3EZC7AJANXZVF3D91QYWM', 'Claude sidebar tab robot icon in claude orange', 'User request (2026-06-12, try-it-out): tint the Claude sidebar tab''s robot icon with the Claude accent (#D97757, claudeAccent) so the Claude surface is identifiable at a glance in the icon rail. Nominative use per the licenses.yaml trademark note (marks Claude''s own panel, not clide branding). If the TabContribution/icon-rail doesn''t support per-tab colour, add an optional iconColor to the contribution and honour it in the rail (active/inactive states: keep the accent when active, mute toward the normal inactive tone when not).', 'backlog', 'low', NULL, NULL, NULL, '2026-06-12 11:26:14', '2026-06-12 11:26:14', NULL, '63c89f03c45308ed5d9fbc78656c0941', 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 ('06FBP3EZC7AJANXZVF3D91QYWM', 'epic', NULL, 'Claude sidebar — power control panel + full harness-command capture', 'PROBLEM. clide runs the Claude CLI headless (stream-json). The CLI''s interactive TUI commands don''t exist there: a typed /effort (observed live, 2026-06-12) is forwarded and the CLI answers with a raw system/local_command error — ''/effort isn''t available in this environment.'' Today clide owns only {clear, resume, fork, model} (slash_commands.dart kClideOwnedCommands); every other TUI-only builtin (/permissions /status /cost /help /config /memory /mcp /agents /usage-adjacent…) falls through to that raw error. Meanwhile the sidebar''s Config/Activity tabs (T-182/T-183) are mostly read-only rows — the lone interactions are the permission badge and T-408''s model picker (which lives in the pane''s interaction zone, not the sidebar). + +EVIDENCE / GROUNDING. clide already caches the CLI initialize handshake per version (~/.config/clide/claude/init-.json, T-151). The advertised slash_commands list = skills + a small headless-capable builtin set (clear, compact, context, init, review, security-review, usage, …). TUI-only builtins are NOT advertised — that''s the authoritative signal for what forwards safely. Current CLI 2.1.175. + +DESIGN — three layers: +1. ROUTING TABLE (one source of truth, replaces kClideOwnedCommands): every command token → forward (advertised by the live probe: skills, compact, context, …) | owned (clide implements natively: clear/resume/fork/model today; effort, permissions, config, memory, mcp, agents, todos, status, help as they land) | unavailable (known TUI-only with no equivalent: doctor, login, logout, terminal-setup, ide, exit → friendly explanation card, never the CLI''s raw error). Probe-driven so new CLI versions stay current without code changes. +2. REACTIVE CATCH-ALL: any system/local_command response whose stdout matches "isn''t available in this environment" renders as a muted hint card (pointing at the matching sidebar control when one exists) instead of raw harness output. This captures FUTURE unknown commands automatically — the guarantee ''nothing surfaces as a raw harness error'', not just a hand-maintained list. +3. SIDEBAR = POWER CONTROL PANEL (D-6 parity): every owned command gets a sidebar interaction and every sidebar control is reachable as a slash command. Config tab gains inline pickers (model T-408, permission T-275, effort new); Activity tab gains session controls (clear/compact/fork/resume) and a usage/cost block (/usage IS advertised in stream-json per the probes — revisit T-158''s upstream blocker). + +Child stories carry the implementation slices. Refs: slash_commands.dart, claude_pane._send, claude_meta_sidebar.dart (+T-395 split), claude_config.dart probe, T-408 set_model spike pattern.', 'ready', 'high', NULL, NULL, NULL, '2026-06-12 08:58:29', '2026-06-12 11:28:23', NULL, '8a1878859ad8d93af006c18f5bfbe8ea', 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 ('06FBP3P91QQQDT5J50F52FPCKM', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Activity tab: session controls + usage block (revisit T-158)', 'Two additions to the sidebar Activity tab. (1) Session controls row: clear / compact / fork / resume as icon buttons driving the exact same handlers as the slash commands (claude_pane._send routes) — D-6 parity for the session-mutating set. Confirm-on-destructive (/clear) via the existing dialog pattern. + +(2) Usage/cost block: the version-keyed probes show /usage IS advertised in stream-json (init-2.1.16x.json) — probe what forwarded /usage returns in headless mode and, if parseable, render plan usage alongside the existing cost/context data from result events (T-168 fields already in SessionStatus). This revisits T-158 ''blocked on upstream'' — if the output is unusable, document that in T-158 and keep the cost/context-only block. Link T-158 either way.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-12 08:59:28', '2026-06-12 11:28:28', NULL, 'b98c9924f0cf669ee87b094291432afb', 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 ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Surface Claude Code Workflow runs in the convo + status surfaces', 'Claude Code''s Workflow mechanism (multi-agent orchestration: wf_ runs, phases, agent fan-outs, live progress) currently renders in clide as ordinary tool-use cards at best. Integrate it properly: (1) CONVO PANEL — recognise a Workflow tool-use and render a dedicated run card: phase groups, per-agent rows with live status, and the run''s result summary; reuse the collapser/agent-card machinery (T-305/T-342) rather than inventing new chrome. (2) STATUS/SIDEBAR — while a workflow runs, show an aggregate indicator (run id, phase, agents active/done) in the Claude sidebar Activity tab and/or status line. + +SPIKE FIRST: capture what the stream-json wire actually emits during a Workflow run (tool_use input shape, progress/notification events, sidechain attribution for workflow-spawned agents) — same probe method as T-410''s. Scope the rendering to what the wire really carries; if progress only exists in the harness UI and not on the wire, document that limit and render what''s available (start/end + result). Filed from user request 2026-06-12.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-12 10:25:00', '2026-06-12 11:28:32', NULL, '07318aee4082e6af072c15605745a41d', 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 ('06FBQ595H08JFTRFSR90GSZQ0G', 'task', '06FBP3EZC7AJANXZVF3D91QYWM', 'Claude sidebar tab robot icon in claude orange', 'User request (2026-06-12, try-it-out): tint the Claude sidebar tab''s robot icon with the Claude accent (#D97757, claudeAccent) so the Claude surface is identifiable at a glance in the icon rail. Nominative use per the licenses.yaml trademark note (marks Claude''s own panel, not clide branding). If the TabContribution/icon-rail doesn''t support per-tab colour, add an optional iconColor to the contribution and honour it in the rail (active/inactive states: keep the accent when active, mute toward the normal inactive tone when not).', 'ready', 'low', NULL, NULL, NULL, '2026-06-12 11:26:14', '2026-06-12 11:28:38', NULL, 'bffe2b9dfafc4a13412ceb5c05700915', 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); diff --git a/lib/builtin/claude/src/extension.dart b/lib/builtin/claude/src/extension.dart index 93023957..c806bef7 100644 --- a/lib/builtin/claude/src/extension.dart +++ b/lib/builtin/claude/src/extension.dart @@ -5,6 +5,7 @@ import 'package:clide/clide.dart'; import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey, nextFoldLevel; import 'package:clide/builtin/claude/src/claude_config.dart'; import 'package:clide/builtin/claude/src/claude_status.dart' show nextSafePermissionMode; +import 'package:clide/builtin/claude/src/conversation_view.dart' show claudeAccent; import 'package:clide/builtin/claude/src/claude_session_host.dart'; import 'package:clide/builtin/claude/src/session_orchestrator.dart'; import 'package:clide/builtin/claude/src/pane_context_status.dart'; @@ -308,6 +309,9 @@ class ClaudeExtension extends ClideExtension { slot: Slots.sidebar, title: 'Activity', icon: PhosphorIcons.byName('robot'), + // Claude's accent marks Claude's own panel in the rail (T-418) — + // nominative use per the licenses.yaml trademark note. + iconColor: claudeAccent, priority: 60, build: (_) => const ClaudeMetaSidebar(), ), diff --git a/lib/extension/src/contribution.dart b/lib/extension/src/contribution.dart index ad7dd7c2..124bb853 100644 --- a/lib/extension/src/contribution.dart +++ b/lib/extension/src/contribution.dart @@ -27,6 +27,7 @@ class TabContribution extends ContributionPoint { required this.title, required this.build, this.icon, + this.iconColor, this.priority = 0, this.fileGlobs = const [], this.listenable, @@ -39,6 +40,9 @@ class TabContribution extends ContributionPoint { final String title; final WidgetBuilder build; final Object? icon; + + /// Optional identity tint for the icon-rail glyph (T-418). + final Color? iconColor; final int priority; final List fileGlobs; final Listenable? listenable; diff --git a/lib/src/shell/layout.dart b/lib/src/shell/layout.dart index 331cc0c6..9ff80efa 100644 --- a/lib/src/shell/layout.dart +++ b/lib/src/shell/layout.dart @@ -146,7 +146,7 @@ class _BottomRail extends StatelessWidget { return Container( color: tokens.chromeBackground, child: ClideIconRail( - items: [for (final t in tabs) ClideIconRailItem(id: t.id, icon: _iconFor(slot, t), tooltip: resolveTabTitle(ctx, t))], + items: [for (final t in tabs) ClideIconRailItem(id: t.id, icon: _iconFor(slot, t), tooltip: resolveTabTitle(ctx, t), iconColor: t.iconColor)], activeId: activeId, onSelect: (id) => kernel.panels.activateTab(slot, id), ), diff --git a/lib/widgets/src/clide_icon_rail.dart b/lib/widgets/src/clide_icon_rail.dart index 77b1576c..cfe1937f 100644 --- a/lib/widgets/src/clide_icon_rail.dart +++ b/lib/widgets/src/clide_icon_rail.dart @@ -4,11 +4,16 @@ import 'package:clide/widgets/src/clide_tappable.dart'; import 'package:flutter/widgets.dart'; class ClideIconRailItem { - const ClideIconRailItem({required this.id, required this.icon, required this.tooltip}); + const ClideIconRailItem({required this.id, required this.icon, required this.tooltip, this.iconColor}); final String id; final ClideIconPainter icon; final String tooltip; + + /// Brand/identity tint for this tab's icon (e.g. the Claude accent on the + /// Claude tab, T-418). Shown full-strength when active/hovered and slightly + /// dimmed when idle; null keeps the normal state colours. + final Color? iconColor; } class ClideIconRail extends StatelessWidget { @@ -60,7 +65,10 @@ class _RailButton extends StatelessWidget { onTap: onTap, tooltip: item.tooltip, builder: (ctx, hovered, _) { - final color = active + final tint = item.iconColor; + final color = tint != null + ? (active || hovered ? tint : tint.withValues(alpha: 0.7)) + : active ? tokens.globalForeground : hovered ? tokens.sidebarForeground diff --git a/test/widgets/icon_rail_tint_test.dart b/test/widgets/icon_rail_tint_test.dart new file mode 100644 index 00000000..8a0b073b --- /dev/null +++ b/test/widgets/icon_rail_tint_test.dart @@ -0,0 +1,55 @@ +/// ClideIconRail identity tint (T-418): an item's iconColor overrides the +/// state colours — full-strength when active or hovered, dimmed when idle. +library; + +import 'package:clide/widgets/widgets.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../helpers/kernel_fixture.dart'; +import '../helpers/widget_harness.dart'; + +void main() { + late KernelFixture f; + setUp(() async => f = await KernelFixture.create()); + tearDown(() => f.dispose()); + + const tint = Color(0xFFD97757); + final items = [ + ClideIconRailItem(id: 'claude', icon: PhosphorIcons.byName('robot'), tooltip: 'Claude', iconColor: tint), + ClideIconRailItem(id: 'files', icon: PhosphorIcons.byName('folder'), tooltip: 'Files'), + ]; + + Future pump(WidgetTester tester, {required String activeId}) async { + await tester.pumpWidget( + harness( + f, + Align( + alignment: Alignment.topLeft, + child: SizedBox( + width: 300, + height: 40, + child: ClideIconRail(items: items, activeId: activeId, onSelect: (_) {}), + ), + ), + ), + ); + await tester.pump(); + } + + ClideIcon iconFor(WidgetTester tester, String tooltip) => + tester.widget(find.descendant(of: find.bySemanticsLabel(tooltip), matching: find.byType(ClideIcon))); + + testWidgets('an active tinted item renders the tint full-strength', (tester) async { + await pump(tester, activeId: 'claude'); + expect(iconFor(tester, 'Claude').color, tint); + }); + + testWidgets('an idle tinted item renders the tint dimmed, untinted items keep state colours', (tester) async { + await pump(tester, activeId: 'files'); + final claude = iconFor(tester, 'Claude').color!; + expect(claude.toARGB32() & 0x00FFFFFF, tint.toARGB32() & 0x00FFFFFF); // same hue + expect(claude.a, lessThan(1.0)); // dimmed while idle + expect(iconFor(tester, 'Files').color, isNot(tint)); // untinted untouched + }); +}