Skip to content

Commit 25c3078

Browse files
committed
feat(app): a rounder, softer look
Buttons, fields and menu rows get 8 px corners, cards and menus 12 px, the chat's bubbles, cards and dialogs 16 px. Menus and dialogs float on a light themed shadow. The chat is a centered column with the day as a pill, and the open conversation in the sidebar is an inset rounded block instead of a full-width bar. Menus share one popover look and one hook that closes them.
1 parent 0f1272f commit 25c3078

25 files changed

Lines changed: 134 additions & 107 deletions

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

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ import {
1212
Settings,
1313
Sparkles,
1414
} from "lucide-react";
15-
import { type ReactNode, useEffect, useRef, useState } from "react";
15+
import { type ReactNode, useCallback, useRef, useState } from "react";
1616
import { LOCALES, setLocaleChoice, systemLocale, useLocale, useT } from "../../i18n";
1717
import type { ClaudeAccount } from "../../lib/protocol.gen";
1818
import { useApp, useHost } from "../../store/context";
19+
import { POPOVER, POPOVER_ITEM } from "../../ui/surface";
1920
import { attempt } from "../../ui/toast";
21+
import { useDismiss } from "../../ui/useDismiss";
2022
import { SettingsDialog } from "./SettingsDialog";
2123
import { UsageDialog } from "./UsageDialog";
2224

@@ -37,23 +39,11 @@ export function AccountArea() {
3739
const [dialog, setDialog] = useState<"usage" | "settings" | null>(null);
3840
const root = useRef<HTMLDivElement>(null);
3941

40-
useEffect(() => {
41-
if (!open) {
42-
return;
43-
}
44-
const onPointer = (event: PointerEvent) => {
45-
if (!root.current?.contains(event.target as Node)) {
46-
setOpen(false);
47-
}
48-
};
49-
const onKey = (event: KeyboardEvent) => event.key === "Escape" && setOpen(false);
50-
window.addEventListener("pointerdown", onPointer);
51-
window.addEventListener("keydown", onKey);
52-
return () => {
53-
window.removeEventListener("pointerdown", onPointer);
54-
window.removeEventListener("keydown", onKey);
55-
};
56-
}, [open]);
42+
useDismiss(
43+
open,
44+
root,
45+
useCallback(() => setOpen(false), []),
46+
);
5747

5848
const name = account?.name.trim() || "Botloft";
5949
const detail = describe(account?.claude ?? null, signedOut, a);
@@ -78,7 +68,7 @@ export function AccountArea() {
7868
aria-expanded={open}
7969
aria-label={a.open(name)}
8070
onClick={() => setOpen(!open)}
81-
className={`flex w-full items-center gap-2.5 px-2 py-1.5 text-left hover:bg-sunken ${open ? "bg-sunken" : ""}`}
71+
className={`flex w-full items-center gap-2.5 rounded-lg px-2 py-1.5 text-left hover:bg-sunken ${open ? "bg-sunken" : ""}`}
8272
>
8373
<span
8474
aria-hidden
@@ -135,10 +125,10 @@ function AccountMenu({
135125
<div
136126
role="menu"
137127
aria-label={name}
138-
className="absolute right-2 bottom-full left-2 z-30 mb-1 border border-line-strong bg-panel py-1"
128+
className={`absolute right-2 bottom-full left-2 mb-1 ${POPOVER}`}
139129
>
140130
{email && (
141-
<p className="truncate px-3 pt-1 pb-2 text-muted text-xs" data-selectable>
131+
<p className="truncate px-2.5 pt-1.5 pb-2 text-muted text-xs" data-selectable>
142132
{email}
143133
</p>
144134
)}
@@ -161,7 +151,7 @@ function AccountMenu({
161151
<div
162152
role="menu"
163153
aria-label={m.language}
164-
className="absolute bottom-0 left-full z-30 ml-1 min-w-56 border border-line-strong bg-panel py-1"
154+
className={`absolute bottom-0 left-full ml-2 min-w-56 ${POPOVER}`}
165155
>
166156
{[{ id: "system" as const, name: t.shell.language.system(systemName) }, ...LOCALES].map(
167157
(entry) => (
@@ -171,7 +161,7 @@ function AccountMenu({
171161
role="menuitemradio"
172162
aria-checked={choice === entry.id}
173163
onClick={() => setLocaleChoice(entry.id)}
174-
className="flex h-8 w-full items-center gap-2 whitespace-nowrap px-3 text-left text-sm hover:bg-sunken"
164+
className={`h-8 whitespace-nowrap ${POPOVER_ITEM}`}
175165
>
176166
<Check aria-hidden size={14} className={choice === entry.id ? "" : "invisible"} />
177167
{entry.name}
@@ -181,7 +171,7 @@ function AccountMenu({
181171
</div>
182172
)}
183173
</div>
184-
<div className="my-1 border-line border-t" />
174+
<div className="mx-1 my-1 border-line border-t" />
185175
<Item icon={Sparkles} onClick={() => open(RELEASES)}>
186176
{m.whatsNew}
187177
</Item>
@@ -212,7 +202,7 @@ function Item({
212202
aria-haspopup={expanded === undefined ? undefined : "menu"}
213203
aria-expanded={expanded}
214204
onClick={onClick}
215-
className="flex h-8 w-full items-center gap-2 px-3 text-left text-ink text-sm hover:bg-sunken"
205+
className={`h-8 text-ink ${POPOVER_ITEM}`}
216206
>
217207
<Icon aria-hidden size={14} className="text-ink-soft" />
218208
<span className="flex-1">{children}</span>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function BotAvatar({
2525
height={size}
2626
// A little room around the body, so its edge is not cut at the sides.
2727
viewBox={framed ? "0 0 1254 1254" : "-24 -24 1302 1302"}
28-
className={`shrink-0 ${framed ? "rounded-[3px] bg-[#0b0b0b]" : ""}`}
28+
className={`shrink-0 ${framed ? "rounded-lg bg-[#0b0b0b]" : ""}`}
2929
>
3030
<path
3131
d={BODY}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function BotDialog(props: Props) {
113113
aria-label={t.bots.dialog.swatch(swatch)}
114114
aria-pressed={color === swatch}
115115
onClick={() => setColor(swatch)}
116-
className={`relative rounded-[3px] p-0.5 ${color === swatch ? "ring-2 ring-accent" : ""}`}
116+
className={`relative rounded-lg p-0.5 ${color === swatch ? "ring-2 ring-accent" : ""}`}
117117
>
118118
<BotAvatar color={swatch} size={26} />
119119
{color === swatch && (

‎app/src/features/chat/AttachmentList.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function FileCard({
4343
? FileText
4444
: File;
4545
return (
46-
<div className="flex w-64 max-w-full items-center gap-2.5 rounded-md border border-line bg-panel py-2 pr-1.5 pl-2.5">
46+
<div className="flex w-64 max-w-full items-center gap-2.5 rounded-xl border border-line bg-panel py-2 pr-1.5 pl-2.5">
4747
<Icon aria-hidden size={22} className="shrink-0 text-muted" />
4848
<div className="min-w-0 flex-1">
4949
<p className="truncate font-medium text-sm" title={attachment.name}>
@@ -64,7 +64,7 @@ function FileCard({
6464
host.openPath(attachmentFolder(workspace, attachment)),
6565
)
6666
}
67-
className="grid h-7 w-7 shrink-0 place-items-center rounded-[3px] text-muted hover:bg-sunken hover:text-ink"
67+
className="grid h-7 w-7 shrink-0 place-items-center rounded-lg text-muted hover:bg-sunken hover:text-ink"
6868
>
6969
<FolderOpen aria-hidden size={14} />
7070
</button>
@@ -93,7 +93,7 @@ function Thumbnail({
9393
}
9494
if (source.kind === "loading") {
9595
return (
96-
<div className="grid h-32 w-44 place-items-center rounded-md border border-line bg-sunken">
96+
<div className="grid h-32 w-44 place-items-center rounded-xl border border-line bg-sunken">
9797
<LoaderCircle
9898
aria-label={t.chat.attachments.loading(attachment.name)}
9999
className="animate-spin text-muted"
@@ -106,7 +106,7 @@ function Thumbnail({
106106
<button
107107
type="button"
108108
onClick={() => setOpen(true)}
109-
className="overflow-hidden rounded-md border border-line bg-sunken"
109+
className="overflow-hidden rounded-xl border border-line bg-sunken"
110110
title={attachment.name}
111111
>
112112
<img

‎app/src/features/chat/BotRun.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ export function BotRun({
6767
}) {
6868
const first = items[0];
6969
return (
70-
<li className="flex gap-3 pr-10">
70+
<li className="flex gap-3">
7171
<BotAvatar color={bot.color} size={28} />
72-
<div className="flex min-w-0 max-w-[48rem] flex-1 flex-col gap-2">
72+
<div className="flex min-w-0 flex-1 flex-col gap-2">
7373
<div className="flex items-center gap-2 text-sm">
7474
<span className="font-semibold">{bot.name}</span>
7575
{first && (

‎app/src/features/chat/ChatView.tsx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ export function ChatView({ bot, stopped }: { bot: Bot; stopped: boolean }) {
6767
const list: ReactNode[] = rows.map((row, index) => {
6868
if (row.kind === "day") {
6969
return (
70-
<li key={row.key} className="flex items-center gap-3 text-muted text-xs">
71-
<span className="h-px flex-1 bg-line" />
72-
{day(row.at)}
73-
<span className="h-px flex-1 bg-line" />
70+
<li key={row.key} className="flex justify-center">
71+
<span className="rounded-full bg-sunken px-3 py-1 font-medium text-muted text-xs">
72+
{day(row.at)}
73+
</span>
7474
</li>
7575
);
7676
}
@@ -126,7 +126,7 @@ export function ChatView({ bot, stopped }: { bot: Bot; stopped: boolean }) {
126126
element.scrollHeight - element.scrollTop - element.clientHeight < STICKY_PX;
127127
}}
128128
>
129-
<div className="mx-auto flex max-w-5xl flex-col px-5 pt-4 pb-2">
129+
<div className="mx-auto flex max-w-3xl flex-col px-5 pt-5 pb-3">
130130
{chat.error && (
131131
<Callout tone="danger" title={t.chat.view.loadFailed}>
132132
{chat.error}
@@ -153,12 +153,12 @@ export function ChatView({ bot, stopped }: { bot: Bot; stopped: boolean }) {
153153
<p className="max-w-md text-muted text-sm">{t.chat.view.emptyBody}</p>
154154
</div>
155155
)}
156-
<ol aria-label={t.chat.view.messages} className="flex flex-col gap-5">
156+
<ol aria-label={t.chat.view.messages} className="flex flex-col gap-6">
157157
{list}
158158
</ol>
159159
</div>
160160
</div>
161-
<div className="mx-auto w-full max-w-5xl">
161+
<div className="mx-auto w-full max-w-3xl">
162162
<ChatComposer
163163
bot={bot}
164164
files={files}
@@ -169,7 +169,7 @@ export function ChatView({ bot, stopped }: { bot: Bot; stopped: boolean }) {
169169
/>
170170
</div>
171171
{dragging && (
172-
<div className="pointer-events-none absolute inset-3 grid place-items-center rounded-lg border-2 border-accent border-dashed bg-canvas/85">
172+
<div className="pointer-events-none absolute inset-3 grid place-items-center rounded-2xl border-2 border-accent border-dashed bg-canvas/85">
173173
<p className="flex items-center gap-2 font-medium">
174174
<Paperclip aria-hidden size={16} />
175175
{t.chat.view.dropToAttach}

‎app/src/features/chat/InboundRow.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ function Time({ at }: { at: number }) {
2121
function OwnerMessage({ message, bot }: { message: Message; bot: Bot }) {
2222
const delivery = useApp((state) => state.deliveries[message.id]);
2323
return (
24-
<li className="flex flex-col items-end gap-1.5 pl-16">
24+
<li className="flex flex-col items-end gap-1.5 pl-12">
2525
<AttachmentList attachments={message.attachments} workspace={bot.workspace} align="end" />
2626
{message.body && (
2727
<p
28-
className="max-w-[42rem] whitespace-pre-wrap break-words rounded-lg rounded-br-sm bg-sunken px-3.5 py-2 leading-relaxed"
28+
className="max-w-[36rem] whitespace-pre-wrap break-words rounded-2xl rounded-br-md bg-sunken px-4 py-2.5 leading-relaxed"
2929
data-selectable
3030
>
3131
{message.body}
@@ -46,7 +46,7 @@ function TaskTag({ message }: { message: Message }) {
4646
}
4747
const Icon = message.kind === "task" ? ListTodo : Reply;
4848
return (
49-
<span className="inline-flex items-center gap-1 rounded-[3px] border border-line px-1.5 text-muted text-xs">
49+
<span className="inline-flex items-center gap-1 rounded-md border border-line px-1.5 text-muted text-xs">
5050
<Icon aria-hidden size={11} />
5151
{message.kind === "task" ? t.chat.inbound.task : t.chat.inbound.result}
5252
</span>
@@ -60,7 +60,7 @@ function OtherMessage({ message }: { message: Message }) {
6060
const system = message.fromKind === "system";
6161
const name = system ? "Botloft" : (sender?.name ?? t.chat.inbound.archivedBot);
6262
return (
63-
<li className="flex gap-3 pr-16">
63+
<li className="flex gap-3 pr-12">
6464
<BotAvatar
6565
color={system ? "#ffffff" : (sender?.color ?? "#6f6f69")}
6666
size={28}
@@ -74,7 +74,7 @@ function OtherMessage({ message }: { message: Message }) {
7474
<Time at={message.createdAt} />
7575
</div>
7676
<p
77-
className={`mt-1 max-w-[42rem] whitespace-pre-wrap break-words rounded-lg rounded-tl-sm border border-line bg-panel px-3.5 py-2 leading-relaxed ${system ? "text-ink-soft" : ""}`}
77+
className={`mt-1 max-w-[36rem] whitespace-pre-wrap break-words rounded-2xl rounded-tl-md border border-line bg-panel px-4 py-2.5 leading-relaxed ${system ? "text-ink-soft" : ""}`}
7878
data-selectable
7979
>
8080
{message.body}

‎app/src/features/chat/NoticeRow.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { type Messages, useT } from "../../i18n";
66
import type { NoticeItem } from "../../lib/protocol.gen";
77

88
const LOOK = {
9-
info: { icon: Info, tone: "text-work", frame: "border-line" },
10-
warning: { icon: TriangleAlert, tone: "text-warn", frame: "border-warn/50" },
11-
error: { icon: CircleAlert, tone: "text-danger", frame: "border-danger/50" },
9+
info: { icon: Info, tone: "text-work", frame: "border-line bg-panel" },
10+
warning: { icon: TriangleAlert, tone: "text-warn", frame: "border-warn/40 bg-warn/6" },
11+
error: { icon: CircleAlert, tone: "text-danger", frame: "border-danger/40 bg-danger/6" },
1212
} as const;
1313

1414
export function NoticeRow({ notice }: { notice: NoticeItem }) {
@@ -17,7 +17,7 @@ export function NoticeRow({ notice }: { notice: NoticeItem }) {
1717
return (
1818
<li
1919
role={notice.level === "info" ? undefined : "alert"}
20-
className={`mx-auto flex max-w-2xl items-start gap-2 rounded-md border bg-panel px-3 py-2 text-sm ${frame}`}
20+
className={`mx-auto flex max-w-2xl items-start gap-2.5 rounded-xl border px-3.5 py-2.5 text-sm ${frame}`}
2121
>
2222
<Icon aria-hidden size={15} className={`mt-0.5 shrink-0 ${tone}`} />
2323
<p className="whitespace-pre-wrap break-words text-ink-soft" data-selectable>

‎app/src/features/crews/CrewBots.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function CrewBots({ crew, bots, onNewBot }: { crew: Crew; bots: Bot[]; on
1212
const selectBot = useApp((state) => state.selectBot);
1313
if (bots.length === 0) {
1414
return (
15-
<div className="border border-line border-dashed px-6 py-10 text-center">
15+
<div className="rounded-2xl border border-line border-dashed px-6 py-10 text-center">
1616
<p className="font-medium">{t.crews.bots.empty(crew.name)}</p>
1717
<p className="mt-1 text-muted text-sm">{t.crews.bots.emptyHint}</p>
1818
<Button className="mt-4" variant="primary" icon={Plus} onClick={onNewBot}>
@@ -28,7 +28,7 @@ export function CrewBots({ crew, bots, onNewBot }: { crew: Crew; bots: Bot[]; on
2828
<button
2929
type="button"
3030
onClick={() => selectBot(bot.id)}
31-
className="flex h-full w-full flex-col gap-2 border border-line bg-panel p-3 text-left hover:border-line-strong"
31+
className="flex h-full w-full flex-col gap-2 rounded-xl border border-line bg-panel p-3.5 text-left transition-colors hover:border-line-strong"
3232
>
3333
<div className="flex w-full items-center gap-2.5">
3434
<BotAvatar color={bot.color} size={32} />

‎app/src/features/crews/Sidebar.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function Sidebar() {
3232
onClick={() => setCreating(true)}
3333
/>
3434
</div>
35-
<ul className="min-h-0 flex-1 overflow-y-auto pb-3">
35+
<ul className="min-h-0 flex-1 overflow-y-auto px-2 pb-3">
3636
{crews.map((crew) => (
3737
<CrewEntry key={crew.id} crew={crew} />
3838
))}
@@ -44,8 +44,7 @@ export function Sidebar() {
4444
);
4545
}
4646

47-
const row = "relative flex w-full items-center text-left hover:bg-sunken";
48-
const marker = "before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-accent";
47+
const row = "relative flex w-full items-center rounded-lg text-left hover:bg-sunken";
4948

5049
function CrewEntry({ crew }: { crew: Crew }) {
5150
const t = useT();
@@ -59,7 +58,7 @@ function CrewEntry({ crew }: { crew: Crew }) {
5958
type="button"
6059
aria-current={selected ? "page" : undefined}
6160
onClick={() => selectCrew(crew.id)}
62-
className={`${row} h-8 gap-2 pr-3 pl-4 font-semibold text-sm ${selected ? `bg-sunken ${marker}` : ""}`}
61+
className={`${row} h-8 gap-2 px-2.5 font-semibold text-sm ${selected ? "bg-sunken" : ""}`}
6362
>
6463
<span className="min-w-0 flex-1 truncate">{crew.name}</span>
6564
{crew.paused && (
@@ -90,7 +89,7 @@ function Conversation({ bot, crew }: { bot: Bot; crew: Crew }) {
9089
aria-current={selected ? "page" : undefined}
9190
aria-label={`${bot.name}, ${stateView(bot, crew.paused).label}`}
9291
onClick={() => selectBot(bot.id)}
93-
className={`${row} gap-2.5 py-2 pr-3 pl-4 ${selected ? `bg-sunken ${marker}` : ""}`}
92+
className={`${row} gap-2.5 px-2.5 py-2 ${selected ? "bg-sunken" : ""}`}
9493
>
9594
<BotAvatar color={bot.color} size={32} />
9695
<span className="min-w-0 flex-1">

0 commit comments

Comments
 (0)