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:
1 parent
86dbb4efa1
commit
a6036308b7
35 files changed
+150
-182
No files matched your search
@@ -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"
|
||||
},
|
||||
|
||||
Generated
-24
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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" }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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]} />;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user