Skip to content

Commit a3538fe

Browse files
authored
Merge pull request #137 from httpsphl/feat/design-primitives
feat(app): shared card, badge, tone and empty-state pieces
2 parents d1378e3 + 83cb0ee commit a3538fe

25 files changed

Lines changed: 650 additions & 491 deletions

‎app/src/features/account/ArchivedSettings.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { Archive, Bot, Crew } from "../../lib/protocol.gen";
1111
import { useApi, useApp } from "../../store/context";
1212
import { Button } from "../../ui/Button";
1313
import { Callout } from "../../ui/Callout";
14+
import { IconBadge } from "../../ui/ChatCard";
1415
import { BotAvatar } from "../bots/BotAvatar";
1516
import { DeleteBot } from "../bots/DeleteBot";
1617
import { DeleteCrew } from "../crews/DeleteCrew";
@@ -73,11 +74,7 @@ export function ArchivedSettings() {
7374
{archive.crews.map((crew) => (
7475
<Row
7576
key={crew.id}
76-
icon={
77-
<span className="grid size-7 place-items-center rounded-full bg-sunken text-muted">
78-
<Users aria-hidden size={14} />
79-
</span>
80-
}
77+
icon={<IconBadge icon={Users} tone="quiet" />}
8178
name={crew.name}
8279
line={s.archivedCrew(botsOf(crew).length, fromNow(crew.archivedAt ?? 0))}
8380
onDelete={() => setAsking({ crew })}

‎app/src/features/bots/BotHeader.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { useT } from "../../i18n";
1313
import type { Bot, Crew } from "../../lib/protocol.gen";
1414
import { useApi, useApp } from "../../store/context";
15+
import { Badge } from "../../ui/Badge";
1516
import { Button } from "../../ui/Button";
1617
import { Menu } from "../../ui/Menu";
1718
import { attempt } from "../../ui/toast";
@@ -91,13 +92,9 @@ export function BotHeader({
9192
<BotStateBadge bot={bot} crewPaused={crew.paused} />
9293
{chief && <ChiefBadge crew={crew} />}
9394
{bot.permissionMode === "bypass_permissions" && (
94-
<span
95-
title={t.chat.mode.badgeHint}
96-
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-danger/10 px-2 py-0.5 font-medium text-danger text-xs"
97-
>
98-
<ShieldOff aria-hidden size={12} />
95+
<Badge tone="danger" icon={ShieldOff} title={t.chat.mode.badgeHint}>
9996
{t.chat.mode.badge}
100-
</span>
97+
</Badge>
10198
)}
10299
<span className="truncate text-muted">{bot.role || words.noRole}</span>
103100
</div>

‎app/src/features/bots/BotStateBadge.tsx‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import {
1010
} from "lucide-react";
1111
import { type Messages, t, useT } from "../../i18n";
1212
import type { Bot, BotState } from "../../lib/protocol.gen";
13+
import { TONES, type Tone as ToneName } from "../../ui/tone";
1314
import { ReadyIcon, type StateIcon, WorkingIcon } from "./StateIcons";
1415

15-
type Tone = "ok" | "work" | "warn" | "danger" | "quiet";
16+
type Tone = Exclude<ToneName, "accent">;
1617

1718
export interface StateView {
1819
label: string;
@@ -57,14 +58,6 @@ export function stateView(
5758
return { ...looks[bot.state], ...words[bot.state] };
5859
}
5960

60-
const toneText: Record<Tone, string> = {
61-
ok: "text-ok",
62-
work: "text-work",
63-
warn: "text-warn",
64-
danger: "text-danger",
65-
quiet: "text-quiet",
66-
};
67-
6861
export function BotStateBadge({
6962
bot,
7063
crewPaused,
@@ -80,7 +73,7 @@ export function BotStateBadge({
8073
return (
8174
<span
8275
title={view.hint}
83-
className={`inline-flex shrink-0 items-center gap-1 font-medium transition-colors duration-300 ${compact ? "text-xs" : "text-sm"} ${toneText[view.tone]}`}
76+
className={`inline-flex shrink-0 items-center gap-1 font-medium transition-colors duration-300 ${compact ? "text-xs" : "text-sm"} ${TONES[view.tone].text}`}
8477
>
8578
<Icon
8679
aria-hidden

‎app/src/features/bots/ChiefBadge.tsx‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { Crown } from "lucide-react";
55
import { useT } from "../../i18n";
66
import type { Bot, Crew } from "../../lib/protocol.gen";
7+
import { Badge } from "../../ui/Badge";
78

89
export const isChief = (bot: Bot, crew: Crew) => crew.leadBotId === bot.id;
910

@@ -18,12 +19,8 @@ export function ChiefBadge({ crew, compact = false }: { crew: Crew; compact?: bo
1819
);
1920
}
2021
return (
21-
<span
22-
title={c.hint(crew.name)}
23-
className="inline-flex shrink-0 items-center gap-1 rounded-full bg-accent/10 px-2 py-0.5 font-medium text-accent-text text-xs"
24-
>
25-
<Crown aria-hidden size={12} />
22+
<Badge tone="accent" icon={Crown} title={c.hint(crew.name)}>
2623
{c.badge}
27-
</span>
24+
</Badge>
2825
);
2926
}

‎app/src/features/browser/BrowserPanel.tsx‎

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import { useT } from "../../i18n";
99
import type { Bot } from "../../lib/protocol.gen";
1010
import { SYSTEM } from "../../lib/system";
1111
import { useApp } from "../../store/context";
12+
import { Badge } from "../../ui/Badge";
1213
import { Button } from "../../ui/Button";
1314
import { Callout } from "../../ui/Callout";
15+
import { EmptyState } from "../../ui/EmptyState";
1416
import { SidePanel } from "../../ui/SidePanel";
1517
import { BotAvatar } from "../bots/BotAvatar";
1618
import { AddressBar } from "./AddressBar";
@@ -81,19 +83,15 @@ export function BrowserPanel({
8183
<h2 className="flex items-center gap-2 font-semibold text-sm">
8284
{t.heading}
8385
{resting ? (
84-
<span
85-
title={t.restingWhy(bot.name)}
86-
className="inline-flex items-center gap-1.5 rounded-full bg-sunken px-2 py-0.5 font-medium text-muted text-xs"
87-
>
88-
<Moon aria-hidden size={11} />
86+
<Badge tone="quiet" icon={Moon} title={t.restingWhy(bot.name)}>
8987
{t.resting}
90-
</span>
88+
</Badge>
9189
) : (
9290
live && (
93-
<span className="inline-flex items-center gap-1.5 rounded-full bg-danger/10 px-2 py-0.5 font-medium text-danger text-xs">
91+
<Badge tone="danger">
9492
<span aria-hidden className="live-dot" />
9593
{t.live}
96-
</span>
94+
</Badge>
9795
)
9896
)}
9997
</h2>
@@ -211,17 +209,6 @@ function Overlay({ busy, title, body }: { busy: boolean; title: string; body: st
211209
function Empty({ bot }: { bot: Bot }) {
212210
const t = useT().browser;
213211
return (
214-
<div className="flex flex-col items-center gap-3 px-6 py-14 text-center">
215-
<span className="relative">
216-
<BotAvatar color={bot.color} size={44} mood="idle" />
217-
<Globe
218-
aria-hidden
219-
size={18}
220-
className="absolute -right-2 -bottom-1 rounded-full bg-panel p-0.5 text-muted"
221-
/>
222-
</span>
223-
<p className="font-semibold text-sm">{t.emptyTitle(bot.name)}</p>
224-
<p className="max-w-80 text-muted text-sm">{t.emptyBody}</p>
225-
</div>
212+
<EmptyState color={bot.color} icon={Globe} title={t.emptyTitle(bot.name)} body={t.emptyBody} />
226213
);
227214
}

‎app/src/features/browser/HelpCard.tsx‎

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
// it" answer the request; once answered it shrinks to one line.
44

55
import { Ban, Check, Hand, TimerOff } from "lucide-react";
6-
import { useContext, useId } from "react";
6+
import { useContext } from "react";
77
import { useT } from "../../i18n";
88
import type { ApprovalItem, Bot } from "../../lib/protocol.gen";
99
import { Button } from "../../ui/Button";
10+
import { NoteInput, RequestCard, SettledLine } from "../../ui/ChatCard";
1011
import { useAnswer } from "../chat/useAnswer";
1112
import { ShowBrowser } from "./showBrowser";
1213

@@ -28,30 +29,15 @@ function read(approval: ApprovalItem): { task: string; site: string | null } {
2829

2930
function Answered({ approval, task }: { approval: ApprovalItem; task: string }) {
3031
const words = useT().browser.help;
31-
const line = "flex min-w-0 items-center gap-2 rounded-lg px-1.5 py-1 text-sm";
3232
switch (approval.status) {
3333
case "allowed":
34-
return (
35-
<p className={line}>
36-
<Check aria-hidden size={14} className="shrink-0 text-ok" />
37-
<span className="truncate font-medium">{words.doneLine(task)}</span>
38-
</p>
39-
);
34+
return <SettledLine icon={Check} tone="ok" text={words.doneLine(task)} />;
4035
case "denied":
4136
return (
42-
<p className={line}>
43-
<Ban aria-hidden size={14} className="shrink-0 text-danger" />
44-
<span className="truncate font-medium">{words.wontLine(task)}</span>
45-
{approval.note && <span className="truncate text-muted text-xs">“{approval.note}”</span>}
46-
</p>
37+
<SettledLine icon={Ban} tone="danger" text={words.wontLine(task)} note={approval.note} />
4738
);
4839
default:
49-
return (
50-
<p className={line}>
51-
<TimerOff aria-hidden size={14} className="shrink-0 text-quiet" />
52-
<span className="truncate font-medium">{words.expired(task)}</span>
53-
</p>
54-
);
40+
return <SettledLine icon={TimerOff} tone="quiet" text={words.expired(task)} />;
5541
}
5642
}
5743

@@ -63,23 +49,18 @@ export function HelpCard({ approval, bot }: { approval: ApprovalItem; bot: Bot }
6349
allow: t.chat.approval.allowFailed,
6450
deny: t.chat.approval.denyFailed,
6551
});
66-
const noteId = useId();
6752
const { task, site } = read(approval);
6853
if (approval.status !== "pending") {
6954
return <Answered approval={approval} task={task} />;
7055
}
7156

7257
return (
73-
<section
74-
aria-label={words.asks(bot.name, task)}
75-
className="my-1 max-w-2xl rounded-2xl border border-accent/40 bg-panel px-4 py-3.5 shadow-sm animate-attention"
58+
<RequestCard
59+
label={words.asks(bot.name, task)}
60+
icon={Hand}
61+
tone="accent"
62+
title={words.needs(bot.name)}
7663
>
77-
<p className="flex items-center gap-2.5 font-semibold text-sm">
78-
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-accent/12 text-accent">
79-
<Hand aria-hidden size={15} />
80-
</span>
81-
{words.needs(bot.name)}
82-
</p>
8364
<p className="mt-2 break-words text-sm" data-selectable>
8465
{task}
8566
</p>
@@ -89,15 +70,11 @@ export function HelpCard({ approval, bot }: { approval: ApprovalItem; bot: Bot }
8970
</p>
9071
)}
9172
<p className="mt-1.5 text-muted text-xs">{words.why}</p>
92-
<label htmlFor={noteId} className="sr-only">
93-
{t.chat.approval.noteLabel(bot.name)}
94-
</label>
95-
<input
96-
id={noteId}
73+
<NoteInput
74+
label={t.chat.approval.noteLabel(bot.name)}
9775
value={note}
98-
onChange={(event) => setNote(event.target.value)}
76+
onChange={setNote}
9977
placeholder={t.chat.approval.notePlaceholder}
100-
className="mt-3 h-9 w-full rounded-xl border border-line-strong bg-canvas px-3 text-sm outline-none placeholder:text-muted focus:border-muted"
10178
/>
10279
<div className="mt-2.5 flex flex-wrap gap-2">
10380
{showBrowser && (
@@ -117,6 +94,6 @@ export function HelpCard({ approval, bot }: { approval: ApprovalItem; bot: Bot }
11794
{words.wontDo}
11895
</Button>
11996
</div>
120-
</section>
97+
</RequestCard>
12198
);
12299
}

‎app/src/features/browser/SiteCard.tsx‎

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
// address, and Allow or Deny. Once answered it shrinks to one line.
33

44
import { Ban, Check, Globe, TimerOff } from "lucide-react";
5-
import { useId } from "react";
65
import { useT } from "../../i18n";
76
import type { ApprovalItem, Bot } from "../../lib/protocol.gen";
87
import { Button } from "../../ui/Button";
8+
import { NoteInput, RequestCard, SettledLine } from "../../ui/ChatCard";
99
import { useAnswer } from "../chat/useAnswer";
1010

1111
/** The request the daemon opens from the browser tools. */
@@ -23,30 +23,15 @@ function addressOf(input: string): string | null {
2323

2424
function Answered({ approval, site }: { approval: ApprovalItem; site: string }) {
2525
const words = useT().browser.site;
26-
const line = "flex min-w-0 items-center gap-2 rounded-lg px-1.5 py-1 text-sm";
2726
switch (approval.status) {
2827
case "allowed":
29-
return (
30-
<p className={line}>
31-
<Check aria-hidden size={14} className="shrink-0 text-ok" />
32-
<span className="truncate font-medium">{words.allowed(site)}</span>
33-
</p>
34-
);
28+
return <SettledLine icon={Check} tone="ok" text={words.allowed(site)} />;
3529
case "denied":
3630
return (
37-
<p className={line}>
38-
<Ban aria-hidden size={14} className="shrink-0 text-danger" />
39-
<span className="shrink-0 font-medium">{words.denied(site)}</span>
40-
{approval.note && <span className="truncate text-muted text-xs">“{approval.note}”</span>}
41-
</p>
31+
<SettledLine icon={Ban} tone="danger" text={words.denied(site)} note={approval.note} />
4232
);
4333
default:
44-
return (
45-
<p className={line}>
46-
<TimerOff aria-hidden size={14} className="shrink-0 text-quiet" />
47-
<span className="font-medium">{words.expired(site)}</span>
48-
</p>
49-
);
34+
return <SettledLine icon={TimerOff} tone="quiet" text={words.expired(site)} />;
5035
}
5136
}
5237

@@ -57,41 +42,34 @@ export function SiteCard({ approval, bot }: { approval: ApprovalItem; bot: Bot }
5742
allow: t.chat.approval.allowFailed,
5843
deny: t.chat.approval.denyFailed,
5944
});
60-
const noteId = useId();
6145
const site = approval.summary;
6246
if (approval.status !== "pending") {
6347
return <Answered approval={approval} site={site} />;
6448
}
6549
const url = addressOf(approval.input);
6650

6751
return (
68-
<section
69-
aria-label={words.asks(bot.name, site)}
70-
className="my-1 max-w-2xl rounded-2xl border border-warn/40 bg-panel px-4 py-3.5 shadow-sm animate-attention"
71-
>
72-
<p className="flex items-center gap-2.5 font-semibold text-sm">
73-
<span className="grid h-7 w-7 shrink-0 place-items-center rounded-full bg-warn/12 text-warn">
74-
<Globe aria-hidden size={15} />
75-
</span>
52+
<RequestCard
53+
label={words.asks(bot.name, site)}
54+
icon={Globe}
55+
tone="warn"
56+
title={
7657
<span>
7758
{words.wants(bot.name)} <span className="font-mono">{site}</span>
7859
</span>
79-
</p>
60+
}
61+
>
8062
{url && (
8163
<p className="mt-2 break-all font-mono text-ink-soft text-xs" data-selectable>
8264
{url}
8365
</p>
8466
)}
8567
<p className="mt-1.5 text-muted text-xs">{words.why}</p>
86-
<label htmlFor={noteId} className="sr-only">
87-
{t.chat.approval.noteLabel(bot.name)}
88-
</label>
89-
<input
90-
id={noteId}
68+
<NoteInput
69+
label={t.chat.approval.noteLabel(bot.name)}
9170
value={note}
92-
onChange={(event) => setNote(event.target.value)}
71+
onChange={setNote}
9372
placeholder={t.chat.approval.notePlaceholder}
94-
className="mt-3 h-9 w-full rounded-xl border border-line-strong bg-canvas px-3 text-sm outline-none placeholder:text-muted focus:border-muted"
9573
/>
9674
<div className="mt-2.5 flex gap-2">
9775
<Button variant="primary" icon={Check} disabled={busy} onClick={() => answer(true)}>
@@ -101,6 +79,6 @@ export function SiteCard({ approval, bot }: { approval: ApprovalItem; bot: Bot }
10179
{t.chat.approval.deny}
10280
</Button>
10381
</div>
104-
</section>
82+
</RequestCard>
10583
);
10684
}

0 commit comments

Comments
 (0)