Phosphor icons: resolve by name via a generated map (T-314)

Replace the 49 hand-maintained named consts with one generated
label→codepoint map (phosphor_glyphs.g.dart, 1512 glyphs from the glyph
table via tool/gen_phosphor_glyphs.dart). Feature code now references
glyphs by their exact kebab-case name — PhosphorIcons.byName('folder') —
with no raw codepoints; this also lets a Lua extension name an icon
without crossing the FFI boundary with a codepoint.

byName is total: an unknown name degrades to the `placeholder` box so the
bug is visible (it's a real error), while phosphor_glyphs_test asserts
every byName('...') literal in lib/ resolves — recovering the typo check a
const gave. Migrated the 89 call sites. Adds EmptyIconPainter for an
intentional blank that still reserves the icon box; ClideFilterBox gains
showIcon to keep the slot aligned when blank.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 12:46:51 +02:00
co-authored by Claude Opus 4.8
parent b8d75230e8
commit 3833a41a9e
38 changed files with 1779 additions and 163 deletions
+2 -2
View File
@@ -564,7 +564,7 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
onTap: () => _removeAttachment(a),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: ClideIcon(PhosphorIcons.xMark, size: 12, color: theme.globalTextMuted),
child: ClideIcon(PhosphorIcons.byName('x'), size: 12, color: theme.globalTextMuted),
),
),
),
@@ -580,6 +580,6 @@ class _ClaudeComposerState extends State<ClaudeComposer> {
if (a.isImage) {
return ImageThumbnail(path: a.path, size: 44);
}
return ClideIcon(PhosphorIcons.fileText, size: 18, color: theme.globalTextMuted);
return ClideIcon(PhosphorIcons.byName('file-text'), size: 18, color: theme.globalTextMuted);
}
}
@@ -902,14 +902,14 @@ class _AgentRosterRowState extends State<_AgentRosterRow> {
children: [
// Show / hide
_IconButton(
painter: isVisible ? PhosphorIcons.eye : PhosphorIcons.eyeSlash,
painter: isVisible ? PhosphorIcons.byName('eye') : PhosphorIcons.byName('eye-slash'),
tooltip: isVisible ? 'Hide pane' : 'Show pane',
color: tokens.globalTextMuted,
onTap: () => isVisible ? widget.orchestrator!.hide(managed.id) : widget.orchestrator!.show(managed.id),
),
// Mute / unmute
_IconButton(
painter: isMuted ? PhosphorIcons.eyeSlash : PhosphorIcons.eye,
painter: isMuted ? PhosphorIcons.byName('eye-slash') : PhosphorIcons.byName('eye'),
// NOTE: We use eye/eyeSlash as stand-ins until a dedicated speaker
// icon is added to PhosphorIcons (no speaker codepoint yet).
// The semantic tooltip still says mute/unmute so AT users are clear.
@@ -919,21 +919,21 @@ class _AgentRosterRowState extends State<_AgentRosterRow> {
),
// Inject message
_IconButton(
painter: PhosphorIcons.chatCircle,
painter: PhosphorIcons.byName('chat-circle'),
tooltip: 'Inject message',
color: isInjecting ? tokens.globalFocus : tokens.globalTextMuted,
onTap: () => widget.onToggleInject(widget.member.name),
),
// Fork session (T-172): branch into a new pane without touching the original.
_IconButton(
painter: PhosphorIcons.gitBranch,
painter: PhosphorIcons.byName('git-branch'),
tooltip: 'Fork session',
color: tokens.globalTextMuted,
onTap: () => widget.onFork(widget.member.name),
),
// Close session
_IconButton(
painter: PhosphorIcons.xMark,
painter: PhosphorIcons.byName('x'),
tooltip: 'Close session',
color: tokens.globalTextMuted,
onTap: () => widget.onClose(widget.member.name),
@@ -958,7 +958,7 @@ class _AgentRosterRowState extends State<_AgentRosterRow> {
),
const SizedBox(width: 4),
_IconButton(
painter: PhosphorIcons.xMark,
painter: PhosphorIcons.byName('x'),
tooltip: 'Cancel',
color: tokens.globalTextMuted,
onTap: () => widget.onToggleInject(widget.member.name),
@@ -1186,7 +1186,7 @@ class _TaskRow extends StatelessWidget {
// Reassign: cycle to the next roster member.
if (broker != null && broker!.members.length > 1)
_IconButton(
painter: PhosphorIcons.arrowClockwise,
painter: PhosphorIcons.byName('arrow-clockwise'),
tooltip: 'Reassign task',
color: tokens.globalTextMuted,
onTap: () => _reassign(context),
@@ -288,7 +288,7 @@ class _ConversationCardState extends State<ConversationCard> {
builder: (_, hovered, pressed) => Padding(
padding: const EdgeInsets.only(right: 6),
child: ClideIcon(
_collapsed ? PhosphorIcons.caretRight : PhosphorIcons.caretDown,
_collapsed ? PhosphorIcons.byName('caret-right') : PhosphorIcons.byName('caret-down'),
size: 12,
color: tokens.globalTextMuted,
),
@@ -535,7 +535,7 @@ class _ConversationTurn extends StatelessWidget {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ClideIcon(PhosphorIcons.image, size: 16, color: tokens.globalTextMuted),
ClideIcon(PhosphorIcons.byName('image'), size: 16, color: tokens.globalTextMuted),
const SizedBox(width: 8),
Flexible(
child: ClideText('could not load $path', fontSize: clideFontMeta, color: tokens.globalTextMuted, maxLines: 1),
+1 -1
View File
@@ -296,7 +296,7 @@ class ClaudeExtension extends ClideExtension {
id: 'claude.meta',
slot: Slots.sidebar,
title: 'Activity',
icon: PhosphorIcons.robot,
icon: PhosphorIcons.byName('robot'),
priority: 60,
build: (_) => const ClaudeMetaSidebar(),
),
+1 -1
View File
@@ -36,7 +36,7 @@ Widget _placeholder(BuildContext context, double size) {
height: size,
color: t.panelBackground,
alignment: Alignment.center,
child: ClideIcon(PhosphorIcons.image, size: size * 0.45, color: t.globalTextMuted),
child: ClideIcon(PhosphorIcons.byName('image'), size: size * 0.45, color: t.globalTextMuted),
);
}
@@ -19,13 +19,13 @@ import 'package:flutter/widgets.dart';
ClideIconPainter permissionModeIcon(String mode) {
switch (mode) {
case 'acceptEdits':
return PhosphorIcons.pencilSimple;
return PhosphorIcons.byName('pencil-simple');
case 'plan':
return PhosphorIcons.listChecks;
return PhosphorIcons.byName('list-checks');
case 'bypassPermissions':
return PhosphorIcons.shieldWarning;
return PhosphorIcons.byName('shield-warning');
default:
return PhosphorIcons.shieldCheck;
return PhosphorIcons.byName('shield-check');
}
}
@@ -150,7 +150,7 @@ class _TeamChatSidebarState extends State<TeamChatSidebar> {
builder: (ctx, hovered, _) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1),
child: ClideIcon(
PhosphorIcons.arrowsOutSimple,
PhosphorIcons.byName('arrows-out-simple'),
size: 10,
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
),
@@ -383,7 +383,7 @@ class _TeamChatPaneState extends State<TeamChatPane> {
),
child: _interrupt
? Center(
child: ClideIcon(PhosphorIcons.check, size: 9, color: tokens.globalFocus),
child: ClideIcon(PhosphorIcons.byName('check'), size: 9, color: tokens.globalFocus),
)
: null,
),
@@ -157,7 +157,7 @@ class _DecisionsViewState extends State<DecisionsView> {
onTap: _refreshing ? null : _refresh,
tooltip: 'Refresh decisions',
builder: (ctx, hovered, _) =>
ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
ClideIcon(PhosphorIcons.byName('arrow-clockwise'), size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
),
),
],
+2 -2
View File
@@ -43,14 +43,14 @@ class DecisionsExtension extends ClideExtension {
title: 'Decisions',
titleKey: 'tab.title',
i18nNamespace: id,
icon: PhosphorIcons.lightbulb,
icon: PhosphorIcons.byName('lightbulb'),
build: (_) => const DecisionsView(),
),
TabContribution(
id: 'decisions.detail',
slot: Slots.contextPanel,
title: 'Decision',
icon: PhosphorIcons.lightbulb,
icon: PhosphorIcons.byName('lightbulb'),
build: (_) => const DecisionDetailView(),
),
];
+1 -1
View File
@@ -23,7 +23,7 @@ class FilesExtension extends ClideExtension {
id: 'files.tree',
slot: Slots.sidebar,
title: 'Files',
icon: PhosphorIcons.folder,
icon: PhosphorIcons.byName('folder'),
titleKey: 'tab.title',
i18nNamespace: id,
priority: -100,
+1 -1
View File
@@ -20,7 +20,7 @@ class GitExtension extends ClideExtension {
id: 'git.panel',
slot: Slots.sidebar,
title: 'Git',
icon: PhosphorIcons.gitBranch,
icon: PhosphorIcons.byName('git-branch'),
titleKey: 'tab.title',
i18nNamespace: id,
priority: -80,
+1 -1
View File
@@ -190,7 +190,7 @@ class _BranchPickerState extends State<_BranchPicker> {
onTap: () => widget.onDismiss(),
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: ClideIcon(PhosphorIcons.xMark, size: 12, color: tokens.globalTextMuted),
child: ClideIcon(PhosphorIcons.byName('x'), size: 12, color: tokens.globalTextMuted),
),
),
],
+1 -1
View File
@@ -23,7 +23,7 @@ class MarkdownExtension extends ClideExtension {
id: 'markdown.viewer',
slot: Slots.contextPanel,
title: 'Markdown',
icon: PhosphorIcons.fileText,
icon: PhosphorIcons.byName('file-text'),
build: (_) => const MarkdownViewer(),
),
];
+1 -1
View File
@@ -21,7 +21,7 @@ class PqlExtension extends ClideExtension {
id: 'pql.backlinks',
slot: Slots.contextPanel,
title: 'Links',
icon: PhosphorIcons.link,
icon: PhosphorIcons.byName('link'),
priority: -80,
build: (_) => const BacklinksView(),
),
+1 -1
View File
@@ -23,7 +23,7 @@ class SearchExtension extends ClideExtension {
id: 'search.findInFiles',
slot: Slots.sidebar,
title: 'Search',
icon: PhosphorIcons.magnifyingGlass,
icon: PhosphorIcons.byName('magnifying-glass'),
priority: -90,
build: (_) => const SearchPanelView(),
),
+5 -5
View File
@@ -51,7 +51,7 @@ class ReaderActionBar extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
_ActionButton(
painter: PhosphorIcons.caretLeft,
painter: PhosphorIcons.byName('caret-left'),
tooltip: 'Back',
enabled: canGoBack,
onTap: canGoBack ? onBack : null,
@@ -59,7 +59,7 @@ class ReaderActionBar extends StatelessWidget {
),
const SizedBox(width: 2),
_ActionButton(
painter: PhosphorIcons.caretRight,
painter: PhosphorIcons.byName('caret-right'),
tooltip: 'Forward',
enabled: canGoForward,
onTap: canGoForward ? onForward : null,
@@ -68,7 +68,7 @@ class ReaderActionBar extends StatelessWidget {
if (hasPinned) ...[
const SizedBox(width: 2),
_ActionButton(
painter: PhosphorIcons.arrowUUpLeft,
painter: PhosphorIcons.byName('arrow-u-up-left'),
tooltip: 'Jump to pin',
enabled: true,
onTap: onJumpToPin,
@@ -78,7 +78,7 @@ class ReaderActionBar extends StatelessWidget {
if (onEdit != null) ...[
const SizedBox(width: 4),
_ActionButton(
painter: PhosphorIcons.pencilSimple,
painter: PhosphorIcons.byName('pencil-simple'),
tooltip: 'Edit in editor',
enabled: true,
onTap: onEdit,
@@ -103,7 +103,7 @@ class ReaderPinButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _ActionButton(
painter: PhosphorIcons.pushPin,
painter: PhosphorIcons.byName('push-pin'),
tooltip: pinned ? 'Unpin' : 'Pin',
enabled: onTap != null,
active: pinned,
@@ -61,7 +61,7 @@ class _ThemeSwitcherStatusItemState extends State<ThemeSwitcherStatusItem> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
ClideIcon(PhosphorIcons.palette, size: 13, color: tokens.statusBarForeground),
ClideIcon(PhosphorIcons.byName('palette'), size: 13, color: tokens.statusBarForeground),
const SizedBox(width: 6),
// The status bar is all-lowercase; the proper-case name stays
// in the Semantics label for screen readers.
+2 -2
View File
@@ -45,14 +45,14 @@ class TicketsExtension extends ClideExtension {
title: 'Tickets',
titleKey: 'tab.title',
i18nNamespace: id,
icon: PhosphorIcons.ticket,
icon: PhosphorIcons.byName('ticket'),
build: (_) => const TicketsView(),
),
TabContribution(
id: 'tickets.detail',
slot: Slots.contextPanel,
title: 'Ticket',
icon: PhosphorIcons.ticket,
icon: PhosphorIcons.byName('ticket'),
build: (_) => const TicketDetailView(),
),
];
+1 -1
View File
@@ -166,7 +166,7 @@ class _TicketsViewState extends State<TicketsView> {
onTap: _refreshing ? null : _refresh,
tooltip: 'Refresh tickets',
builder: (ctx, hovered, _) =>
ClideIcon(PhosphorIcons.arrowClockwise, size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
ClideIcon(PhosphorIcons.byName('arrow-clockwise'), size: 13, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
),
),
],
+5 -5
View File
@@ -167,21 +167,21 @@ class _StartColumn extends StatelessWidget {
ClideText('START', fontSize: clideFontSmall, color: tokens.sidebarSectionHeader, fontFamily: clideMonoFamily),
const SizedBox(height: 20),
_ActionRow(
icon: PhosphorIcons.folder,
icon: PhosphorIcons.byName('folder'),
label: 'Open folder…',
shortcut: '⌘O',
tokens: tokens,
onTap: () => _openFolder(context),
),
_ActionRow(
icon: PhosphorIcons.gitBranch,
icon: PhosphorIcons.byName('git-branch'),
label: 'Clone from git…',
shortcut: '⌘G',
tokens: tokens,
onTap: () {},
),
_ActionRow(
icon: PhosphorIcons.chatCircle,
icon: PhosphorIcons.byName('chat-circle'),
label: 'Start a Claude session',
shortcut: '⌘C',
tokens: tokens,
@@ -332,7 +332,7 @@ class _RecentRow extends StatelessWidget {
muted: true, fontSize: clideFontMeta, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis)),
if (project.branch != null) ...[
ClideText(' · ', muted: true, fontSize: clideFontMeta),
ClideIcon(PhosphorIcons.gitBranch, size: 11, color: tokens.globalTextMuted),
ClideIcon(PhosphorIcons.byName('git-branch'), size: 11, color: tokens.globalTextMuted),
const SizedBox(width: 3),
Flexible(
child: ClideText(project.branch!,
@@ -384,7 +384,7 @@ class _StickyToggle extends StatelessWidget {
border: Border.all(color: hovered || sticky ? tokens.panelActiveBorder : tokens.globalBorder),
borderRadius: BorderRadius.circular(3),
),
child: sticky ? ClideIcon(PhosphorIcons.check, size: 12, color: tokens.buttonForeground) : null,
child: sticky ? ClideIcon(PhosphorIcons.byName('check'), size: 12, color: tokens.buttonForeground) : null,
),
),
);