tint the Claude sidebar tab icon with the Claude accent (T-418)

TabContribution gains an optional iconColor honoured by the icon rail:
full-strength when active/hovered, dimmed (70%) when idle, so the tint
reads as identity without outshouting the active-state border. The
Claude Activity tab sets claudeAccent (#D97757) — nominative use per the
licenses.yaml trademark note (it marks Claude's own panel).

Filed and closed as a try-it-out (user request); trivially revertible if
the accent doesn't land visually.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 13:30:22 +02:00
co-authored by Claude Fable 5
parent 959ec7ddc4
commit 1674d3021a
8 changed files with 108 additions and 3 deletions
@@ -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;
+1
View File
@@ -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);
+28
View File
@@ -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-<version>.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_<id> 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);
+4
View File
@@ -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(),
),
+4
View File
@@ -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<String> fileGlobs;
final Listenable? listenable;
+1 -1
View File
@@ -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),
),
+10 -2
View File
@@ -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
+55
View File
@@ -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<void> 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<ClideIcon>(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
});
}