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:
+18
-18
@@ -395,7 +395,7 @@ class _RightHatContent extends StatelessWidget {
|
||||
return Row(children: [
|
||||
_WinBtn(icon: const PhosphorIconPainter(0xe32a), onTap: wc.minimize, tokens: tokens),
|
||||
_WinBtn(icon: const PhosphorIconPainter(0xe45e), onTap: wc.toggleMaximize, tokens: tokens),
|
||||
_WinBtn(icon: PhosphorIcons.xMark, onTap: wc.close, tokens: tokens, isClose: true),
|
||||
_WinBtn(icon: PhosphorIcons.byName('x'), onTap: wc.close, tokens: tokens, isClose: true),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -452,7 +452,7 @@ class _ProjectSwitcherButton extends StatelessWidget {
|
||||
children: [
|
||||
ClideText(label, fontSize: 12, color: hovered ? tokens.globalForeground : tokens.chromeForeground, fontFamily: clideMonoFamily),
|
||||
const SizedBox(width: 4),
|
||||
ClideIcon(PhosphorIcons.caretDown, size: 8, color: tokens.chromeForeground),
|
||||
ClideIcon(PhosphorIcons.byName('caret-down'), size: 8, color: tokens.chromeForeground),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -591,7 +591,7 @@ class _RecentProjectRow extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
ClideIcon(PhosphorIcons.folder, size: 14, color: tokens.globalTextMuted),
|
||||
ClideIcon(PhosphorIcons.byName('folder'), size: 14, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -608,7 +608,7 @@ class _RecentProjectRow extends StatelessWidget {
|
||||
muted: true, fontSize: 12, fontFamily: clideMonoFamily, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
ClideText(' · ', muted: true, fontSize: 12),
|
||||
ClideIcon(PhosphorIcons.gitBranch, size: 10, color: tokens.globalTextMuted),
|
||||
ClideIcon(PhosphorIcons.byName('git-branch'), size: 10, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 3),
|
||||
ClideText(project.branch!, muted: true, fontSize: 12, fontFamily: clideMonoFamily),
|
||||
],
|
||||
@@ -920,7 +920,7 @@ class _RevealedTab extends StatelessWidget {
|
||||
tooltip: 'Close',
|
||||
builder: (_, hovered, __) => Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: ClideIcon(PhosphorIcons.xMark, size: 12, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
child: ClideIcon(PhosphorIcons.byName('x'), size: 12, color: hovered ? tokens.globalForeground : tokens.globalTextMuted),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1088,20 +1088,20 @@ class _BottomRail extends StatelessWidget {
|
||||
if (t.icon is ClideIconPainter) return t.icon as ClideIconPainter;
|
||||
if (slot == Slots.sidebar) {
|
||||
return switch (t.id) {
|
||||
'files.tree' => PhosphorIcons.folder,
|
||||
'git.panel' => PhosphorIcons.gitBranch,
|
||||
'pql.panel' => PhosphorIcons.magnifyingGlass,
|
||||
'problems.panel' => PhosphorIcons.warningCircle,
|
||||
'decisions.panel' => PhosphorIcons.lightbulb,
|
||||
'tickets.panel' => PhosphorIcons.ticket,
|
||||
_ => PhosphorIcons.circlesFour,
|
||||
'files.tree' => PhosphorIcons.byName('folder'),
|
||||
'git.panel' => PhosphorIcons.byName('git-branch'),
|
||||
'pql.panel' => PhosphorIcons.byName('magnifying-glass'),
|
||||
'problems.panel' => PhosphorIcons.byName('warning-circle'),
|
||||
'decisions.panel' => PhosphorIcons.byName('lightbulb'),
|
||||
'tickets.panel' => PhosphorIcons.byName('ticket'),
|
||||
_ => PhosphorIcons.byName('circles-four'),
|
||||
};
|
||||
}
|
||||
return switch (t.id) {
|
||||
'markdown.viewer' => PhosphorIcons.eye,
|
||||
'graph.view' => PhosphorIcons.graph,
|
||||
'pql.backlinks' => PhosphorIcons.link,
|
||||
_ => PhosphorIcons.circlesFour,
|
||||
'markdown.viewer' => PhosphorIcons.byName('eye'),
|
||||
'graph.view' => PhosphorIcons.byName('graph'),
|
||||
'pql.backlinks' => PhosphorIcons.byName('link'),
|
||||
_ => PhosphorIcons.byName('circles-four'),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1136,8 +1136,8 @@ class StatusbarCollapseToggle extends StatelessWidget {
|
||||
// The chevron points the DIRECTION OF THE ACTION: collapsing tucks the pane
|
||||
// toward its own edge, expanding brings it back toward the centre.
|
||||
final icon = _isSidebar
|
||||
? (collapsed ? PhosphorIcons.caretLineRight : PhosphorIcons.caretLineLeft)
|
||||
: (collapsed ? PhosphorIcons.caretLineLeft : PhosphorIcons.caretLineRight);
|
||||
? (collapsed ? PhosphorIcons.byName('caret-line-right') : PhosphorIcons.byName('caret-line-left'))
|
||||
: (collapsed ? PhosphorIcons.byName('caret-line-left') : PhosphorIcons.byName('caret-line-right'));
|
||||
final what = _isSidebar ? 'sidebar' : 'context panel';
|
||||
return SizedBox(
|
||||
width: 24,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ class ClideAccordion extends StatelessWidget {
|
||||
padding: const EdgeInsets.only(left: 4, top: 10, bottom: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
ClideIcon(expanded ? PhosphorIcons.caretDown : PhosphorIcons.caretRight, size: 10, color: tokens.globalTextMuted),
|
||||
ClideIcon(expanded ? PhosphorIcons.byName('caret-down') : PhosphorIcons.byName('caret-right'), size: 10, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 6),
|
||||
if (leading != null) ...[leading!, const SizedBox(width: 6)],
|
||||
ClideText('$label · $count',
|
||||
|
||||
@@ -94,7 +94,7 @@ class _RightContent extends StatelessWidget {
|
||||
children: [
|
||||
_WinButton(icon: const PhosphorIconPainter(0xe32a), onTap: wc.minimize, tokens: tokens),
|
||||
_WinButton(icon: const PhosphorIconPainter(0xe45e), onTap: wc.toggleMaximize, tokens: tokens),
|
||||
_WinButton(icon: PhosphorIcons.xMark, onTap: wc.close, tokens: tokens, isClose: true),
|
||||
_WinButton(icon: PhosphorIcons.byName('x'), onTap: wc.close, tokens: tokens, isClose: true),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ class ClideFilterBox extends StatefulWidget {
|
||||
this.hint = 'Filter…',
|
||||
this.debounce = const Duration(milliseconds: 200),
|
||||
this.onSubmitted,
|
||||
this.icon = PhosphorIcons.magnifyingGlass,
|
||||
this.icon,
|
||||
this.showIcon = true,
|
||||
this.address,
|
||||
});
|
||||
|
||||
@@ -24,10 +25,16 @@ class ClideFilterBox extends StatefulWidget {
|
||||
final Duration debounce;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
|
||||
/// Leading glyph. Defaults to the search glass; pass null for inputs
|
||||
/// that aren't searches (e.g. a replace or glob field).
|
||||
/// Leading glyph. Defaults to the search glass (resolved at build, since
|
||||
/// [PhosphorIcons.byName] isn't const); pass a custom painter to override it,
|
||||
/// or set [showIcon] = false for inputs that aren't searches (a replace/glob
|
||||
/// field).
|
||||
final ClideIconPainter? icon;
|
||||
|
||||
/// Whether to draw a leading glyph. False draws a blank one but keeps the
|
||||
/// slot's space, so non-search fields stay aligned with search ones.
|
||||
final bool showIcon;
|
||||
|
||||
/// Makes this box addressable from the CLI (D-6 parity, T-270). When set,
|
||||
/// the box listens on the MessageBus `filter.set` channel for its address
|
||||
/// — so `clide ui filter <address> <text>` drives it exactly as a UI
|
||||
@@ -123,10 +130,14 @@ class _ClideFilterBoxState extends State<ClideFilterBox> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.icon != null) ...[
|
||||
ClideIcon(widget.icon!, size: 13, color: tokens.globalTextMuted),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
// The slot is always reserved (size + gap) so search and non-search
|
||||
// boxes align; showIcon: false just draws a blank glyph (T-314).
|
||||
ClideIcon(
|
||||
widget.showIcon ? (widget.icon ?? PhosphorIcons.byName('magnifying-glass')) : const EmptyIconPainter(),
|
||||
size: 13,
|
||||
color: tokens.globalTextMuted,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
@@ -160,7 +171,7 @@ class _ClideFilterBoxState extends State<ClideFilterBox> {
|
||||
onTap: _clear,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: ClideIcon(PhosphorIcons.xMark, size: 11, color: tokens.globalTextMuted),
|
||||
child: ClideIcon(PhosphorIcons.byName('x'), size: 11, color: tokens.globalTextMuted),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -11,6 +11,23 @@ abstract class ClideIconPainter {
|
||||
void paint(Canvas canvas, Color color);
|
||||
}
|
||||
|
||||
/// Draws nothing, but [ClideIcon] still reserves its `size×size` box — so a
|
||||
/// caller can keep the icon slot for alignment without showing a glyph. This is
|
||||
/// the *intentional* empty (e.g. a non-search filter field), distinct from an
|
||||
/// unknown glyph name, which renders a visible `placeholder` error box (T-314).
|
||||
class EmptyIconPainter extends ClideIconPainter {
|
||||
const EmptyIconPainter();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is EmptyIconPainter;
|
||||
|
||||
@override
|
||||
int get hashCode => (EmptyIconPainter).hashCode;
|
||||
}
|
||||
|
||||
class ClideIcon extends StatelessWidget {
|
||||
const ClideIcon(
|
||||
this.painter, {
|
||||
|
||||
@@ -114,7 +114,7 @@ class _ClideLightboxState extends State<ClideLightbox> {
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: _IconChip(icon: PhosphorIcons.xMark, label: 'close', onTap: widget.onDismiss, tokens: tokens),
|
||||
child: _IconChip(icon: PhosphorIcons.byName('x'), label: 'close', onTap: widget.onDismiss, tokens: tokens),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
|
||||
@@ -12,13 +12,13 @@ import 'package:flutter/widgets.dart';
|
||||
({Color color, ClideIconPainter icon}) _styleFor(ToastSeverity s, SurfaceTokens t) {
|
||||
switch (s) {
|
||||
case ToastSeverity.success:
|
||||
return (color: t.statusSuccess, icon: PhosphorIcons.checkCircle);
|
||||
return (color: t.statusSuccess, icon: PhosphorIcons.byName('check-circle'));
|
||||
case ToastSeverity.warning:
|
||||
return (color: t.statusWarning, icon: PhosphorIcons.warningCircle);
|
||||
return (color: t.statusWarning, icon: PhosphorIcons.byName('warning-circle'));
|
||||
case ToastSeverity.error:
|
||||
return (color: t.statusError, icon: PhosphorIcons.warningCircle);
|
||||
return (color: t.statusError, icon: PhosphorIcons.byName('warning-circle'));
|
||||
case ToastSeverity.info:
|
||||
return (color: t.statusInfo, icon: PhosphorIcons.circlesFour);
|
||||
return (color: t.statusInfo, icon: PhosphorIcons.byName('circles-four'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ class _ClideToastState extends State<ClideToast> {
|
||||
child: ClideTappable(
|
||||
onTap: widget.onDismiss,
|
||||
builder: (ctx, hovered, _) => ClideIcon(
|
||||
PhosphorIcons.xMark,
|
||||
PhosphorIcons.byName('x'),
|
||||
size: clideIconStandard,
|
||||
color: hovered ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:clide/widgets/src/clide_icon.dart';
|
||||
import 'package:clide/widgets/src/icons/phosphor_glyphs.g.dart';
|
||||
|
||||
class PhosphorIconPainter extends ClideIconPainter {
|
||||
const PhosphorIconPainter(this.codePoint, {this.family = 'Phosphor'});
|
||||
@@ -27,55 +28,23 @@ class PhosphorIconPainter extends ClideIconPainter {
|
||||
int get hashCode => Object.hash(codePoint, family);
|
||||
}
|
||||
|
||||
/// Phosphor glyphs, resolved by their exact kebab-case name (e.g. `folder`,
|
||||
/// `folder-simple`, `git-branch`). The full label→codepoint table is the
|
||||
/// generated [kPhosphorGlyphs]; raw codepoints live only there, so feature code
|
||||
/// reads `PhosphorIcons.byName('folder')` rather than `0xe24a` (T-314). A
|
||||
/// string key also lets a Lua extension name an icon without crossing the FFI
|
||||
/// boundary with a codepoint.
|
||||
abstract class PhosphorIcons {
|
||||
static const folder = PhosphorIconPainter(0xe24a);
|
||||
static const fileText = PhosphorIconPainter(0xe23a);
|
||||
static const gitBranch = PhosphorIconPainter(0xe278);
|
||||
static const gitCommit = PhosphorIconPainter(0xe27a);
|
||||
static const gitDiff = PhosphorIconPainter(0xe27c);
|
||||
static const gitPullRequest = PhosphorIconPainter(0xe282);
|
||||
static const magnifyingGlass = PhosphorIconPainter(0xe30c);
|
||||
static const terminal = PhosphorIconPainter(0xe47e);
|
||||
static const terminalWindow = PhosphorIconPainter(0xeae8);
|
||||
static const code = PhosphorIconPainter(0xe1bc);
|
||||
static const codeBlock = PhosphorIconPainter(0xeafe);
|
||||
static const pencilSimple = PhosphorIconPainter(0xe3b4);
|
||||
static const eye = PhosphorIconPainter(0xe220);
|
||||
static const eyeSlash = PhosphorIconPainter(0xe224);
|
||||
static const arrowClockwise = PhosphorIconPainter(0xe036);
|
||||
static const arrowsOutSimple = PhosphorIconPainter(0xe0a6);
|
||||
static const arrowsInSimple = PhosphorIconPainter(0xe09e);
|
||||
static const list = PhosphorIconPainter(0xe2f0);
|
||||
static const listChecks = PhosphorIconPainter(0xeadc);
|
||||
static const gear = PhosphorIconPainter(0xe270);
|
||||
static const puzzlePiece = PhosphorIconPainter(0xe596);
|
||||
static const keyboard = PhosphorIconPainter(0xe2d8);
|
||||
static const palette = PhosphorIconPainter(0xe6c8);
|
||||
static const warning = PhosphorIconPainter(0xe4e0);
|
||||
static const warningCircle = PhosphorIconPainter(0xe4e2);
|
||||
static const shieldCheck = PhosphorIconPainter(0xe40c);
|
||||
static const shieldWarning = PhosphorIconPainter(0xe412);
|
||||
static const check = PhosphorIconPainter(0xe182);
|
||||
static const checkCircle = PhosphorIconPainter(0xe184);
|
||||
static const caretLeft = PhosphorIconPainter(0xe138);
|
||||
static const caretRight = PhosphorIconPainter(0xe13a);
|
||||
static const caretDown = PhosphorIconPainter(0xe136);
|
||||
static const caretUp = PhosphorIconPainter(0xe13c);
|
||||
// Chevron-with-edge-line: reads as "collapse to / expand from the edge" (T-294).
|
||||
static const caretLineLeft = PhosphorIconPainter(0xe132);
|
||||
static const caretLineRight = PhosphorIconPainter(0xe130);
|
||||
static const graph = PhosphorIconPainter(0xeb58);
|
||||
static const treeStructure = PhosphorIconPainter(0xe67c);
|
||||
static const image = PhosphorIconPainter(0xe2ca);
|
||||
static const link = PhosphorIconPainter(0xe2e2);
|
||||
static const pushPin = PhosphorIconPainter(0xe3e2);
|
||||
static const arrowUUpLeft = PhosphorIconPainter(0xe08a);
|
||||
static const chatCircle = PhosphorIconPainter(0xe168);
|
||||
static const robot = PhosphorIconPainter(0xe762);
|
||||
static const ticket = PhosphorIconPainter(0xe490);
|
||||
static const lightbulb = PhosphorIconPainter(0xe2dc);
|
||||
static const notepad = PhosphorIconPainter(0xe63e);
|
||||
static const bookOpen = PhosphorIconPainter(0xe0e6);
|
||||
static const xMark = PhosphorIconPainter(0xe4f6);
|
||||
static const circlesFour = PhosphorIconPainter(0xe190);
|
||||
/// Shown when a name doesn't resolve. An unresolved name is a real error — a
|
||||
/// wrong or missing label — so the fallback is `placeholder`: the box reads
|
||||
/// as a render error and surfaces the bug honestly, rather than masking it as
|
||||
/// an intentional "unknown" the way a `question` mark would.
|
||||
static const String fallbackName = 'placeholder';
|
||||
|
||||
/// Resolve a glyph by its kebab-case [name]. A total function: an unknown
|
||||
/// name degrades to [fallbackName] so a bad value (a typo, a stale setting, a
|
||||
/// Lua extension's string) never crashes the UI. Typos in clide's own call
|
||||
/// sites are caught at CI by `phosphor_glyphs_test`, which asserts every
|
||||
/// `byName('…')` literal in `lib/` exists in the map.
|
||||
static PhosphorIconPainter byName(String name) => PhosphorIconPainter(kPhosphorGlyphs[name] ?? kPhosphorGlyphs[fallbackName]!);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user