From 1cf75ba6e3757425addce63d20ae5a58bd1e4df7 Mon Sep 17 00:00:00 2001 From: truongthinh1412 <9260650+truongthinh1412@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:42:57 +0700 Subject: [PATCH 1/5] refactor(ui): migrate app chrome icons to the shared icon module (tranche 1) --- src/App.tsx | 12 +- src/components/ConnectPhoneModal.tsx | 15 +- src/components/NewTaskPlaceholder.tsx | 5 +- src/components/Sidebar.tsx | 65 ++----- src/components/SidebarFooter.tsx | 59 +----- src/components/TaskAITerminal.tsx | 36 +--- src/components/TaskTitleBar.tsx | 40 +--- src/components/TilingLayout.tsx | 22 +-- src/components/UpdateButton.tsx | 38 +--- src/components/WindowTitleBar.tsx | 43 +--- src/components/WorkspaceControls.tsx | 17 +- src/components/icons.client.test.tsx | 71 +++++++ src/components/icons.tsx | 269 ++++++++++++++++++++++++++ 13 files changed, 402 insertions(+), 290 deletions(-) create mode 100644 src/components/icons.client.test.tsx diff --git a/src/App.tsx b/src/App.tsx index 9514c538c..3a84ea87d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -126,7 +126,7 @@ import { startAgentHookStatusListener } from './store/agentHookStatus'; import { applyPlanContent } from './store/canvas'; import { startEvidenceAutoBuild } from './store/evidence-auto'; import type { PlanContentMessage } from './store/canvas'; -import { SyncIcon } from './components/icons'; +import { GitHubMarkIcon, SyncIcon } from './components/icons'; const ThinkingSpace = lazy(() => import('./components/thinking/ThinkingSpace').then((m) => ({ default: m.ThinkingSpace })), @@ -161,15 +161,7 @@ function DropOverlay() { 'backdrop-filter': 'blur(4px)', }} > - - - + - - - + {store.remoteAccess.connectedClients} client(s) connected diff --git a/src/components/NewTaskPlaceholder.tsx b/src/components/NewTaskPlaceholder.tsx index efe0ef2a3..94021f955 100644 --- a/src/components/NewTaskPlaceholder.tsx +++ b/src/components/NewTaskPlaceholder.tsx @@ -3,6 +3,7 @@ import { toggleNewTaskPanel, createTerminal, unfocusPlaceholder } from '../store import { registerFocusFn, unregisterFocusFn } from '../store/focus'; import { theme } from '../lib/theme'; import { mod } from '../lib/platform'; +import { PlusLargeIcon } from './icons'; /** Quiet ghost surface: a faint fill instead of a dashed wireframe border, so * the add column reads as part of the strip rather than a placeholder. The @@ -62,9 +63,7 @@ export function NewTaskPlaceholder() { style={{ ...ghostStyle, flex: '1' }} title={`New task (${mod}+N)`} > - + {/* Terminal button — same width, fixed height */} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e279c4a43..3e6c71a91 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -3,7 +3,14 @@ import { TaskProjectFilter } from './TaskProjectFilter'; import { createProjectFilterTransition } from '../lib/projectFilterTransition'; import { createSignal, createEffect, createMemo, onMount, onCleanup, For, Show } from 'solid-js'; import type { JSX } from 'solid-js'; -import { GitHubIcon, KanbanIcon } from './icons'; +import { + BotIcon, + ChevronDownAltIcon, + FolderIcon, + GitHubIcon, + KanbanIcon, + PlusLargeIcon, +} from './icons'; import { openGitHubIssues } from '../store/github'; import { openKanban } from '../store/kanban'; import { errMessage } from '../lib/log'; @@ -119,17 +126,7 @@ function createOffscreenAttentionState(taskId: () => string) { /** Small bot/coordinator icon (16x16 SVG). */ function CoordinatorIcon() { - return ( - - - - ); + return ; } function DirectBranchBadge(props: { branchName: string }) { @@ -220,20 +217,14 @@ export function ProjectTaskGroupToggle(props: { gap: '5px', }} > - + /> - + /> - - - } + icon={} onClick={handleAddProject} title="Add project" size="sm" @@ -872,15 +853,7 @@ export function Sidebar() { width: '100%', }} > - + Link Project } @@ -904,9 +877,7 @@ export function Sidebar() { width: '100%', }} > - - - + New Task diff --git a/src/components/SidebarFooter.tsx b/src/components/SidebarFooter.tsx index a519051c4..e4d615075 100644 --- a/src/components/SidebarFooter.tsx +++ b/src/components/SidebarFooter.tsx @@ -11,6 +11,7 @@ import { } from '../store/store'; import { theme } from '../lib/theme'; import { alt, mod } from '../lib/platform'; +import { KeyboardIcon, LightbulbIcon, PhoneIcon, ScrambleIcon } from './icons'; /** Compact utilities and optional activity totals, shared by every theme. */ export function SidebarFooter(props: { onConnectPhone: () => void }) { @@ -70,20 +71,7 @@ export function SidebarFooter(props: { onConnectPhone: () => void }) { phoneConnected() ? 'Phone connected: manage remote access' : 'Connect phone' } > - + Phone access @@ -708,9 +692,7 @@ function AddAgentMenu(props: { taskId: string }) { padding: '0', }} > - - - +
- - - + diff --git a/src/components/TaskTitleBar.tsx b/src/components/TaskTitleBar.tsx index 94e890337..78bc4f6b4 100644 --- a/src/components/TaskTitleBar.tsx +++ b/src/components/TaskTitleBar.tsx @@ -21,9 +21,13 @@ import { MoreMenu } from './chat/MoreMenu'; import { StatusDot, getDotTooltip } from './StatusDot'; import { ActivityIcon, + CheckCircleIcon, CheckIcon, ClockIcon, CloseIcon, + CollapseAltIcon, + MinusIcon, + PanelRightIcon, PlayIcon, StopIcon, UndoIcon, @@ -289,9 +293,7 @@ export function TaskTitleBar(props: TaskTitleBarProps) { /> } > - - - + {props.pushing ? 'Pushing…' : props.pushSuccess ? 'Pushed' : 'Finish'} @@ -343,17 +345,7 @@ export function TaskTitleBar(props: TaskTitleBarProps) { ? [ { label: 'Minimize (stop agents)', - icon: ( - - ), + icon: , run: () => void collapseTask(props.task.id), }, ] @@ -363,19 +355,7 @@ export function TaskTitleBar(props: TaskTitleBarProps) {
- - - - } + icon={} onClick={() => isTaskCanvasVisible(props.task) ? closeTaskCanvas(props.task.id) @@ -385,11 +365,7 @@ export function TaskTitleBar(props: TaskTitleBarProps) { pressed={isTaskCanvasVisible(props.task)} /> - - - } + icon={} onClick={() => { toggleTaskFocusMode(props.task.id); }} diff --git a/src/components/TilingLayout.tsx b/src/components/TilingLayout.tsx index a092da25c..cc18d45d8 100644 --- a/src/components/TilingLayout.tsx +++ b/src/components/TilingLayout.tsx @@ -41,7 +41,7 @@ import { mod } from '../lib/platform'; import { createCtrlShiftWheelResizeHandler } from '../lib/wheelZoom'; import { shouldAnimateTaskAppearance } from '../lib/reducedMotion'; import { TASK_TILE_DEFAULT_WIDTH, TASK_TILE_MIN_WIDTH } from '../lib/layout-sizes'; -import { SyncIcon } from './icons'; +import { FolderIcon, SyncIcon } from './icons'; // Lazy: the document workspace (CodeMirror, parse5) loads when its panel opens. const DocumentWorkspacePanel = lazy(() => @@ -605,15 +605,7 @@ export function TilingLayout() { color: theme.fgSubtle, }} > - +
- + Link Project diff --git a/src/components/UpdateButton.tsx b/src/components/UpdateButton.tsx index 673e07997..1b3f8a850 100644 --- a/src/components/UpdateButton.tsx +++ b/src/components/UpdateButton.tsx @@ -11,41 +11,7 @@ import { Show } from 'solid-js'; import { theme } from '../lib/theme'; import { updateStatus, downloadUpdate, installUpdate } from '../store/store'; - -const DownloadIcon = () => ( - -); - -const RestartIcon = () => ( - -); +import { DownloadAltIcon, RefreshIcon } from './icons'; export function UpdateButton() { const phase = () => updateStatus().phase; @@ -94,7 +60,7 @@ export function UpdateButton() { > : } + fallback={phase() === 'downloaded' ? : } > {updateStatus().downloadPercent}% diff --git a/src/components/WindowTitleBar.tsx b/src/components/WindowTitleBar.tsx index 9befc69cb..01ce34d92 100644 --- a/src/components/WindowTitleBar.tsx +++ b/src/components/WindowTitleBar.tsx @@ -2,6 +2,7 @@ import { createSignal, onCleanup, onMount } from 'solid-js'; import { WorkspaceControls } from './WorkspaceControls'; import { appWindow } from '../lib/window'; import { FocusModeTaskIndicators } from './FocusModeTaskIndicators'; +import { CloseThinIcon, LogoIcon, MaximizeIcon, MinimizeIcon, RestoreIcon } from './icons'; export function WindowTitleBar() { const [isFocused, setIsFocused] = createSignal(true); @@ -86,21 +87,11 @@ export function WindowTitleBar() { class="window-drag-region" onDblClick={() => void handleToggleMaximize()} > - + style={{ 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }} + />
@@ -115,9 +106,7 @@ export function WindowTitleBar() { aria-label="Minimize window" title="Minimize" > - +
diff --git a/src/components/WorkspaceControls.tsx b/src/components/WorkspaceControls.tsx index 07502c7db..04b3d1763 100644 --- a/src/components/WorkspaceControls.tsx +++ b/src/components/WorkspaceControls.tsx @@ -1,5 +1,6 @@ import { store, toggleSettingsDialog, toggleSidebar } from '../store/store'; import { mod } from '../lib/platform'; +import { ChevronLeftAltIcon, GearIcon } from './icons'; import { IconButton } from './IconButton'; import { UpdateButton } from './UpdateButton'; @@ -13,25 +14,15 @@ export function WorkspaceControls() { > - - + /> } onClick={() => toggleSidebar()} title={`${store.sidebarVisible ? 'Collapse' : 'Show'} sidebar (${mod}+B)`} /> - - - } + icon={} onClick={() => toggleSettingsDialog(true)} title={`Settings (${mod}+,)`} /> diff --git a/src/components/icons.client.test.tsx b/src/components/icons.client.test.tsx new file mode 100644 index 000000000..524df49cd --- /dev/null +++ b/src/components/icons.client.test.tsx @@ -0,0 +1,71 @@ +import { render } from 'solid-js/web'; +import { afterEach, describe, expect, it } from 'vitest'; +import * as icons from './icons'; + +const disposers: Array<() => void> = []; + +afterEach(() => { + while (disposers.length > 0) disposers.pop()?.(); + document.body.replaceChildren(); +}); + +/** Every exported icon component, so the whole module stays render-covered. */ +const ALL_ICONS = Object.values(icons); + +describe('shared icon module', () => { + it('renders every icon at its default size of 16 and hides it from the a11y tree', () => { + for (const Icon of ALL_ICONS) { + const container = document.createElement('div'); + document.body.append(container); + disposers.push(render(() => , container)); + + const svg = container.querySelector('svg'); + expect(svg, Icon.name).not.toBeNull(); + expect(svg?.getAttribute('width'), Icon.name).toBe('16'); + expect(svg?.getAttribute('height'), Icon.name).toBe('16'); + expect(svg?.getAttribute('aria-hidden'), Icon.name).toBe('true'); + expect(svg?.getAttribute('role'), Icon.name).toBeNull(); + expect(svg?.querySelector('title'), Icon.name).toBeNull(); + } + }); + + it('honors a custom size (12, the small size from issue #208)', () => { + for (const Icon of ALL_ICONS) { + const container = document.createElement('div'); + document.body.append(container); + disposers.push(render(() => , container)); + + const svg = container.querySelector('svg'); + expect(svg?.getAttribute('width'), Icon.name).toBe('12'); + expect(svg?.getAttribute('height'), Icon.name).toBe('12'); + } + }); + + it('keeps the monochrome currentColor contract', () => { + for (const Icon of ALL_ICONS) { + const container = document.createElement('div'); + document.body.append(container); + disposers.push(render(() => , container)); + + const svg = container.querySelector('svg'); + const fill = svg?.getAttribute('fill'); + expect(['currentColor', 'none'], Icon.name).toContain(fill); + if (fill === 'none') { + expect(svg?.getAttribute('stroke'), Icon.name).toBe('currentColor'); + } + } + }); + + it('renders a title with role="img" when a title is provided', () => { + for (const Icon of ALL_ICONS) { + const container = document.createElement('div'); + document.body.append(container); + disposers.push(render(() => , container)); + + const svg = container.querySelector('svg'); + expect(svg?.getAttribute('aria-hidden'), Icon.name).toBeNull(); + expect(svg?.getAttribute('role'), Icon.name).toBe('img'); + expect(svg?.querySelector('title')?.textContent, Icon.name).toBe('Test label'); + } + }); +}); diff --git a/src/components/icons.tsx b/src/components/icons.tsx index e17ab3b37..2ad6e5fa2 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -550,3 +550,272 @@ export function CircleCheckIcon(props: IconProps): JSX.Element { ); } + +interface StrokeSvgIconProps extends IconProps { + children: JSX.Element; + viewBox?: string; + strokeWidth: number | string; + roundCaps?: boolean; + roundJoins?: boolean; +} + +/** Stroke-drawn glyphs (fill="none"): attributes mirror what the call sites pasted. */ +function StrokeSvgIcon(props: StrokeSvgIconProps): JSX.Element { + const size = () => props.size ?? 16; + + return ( + + {props.title ? {props.title} : null} + {props.children} + + ); +} + +/** The parallel-bars logo from the window title bar. */ +export function LogoIcon(props: IconProps): JSX.Element { + return ( + + + + + + + ); +} + +/** Thin 10x10 window controls (custom title bar). */ +export function MinimizeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function MaximizeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function RestoreIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function CloseThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Coordinator/bot marker shown next to agent-driven tasks. */ +export function BotIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Filled chevron for collapsible section headers; see also ChevronDownIcon above. */ +export function ChevronDownAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Chunky plus for "create" affordances; PlusIcon above is the inline-text variant. */ +export function PlusLargeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Compact plus that stays legible at 13px and below. */ +export function PlusSmallIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Single horizontal bar: the collapsed-task affordance. */ +export function MinusIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Keyboard: the shortcuts cheat-sheet affordance in the sidebar footer. */ +export function KeyboardIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Side panel with a divider: toggles the task canvas. */ +export function PanelRightIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Corners-in brackets: shown while unfocused as the enter-focus affordance in the document workspace, and always on the task title bar. */ +export function CollapseAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Check inside a circle: the Finish (merge or push) button's idle glyph. */ +export function CheckCircleIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Terminal window chrome (panel-layout toggle); see also the prompt-style TerminalIcon above. */ +export function TerminalAltIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Two side-by-side panels: the split-terminal layout toggle. */ +export function ColumnsIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Phone handset: remote-access affordances. */ +export function PhoneIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Crossing strokes: the Arena (agent-vs-agent) affordance. */ +export function ScrambleIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Stroke-drawn download; see also the filled DownloadIcon above. */ +export function DownloadAltIcon(props: IconProps): JSX.Element { + return ( + + + + + + ); +} + +/** Circular arrow: restart to install an update. */ +export function RefreshIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Oversized check for success states (phone connected). */ +export function CheckLargeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** The GitHub octocat mark; GitHubIcon above is this project's own glyph. */ +export function GitHubMarkIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Gear: project settings — name, folder and agents. */ +export function GearIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Filled octicon chevron for commit-step navigation; see also the stroke ChevronLeftIcon above. */ +export function ChevronLeftAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Lit bulb: the thinking-tours affordance in the sidebar footer. */ +export function LightbulbIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} From 55504740dc31a530e277d66beda0dc329e355ff2 Mon Sep 17 00:00:00 2001 From: truongthinh1412 <9260650+truongthinh1412@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:50:54 +0700 Subject: [PATCH 2/5] refactor(ui): migrate documents and arena icons to the shared module (tranche 2) --- src/arena/ArenaOverlay.tsx | 15 +- src/arena/BattleScreen.tsx | 5 +- src/arena/ConfigScreen.tsx | 17 +- src/arena/HistoryScreen.tsx | 27 +-- src/arena/ResultsScreen.tsx | 113 ++------- src/components/CanvasTabStrip.tsx | 10 +- src/components/ChangeTourButton.tsx | 36 +-- src/components/NewTaskPanel.tsx | 22 +- src/components/Sidebar.tsx | 2 +- src/components/TaskAITerminal.tsx | 2 +- src/components/TerminalSelectionMenu.tsx | 3 +- src/components/icons.tsx | 219 ++++++++++++++++++ src/documents/AnnotationMarker.tsx | 20 +- src/documents/BlockActions.tsx | 33 +-- src/documents/DocumentIcon.tsx | 19 -- src/documents/DocumentWorkspacePanel.tsx | 91 ++------ src/documents/FileTreePanel.tsx | 16 +- .../RunComposer.draft.client.test.tsx | 31 +++ src/documents/RunComposer.tsx | 6 +- 19 files changed, 357 insertions(+), 330 deletions(-) delete mode 100644 src/documents/DocumentIcon.tsx diff --git a/src/arena/ArenaOverlay.tsx b/src/arena/ArenaOverlay.tsx index a5f49d231..ef5f3beaf 100644 --- a/src/arena/ArenaOverlay.tsx +++ b/src/arena/ArenaOverlay.tsx @@ -5,6 +5,7 @@ import './arena-battle.css'; import './arena-results.css'; import './arena-history.css'; import { Show, onMount } from 'solid-js'; +import { ScrambleIcon } from '../components/icons'; import { arenaStore } from './store'; import { loadArenaPresets, loadArenaHistory } from './persistence'; import { ConfigScreen } from './ConfigScreen'; @@ -31,19 +32,7 @@ export function ArenaOverlay(props: ArenaOverlayProps) {
- - - - + AI Arena
diff --git a/src/arena/ConfigScreen.tsx b/src/arena/ConfigScreen.tsx index 6cc362cec..dac7981b6 100644 --- a/src/arena/ConfigScreen.tsx +++ b/src/arena/ConfigScreen.tsx @@ -19,7 +19,7 @@ import { invoke } from '../lib/ipc'; import { IPC } from '../../electron/ipc/channels'; import { saveArenaPresets } from './persistence'; import { ProjectSelect } from '../components/ProjectSelect'; -import { CloseIcon, HistoryIcon, PlusIcon } from '../components/icons'; +import { CloseIcon, HistoryIcon, PlusIcon, ScrambleIcon } from '../components/icons'; import { MAX_COMPETITORS, MIN_COMPETITORS } from './store'; import type { BattleCompetitor } from './types'; @@ -221,20 +221,7 @@ export function ConfigScreen() { {/* Actions */}
diff --git a/src/arena/HistoryScreen.tsx b/src/arena/HistoryScreen.tsx index f0bac0db5..a23c656c5 100644 --- a/src/arena/HistoryScreen.tsx +++ b/src/arena/HistoryScreen.tsx @@ -4,6 +4,7 @@ import { saveArenaHistory } from './persistence'; import { formatDuration } from './utils'; import { confirm } from '../lib/dialog'; import { invoke } from '../lib/ipc'; +import { ChevronLeftThinIcon, TrashAltIcon } from '../components/icons'; import { IPC } from '../../electron/ipc/channels'; function formatDate(iso: string): string { @@ -82,18 +83,7 @@ export function HistoryScreen() { class="arena-close-btn" onClick={() => setPhase(arenaStore.previousPhase ?? 'config')} > - - - + Back
@@ -123,18 +113,7 @@ export function HistoryScreen() { title="Delete match and clean up worktrees" > ...
}> - - - + diff --git a/src/arena/ResultsScreen.tsx b/src/arena/ResultsScreen.tsx index b6b14cc30..7af8457c4 100644 --- a/src/arena/ResultsScreen.tsx +++ b/src/arena/ResultsScreen.tsx @@ -1,4 +1,14 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; +import { + ChevronLeftThinIcon, + ClockAltIcon, + CompareIcon, + MergeIcon, + PlusThinIcon, + ShieldAltIcon, + StarIcon, + SyncAltIcon, +} from '../components/icons'; import { ChangedFilesList } from '../components/ChangedFilesList'; import { DiffViewerDialog } from '../components/DiffViewerDialog'; import { CommitDialog } from './CommitDialog'; @@ -259,9 +269,7 @@ export function ResultsScreen() { onClick={() => setRating(competitor.id, star)} title={`${star} star${star > 1 ? 's' : ''}`} > - - - + )} @@ -280,21 +288,7 @@ export function ResultsScreen() { disabled={merge.merging() || merge.mergedId() !== null} onClick={() => merge.handleMergeClick(competitor)} > - - - - - - + {merge.merging() ? 'Merging...' : 'Merge'} @@ -312,104 +306,33 @@ export function ResultsScreen() {
- - - - + {projectLabel()}
diff --git a/src/components/CanvasTabStrip.tsx b/src/components/CanvasTabStrip.tsx index a524102fb..e0202f709 100644 --- a/src/components/CanvasTabStrip.tsx +++ b/src/components/CanvasTabStrip.tsx @@ -4,12 +4,18 @@ import { sf } from '../lib/fontScale'; import { canvasTabKey } from '../lib/canvas-tabs'; import type { CanvasTab, CanvasTabKind } from '../store/types'; import { IconButton } from './IconButton'; -import { CloseIcon, ExpandIcon, ExternalLinkIcon, PlusIcon, ScreenNormalIcon } from './icons'; +import { + CloseIcon, + DocumentIcon, + ExpandIcon, + ExternalLinkIcon, + PlusIcon, + ScreenNormalIcon, +} from './icons'; import { UnderstandButton, tourButtonStyle } from './understanding/UnderstandButton'; import type { UnderstandingTourController } from '../lib/create-understanding-tour'; import { isTourFilePath } from '../lib/tour-file'; import { isEditableDocumentPath } from '../lib/document-path'; -import { DocumentIcon } from '../documents/DocumentIcon'; interface CanvasTabStripProps { tabs: CanvasTab[]; diff --git a/src/components/ChangeTourButton.tsx b/src/components/ChangeTourButton.tsx index 55d08d7ab..b04a52d1f 100644 --- a/src/components/ChangeTourButton.tsx +++ b/src/components/ChangeTourButton.tsx @@ -12,6 +12,7 @@ import { import { store } from '../store/store'; import { askCodeLabel } from './understanding/ask-code-label'; import { TourModelMenu } from './understanding/TourModelMenu'; +import { ArrowRightThinIcon, CloseAltIcon, CycleIcon } from './icons'; export function ChangeTourButton(props: { tour: ChangeTourController; @@ -117,26 +118,7 @@ export function ChangeTourButton(props: {
{props.children} diff --git a/src/components/NewTaskPanel.tsx b/src/components/NewTaskPanel.tsx index 721219792..f018a5ba7 100644 --- a/src/components/NewTaskPanel.tsx +++ b/src/components/NewTaskPanel.tsx @@ -20,7 +20,15 @@ import { createFocusRestore } from '../lib/focus-restore'; import { topDialog } from '../lib/dialog-stack'; import { registerFocusFn, unregisterFocusFn } from '../store/focused-panel'; import { setStore } from '../store/core'; -import { ContainerIcon, FolderIcon, GitBranchIcon, GitHubIcon, PlusIcon, SyncIcon } from './icons'; +import { + ChevronRightThinIcon, + ContainerIcon, + FolderIcon, + GitBranchIcon, + GitHubIcon, + PlusIcon, + SyncIcon, +} from './icons'; import { ConfirmDialog } from './ConfirmDialog'; import { errMessage } from '../lib/log'; import { invoke } from '../lib/ipc'; @@ -1420,16 +1428,10 @@ export function NewTaskPanel(props: NewTaskPanelProps) { cursor: 'pointer', }} > - + /> Advanced options diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 3e6c71a91..5220ee9f7 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -6,6 +6,7 @@ import type { JSX } from 'solid-js'; import { BotIcon, ChevronDownAltIcon, + DocumentIcon, FolderIcon, GitHubIcon, KanbanIcon, @@ -54,7 +55,6 @@ import { EditProjectDialog } from './EditProjectDialog'; import { AddProjectDialog } from '../documents/AddProjectDialog'; import { openDocumentWorkspace } from '../documents/store'; import { codeProjects, isDocumentProject } from '../store/projects'; -import { DocumentIcon } from '../documents/DocumentIcon'; import { ImportWorktreesDialog } from './ImportWorktreesDialog'; import { SidebarFooter } from './SidebarFooter'; import { IconButton } from './IconButton'; diff --git a/src/components/TaskAITerminal.tsx b/src/components/TaskAITerminal.tsx index e5586912f..1e2a836b3 100644 --- a/src/components/TaskAITerminal.tsx +++ b/src/components/TaskAITerminal.tsx @@ -35,7 +35,6 @@ import { AgentChatView } from './AgentChatView'; import { isAgentChat, agentChatProvider, agentChatUnavailableReason } from '../store/agent-chat'; import { agentViewHandsOff, agentViewSwitchCost, switchMainAgentView } from '../store/agent-view'; import { setStore } from '../store/core'; -import { DocumentIcon } from '../documents/DocumentIcon'; import { isEditableDocumentPath } from '../lib/document-path'; import { editAsDocument } from '../documents/store'; import { saveState } from '../store/persistence'; @@ -49,6 +48,7 @@ import { CloseIcon, ColumnsIcon, CommentIcon, + DocumentIcon, ExternalLinkIcon, EyeIcon, CopyIcon, diff --git a/src/components/TerminalSelectionMenu.tsx b/src/components/TerminalSelectionMenu.tsx index ef293b0e1..a10ec1122 100644 --- a/src/components/TerminalSelectionMenu.tsx +++ b/src/components/TerminalSelectionMenu.tsx @@ -1,7 +1,6 @@ import { createSignal, For, onMount, Show, untrack } from 'solid-js'; import { Portal } from 'solid-js/web'; -import { CopyIcon, EyeIcon, SendIcon } from './icons'; -import { DocumentIcon } from '../documents/DocumentIcon'; +import { CopyIcon, DocumentIcon, EyeIcon, SendIcon } from './icons'; /** Selection text is captured before opening, so focus changes cannot alter the handoff. */ export function TerminalSelectionMenu(props: { diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 2ad6e5fa2..1272e2d2b 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -819,3 +819,222 @@ export function LightbulbIcon(props: IconProps): JSX.Element { ); } + +/** Stroke-drawn bar: minimize the document workspace back to the sidebar. */ +export function MinimizeBarIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Filled octicon chevron for commit-step navigation; see also the stroke ChevronRightIcon above. */ +export function ChevronRightAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Sharp stroke chevron on the Advanced options disclosure; see also ChevronRightIcon above. */ +export function ChevronRightThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Rounded stroke chevron for Arena back buttons; see also ChevronLeftIcon above. */ +export function ChevronLeftThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Corners-out brackets: shown while focus mode is active, as the exit-focus affordance; pairs with CollapseAltIcon. */ +export function ExpandAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Margins with in/out arrows: the reading-width toggle in the document toolbar. */ +export function FullWidthIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Outline folder: reveal the project folder in the file manager; FolderIcon above is filled. */ +export function FolderOpenIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** The document-project glyph: sidebar rows, the workspace header and the file tree. */ +export function DocumentIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Stroke-drawn note card in the annotation margin; CommentIcon above is the speech bubble. */ +export function CommentAltIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Note card with lines: the composer's note mode. */ +export function NoteIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Circled question mark: the composer's ask mode. */ +export function QuestionIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Filled square: stop a running arena competitor; StopIcon above is the octagon. */ +export function StopAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Stroke-drawn trash bin: delete a history match; TrashIcon above is the filled one. */ +export function TrashAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Five-pointed star: rate an arena competitor's approach. */ +export function StarIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Cradle joining two nodes: merge a worktree into the project's main branch. */ +export function MergeIcon(props: IconProps): JSX.Element { + return ( + + + + + + + ); +} + +/** Boxed shield: the arena project's container; ShieldIcon above is the filled badge. */ +export function ShieldAltIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Two ruled columns: compare every arena approach side by side. */ +export function CompareIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Chasing arrows: run the arena match again; SyncIcon above is the filled one. */ +export function SyncAltIcon(props: IconProps): JSX.Element { + return ( + + + + + + + ); +} + +/** Stroke-drawn plus: start a fresh arena match. */ +export function PlusThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Stroke-drawn clock face: arena match history; ClockIcon above is the filled one. */ +export function ClockAltIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Two nodes joined by a loop: generate a tour of the changes. */ +export function CycleIcon(props: IconProps): JSX.Element { + return ( + + + + + + ); +} + +/** Stroke-drawn X: cancel tour generation; CloseThinIcon above is the 10x10 window control. */ +export function CloseAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Stroke-drawn arrow: the tour button's go affordance once it is idle. */ +export function ArrowRightThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} diff --git a/src/documents/AnnotationMarker.tsx b/src/documents/AnnotationMarker.tsx index a35de063f..6c7a705bb 100644 --- a/src/documents/AnnotationMarker.tsx +++ b/src/documents/AnnotationMarker.tsx @@ -9,6 +9,7 @@ import { import { AnnotationBubble } from './AnnotationBubble'; import type { DocumentAnnotation } from './types'; import { focusAnnotation, registerPinnedBubble, workspaceUi } from './workspace-ui'; +import { CommentAltIcon } from '../components/icons'; interface AnnotationMarkerProps { /** Portalled controls must follow their owning panel’s visibility. */ @@ -172,24 +173,7 @@ export function AnnotationMarker(props: AnnotationMarkerProps) { > {/* A question waiting on its agent spins in the margin, so the wait is visible without opening the bubble to read "Answering…". */} -
{nodeProps.node.name} diff --git a/src/documents/RunComposer.draft.client.test.tsx b/src/documents/RunComposer.draft.client.test.tsx index a1ceafeae..4b1dd59a7 100644 --- a/src/documents/RunComposer.draft.client.test.tsx +++ b/src/documents/RunComposer.draft.client.test.tsx @@ -1,6 +1,8 @@ import { render } from 'solid-js/web'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { setStore } from '../store/core'; +import { GitGraphIcon, NoteIcon, QuestionIcon } from '../components/icons'; +import { ActionIcon, BLOCK_ACTIONS } from './BlockActions'; import { RunComposer } from './RunComposer'; import { documentStore, setDocumentComposerDraft, type DocumentSelection } from './store'; import type { DocumentBlock } from './markdown-blocks'; @@ -39,6 +41,35 @@ const passage: DocumentSelection = { wholeDocument: false, }; +describe('block action glyphs', () => { + it('points every action at its shared icon-module component', () => { + expect(BLOCK_ACTIONS.find((a) => a.kind === 'proposals')?.icon).toBe(GitGraphIcon); + expect(BLOCK_ACTIONS.find((a) => a.kind === 'note')?.icon).toBe(NoteIcon); + expect(BLOCK_ACTIONS.find((a) => a.kind === 'question')?.icon).toBe(QuestionIcon); + }); + + it('renders the same glyph body as the module icon itself', () => { + for (const action of BLOCK_ACTIONS) { + const host = document.createElement('div'); + document.body.append(host); + const Icon = action.icon; + disposers.push( + render( + () => ( + <> + + + + ), + host, + ), + ); + const [fromAction, fromModule] = Array.from(host.querySelectorAll('path')); + expect(fromAction.getAttribute('d'), action.kind).toBe(fromModule.getAttribute('d')); + } + }); +}); + describe('RunComposer draft mode', () => { it('opens on the mode a hover action asked for', () => { setDocumentComposerDraft({ text: '', mode: 'note' }); diff --git a/src/documents/RunComposer.tsx b/src/documents/RunComposer.tsx index 31a6c57b0..509dba231 100644 --- a/src/documents/RunComposer.tsx +++ b/src/documents/RunComposer.tsx @@ -39,7 +39,7 @@ import type { DocumentModelChoice } from '../store/types'; import { ModelRows, type ChoiceSlot, type ModelSlot } from './ModelRows'; import { buildInteractivePrompt } from './interactive-prompt'; import { sendToDocumentAgent } from './agent-task'; -import { SendIcon, SparkleIcon } from '../components/icons'; +import { CloseIcon, SendIcon, SparkleIcon } from '../components/icons'; interface RunComposerProps { /** The picked passage; null means the whole document. */ @@ -484,9 +484,7 @@ export function RunComposer(props: RunComposerProps) { title="Close · Esc" onClick={close} > - + From 6d1f188a9fcdbdbccd88002e71d1bf67e664626b Mon Sep 17 00:00:00 2001 From: truongthinh1412 <9260650+truongthinh1412@users.noreply.github.com> Date: Sat, 17 Oct 2026 22:54:37 +0700 Subject: [PATCH 3/5] refactor(ui): migrate task component icons to the shared module (tranche 3) --- src/components/AskCodeCard.tsx | 3 +- src/components/BranchPrefixField.tsx | 21 ++--------- src/components/ChangedFilesList.tsx | 26 ++----------- src/components/CommitNavBar.tsx | 9 ++--- src/components/EditProjectDialog.tsx | 20 +++++----- src/components/MergeReadinessPanel.tsx | 8 ++-- src/components/PlanViewerDialog.tsx | 6 +-- src/components/PromptInput.tsx | 11 +----- src/components/ScrollingDiffView.tsx | 5 +-- src/components/TaskBranchAdoptionBanner.tsx | 4 +- src/components/TaskBranchInfoBar.tsx | 42 +++++++-------------- src/components/TaskNotesBody.tsx | 11 +----- src/components/icons.tsx | 27 +++++++++++++ src/remote/TaskActionsDialog.tsx | 8 ++-- 14 files changed, 83 insertions(+), 118 deletions(-) diff --git a/src/components/AskCodeCard.tsx b/src/components/AskCodeCard.tsx index 066f0a7e3..2a09ce6c8 100644 --- a/src/components/AskCodeCard.tsx +++ b/src/components/AskCodeCard.tsx @@ -6,6 +6,7 @@ import { IPC } from '../../electron/ipc/channels'; import { askCodeEnvFile } from '../../electron/shared/ask-code-models'; import { store } from '../store/store'; import { warn as logWarn, errMessage } from '../lib/log'; +import { CloseIcon } from './icons'; interface AskCodeCardProps { requestId: string; @@ -132,7 +133,7 @@ export function AskCodeCard(props: AskCodeCardProps) { }} title="Dismiss" > - × + diff --git a/src/components/BranchPrefixField.tsx b/src/components/BranchPrefixField.tsx index fe897b31d..6d1570668 100644 --- a/src/components/BranchPrefixField.tsx +++ b/src/components/BranchPrefixField.tsx @@ -1,5 +1,6 @@ import { Show } from 'solid-js'; import { theme } from '../lib/theme'; +import { FolderIcon, GitBranchIcon } from './icons'; interface BranchPrefixFieldProps { branchPrefix: string; @@ -54,27 +55,11 @@ export function BranchPrefixField(props: BranchPrefixFieldProps) { }} > - - - + {props.branchPreview} - - - + {props.projectPath}/.worktrees/{props.branchPreview} diff --git a/src/components/ChangedFilesList.tsx b/src/components/ChangedFilesList.tsx index b05ac8543..6eb867833 100644 --- a/src/components/ChangedFilesList.tsx +++ b/src/components/ChangedFilesList.tsx @@ -8,6 +8,7 @@ import { getStatusColor } from '../lib/status-colors'; import { openFileInEditor } from '../lib/shell'; import { isMarkdownPath } from '../lib/canvas-tabs'; import { buildFileTree, flattenVisibleTree } from '../lib/file-tree'; +import { ExternalLinkIcon, PanelRightIcon, QuestionAltIcon } from './icons'; import { buildCoverageComparison, formatCoverageDelta, @@ -356,9 +357,7 @@ function OpenInEditorButton(props: { title="Open in editor" aria-label={`Open ${props.filePath} in editor`} > - - - + ); } @@ -403,14 +402,7 @@ function UnderstandFileButton(props: { }} aria-label={`Understand ${props.filePath}`} > - - - - } - > + }> @@ -443,17 +435,7 @@ function OpenMarkdownButton(props: { filePath: string; onOpenMarkdownClick?: () title="Open in canvas" aria-label={`Open ${props.filePath} in canvas`} > - - - - + ); } diff --git a/src/components/CommitNavBar.tsx b/src/components/CommitNavBar.tsx index b986206d1..0e5b83ab2 100644 --- a/src/components/CommitNavBar.tsx +++ b/src/components/CommitNavBar.tsx @@ -3,6 +3,7 @@ import { theme } from '../lib/theme'; import { sf } from '../lib/fontScale'; import { accentControlColors } from '../lib/controlStyle'; import type { CommitInfo } from '../ipc/types'; +import { ChevronLeftAltIcon, ChevronRightAltIcon } from './icons'; /** * Sentinel value used in place of a commit hash to mean "show only currently @@ -138,9 +139,7 @@ export function CommitNavBar(props: CommitNavBarProps) { 'flex-shrink': '0', }} > - - - + {/* Chevron Right */} @@ -167,9 +166,7 @@ export function CommitNavBar(props: CommitNavBarProps) { 'flex-shrink': '0', }} > - - - + {/* Uncommitted-only button */} diff --git a/src/components/EditProjectDialog.tsx b/src/components/EditProjectDialog.tsx index d73101f06..3b1adc7bc 100644 --- a/src/components/EditProjectDialog.tsx +++ b/src/components/EditProjectDialog.tsx @@ -22,7 +22,15 @@ import { SegmentedButtons } from './SegmentedButtons'; import { SuggestChecksButton } from './SuggestChecksButton'; import { keepCheckIds } from '../lib/check-suggestion'; import { ImportWorktreesDialog } from './ImportWorktreesDialog'; -import { CloseIcon, DownloadIcon, FolderIcon, LinkIcon, PlusIcon, TrashIcon } from './icons'; +import { + CloseIcon, + DownloadIcon, + FolderIcon, + GitBranchIcon, + LinkIcon, + PlusIcon, + TrashIcon, +} from './icons'; import { RemoveProjectConfirm } from './RemoveProjectConfirm'; import { updateProjectCoordination } from '../store/projects'; import { isDocumentProject } from '../store/projects'; @@ -399,15 +407,7 @@ export function EditProjectDialog(props: EditProjectDialogProps) { gap: '6px', }} > - - - + {sanitizeBranchPrefix(branchPrefix())}/{toBranchName('example-branch-name')} diff --git a/src/components/MergeReadinessPanel.tsx b/src/components/MergeReadinessPanel.tsx index f7817224c..b18ccee26 100644 --- a/src/components/MergeReadinessPanel.tsx +++ b/src/components/MergeReadinessPanel.tsx @@ -1,6 +1,8 @@ import { For, Show } from 'solid-js'; +import type { JSX } from 'solid-js'; import { theme } from '../lib/theme'; import { sf } from '../lib/fontScale'; +import { CheckIcon, CloseIcon } from './icons'; import type { MergeReadinessCheck, MergeReadinessCheckStatus } from './merge-readiness'; function checkHelp(label: string): string | undefined { @@ -32,9 +34,9 @@ function statusColor(status: MergeReadinessCheckStatus): string { return theme.fgMuted; } -function statusSymbol(status: MergeReadinessCheckStatus): string { - if (status === 'pass') return '✓'; - if (status === 'blocked') return '×'; +function statusSymbol(status: MergeReadinessCheckStatus): JSX.Element { + if (status === 'pass') return ; + if (status === 'blocked') return ; if (status === 'warning') return '!'; if (status === 'checking') return '…'; return '—'; diff --git a/src/components/PlanViewerDialog.tsx b/src/components/PlanViewerDialog.tsx index 9c7a081cd..b730ff3c7 100644 --- a/src/components/PlanViewerDialog.tsx +++ b/src/components/PlanViewerDialog.tsx @@ -7,7 +7,7 @@ import { ReviewCommentsButton, ReviewSidebarPanel } from './ReviewSidebarPanel'; import { ReviewCommentCard } from './ReviewCommentCard'; import { InlineInput } from './InlineInput'; import { AskCodeCard } from './AskCodeCard'; -import { CloseIcon } from './icons'; +import { CloseIcon, ExternalLinkIcon } from './icons'; import { createHighlightedMarkdown } from '../lib/marked-shiki'; import { renderMermaidIn } from '../lib/mermaid'; import { @@ -349,9 +349,7 @@ function PlanViewerContent(props: PlanViewerContentProps) { }} title="Open in editor" > - - - + diff --git a/src/components/PromptInput.tsx b/src/components/PromptInput.tsx index f7a845687..10c533cd4 100644 --- a/src/components/PromptInput.tsx +++ b/src/components/PromptInput.tsx @@ -47,6 +47,7 @@ import { import { taskUsesAgentChat } from '../store/agent-chat'; import { isLandedTaskState } from '../store/landing'; import { processAutoFireTick } from './autofire-tick'; +import { ArrowUpIcon } from './icons'; import { resolveAutoSendVerifyOutcome, shouldAckInitialPromptDelivery, @@ -1046,15 +1047,7 @@ export function PromptInput(props: PromptInputProps) { }} title="Send prompt" > - - - + - - - + diff --git a/src/components/TaskBranchAdoptionBanner.tsx b/src/components/TaskBranchAdoptionBanner.tsx index 2b5aa43a0..c0edfbe93 100644 --- a/src/components/TaskBranchAdoptionBanner.tsx +++ b/src/components/TaskBranchAdoptionBanner.tsx @@ -2,7 +2,7 @@ import { Show, type JSX } from 'solid-js'; import { undoBranchAdoption, dismissBranchAdoptionNotice } from '../store/store'; import { theme } from '../lib/theme'; import type { Task } from '../store/types'; -import { UndoIcon } from './icons'; +import { CloseIcon, UndoIcon } from './icons'; const bannerBtnStyle: JSX.CSSProperties = { background: 'transparent', @@ -59,7 +59,7 @@ export function TaskBranchAdoptionBanner(props: { task: Task }) { style={{ ...bannerBtnStyle, border: 'none', padding: '1px 4px', 'font-size': '13px' }} onClick={() => dismissBranchAdoptionNotice(props.task.id)} > - × + )} diff --git a/src/components/TaskBranchInfoBar.tsx b/src/components/TaskBranchInfoBar.tsx index bf0d647fd..de0ed36e4 100644 --- a/src/components/TaskBranchInfoBar.tsx +++ b/src/components/TaskBranchInfoBar.tsx @@ -18,7 +18,17 @@ import { parseGitHubUrl } from '../lib/github-url'; import { abbreviateHomePath } from '../lib/path'; import { projectInitials } from '../lib/project-initials'; import type { Task } from '../store/types'; -import { AlertIcon, CheckIcon, GitMergeIcon, PencilIcon, PersonIcon, ToolsIcon } from './icons'; +import { + AlertIcon, + CheckIcon, + FolderIcon, + GitBranchIcon, + GitMergeIcon, + GitHubMarkIcon, + PencilIcon, + PersonIcon, + ToolsIcon, +} from './icons'; const infoBarBtnStyle: JSX.CSSProperties = { 'align-self': 'stretch', @@ -462,15 +472,7 @@ export function TaskBranchInfoBar(props: TaskBranchInfoBarProps) { > Source
- - - + {githubLabel(url())} {compactSourceLabel(url())} @@ -484,15 +486,7 @@ export function TaskBranchInfoBar(props: TaskBranchInfoBarProps) { onClick={handleOpenInEditor} style={{ ...infoBarBtnStyle, 'margin-right': '12px' }} > - - - + {props.task.branchName} @@ -534,15 +528,7 @@ export function TaskBranchInfoBar(props: TaskBranchInfoBarProps) { onClick={handleOpenInEditor} style={{ ...infoBarBtnStyle, opacity: 0.6, 'min-width': '0', overflow: 'hidden' }} > - - - + - - - + diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 1272e2d2b..851372dca 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1038,3 +1038,30 @@ export function ArrowRightThinIcon(props: IconProps): JSX.Element { ); } + +/** Rounded send-arrow for the prompt composer's submit button. */ +export function ArrowUpIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Rounded send-arrow for the task-notes composer's submit button. */ +export function ArrowDownIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Simplified question mark: the file Understand affordance; the octicon-style question differs. */ +export function QuestionAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} diff --git a/src/remote/TaskActionsDialog.tsx b/src/remote/TaskActionsDialog.tsx index ffcb9b7bf..8994f5051 100644 --- a/src/remote/TaskActionsDialog.tsx +++ b/src/remote/TaskActionsDialog.tsx @@ -1,4 +1,6 @@ import { createSignal, createResource, For, Show } from 'solid-js'; +import type { JSX } from 'solid-js'; +import { CheckIcon, CloseIcon } from '../components/icons'; import { fetchMergeReadiness, mergeTask as mergeTaskRequest, @@ -27,10 +29,10 @@ const OVERALL_COPY: Record = { checking: { title: 'Checking', detail: 'Waiting for merge status.' }, }; -const SYMBOL: Record = { - pass: '✓', +const SYMBOL: Record = { + pass: , warning: '!', - blocked: '×', + blocked: , checking: '…', neutral: '—', }; From 4dcc98f864f26e428d603353e8a52514659f6225 Mon Sep 17 00:00:00 2001 From: truongthinh1412 <9260650+truongthinh1412@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:01:35 +0700 Subject: [PATCH 4/5] refactor(ui): migrate remaining upstream call sites to the shared icon module (tranche 4) --- src/components/PromptHistory.tsx | 15 +- src/components/TaskProjectFilter.tsx | 5 +- src/components/TaskShellSection.tsx | 18 +- src/components/chat/Composer.tsx | 22 +-- src/components/icons.tsx | 173 +++++++++++++++++++ src/components/understanding/TourCard.tsx | 13 +- src/components/understanding/TourOutline.tsx | 7 +- src/documents/FilesPopover.tsx | 13 +- src/graph/GraphCanvas.tsx | 66 ++----- src/graph/KindMark.tsx | 19 +- 10 files changed, 202 insertions(+), 149 deletions(-) diff --git a/src/components/PromptHistory.tsx b/src/components/PromptHistory.tsx index a5658dadb..66fd8bce5 100644 --- a/src/components/PromptHistory.tsx +++ b/src/components/PromptHistory.tsx @@ -4,6 +4,7 @@ import { createAnchorEffect, placeBelow } from '../lib/floating'; import type { BelowAnchor } from '../lib/floating'; import type { Task } from '../store/types'; import { promptHistoryOf } from '../lib/prompt-history'; +import { HistoryAltIcon } from './icons'; export function PromptHistory(props: { task: Task; emptyLabel: string }) { const id = createUniqueId(); @@ -79,19 +80,7 @@ export function PromptHistory(props: { task: Task; emptyLabel: string }) { } }} > - + {props.task.lastPrompt || props.emptyLabel} diff --git a/src/components/TaskProjectFilter.tsx b/src/components/TaskProjectFilter.tsx index 0470982b4..967e8a14d 100644 --- a/src/components/TaskProjectFilter.tsx +++ b/src/components/TaskProjectFilter.tsx @@ -6,6 +6,7 @@ import { store } from '../store/core'; import { setTaskProjectFilter } from '../store/navigation'; import { taskProjectFilter } from '../store/task-project-filter'; import { ProjectSwatch } from './ProjectSwatch'; +import { CaretDownIcon } from './icons'; import './TaskProjectFilter.css'; export function TaskProjectFilter() { @@ -61,9 +62,7 @@ export function TaskProjectFilter() { {(project) => } {selected()?.name ?? 'All projects'} - + {(bookmark, i) => ( diff --git a/src/components/chat/Composer.tsx b/src/components/chat/Composer.tsx index f20582b86..c5057867b 100644 --- a/src/components/chat/Composer.tsx +++ b/src/components/chat/Composer.tsx @@ -11,7 +11,7 @@ import { shellCommand, type Composer as ComposerState } from './composer-state'; import { ModelPicker, PermissionPicker } from './ModelPicker'; import { Progress } from './Progress'; import { RequestCard, type RespondToRequest } from './RequestCard'; -import { PencilIcon, SendIcon, SyncIcon } from '../icons'; +import { ArrowUpAltIcon, PencilIcon, SendIcon, StopSmallIcon, SyncIcon } from '../icons'; export interface ChatHistoryEntry { title: string; @@ -345,15 +345,7 @@ export function Composer(props: ComposerProps) { } onClick={() => void c.send()} > - + @@ -364,15 +356,7 @@ export function Composer(props: ComposerProps) { title={stopLabel()} onClick={() => void c.stop()} > - + diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 851372dca..d808fc149 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1065,3 +1065,176 @@ export function QuestionAltIcon(props: IconProps): JSX.Element { ); } + +/** Stroke-drawn padlock: a mind-map node the user has edited. */ +export function LockIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Page with ruled lines: the notes indicator on a mind-map node. */ +export function NotePageIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Four corner brackets: frame the whole mind map (Fit map). */ +export function FrameCornersIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Circle with cross ticks: locate the current work on the mind map. */ +export function CrosshairIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** Tall ruled page marking an observation node; pass height to keep the 8:7 ratio. */ +export function DocumentLinesIcon(props: IconProps & { height?: number | string }): JSX.Element { + const width = () => props.size ?? 16; + const height = () => props.height ?? width(); + + return ( + + {props.title ? {props.title} : null} + + + ); +} + +/** Tiny 8x8 chevron for the compact tour outline rows. */ +export function ChevronRightTinyIcon(props: IconProps): JSX.Element { + return ( + + {props.title ? {props.title} : null} + + + ); +} + +/** Solid triangle caret: the project filter's dropdown affordance. */ +export function CaretDownIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Sharp stroke chevron under the workspace's file path; see also ChevronDownIcon above. */ +export function ChevronDownThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Stroke-drawn history: the prompt-history trigger; HistoryIcon above is the filled one. */ +export function HistoryAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Compact prompt-style terminal for the shell toolbar; see also TerminalIcon above. */ +export function TerminalSmallIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +/** The chat composer's send arrow; ArrowUpIcon is the prompt bar's compact variant. */ +export function ArrowUpAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Compact stop square: the chat composer's stop button. */ +export function StopSmallIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function FileIcon(props: IconProps & { height?: number | string }): JSX.Element { + const width = () => props.size ?? 16; + const height = () => props.height ?? props.size ?? 16; + + return ( + + {props.title ? {props.title} : null} + + + ); +} diff --git a/src/components/understanding/TourCard.tsx b/src/components/understanding/TourCard.tsx index efb0a5155..14850f169 100644 --- a/src/components/understanding/TourCard.tsx +++ b/src/components/understanding/TourCard.tsx @@ -4,6 +4,7 @@ import { createHighlightedMarkdown } from '../../lib/marked-shiki'; import { renderMermaidIn } from '../../lib/mermaid'; import { theme } from '../../lib/theme'; import type { TourCard as TourCardData, TourRef, TourTone } from '../../lib/understanding-tour'; +import { FileIcon } from '../icons'; /** Tone colour per card; `neutral` and `mechanical` carry no accent. */ const TONE_COLOR: Record = { @@ -25,17 +26,7 @@ function refLabel(ref: TourRef): string { /** Marks a ref chip as a place in the codebase rather than a word. */ function FileGlyph() { - return ( - - ); + return ; } /** diff --git a/src/components/understanding/TourOutline.tsx b/src/components/understanding/TourOutline.tsx index 0a9d66676..d42e55e9c 100644 --- a/src/components/understanding/TourOutline.tsx +++ b/src/components/understanding/TourOutline.tsx @@ -1,12 +1,9 @@ import { For, Show, createEffect, createSignal } from 'solid-js'; import { nodeTrail, type TourNode } from '../../lib/tour-tree'; +import { ChevronRightTinyIcon } from '../icons'; function Chevron() { - return ( - - ); + return ; } /** diff --git a/src/documents/FilesPopover.tsx b/src/documents/FilesPopover.tsx index 66e4a3a21..e681143a6 100644 --- a/src/documents/FilesPopover.tsx +++ b/src/documents/FilesPopover.tsx @@ -1,5 +1,6 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup } from 'solid-js'; import { FileTreePanel } from './FileTreePanel'; +import { ChevronDownThinIcon } from '../components/icons'; import { openDocumentFile } from './store'; import { setFilesOpen, workspaceUi } from './workspace-ui'; @@ -108,17 +109,7 @@ export function FilesPopover(props: FilesPopoverProps) { onClick={() => setFilesOpen(!open())} > {props.openPath} - +