diff --git a/src/App.tsx b/src/App.tsx index 57f456b5..5caf6139 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,6 +4,7 @@ import '@fontsource/inter/500.css'; import '@fontsource/inter/600.css'; import '@fontsource/sora/400.css'; import '@fontsource/sora/500.css'; +import { ChevronRightIcon, GitHubIcon } from './components/icons'; import '@fontsource/sora/600.css'; import '@fontsource/sora/700.css'; import '@fontsource/space-grotesk/500.css'; @@ -125,15 +126,7 @@ function DropOverlay() { 'backdrop-filter': 'blur(4px)', }} > - - - + - - - +
diff --git a/src/arena/ArenaOverlay.tsx b/src/arena/ArenaOverlay.tsx index a5f49d23..9a391a09 100644 --- a/src/arena/ArenaOverlay.tsx +++ b/src/arena/ArenaOverlay.tsx @@ -4,6 +4,7 @@ import './arena-countdown.css'; import './arena-battle.css'; import './arena-results.css'; import './arena-history.css'; +import { ScrambleIcon } from '../components/icons'; import { Show, onMount } from 'solid-js'; import { arenaStore } from './store'; import { loadArenaPresets, loadArenaHistory } from './persistence'; @@ -31,19 +32,7 @@ export function ArenaOverlay(props: ArenaOverlayProps) {
- - - - + AI Arena
diff --git a/src/arena/ConfigScreen.tsx b/src/arena/ConfigScreen.tsx index 7f00be2b..800a3cb1 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 } from '../components/icons'; +import { CloseIcon, ScrambleIcon } from '../components/icons'; import { MAX_COMPETITORS, MIN_COMPETITORS } from './store'; import type { BattleCompetitor } from './types'; @@ -218,20 +218,7 @@ export function ConfigScreen() { {/* Actions */}
diff --git a/src/arena/HistoryScreen.tsx b/src/arena/HistoryScreen.tsx index f0bac0db..3e18d699 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, TrashIcon } 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 b6b14cc3..650c2e44 100644 --- a/src/arena/ResultsScreen.tsx +++ b/src/arena/ResultsScreen.tsx @@ -1,5 +1,15 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import { ChangedFilesList } from '../components/ChangedFilesList'; +import { + ChevronLeftThinIcon, + ClockIcon, + CompareIcon, + MergeIcon, + PlusThinIcon, + ShieldIcon, + StarIcon, + SyncIcon, +} from '../components/icons'; import { DiffViewerDialog } from '../components/DiffViewerDialog'; import { CommitDialog } from './CommitDialog'; import { createMergeWorkflow } from './merge'; @@ -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/AddProjectMenu.tsx b/src/components/AddProjectMenu.tsx index 65bb6647..53cf9433 100644 --- a/src/components/AddProjectMenu.tsx +++ b/src/components/AddProjectMenu.tsx @@ -2,7 +2,7 @@ import { onCleanup, onMount } from 'solid-js'; import { Portal } from 'solid-js/web'; import { theme } from '../lib/theme'; import { sf } from '../lib/fontScale'; -import { DocumentIcon } from '../documents/DocumentIcon'; +import { DocumentIcon, FolderAltIcon } from '../components/icons'; export type ProjectKindChoice = 'code' | 'document'; @@ -111,9 +111,7 @@ export function AddProjectMenu(props: AddProjectMenuProps) { {row( 'code', () => ( - - - + ), 'Code project…', 'A git repo that holds tasks', diff --git a/src/components/AskCodeCard.tsx b/src/components/AskCodeCard.tsx index 6b74ff9b..fa594016 100644 --- a/src/components/AskCodeCard.tsx +++ b/src/components/AskCodeCard.tsx @@ -2,6 +2,7 @@ import { createSignal, onCleanup, onMount, Show } from 'solid-js'; import { theme } from '../lib/theme'; import { sf } from '../lib/fontScale'; import { Channel, invoke } from '../lib/ipc'; +import { CloseIcon } from './icons'; import { IPC } from '../../electron/ipc/channels'; import { store } from '../store/store'; import { warn as logWarn, errMessage } from '../lib/log'; @@ -130,7 +131,7 @@ export function AskCodeCard(props: AskCodeCardProps) { }} title="Dismiss" > - × +
diff --git a/src/components/BranchPrefixField.tsx b/src/components/BranchPrefixField.tsx index fe897b31..6d157066 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/ChangeTourButton.tsx b/src/components/ChangeTourButton.tsx index e43a21c4..dd9964da 100644 --- a/src/components/ChangeTourButton.tsx +++ b/src/components/ChangeTourButton.tsx @@ -3,6 +3,7 @@ import { Portal } from 'solid-js/web'; import type { ChangeTourController } from '../lib/create-change-tour'; import type { ChangeTourScope } from '../lib/change-tour'; import { theme } from '../lib/theme'; +import { CheckIcon } from '../components/icons'; import { sf } from '../lib/fontScale'; import { createAnchorEffect, @@ -139,7 +140,7 @@ export function ChangeTourButton(props: { - ✓ + diff --git a/src/components/ChangedFilesList.tsx b/src/components/ChangedFilesList.tsx index c951db0e..c7896410 100644 --- a/src/components/ChangedFilesList.tsx +++ b/src/components/ChangedFilesList.tsx @@ -4,6 +4,7 @@ import { IPC } from '../../electron/ipc/channels'; import { theme } from '../lib/theme'; import { sf } from '../lib/fontScale'; import { getStatusColor } from '../lib/status-colors'; +import { ExternalLinkIcon } from './icons'; import { openFileInEditor } from '../lib/shell'; import { buildFileTree, flattenVisibleTree } from '../lib/file-tree'; import { @@ -346,9 +347,7 @@ function OpenInEditorButton(props: { title="Open in editor" aria-label={`Open ${props.filePath} in editor`} > - - - + ); } diff --git a/src/components/CommitNavBar.tsx b/src/components/CommitNavBar.tsx index b986206d..063bb5ec 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 { ChevronLeftIcon, ChevronRightIcon } 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/ConnectPhoneModal.tsx b/src/components/ConnectPhoneModal.tsx index 9d8276c5..a7d736d3 100644 --- a/src/components/ConnectPhoneModal.tsx +++ b/src/components/ConnectPhoneModal.tsx @@ -11,6 +11,7 @@ import { generatePairingPin, } from '../store/remote'; import { theme } from '../lib/theme'; +import { CheckLargeIcon } from './icons'; import type { RemoteAccess } from '../store/types'; type NetworkMode = 'wifi' | 'tailscale'; @@ -446,18 +447,7 @@ export function ConnectPhoneModal(props: ConnectPhoneModalProps) { gap: '8px', }} > - - - + {store.remoteAccess.connectedClients} client(s) connected diff --git a/src/components/EditProjectDialog.tsx b/src/components/EditProjectDialog.tsx index 1b21c914..352d0f71 100644 --- a/src/components/EditProjectDialog.tsx +++ b/src/components/EditProjectDialog.tsx @@ -6,7 +6,7 @@ import { theme, sectionLabelStyle } from '../lib/theme'; import type { Project, TerminalBookmark, GitIsolationMode } from '../store/types'; import { SegmentedButtons } from './SegmentedButtons'; import { ImportWorktreesDialog } from './ImportWorktreesDialog'; -import { CloseIcon } from './icons'; +import { CloseIcon, GitBranchIcon } from './icons'; import { RemoveProjectConfirm } from './RemoveProjectConfirm'; import { isDocumentProject } from '../store/projects'; @@ -283,15 +283,7 @@ export function EditProjectDialog(props: EditProjectDialogProps) { gap: '6px', }} > - - - + {sanitizeBranchPrefix(branchPrefix())}/{toBranchName('example-branch-name')} diff --git a/src/components/MergeDialog.tsx b/src/components/MergeDialog.tsx index 74162811..37ca17c0 100644 --- a/src/components/MergeDialog.tsx +++ b/src/components/MergeDialog.tsx @@ -11,6 +11,7 @@ import { updateTaskBranch, } from '../store/store'; import { ConfirmDialog } from './ConfirmDialog'; +import { CircleIcon } from './icons'; import { ChangedFilesList } from './ChangedFilesList'; import { MergeReadinessPanel } from './MergeReadinessPanel'; import { VerificationPanel } from './VerificationPanel'; @@ -424,21 +425,7 @@ export function MergeDialog(props: MergeDialogProps) { color: theme.fg, }} > - - - + {commit.hash} diff --git a/src/components/MergeReadinessPanel.tsx b/src/components/MergeReadinessPanel.tsx index c4d6ad9b..a354e398 100644 --- a/src/components/MergeReadinessPanel.tsx +++ b/src/components/MergeReadinessPanel.tsx @@ -1,4 +1,6 @@ import { For } from 'solid-js'; +import type { JSX } from 'solid-js'; +import { CheckIcon, CloseIcon } from './icons'; import { theme } from '../lib/theme'; import { sf } from '../lib/fontScale'; import type { MergeReadiness, MergeReadinessCheckStatus } from './merge-readiness'; @@ -36,9 +38,9 @@ function statusColor(status: MergeReadinessCheckStatus | MergeReadiness['overall 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/NewTaskPanel.tsx b/src/components/NewTaskPanel.tsx index a753d7f4..9e9a1da3 100644 --- a/src/components/NewTaskPanel.tsx +++ b/src/components/NewTaskPanel.tsx @@ -14,6 +14,7 @@ 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 { ChevronRightThinIcon } from '../components/icons'; import { FolderIcon, GitBranchIcon } from './icons'; import { ConfirmDialog } from './ConfirmDialog'; import { errMessage } from '../lib/log'; @@ -1229,16 +1230,10 @@ export function NewTaskPanel(props: NewTaskPanelProps) { cursor: 'pointer', }} > - + /> Advanced options diff --git a/src/components/NewTaskPlaceholder.tsx b/src/components/NewTaskPlaceholder.tsx index efe0ef2a..94021f95 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/PlanViewerDialog.tsx b/src/components/PlanViewerDialog.tsx index 4d82476d..1876d017 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 { @@ -325,9 +325,7 @@ function PlanViewerContent(props: PlanViewerContentProps) { }} title="Open in editor" > - - - + diff --git a/src/components/PromptInput.tsx b/src/components/PromptInput.tsx index 00c9069f..6e4f9ed1 100644 --- a/src/components/PromptInput.tsx +++ b/src/components/PromptInput.tsx @@ -1,5 +1,6 @@ import { createSignal, createEffect, on, Show, onMount, onCleanup, untrack, batch } from 'solid-js'; import { fireAndForget, invoke } from '../lib/ipc'; +import { ArrowUpIcon } from '../components/icons'; import { IPC } from '../../electron/ipc/channels'; import { store, @@ -951,15 +952,7 @@ export function PromptInput(props: PromptInputProps) { }} title="Send prompt" > - - - + - - - + diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e34e515d..248463b7 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,6 +1,17 @@ import { createSignal, createEffect, createMemo, onMount, onCleanup, For, Show } from 'solid-js'; import type { JSX } from 'solid-js'; import { errMessage } from '../lib/log'; +import { + BotIcon, + ChevronDownIcon, + ChevronLeftIcon, + CloseIcon, + DocumentIcon, + FolderIcon, + GearIcon, + LogoIcon, + PlusLargeIcon, +} from './icons'; import { store, pickAndAddProject, @@ -43,7 +54,6 @@ import { EditProjectDialog } from './EditProjectDialog'; import { NewDocumentProjectDialog } from '../documents/NewDocumentProjectDialog'; import { openDocumentWorkspace } from '../documents/store'; import { codeProjects, isDocumentProject } from '../store/projects'; -import { DocumentIcon } from '../documents/DocumentIcon'; import { AddProjectMenu } from './AddProjectMenu'; import { ImportWorktreesDialog } from './ImportWorktreesDialog'; import { SidebarFooter } from './SidebarFooter'; @@ -116,17 +126,7 @@ function createOffscreenAttentionState(taskId: () => string) { /** Small bot/coordinator icon (16x16 SVG). */ function CoordinatorIcon() { - return ( - - - - ); + return ; } function DirectBranchBadge(props: { branchName: string }) { @@ -203,20 +203,14 @@ export function ProjectTaskGroupToggle(props: { gap: '5px', }} > - + /> - - - } + icon={} onClick={() => setSidebarNeedsInputFirst(false)} title="Stop pinning tasks that need input (re-enable in Settings)" size="sm" @@ -726,20 +716,7 @@ export function Sidebar() { style={{ display: 'flex', 'align-items': 'center', 'justify-content': 'space-between' }} >
- - - - - - + - - - } + icon={} onClick={() => toggleSettingsDialog(true)} title={`Settings (${mod}+,)`} /> - - - } + icon={} onClick={() => toggleSidebar()} title={`Collapse sidebar (${mod}+B)`} /> @@ -813,20 +782,14 @@ export function Sidebar() { color: theme.fgMuted, }} > - + /> - - - } + icon={} onClick={(e) => handleAddProject(e.currentTarget)} title="Add project" size="sm" @@ -993,15 +952,7 @@ export function Sidebar() { width: '100%', }} > - + Link Project } @@ -1025,9 +976,7 @@ export function Sidebar() { width: '100%', }} > - - - + New Task diff --git a/src/components/SidebarFooter.tsx b/src/components/SidebarFooter.tsx index 03c18f56..79ffd7e1 100644 --- a/src/components/SidebarFooter.tsx +++ b/src/components/SidebarFooter.tsx @@ -10,6 +10,7 @@ import { stopMCPStatusPolling, } from '../store/store'; import { theme } from '../lib/theme'; +import { PhoneIcon, ScrambleIcon } from './icons'; import { sf } from '../lib/fontScale'; import { alt, mod } from '../lib/platform'; @@ -101,20 +102,7 @@ export function SidebarFooter(props: { onConnectPhone: () => void }) { } style={footerButtonStyle(phoneConnected())} > - + {phoneConnected() ? 'Phone connected' : 'Phone'}
diff --git a/src/components/TaskAITerminal.tsx b/src/components/TaskAITerminal.tsx index 5a337a21..800b6faa 100644 --- a/src/components/TaskAITerminal.tsx +++ b/src/components/TaskAITerminal.tsx @@ -25,7 +25,7 @@ import { warn as logWarn } from '../lib/log'; import { InfoBar } from './InfoBar'; import { TerminalView } from './TerminalView'; import { Dialog } from './Dialog'; -import { CloseIcon } from './icons'; +import { CloseIcon, ColumnsIcon, ExternalLinkIcon, PlusSmallIcon, TerminalIcon } from './icons'; import { theme } from '../lib/theme'; import { sf } from '../lib/fontScale'; import { invoke } from '../lib/ipc'; @@ -381,31 +381,11 @@ export function TaskAITerminal(props: TaskAITerminalProps) { when={tabsMode()} fallback={ /* Currently side-by-side → click switches to tabs (one panel). */ - - - - + } > {/* Currently tabbed → click switches to side-by-side columns. */} - - - - +
@@ -510,9 +490,7 @@ function AddAgentMenu(props: { taskId: string }) { padding: '0', }} > - - - +
- - - +
)} diff --git a/src/components/TaskBranchInfoBar.tsx b/src/components/TaskBranchInfoBar.tsx index f6030169..5759b12a 100644 --- a/src/components/TaskBranchInfoBar.tsx +++ b/src/components/TaskBranchInfoBar.tsx @@ -17,7 +17,15 @@ 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, PencilIcon, PersonIcon } from './icons'; +import { + AlertIcon, + CheckIcon, + FolderIcon, + GitHubIcon, + GitBranchIcon, + PencilIcon, + PersonIcon, +} from './icons'; const infoBarBtnStyle: JSX.CSSProperties = { 'align-self': 'stretch', @@ -305,15 +313,7 @@ export function TaskBranchInfoBar(props: TaskBranchInfoBarProps) { > Source
- - - + {githubLabel(url())} {compactSourceLabel(url())} @@ -327,15 +327,7 @@ export function TaskBranchInfoBar(props: TaskBranchInfoBarProps) { onClick={handleOpenInEditor} style={{ ...infoBarBtnStyle, 'margin-right': '12px' }} > - - - + {props.task.branchName} @@ -377,15 +369,7 @@ export function TaskBranchInfoBar(props: TaskBranchInfoBarProps) { onClick={handleOpenInEditor} style={{ ...infoBarBtnStyle, opacity: 0.6, 'min-width': '0', overflow: 'hidden' }} > - - - + - - - + diff --git a/src/components/UpdateButton.tsx b/src/components/UpdateButton.tsx index 673e0799..b5b4d810 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 { DownloadIcon, 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 db3e7a0e..c5d3d3e9 100644 --- a/src/components/WindowTitleBar.tsx +++ b/src/components/WindowTitleBar.tsx @@ -1,6 +1,7 @@ import { createSignal, onCleanup, onMount } from 'solid-js'; 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); @@ -85,21 +86,15 @@ export function WindowTitleBar() { class="window-drag-region" onDblClick={() => void handleToggleMaximize()} > - + style={{ + color: '#ffffff', + 'stroke-linecap': 'round', + 'stroke-linejoin': 'round', + }} + />
@@ -113,9 +108,7 @@ export function WindowTitleBar() { aria-label="Minimize window" title="Minimize" > - +
diff --git a/src/components/icons.client.test.tsx b/src/components/icons.client.test.tsx new file mode 100644 index 00000000..524df49c --- /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 a79d5592..206009c1 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -31,6 +31,39 @@ function SvgIcon(props: SvgIconProps): 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} + + ); +} + export function CheckIcon(props: IconProps): JSX.Element { return ( @@ -118,3 +151,460 @@ export function BookmarkIcon(props: IconProps): JSX.Element { ); } + +export function ExternalLinkIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ChevronLeftIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ChevronRightIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ChevronDownIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function StopIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function StarIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function GitHubIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function BotIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function GearIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function PlusLargeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function PlusSmallIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function MinusIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ExpandIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function CollapseIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function EnterFocusIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ExitFocusIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ChevronDownThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ChevronRightThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function FullWidthIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function GitBranchAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function PullRequestIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function PushIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function FolderOpenIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function FolderAltIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ZapIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function NoteIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function QuestionIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function EditIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function LogoIcon(props: IconProps): JSX.Element { + return ( + + + + + + + ); +} + +export function ChevronLeftThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ScrambleIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function TrashIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function MergeIcon(props: IconProps): JSX.Element { + return ( + + + + + + + ); +} + +export function ShieldIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function CompareIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function SyncIcon(props: IconProps): JSX.Element { + return ( + + + + + + + ); +} + +export function PlusThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ClockIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function CheckLargeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function PhoneIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function TerminalIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function ColumnsIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function PanelRightIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function DownloadIcon(props: IconProps): JSX.Element { + return ( + + + + + + ); +} + +export function RefreshIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function MinimizeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function RestoreIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function MaximizeIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function CloseThinIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ArrowUpIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function ArrowDownIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} + +export function DocumentIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function CommentIcon(props: IconProps): JSX.Element { + return ( + + + + + ); +} + +export function CircleIcon(props: IconProps): JSX.Element { + return ( + + + + ); +} diff --git a/src/documents/AnnotationBubble.tsx b/src/documents/AnnotationBubble.tsx index ed3b0361..e6101b57 100644 --- a/src/documents/AnnotationBubble.tsx +++ b/src/documents/AnnotationBubble.tsx @@ -2,6 +2,7 @@ import { For, Show, createMemo, createSignal } from 'solid-js'; import { createHighlightedMarkdown } from '../lib/marked-shiki'; import { store } from '../store/core'; import { getProject } from '../store/projects'; +import { CloseIcon } from '../components/icons'; import { askDocumentAnnotation, askFollowUpQuestion, @@ -231,7 +232,7 @@ export function AnnotationBubble(props: AnnotationBubbleProps) { title="Delete (Del). Undo from the toolbar." onClick={() => void deleteDocumentAnnotation(props.annotation.id)} > - × + diff --git a/src/documents/AnnotationMarker.tsx b/src/documents/AnnotationMarker.tsx index 34fe2743..40809881 100644 --- a/src/documents/AnnotationMarker.tsx +++ b/src/documents/AnnotationMarker.tsx @@ -7,6 +7,7 @@ import { type BelowAnchor, } from '../lib/floating'; import { AnnotationBubble } from './AnnotationBubble'; +import { CommentIcon } from '../components/icons'; import type { DocumentAnnotation } from './types'; import { registerPinnedBubble } from './workspace-ui'; @@ -148,24 +149,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…". */} -