From 61d7ad511a071df0146fd46f5bea6384089214a0 Mon Sep 17 00:00:00 2001 From: truongthinh1412 <9260650+truongthinh1412@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:19:47 +0700 Subject: [PATCH] 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 | 18 +- src/components/MarkdownViewerDialog.tsx | 3 +- src/components/NewTaskPanel.tsx | 22 +- src/components/Sidebar.tsx | 10 +- src/components/TerminalSelectionMenu.tsx | 3 +- src/components/file-link-menu.tsx | 3 +- src/components/icons.tsx | 200 ++++++++++++++++++ src/components/project-menu.tsx | 12 +- 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 +- 21 files changed, 358 insertions(+), 316 deletions(-) delete mode 100644 src/documents/DocumentIcon.tsx diff --git a/src/arena/ArenaOverlay.tsx b/src/arena/ArenaOverlay.tsx index f8553b5cf..964a8d05f 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'; @@ -54,19 +55,7 @@ export function ArenaOverlay(props: ArenaOverlayProps) { >
- - - - + AI Arena
diff --git a/src/arena/ConfigScreen.tsx b/src/arena/ConfigScreen.tsx index 06f6f254c..b47a192a3 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'; @@ -220,20 +220,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..37d506920 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, + BoxIcon, + 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 ea54b7a44..1af72f88a 100644 --- a/src/components/CanvasTabStrip.tsx +++ b/src/components/CanvasTabStrip.tsx @@ -4,13 +4,19 @@ 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 { focusFirstMenuItem, handleMenuKeyDown } from '../lib/menu-keys'; -import { DocumentIcon } from '../documents/DocumentIcon'; interface CanvasTabStripProps { tabs: CanvasTab[]; diff --git a/src/components/ChangeTourButton.tsx b/src/components/ChangeTourButton.tsx index 9269502bf..45ac6ad52 100644 --- a/src/components/ChangeTourButton.tsx +++ b/src/components/ChangeTourButton.tsx @@ -11,7 +11,7 @@ import { } from '../lib/floating'; import { store } from '../store/store'; import { activeFeatureHint } from '../store/onboarding'; -import { CloseIcon, TourIcon } from './icons'; +import { ArrowRightThinIcon, CloseIcon, TourIcon } from './icons'; import { askCodeLabel } from './understanding/ask-code-label'; import { TourModelMenu } from './understanding/TourModelMenu'; @@ -195,21 +195,11 @@ export function ChangeTourButton(props: { {/* Hidden, not removed, so the label keeps its width while the tour generates. */} - + />
{props.children} diff --git a/src/components/MarkdownViewerDialog.tsx b/src/components/MarkdownViewerDialog.tsx index 76975f8f4..5e5964461 100644 --- a/src/components/MarkdownViewerDialog.tsx +++ b/src/components/MarkdownViewerDialog.tsx @@ -1,7 +1,8 @@ +import { DocumentIcon } from './icons'; import { Show, createSignal, onCleanup } from 'solid-js'; import type { JSX } from 'solid-js'; import { IPC } from '../../electron/ipc/channels'; -import { DocumentIcon } from '../documents/DocumentIcon'; + import { editAsDocument } from '../documents/store'; import { isEditableDocumentPath } from '../lib/document-path'; import { sf } from '../lib/fontScale'; diff --git a/src/components/NewTaskPanel.tsx b/src/components/NewTaskPanel.tsx index 1e1063acf..55716766b 100644 --- a/src/components/NewTaskPanel.tsx +++ b/src/components/NewTaskPanel.tsx @@ -24,7 +24,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 { withViewTransition } from '../lib/viewTransition'; @@ -1722,16 +1730,10 @@ export function NewTaskPanel(props: NewTaskPanelProps) { cursor: 'pointer', }} > - + /> Advanced options diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index fbcb1a49d..3e559c2bf 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 { BotIcon, ChevronDownAltIcon, ClockIcon, FolderIcon, PlusLargeIcon } from './icons'; +import { + BotIcon, + ChevronDownAltIcon, + ClockIcon, + DocumentIcon, + FolderIcon, + PlusLargeIcon, +} from './icons'; import { createTaskContextMenu } from './task-context-menu'; import { errMessage } from '../lib/log'; import { @@ -45,7 +52,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/TerminalSelectionMenu.tsx b/src/components/TerminalSelectionMenu.tsx index 644e5ef5d..14f96bbcb 100644 --- a/src/components/TerminalSelectionMenu.tsx +++ b/src/components/TerminalSelectionMenu.tsx @@ -1,8 +1,7 @@ import { createSignal, For, onMount, Show, untrack } from 'solid-js'; import type { JSX } 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'; /** An entry of a clicked link's menu; the menu closes after `run`. */ export interface TerminalLinkAction { diff --git a/src/components/file-link-menu.tsx b/src/components/file-link-menu.tsx index be4427bf1..c7a590898 100644 --- a/src/components/file-link-menu.tsx +++ b/src/components/file-link-menu.tsx @@ -1,7 +1,8 @@ +import { DocumentIcon } from './icons'; import { IPC } from '../../electron/ipc/channels'; import { isExecutablePath } from '../../electron/shared/executable-path'; import { editAsDocument } from '../documents/store'; -import { DocumentIcon } from '../documents/DocumentIcon'; + import { isMarkdownPath } from '../lib/canvas-tabs'; import { invoke } from '../lib/ipc'; import { errMessage } from '../lib/log'; diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 1f47c1efb..ad3d81e30 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -944,3 +944,203 @@ export function ChevronLeftAltIcon(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 ( + + + + + + + ); +} + +/** Box with a center seam: the arena project's container; ShieldIcon above is the filled badge. */ +export function BoxIcon(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 ( + + + + + ); +} + +/** Stroke-drawn arrow: the tour button's go affordance once it is idle. */ +export function ArrowRightThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +/** Rounded send-arrow for the prompt composer's submit button. */ diff --git a/src/components/project-menu.tsx b/src/components/project-menu.tsx index cf6cfe31e..b2af7e34c 100644 --- a/src/components/project-menu.tsx +++ b/src/components/project-menu.tsx @@ -8,9 +8,15 @@ import { isDocumentProject } from '../store/projects'; import { isProjectMissing, showNotification, startProjectTask, store } from '../store/store'; import type { Project } from '../store/types'; import { ContextMenu, type MoreMenuItem } from './chat/MoreMenu'; -import { DocumentIcon } from '../documents/DocumentIcon'; -import { FolderIcon, GitHubIcon, PencilIcon, PlanIcon, PlusIcon, TrashIcon } from './icons'; - +import { + DocumentIcon, + FolderIcon, + GitHubIcon, + PencilIcon, + PlanIcon, + PlusIcon, + TrashIcon, +} from './icons'; /** Dialogs the menu opens; whoever renders the menu owns them. */ export interface ProjectMenuDialogs { editSettings: (project: Project) => void; 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} > - +