S3.2: hand-placed 12x12 pixel icon set replaces lucide and reicon

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 12:13:33 -04:00
1 parent 86dbb4efa1
commit a6036308b7
35 files changed
+150 -182

No files matched your search

-2
View File
@@ -31,11 +31,9 @@
"postinstall": "node scripts/ensure-electron.cjs"
},
"dependencies": {
"lucide-react": "0.468.0",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-markdown": "10.1.0",
"reicon-react": "^1.1.2",
"remark-gfm": "4.0.1",
"zod": "4.3.6"
},
-24
View File
@@ -14,9 +14,6 @@ importers:
.:
dependencies:
lucide-react:
specifier: 0.468.0
version: 0.468.0(react@19.2.4)
react:
specifier: 19.2.4
version: 19.2.4
@@ -26,9 +23,6 @@ importers:
react-markdown:
specifier: 10.1.0
version: 10.1.0(@types/react@19.2.2)(react@19.2.4)
reicon-react:
specifier: ^1.1.2
version: 1.1.2(react@19.2.4)
remark-gfm:
specifier: 4.0.1
version: 4.0.1
@@ -1950,11 +1944,6 @@ packages:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
lucide-react@0.468.0:
resolution: {integrity: sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -2426,11 +2415,6 @@ packages:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
reicon-react@1.1.2:
resolution: {integrity: sha512-dbp1CPEPKFjWfG41lGRMWi74YKfKJlb2JcB/TRBAUQF6jalZYW1dRZisRPCpze5TfQwRBD30p40qnoY+oYi7MQ==}
peerDependencies:
react: '>=16.8.0'
remark-gfm@4.0.1:
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
@@ -4908,10 +4892,6 @@ snapshots:
dependencies:
yallist: 4.0.0
lucide-react@0.468.0(react@19.2.4):
dependencies:
react: 19.2.4
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -5623,10 +5603,6 @@ snapshots:
string_decoder: 1.1.1
util-deprecate: 1.0.2
reicon-react@1.1.2(react@19.2.4):
dependencies:
react: 19.2.4
remark-gfm@4.0.1:
dependencies:
'@types/mdast': 4.0.4
+2 -2
View File
@@ -188,7 +188,7 @@ export function Onboarding({
<div className="card" style={{ maxWidth: 460 }}>
<div className="card-label" style={{ marginBottom: 10 }}>{t("Runtime selected")}</div>
<div className="row" style={{ gap: 10 }}>
<Icon name="bot" size={18} />
<Icon name="bot" />
<strong style={{ fontSize: 14 }}>{selectedProvider.label}</strong>
{draft.providerModel && <Pill tone="violet">{draft.providerModel}</Pill>}
</div>
@@ -321,7 +321,7 @@ function CompanyStep({ draft, onChange, onBack, onNext }: StepProps) {
<Field label={t("Workspace path (optional)")}>
<div className="row" style={{ gap: 6 }}>
<input className="input" value={draft.workspacePath} onChange={(event) => onChange({ workspacePath: event.target.value })} />
<Button tone="ghost" onClick={chooseWorkspace}><Icon name="folder" size={16} /> {t("Choose")}</Button>
<Button tone="ghost" onClick={chooseWorkspace}><Icon name="folder" /> {t("Choose")}</Button>
</div>
</Field>
<Actions onBack={onBack} onNext={onNext} />
+1 -1
View File
@@ -341,7 +341,7 @@ function DecidedRow({
background: focused ? "var(--accent-soft)" : undefined,
}}
>
<Icon name={approved ? "check" : "x"} size={14} color={approved ? "var(--green)" : superseded ? "var(--text-3)" : "var(--red)"} />
<Icon name={approved ? "check" : "x"} color={approved ? "var(--green)" : superseded ? "var(--text-3)" : "var(--red)"} />
<button
type="button"
className="link-button"
+3 -3
View File
@@ -90,7 +90,7 @@ export function FilesPage({ snapshot, actions, focus }: PageProps) {
]}
/>
<Button tone="primary" onClick={upload}>
<Icon name="upload" size={16} /> {t("Upload")}
<Icon name="upload" /> {t("Upload")}
</Button>
</TopbarActions>
<h1 className="page-h1">{t("Files")}</h1>
@@ -163,7 +163,7 @@ function FilesFilter({
<Popover
trigger={({ toggle }) => (
<button type="button" className="button ghost" aria-label={t("Filter files")} onClick={toggle}>
<Icon name="filter" size={16} color={scan !== "all" ? "var(--accent)" : "currentColor"} />
<Icon name="filter" color={scan !== "all" ? "var(--accent)" : "currentColor"} />
</button>
)}
>
@@ -173,7 +173,7 @@ function FilesFilter({
{options.map((option) => (
<MenuRow key={option.id} active={scan === option.id} onClick={() => onScan(option.id)}>
{option.label}
{scan === option.id && <span className="menu-check"><Icon name="check" size={16} /></span>}
{scan === option.id && <span className="menu-check"><Icon name="check" /></span>}
</MenuRow>
))}
</>
+1 -1
View File
@@ -57,7 +57,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) {
return (
<div className="page-wide" style={{ maxWidth: 1240 }}>
<TopbarActions>
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={16} /> {t("New task")}</Button>
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" /> {t("New task")}</Button>
</TopbarActions>
<h1 className="page-h1">{greeting}, {firstName}</h1>
<p className="page-sub">{renderSummary(needsYou.length, agents.length)}</p>
+2 -2
View File
@@ -105,9 +105,9 @@ export function TeamPage({ snapshot, actions, focus }: PageProps) {
<div className="page">
<TopbarActions>
<Button tone="ghost" onClick={() => cycleFilter(filter, setFilter)} title={t("Filter")}>
<Icon name="filter" size={16} /> {filterLabel(filter)}
<Icon name="filter" /> {filterLabel(filter)}
</Button>
<Button tone="primary" onClick={openHireDialog}><Icon name="plus" size={16} /> {t("Hire")}</Button>
<Button tone="primary" onClick={openHireDialog}><Icon name="plus" /> {t("Hire")}</Button>
</TopbarActions>
<h1 className="page-h1">{t("Team")}</h1>
<p className="page-sub">
+7 -7
View File
@@ -108,9 +108,9 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) {
onPriority={setPriorityFilter}
onAssignee={setAssigneeFilter}
/>
<Button tone="ghost" onClick={() => setNewGoalOpen(true)}><Icon name="plus" size={16} /> {t("Goal")}</Button>
<Button tone="ghost" onClick={() => setNewProjectOpen(true)}><Icon name="plus" size={16} /> {t("Project")}</Button>
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" size={16} /> {t("New task")}</Button>
<Button tone="ghost" onClick={() => setNewGoalOpen(true)}><Icon name="plus" /> {t("Goal")}</Button>
<Button tone="ghost" onClick={() => setNewProjectOpen(true)}><Icon name="plus" /> {t("Project")}</Button>
<Button tone="primary" onClick={() => setNewTaskOpen(true)}><Icon name="plus" /> {t("New task")}</Button>
</TopbarActions>
<h1 className="page-h1">{t("Work")}</h1>
<p className="page-sub">{t("Only a task runs: an agent picks it up and does the work. Goals and projects group tasks; documents and knowledge are what agents read and write along the way.")}</p>
@@ -227,7 +227,7 @@ function WorkFilter({
<Popover
trigger={({ toggle }) => (
<button type="button" className="button ghost" aria-label={t("Filter tasks")} onClick={toggle}>
<Icon name="filter" size={16} color={active ? "var(--accent)" : "currentColor"} />
<Icon name="filter" color={active ? "var(--accent)" : "currentColor"} />
</button>
)}
>
@@ -237,19 +237,19 @@ function WorkFilter({
{priorities.map((option) => (
<MenuRow key={option.id} active={priority === option.id} onClick={() => onPriority(option.id)}>
{option.label}
{priority === option.id && <span className="menu-check"><Icon name="check" size={16} /></span>}
{priority === option.id && <span className="menu-check"><Icon name="check" /></span>}
</MenuRow>
))}
<MenuDivider />
<MenuLabel>{t("Assignee")}</MenuLabel>
<MenuRow active={assignee === "all"} onClick={() => onAssignee("all")}>
{t("All assignees")}
{assignee === "all" && <span className="menu-check"><Icon name="check" size={16} /></span>}
{assignee === "all" && <span className="menu-check"><Icon name="check" /></span>}
</MenuRow>
{agents.map((agent) => (
<MenuRow key={agent.id} active={assignee === agent.id} onClick={() => onAssignee(agent.id)}>
{agent.name}
{assignee === agent.id && <span className="menu-check"><Icon name="check" size={16} /></span>}
{assignee === agent.id && <span className="menu-check"><Icon name="check" /></span>}
</MenuRow>
))}
</>
+3 -3
View File
@@ -97,7 +97,7 @@ export function ChatComposer({
<IconButton icon="sparkle" label={t("Commands")} onClick={() => setSlashOpen((open) => !open)} disabled={locked} />
{attachments.map((attachment) => (
<span key={attachment.id} className="pill" style={{ maxWidth: 220 }}>
<Icon name="file" size={14} />
<Icon name="file" />
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{attachment.name}</span>
<button
type="button"
@@ -106,13 +106,13 @@ export function ChatComposer({
onClick={() => onAttachmentRemove(attachment.id)}
style={{ display: "inline-flex", border: 0, background: "transparent", padding: 0, color: "var(--text-3)", cursor: "pointer" }}
>
<Icon name="x" size={14} />
<Icon name="x" />
</button>
</span>
))}
<div style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 6 }}>
<span className="mono" style={{ fontSize: 12, color: "var(--text-3)" }}>{shortcutLabel("Enter")}</span>
<Button tone="primary" size="small" type="submit" disabled={locked}><Icon name="send" size={12} /> {t("Send")}</Button>
<Button tone="primary" size="small" type="submit" disabled={locked}><Icon name="send" /> {t("Send")}</Button>
</div>
</div>
{send.error && <div className="form-error" role="alert">{t("Not sent: {reason}", { reason: send.error })}</div>}
@@ -297,7 +297,7 @@ function ConversationHeader({
};
return (
<header className="row" style={{ height: 52, padding: "0 24px", borderBottom: "1px solid var(--border-soft)", flexShrink: 0 }}>
<Icon name={space.visibility === "restricted" ? "lock" : "hash"} size={15} color="var(--text-3)" />
<Icon name={space.visibility === "restricted" ? "lock" : "hash"} color="var(--text-3)" />
<span style={{ fontSize: 14, fontWeight: 600 }}>{space.name}</span>
<span
style={{
@@ -89,9 +89,9 @@ export function ChatMessageRow({
{message.editedAt && !deleted && <span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("(edited)")}</span>}
{!editing && (canModify || (onOpenThread && !deleted)) && (
<span style={{ marginLeft: "auto", display: "flex", gap: 2 }}>
{onOpenThread && !deleted && <IconButton icon="chat" label={t("Reply in thread")} size={16} onClick={onOpenThread} />}
{canModify && <IconButton icon="edit" label={t("Edit message")} size={16} onClick={() => setEditing(true)} />}
{canModify && <IconButton icon="x" label={t("Delete message")} size={16} onClick={onDelete} />}
{onOpenThread && !deleted && <IconButton icon="chat" label={t("Reply in thread")} onClick={onOpenThread} />}
{canModify && <IconButton icon="edit" label={t("Edit message")} onClick={() => setEditing(true)} />}
{canModify && <IconButton icon="x" label={t("Delete message")} onClick={onDelete} />}
</span>
)}
</div>
@@ -254,14 +254,14 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec
placeItems: "center",
}}
>
<Icon name={file.kind === "folder" ? "folder" : "file"} size={16} />
<Icon name={file.kind === "folder" ? "folder" : "file"} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div className="row" style={{ gap: 6, minWidth: 0 }}>
<span style={{ fontSize: 12, fontWeight: 540, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{file.name}</span>
{handoff && (
<span className="pill violet" style={{ flexShrink: 0 }}>
<Icon name="bot" size={10} /> {t("AI → AI")}
<Icon name="bot" /> {t("AI → AI")}
</span>
)}
</div>
+1 -1
View File
@@ -156,7 +156,7 @@ function ConversationRow({
marginTop: 2,
}}
>
<Icon name={conversation.space.visibility === "restricted" ? "lock" : "hash"} size={13} color="var(--text-3)" />
<Icon name={conversation.space.visibility === "restricted" ? "lock" : "hash"} color="var(--text-3)" />
</span>
)}
<span style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 1 }}>
+1 -1
View File
@@ -148,7 +148,7 @@ function ApprovalInline({ approval, onApprove, onDecline }: { approval: Approval
return (
<div style={{ margin: "14px 0 18px 44px", maxWidth: 480, border: "1px solid var(--border-strong)", borderRadius: 12, background: "var(--panel)", padding: 14 }}>
<div className="row" style={{ marginBottom: 8 }}>
<Icon name="approve" size={14} color="var(--amber)" />
<Icon name="approve" color="var(--amber)" />
<span style={{ fontSize: 12, color: "var(--amber)", fontWeight: 540 }}>{t("Approval needed")}</span>
<Pill tone={approval.risk === "high" ? "red" : approval.risk === "medium" ? "amber" : undefined}>{riskLabel(approval.risk)}</Pill>
</div>
@@ -96,7 +96,7 @@ export function ConnectedList({
{holders.map((holder) => (
<span key={holder.agentId} className="row" style={{ gap: 2 }}>
<Pill>{holder.text}</Pill>
<IconButton icon="x" size={12} label={t("Revoke {name}", { name: holder.text })} disabled={busy} onClick={() => onRevoke(holder.agentId, account.toolkitSlug)} />
<IconButton icon="x" label={t("Revoke {name}", { name: holder.text })} disabled={busy} onClick={() => onRevoke(holder.agentId, account.toolkitSlug)} />
</span>
))}
{account.status === "ACTIVE" && grantable.length > 0 && (
@@ -68,7 +68,7 @@ export function ToolkitCatalog({
<div className="row" style={{ marginBottom: 12, gap: 10 }}>
<div className="section-label" style={{ flex: 1 }}>{t("Catalog")}</div>
<div className="row" style={{ gap: 6, background: "var(--panel-2)", borderRadius: 8, padding: "5px 10px", width: 240 }}>
<Icon name="search" size={13} color="var(--text-3)" />
<Icon name="search" color="var(--text-3)" />
<input
aria-label={t("Search connectors")}
value={query}
+1 -1
View File
@@ -9,7 +9,7 @@ export function FileIcon({ kind, big = false }: { kind: string; big?: boolean })
if (kind === "folder") {
return (
<div style={{ width: big ? 28 : 22, height: big ? 28 : 22, display: "grid", placeItems: "center", color: "var(--text-3)" }}>
<Icon name="folder" size={big ? 22 : 17} />
<Icon name="folder" />
</div>
);
}
+1 -1
View File
@@ -83,7 +83,7 @@ export function PinnedCard({
onClick={(event) => { event.stopPropagation(); onUnpin(); }}
style={{ marginLeft: "auto", padding: 4, border: 0, background: "transparent", color: file.pinned ? "var(--accent)" : "var(--text-4)", cursor: "pointer" }}
>
<Icon name="pin" size={14} />
<Icon name="pin" />
</button>
</div>
<div style={{ fontSize: 14, fontWeight: 540, letterSpacing: "-0.005em", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
+3 -2
View File
@@ -1,4 +1,5 @@
import { Icon } from "../../shared/ui";
import type { IconName } from "../../shared/Icon";
import type { AgentRecord, FileObjectRecord } from "../../../../shared/domain";
import { isRecent, type Section } from "./file-helpers";
import { t } from "../../i18n/i18n";
@@ -84,7 +85,7 @@ function TreeItem({
active,
onClick,
}: {
icon: string;
icon: IconName;
label: string;
count: number;
active: boolean;
@@ -92,7 +93,7 @@ function TreeItem({
}) {
return (
<button type="button" aria-current={active ? "true" : undefined} className={`nav-item ${active ? "active" : ""}`.trim()} onClick={onClick}>
<span className="icon-box"><Icon name={icon} size={18} /></span>
<span className="icon-box"><Icon name={icon} /></span>
<span>{label}</span>
<span className="nav-badge num">{count}</span>
</button>
+1 -1
View File
@@ -36,7 +36,7 @@ export function NextUpCard({
{needsYou.length === 0 ? (
<>
<div className="row" style={{ gap: 8, color: "var(--text-3)", fontSize: 14, marginBottom: 12 }}>
<Icon name="check" size={16} /><span>{t("You're all caught up")}</span>
<Icon name="check" /><span>{t("You're all caught up")}</span>
</div>
{next?.kind === "next" && (
<div>
@@ -44,7 +44,7 @@ export function AgentDetailCard({ agent, onClose }: { agent: OfficeAgentInput; o
</div>
{agent.taskTitle && (
<div className="row" style={{ gap: 7, marginTop: 12, alignItems: "flex-start" }}>
<Icon name="work" size={13} color="var(--text-3)" />
<Icon name="work" color="var(--text-3)" />
<span
style={{
fontSize: 12,
@@ -108,7 +108,7 @@ export function ProviderRequestPanel({
</div>
<Button type="submit" tone="primary">
<span className="row" style={{ gap: 6 }}>
<Icon name="send" size={16} />
<Icon name="send" />
{t("Run")}
</span>
</Button>
@@ -180,7 +180,7 @@ function ProviderRequestRow({
<div className="mono" style={{ color: "var(--text-2)" }}>{t("{count} chars", { count: request.outputCharCount })}</div>
<Button size="small" tone="ghost" onClick={cancel} disabled={!canCancel}>
<span className="row" style={{ gap: 5 }}>
<Icon name="x" size={12} />
<Icon name="x" />
{t("Cancel")}
</span>
</Button>
@@ -67,7 +67,7 @@ export function WorkspacePanel({
<div className="page">
<div className="row" style={{ alignItems: "baseline" }}>
<h1 className="page-h1">{t("General")}</h1>
<span style={{ marginLeft: "auto" }}><Button onClick={() => setEditOpen(true)}><Icon name="edit" size={14} /> {t("Edit company")}</Button></span>
<span style={{ marginLeft: "auto" }}><Button onClick={() => setEditOpen(true)}><Icon name="edit" /> {t("Edit company")}</Button></span>
</div>
<ScopeNote scope="company" company={organization.name} />
<CompanyDialog key={editOpen ? "open" : "closed"} organization={organization} open={editOpen} actions={actions} onClose={() => setEditOpen(false)} />
@@ -109,7 +109,7 @@ export function WorkspacePanel({
<div className="row reveal-row" style={{ marginBottom: 8 }}>
<div className="section-label">{t("Workspaces")}</div>
<span className="reveal" style={{ marginLeft: "auto" }}>
<Button tone="ghost" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> {t("Add")}</Button>
<Button tone="ghost" onClick={() => setCreateOpen(true)}><Icon name="plus" /> {t("Add")}</Button>
</span>
</div>
<ScopeNote scope="company_folders" company={organization.name} />
@@ -126,7 +126,7 @@ export function WorkspacePanel({
const form = document.querySelector<HTMLFormElement>("form:has([name='localPath'])");
if (form) void chooseDirectory(form);
}}>
<Icon name="folder" size={16} /> {t("Browse")}
<Icon name="folder" /> {t("Browse")}
</Button>
</div>
</Field>
@@ -55,7 +55,7 @@ export function WorkspaceRow({ workspace, projects, focused, actions }: { worksp
<Field label={t("Local path")}>
<div className="row" style={{ gap: 6 }}>
<input className="input" name="localPath" data-workspace-path={workspace.id} defaultValue={workspace.localPath} required style={{ flex: 1 }} />
<Button tone="ghost" onClick={browse}><Icon name="folder" size={16} /> {t("Browse")}</Button>
<Button tone="ghost" onClick={browse}><Icon name="folder" /> {t("Browse")}</Button>
</div>
</Field>
<Field label={t("Project")}>
@@ -203,15 +203,15 @@ export function TaskDetailDialog({
)}
{actionError && <div className="form-error" role="alert" style={{ marginTop: 8 }}>{actionError}</div>}
<div className="modal-actions">
<Button tone="ghost" onClick={askDelete} disabled={deleteImpact !== null}><Icon name="x" size={16} /> {t("Delete")}</Button>
<Button tone="ghost" onClick={askDelete} disabled={deleteImpact !== null}><Icon name="x" /> {t("Delete")}</Button>
{task.blockedReason
? <Button onClick={() => act(() => onBlock(task, null))}>{t("Unblock")}</Button>
: task.status !== TASK_STATUS.DONE && !blocking && <Button onClick={() => setBlocking(true)}>{t("Block")}</Button>}
{task.status === TASK_STATUS.REVIEW && <Button onClick={() => act(() => onMove(task, TASK_STATUS.IN_PROGRESS))}>{t("Send back")}</Button>}
{task.status === TASK_STATUS.DONE && <Button onClick={() => act(() => onMove(task, TASK_STATUS.IN_PROGRESS))}>{t("Reopen")}</Button>}
{task.status === TASK_STATUS.IN_PROGRESS && <Button onClick={() => act(() => onMove(task, TASK_STATUS.BACKLOG))}>{t("Back to backlog")}</Button>}
{assignee && <Button onClick={() => onRun(task)} disabled={runBlocker(task, provider, taskRuns) !== null}><Icon name="send" size={16} /> {t("Run")}</Button>}
{advanceTo && <Button tone="primary" onClick={() => onAdvance(task)}><Icon name="check" size={16} /> {prettyStatus(advanceTo)}</Button>}
{assignee && <Button onClick={() => onRun(task)} disabled={runBlocker(task, provider, taskRuns) !== null}><Icon name="send" /> {t("Run")}</Button>}
{advanceTo && <Button tone="primary" onClick={() => onAdvance(task)}><Icon name="check" /> {prettyStatus(advanceTo)}</Button>}
</div>
{editing && (
@@ -292,7 +292,7 @@ function SubtasksTab({ subtasks, agents, onOpenTask }: { subtasks: TaskRecord[];
const owner = agents.find((a) => a.id === st.assigneeAgentId);
return (
<div key={st.id} className="row" style={{ gap: 10, padding: "8px 0", borderBottom: "1px solid var(--border-soft)" }}>
<Icon name={st.status === TASK_STATUS.DONE ? "check" : "work"} size={16} color={st.status === TASK_STATUS.DONE ? "var(--green)" : "var(--text-3)"} />
<Icon name={st.status === TASK_STATUS.DONE ? "check" : "work"} color={st.status === TASK_STATUS.DONE ? "var(--green)" : "var(--text-3)"} />
<button type="button" className="link-button" aria-label={t("Open task {title}", { title: st.title })} onClick={() => onOpenTask(st.id)} style={{ flex: 1, fontSize: 14, fontWeight: 480, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", textAlign: "left" }}>{st.title}</button>
<Pill>{prettyStatus(st.status)}</Pill>
{owner && <Avatar name={owner.name} size="sm" bot />}
+17 -68
View File
@@ -1,79 +1,28 @@
import type { ReactNode } from "react";
import { PIXEL_ICONS } from "./pixel-icons";
const PATHS: Record<string, ReactNode> = {
home: <><path d="M3 12 12 3l9 9" /><path d="M5 10v10h14V10" /></>,
inbox: <><path d="M22 12h-6l-2 3h-4l-2-3H2" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z" /></>,
team: <><circle cx="9" cy="8" r="3.2" /><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6" /><circle cx="17" cy="9" r="2.6" /><path d="M21 18.5c0-2.5-2-4.5-4.5-4.5" /></>,
work: <><rect x="3" y="4" width="18" height="16" rx="2" /><path d="M8 4v16M16 4v16" /></>,
chat: <path d="M21 12a8 8 0 0 1-11.6 7.15L4 20l1-4.4A8 8 0 1 1 21 12Z" />,
docs: <><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" /><path d="M14 3v5h5" /></>,
approve: <><path d="M9 12l2 2 4-4" /><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" /></>,
social: <><path d="M4 11a8 8 0 0 1 8-8" /><path d="M4 11v4a4 4 0 0 0 4 4h1" /><circle cx="18" cy="6" r="2" /><circle cx="6" cy="18" r="2" /><circle cx="18" cy="18" r="2" /><path d="M8 17l8-9M18 8v8" /></>,
settings: <><circle cx="12" cy="12" r="3" /><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z" /></>,
search: <><circle cx="11" cy="11" r="7" /><path d="m20 20-3-3" /></>,
plus: <path d="M12 5v14M5 12h14" />,
minus: <path d="M5 12h14" />,
chevron: <path d="m9 18 6-6-6-6" />,
chevronDown: <path d="m6 9 6 6 6-6" />,
chevronLeft: <path d="m15 18-6-6 6-6" />,
more: <><circle cx="5" cy="12" r="1" /><circle cx="12" cy="12" r="1" /><circle cx="19" cy="12" r="1" /></>,
edit: <><path d="M12 20h9" /><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" /></>,
filter: <path d="M3 5h18M6 12h12M10 19h4" />,
file: <><path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" /><path d="M14 3v6h6" /></>,
folder: <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z" />,
grid: <><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></>,
list: <path d="M8 6h13M8 12h13M8 18h13M4 6h.01M4 12h.01M4 18h.01" />,
upload: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m17 8-5-5-5 5M12 3v12" /></>,
arrowRight: <path d="M5 12h14M13 5l7 7-7 7" />,
arrowUpRight: <path d="M7 17 17 7M7 7h10v10" />,
sparkle: <path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" />,
bot: <><rect x="4" y="7" width="16" height="12" rx="3" /><circle cx="9" cy="13" r="1" /><circle cx="15" cy="13" r="1" /><path d="M12 3v4M8 19v2M16 19v2" /></>,
user: <><circle cx="12" cy="8" r="4" /><path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8" /></>,
check: <path d="M5 12.5 10 17l9-10" />,
x: <path d="M18 6 6 18M6 6l12 12" />,
pin: <><path d="m12 17 .01 5M9.2 12.8 5 17M19 8c-1.3 0-3.5.3-4.7 1.5l-2 2c-.4.5-.4 1.1 0 1.5L14.7 15c.4.4 1 .4 1.5 0l2-2C19.4 11.6 20 9.5 20 8.2c0-.1-.1-.2-.2-.2" /></>,
attach: <path d="m21 11.5-9.6 9.6a5.5 5.5 0 0 1-7.8-7.8l9.6-9.6a3.7 3.7 0 0 1 5.2 5.2L8.8 18.4a1.8 1.8 0 0 1-2.6-2.6L15 7" />,
send: <><path d="m22 2-7 20-4-9-9-4Z" /><path d="M22 2 11 13" /></>,
hash: <path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18" />,
lock: <><rect x="4" y="11" width="16" height="10" rx="2" /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></>,
bell: <><path d="M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10 21a2 2 0 0 0 4 0" /></>,
cmd: <path d="M15 15h3a3 3 0 0 0 0-6h-3v6Zm0 0v-6h-6m6 6v3a3 3 0 0 1-6 0v-3m0 0H6a3 3 0 0 1 0-6h3m0 6v-6m0 0V6a3 3 0 1 0-3 3h3Z" />,
refresh: <><path d="M21 12a9 9 0 1 1-3-6.7" /><path d="M21 3v6h-6" /></>,
pause: <><rect x="6" y="5" width="4" height="14" rx="1" /><rect x="14" y="5" width="4" height="14" rx="1" /></>,
shield: <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z" />,
calendar: <><rect x="3" y="4.5" width="18" height="16" rx="2" /><path d="M3 9h18M8 2.5v4M16 2.5v4" /></>,
target: <><circle cx="12" cy="12" r="8" /><circle cx="12" cy="12" r="3.6" /></>,
};
export type IconName = keyof typeof PIXEL_ICONS;
export type IconName = keyof typeof PATHS | (string & {});
/** Each row's lit runs as one path, so an icon is a single crisp shape. */
function bitmapPath(bitmap: string): string {
return bitmap.split(" ").flatMap((row, y) =>
[...row.matchAll(/#+/g)].map((run) => `M${run.index} ${y}h${run[0].length}v1h-${run[0].length}z`),
).join("");
}
export function Icon({
name,
size = 18,
color = "currentColor",
strokeWidth = 1.5,
}: {
name: IconName;
size?: number;
color?: string;
strokeWidth?: number;
}) {
const node = PATHS[name];
if (!node) throw new Error(`Unknown icon: ${name}`);
const PATHS = Object.fromEntries(Object.entries(PIXEL_ICONS).map(([name, bitmap]) => [name, bitmapPath(bitmap)])) as Record<IconName, string>;
/** A 12-pixel icon drawn on the same whole-screen-pixel grid as the pixel font. */
export function Icon({ name, color = "currentColor" }: { name: IconName; color?: string }) {
return (
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
viewBox="0 0 12 12"
className="pixel-icon"
fill={color}
shapeRendering="crispEdges"
aria-hidden="true"
focusable="false"
>
{node}
<path d={PATHS[name]} />
</svg>
);
}
+16 -29
View File
@@ -1,34 +1,21 @@
import {
BookOpen,
Briefcase,
Buildings2,
ChatRoundDots,
Folder,
Gear,
Home,
Plug2,
Send,
ShieldCheck,
Users,
} from "reicon-react";
import { Icon, type IconName } from "./Icon";
import type { RouteId } from "./Shell";
/** One glyph per navigation surface, from the Reicon set. */
const NAV_ICONS: Record<RouteId, typeof Home> = {
home: Home,
work: Briefcase,
team: Users,
chat: ChatRoundDots,
runs: Send,
files: Folder,
library: BookOpen,
approvals: ShieldCheck,
connectors: Plug2,
office: Buildings2,
settings: Gear,
/** One pixel glyph per navigation surface. */
const NAV_ICONS: Record<RouteId, IconName> = {
home: "home",
work: "work",
team: "team",
chat: "chat",
runs: "send",
files: "folder",
library: "book",
approvals: "approve",
connectors: "plug",
office: "building",
settings: "settings",
};
export function NavIcon({ route, size = 18 }: { route: RouteId; size?: number }) {
const Glyph = NAV_ICONS[route];
return <Glyph size={size} />;
export function NavIcon({ route }: { route: RouteId }) {
return <Icon name={NAV_ICONS[route]} />;
}
+6 -6
View File
@@ -62,7 +62,7 @@ export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete }
>
<span className="brand-mark">{orgInitial(active.name)}</span>
<span className="brand-name">{active.name}</span>
<Icon name="chevronDown" size={14} color="var(--text-3)" />
<Icon name="chevronDown" color="var(--text-3)" />
</button>
{open && (confirmingOrg ? (
<div role="menu" className="org-menu">
@@ -100,8 +100,8 @@ export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete }
>
<span className="brand-mark sm">{orgInitial(org.name)}</span>
<span className="org-menu-name">{org.name}</span>
{org.id === activeOrgId && <Icon name="check" size={14} color="var(--text-1)" />}
<span className="org-menu-chevron"><Icon name="chevron" size={14} /></span>
{org.id === activeOrgId && <Icon name="check" color="var(--text-1)" />}
<span className="org-menu-chevron"><Icon name="chevron" /></span>
</button>
</CascadeRow>
))}
@@ -112,7 +112,7 @@ export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete }
className="org-menu-action"
onClick={() => { setOpen(false); setConfirmingDelete(null); onNew(); }}
>
<span className="icon-box" style={{ width: 18, height: 18 }}><Icon name="plus" size={16} color="var(--text-2)" /></span>
<span className="icon-box" style={{ width: 18, height: 18 }}><Icon name="plus" color="var(--text-2)" /></span>
<span>{t("New organization")}</span>
</button>
</CascadeMenu>
@@ -137,12 +137,12 @@ function OrgActions({
<div className="org-menu-section-label">{org.name}</div>
{!isActive && (
<button type="button" role="menuitem" className="org-menu-action" onClick={onSwitch}>
<span className="icon-box" style={{ width: 18, height: 18 }}><Icon name="arrowRight" size={16} color="var(--text-2)" /></span>
<span className="icon-box" style={{ width: 18, height: 18 }}><Icon name="arrowRight" color="var(--text-2)" /></span>
<span>{t("Switch to this org")}</span>
</button>
)}
<button type="button" role="menuitem" className="org-menu-action danger" onClick={onDelete}>
<span className="icon-box" style={{ width: 18, height: 18 }}><Icon name="x" size={16} color="var(--red)" /></span>
<span className="icon-box" style={{ width: 18, height: 18 }}><Icon name="x" color="var(--red)" /></span>
<span>{t("Delete organization")}</span>
</button>
</>
@@ -73,7 +73,7 @@ export function ProviderOAuthConnect({
if (phase === "done") {
return (
<div className="oauth-connect-row done">
<Icon name="check" size={14} color="var(--green)" />
<Icon name="check" color="var(--green)" />
<span>{t("Signed in to {label}", { label })}</span>
<button type="button" className="link-button" onClick={run}>{t("Sign in again")}</button>
</div>
+1 -1
View File
@@ -120,7 +120,7 @@ export function SearchDialog({
<Modal label={t("Search")} onEscape={onClose} onBackdrop={onClose} backdropStyle={overlayStyle}>
<form onSubmit={submit} style={panelStyle}>
<div className="row" style={{ padding: "10px 12px", borderBottom: "1px solid var(--border-soft)" }}>
<Icon name="search" size={14} color="var(--text-3)" />
<Icon name="search" color="var(--text-3)" />
<input
autoFocus
aria-label={t("Search query")}
+1 -1
View File
@@ -93,7 +93,7 @@ export function Shell({
onDelete={onDeleteOrg}
/>
<button type="button" className="search-bar" aria-label={t("Search")} onClick={() => setSearchOpen(true)}>
<Icon name="search" size={13} />
<Icon name="search" />
<span>{t("Search")}</span>
<span className="search-kbd">{shortcutLabel("K")}</span>
</button>
+38
View File
@@ -0,0 +1,38 @@
/** Hand-placed 12x12 pixel icons: rows separated by spaces, "#" is a lit pixel. */
export const PIXEL_ICONS = {
home: ".....##..... ....####.... ...##..##... ..##....##.. .##......##. ############ .#........#. .#..####..#. .#..#..#..#. .#..#..#..#. .#..#..#..#. .##########.",
work: "............ ....####.... ....#..#.... ############ #..........# #..........# #####..##### #....##....# #..........# #..........# ############ ............",
team: "............ ..##....##.. .####..####. .####..####. ..##....##.. ............ .####..####. ######.##### ######.##### ######.##### ............ ............",
user: "............ ....####.... ...######... ...######... ...######... ....####.... ............ ..########.. .##########. .##########. .##########. ............",
chat: "............ .##########. #..........# #..........# #.##.##.##.# #..........# #..........# .###.######. ...#.#...... ...##....... ...#........ ............",
send: "..........## ........#### ......###.## ....###..##. ..###...##.. ####...##... ..###.##.... ....####.... .....###.... ......##.... ......#..... ............",
folder: "............ ####........ #..#........ #..#######.. #........#.. ############ #..........# #..........# #..........# #..........# ############ ............",
file: "#######..... #.....##.... #.....#.#... #.....####.. #........#.. #.#####..#.. #........#.. #.#####..#.. #........#.. #.####...#.. #........#.. ##########..",
book: "............ .####..####. #....##....# #.##.##.##.# #....##....# #.##.##.##.# #....##....# #.##.##.##.# #....##....# .####..####. .....##..... ............",
approve: ".....##..... ..########.. .#........#. .#........#. .#......#.#. .#.#...##.#. .#.##.##..#. .#..###...#. ..#..#...#.. ...#....#... ....#..#.... .....##.....",
plug: "............ ...#....#... ...#....#... ..########.. ..#......#.. ..#......#.. ..#......#.. ...#....#... ....####.... .....##..... .....##..... .....##.....",
building: "..########.. ..#......#.. ..#.##.#.#.. ..#......#.. ..#.##.#.#.. ..#......#.. ..#.##.#.#.. ..#......#.. ###.####.### #.#.#..#.#.# #.#.#..#.#.# ############",
settings: ".....##..... ..#.####.#.. .##########. ..###..###.. .###....###. ####....#### ####....#### .###....###. ..###..###.. .##########. ..#.####.#.. .....##.....",
search: "............ ..#####..... .#.....#.... #.......#... #.......#... #.......#... #.......#... .#.....#.... ..######.... .......###.. ........###. .........##.",
plus: "............ ............ .....##..... .....##..... .....##..... ..########.. ..########.. .....##..... .....##..... .....##..... ............ ............",
minus: "............ ............ ............ ............ ............ ..########.. ..########.. ............ ............ ............ ............ ............",
chevron: "............ ............ ....##...... .....##..... ......##.... .......##... .......##... ......##.... .....##..... ....##...... ............ ............",
chevronDown: "............ ............ ............ ............ ..##....##.. ...##..##... ....####.... .....##..... ............ ............ ............ ............",
chevronLeft: "............ ............ ......##.... .....##..... ....##...... ...##....... ...##....... ....##...... .....##..... ......##.... ............ ............",
more: "............ ............ ............ ............ ............ .##..##..##. .##..##..##. ............ ............ ............ ............ ............",
edit: ".........##. ........#### .......####. ......####.. .....####... ....####.... ...####..... ..####...... .#.##....... .##......... .###........ ............",
filter: "............ ############ .##########. ..########.. ...######... ....####.... ....####.... ....####.... ....####.... ....###..... ....##...... ............",
grid: "............ .####..####. .####..####. .####..####. .####..####. ............ ............ .####..####. .####..####. .####..####. .####..####. ............",
list: "............ ##..######## ##..######## ............ ............ ##..######## ##..######## ............ ............ ##..######## ##..######## ............",
upload: ".....##..... ....####.... ...######... ..##.##.##.. .....##..... .....##..... .....##..... #....##....# #..........# #..........# ############ ............",
arrowRight: "............ ............ .......#.... .......##... .......###.. ##########.. ##########.. .......###.. .......##... .......#.... ............ ............",
arrowUpRight: "............ ....#######. .....######. ........###. .......####. ......###.#. .....###.... ....###..... ...###...... ..###....... .###........ ............",
sparkle: ".....##..... .....##..... .....##..... ....####.... ..########.. ############ ..########.. ....####.... .....##..... .....##..... .....##..... ............",
bot: ".....##..... .....##..... ..########.. .#........#. .#.##..##.#. .#.##..##.#. .#........#. .#..####..#. .#........#. ..########.. ...#....#... ...#....#...",
check: "............ ............ ..........## .........##. ........##.. .......##... ##....##.... .##..##..... ..####...... ...##....... ............ ............",
x: "............ .##......##. ..##....##.. ...##..##... ....####.... .....##..... ....####.... ...##..##... ..##....##.. .##......##. ............ ............",
pin: "............ ....######.. .....####... .....####... .....####... ....######.. ...########. .......#.... .......#.... .......#.... .......#.... ............",
attach: "....####.... ...#....#... ...#.##.#... ...#.#..#... ...#.#..#... ...#.#..#... ...#.#..#... ...#.#..#... ...#....#... ...#....#... ....####.... ............",
hash: "............ ...#...#.... ...#...#.... .#########.. ...#...#.... ...#...#.... ..#...#..... #########... ..#...#..... ..#...#..... ............ ............",
lock: "............ ....####.... ...#....#... ...#....#... ...#....#... ..########.. ..########.. ..###..###.. ..###..###.. ..########.. ..########.. ............",
} as const;
+2 -5
View File
@@ -66,7 +66,6 @@ export function Avatar({
const [hashedBg, hashedFg] = paletteFor(name);
const bg = custom?.color || hashedBg;
const fg = custom?.color ? "#fff" : hashedFg;
const dim = size === "sm" ? 14 : size === "lg" ? 18 : size === "xl" ? 24 : 14;
const emoji = custom?.emoji ?? null;
const image = custom?.image ?? null;
return (
@@ -76,7 +75,7 @@ export function Avatar({
) : emoji ? (
<span aria-hidden>{emoji}</span>
) : bot ? (
<Icon name="bot" size={dim} />
<Icon name="bot" />
) : (
initials(name)
)}
@@ -148,14 +147,12 @@ export function IconButton({
onClick,
label,
variant = "ghost",
size = 14,
disabled = false,
}: {
icon: IconName;
onClick?: () => void;
label: string;
variant?: "ghost" | "solid";
size?: number;
disabled?: boolean;
}) {
return (
@@ -167,7 +164,7 @@ export function IconButton({
onClick={onClick}
disabled={disabled}
>
<Icon name={icon} size={size} />
<Icon name={icon} />
</button>
);
}
+3
View File
@@ -56,3 +56,6 @@
animation: px-blink 0.8s steps(1, end) infinite;
}
@keyframes px-blink { 50% { visibility: hidden; } }
/* Icons share the font's grid: 12 icon pixels at the same whole-screen-pixel scale. */
.pixel-icon { width: calc(12px * var(--px)); height: calc(12px * var(--px)); flex-shrink: 0; }
+6
View File
@@ -44,6 +44,12 @@ test("text uses the bundled pixel font, and the palette follows the day part, se
const idle = rows.find((row) => !row.matches(".active, [aria-current]"))!;
return [getComputedStyle(active, "::before").animationName, getComputedStyle(idle, "::before").animationName, getComputedStyle(idle, "::before").visibility];
})).toEqual(["px-blink", "none", "hidden"]);
// Every icon on screen is a pixel glyph on the font's grid; no stroked vector icons remain.
expect(await page.evaluate(() => ({
stroked: [...document.querySelectorAll("svg")].filter((svg) => !svg.closest("canvas") && svg.getAttribute("stroke") && svg.getAttribute("stroke") !== "none").length,
size: getComputedStyle(document.querySelector("svg.pixel-icon")!).width === getComputedStyle(document.body).fontSize,
}))).toEqual({ stroked: 0, size: true });
expect(await page.evaluate(() => document.querySelectorAll(".nav > .nav-item").length)).toBe(await page.evaluate(() => document.querySelectorAll(".nav > .nav-item svg.pixel-icon").length));
await page.getByRole("button", { name: "New task" }).click();
const dialog = page.getByRole("dialog", { name: "New task" });
await expect(dialog).toBeVisible();
+13
View File
@@ -0,0 +1,13 @@
import { expect, test } from "vitest";
import { PIXEL_ICONS } from "../../src/renderer/src/shared/pixel-icons";
test("every icon is a 12x12 bitmap of lit and dark pixels, and no two icons are the same", () => {
for (const [name, bitmap] of Object.entries(PIXEL_ICONS)) {
const rows = bitmap.split(" ");
expect(rows, name).toHaveLength(12);
for (const row of rows) expect(row, name).toMatch(/^[#.]{12}$/);
expect(bitmap.includes("#"), name).toBe(true);
}
const bitmaps = Object.values(PIXEL_ICONS);
expect(new Set(bitmaps).size).toBe(bitmaps.length);
});