From a6036308b7bbaac770133acaef83033f94d211ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 12:13:33 -0400 Subject: [PATCH] S3.2: hand-placed 12x12 pixel icon set replaces lucide and reicon Co-Authored-By: Claude Opus 5.5 --- package.json | 2 - pnpm-lock.yaml | 24 ------ src/renderer/src/app/Onboarding.tsx | 4 +- src/renderer/src/pages/ApprovalsPage.tsx | 2 +- src/renderer/src/pages/FilesPage.tsx | 6 +- src/renderer/src/pages/HomePage.tsx | 2 +- src/renderer/src/pages/TeamPage.tsx | 4 +- src/renderer/src/pages/WorkPage.tsx | 14 +-- src/renderer/src/pages/chat/ChatComposer.tsx | 6 +- .../src/pages/chat/ChatConversation.tsx | 2 +- .../src/pages/chat/ChatMessageRow.tsx | 10 +-- src/renderer/src/pages/chat/ChatSidebar.tsx | 2 +- src/renderer/src/pages/chat/ChatTimeline.tsx | 2 +- .../src/pages/connectors/ConnectedList.tsx | 2 +- .../src/pages/connectors/ToolkitCatalog.tsx | 2 +- src/renderer/src/pages/files/FileVisuals.tsx | 2 +- src/renderer/src/pages/files/FilesGrid.tsx | 2 +- src/renderer/src/pages/files/FilesTree.tsx | 5 +- src/renderer/src/pages/home/NextUpCard.tsx | 2 +- .../src/pages/office/AgentDetailCard.tsx | 2 +- .../pages/settings/ProviderRequestPanel.tsx | 4 +- .../src/pages/settings/WorkspacePanel.tsx | 6 +- .../src/pages/settings/WorkspaceRow.tsx | 2 +- .../src/pages/work/TaskDetailDialog.tsx | 8 +- src/renderer/src/shared/Icon.tsx | 85 ++++--------------- src/renderer/src/shared/NavIcon.tsx | 45 ++++------ src/renderer/src/shared/OrgSwitcher.tsx | 12 +-- .../src/shared/ProviderOAuthConnect.tsx | 2 +- src/renderer/src/shared/SearchDialog.tsx | 2 +- src/renderer/src/shared/Shell.tsx | 2 +- src/renderer/src/shared/pixel-icons.ts | 38 +++++++++ src/renderer/src/shared/ui.tsx | 7 +- src/renderer/src/styles/pixel.css | 3 + tests/e2e/pixel-style.spec.ts | 6 ++ tests/unit/pixel-icons.test.ts | 13 +++ 35 files changed, 150 insertions(+), 182 deletions(-) create mode 100644 src/renderer/src/shared/pixel-icons.ts create mode 100644 tests/unit/pixel-icons.test.ts diff --git a/package.json b/package.json index 176b4980..445e407a 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7c612c2..c61e9b81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/renderer/src/app/Onboarding.tsx b/src/renderer/src/app/Onboarding.tsx index 2cce20aa..dc106d70 100644 --- a/src/renderer/src/app/Onboarding.tsx +++ b/src/renderer/src/app/Onboarding.tsx @@ -188,7 +188,7 @@ export function Onboarding({
{t("Runtime selected")}
- + {selectedProvider.label} {draft.providerModel && {draft.providerModel}}
@@ -321,7 +321,7 @@ function CompanyStep({ draft, onChange, onBack, onNext }: StepProps) {
onChange({ workspacePath: event.target.value })} /> - +
diff --git a/src/renderer/src/pages/ApprovalsPage.tsx b/src/renderer/src/pages/ApprovalsPage.tsx index 158859ba..7ba91301 100644 --- a/src/renderer/src/pages/ApprovalsPage.tsx +++ b/src/renderer/src/pages/ApprovalsPage.tsx @@ -341,7 +341,7 @@ function DecidedRow({ background: focused ? "var(--accent-soft)" : undefined, }} > - +

{t("Files")}

@@ -163,7 +163,7 @@ function FilesFilter({ ( )} > @@ -173,7 +173,7 @@ function FilesFilter({ {options.map((option) => ( onScan(option.id)}> {option.label} - {scan === option.id && } + {scan === option.id && } ))} diff --git a/src/renderer/src/pages/HomePage.tsx b/src/renderer/src/pages/HomePage.tsx index 0d0d9a6e..23891e66 100644 --- a/src/renderer/src/pages/HomePage.tsx +++ b/src/renderer/src/pages/HomePage.tsx @@ -57,7 +57,7 @@ export function HomePage({ snapshot, actions, navigate }: PageProps) { return (
- +

{greeting}, {firstName}

{renderSummary(needsYou.length, agents.length)}

diff --git a/src/renderer/src/pages/TeamPage.tsx b/src/renderer/src/pages/TeamPage.tsx index 51d030e9..6a0feea2 100644 --- a/src/renderer/src/pages/TeamPage.tsx +++ b/src/renderer/src/pages/TeamPage.tsx @@ -105,9 +105,9 @@ export function TeamPage({ snapshot, actions, focus }: PageProps) {
- +

{t("Team")}

diff --git a/src/renderer/src/pages/WorkPage.tsx b/src/renderer/src/pages/WorkPage.tsx index 5e743616..2692b2ae 100644 --- a/src/renderer/src/pages/WorkPage.tsx +++ b/src/renderer/src/pages/WorkPage.tsx @@ -108,9 +108,9 @@ export function WorkPage({ snapshot, actions, navigate, focus }: PageProps) { onPriority={setPriorityFilter} onAssignee={setAssigneeFilter} /> - - - + + +

{t("Work")}

{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.")}

@@ -227,7 +227,7 @@ function WorkFilter({ ( )} > @@ -237,19 +237,19 @@ function WorkFilter({ {priorities.map((option) => ( onPriority(option.id)}> {option.label} - {priority === option.id && } + {priority === option.id && } ))} {t("Assignee")} onAssignee("all")}> {t("All assignees")} - {assignee === "all" && } + {assignee === "all" && } {agents.map((agent) => ( onAssignee(agent.id)}> {agent.name} - {assignee === agent.id && } + {assignee === agent.id && } ))} diff --git a/src/renderer/src/pages/chat/ChatComposer.tsx b/src/renderer/src/pages/chat/ChatComposer.tsx index 115c9b38..5ac407ab 100644 --- a/src/renderer/src/pages/chat/ChatComposer.tsx +++ b/src/renderer/src/pages/chat/ChatComposer.tsx @@ -97,7 +97,7 @@ export function ChatComposer({ setSlashOpen((open) => !open)} disabled={locked} /> {attachments.map((attachment) => ( - + {attachment.name} ))}
{shortcutLabel("Enter")} - +
{send.error &&
{t("Not sent: {reason}", { reason: send.error })}
} diff --git a/src/renderer/src/pages/chat/ChatConversation.tsx b/src/renderer/src/pages/chat/ChatConversation.tsx index d4b44544..5d63c3eb 100644 --- a/src/renderer/src/pages/chat/ChatConversation.tsx +++ b/src/renderer/src/pages/chat/ChatConversation.tsx @@ -297,7 +297,7 @@ function ConversationHeader({ }; return (
- + {space.name} {t("(edited)")}} {!editing && (canModify || (onOpenThread && !deleted)) && ( - {onOpenThread && !deleted && } - {canModify && setEditing(true)} />} - {canModify && } + {onOpenThread && !deleted && } + {canModify && setEditing(true)} />} + {canModify && } )}
@@ -254,14 +254,14 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec placeItems: "center", }} > - +
{file.name} {handoff && ( - {t("AI → AI")} + {t("AI → AI")} )}
diff --git a/src/renderer/src/pages/chat/ChatSidebar.tsx b/src/renderer/src/pages/chat/ChatSidebar.tsx index 9a14ed51..faac08d4 100644 --- a/src/renderer/src/pages/chat/ChatSidebar.tsx +++ b/src/renderer/src/pages/chat/ChatSidebar.tsx @@ -156,7 +156,7 @@ function ConversationRow({ marginTop: 2, }} > - + )} diff --git a/src/renderer/src/pages/chat/ChatTimeline.tsx b/src/renderer/src/pages/chat/ChatTimeline.tsx index d9146e7d..719270e7 100644 --- a/src/renderer/src/pages/chat/ChatTimeline.tsx +++ b/src/renderer/src/pages/chat/ChatTimeline.tsx @@ -148,7 +148,7 @@ function ApprovalInline({ approval, onApprove, onDecline }: { approval: Approval return (
- + {t("Approval needed")} {riskLabel(approval.risk)}
diff --git a/src/renderer/src/pages/connectors/ConnectedList.tsx b/src/renderer/src/pages/connectors/ConnectedList.tsx index 402ca9ea..f0bc989e 100644 --- a/src/renderer/src/pages/connectors/ConnectedList.tsx +++ b/src/renderer/src/pages/connectors/ConnectedList.tsx @@ -96,7 +96,7 @@ export function ConnectedList({ {holders.map((holder) => ( {holder.text} - onRevoke(holder.agentId, account.toolkitSlug)} /> + onRevoke(holder.agentId, account.toolkitSlug)} /> ))} {account.status === "ACTIVE" && grantable.length > 0 && ( diff --git a/src/renderer/src/pages/connectors/ToolkitCatalog.tsx b/src/renderer/src/pages/connectors/ToolkitCatalog.tsx index 1841918b..6e892a37 100644 --- a/src/renderer/src/pages/connectors/ToolkitCatalog.tsx +++ b/src/renderer/src/pages/connectors/ToolkitCatalog.tsx @@ -68,7 +68,7 @@ export function ToolkitCatalog({
{t("Catalog")}
- + - +
); } diff --git a/src/renderer/src/pages/files/FilesGrid.tsx b/src/renderer/src/pages/files/FilesGrid.tsx index 751aa48a..62eec51f 100644 --- a/src/renderer/src/pages/files/FilesGrid.tsx +++ b/src/renderer/src/pages/files/FilesGrid.tsx @@ -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" }} > - +
diff --git a/src/renderer/src/pages/files/FilesTree.tsx b/src/renderer/src/pages/files/FilesTree.tsx index 050ee20a..37c7d290 100644 --- a/src/renderer/src/pages/files/FilesTree.tsx +++ b/src/renderer/src/pages/files/FilesTree.tsx @@ -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 ( diff --git a/src/renderer/src/pages/home/NextUpCard.tsx b/src/renderer/src/pages/home/NextUpCard.tsx index 77f3d80c..4d8df479 100644 --- a/src/renderer/src/pages/home/NextUpCard.tsx +++ b/src/renderer/src/pages/home/NextUpCard.tsx @@ -36,7 +36,7 @@ export function NextUpCard({ {needsYou.length === 0 ? ( <>
- {t("You're all caught up")} + {t("You're all caught up")}
{next?.kind === "next" && (
diff --git a/src/renderer/src/pages/office/AgentDetailCard.tsx b/src/renderer/src/pages/office/AgentDetailCard.tsx index 1b69106b..e1fe6eec 100644 --- a/src/renderer/src/pages/office/AgentDetailCard.tsx +++ b/src/renderer/src/pages/office/AgentDetailCard.tsx @@ -44,7 +44,7 @@ export function AgentDetailCard({ agent, onClose }: { agent: OfficeAgentInput; o
{agent.taskTitle && (
- + @@ -180,7 +180,7 @@ function ProviderRequestRow({
{t("{count} chars", { count: request.outputCharCount })}
diff --git a/src/renderer/src/pages/settings/WorkspacePanel.tsx b/src/renderer/src/pages/settings/WorkspacePanel.tsx index 860e3c04..564f015b 100644 --- a/src/renderer/src/pages/settings/WorkspacePanel.tsx +++ b/src/renderer/src/pages/settings/WorkspacePanel.tsx @@ -67,7 +67,7 @@ export function WorkspacePanel({

{t("General")}

- +
setEditOpen(false)} /> @@ -109,7 +109,7 @@ export function WorkspacePanel({
{t("Workspaces")}
- +
@@ -126,7 +126,7 @@ export function WorkspacePanel({ const form = document.querySelector("form:has([name='localPath'])"); if (form) void chooseDirectory(form); }}> - {t("Browse")} + {t("Browse")}
diff --git a/src/renderer/src/pages/settings/WorkspaceRow.tsx b/src/renderer/src/pages/settings/WorkspaceRow.tsx index 1fff4c02..00e4490f 100644 --- a/src/renderer/src/pages/settings/WorkspaceRow.tsx +++ b/src/renderer/src/pages/settings/WorkspaceRow.tsx @@ -55,7 +55,7 @@ export function WorkspaceRow({ workspace, projects, focused, actions }: { worksp
- +
diff --git a/src/renderer/src/pages/work/TaskDetailDialog.tsx b/src/renderer/src/pages/work/TaskDetailDialog.tsx index ed64a60d..d8b4a160 100644 --- a/src/renderer/src/pages/work/TaskDetailDialog.tsx +++ b/src/renderer/src/pages/work/TaskDetailDialog.tsx @@ -203,15 +203,15 @@ export function TaskDetailDialog({ )} {actionError &&
{actionError}
}
- + {task.blockedReason ? : task.status !== TASK_STATUS.DONE && !blocking && } {task.status === TASK_STATUS.REVIEW && } {task.status === TASK_STATUS.DONE && } {task.status === TASK_STATUS.IN_PROGRESS && } - {assignee && } - {advanceTo && } + {assignee && } + {advanceTo && }
{editing && ( @@ -292,7 +292,7 @@ function SubtasksTab({ subtasks, agents, onOpenTask }: { subtasks: TaskRecord[]; const owner = agents.find((a) => a.id === st.assigneeAgentId); return (
- + {prettyStatus(st.status)} {owner && } diff --git a/src/renderer/src/shared/Icon.tsx b/src/renderer/src/shared/Icon.tsx index f8eb56e8..441ecdde 100644 --- a/src/renderer/src/shared/Icon.tsx +++ b/src/renderer/src/shared/Icon.tsx @@ -1,79 +1,28 @@ -import type { ReactNode } from "react"; +import { PIXEL_ICONS } from "./pixel-icons"; -const PATHS: Record = { - home: <>, - inbox: <>, - team: <>, - work: <>, - chat: , - docs: <>, - approve: <>, - social: <>, - settings: <>, - search: <>, - plus: , - minus: , - chevron: , - chevronDown: , - chevronLeft: , - more: <>, - edit: <>, - filter: , - file: <>, - folder: , - grid: <>, - list: , - upload: <>, - arrowRight: , - arrowUpRight: , - sparkle: , - bot: <>, - user: <>, - check: , - x: , - pin: <>, - attach: , - send: <>, - hash: , - lock: <>, - bell: <>, - cmd: , - refresh: <>, - pause: <>, - shield: , - calendar: <>, - target: <>, -}; +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; + +/** 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 ( ); } diff --git a/src/renderer/src/shared/NavIcon.tsx b/src/renderer/src/shared/NavIcon.tsx index 91bd868b..477d447c 100644 --- a/src/renderer/src/shared/NavIcon.tsx +++ b/src/renderer/src/shared/NavIcon.tsx @@ -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 = { - 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 = { + 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 ; +export function NavIcon({ route }: { route: RouteId }) { + return ; } diff --git a/src/renderer/src/shared/OrgSwitcher.tsx b/src/renderer/src/shared/OrgSwitcher.tsx index efcef9b3..33671883 100644 --- a/src/renderer/src/shared/OrgSwitcher.tsx +++ b/src/renderer/src/shared/OrgSwitcher.tsx @@ -62,7 +62,7 @@ export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete } > {orgInitial(active.name)} {active.name} - + {open && (confirmingOrg ? (
@@ -100,8 +100,8 @@ export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete } > {orgInitial(org.name)} {org.name} - {org.id === activeOrgId && } - + {org.id === activeOrgId && } + ))} @@ -112,7 +112,7 @@ export function OrgSwitcher({ snapshot, activeOrgId, onSelect, onNew, onDelete } className="org-menu-action" onClick={() => { setOpen(false); setConfirmingDelete(null); onNew(); }} > - + {t("New organization")} @@ -137,12 +137,12 @@ function OrgActions({
{org.name}
{!isActive && ( )} diff --git a/src/renderer/src/shared/ProviderOAuthConnect.tsx b/src/renderer/src/shared/ProviderOAuthConnect.tsx index 867d1be1..3e40faa0 100644 --- a/src/renderer/src/shared/ProviderOAuthConnect.tsx +++ b/src/renderer/src/shared/ProviderOAuthConnect.tsx @@ -73,7 +73,7 @@ export function ProviderOAuthConnect({ if (phase === "done") { return (
- + {t("Signed in to {label}", { label })}
diff --git a/src/renderer/src/shared/SearchDialog.tsx b/src/renderer/src/shared/SearchDialog.tsx index 175d966b..045239bf 100644 --- a/src/renderer/src/shared/SearchDialog.tsx +++ b/src/renderer/src/shared/SearchDialog.tsx @@ -120,7 +120,7 @@ export function SearchDialog({
- + diff --git a/src/renderer/src/shared/pixel-icons.ts b/src/renderer/src/shared/pixel-icons.ts new file mode 100644 index 00000000..ec5b5805 --- /dev/null +++ b/src/renderer/src/shared/pixel-icons.ts @@ -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; diff --git a/src/renderer/src/shared/ui.tsx b/src/renderer/src/shared/ui.tsx index 626c5a1b..9a751eae 100644 --- a/src/renderer/src/shared/ui.tsx +++ b/src/renderer/src/shared/ui.tsx @@ -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 ? ( {emoji} ) : 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} > - + ); } diff --git a/src/renderer/src/styles/pixel.css b/src/renderer/src/styles/pixel.css index 1f7303ab..e7066841 100644 --- a/src/renderer/src/styles/pixel.css +++ b/src/renderer/src/styles/pixel.css @@ -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; } diff --git a/tests/e2e/pixel-style.spec.ts b/tests/e2e/pixel-style.spec.ts index 56e1795e..10acec8c 100644 --- a/tests/e2e/pixel-style.spec.ts +++ b/tests/e2e/pixel-style.spec.ts @@ -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(); diff --git a/tests/unit/pixel-icons.test.ts b/tests/unit/pixel-icons.test.ts new file mode 100644 index 00000000..67fc4c66 --- /dev/null +++ b/tests/unit/pixel-icons.test.ts @@ -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); +});