Skip to content

Commit

The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after.

syntaqxcommitted Parentfd02088Browse files
288 files+1956−16870/288 viewed
+18−15
1111 allows,
1212 } from "@g1t/contracts";
1313
14+import { Card } from "./ui/card";
1415 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
1516
1617 /** A repository role picker, each role with what it is for. */
5051 return (
5152 <>
5253 {/* On a phone: each capability with the least role that has it. */}
53− <ul className="divide-y divide-line rounded-xl border border-line sm:hidden">
54− {CAPABILITIES.map((row) => {
55− const owners = (OWNER_ONLY as readonly string[]).includes(row.capability);
56− return (
57− <li key={row.capability} className="flex items-start justify-between gap-3 px-4 py-2.5 text-sm">
58− <span className="min-w-0 text-fg/90">{row.about}</span>
59− <span className="shrink-0 text-xs whitespace-nowrap text-muted">
60− {owners ? "Owners" : row.role === "admin" ? "Admin" : `${REPO_ROLE_LABELS[row.role]} and up`}
61− </span>
62− </li>
63− );
64− })}
65− </ul>
66− <div className="hidden overflow-x-auto rounded-xl border border-line sm:block">
54+ <Card asChild tone="plain" divided className="sm:hidden">
55+ <ul>
56+ {CAPABILITIES.map((row) => {
57+ const owners = (OWNER_ONLY as readonly string[]).includes(row.capability);
58+ return (
59+ <li key={row.capability} className="flex items-start justify-between gap-3 px-4 py-2.5 text-sm">
60+ <span className="min-w-0 text-fg/90">{row.about}</span>
61+ <span className="shrink-0 text-xs whitespace-nowrap text-muted">
62+ {owners ? "Owners" : row.role === "admin" ? "Admin" : `${REPO_ROLE_LABELS[row.role]} and up`}
63+ </span>
64+ </li>
65+ );
66+ })}
67+ </ul>
68+ </Card>
69+ <Card tone="plain" className="hidden overflow-x-auto sm:block">
6770 <table className="w-full min-w-[34rem] text-sm">
6871 <thead>
6972 <tr className="border-b border-line bg-surface text-left text-xs text-muted">
99102 })}
100103 </tbody>
101104 </table>
102− </div>
105+ </Card>
103106 </>
104107 );
105108 }
+1−1
2222 <Form method="post" className="ml-auto">
2323 <input type="hidden" name="intent" value={intent} />
2424 <input type="hidden" name="id" value={id} />
25− <SubmitButton variant="quiet" pending={pending} match={{ intent, id }}>
25+ <SubmitButton variant="outline" pending={pending} match={{ intent, id }}>
2626 {label}
2727 </SubmitButton>
2828 </Form>
+54−49
99
1010 import type { CommitStatus, Conclusion, LogChunk, WorkflowNote } from "@g1t/contracts";
1111
12+import { Card } from "./ui/card";
1213 import { Hint } from "./ui/hint";
1314 import { type Line, blocks, highlight, searchLog } from "../lib/log-lines";
1415
236237 const sorted = [...notes].sort((a, b) => order[a.severity] - order[b.severity]);
237238 const blocking = notes.filter((n) => n.severity === "unsupported").length;
238239 return (
239− <details className="group rounded-xl border border-line bg-surface" open={blocking > 0}>
240− <summary className="cursor-pointer list-none px-4 py-2.5 text-sm">
241− <span className="font-medium">How this runs on g1t</span>
242− <span className="text-muted">
243− {" · "}
244− {blocking > 0 ? `${blocking} thing${blocking === 1 ? "" : "s"} g1t cannot run yet` : `${notes.length} note${notes.length === 1 ? "" : "s"}`}
245− </span>
246− </summary>
247− <ul className="space-y-2 border-t border-line px-4 py-3 text-sm">
248− {sorted.map((note, index) => (
249− <li key={index} className="flex gap-2">
250− {NOTE_ICON[note.severity]}
251− <span>
252− {note.job && <span className="font-mono text-xs text-muted">{note.job}: </span>}
253− {note.message}
254− </span>
255− </li>
256− ))}
257− </ul>
258− </details>
240+ <Card asChild className="group">
241+ <details open={blocking > 0}>
242+ <summary className="cursor-pointer list-none px-4 py-2.5 text-sm">
243+ <span className="font-medium">How this runs on g1t</span>
244+ <span className="text-muted">
245+ {" · "}
246+ {blocking > 0 ? `${blocking} thing${blocking === 1 ? "" : "s"} g1t cannot run yet` : `${notes.length} note${notes.length === 1 ? "" : "s"}`}
247+ </span>
248+ </summary>
249+ <ul className="space-y-2 border-t border-line px-4 py-3 text-sm">
250+ {sorted.map((note, index) => (
251+ <li key={index} className="flex gap-2">
252+ {NOTE_ICON[note.severity]}
253+ <span>
254+ {note.job && <span className="font-mono text-xs text-muted">{note.job}: </span>}
255+ {note.message}
256+ </span>
257+ </li>
258+ ))}
259+ </ul>
260+ </details>
261+ </Card>
259262 );
260263 }
261264
270273 const failed = statuses.filter((s) => s.state === "failure" || s.state === "error").length;
271274 const pending = statuses.filter((s) => s.state === "pending").length;
272275 return (
273− <section className="rounded-xl border border-line bg-surface p-4">
274− <h3 className="text-sm font-medium">
275− {failed > 0 ? `${failed} workflow${failed === 1 ? "" : "s"} failed` : pending > 0 ? "Workflows running" : "Workflows passed"}
276− </h3>
277− <ul className="mt-3 space-y-2 text-sm">
278− {statuses.map((status) => {
279− const path = status.targetUrl?.replace(/^https:\/\/g1t\.sh/, "") ?? null;
280− const row = (
281− <>
282− <StatusIcon {...standing(status.state)} size={14} />
283− <span className="min-w-0 truncate">{status.context}</span>
284− </>
285− );
286− return (
287− <li key={status.context}>
288− {path ? (
289− <Hint label={status.description}>
290− <Link to={path} className="flex items-center gap-2 hover:text-fg">
291− {row}
292− </Link>
293− </Hint>
294− ) : (
295− <span className="flex items-center gap-2">{row}</span>
296− )}
297− </li>
298− );
299− })}
300− </ul>
301− </section>
276+ <Card asChild className="p-4">
277+ <section>
278+ <h3 className="text-sm font-medium">
279+ {failed > 0 ? `${failed} workflow${failed === 1 ? "" : "s"} failed` : pending > 0 ? "Workflows running" : "Workflows passed"}
280+ </h3>
281+ <ul className="mt-3 space-y-2 text-sm">
282+ {statuses.map((status) => {
283+ const path = status.targetUrl?.replace(/^https:\/\/g1t\.sh/, "") ?? null;
284+ const row = (
285+ <>
286+ <StatusIcon {...standing(status.state)} size={14} />
287+ <span className="min-w-0 truncate">{status.context}</span>
288+ </>
289+ );
290+ return (
291+ <li key={status.context}>
292+ {path ? (
293+ <Hint label={status.description}>
294+ <Link to={path} className="flex items-center gap-2 hover:text-fg">
295+ {row}
296+ </Link>
297+ </Hint>
298+ ) : (
299+ <span className="flex items-center gap-2">{row}</span>
300+ )}
301+ </li>
302+ );
303+ })}
304+ </ul>
305+ </section>
306+ </Card>
302307 );
303308 }
+2−1
1414
1515 import type { AgentMessage, G1tEvent } from "@g1t/contracts";
1616
17−import { Avatar, TimeAgo } from "./ui";
17+import { TimeAgo } from "./ui";
18+import { Avatar } from "./ui/avatar";
1819
1920 /** Whether an actor is g1t itself: its agent or its machinery. */
2021 const isG1t = (actor: string | null): actor is string => actor === "g1t";
+1−1
3434 </p>
3535 {canAdd && (
3636 <fetcher.Form method="post" action={`/${owner}/${repo}/add-ci`} className="mt-3 flex flex-wrap items-center gap-3">
37− <SubmitButton variant="primary" fetcher={fetcher} pending="Opening a pull request…">
37+ <SubmitButton fetcher={fetcher} pending="Opening a pull request…">
3838 <Plus size={14} />
3939 Add CI
4040 </SubmitButton>
+2−1
11 import { useMemo } from "react";
22
33 import { G1tMark } from "./orchestrator";
4−import { Avatar } from "./ui";
4+
55 import { COLUMNS, ROWS, creature } from "../lib/pixel-creature";
6+import { Avatar } from "./ui/avatar";
67
78 /** What an agent's face is drawn from. */
89 export type AgentLike = {
+7−4
66
77 import { cn } from "../lib/cn";
88 import type { NotStarted } from "../lib/delegate";
9−import { Avatar } from "./ui";
9+
10+import { Avatar } from "./ui/avatar";
11+import { Button } from "./ui/button";
1012 import { CONTROL } from "./ui/input";
1113 import { SelectField } from "./ui/select";
1214
153155 <Avatar name="g1t" size={16} />
154156 <span className="truncate">g1t · no model to choose</span>
155157 </span>
156− <button
158+ <Button
157159 type="submit"
158160 disabled={busy || repos.length === 0}
159− className="inline-flex shrink-0 items-center gap-1.5 rounded-md bg-accent px-3.5 py-2 text-sm font-semibold text-bg transition-colors hover:bg-accent-hover disabled:opacity-60"
161+ variant="accent"
162+ className="gap-1.5 font-semibold disabled:opacity-60"
160163 >
161164 {busy ? <LoaderCircle size={14} className="animate-spin" /> : <Sparkles size={14} />}
162165 {busy ? "Starting…" : "Put an agent on it"}
163− </button>
166+ </Button>
164167 </div>
165168 </fetcher.Form>
166169 </div>
+32−29
55
66 import { Idle } from "./agents";
77 import { TimeAgo } from "./ui";
8+import { Card } from "./ui/card";
89 import { Hint } from "./ui/hint";
910
1011 const ROLE: Record<RepoInstructions["files"][number]["role"], string> = {
5354 ) : (
5455 <ul className="mt-3 space-y-3">
5556 {files.map((file) => (
56− <li key={file.path} className="rounded-xl border border-line bg-surface">
57− <details>
58− <summary className="flex cursor-pointer flex-wrap items-center gap-x-3 gap-y-1 px-4 py-3 text-sm">
59− <FileText size={14} className="text-muted" />
60− <span className="font-mono font-medium">{file.path}</span>
61− <span className="rounded-full bg-raised px-2 py-px text-xs text-muted">{ROLE[file.role]}</span>
62− {file.truncated && <span className="text-xs text-warn">Longer than agents are given</span>}
63− <span className="ml-auto flex items-center gap-2 text-xs text-faint">
64− {file.lastChanged && (
65− <>
66− <Hint label={file.lastChanged.message}>
67− <Link to={`${base}/commit/${file.lastChanged.commit}`} className="font-mono hover:text-fg">
68− {file.lastChanged.commit.slice(0, 7)}
69− </Link>
70− </Hint>
71− <span>by {file.lastChanged.author}</span>
72− <TimeAgo at={file.lastChanged.at} />
73− </>
74− )}
75− <Link to={`${base}/blob/${encodeURIComponent(branch)}/${file.path}`} className="text-muted hover:text-fg">
76− Open in code
77− </Link>
78− </span>
79− </summary>
80− <pre className="max-h-96 overflow-auto border-t border-line px-4 py-3 font-mono text-xs whitespace-pre-wrap text-muted">
81− {file.text || "(empty)"}
82− </pre>
83− </details>
84− </li>
57+ <Card asChild key={file.path}>
58+ <li>
59+ <details>
60+ <summary className="flex cursor-pointer flex-wrap items-center gap-x-3 gap-y-1 px-4 py-3 text-sm">
61+ <FileText size={14} className="text-muted" />
62+ <span className="font-mono font-medium">{file.path}</span>
63+ <span className="rounded-full bg-raised px-2 py-px text-xs text-muted">{ROLE[file.role]}</span>
64+ {file.truncated && <span className="text-xs text-warn">Longer than agents are given</span>}
65+ <span className="ml-auto flex items-center gap-2 text-xs text-faint">
66+ {file.lastChanged && (
67+ <>
68+ <Hint label={file.lastChanged.message}>
69+ <Link to={`${base}/commit/${file.lastChanged.commit}`} className="font-mono hover:text-fg">
70+ {file.lastChanged.commit.slice(0, 7)}
71+ </Link>
72+ </Hint>
73+ <span>by {file.lastChanged.author}</span>
74+ <TimeAgo at={file.lastChanged.at} />
75+ </>
76+ )}
77+ <Link to={`${base}/blob/${encodeURIComponent(branch)}/${file.path}`} className="text-muted hover:text-fg">
78+ Open in code
79+ </Link>
80+ </span>
81+ </summary>
82+ <pre className="max-h-96 overflow-auto border-t border-line px-4 py-3 font-mono text-xs whitespace-pre-wrap text-muted">
83+ {file.text || "(empty)"}
84+ </pre>
85+ </details>
86+ </li>
87+ </Card>
8588 ))}
8689 </ul>
8790 )}
+68−51
33
44 import type { AgentRouting, ModelTier, SubagentDef } from "@g1t/contracts";
55
6+import { Button } from "./ui/button";
7+import { Card } from "./ui/card";
68 import { Hint } from "./ui/hint";
79 import { Input } from "./ui/input";
810 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
158160 />
159161 {!locked && (
160162 <Hint label="Remove">
161− <button
163+ <Button
162164 type="button"
163165 aria-label={`Remove responsibility ${index + 1}`}
164166 onClick={() => setItems((now) => (now.length > 1 ? now.filter((_, i) => i !== index) : [""]))}
165− className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
167+ variant="ghost"
168+ size="icon-sm"
169+ className="text-faint"
166170 >
167171 <X size={14} />
168− </button>
172+ </Button>
169173 </Hint>
170174 )}
171175 </li>
172176 ))}
173177 </ul>
174178 {!locked && items.length < MAX_RESPONSIBILITIES && (
175− <button
179+ <Button
176180 type="button"
177181 onClick={() => setItems((now) => [...now, ""])}
178− className="mt-2 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[0.8125rem] text-muted hover:bg-raised hover:text-fg"
182+ variant="ghost"
183+ size="xs"
184+ className="mt-2 gap-1.5 text-[0.8125rem] font-normal"
179185 >
180186 <Plus size={14} />
181187 Add a responsibility
182− </button>
188+ </Button>
183189 )}
184190 </div>
185191 </>
211217 <input type="hidden" name="subagents" value={JSON.stringify(list)} />
212218 <p className="mb-3 inline-flex items-center gap-1.5 rounded-full border border-line px-2.5 py-0.5 text-xs text-muted">Used inside tasks: coming soon</p>
213219 {list.length === 0 ? (
214− <p className="rounded-lg border border-dashed border-line px-4 py-5 text-sm text-muted">None yet. A subagent is help it calls on for one kind of work, such as finding flaky tests.</p>
220+ <Card asChild tone="plain" radius="lg" className="border-dashed px-4 py-5 text-sm text-muted">
221+ <p>None yet. A subagent is help it calls on for one kind of work, such as finding flaky tests.</p>
222+ </Card>
215223 ) : (
216− <ul className="divide-y divide-line rounded-lg border border-line">
217− {list.map((sub, index) => (
218− <li key={sub.name} className="flex items-center gap-3 px-3.5 py-2.5">
219− <span className="min-w-0 grow">
220− <span className="block font-mono text-sm text-fg">{sub.name}</span>
221− <span className="block truncate text-xs text-muted">{sub.description || "No description"}</span>
222− </span>
223− <span className="hidden text-xs text-faint sm:inline">
224− {sub.routing.floor ?? "any"} to {sub.routing.ceiling ?? "any"} · {sub.max_parallel} at once
225− </span>
226− <Hint label="Edit">
227− <button
228− type="button"
229− aria-label={`Edit ${sub.name}`}
230− onClick={() => {
231− setError(null);
232− setEditing({ index, draft: sub });
233− }}
234− className="flex size-8 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg"
235− >
236− <Pencil size={14} />
237− </button>
238− </Hint>
239− <Hint label="Remove">
240− <button
241− type="button"
242− aria-label={`Remove ${sub.name}`}
243− onClick={() => setList((now) => now.filter((_, i) => i !== index))}
244− className="flex size-8 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-danger"
245− >
246− <Trash2 size={14} />
247− </button>
248− </Hint>
249− </li>
250− ))}
251− </ul>
224+ <Card asChild tone="plain" radius="lg" divided>
225+ <ul>
226+ {list.map((sub, index) => (
227+ <li key={sub.name} className="flex items-center gap-3 px-3.5 py-2.5">
228+ <span className="min-w-0 grow">
229+ <span className="block font-mono text-sm text-fg">{sub.name}</span>
230+ <span className="block truncate text-xs text-muted">{sub.description || "No description"}</span>
231+ </span>
232+ <span className="hidden text-xs text-faint sm:inline">
233+ {sub.routing.floor ?? "any"} to {sub.routing.ceiling ?? "any"} · {sub.max_parallel} at once
234+ </span>
235+ <Hint label="Edit">
236+ <Button
237+ type="button"
238+ aria-label={`Edit ${sub.name}`}
239+ onClick={() => {
240+ setError(null);
241+ setEditing({ index, draft: sub });
242+ }}
243+ variant="ghost"
244+ size="icon-sm"
245+ className="text-faint"
246+ >
247+ <Pencil size={14} />
248+ </Button>
249+ </Hint>
250+ <Hint label="Remove">
251+ <Button
252+ type="button"
253+ aria-label={`Remove ${sub.name}`}
254+ onClick={() => setList((now) => now.filter((_, i) => i !== index))}
255+ variant="ghost"
256+ size="icon-sm"
257+ className="text-faint hover:text-danger"
258+ >
259+ <Trash2 size={14} />
260+ </Button>
261+ </Hint>
262+ </li>
263+ ))}
264+ </ul>
265+ </Card>
252266 )}
253− <button
267+ <Button
254268 type="button"
255269 onClick={() => {
256270 setError(null);
257271 setEditing({ index: null, draft: { ...BLANK_SUBAGENT, routing: clampRouting(BLANK_SUBAGENT.routing, routing) } });
258272 }}
259− className="mt-2 inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-[0.8125rem] text-muted hover:bg-raised hover:text-fg"
273+ variant="ghost"
274+ size="xs"
275+ className="mt-2 gap-1.5 text-[0.8125rem] font-normal"
260276 >
261277 <Plus size={14} />
262278 Add a subagent
263− </button>
279+ </Button>
264280 <Sheet open={editing != null} onOpenChange={(open) => !open && setEditing(null)}>
265281 <SheetContent side="right" className="w-[28rem] max-w-[100vw]">
266282 {editing && (
342358 </div>
343359 <div className="flex items-center gap-3 border-t border-line px-5 py-3">
344360 {error && <p className="text-sm text-danger">{error}</p>}
345− <button
361+ <Button
346362 type="button"
347363 onClick={() => setEditing(null)}
348− className="ml-auto h-9 rounded-md border border-line px-3.5 text-sm font-medium text-fg/90 hover:border-line-strong hover:bg-surface"
364+ variant="outline"
365+ className="ml-auto text-fg/90"
349366 >
350367 Cancel
351− </button>
352− <button type="button" onClick={save} className="h-9 rounded-md bg-accent px-3.5 text-sm font-medium text-bg hover:bg-accent-hover">
368+ </Button>
369+ <Button type="button" onClick={save} variant="accent">
353370 {editing.index == null ? "Add" : "Done"}
354− </button>
371+ </Button>
355372 </div>
356373 </div>
357374 )}
+9−5
1313 } from "../lib/agent-setup";
1414 import { useAddresses } from "../lib/addresses";
1515 import { cn } from "../lib/cn";
16+import { Button } from "./ui/button";
17+import { Card } from "./ui/card";
1618
1719 /** The agent this browser chose last; every toggle on the page shares it. */
1820 export function useAgentChoice(): [AgentId, (id: AgentId) => void] {
5456
5557 return (
5658 <div className={className}>
57− <div className="@container overflow-hidden rounded-lg border border-line bg-surface">
59+ <Card radius="lg" className="@container overflow-hidden">
5860 <div
5961 role="tablist"
6062 aria-label="Coding agent"
99101 : agent.code}
100102 </code>
101103 </pre>
102− <button
104+ <Button
103105 type="button"
104106 aria-label={copied ? "Copied" : `Copy the ${agent.label} ${agent.file ? "config" : "command"}`}
105107 onClick={copy}
106− className="absolute top-1.5 right-1.5 rounded-md p-1.5 text-faint transition-colors hover:bg-raised hover:text-fg"
108+ variant="ghost"
109+ size="inline"
110+ className="absolute top-1.5 right-1.5 p-1.5 text-faint"
107111 >
108112 {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
109− </button>
113+ </Button>
110114 </div>
111− </div>
115+ </Card>
112116 {hint && (
113117 <p className="mt-2 text-xs leading-5 text-muted">
114118 {agent.then.split("`").map((part, index) =>
+37−35
77 import { Link } from "react-router";
88
99 import { PixelCreature } from "./agent-avatar";
10+import { ButtonLink } from "./ui";
11+import { buttonVariants } from "./ui/button";
12+import { Card } from "./ui/card";
1013 import { Hint } from "./ui/hint";
1114 import { type TemplateListing, startPath, templatePath } from "../lib/agent-templates";
1215 import { cn } from "../lib/cn";
1316
14−const SMALL = "inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-md px-3 text-[0.8125rem] font-medium transition-colors";
15−const PRIMARY = `${SMALL} bg-fg text-bg hover:bg-fg-hover`;
16−const QUIET = `${SMALL} border border-line text-fg/85 hover:border-line-strong hover:bg-raised hover:text-fg`;
1717
1818 /** Starting an agent from a template: the new-agent form with it chosen, for owners; a word on who can, for everyone else. */
1919 export function StartAction({ listing, slug, owner, className }: { listing: TemplateListing; slug: string; owner: boolean; className?: string }) {
2020 if (!owner) {
2121 return (
2222 <Hint label="This workspace's owners have turned off personal agents. Ask an owner in chat if you want an agent like this.">
23− <span tabIndex={0} className={cn(SMALL, "border border-line text-muted outline-none focus-visible:ring-2 focus-visible:ring-accent", className)}>
23+ <span tabIndex={0} className={cn(buttonVariants({ variant: "outline", size: "sm" }), "text-muted hover:border-line hover:bg-transparent hover:text-muted", className)}>
2424 Owners start agents
2525 </span>
2626 </Hint>
2727 );
2828 }
2929 return (
30− <Link to={startPath(slug, listing.template.id)} className={cn(listing.agents.length > 0 ? QUIET : PRIMARY, className)}>
30+ <ButtonLink to={startPath(slug, listing.template.id)} variant={listing.agents.length > 0 ? "outline" : "default"} size="sm" className={className}>
3131 <Plus size={14} />
3232 {listing.agents.length > 0 ? "Start another" : "Start from this"}
33− </Link>
33+ </ButtonLink>
3434 );
3535 }
3636
3838 export function TemplateCard({ listing, slug, owner }: { listing: TemplateListing; slug: string; owner: boolean }) {
3939 const { template, agents } = listing;
4040 return (
41− <article className="relative flex flex-col rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong">
42− <div className="flex items-start gap-3">
43− <PixelCreature seed={template.handle} size={44} />
44− <div className="min-w-0 grow">
45− <h3 className="truncate text-sm font-semibold">
46− <Link to={templatePath(slug, template.id)} prefetch="intent" className="outline-none after:absolute after:inset-0 after:rounded-xl focus-visible:after:ring-2 focus-visible:after:ring-accent">
47− {template.title}
48− </Link>
49− </h3>
50− <p className="truncate text-xs text-muted">
51− Suggests {template.display_name}
52− </p>
41+ <Card asChild className="relative flex flex-col p-4 transition-colors hover:border-line-strong">
42+ <article>
43+ <div className="flex items-start gap-3">
44+ <PixelCreature seed={template.handle} size={44} />
45+ <div className="min-w-0 grow">
46+ <h3 className="truncate text-sm font-semibold">
47+ <Link to={templatePath(slug, template.id)} prefetch="intent" className="outline-none after:absolute after:inset-0 after:rounded-xl focus-visible:after:ring-2 focus-visible:after:ring-accent">
48+ {template.title}
49+ </Link>
50+ </h3>
51+ <p className="truncate text-xs text-muted">
52+ Suggests {template.display_name}
53+ </p>
54+ </div>
55+ </div>
56+ <ul className="mt-3 grow space-y-1">
57+ {template.responsibilities.slice(0, 3).map((duty) => (
58+ <li key={duty} className="flex gap-2 text-[0.8125rem] leading-snug text-muted">
59+ <span className="mt-[0.45rem] size-1 shrink-0 rounded-full bg-line-strong" aria-hidden="true" />
60+ <span className="line-clamp-1">{duty}</span>
61+ </li>
62+ ))}
63+ </ul>
64+ <div className="relative mt-4 flex items-center justify-between gap-3 border-t border-line pt-3">
65+ <span className="min-w-0 truncate text-xs text-faint">
66+ {agents.length === 0 ? "No agent started from it" : agents.length === 1 ? `@${agents[0]!.handle} started from it` : `${agents.length} agents started from it`}
67+ </span>
68+ {/* A member reads on; who may start one is said on the template's page. */}
69+ {owner && <StartAction listing={listing} slug={slug} owner={owner} />}
5370 </div>
54− </div>
55− <ul className="mt-3 grow space-y-1">
56− {template.responsibilities.slice(0, 3).map((duty) => (
57− <li key={duty} className="flex gap-2 text-[0.8125rem] leading-snug text-muted">
58− <span className="mt-[0.45rem] size-1 shrink-0 rounded-full bg-line-strong" aria-hidden="true" />
59− <span className="line-clamp-1">{duty}</span>
60− </li>
61− ))}
62− </ul>
63− <div className="relative mt-4 flex items-center justify-between gap-3 border-t border-line pt-3">
64− <span className="min-w-0 truncate text-xs text-faint">
65− {agents.length === 0 ? "No agent started from it" : agents.length === 1 ? `@${agents[0]!.handle} started from it` : `${agents.length} agents started from it`}
66− </span>
67− {/* A member reads on; who may start one is said on the template's page. */}
68− {owner && <StartAction listing={listing} slug={slug} owner={owner} />}
69− </div>
70− </article>
71+ </article>
72+ </Card>
7173 );
7274 }
+19−16
77 import { type AgentLike, AgentAvatar, PixelCreature } from "./agent-avatar";
88 import { StatusDot, statusLabel } from "./chat/marks";
99 import { isOrchestrator } from "./orchestrator";
10+import { Button } from "./ui/button";
11+import { Card } from "./ui/card";
1012 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
1113 import { Skeleton } from "./ui/skeleton";
1214 import { AUTONOMY, type AgentDraft, PRESETS, TIER_LABELS, cleanHandle, dollarsField } from "../lib/agent-form";
529531 />
530532 {!locked && (
531533 <Hint label="Another name">
532− <button
534+ <Button
533535 type="button"
534536 aria-label="Another name"
535537 onClick={shuffle}
536− className="flex size-[38px] shrink-0 items-center justify-center rounded-md border border-line text-muted transition-colors hover:border-line-strong hover:bg-raised hover:text-fg active:rotate-12"
538+ variant="outline" size="icon"
539+ className="size-[38px] text-muted hover:bg-raised active:rotate-12"
537540 >
538541 <Dices size={17} />
539− </button>
542+ </Button>
540543 </Hint>
541544 )}
542545 </div>
616619 {spaces.length ? (
617620 <div className="grid gap-1.5 sm:grid-cols-2">
618621 {spaces.map((space) => (
619− <label
620− key={space.id}
621− className="flex min-h-10 cursor-pointer items-center gap-2.5 rounded-lg border border-line bg-surface px-3 py-2 text-sm has-[[data-state=checked]]:border-accent/50 has-[[data-state=checked]]:bg-accent/5"
622− >
623− <Checkbox name="reading" value={space.id} defaultChecked={(draft.reading ?? []).includes(space.id)} />
624− <span className="min-w-0 grow truncate">{space.name}</span>
625− <span className="shrink-0 text-xs text-faint capitalize">{space.kind}</span>
626− </label>
622+ <Card asChild key={space.id} radius="lg" className="flex min-h-10 cursor-pointer items-center gap-2.5 px-3 py-2 text-sm has-[[data-state=checked]]:border-accent/50 has-[[data-state=checked]]:bg-accent/5">
623+ <label>
624+ <Checkbox name="reading" value={space.id} defaultChecked={(draft.reading ?? []).includes(space.id)} />
625+ <span className="min-w-0 grow truncate">{space.name}</span>
626+ <span className="shrink-0 text-xs text-faint capitalize">{space.kind}</span>
627+ </label>
628+ </Card>
627629 ))}
628630 </div>
629631 ) : (
728730 </Select>
729731 </div>
730732 </div>
731− <div className="rounded-lg border border-line">
733+ <Card tone="plain" radius="lg">
732734 <button
733735 type="button"
734736 onClick={() => setAdvanced(!advanced)}
745747 </Label>
746748 <input id="pinned" name="pinned" defaultValue={draft.routing.pinned ?? ""} placeholder="provider/model" className={`${FIELD} font-mono`} autoComplete="off" data-1p-ignore />
747749 </div>
748− </div>
750+ </Card>
749751 </FormSection>
750752
751753 <FormSection title="Budget" about="Checked before work starts and enforced while it runs. At 80% of the month it tells you; at 100% it takes no new tasks.">
789791
790792 <div className="sticky bottom-(--tabbar-h) in-data-[keyboard=open]:bottom-0 z-10 -mx-4 flex items-center justify-end gap-3 border-t border-line bg-bg/90 px-4 py-3 backdrop-blur sm:-mx-8 sm:px-8">
791793 {Object.keys(e).length > 0 && <p className="mr-auto text-sm text-danger">Check the fields marked above.</p>}
792− <button
794+ <Button
793795 type="submit"
794796 disabled={busy}
795− className="inline-flex h-9 items-center gap-2 rounded-md bg-accent px-4 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-60"
797+ variant="accent"
798+ className="px-4 disabled:opacity-60"
796799 >
797800 <Sparkles size={15} />
798801 {busy ? "Saving…" : submit}
799− </button>
802+ </Button>
800803 </div>
801804 </Form>
802805 );
+14−11
2323
2424 import { RunAudit } from "./audit";
2525 import { STAGE_LABEL, StageDots } from "./lifecycle";
26−import { Avatar, SubmitButton, TimeAgo } from "./ui";
26+import { SubmitButton, TimeAgo } from "./ui";
2727 import { Hint } from "./ui/hint";
2828 import {
2929 AlertDialog,
3636 AlertDialogTitle,
3737 AlertDialogTrigger,
3838 } from "./ui/alert-dialog";
39+import { Avatar } from "./ui/avatar";
40+import { Badge } from "./ui/badge";
41+import { Card } from "./ui/card";
3942 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
4043 import { shownStep } from "../lib/agent-step";
4144 import { inlinePlain } from "../lib/inline-markdown";
199202 <div className="flex justify-end">
200203 <SubmitButton
201204 fetcher={fetcher}
202− pending="Sending…"
203− className="inline-flex items-center gap-2 rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-fg-hover disabled:opacity-50"
204− >
205+ pending="Sending…">
205206 Send
206207 </SubmitButton>
207208 </div>
484485 if (!run) return null;
485486 return (
486487 <Hint label={run.step ? inlinePlain(shownStep(run.step)) : undefined}>
487− <span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-merged/40 bg-accent/10 px-2 py-0.5 text-xs text-merged">
488+ <Badge tone="merged" size="md">
488489 <Loader2 size={11} className="animate-spin" />
489490 {RUN_KIND_LABEL[run.kind]}
490− </span>
491+ </Badge>
491492 </Hint>
492493 );
493494 }
507508 /** Nothing running, said plainly. */
508509 export function Idle({ children }: { children: ReactNode }) {
509510 return (
510− <p className="flex items-start gap-2 rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">
511− <CircleSlash size={15} className="mt-0.5 shrink-0 text-faint" />
512− {/* One run of text: a link inside it stays in the sentence. */}
513− <span className="min-w-0">{children}</span>
514− </p>
511+ <Card asChild tone="plain" className="flex items-start gap-2 border-dashed px-4 py-6 text-sm text-muted">
512+ <p>
513+ <CircleSlash size={15} className="mt-0.5 shrink-0 text-faint" />
514+ {/* One run of text: a link inside it stays in the sentence. */}
515+ <span className="min-w-0">{children}</span>
516+ </p>
517+ </Card>
515518 );
516519 }
517520
+7−6
66 import { ABILITY_LEVEL_LABELS } from "@g1t/contracts/abilities";
77
88 import { cn } from "../../lib/cn";
9−import { Button, ButtonLink, SubmitButton } from "../ui";
9+import { ButtonLink, SubmitButton } from "../ui";
10+import { Button } from "../ui/button";
1011 import { Badge } from "../ui/badge";
1112 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
1213 import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field";
168169 )}
169170 </div>
170171 <div className="flex shrink-0 flex-wrap items-center gap-2">
171− {integration && !source.connected && (isOwner ? <ButtonLink to={source.href ?? askHref} variant="quiet">Connect</ButtonLink> : <ButtonLink to={askHref} variant="quiet">Ask an owner</ButtonLink>)}
172+ {integration && !source.connected && (isOwner ? <ButtonLink to={source.href ?? askHref} variant="outline">Connect</ButtonLink> : <ButtonLink to={askHref} variant="outline">Ask an owner</ButtonLink>)}
172173 {integration && source.connected && source.href && (
173174 <Link to={source.href} className="text-xs text-muted underline-offset-2 hover:underline">
174175 Manage
180181 <input type="hidden" name="intent" value="mcp_refresh" />
181182 <input type="hidden" name="server" value={source.id} />
182183 <Hint label="List its tools again">
183− <SubmitButton variant="quiet" icon pending="" className="inline-flex size-8 items-center justify-center rounded-md border border-line text-muted hover:border-line-strong hover:text-fg" fetcher={fetcher} match={{ intent: "mcp_refresh", server: source.id }} aria-label={`List ${source.name}'s tools again`}>
184+ <SubmitButton variant="outline" icon pending="" className="inline-flex size-8 items-center justify-center rounded-md border border-line text-muted hover:border-line-strong hover:text-fg" fetcher={fetcher} match={{ intent: "mcp_refresh", server: source.id }} aria-label={`List ${source.name}'s tools again`}>
184185 <RefreshCw size={14} />
185186 </SubmitButton>
186187 </Hint>
189190 <input type="hidden" name="intent" value="mcp_remove" />
190191 <input type="hidden" name="server" value={source.id} />
191192 <Hint label="Remove this server">
192− <SubmitButton variant="danger" icon pending="" className="inline-flex size-8 items-center justify-center rounded-md border border-danger/40 text-danger hover:border-danger hover:bg-danger/10" fetcher={fetcher} match={{ intent: "mcp_remove", server: source.id }} aria-label={`Remove ${source.name}`}>
193+ <SubmitButton variant="destructive" icon pending="" className="inline-flex size-8 items-center justify-center rounded-md border border-danger/40 text-danger hover:border-danger hover:bg-danger/10" fetcher={fetcher} match={{ intent: "mcp_remove", server: source.id }} aria-label={`Remove ${source.name}`}>
193194 <Trash2 size={14} />
194195 </SubmitButton>
195196 </Hint>
218219 return (
219220 <Dialog open={open} onOpenChange={setOpen}>
220221 <DialogTrigger asChild>
221− <Button type="button" variant="quiet" disabled={count >= max}>
222+ <Button type="button" variant="outline" disabled={count >= max}>
222223 <Plus size={14} />
223224 Add a server
224225 </Button>
244245 </Field>
245246 <FieldError>{error}</FieldError>
246247 <DialogFooter>
247− <Button type="button" variant="quiet" onClick={() => setOpen(false)}>
248+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
248249 Cancel
249250 </Button>
250251 <SubmitButton variant="accent" fetcher={fetcher} match={{ intent: "mcp_add" }} busy={busy} pending="Listing its tools…">
+142−118
1313 import { TeamPicker } from "../agent-role";
1414 import { AgentAvatar, PixelCreature } from "../agent-avatar";
1515 import { Markdown } from "../markdown";
16+import { Alert } from "../ui/alert";
1617 import { Badge } from "../ui/badge";
18+import { Button } from "../ui/button";
19+import { Card } from "../ui/card";
1720 import { Checkbox } from "../ui/checkbox";
1821 import { Hint } from "../ui/hint";
1922 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
2427
2528 const FIELD =
2629 "w-full rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim";
27−const PRIMARY = "inline-flex h-9 items-center justify-center gap-2 rounded-md bg-accent px-4 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-60";
28−const QUIET =
29− "inline-flex h-9 items-center justify-center gap-2 rounded-md border border-line px-3.5 text-sm font-medium text-fg/85 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg disabled:opacity-60";
30−
3130 /** What the builder's actions answer. */
3231 export type BuilderAnswer =
3332 | { intent: "draft"; ok: true; proposal: AgentProposal }
130129 )}
131130 <span className="grow" />
132131 <span className="hidden text-xs text-faint sm:inline">{busy ? "Drafting…" : "Ctrl+Enter"}</span>
133− <button type="submit" disabled={!mayCreate || busy || text.trim().length < 3} className={cn(PRIMARY, "h-8 px-3")}>
132+ <Button type="submit" disabled={!mayCreate || busy || text.trim().length < 3} variant="accent" size="sm">
134133 <Sparkles size={14} className={busy ? "animate-pulse motion-reduce:animate-none" : ""} />
135134 {busy ? "Drafting…" : "Draft it"}
136− </button>
135+ </Button>
137136 </div>
138137 </div>
139138 </fetcher.Form>
140− {error && <p className="mt-3 rounded-lg border border-danger/40 bg-danger/10 px-4 py-2.5 text-sm text-danger">{error}</p>}
139+ {error && (
140+ <Alert asChild className="mt-3 px-4 py-2.5">
141+ <p>{error}</p>
142+ </Alert>
143+ )}
141144 <p className="mt-3 text-xs leading-relaxed text-muted">
142145 {scope === "personal"
143146 ? "Only you can talk to it, in your direct message with it. It spends from your own budget: $20 a month and $2 a session to start."
264267 <div className="relative shrink-0">
265268 <AgentAvatar agent={{ handle: value.handle, avatar_seed: seed }} size={56} />
266269 <Hint label="Another face">
267− <button
270+ <Button
268271 type="button"
269272 aria-label="Another face"
270273 onClick={() => set({ avatar_seed: `${value.handle || "agent"}-${Math.random().toString(36).slice(2, 7)}` })}
271− className="absolute -right-1.5 -bottom-1.5 flex size-6 items-center justify-center rounded-full border border-line bg-bg text-muted shadow-sm transition-colors hover:text-fg"
274+ variant="outline" size="icon"
275+ className="absolute -right-1.5 -bottom-1.5 size-6 rounded-full bg-bg text-muted shadow-sm"
272276 >
273277 <Dices size={13} />
274− </button>
278+ </Button>
275279 </Hint>
276280 </div>
277281 <div className="grid min-w-0 grow gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,0.8fr)]">
281285 <input id="b-name" value={value.display_name} onChange={(e) => rename(e.target.value)} className={FIELD} autoComplete="off" data-1p-ignore required />
282286 {ideas.length > 1 && (
283287 <Hint label="Another name">
284− <button
288+ <Button
285289 type="button"
286290 aria-label="Another name"
287291 onClick={() => {
289293 setIdea(next);
290294 rename(ideas[next]!);
291295 }}
292− className="flex size-9.5 shrink-0 items-center justify-center rounded-md border border-line text-muted transition-colors hover:border-line-strong hover:bg-raised hover:text-fg"
296+ variant="outline" size="icon"
297+ className="size-9.5 text-muted hover:bg-raised"
293298 >
294299 <Shuffle size={15} />
295− </button>
300+ </Button>
296301 </Hint>
297302 )}
298303 </div>
377382 {FOUNDATIONAL_SKILLS.map((skill) => {
378383 const on = !off.has(skill.id);
379384 return (
380− <label
381− key={skill.id}
382− className="flex min-h-10 cursor-pointer items-center gap-2.5 rounded-lg border border-line bg-bg px-3 py-2 text-sm has-[[data-state=checked]]:border-accent/50 has-[[data-state=checked]]:bg-accent/5"
383− >
384− <Checkbox
385− checked={on}
386− onCheckedChange={() => {
387− const next = new Set(off);
388− if (on) next.add(skill.id);
389− else next.delete(skill.id);
390− set({ skills_off: FOUNDATIONAL_SKILLS.map((s) => s.id).filter((id) => next.has(id)) });
391− }}
392− />
393− <span className="min-w-0 grow truncate">{skill.name}</span>
394− {suggested.has(skill.id) && suggested.size < FOUNDATIONAL_SKILLS.length && (
395− <Badge tone="accent" className="shrink-0">
396− Suggested
397− </Badge>
398− )}
399− </label>
385+ <Card asChild key={skill.id} tone="bg" radius="lg" className="flex min-h-10 cursor-pointer items-center gap-2.5 px-3 py-2 text-sm has-[[data-state=checked]]:border-accent/50 has-[[data-state=checked]]:bg-accent/5">
386+ <label>
387+ <Checkbox
388+ checked={on}
389+ onCheckedChange={() => {
390+ const next = new Set(off);
391+ if (on) next.add(skill.id);
392+ else next.delete(skill.id);
393+ set({ skills_off: FOUNDATIONAL_SKILLS.map((s) => s.id).filter((id) => next.has(id)) });
394+ }}
395+ />
396+ <span className="min-w-0 grow truncate">{skill.name}</span>
397+ {suggested.has(skill.id) && suggested.size < FOUNDATIONAL_SKILLS.length && (
398+ <Badge tone="accent" className="shrink-0">
399+ Suggested
400+ </Badge>
401+ )}
402+ </label>
403+ </Card>
400404 );
401405 })}
402406 </div>
404408
405409 {hints.length > 0 && (
406410 <Section title="What it needs connected" about="Integrations its job would use. It works without them, and says so when asked for something that needs one.">
407− <ul className="divide-y divide-line overflow-hidden rounded-lg border border-line bg-bg">
408− {hints.map((hint) => (
409− <li key={hint.id} className="flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2.5">
410− <Plug size={14} className="shrink-0 text-faint" />
411− <span className="min-w-0 grow basis-40">
412− <span className="block text-sm font-medium">{hint.name}</span>
413− {hint.why && <span className="block text-xs text-muted">{hint.why}</span>}
414− </span>
415− {hint.action === "soon" ? (
416− <Badge>Soon</Badge>
417− ) : (
418− <Link to={hint.href} target="_blank" rel="noreferrer" className="text-xs font-medium text-accent hover:underline">
419− {hint.action === "connect" ? "Connect" : "Ask an owner"}
420− </Link>
421− )}
422− </li>
423− ))}
424− </ul>
411+ <Card asChild tone="bg" radius="lg" divided className="overflow-hidden">
412+ <ul>
413+ {hints.map((hint) => (
414+ <li key={hint.id} className="flex flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2.5">
415+ <Plug size={14} className="shrink-0 text-faint" />
416+ <span className="min-w-0 grow basis-40">
417+ <span className="block text-sm font-medium">{hint.name}</span>
418+ {hint.why && <span className="block text-xs text-muted">{hint.why}</span>}
419+ </span>
420+ {hint.action === "soon" ? (
421+ <Badge>Soon</Badge>
422+ ) : (
423+ <Link to={hint.href} target="_blank" rel="noreferrer" className="text-xs font-medium text-accent hover:underline">
424+ {hint.action === "connect" ? "Connect" : "Ask an owner"}
425+ </Link>
426+ )}
427+ </li>
428+ ))}
429+ </ul>
430+ </Card>
425431 </Section>
426432 )}
427433
432438 >
433439 <ul className="space-y-2">
434440 {proposal.routines.map((routine) => (
435− <li key={routine.name} className="rounded-lg border border-line bg-bg px-3 py-2.5">
436− <p className="text-sm font-medium">
437− {routine.name} <span className="font-normal text-muted">· {routine.when}</span>
438− </p>
439− {routine.instructions && <p className="mt-0.5 text-xs text-muted">{routine.instructions}</p>}
440− </li>
441+ <Card asChild key={routine.name} tone="bg" radius="lg" className="px-3 py-2.5">
442+ <li>
443+ <p className="text-sm font-medium">
444+ {routine.name} <span className="font-normal text-muted">· {routine.when}</span>
445+ </p>
446+ {routine.instructions && <p className="mt-0.5 text-xs text-muted">{routine.instructions}</p>}
447+ </li>
448+ </Card>
441449 ))}
442450 </ul>
443451 </Section>
549557 </div>
550558 {turns.length > 0 && (
551559 <Hint label="Start over">
552− <button
560+ <Button
553561 type="button"
554562 aria-label="Start over"
555563 onClick={() => {
556564 setTurns([]);
557565 onReset?.();
558566 }}
559− className="flex size-8 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
567+ variant="ghost"
568+ size="icon-sm"
569+ className="text-faint"
560570 >
561571 <RotateCcw size={14} />
562− </button>
572+ </Button>
563573 </Hint>
564574 )}
565575 </div>
591601 {name} is typing…
592602 </div>
593603 )}
594− {error && <p className="rounded-lg border border-danger/40 bg-danger/10 px-3 py-2 text-xs text-danger">{error}</p>}
604+ {error && (
605+ <Alert asChild className="text-xs">
606+ <p>{error}</p>
607+ </Alert>
608+ )}
595609 </div>
596610 <form
597611 className="flex items-end gap-2 border-t border-line p-2.5"
618632 placeholder={`Message ${name}`}
619633 className="max-h-32 min-h-9 min-w-0 grow resize-none rounded-md border border-line bg-bg px-3 py-2 text-sm outline-none placeholder:text-faint focus:border-accent-dim"
620634 />
621− <button type="submit" aria-label="Send" disabled={busy || !text.trim()} className={cn(PRIMARY, "size-9 shrink-0 px-0")}>
635+ <Button type="submit" aria-label="Send" disabled={busy || !text.trim()} variant="accent" size="icon">
622636 <ArrowUp size={16} />
623− </button>
637+ </Button>
624638 </form>
625639 {spent > 0 && <p className="border-t border-line px-4 py-1.5 text-[0.6875rem] text-faint tabular-nums">Trying it has cost you {money(spent)} so far.</p>}
626640 </div>
641655 {teams.map((slug) => (
642656 <input key={slug} type="hidden" name="teams" value={slug} />
643657 ))}
644− <button type="button" onClick={onEditAll} className={QUIET}>
658+ <Button type="button" onClick={onEditAll} variant="outline">
645659 Edit all fields
646− </button>
647− <button type="submit" disabled={busy || !ready} className={PRIMARY}>
660+ </Button>
661+ <Button type="submit" disabled={busy || !ready} variant="accent">
648662 <Check size={15} />
649663 {busy ? "Creating…" : `Create ${definition.display_name.trim() || "agent"}`}
650− </button>
664+ </Button>
651665 </Form>
652666 );
653667 }
707721 placeholder={`Answer in Spanish, and keep replies under five sentences`}
708722 className={`${FIELD} grow resize-y`}
709723 />
710− <button type="submit" disabled={busy || text.trim().length < 3} className={cn(PRIMARY, "shrink-0")}>
724+ <Button type="submit" disabled={busy || text.trim().length < 3} variant="accent">
711725 <Sparkles size={14} className={busy ? "animate-pulse motion-reduce:animate-none" : ""} />
712726 {busy ? "Drafting…" : "Draft the change"}
713− </button>
727+ </Button>
714728 </div>
715− {error && <p className="mt-3 rounded-lg border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger">{error}</p>}
729+ {error && (
730+ <Alert asChild className="mt-3">
731+ <p>{error}</p>
732+ </Alert>
733+ )}
716734 </fetcher.Form>
717735 {shown && (
718736 <div className="border-t border-line p-4 sm:p-5">
723741 </span>
724742 </p>
725743 <ChangeList rows={rows} />
726− {saveError && <p className="mt-3 rounded-lg border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger">{saveError}</p>}
744+ {saveError && (
745+ <Alert asChild className="mt-3">
746+ <p>{saveError}</p>
747+ </Alert>
748+ )}
727749 <save.Form method="post" className="mt-4 flex flex-wrap items-center justify-end gap-2">
728750 <input type="hidden" name="intent" value="apply" />
729751 <input type="hidden" name="changes" value={JSON.stringify(shown.changes)} />
730752 <input type="hidden" name="from_version" value={shown.from_version} />
731− <button type="button" onClick={() => setShown(null)} className={QUIET}>
753+ <Button type="button" onClick={() => setShown(null)} variant="outline">
732754 Discard
733− </button>
734− <button type="submit" disabled={save.state !== "idle"} className={PRIMARY}>
755+ </Button>
756+ <Button type="submit" disabled={save.state !== "idle"} variant="accent">
735757 <Check size={15} />
736758 {save.state !== "idle" ? "Saving…" : `Save as version ${agent.version + 1}`}
737− </button>
759+ </Button>
738760 </save.Form>
739761 </div>
740762 )}
745767 /** Each field a change touches, before and after; the job and duties as a line diff. */
746768 export function ChangeList({ rows }: { rows: ChangeRow[] }) {
747769 return (
748− <dl className="mt-3 divide-y divide-line overflow-hidden rounded-lg border border-line bg-bg">
749− {rows.map((row) => (
750− <div key={row.field} className="px-3 py-2.5">
751− <dt className="text-xs font-medium text-faint">{row.label}</dt>
752− {row.lines ? (
753− <dd className="mt-1.5 max-h-80 overflow-auto rounded-md border border-line font-mono text-[0.75rem] leading-relaxed">
754− {row.lines.map((line, at) => (
755− <div
756− key={at}
757− className={cn(
758− "flex gap-2 px-2 whitespace-pre-wrap",
759− line.kind === "added" && "bg-success/10 text-success",
760− line.kind === "removed" && "bg-danger/10 text-danger line-through decoration-danger/40",
761− line.kind === "same" && "text-muted",
762− )}
763− >
764− <span aria-hidden="true" className="w-3 shrink-0 select-none">
765− {line.kind === "added" ? "+" : line.kind === "removed" ? "−" : " "}
766− </span>
767− <span className="sr-only">{line.kind === "added" ? "Added: " : line.kind === "removed" ? "Removed: " : ""}</span>
768− <span className="min-w-0">{line.text || " "}</span>
769− </div>
770− ))}
771− </dd>
772− ) : (
773− <dd className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
774− <span className="text-muted line-through decoration-line-strong">{row.before}</span>
775− <span aria-hidden="true" className="text-faint">
776− →
777− </span>
778− <span className="sr-only">becomes</span>
779− <span className="font-medium text-fg">{row.after}</span>
780− </dd>
781− )}
782− </div>
783− ))}
784− </dl>
770+ <Card asChild tone="bg" radius="lg" divided className="mt-3 overflow-hidden">
771+ <dl>
772+ {rows.map((row) => (
773+ <div key={row.field} className="px-3 py-2.5">
774+ <dt className="text-xs font-medium text-faint">{row.label}</dt>
775+ {row.lines ? (
776+ <dd className="mt-1.5 max-h-80 overflow-auto rounded-md border border-line font-mono text-[0.75rem] leading-relaxed">
777+ {row.lines.map((line, at) => (
778+ <div
779+ key={at}
780+ className={cn(
781+ "flex gap-2 px-2 whitespace-pre-wrap",
782+ line.kind === "added" && "bg-success/10 text-success",
783+ line.kind === "removed" && "bg-danger/10 text-danger line-through decoration-danger/40",
784+ line.kind === "same" && "text-muted",
785+ )}
786+ >
787+ <span aria-hidden="true" className="w-3 shrink-0 select-none">
788+ {line.kind === "added" ? "+" : line.kind === "removed" ? "−" : " "}
789+ </span>
790+ <span className="sr-only">{line.kind === "added" ? "Added: " : line.kind === "removed" ? "Removed: " : ""}</span>
791+ <span className="min-w-0">{line.text || " "}</span>
792+ </div>
793+ ))}
794+ </dd>
795+ ) : (
796+ <dd className="mt-1 flex flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
797+ <span className="text-muted line-through decoration-line-strong">{row.before}</span>
798+ <span aria-hidden="true" className="text-faint">
799+ →
800+ </span>
801+ <span className="sr-only">becomes</span>
802+ <span className="font-medium text-fg">{row.after}</span>
803+ </dd>
804+ )}
805+ </div>
806+ ))}
807+ </dl>
808+ </Card>
785809 );
786810 }
787811
811835 {mine ? "your" : `@${agent.personal_owner}'s`} direct messages stays behind with the personal agent, which is archived.
812836 </p>
813837 <div className="flex flex-wrap gap-2">
814− <button type="submit" disabled={busy} className={PRIMARY}>
838+ <Button type="submit" disabled={busy} variant="accent">
815839 <Users size={14} />
816840 {busy ? "Promoting…" : `Promote ${agent.display_name}`}
817− </button>
818− <button type="button" onClick={() => setAsking(false)} className={QUIET}>
841+ </Button>
842+ <Button type="button" onClick={() => setAsking(false)} variant="outline">
819843 Cancel
820− </button>
844+ </Button>
821845 </div>
822846 </Form>
823847 ) : (
824− <button type="button" onClick={() => setAsking(true)} className={QUIET}>
848+ <Button type="button" onClick={() => setAsking(true)} variant="outline">
825849 <Users size={14} />
826850 Promote to a workspace agent…
827− </button>
851+ </Button>
828852 )}
829853 </div>
830854 )}
+18−19
1313 AlertDialogTitle,
1414 AlertDialogTrigger,
1515 } from "../ui/alert-dialog";
16+import { Button } from "../ui/button";
17+import { Card } from "../ui/card";
1618 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
1719 import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field";
1820 import { Input, InputAddon, InputGroup } from "../ui/input";
3436 return { fetcher, open, setOpen, error, busy: fetcher.state !== "idle" };
3537 }
3638
37−const PRIMARY = "inline-flex items-center justify-center gap-2 rounded-md bg-accent px-3.5 py-2 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-50";
38−const QUIET = "inline-flex items-center justify-center gap-2 rounded-md border border-line px-3.5 py-2 text-sm font-medium text-fg/80 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg";
3939
4040 /** A dollars field: `$` before it, posted as dollars. */
4141 export function DollarsInput({ id, name, defaultValue, placeholder, required }: { id: string; name: string; defaultValue: string; placeholder?: string; required?: boolean }) {
8383 </Field>
8484 <FieldError>{error}</FieldError>
8585 <DialogFooter>
86− <button type="button" className={QUIET} onClick={() => setOpen(false)}>
86+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
8787 Cancel
88− </button>
89− <button type="submit" className={PRIMARY} disabled={busy}>
88+ </Button>
89+ <Button type="submit" variant="accent" disabled={busy}>
9090 {busy ? "Saving…" : "Save budget"}
91− </button>
91+ </Button>
9292 </DialogFooter>
9393 </fetcher.Form>
9494 </DialogContent>
117117 {session.visible ? <>&ldquo;{session.title}&rdquo;</> : <PrivateTitle />} stopped at its cap. A new cap lets it go on; it stops again there.
118118 </DialogDescription>
119119 </DialogHeader>
120− <p className="rounded-lg border border-line bg-bg/50 px-3 py-2 text-sm text-muted">
121− {session.agent_name} has spent <SpendOfCap spent={session.charged_micros} cap={session.cap_micros} /> on it so far.
122− </p>
120+ <Card asChild tone="plain" radius="lg" className="bg-bg/50 px-3 py-2 text-sm text-muted">
121+ <p>
122+ {session.agent_name} has spent <SpendOfCap spent={session.charged_micros} cap={session.cap_micros} /> on it so far.
123+ </p>
124+ </Card>
123125 <fetcher.Form method="post" action={`/${slug}/-/agents/${session.agent_handle}/sessions/${session.id}`} className="grid gap-5">
124126 <input type="hidden" name="intent" value="approve" />
125127 <Field>
129131 </Field>
130132 <FieldError>{error}</FieldError>
131133 <DialogFooter>
132− <button type="button" className={QUIET} onClick={() => setOpen(false)}>
134+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
133135 Cancel
134− </button>
135− <button type="submit" className={PRIMARY} disabled={busy}>
136+ </Button>
137+ <Button type="submit" variant="accent" disabled={busy}>
136138 {busy ? "Approving…" : "Approve and go on"}
137− </button>
139+ </Button>
138140 </DialogFooter>
139141 </fetcher.Form>
140142 </DialogContent>
181183 ))}
182184 <AlertDialogFooter>
183185 <AlertDialogCancel type="button">Cancel</AlertDialogCancel>
184− <button
186+ <Button
185187 type="submit"
186− disabled={busy}
187− className="inline-flex items-center justify-center gap-2 rounded-md bg-danger px-3.5 py-2 text-sm font-medium text-bg transition-colors hover:bg-danger/90 disabled:opacity-50"
188− >
188+ disabled={busy} className="bg-danger hover:bg-danger/90">
189189 {busy ? "Working…" : confirm}
190− </button>
190+ </Button>
191191 </AlertDialogFooter>
192192 </fetcher.Form>
193193 </AlertDialogContent>
195195 );
196196 }
197197
198−export const BUTTONS = { PRIMARY, QUIET };
+30−27
88 import { Badge } from "../ui/badge";
99 import { Hint } from "../ui/hint";
1010 import { TimeAgo } from "../ui";
11+import { Card } from "../ui/card";
1112 import { cn } from "../../lib/cn";
1213 import { money } from "../../lib/usage";
1314 import { kindLabel, meterTone, monthDays, sessionStatus, shareOf, shortDay, topSlices, whereLabel } from "./format";
150151 /** A live session as a card: the agent, what it is doing, how far and what it has cost. */
151152 export function SessionCard({ slug, session, action }: { slug: string; session: AgentSession; action?: ReactNode }) {
152153 return (
153− <li className="relative flex flex-col rounded-xl border border-line bg-surface p-4 transition-colors hover:border-line-strong">
154− <div className="flex items-center gap-2.5">
155− <AgentAvatar agent={{ handle: session.agent_handle, avatar_seed: session.agent_avatar_seed }} size={28} />
156− <div className="min-w-0 grow leading-tight">
157− <p className="truncate text-sm font-medium">{session.agent_name}</p>
158− <p className="truncate text-xs text-faint">
159− {whereLabel(session)}
160− {session.asked_by_username ? ` · @${session.asked_by_username}` : ""}
161− </p>
154+ <Card asChild className="relative flex flex-col p-4 transition-colors hover:border-line-strong">
155+ <li>
156+ <div className="flex items-center gap-2.5">
157+ <AgentAvatar agent={{ handle: session.agent_handle, avatar_seed: session.agent_avatar_seed }} size={28} />
158+ <div className="min-w-0 grow leading-tight">
159+ <p className="truncate text-sm font-medium">{session.agent_name}</p>
160+ <p className="truncate text-xs text-faint">
161+ {whereLabel(session)}
162+ {session.asked_by_username ? ` · @${session.asked_by_username}` : ""}
163+ </p>
164+ </div>
165+ <StatusChip status={session.status} />
162166 </div>
163− <StatusChip status={session.status} />
164− </div>
165− <Link to={sessionHref(slug, session)} className="mt-3 line-clamp-2 text-[0.9375rem] font-medium text-fg after:absolute after:inset-0 hover:underline">
166− {session.visible ? session.title || "Untitled session" : "A private session"}
167− </Link>
168− {session.status_note && <p className="mt-1 line-clamp-2 text-xs text-muted">{session.status_note}</p>}
169− <div className="mt-auto pt-4">
170− <div className="flex items-center justify-between gap-3 text-xs text-muted">
171− <span>
172− {stepsLine(session)} · <TimeAgo at={session.created_at} />
173− </span>
174− <SpendOfCap spent={session.charged_micros} cap={session.cap_micros} />
167+ <Link to={sessionHref(slug, session)} className="mt-3 line-clamp-2 text-[0.9375rem] font-medium text-fg after:absolute after:inset-0 hover:underline">
168+ {session.visible ? session.title || "Untitled session" : "A private session"}
169+ </Link>
170+ {session.status_note && <p className="mt-1 line-clamp-2 text-xs text-muted">{session.status_note}</p>}
171+ <div className="mt-auto pt-4">
172+ <div className="flex items-center justify-between gap-3 text-xs text-muted">
173+ <span>
174+ {stepsLine(session)} · <TimeAgo at={session.created_at} />
175+ </span>
176+ <SpendOfCap spent={session.charged_micros} cap={session.cap_micros} />
177+ </div>
178+ {session.cap_micros != null && <Meter spent={session.charged_micros} cap={session.cap_micros} label="Spent of its cap" size="sm" className="mt-2" />}
179+ {action && <div className="relative z-10 mt-3">{action}</div>}
175180 </div>
176− {session.cap_micros != null && <Meter spent={session.charged_micros} cap={session.cap_micros} label="Spent of its cap" size="sm" className="mt-2" />}
177− {action && <div className="relative z-10 mt-3">{action}</div>}
178− </div>
179− </li>
181+ </li>
182+ </Card>
180183 );
181184 }
182185
286289 /** A quiet empty state inside a panel or a tab. */
287290 export function Quiet({ title, children, icon }: { title: string; children?: ReactNode; icon?: ReactNode }) {
288291 return (
289− <div className="rounded-xl border border-dashed border-line px-6 py-10 text-center">
292+ <Card tone="plain" className="border-dashed px-6 py-10 text-center">
290293 {icon && <span className="mx-auto mb-3 flex size-10 items-center justify-center rounded-xl bg-raised text-muted">{icon}</span>}
291294 <p className="text-sm font-medium">{title}</p>
292295 {children && <div className="mx-auto mt-1 max-w-md text-sm text-muted">{children}</div>}
293− </div>
296+ </Card>
294297 );
295298 }
+92−88
22
33 import { type NewRoutine, ROUTINE_EVENTS, type RoutineEvent, type RoutineSchedule } from "@g1t/contracts";
44
5+import { Button } from "../ui/button";
6+import { Card } from "../ui/card";
57 import { CheckboxOption } from "../ui/checkbox";
68 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
79 import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field";
911 import { SelectField } from "../ui/select";
1012 import { Switch } from "../ui/switch";
1113 import { Textarea } from "../ui/textarea";
12−import { BUTTONS, useDialogFetcher } from "./dialogs";
14+import { useDialogFetcher } from "./dialogs";
1315 import { timeField, weekdayName } from "./format";
1416
1517 /** A conversation a routine can post in. */
8284 />
8385 </Field>
8486
85− <fieldset className="grid gap-4 rounded-lg border border-line p-4">
86− <legend className="px-1 text-sm font-medium">When</legend>
87− <label className="flex items-center justify-between gap-3 text-sm">
88− <span>
89− On a schedule
90− <span className="block text-xs text-faint">In UTC.</span>
91− </span>
92− <Switch checked={scheduled} onCheckedChange={setScheduled} aria-label="On a schedule" />
93− </label>
94− {scheduled && (
95− <div className="flex flex-wrap items-center gap-2">
96− <input type="hidden" name="every" value={schedule.every} />
97− <input type="hidden" name="weekday" value={schedule.weekday} />
98− <div className="w-40">
99− <SelectField
100− value={schedule.every}
101− onValueChange={(every) => setSchedule({ ...schedule, every: every as RoutineSchedule["every"] })}
102− options={EVERY}
103− size="sm"
104− aria-label="How often"
105− />
106− </div>
107− {schedule.every === "week" && (
108− <div className="w-36">
87+ <Card asChild tone="plain" radius="lg" className="grid gap-4 p-4">
88+ <fieldset>
89+ <legend className="px-1 text-sm font-medium">When</legend>
90+ <label className="flex items-center justify-between gap-3 text-sm">
91+ <span>
92+ On a schedule
93+ <span className="block text-xs text-faint">In UTC.</span>
94+ </span>
95+ <Switch checked={scheduled} onCheckedChange={setScheduled} aria-label="On a schedule" />
96+ </label>
97+ {scheduled && (
98+ <div className="flex flex-wrap items-center gap-2">
99+ <input type="hidden" name="every" value={schedule.every} />
100+ <input type="hidden" name="weekday" value={schedule.weekday} />
101+ <div className="w-40">
109102 <SelectField
110− value={String(schedule.weekday)}
111− onValueChange={(day) => setSchedule({ ...schedule, weekday: Number(day) })}
112− options={[1, 2, 3, 4, 5, 6, 0].map((d) => ({ value: String(d), label: `on ${weekdayName(d)}` }))}
103+ value={schedule.every}
104+ onValueChange={(every) => setSchedule({ ...schedule, every: every as RoutineSchedule["every"] })}
105+ options={EVERY}
113106 size="sm"
114− aria-label="Day of the week"
107+ aria-label="How often"
115108 />
116109 </div>
117− )}
118− {schedule.every === "hour" ? (
119− <label className="flex items-center gap-2 text-sm text-muted">
120− at minute
121− <Input
122− name="minute"
123− type="number"
124− min={0}
125− max={59}
126− value={schedule.minute}
127− onChange={(e) => setSchedule({ ...schedule, minute: Math.min(59, Math.max(0, Number(e.target.value) || 0)) })}
128− className="h-8 w-20"
110+ {schedule.every === "week" && (
111+ <div className="w-36">
112+ <SelectField
113+ value={String(schedule.weekday)}
114+ onValueChange={(day) => setSchedule({ ...schedule, weekday: Number(day) })}
115+ options={[1, 2, 3, 4, 5, 6, 0].map((d) => ({ value: String(d), label: `on ${weekdayName(d)}` }))}
116+ size="sm"
117+ aria-label="Day of the week"
118+ />
119+ </div>
120+ )}
121+ {schedule.every === "hour" ? (
122+ <label className="flex items-center gap-2 text-sm text-muted">
123+ at minute
124+ <Input
125+ name="minute"
126+ type="number"
127+ min={0}
128+ max={59}
129+ value={schedule.minute}
130+ onChange={(e) => setSchedule({ ...schedule, minute: Math.min(59, Math.max(0, Number(e.target.value) || 0)) })}
131+ className="h-8 w-20"
132+ />
133+ </label>
134+ ) : (
135+ <label className="flex items-center gap-2 text-sm text-muted">
136+ at
137+ <Input
138+ name="time"
139+ type="time"
140+ value={timeField(schedule)}
141+ onChange={(e) => {
142+ const [h, m] = e.target.value.split(":").map(Number);
143+ setSchedule({ ...schedule, hour: h || 0, minute: m || 0 });
144+ }}
145+ className="h-8 w-28"
146+ />
147+ UTC
148+ </label>
149+ )}
150+ </div>
151+ )}
152+ <div className="border-t border-line pt-4">
153+ <p className="text-sm">When something happens</p>
154+ <p className="text-xs text-faint">Each run is about the one thing that happened, in a repository you can read.</p>
155+ <div className="mt-3 grid gap-2.5">
156+ {ROUTINE_EVENTS.map((event) => (
157+ <CheckboxOption
158+ key={event.key}
159+ name="events"
160+ value={event.key}
161+ checked={events.includes(event.key)}
162+ onCheckedChange={(on) => setEvents(on ? [...events, event.key] : events.filter((e) => e !== event.key))}
163+ label={event.label}
164+ description={event.hint}
129165 />
130− </label>
131− ) : (
132− <label className="flex items-center gap-2 text-sm text-muted">
133− at
134− <Input
135− name="time"
136− type="time"
137− value={timeField(schedule)}
138− onChange={(e) => {
139− const [h, m] = e.target.value.split(":").map(Number);
140− setSchedule({ ...schedule, hour: h || 0, minute: m || 0 });
141− }}
142− className="h-8 w-28"
143− />
144− UTC
145− </label>
166+ ))}
167+ </div>
168+ {events.length > 0 && (
169+ <Field className="mt-4">
170+ <FieldLabel htmlFor={`${id}-repos`}>Repositories</FieldLabel>
171+ <Input id={`${id}-repos`} name="repos" defaultValue={(draft.repos ?? []).join(", ")} placeholder="acme/web, acme/api" autoComplete="off" />
172+ <FieldDescription>Empty: every repository you can read.</FieldDescription>
173+ </Field>
146174 )}
147175 </div>
148− )}
149− <div className="border-t border-line pt-4">
150− <p className="text-sm">When something happens</p>
151− <p className="text-xs text-faint">Each run is about the one thing that happened, in a repository you can read.</p>
152− <div className="mt-3 grid gap-2.5">
153− {ROUTINE_EVENTS.map((event) => (
154− <CheckboxOption
155− key={event.key}
156− name="events"
157− value={event.key}
158− checked={events.includes(event.key)}
159− onCheckedChange={(on) => setEvents(on ? [...events, event.key] : events.filter((e) => e !== event.key))}
160− label={event.label}
161− description={event.hint}
162− />
163− ))}
164− </div>
165− {events.length > 0 && (
166− <Field className="mt-4">
167− <FieldLabel htmlFor={`${id}-repos`}>Repositories</FieldLabel>
168− <Input id={`${id}-repos`} name="repos" defaultValue={(draft.repos ?? []).join(", ")} placeholder="acme/web, acme/api" autoComplete="off" />
169− <FieldDescription>Empty: every repository you can read.</FieldDescription>
170− </Field>
171− )}
172− </div>
173− {!scheduled && events.length === 0 && <p className="text-xs text-warn">Choose a schedule, something that happens, or both.</p>}
174− </fieldset>
176+ {!scheduled && events.length === 0 && <p className="text-xs text-warn">Choose a schedule, something that happens, or both.</p>}
177+ </fieldset>
178+ </Card>
175179 {scheduled ? null : <input type="hidden" name="every" value="none" />}
176180
177181 <Field>
192196 </Field>
193197 <FieldError>{error}</FieldError>
194198 <DialogFooter>
195− <button type="button" className={BUTTONS.QUIET} onClick={() => setOpen(false)}>
199+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
196200 Cancel
197− </button>
198− <button type="submit" className={BUTTONS.PRIMARY} disabled={busy || (!scheduled && events.length === 0)}>
201+ </Button>
202+ <Button type="submit" variant="accent" disabled={busy || (!scheduled && events.length === 0)}>
199203 {busy ? "Saving…" : draft.id ? "Save routine" : "Add routine"}
200− </button>
204+ </Button>
201205 </DialogFooter>
202206 </fetcher.Form>
203207 </DialogContent>
+10−9
99 import type { LibrarySkill, SkillAttachment, SkillLibrary, SkillOrigin, SkillScope } from "@g1t/contracts";
1010
1111 import { Badge } from "../ui/badge";
12+import { Button } from "../ui/button";
1213 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
1314 import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field";
1415 import { Hint } from "../ui/hint";
1516 import { SelectField } from "../ui/select";
16−import { BUTTONS, useDialogFetcher } from "./dialogs";
17+import { useDialogFetcher } from "./dialogs";
1718
1819 /** `/acme/-/agents/skills`, or one skill's page. */
1920 export function skillsPath(slug: string, name?: string, rest = ""): string {
148149 </Field>
149150 <FieldError>{error}</FieldError>
150151 <DialogFooter>
151− <button type="button" className={BUTTONS.QUIET} onClick={() => setOpen(false)}>
152+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
152153 Cancel
153− </button>
154− <button type="submit" className={BUTTONS.PRIMARY} disabled={busy || !where}>
154+ </Button>
155+ <Button type="submit" variant="accent" disabled={busy || !where}>
155156 {busy ? "Attaching…" : "Attach"}
156− </button>
157+ </Button>
157158 </DialogFooter>
158159 </fetcher.Form>
159160 </DialogContent>
189190 </Field>
190191 <FieldError>{error}</FieldError>
191192 <DialogFooter>
192− <button type="button" className={BUTTONS.QUIET} onClick={() => setOpen(false)}>
193+ <Button type="button" variant="outline" onClick={() => setOpen(false)}>
193194 Cancel
194− </button>
195− <button type="submit" className={BUTTONS.PRIMARY} disabled={busy || !name}>
195+ </Button>
196+ <Button type="submit" variant="accent" disabled={busy || !name}>
196197 {busy ? "Attaching…" : "Attach"}
197− </button>
198+ </Button>
198199 </DialogFooter>
199200 </fetcher.Form>
200201 </DialogContent>
+10−6
88 import { Link } from "react-router";
99
1010 import { analyticsHere, choice, choose, consentRequired, onChoice } from "../lib/analytics-consent";
11+import { Button } from "./ui/button";
1112
1213 /** Whether this visitor is asked at all, known only once the page is in the browser. */
1314 export function useAsksFirst(): boolean {
3334 </Link>
3435 </p>
3536 <div className="mt-3 flex gap-2">
36− <button
37+ <Button
3738 type="button"
3839 onClick={() => choose("yes")}
39− className="flex-1 rounded-lg bg-fg px-3 py-2 font-medium text-bg transition-colors hover:bg-fg-hover"
40+ size="inline"
41+ className="flex-1 rounded-lg px-3 py-2"
4042 >
4143 Allow
42− </button>
43− <button
44+ </Button>
45+ <Button
4446 type="button"
4547 onClick={() => choose("no")}
46− className="flex-1 rounded-lg px-3 py-2 font-medium text-fg ring-1 ring-line-strong transition-colors hover:bg-raised"
48+ variant="ghost"
49+ size="inline"
50+ className="flex-1 rounded-lg px-3 py-2 text-fg ring-1 ring-line-strong"
4751 >
4852 No thanks
49− </button>
53+ </Button>
5054 </div>
5155 </section>
5256 );
+14−11
44
55 import { ConnectorMark } from "./connectors";
66 import { TierBadge } from "./marketplace";
7+import { Card } from "./ui/card";
78 import { Skeleton } from "./ui/skeleton";
89 import { type BuiltinApp, type InstalledAppData, type PinnableApp, appPinFromForm, installedAppOf, withPin } from "../lib/apps";
910 import { cn } from "../lib/cn";
196197 <NoApps slug={slug} onOpen={onClose} compact />
197198 ) : (
198199 <>
199− <label className="flex h-9 items-center gap-2 rounded-lg border border-line bg-bg px-2.5 text-sm focus-within:border-line-strong">
200− <Search size={15} className="shrink-0 text-faint" />
201− <input
202− value={query}
203− onChange={(event) => setQuery(event.target.value)}
204− placeholder="Find an app"
205− aria-label="Find an app"
206− autoComplete="off"
207− className="min-w-0 grow bg-transparent outline-none placeholder:text-faint"
208− />
209− </label>
200+ <Card asChild tone="bg" radius="lg" className="flex h-9 items-center gap-2 px-2.5 text-sm focus-within:border-line-strong">
201+ <label>
202+ <Search size={15} className="shrink-0 text-faint" />
203+ <input
204+ value={query}
205+ onChange={(event) => setQuery(event.target.value)}
206+ placeholder="Find an app"
207+ aria-label="Find an app"
208+ autoComplete="off"
209+ className="min-w-0 grow bg-transparent outline-none placeholder:text-faint"
210+ />
211+ </label>
212+ </Card>
210213 {apps.length === 0 ? (
211214 <p className="px-2 py-6 text-center text-sm text-muted">No installed app matches.</p>
212215 ) : (
+5−3
11 import { ArrowUp, AtSign, BookMarked, ChevronDown, MessageSquare, Plus, Sparkles } from "lucide-react";
22 import type { ReactNode } from "react";
33
4+import { Badge } from "./ui/badge";
5+import { Card } from "./ui/card";
46 import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
57
68 /**
1214 export function AskComposer({ children }: { children?: ReactNode }) {
1315 return (
1416 <section aria-label="Agent" className="space-y-3">
15− <div aria-disabled="true" className="rounded-xl border border-line bg-surface opacity-80">
17+ <Card aria-disabled="true" className="opacity-80">
1618 <p className="flex items-center gap-2 px-4 pt-3 text-sm font-medium text-fg">
1719 <Sparkles size={14} className="text-accent" aria-hidden="true" />
1820 Agent
19− <span className="rounded-full border border-line px-1.5 text-[0.6875rem] font-normal text-muted">Coming soon</span>
21+ <Badge className="px-1.5 py-0 font-normal">Coming soon</Badge>
2022 </p>
2123 <label htmlFor="ask-g1t" className="sr-only">
2224 Ask the agent (coming soon)
4648 <ArrowUp size={15} />
4749 </Soon>
4850 </div>
49− </div>
51+ </Card>
5052 {children && <div className="flex flex-wrap items-center gap-2">{children}</div>}
5153 </section>
5254 );
+21−17
1212 import { CheckStateIcon } from "./commit-checks";
1313 import { ChangeSize } from "./work";
1414 import { PullIcon } from "./work-icons";
15+import { Badge } from "./ui/badge";
16+import { Card } from "./ui/card";
1517 import { SkeletonLine } from "./ui/skeleton";
1618
1719 function ReviewIcon({ state }: { state: Attempt["review"]["state"] }) {
3739 </Link>
3840 )}
3941 {attempt.current && (
40− <span className="shrink-0 rounded-full border border-line px-1.5 py-px text-[0.625rem] text-faint">This one</span>
42+ <Badge className="px-1.5 text-[0.625rem] font-normal text-faint">This one</Badge>
4143 )}
4244 <span className="ml-auto shrink-0">
4345 <ChangeSize files={attempt.files} />
7678 return (
7779 <Suspense
7880 fallback={
79− <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3" aria-busy>
81+ <Card className="mt-4 px-4 py-3" aria-busy>
8082 <SkeletonLine className="text-sm" barClassName="w-48" />
81− </div>
83+ </Card>
8284 }
8385 >
8486 <Await resolve={attempts} errorElement={null}>
8587 {(found) =>
8688 found.filter((attempt) => !attempt.current).length > 0 && (
87− <section aria-label={`Other attempts at #${issue}`} className="mt-4 overflow-hidden rounded-xl border border-line bg-surface text-sm">
88− <h3 className="flex items-center gap-2.5 px-4 pt-3 font-medium">
89− <GitPullRequestArrow size={16} className="shrink-0 text-info" />
90− Other attempts at #{issue}
91− </h3>
92− <p className="px-4 pt-0.5 pb-2 text-xs text-faint">
93− Pull requests for the same issue. Merging one closes the others.
94− </p>
95− <ul className="divide-y divide-line border-t border-line">
96− {found.map((attempt) => (
97− <AttemptRow key={attempt.number} attempt={attempt} base={base} />
98− ))}
99− </ul>
100− </section>
89+ <Card asChild className="mt-4 overflow-hidden text-sm">
90+ <section aria-label={`Other attempts at #${issue}`}>
91+ <h3 className="flex items-center gap-2.5 px-4 pt-3 font-medium">
92+ <GitPullRequestArrow size={16} className="shrink-0 text-info" />
93+ Other attempts at #{issue}
94+ </h3>
95+ <p className="px-4 pt-0.5 pb-2 text-xs text-faint">
96+ Pull requests for the same issue. Merging one closes the others.
97+ </p>
98+ <ul className="divide-y divide-line border-t border-line">
99+ {found.map((attempt) => (
100+ <AttemptRow key={attempt.number} attempt={attempt} base={base} />
101+ ))}
102+ </ul>
103+ </section>
104+ </Card>
101105 )
102106 }
103107 </Await>
+75−69
1010 import type { AuditEntry } from "@g1t/contracts";
1111
1212 import { actionLabel, actorLabel, ruleLabel, targetLabel } from "../lib/audit";
13−import { Avatar, TimeAgo } from "./ui";
13+import { TimeAgo } from "./ui";
14+import { Avatar } from "./ui/avatar";
15+import { Card } from "./ui/card";
1416 import { Hint } from "./ui/hint";
1517 import { Loading, SkeletonRows } from "./ui/skeleton";
1618
5860 /** The workspace's log, one row an entry, newest first. */
5961 export function AuditTable({ entries, base }: { entries: AuditEntry[]; base: string }) {
6062 return (
61− <ol className="divide-y divide-line rounded-xl border border-line bg-surface">
62− {entries.map((entry) => (
63− <li key={entry.id} className="grid gap-x-4 gap-y-1 px-4 py-3 text-sm sm:grid-cols-[9rem_1fr_auto]">
64− <span className="text-xs leading-5 text-faint">
65− <TimeAgo at={entry.time} />
66− </span>
67− <div className="min-w-0">
68− <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
69− <ActorLine entry={entry} />
70− <span className="font-mono text-xs text-fg/85">{actionLabel(entry.action)}</span>
71− <span className="truncate font-mono text-xs text-muted">{targetLabel(entry)}</span>
72− </div>
73− <p className="mt-1 flex flex-wrap gap-x-3 text-xs text-faint">
74− <Hint label={entry.rule}>
75− <span>{ruleLabel(entry.rule)}</span>
76− </Hint>
77− <span>{entry.surface.toUpperCase()}</span>
78− {entry.result && entry.result !== "ok" && <span>result: {entry.result}</span>}
79− {entry.runId && entry.repo && (
80− <Link to={`/${entry.repo}/agents/runs/${entry.runId}`} className="hover:text-fg">
81− {entry.runKind ?? "agent"} run
82− </Link>
83− )}
84− {entry.runId && (
85− <Link to={`${base}?run=${encodeURIComponent(entry.runId)}`} className="hover:text-fg">
86− everything this run did
87− </Link>
88− )}
89− {entry.credentialId && (
90− <Hint label="Credential">
63+ <Card asChild divided>
64+ <ol>
65+ {entries.map((entry) => (
66+ <li key={entry.id} className="grid gap-x-4 gap-y-1 px-4 py-3 text-sm sm:grid-cols-[9rem_1fr_auto]">
67+ <span className="text-xs leading-5 text-faint">
68+ <TimeAgo at={entry.time} />
69+ </span>
70+ <div className="min-w-0">
71+ <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
72+ <ActorLine entry={entry} />
73+ <span className="font-mono text-xs text-fg/85">{actionLabel(entry.action)}</span>
74+ <span className="truncate font-mono text-xs text-muted">{targetLabel(entry)}</span>
75+ </div>
76+ <p className="mt-1 flex flex-wrap gap-x-3 text-xs text-faint">
77+ <Hint label={entry.rule}>
78+ <span>{ruleLabel(entry.rule)}</span>
79+ </Hint>
80+ <span>{entry.surface.toUpperCase()}</span>
81+ {entry.result && entry.result !== "ok" && <span>result: {entry.result}</span>}
82+ {entry.runId && entry.repo && (
83+ <Link to={`/${entry.repo}/agents/runs/${entry.runId}`} className="hover:text-fg">
84+ {entry.runKind ?? "agent"} run
85+ </Link>
86+ )}
87+ {entry.runId && (
88+ <Link to={`${base}?run=${encodeURIComponent(entry.runId)}`} className="hover:text-fg">
89+ everything this run did
90+ </Link>
91+ )}
92+ {entry.credentialId && (
93+ <Hint label="Credential">
94+ <span className="font-mono">
95+ <span className="sr-only">Credential </span>
96+ {entry.credentialId}
97+ </span>
98+ </Hint>
99+ )}
100+ <Hint label="Request id">
91101 <span className="font-mono">
92− <span className="sr-only">Credential </span>
93− {entry.credentialId}
102+ <span className="sr-only">Request id </span>
103+ {entry.requestId}
94104 </span>
95105 </Hint>
96− )}
97− <Hint label="Request id">
98− <span className="font-mono">
99− <span className="sr-only">Request id </span>
100− {entry.requestId}
101− </span>
102− </Hint>
103− </p>
104− {entry.outcome === "denied" && entry.message && <p className="mt-1 text-xs text-danger">{entry.message}</p>}
105− </div>
106− <span className="sm:text-right">
107− <OutcomeMark entry={entry} />
108− </span>
109− </li>
110− ))}
111− </ol>
106+ </p>
107+ {entry.outcome === "denied" && entry.message && <p className="mt-1 text-xs text-danger">{entry.message}</p>}
108+ </div>
109+ <span className="sm:text-right">
110+ <OutcomeMark entry={entry} />
111+ </span>
112+ </li>
113+ ))}
114+ </ol>
115+ </Card>
112116 );
113117 }
114118
134138 {denied > 0 && <span className="text-danger"> · {denied} refused</span>}
135139 {compact && entries.length > shown.length && ` · the latest ${shown.length}`}
136140 </p>
137− <ol className="mt-2 divide-y divide-line rounded-xl border border-line bg-surface">
138− {shown.map((entry) => (
139− <li key={entry.id} className="flex items-start gap-3 px-4 py-2 text-sm">
140− <time dateTime={entry.time} className="shrink-0 font-mono text-xs leading-5 text-faint" suppressHydrationWarning>
141− {clock(entry.time)}
142− </time>
143− <div className="min-w-0 grow">
144− <p className="flex min-w-0 flex-wrap items-center gap-x-2 font-mono text-xs leading-5">
145− <span className="text-fg/85">{actionLabel(entry.action)}</span>
146− <span className="truncate text-muted">{targetLabel(entry)}</span>
147− </p>
148− {entry.outcome === "denied" && (
149− <p className="text-xs text-danger">
150− {entry.message ?? "Refused."} <span className="text-faint">({ruleLabel(entry.rule)})</span>
141+ <Card asChild divided className="mt-2">
142+ <ol>
143+ {shown.map((entry) => (
144+ <li key={entry.id} className="flex items-start gap-3 px-4 py-2 text-sm">
145+ <time dateTime={entry.time} className="shrink-0 font-mono text-xs leading-5 text-faint" suppressHydrationWarning>
146+ {clock(entry.time)}
147+ </time>
148+ <div className="min-w-0 grow">
149+ <p className="flex min-w-0 flex-wrap items-center gap-x-2 font-mono text-xs leading-5">
150+ <span className="text-fg/85">{actionLabel(entry.action)}</span>
151+ <span className="truncate text-muted">{targetLabel(entry)}</span>
151152 </p>
152− )}
153− </div>
154− <OutcomeMark entry={entry} />
155− </li>
156− ))}
157− </ol>
153+ {entry.outcome === "denied" && (
154+ <p className="text-xs text-danger">
155+ {entry.message ?? "Refused."} <span className="text-faint">({ruleLabel(entry.rule)})</span>
156+ </p>
157+ )}
158+ </div>
159+ <OutcomeMark entry={entry} />
160+ </li>
161+ ))}
162+ </ol>
163+ </Card>
158164 </div>
159165 );
160166 }
+3−2
33 import { Form } from "react-router";
44
55 import { AVATAR_ACCEPT } from "../lib/avatar-upload";
6−import { Avatar, ErrorText, SubmitButton, usePending } from "./ui";
6+import { ErrorText, SubmitButton, usePending } from "./ui";
7+import { Avatar } from "./ui/avatar";
78
89 /**
910 * Uploading an avatar, as GitHub's settings do: the one shown now, a
6364 {image && (
6465 <Form method="post">
6566 <input type="hidden" name="intent" value="remove-avatar" />
66− <SubmitButton variant="quiet" pending="Removing…" match={{ intent: "remove-avatar" }} disabled={uploading}>
67+ <SubmitButton variant="outline" pending="Removing…" match={{ intent: "remove-avatar" }} disabled={uploading}>
6768 <Trash2 size={15} />
6869 Remove
6970 </SubmitButton>
+7−4
33 import { useFetcher } from "react-router";
44
55 import { ErrorText } from "./ui";
6+import { Button } from "./ui/button";
67 import { Hint } from "./ui/hint";
7−import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/combobox";
8+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/command";
89 import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
910
1011 /**
5051 >
5152 <Hint label="Change the branch it merges into">
5253 <PopoverTrigger asChild>
53− <button
54+ <Button
5455 type="button"
55− className="group inline-flex items-center gap-1 rounded-md border border-transparent px-1 font-mono text-fg transition-colors hover:border-line hover:bg-raised"
56+ variant="ghost"
57+ size="inline"
58+ className="group gap-1 border border-transparent px-1 font-mono font-normal text-fg hover:border-line"
5659 >
5760 {shown}
5861 <Pencil size={12} className="text-faint group-hover:text-fg" />
5962 <span className="sr-only">Change the branch it merges into</span>
60− </button>
63+ </Button>
6164 </PopoverTrigger>
6265 </Hint>
6366 <PopoverContent align="start" className="w-72 p-0">
+5−5
181181 Keep the plan
182182 </SubmitButton>
183183 ) : status.kind === "paid" && subscription ? (
184− <SubmitButton variant="quiet" name="intent" value="cancel" pending="Saving…">
184+ <SubmitButton variant="outline" name="intent" value="cancel" pending="Saving…">
185185 Downgrade to free at the period's end
186186 </SubmitButton>
187187 ) : null}
342342 At most <Dollar name="monthly" label="Monthly maximum in dollars" defaultValue={whole(r.monthlyMaxMicros)} /> a month
343343 {(r.reloadedMicros ?? 0) > 0 && <span className="text-faint">· {money(r.reloadedMicros ?? 0)} reloaded this month</span>}
344344 </div>
345− <SubmitButton variant="quiet" match={{ intent: "ai-reload" }} pending="Saving…">
345+ <SubmitButton variant="outline" match={{ intent: "ai-reload" }} pending="Saving…">
346346 Save auto-reload
347347 </SubmitButton>
348348 <p className="text-xs text-faint">A reload that cannot be charged turns auto-reload off and tells the owners by email.</p>
403403 <span className="block text-xs text-muted">Webhook, told of each alert (optional)</span>
404404 <input name="webhook" type="url" placeholder="https://hooks.example.com/g1t" defaultValue={limit.budgetWebhook ?? ""} className={`mt-1 ${FIELD} max-w-md`} />
405405 </label>
406− <SubmitButton variant="quiet" match={{ intent: "budget" }} pending="Saving…">
406+ <SubmitButton variant="outline" match={{ intent: "budget" }} pending="Saving…">
407407 Save alerts
408408 </SubmitButton>
409409 </Form>
430430 aside={
431431 owner && enabled ? (
432432 <Form method="post">
433− <SubmitButton variant="quiet" name="intent" value="portal" pending="Opening Stripe…">
433+ <SubmitButton variant="outline" name="intent" value="portal" pending="Opening Stripe…">
434434 Manage in Stripe
435435 <ArrowUpRight size={14} />
436436 </SubmitButton>
633633 </label>
634634 {!disabled && (
635635 <div className="flex items-end">
636− <SubmitButton variant="quiet" match={{ intent: "details" }} pending="Saving…">
636+ <SubmitButton variant="outline" match={{ intent: "details" }} pending="Saving…">
637637 Save invoice details
638638 </SubmitButton>
639639 </div>
+11−10
3030 wholeDollars,
3131 } from "../lib/billing";
3232 import { ErrorText, SubmitButton } from "./ui";
33+import { Badge } from "./ui/badge";
3334 import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
3435
3536 /** An error the action returned, for the section it belongs to. */
287288 </SubmitButton>
288289 ) : null}
289290 {(status.kind === "paid" || status.kind === "canceling" || status.kind === "past_due") && (
290− <SubmitButton variant={status.kind === "past_due" ? "accent" : "quiet"} name="intent" value="portal" pending="Opening Stripe…">
291+ <SubmitButton variant={status.kind === "past_due" ? "accent" : "outline"} name="intent" value="portal" pending="Opening Stripe…">
291292 {status.kind === "past_due" ? "Update payment on Stripe" : "Manage on Stripe"}
292293 <ArrowUpRight size={14} />
293294 </SubmitButton>
294295 )}
295296 {status.kind === "paid" && subscription && (
296− <SubmitButton variant="quiet" name="intent" value="cancel" pending="Saving…">
297+ <SubmitButton variant="outline" name="intent" value="cancel" pending="Saving…">
297298 End at the end of the period
298299 </SubmitButton>
299300 )}
401402 {verified && left > 0 && <Meter label="Trial used" used={Math.max(0, trialMicros - left)} of={trialMicros} />}
402403 {!verified && enabled && owner && (
403404 <Form method="post" className="mt-4 flex flex-wrap items-center gap-3">
404− <SubmitButton variant="quiet" name="intent" value="card-check" pending="Opening Stripe…">
405+ <SubmitButton variant="outline" name="intent" value="card-check" pending="Opening Stripe…">
405406 <ShieldCheck size={14} />
406407 Check a card
407408 </SubmitButton>
429430 icon={<Gauge size={16} />}
430431 title="Spend limit"
431432 about="What on-demand usage may reach this month, past what the plan includes. At the limit new work waits; runs already going finish."
432− aside={limit.firstMonth ? <span className="rounded-full border border-line px-2 py-0.5 text-xs text-muted">First month</span> : null}
433+ aside={limit.firstMonth ? <Badge size="md">First month</Badge> : null}
433434 >
434435 {limit.account.startsWith("ent_") && (
435436 <p className="mt-2 text-sm text-muted">
517518 </RadioGroup>
518519 </fieldset>
519520 <div className="mt-3 flex flex-wrap items-center gap-3">
520− <SubmitButton variant="quiet" match={{ intent: "spend-limit" }} pending="Saving…">
521+ <SubmitButton variant="outline" match={{ intent: "spend-limit" }} pending="Saving…">
521522 Save limit
522523 </SubmitButton>
523524 <a href="#raise" className="text-sm text-muted hover:text-fg">
596597 className={`${CONTROL} w-full`}
597598 />
598599 </label>
599− <SubmitButton variant="quiet" match={{ intent: "request", kind: "limit" }} pending="Sending…">
600+ <SubmitButton variant="outline" match={{ intent: "request", kind: "limit" }} pending="Sending…">
600601 Send the request
601602 </SubmitButton>
602603 <ErrorText>{error}</ErrorText>
636637 <li key={grant.id} className={`px-3.5 py-2.5 text-sm ${grant.state === "open" ? "" : "text-muted"}`}>
637638 <p className="flex flex-wrap items-center gap-x-2 gap-y-1">
638639 <span className={`tabular-nums ${grant.state === "open" ? "font-medium" : ""}`}>{creditLine(grant)}</span>
639− <span className="rounded-full border border-line px-2 py-px text-xs text-muted">{CREDIT_KIND[grant.kind]}</span>
640+ <Badge size="md" className="py-px">{CREDIT_KIND[grant.kind]}</Badge>
640641 </p>
641642 <p className="mt-0.5 text-xs text-faint">
642643 {grant.kind === "refund" && grant.refundFor ? `For ${grant.refundFor}. ` : ""}
682683 <input type="hidden" name="intent" value="prepay" />
683684 <div className="flex flex-wrap items-center gap-2">
684685 {PREPAY.presets.map((amount) => (
685− <SubmitButton key={amount} variant="quiet" name="amount" value={amount} match={{ intent: "prepay" }} pending="Opening Stripe…">
686+ <SubmitButton key={amount} variant="outline" name="amount" value={amount} match={{ intent: "prepay" }} pending="Opening Stripe…">
686687 {wholeDollars(amount * 1_000_000)}
687688 </SubmitButton>
688689 ))}
758759 </span>
759760 <DollarInput name="issue" label="Issue cap in dollars" defaultValue={String(entitlements.issueCapMicros / 1_000_000)} />
760761 </label>
761− <SubmitButton variant="quiet" match={{ intent: "caps" }} pending="Saving…">
762+ <SubmitButton variant="outline" match={{ intent: "caps" }} pending="Saving…">
762763 Save caps
763764 </SubmitButton>
764765 </Form>
795796 placeholder="An agent kept retrying a failing check on Tuesday night."
796797 className={`${CONTROL} w-full`}
797798 />
798− <SubmitButton variant="quiet" match={{ intent: "request", kind: "overage" }} pending="Sending…">
799+ <SubmitButton variant="outline" match={{ intent: "request", kind: "overage" }} pending="Sending…">
799800 Tell g1t
800801 </SubmitButton>
801802 <ErrorText>{error}</ErrorText>
+6−3
1818 import { CommitAvatars, CommitNames } from "./commit-person";
1919 import { Markdown } from "./markdown";
2020 import { TimeAgo } from "./ui";
21+import { Button } from "./ui/button";
2122 import { Skeleton } from "./ui/skeleton";
2223
2324 /**
204205 <span className="grow text-sm font-medium">
205206 Why line{picked.start === picked.end ? ` ${picked.start}` : `s ${picked.start}–${picked.end}`}
206207 </span>
207− <button
208+ <Button
208209 type="button"
209210 onClick={onClose}
210211 aria-label="Close"
211− className="rounded p-1 text-faint hover:bg-raised hover:text-fg"
212+ variant="ghost"
213+ size="inline"
214+ className="rounded p-1 text-faint"
212215 >
213216 <X size={14} />
214− </button>
217+ </Button>
215218 </header>
216219 {!current ? (
217220 <div className="space-y-3 p-4" aria-busy={loading}>
+82−79
1010 import { type ChecksSource, CommitChecksBadge } from "./commit-checks";
1111 import { host } from "./deploy";
1212 import { TimeAgo } from "./ui";
13+import { Card } from "./ui/card";
1314 import { Hint } from "./ui/hint";
1415
1516 function drift({ ahead, behind }: Drift, main: string): string {
3940 checks?: ChecksSource;
4041 }) {
4142 return (
42− <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
43− {branches.map((branch) => (
44− <li key={branch.name} className="flex flex-wrap items-start gap-x-3 gap-y-1.5 px-4 py-3">
45− <GitBranch size={15} className="mt-0.5 shrink-0 text-faint" />
46− <span className="min-w-0 grow basis-48">
47− <Link
48− to={`${base}/tree/${encodeURIComponent(branch.name)}`}
49− className="block truncate font-mono text-[0.8125rem] font-medium hover:text-accent"
50− >
51− {branch.name}
52− </Link>
53− {branch.commit && (
54− <span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted">
55− <CommitAvatars commit={branch.commit} size={14} max={2} />
56− <span className="shrink-0">
57− <CommitNames commit={branch.commit} className="hover:text-fg" />
58− </span>
59− <span className="text-faint">·</span>
60− <Hint label={branch.commit.message}>
61− <Link to={`${base}/commit/${branch.commit.hash}`} className="min-w-0 truncate hover:text-fg">
62− {branch.commit.message}
63− </Link>
64− </Hint>
65− <CommitChecksBadge checks={checks} sha={branch.commit.hash} className="size-5" />
66− <span className="shrink-0 text-faint">
67− · <TimeAgo at={branch.commit.at} />
68− </span>
69− </span>
70− )}
71− </span>
72− <span className="ml-6.5 flex shrink-0 flex-wrap items-center gap-2 text-xs sm:ml-0">
73− {branch.drift && (
74− <Hint label={drift(branch.drift, main)}>
75− <span className="inline-flex items-center gap-1.5 rounded-md border border-line px-1.5 py-0.5 font-mono tabular-nums text-muted">
76− <span className="sr-only">{drift(branch.drift, main)}</span>
77− <span className="inline-flex items-center gap-0.5" aria-hidden>
78− <ArrowUp size={11} className={branch.drift.ahead > 0 ? "text-success" : "text-faint"} />
79− {branch.drift.ahead}
43+ <Card asChild divided className="overflow-hidden">
44+ <ul>
45+ {branches.map((branch) => (
46+ <li key={branch.name} className="flex flex-wrap items-start gap-x-3 gap-y-1.5 px-4 py-3">
47+ <GitBranch size={15} className="mt-0.5 shrink-0 text-faint" />
48+ <span className="min-w-0 grow basis-48">
49+ <Link
50+ to={`${base}/tree/${encodeURIComponent(branch.name)}`}
51+ className="block truncate font-mono text-[0.8125rem] font-medium hover:text-accent"
52+ >
53+ {branch.name}
54+ </Link>
55+ {branch.commit && (
56+ <span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-muted">
57+ <CommitAvatars commit={branch.commit} size={14} max={2} />
58+ <span className="shrink-0">
59+ <CommitNames commit={branch.commit} className="hover:text-fg" />
8060 </span>
81− <span className="inline-flex items-center gap-0.5" aria-hidden>
82− <ArrowDown size={11} className={branch.drift.behind > 0 ? "text-warn" : "text-faint"} />
83− {branch.drift.behind}
61+ <span className="text-faint">·</span>
62+ <Hint label={branch.commit.message}>
63+ <Link to={`${base}/commit/${branch.commit.hash}`} className="min-w-0 truncate hover:text-fg">
64+ {branch.commit.message}
65+ </Link>
66+ </Hint>
67+ <CommitChecksBadge checks={checks} sha={branch.commit.hash} className="size-5" />
68+ <span className="shrink-0 text-faint">
69+ · <TimeAgo at={branch.commit.at} />
8470 </span>
8571 </span>
86− </Hint>
87− )}
88− {branch.pull ? (
89− <Hint label={branch.pull.title}>
72+ )}
73+ </span>
74+ <span className="ml-6.5 flex shrink-0 flex-wrap items-center gap-2 text-xs sm:ml-0">
75+ {branch.drift && (
76+ <Hint label={drift(branch.drift, main)}>
77+ <span className="inline-flex items-center gap-1.5 rounded-md border border-line px-1.5 py-0.5 font-mono tabular-nums text-muted">
78+ <span className="sr-only">{drift(branch.drift, main)}</span>
79+ <span className="inline-flex items-center gap-0.5" aria-hidden>
80+ <ArrowUp size={11} className={branch.drift.ahead > 0 ? "text-success" : "text-faint"} />
81+ {branch.drift.ahead}
82+ </span>
83+ <span className="inline-flex items-center gap-0.5" aria-hidden>
84+ <ArrowDown size={11} className={branch.drift.behind > 0 ? "text-warn" : "text-faint"} />
85+ {branch.drift.behind}
86+ </span>
87+ </span>
88+ </Hint>
89+ )}
90+ {branch.pull ? (
91+ <Hint label={branch.pull.title}>
92+ <Link
93+ to={`${base}/pull/${branch.pull.number}`}
94+ className="inline-flex items-center gap-1.5 rounded-md border border-line px-1.5 py-0.5 text-muted hover:border-line-strong hover:text-fg"
95+ >
96+ <GitPullRequest size={12} className={branch.pull.draft ? "text-faint" : "text-success"} />#{branch.pull.number}
97+ <span className="sr-only">{branch.pull.title}</span>
98+ <CheckBadge status={branch.pull.checkStatus} />
99+ </Link>
100+ </Hint>
101+ ) : branch.drift?.ahead === 0 ? (
102+ // Nothing here that the default branch lacks: no pull request to open.
103+ <Hint label={`Every commit on ${branch.name} is already on ${main}.`}>
104+ <span className="px-1.5 py-0.5 text-faint">Nothing to merge</span>
105+ </Hint>
106+ ) : (
90107 <Link
91− to={`${base}/pull/${branch.pull.number}`}
92− className="inline-flex items-center gap-1.5 rounded-md border border-line px-1.5 py-0.5 text-muted hover:border-line-strong hover:text-fg"
108+ to={`${base}/pulls/new?branch=${encodeURIComponent(branch.name)}`}
109+ className="rounded-md px-1.5 py-0.5 text-faint hover:text-fg"
93110 >
94− <GitPullRequest size={12} className={branch.pull.draft ? "text-faint" : "text-success"} />#{branch.pull.number}
95− <span className="sr-only">{branch.pull.title}</span>
96− <CheckBadge status={branch.pull.checkStatus} />
111+ Open a pull request
97112 </Link>
98− </Hint>
99− ) : branch.drift?.ahead === 0 ? (
100− // Nothing here that the default branch lacks: no pull request to open.
101− <Hint label={`Every commit on ${branch.name} is already on ${main}.`}>
102− <span className="px-1.5 py-0.5 text-faint">Nothing to merge</span>
103− </Hint>
104− ) : (
105− <Link
106− to={`${base}/pulls/new?branch=${encodeURIComponent(branch.name)}`}
107− className="rounded-md px-1.5 py-0.5 text-faint hover:text-fg"
108− >
109− Open a pull request
110− </Link>
111− )}
112− {branch.preview && (
113− <Hint label={host(branch.preview)}>
114− <a
115− href={branch.preview}
116− className="inline-flex max-w-48 items-center gap-1 truncate rounded-md border border-line px-1.5 py-0.5 text-muted hover:border-line-strong hover:text-fg"
117− >
118− <Globe size={12} className="shrink-0 text-accent" />
119− Preview
120− </a>
121− </Hint>
122− )}
123− </span>
124− </li>
125− ))}
126− </ul>
113+ )}
114+ {branch.preview && (
115+ <Hint label={host(branch.preview)}>
116+ <a
117+ href={branch.preview}
118+ className="inline-flex max-w-48 items-center gap-1 truncate rounded-md border border-line px-1.5 py-0.5 text-muted hover:border-line-strong hover:text-fg"
119+ >
120+ <Globe size={12} className="shrink-0 text-accent" />
121+ Preview
122+ </a>
123+ </Hint>
124+ )}
125+ </span>
126+ </li>
127+ ))}
128+ </ul>
129+ </Card>
127130 );
128131 }
+54−51
66
77 import { AgentPill } from "./marks";
88 import { AgentAvatar } from "../agent-avatar";
9−import { Avatar } from "../ui";
9+
10+import { Avatar } from "../ui/avatar";
11+import { Button } from "../ui/button";
12+import { Card } from "../ui/card";
1013 import { Hint } from "../ui/hint";
1114 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
1215 import { Switch } from "../ui/switch";
8588 [slug],
8689 );
8790 }
88−
89−const QUIET_BUTTON =
90− "inline-flex h-9 items-center gap-2 rounded-md border border-line px-3 text-sm font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-surface hover:text-fg";
91−const ICON_BUTTON =
92− "flex size-7 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent";
9391
9492 /** Why someone may not make a channel: the workspace keeps it to its owners. */
9593 export const OWNERS_ONLY_CHANNELS = "Only workspace owners can create channels here.";
140138 return (
141139 <Hint label={OWNERS_ONLY_CHANNELS} disabled>
142140 {variant === "icon" ? (
143− <button type="button" aria-label="Create a channel" disabled className={`${ICON_BUTTON} opacity-40`}>
141+ <Button type="button" aria-label="Create a channel" disabled variant="ghost" size="icon-xs" className="text-faint opacity-40">
144142 <Plus size={14} />
145− </button>
143+ </Button>
146144 ) : (
147− <button type="button" disabled className={`${QUIET_BUTTON} opacity-50`}>
145+ <Button type="button" disabled variant="outline" className="opacity-50">
148146 <Lock size={14} />
149147 Create a channel
150− </button>
148+ </Button>
151149 )}
152150 </Hint>
153151 );
156154 <>
157155 {variant === "icon" ? (
158156 <Hint label="Create a channel">
159− <button type="button" aria-label="Create a channel" onClick={() => setOpen(true)} className={ICON_BUTTON}>
157+ <Button type="button" aria-label="Create a channel" onClick={() => setOpen(true)} variant="ghost" size="icon-xs" className="text-faint">
160158 <Plus size={14} />
161− </button>
159+ </Button>
162160 </Hint>
163161 ) : (
164− <button type="button" onClick={() => setOpen(true)} className={QUIET_BUTTON}>
162+ <Button type="button" onClick={() => setOpen(true)} variant="outline">
165163 <Hash size={15} />
166164 Create a channel
167− </button>
165+ </Button>
168166 )}
169167 <Dialog open={open} onOpenChange={setOpen}>
170168 <DialogContent>
210208 className="h-10 rounded-md border border-line bg-bg px-3 text-sm outline-none placeholder:text-faint focus:border-accent-dim"
211209 />
212210 </label>
213− <label className="flex items-start justify-between gap-4 rounded-lg border border-line bg-bg/60 p-3">
214− <span>
215− <span className="flex items-center gap-1.5 text-sm font-medium">
216− {isPrivate ? <Lock size={13} className="text-muted" /> : <Hash size={13} className="text-muted" />}
217− {isPrivate ? "Private" : "Public"}
218− </span>
219− <span className="mt-0.5 block text-xs text-muted">
220− {!may.public
221− ? "Only workspace owners can create public channels here, so this one is private: only people and agents you invite can find and read it."
222− : !may.private
223− ? "Only workspace owners can create private channels here, so this one is public: anyone in the workspace can find, read and join it."
224− : isPrivate
225− ? "Only people and agents you invite can find and read it."
226− : "Anyone in the workspace can find, read and join it. Turn on to keep it to the people and agents you invite."}
211+ <Card asChild tone="plain" radius="lg" className="flex items-start justify-between gap-4 bg-bg/60 p-3">
212+ <label>
213+ <span>
214+ <span className="flex items-center gap-1.5 text-sm font-medium">
215+ {isPrivate ? <Lock size={13} className="text-muted" /> : <Hash size={13} className="text-muted" />}
216+ {isPrivate ? "Private" : "Public"}
217+ </span>
218+ <span className="mt-0.5 block text-xs text-muted">
219+ {!may.public
220+ ? "Only workspace owners can create public channels here, so this one is private: only people and agents you invite can find and read it."
221+ : !may.private
222+ ? "Only workspace owners can create private channels here, so this one is public: anyone in the workspace can find, read and join it."
223+ : isPrivate
224+ ? "Only people and agents you invite can find and read it."
225+ : "Anyone in the workspace can find, read and join it. Turn on to keep it to the people and agents you invite."}
226+ </span>
227227 </span>
228− </span>
229− <Switch checked={isPrivate} onCheckedChange={setSecret} disabled={!may.public || !may.private} aria-label="Private" />
230− </label>
228+ <Switch checked={isPrivate} onCheckedChange={setSecret} disabled={!may.public || !may.private} aria-label="Private" />
229+ </label>
230+ </Card>
231231 {error && <p className="text-sm text-danger">{error}</p>}
232232 <DialogFooter>
233− <button type="button" onClick={() => setOpen(false)} className={QUIET_BUTTON}>
233+ <Button type="button" onClick={() => setOpen(false)} variant="outline">
234234 Cancel
235− </button>
236− <button
235+ </Button>
236+ <Button
237237 type="submit"
238238 disabled={busy || !clean}
239− className="inline-flex h-9 items-center justify-center rounded-md bg-accent px-3.5 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-50"
239+ variant="accent"
240240 >
241241 {busy ? "Creating…" : "Create channel"}
242− </button>
242+ </Button>
243243 </DialogFooter>
244244 </form>
245245 </DialogContent>
261261 <>
262262 {variant === "fab" ? (
263263 // A phone's list: the round button above the tab bar.
264− <button
264+ <Button
265265 type="button"
266266 aria-label="New message"
267267 onClick={() => setOpen(true)}
268− className="flex size-14 items-center justify-center rounded-2xl bg-accent text-bg shadow-lg shadow-black/50 transition-transform active:scale-95"
268+ variant="accent" size="icon"
269+ className="size-14 rounded-2xl shadow-lg shadow-black/50 transition-transform active:scale-95"
269270 >
270271 <SquarePen size={22} />
271− </button>
272+ </Button>
272273 ) : variant === "icon" ? (
273274 <Hint label="New message">
274− <button type="button" aria-label="New message" onClick={() => setOpen(true)} className={ICON_BUTTON.replace("size-7", "size-8")}>
275+ <Button type="button" aria-label="New message" onClick={() => setOpen(true)} variant="ghost" size="icon-sm" className="text-faint">
275276 <SquarePen size={15} />
276− </button>
277+ </Button>
277278 </Hint>
278279 ) : (
279− <button type="button" onClick={() => setOpen(true)} className={QUIET_BUTTON}>
280+ <Button type="button" onClick={() => setOpen(true)} variant="outline">
280281 <SquarePen size={15} />
281282 {children ?? "New message"}
282− </button>
283+ </Button>
283284 )}
284285 <Dialog open={open} onOpenChange={setOpen}>
285286 <DialogContent className="max-w-md gap-0 p-0 max-sm:pb-[env(safe-area-inset-bottom)]" showClose={false}>
350351 <span key={person.key} className="flex h-6 items-center gap-1.5 rounded-md bg-raised pr-1 pl-1 text-[0.8125rem]">
351352 {person.kind === "agent" ? <AgentAvatar agent={{ id: person.key.slice(6), handle: person.name, avatar: person.avatar }} size={16} /> : <Avatar name={person.name} image={person.avatar} size={16} />}
352353 {person.display}
353− <button
354+ <Button
354355 type="button"
355356 aria-label={`Remove ${person.display}`}
356357 onClick={() => setChosen((now) => now.filter((p) => p.key !== person.key))}
357− className="rounded p-0.5 text-faint hover:bg-line hover:text-fg"
358+ variant="ghost"
359+ size="inline"
360+ className="rounded p-0.5 text-faint hover:bg-line"
358361 >
359362 <X size={12} />
360− </button>
363+ </Button>
361364 </span>
362365 ))}
363366 <input
424427 <div className="flex items-center justify-between gap-3 border-t border-line px-5 py-3">
425428 <span className="min-w-0 truncate text-sm text-danger">{error}</span>
426429 <div className="flex shrink-0 gap-2">
427− <button type="button" onClick={onDone} className={QUIET_BUTTON}>
430+ <Button type="button" onClick={onDone} variant="outline">
428431 Cancel
429− </button>
430− <button
432+ </Button>
433+ <Button
431434 type="button"
432435 disabled={busy || chosen.length === 0}
433436 onClick={() => void go()}
434− className="inline-flex h-9 items-center justify-center rounded-md bg-accent px-3.5 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-50"
437+ variant="accent"
435438 >
436439 {busy ? "Opening…" : "Open conversation"}
437− </button>
440+ </Button>
438441 </div>
439442 </div>
440443 </div>
+24−25
4040 AlertDialogTitle,
4141 } from "../ui/alert-dialog";
4242 import { Badge, type BadgeTone } from "../ui/badge";
43+import { ButtonLink } from "../ui";
44+import { Button, type ButtonVariant } from "../ui/button";
4345 import { InputAddon, InputGroup } from "../ui/input";
4446 import { cn } from "../../lib/cn";
4547 import { type CardChip, actionMode, cardChip, foldsBody, inputValue, moneyInitial, shownActions } from "../../lib/card-actions";
171173 {toast.ok ? <CircleCheck size={16} /> : <CircleAlert size={16} />}
172174 </span>
173175 <p className="min-w-0 grow">{toast.message}</p>
174− <button
176+ <Button
175177 type="button"
176178 aria-label="Dismiss"
177179 onClick={() => dismissToast(toast.id)}
178− className="flex size-6 shrink-0 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
180+ variant="ghost" size="icon"
181+ className="size-6 text-faint"
179182 >
180183 <X size={14} />
181− </button>
184+ </Button>
182185 </div>
183186 ))}
184187 </section>
186189 }
187190
188191 // ── The card.
189−
190−const ACTION_BASE =
191− "inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-md px-3 text-[0.8125rem] font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:pointer-events-none disabled:opacity-50 max-md:h-10 max-md:px-3.5";
192192
193−const ACTION_STYLES: Record<NonNullable<CardAction["style"]>, string> = {
194− primary: "bg-accent text-bg hover:bg-accent-hover",
195− danger: "border border-danger/40 text-danger hover:border-danger hover:bg-danger/10",
196− default: "border border-line text-fg/85 hover:border-line-strong hover:bg-raised hover:text-fg",
197−};
193+/** A card's actions are small buttons, a finger tall on a phone; the action's style names the variant. */
194+const ACTION_VARIANT: Record<NonNullable<CardAction["style"]>, ButtonVariant> = { primary: "accent", danger: "destructive", default: "outline" };
198195
199−function actionClass(action: CardAction, active = false) {
200− return cn(ACTION_BASE, ACTION_STYLES[action.style ?? "default"], active && "border-accent/60 bg-raised text-fg");
196+function actionProps(action: Pick<CardAction, "style">, active = false) {
197+ return { variant: ACTION_VARIANT[action.style ?? "default"], size: "sm" as const, className: cn("max-md:h-10 max-md:px-3.5", active && "border-accent/60 bg-raised text-fg") };
201198 }
202199
203200 /** Sends a card's action; says what happened in a toast; true when it was done. */
352349 const target = actionMode(action) === "link" ? cardHref(action.href, slug, code) : null;
353350 if (target) {
354351 return (
355− <Link key={action.id} to={target} className={actionClass(action)}>
352+ <ButtonLink key={action.id} to={target} {...actionProps(action)}>
356353 {action.label}
357354 <ArrowUpRight size={14} aria-hidden="true" className="-mr-0.5 text-faint" />
358− </Link>
355+ </ButtonLink>
359356 );
360357 }
361358 const working = busy === action.id;
362359 const opens = !!action.input;
363360 return (
364− <button
361+ <Button
365362 key={action.id}
366363 ref={(element) => {
367364 if (element) triggers.current.set(action.id, element);
374371 aria-controls={opens && asking === action.id ? panelId : undefined}
375372 aria-haspopup={action.confirm && !opens ? "dialog" : undefined}
376373 onClick={() => press(action)}
377− className={actionClass(action, opens && asking === action.id)}
374+ {...actionProps(action, opens && asking === action.id)}
378375 >
379376 {working && <LoaderCircle size={14} aria-hidden="true" className="-ml-0.5 animate-spin" />}
380377 {action.label}
381− </button>
378+ </Button>
382379 );
383380 })}
384381 </div>
453450 )}
454451 </div>
455452 {(folded || open) && (
456− <button
453+ <Button
457454 type="button"
458455 aria-expanded={open}
459456 aria-controls={id}
460457 onClick={() => setOpen((now) => !now)}
461− className="mt-1 rounded px-1 py-0.5 text-xs font-medium text-accent transition-colors outline-none hover:bg-raised focus-visible:ring-2 focus-visible:ring-accent/50 max-md:min-h-9"
458+ variant="ghost"
459+ size="inline"
460+ className="mt-1 rounded px-1 py-0.5 text-xs text-accent focus-visible:ring-accent/50 max-md:min-h-9"
462461 >
463462 {open ? "Show less" : "Show more"}
464− </button>
463+ </Button>
465464 )}
466465 </div>
467466 );
589588 />
590589 )}
591590 <div className="flex shrink-0 gap-2 max-sm:[&>*]:grow">
592− <button type="button" onClick={onCancel} className={actionClass({ id: "cancel", label: "Cancel" })}>
591+ <Button type="button" onClick={onCancel} {...actionProps({})}>
593592 Cancel
594− </button>
595− <button type="submit" disabled={disabled} aria-busy={busy || undefined} className={actionClass({ ...action, style: action.style === "danger" ? "danger" : "primary" })}>
593+ </Button>
594+ <Button type="submit" disabled={disabled} aria-busy={busy || undefined} {...actionProps({ style: action.style === "danger" ? "danger" : "primary" })}>
596595 {busy && <LoaderCircle size={14} aria-hidden="true" className="-ml-0.5 animate-spin" />}
597596 {action.label}
598− </button>
597+ </Button>
599598 </div>
600599 </div>
601600 {invalid ? (
+83−70
4040 import { AgentPill, MemberAvatar } from "./marks";
4141 import { PersonStatusEmoji, PersonStatusLine, PresenceSummary, WithPresence } from "../presence";
4242 import { CardContext, MemberCard, type PersonCard, personCard } from "./profile-card";
43−import { Avatar } from "../ui";
43+
4444 import { localTime } from "../../lib/time-zone";
4545 import { BottomSheet, SheetRow, useBack, useSwipeBack } from "../mobile";
4646 import { MessageText, type TextContext } from "./text";
5858 AlertDialogHeader,
5959 AlertDialogTitle,
6060 } from "../ui/alert-dialog";
61+import { Avatar } from "../ui/avatar";
6162 import { Badge } from "../ui/badge";
6263 import { Hint } from "../ui/hint";
6364 import { Button } from "../ui/button";
65+import { Card } from "../ui/card";
6466 import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
6567 import { type Mentionable, type ShownMessage, channelPath, mentionNames, mergeMessages, shownHandle, shownName, timeline } from "../../lib/chat";
6668 import { codeAccessPath } from "../../lib/workspace-nav";
638640 <div className="mx-auto flex min-h-full max-w-[56rem] flex-col justify-end px-2 pt-6 pb-3 sm:px-4">
639641 {older ? (
640642 <div className="flex justify-center pb-4">
641− <button
643+ <Button
642644 type="button"
643645 onClick={() => void loadOlder()}
644− className="rounded-full border border-line px-3 py-1 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg"
646+ variant="outline"
647+ size="inline"
648+ className="rounded-full px-3 py-1 text-xs text-muted font-normal"
645649 >
646650 {loadingOlder ? "Loading…" : "Load earlier messages"}
647− </button>
651+ </Button>
648652 </div>
649653 ) : (
650654 <ConversationStart data={view} others={others} />
678682 <div className="mx-auto max-w-[56rem]">
679683 <TypingLine members={typers} />
680684 {archived ? (
681− <div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-line bg-surface px-4 py-3">
685+ <Card className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
682686 <p className="flex items-center gap-2 text-sm text-muted">
683687 <Archive size={15} className="shrink-0 text-faint" />
684688 <span>
686690 </span>
687691 </p>
688692 {canManage && (
689− <button
693+ <Button
690694 type="button"
691695 onClick={() => void updateChannel({ archived: false })}
692− className="inline-flex h-9 items-center gap-2 rounded-md border border-line px-3 text-sm font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-raised"
696+ variant="outline"
697+ className="px-3 text-fg/90 hover:bg-raised"
693698 >
694699 <ArchiveRestore size={15} />
695700 Unarchive
696− </button>
701+ </Button>
697702 )}
698− </div>
703+ </Card>
699704 ) : joined ? (
700705 <Composer draftKey={data.channel.id} placeholder={placeholder} people={people} onSend={(body) => void post(body, null)} onTyping={onTyping} autoFocus />
701706 ) : (
702− <div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-line bg-surface px-4 py-3">
707+ <Card className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
703708 <p className="text-sm text-muted">
704709 You're reading <span className="font-medium text-fg">#{name}</span>. Join to post and get its notifications.
705710 </p>
706− <button
711+ <Button
707712 type="button"
708713 onClick={() => void join()}
709− className="inline-flex h-9 items-center rounded-md bg-accent px-3.5 text-sm font-medium text-bg hover:bg-accent-hover"
714+ variant="accent"
710715 >
711716 Join channel
712− </button>
713− </div>
717+ </Button>
718+ </Card>
714719 )}
715720 </div>
716721 </div>
978983 return (
979984 <div role="separator" aria-label={label} className="relative my-3 flex items-center justify-center">
980985 <span aria-hidden="true" className="absolute inset-x-2 top-1/2 h-px bg-line" />
981− <span className="relative rounded-full border border-line bg-bg px-3 py-0.5 text-[0.6875rem] font-medium text-muted" suppressHydrationWarning>
986+ <Badge className="relative bg-bg px-3 py-0.5" suppressHydrationWarning>
982987 {label}
983− </span>
988+ </Badge>
984989 </div>
985990 );
986991 }
10661071 ) : (
10671072 <ul className="space-y-3">
10681073 {theirs.map((m) => (
1069− <li key={m.id} className="rounded-lg border border-line bg-surface px-3 py-2.5">
1070− <p className="mb-1 text-[0.6875rem] text-faint" suppressHydrationWarning>
1071− {clock(m.created_at, zone)}
1072− </p>
1073− <div className="line-clamp-4 text-sm">
1074− <MessageText body={m.body} context={context} />
1075− </div>
1076− </li>
1074+ <Card asChild key={m.id} radius="lg" className="px-3 py-2.5">
1075+ <li>
1076+ <p className="mb-1 text-[0.6875rem] text-faint" suppressHydrationWarning>
1077+ {clock(m.created_at, zone)}
1078+ </p>
1079+ <div className="line-clamp-4 text-sm">
1080+ <MessageText body={m.body} context={context} />
1081+ </div>
1082+ </li>
1083+ </Card>
10771084 ))}
10781085 </ul>
10791086 )}
12841291 {message.failed && (
12851292 <p className="mt-0.5 text-xs text-danger">
12861293 Not sent.{" "}
1287− <button type="button" onClick={onRetry} className="font-medium underline underline-offset-2 hover:text-fg">
1294+ <Button type="button" onClick={onRetry} variant="link" size="inline" className="text-inherit underline underline-offset-2 hover:text-fg">
12881295 Try again
1289− </button>
1296+ </Button>
12901297 </p>
12911298 )}
12921299 {!inThread && message.reply_count > 0 && onThread && (
1293− <button
1300+ <Button
12941301 type="button"
12951302 onClick={onThread}
1296− className="mt-1 -ml-1 flex items-center gap-2 rounded-md px-1 py-0.5 text-[0.8125rem] font-medium text-accent transition-colors hover:bg-raised"
1303+ variant="ghost"
1304+ size="inline"
1305+ className="mt-1 -ml-1 flex gap-2 px-1 py-0.5 text-[0.8125rem] text-accent"
12971306 >
12981307 <MessageSquareText size={14} />
12991308 {message.reply_count} {message.reply_count === 1 ? "reply" : "replies"}
13021311 Last reply {clock(message.last_reply_at, zone)}
13031312 </span>
13041313 )}
1305− </button>
1314+ </Button>
13061315 )}
13071316 </div>
13081317 {!inThread && onThread && !message.pending && (
1309− <div className="absolute -top-3 right-3 hidden rounded-lg border border-line bg-surface p-0.5 shadow-lg shadow-black/30 group-hover/message:flex group-focus-within/message:flex has-[[data-state=open]]:flex">
1318+ <Card radius="lg" className="absolute -top-3 right-3 hidden p-0.5 shadow-lg shadow-black/30 group-hover/message:flex group-focus-within/message:flex has-[[data-state=open]]:flex">
13101319 <AddReaction messageId={message.id} />
13111320 <Hint label="Reply in thread">
1312− <button
1321+ <Button
13131322 type="button"
13141323 aria-label="Reply in thread"
13151324 onClick={onThread}
1316− className="flex size-7 items-center justify-center rounded-md text-muted hover:bg-raised hover:text-fg"
1325+ variant="ghost"
1326+ size="icon-xs"
13171327 >
13181328 <MessageSquareText size={15} />
1319− </button>
1329+ </Button>
13201330 </Hint>
13211331 {onCopyLink && <MoreActions onCopyLink={onCopyLink} onWriteUp={onWriteUp} onEdit={onEdit} />}
1322− </div>
1332+ </Card>
13231333 )}
13241334 </article>
13251335 );
13721382 return (
13731383 <DropdownMenu>
13741384 <Hint label="Thread actions">
1375− <DropdownMenuTrigger
1376− aria-label="Thread actions"
1377− className="flex size-10 items-center justify-center rounded-full text-muted outline-none hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/50 active:bg-raised lg:size-7 lg:rounded-md"
1378− >
1379− <Ellipsis size={16} />
1385+ <DropdownMenuTrigger asChild>
1386+ <Button variant="ghost" size="icon-lg" aria-label="Thread actions" className="rounded-full active:bg-raised lg:size-7 lg:rounded-md">
1387+ <Ellipsis size={16} />
1388+ </Button>
13801389 </DropdownMenuTrigger>
13811390 </Hint>
13821391 <DropdownMenuContent align="end">
14321441 className="fixed inset-0 z-40 flex flex-col bg-bg lg:static lg:z-auto lg:w-[22rem] lg:shrink-0 lg:border-l lg:border-line xl:w-[24rem] max-md:top-(--vv-top,0px) max-md:bottom-auto max-md:h-(--vv-height,100dvh) max-md:pb-[env(safe-area-inset-bottom)]"
14331442 >
14341443 <div className="flex h-14 shrink-0 items-center gap-2 border-b border-line px-4 max-md:h-[calc(3.5rem+env(safe-area-inset-top))] max-md:pt-[env(safe-area-inset-top)] max-md:pl-1.5">
1435− <button type="button" onClick={onClose} aria-label="Back" className="flex size-10 items-center justify-center rounded-full text-muted active:bg-raised lg:hidden">
1444+ <Button type="button" onClick={onClose} aria-label="Back" variant="ghost" size="icon-lg" className="rounded-full active:bg-raised lg:hidden">
14361445 <ChevronLeft size={22} />
1437− </button>
1446+ </Button>
14381447 <h2 className="text-[0.9375rem] font-semibold">{title}</h2>
14391448 {subtitle && <span className="truncate text-sm text-faint">{subtitle}</span>}
14401449 <div className="ml-auto flex shrink-0 items-center gap-1">
14411450 {menu}
1442− <button type="button" onClick={onClose} aria-label="Close" className="hidden rounded-md p-1 text-faint hover:bg-raised hover:text-fg lg:block">
1451+ <Button type="button" onClick={onClose} aria-label="Close" variant="ghost" size="inline" className="hidden p-1 text-faint lg:block">
14431452 <X size={16} />
1444− </button>
1453+ </Button>
14451454 </div>
14461455 </div>
14471456 <div className="flex min-h-0 grow flex-col">{children}</div>
15781587 />
15791588 </span>
15801589 <div className="flex justify-end gap-1.5">
1581− <button type="button" onClick={() => setEditing(null)} className="h-7 rounded-md px-2.5 text-xs text-muted hover:bg-raised hover:text-fg">
1590+ <Button type="button" onClick={() => setEditing(null)} variant="ghost" size="xs" className="px-2.5 font-normal">
15821591 Cancel
1583− </button>
1584− <button type="submit" disabled={busy} className="h-7 rounded-md bg-accent px-2.5 text-xs font-medium text-bg hover:bg-accent-hover disabled:opacity-50">
1592+ </Button>
1593+ <Button type="submit" disabled={busy} variant="accent" size="xs" className="px-2.5">
15851594 {busy ? "Saving…" : "Save"}
1586− </button>
1595+ </Button>
15871596 </div>
15881597 </form>
15891598 );
15901599 const editButton = (field: "name" | "topic", label: string) => (
1591− <button
1600+ <Button
15921601 type="button"
15931602 onClick={() => start(field)}
15941603 aria-label={label}
1595− className="rounded px-1.5 py-0.5 text-xs text-muted opacity-0 transition-opacity group-hover/about:opacity-100 hover:bg-raised hover:text-fg focus-visible:opacity-100 [@media(hover:none)]:opacity-100"
1604+ variant="ghost"
1605+ size="inline"
1606+ className="rounded px-1.5 py-0.5 text-xs opacity-0 transition-opacity group-hover/about:opacity-100 focus-visible:opacity-100 [@media(hover:none)]:opacity-100 font-normal"
15961607 >
15971608 Edit
1598− </button>
1609+ </Button>
15991610 );
16001611 return (
16011612 <>
1602− <section className="group/about rounded-xl border border-line bg-surface p-3.5">
1603− <div className="flex items-center justify-between">
1604− <h3 className="text-xs font-medium text-faint">Name</h3>
1605− {mayRename && editing !== "name" && editButton("name", "Rename channel")}
1606− </div>
1607− {editing === "name" ? edit("name") : <p className="mt-1 text-sm text-fg-soft">#{channel.name}</p>}
1608− <div className="mt-3 flex items-center justify-between">
1609− <h3 className="text-xs font-medium text-faint">Topic</h3>
1610− {mayTopic && editing !== "topic" && editButton("topic", "Edit topic")}
1611− </div>
1612− {editing === "topic" ? edit("topic") : <p className="mt-1 text-sm text-fg-soft">{channel.topic || "No topic yet."}</p>}
1613− <h3 className="mt-3 text-xs font-medium text-faint">Visibility</h3>
1614− <p className="mt-1 flex items-center gap-1.5 text-sm text-fg-soft">
1615− {channel.private ? <Lock size={13} /> : <Hash size={13} />}
1616− {channel.private ? "Private: only its members can find and read it" : "Public: anyone in the workspace can read and join"}
1617− </p>
1618− {error && <p className="mt-2 text-xs text-danger">{error}</p>}
1619− </section>
1613+ <Card asChild className="group/about p-3.5">
1614+ <section>
1615+ <div className="flex items-center justify-between">
1616+ <h3 className="text-xs font-medium text-faint">Name</h3>
1617+ {mayRename && editing !== "name" && editButton("name", "Rename channel")}
1618+ </div>
1619+ {editing === "name" ? edit("name") : <p className="mt-1 text-sm text-fg-soft">#{channel.name}</p>}
1620+ <div className="mt-3 flex items-center justify-between">
1621+ <h3 className="text-xs font-medium text-faint">Topic</h3>
1622+ {mayTopic && editing !== "topic" && editButton("topic", "Edit topic")}
1623+ </div>
1624+ {editing === "topic" ? edit("topic") : <p className="mt-1 text-sm text-fg-soft">{channel.topic || "No topic yet."}</p>}
1625+ <h3 className="mt-3 text-xs font-medium text-faint">Visibility</h3>
1626+ <p className="mt-1 flex items-center gap-1.5 text-sm text-fg-soft">
1627+ {channel.private ? <Lock size={13} /> : <Hash size={13} />}
1628+ {channel.private ? "Private: only its members can find and read it" : "Public: anyone in the workspace can read and join"}
1629+ </p>
1630+ {error && <p className="mt-2 text-xs text-danger">{error}</p>}
1631+ </section>
1632+ </Card>
16201633 {canManage && !general && (
16211634 <button
16221635 type="button"
17691782 return (
17701783 <Popover open={open} onOpenChange={setOpen}>
17711784 <PopoverTrigger asChild>
1772− <button type="button" className="flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs text-muted hover:bg-raised hover:text-fg">
1785+ <Button type="button" variant="ghost" size="inline" className="flex gap-1 px-1.5 py-0.5 text-xs font-normal">
17731786 <UserPlus size={13} />
17741787 Add
1775− </button>
1788+ </Button>
17761789 </PopoverTrigger>
17771790 <PopoverContent align="end" className="w-72 p-1.5">
17781791 <input
+36−27
2323 import { type KeyboardEvent as ReactKeyboardEvent, type ReactNode, type RefObject, useEffect, useId, useMemo, useRef, useState } from "react";
2424
2525 import { AgentPill, MemberAvatar } from "./marks";
26+import { Button } from "../ui/button";
2627 import { Hint } from "../ui/hint";
2728 import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
2829 // `:shortcode` completion and the emoji picker (components/emoji).
622623 </button>
623624 </Hint>
624625 <Hint label="Mention someone">
625− <button
626+ <Button
626627 type="button"
627628 aria-label="Mention someone"
628629 disabled={disabled || !editor}
638639 .run();
639640 look(editor);
640641 }}
641− className="flex size-7 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
642+ variant="ghost"
643+ size="icon-xs"
644+ className="text-faint"
642645 >
643646 <AtSign size={15} />
644− </button>
647+ </Button>
645648 </Hint>
646649 <EmojiPickerPopover side="top" align="start" onPick={(picked) => editor?.chain().focus().insertContent({ type: "text", text: picked }).run()}>
647650 <Hint label="Add an emoji">
648− <button
651+ <Button
649652 type="button"
650653 aria-label="Add an emoji"
651654 disabled={disabled || !editor}
652− className="flex size-7 items-center justify-center rounded-md text-faint transition-colors hover:bg-raised hover:text-fg"
655+ variant="ghost"
656+ size="icon-xs"
657+ className="text-faint"
653658 >
654659 <Smile size={15} />
655− </button>
660+ </Button>
656661 </Hint>
657662 </EmojiPickerPopover>
658663 {editing ? (
661666 <kbd className="font-sans">Esc</kbd> to cancel · <kbd className="font-sans">Enter</kbd> to save
662667 </span>
663668 <span className="ml-auto flex items-center gap-1.5">
664− <button
669+ <Button
665670 type="button"
666671 onClick={onCancel}
667− className="h-8 rounded-lg border border-line px-3 text-[0.8125rem] font-medium text-fg-soft transition-colors hover:border-line-strong hover:bg-raised"
672+ variant="outline"
673+ size="sm"
674+ className="rounded-lg px-3 text-fg-soft hover:bg-raised"
668675 >
669676 Cancel
670− </button>
671− <button
677+ </Button>
678+ <Button
672679 type="button"
673680 disabled={disabled || !editor || state?.empty}
674681 onClick={send}
675− className="h-8 rounded-lg bg-accent px-3 text-[0.8125rem] font-medium text-bg transition-colors hover:bg-accent-hover disabled:bg-line disabled:text-faint"
682+ variant="accent"
683+ size="sm"
684+ className="rounded-lg px-3 disabled:bg-line disabled:text-faint disabled:opacity-100"
676685 >
677686 Save
678− </button>
687+ </Button>
679688 </span>
680689 </>
681690 ) : (
683692 <span className="ml-1 hidden text-[0.6875rem] text-faint sm:inline">
684693 <kbd className="font-sans">Enter</kbd> to send · <kbd className="font-sans">Shift+Enter</kbd> for a new line
685694 </span>
686− <button
695+ <Button
687696 type="button"
688697 aria-label="Send"
689698 disabled={disabled || !editor || state?.empty !== false}
690699 onMouseDown={(event) => event.preventDefault()}
691700 onClick={send}
692− className="ml-auto flex size-8 items-center justify-center rounded-lg bg-accent text-bg transition-[background-color,opacity] hover:bg-accent-hover disabled:bg-line disabled:text-faint"
701+ variant="accent"
702+ size="icon-sm"
703+ className="ml-auto rounded-lg transition-[background-color,opacity] disabled:bg-line disabled:text-faint disabled:opacity-100"
693704 >
694705 <ArrowUp size={16} strokeWidth={2.4} />
695− </button>
706+ </Button>
696707 </>
697708 )}
698709 </div>
706717 return (
707718 <Popover>
708719 <Hint label="Formatting shortcuts">
709− <PopoverTrigger
710− aria-label="Formatting shortcuts"
711− onMouseDown={(event) => event.preventDefault()}
712− className="ml-auto flex size-8 shrink-0 items-center justify-center rounded-md text-faint transition-colors outline-none hover:bg-raised hover:text-fg focus-visible:ring-2 focus-visible:ring-accent/50 sm:size-7"
713− >
714− <Keyboard size={15} />
720+ <PopoverTrigger asChild>
721+ <Button variant="ghost" size="icon-sm" aria-label="Formatting shortcuts" onMouseDown={(event) => event.preventDefault()} className="ml-auto text-faint sm:size-7">
722+ <Keyboard size={15} />
723+ </Button>
715724 </PopoverTrigger>
716725 </Hint>
717726 <PopoverContent side="top" align="end" className="w-80 p-3" onOpenAutoFocus={(event) => event.preventDefault()}>
823832 </div>
824833 <div className="flex items-center gap-1.5 pt-0.5">
825834 {onRemove && (
826− <button type="button" onClick={onRemove} className="flex h-8 items-center gap-1.5 rounded-md px-2 text-[0.8125rem] text-muted hover:bg-line hover:text-fg">
835+ <Button type="button" onClick={onRemove} variant="ghost" size="sm" className="flex px-2 hover:bg-line font-normal">
827836 <Unlink size={14} />
828837 Remove link
829− </button>
838+ </Button>
830839 )}
831− <button type="button" onClick={onClose} className="ml-auto h-8 rounded-md px-3 text-[0.8125rem] text-muted hover:bg-line hover:text-fg">
840+ <Button type="button" onClick={onClose} variant="ghost" size="sm" className="ml-auto px-3 hover:bg-line font-normal">
832841 Cancel
833− </button>
834− <button type="submit" disabled={!href.trim()} className="h-8 rounded-md bg-accent px-3 text-[0.8125rem] font-medium text-bg hover:bg-accent-hover disabled:bg-line disabled:text-faint">
842+ </Button>
843+ <Button type="submit" disabled={!href.trim()} variant="accent" size="sm" className="px-3 disabled:bg-line disabled:text-faint disabled:opacity-100">
835844 {initial.existing ? "Save" : "Add link"}
836− </button>
845+ </Button>
837846 </div>
838847 </form>
839848 );
+2−1
33 import { useParams } from "react-router";
44
55 import { CreateChannelButton, NewMessageButton } from "./actions";
6+import { Card } from "../ui/card";
67
78 /** A calm full-height panel for a conversation that cannot be shown. */
89 function Panel({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) {
910 return (
1011 <div className="flex h-(--page-h) items-center justify-center px-6">
1112 <div className="max-w-sm text-center">
12− <div className="mx-auto flex size-12 items-center justify-center rounded-xl border border-line bg-surface text-muted">{icon}</div>
13+ <Card className="mx-auto flex size-12 items-center justify-center text-muted">{icon}</Card>
1314 <h1 className="mt-4 text-base font-semibold">{title}</h1>
1415 <div className="mt-1.5 text-sm text-muted">{children}</div>
1516 </div>
+2−1
44
55 import { AgentAvatar } from "../agent-avatar";
66 import { WithPresence } from "../presence";
7−import { Avatar } from "../ui";
7+
88 import { cn } from "../../lib/cn";
9+import { Avatar } from "../ui/avatar";
910
1011 /**
1112 * A workspace agent's face when it has no picture: a sparkle on a lavender
+13−13
1010 import { BottomSheet } from "../mobile";
1111 import { PresenceSummary, WithPresence } from "../presence";
1212 import { isOrchestrator } from "../orchestrator";
13−import { Avatar } from "../ui";
13+
14+import { ButtonLink } from "../ui";
15+import { Avatar } from "../ui/avatar";
16+import { Button } from "../ui/button";
1417 import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
1518 import { Skeleton } from "../ui/skeleton";
1619 import { formatDollars } from "../../lib/agent-form";
7780 };
7881 }
7982
80−const PRIMARY = "inline-flex h-9 grow items-center justify-center gap-1.5 rounded-lg bg-accent px-3 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-60 max-md:h-11";
81−const QUIET = "inline-flex h-9 grow items-center justify-center gap-1.5 rounded-lg border border-line px-3 text-sm font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-raised max-md:h-11";
8283
8384 function PersonBody({ username, display, avatar, ctx, open, close }: { username: string; display: string; avatar: string | null; ctx: CardContextValue; open: boolean; close: () => void }) {
8485 const card = usePersonCard(ctx.slug, username, open);
133134 )}
134135 </div>
135136 <div className="mt-4 flex gap-2">
136− <button type="button" disabled={message.busy} onClick={() => void message.open(`user:${username}`)} className={PRIMARY}>
137+ <Button type="button" disabled={message.busy} onClick={() => void message.open(`user:${username}`)} variant="accent" className="grow rounded-lg max-md:h-11">
137138 <MessageSquare size={15} />
138139 {message.busy ? "Opening…" : "Message"}
139− </button>
140− <button
140+ </Button>
141+ <Button
141142 type="button"
142143 onClick={() => {
143144 close();
144145 ctx.onViewProfile(username);
145146 }}
146− className={QUIET}
147− >
147+ variant="outline" className="grow rounded-lg max-md:h-11">
148148 <UserRound size={15} />
149149 View profile
150− </button>
150+ </Button>
151151 </div>
152152 </div>
153153 );
192192 )}
193193 </div>
194194 <div className="mt-4 flex gap-2">
195− <button type="button" disabled={message.busy} onClick={() => void message.open(`agent:${agent.id}`)} className={PRIMARY}>
195+ <Button type="button" disabled={message.busy} onClick={() => void message.open(`agent:${agent.id}`)} variant="accent" className="grow rounded-lg max-md:h-11">
196196 <MessageSquare size={15} />
197197 {message.busy ? "Opening…" : "Message"}
198− </button>
199− <Link to={`/${ctx.slug}/-/agents/${agent.handle}`} className={QUIET}>
198+ </Button>
199+ <ButtonLink to={`/${ctx.slug}/-/agents/${agent.handle}`} variant="outline" className="grow rounded-lg max-md:h-11">
200200 Profile
201− </Link>
201+ </ButtonLink>
202202 </div>
203203 </div>
204204 );
+5−3
1111 import { AgentAvatar } from "../agent-avatar";
1212 import { PersonStatusEmoji } from "../presence";
1313 import { isOrchestrator } from "../orchestrator";
14+import { Button } from "../ui/button";
1415 import { Hint } from "../ui/hint";
1516 import { Skeleton } from "../ui/skeleton";
1617 import { type ChatFilter, agentDmOf, channelPath, filterEntries, sections } from "../../lib/chat";
372373 function PinButton({ pinned, onPin, label }: { pinned: boolean; onPin: () => void; label: string }) {
373374 return (
374375 <Hint label={pinned ? "Unpin" : "Pin to the top"}>
375− <button
376+ <Button
376377 type="button"
377378 aria-label={`${pinned ? "Unpin" : "Pin"} ${label}`}
378379 onClick={(event) => {
380381 event.stopPropagation();
381382 onPin();
382383 }}
383− className="absolute top-1/2 right-1 hidden size-6 -translate-y-1/2 items-center justify-center rounded bg-raised text-faint group-hover/row:flex group-focus-within/row:flex hover:bg-line hover:text-fg [@media(hover:none)]:hidden"
384+ variant="secondary" size="icon"
385+ className="absolute top-1/2 right-1 hidden size-6 -translate-y-1/2 rounded text-faint group-hover/row:flex group-focus-within/row:flex hover:text-fg [@media(hover:none)]:hidden"
384386 >
385387 {pinned ? <PinOff size={13} /> : <Pin size={13} />}
386− </button>
388+ </Button>
387389 </Hint>
388390 );
389391 }
+5−3
33 import { Link } from "react-router";
44
55 import { MemberCard } from "./profile-card";
6+import { Button } from "../ui/button";
67 import { Hint } from "../ui/hint";
78 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table";
89 import { type Block, type Span, blocks, onlyEmoji } from "../../lib/chat";
252253 <div className="absolute top-1.5 right-1.5 flex items-center gap-2 opacity-0 transition-opacity group-hover/code:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100">
253254 {language && <span className="font-mono text-[0.6875rem] text-faint pointer-coarse:hidden">{language}</span>}
254255 <Hint label={copied ? "Copied" : "Copy code"}>
255− <button
256+ <Button
256257 type="button"
257258 aria-label="Copy code"
258259 onClick={() => {
261262 setTimeout(() => setCopied(false), 1500);
262263 });
263264 }}
264− className="flex size-6 items-center justify-center rounded-md border border-line bg-raised text-muted hover:text-fg"
265+ variant="outline" size="icon"
266+ className="size-6 bg-raised text-muted"
265267 >
266268 {copied ? <Check size={12} className="text-success" /> : <Copy size={12} />}
267− </button>
269+ </Button>
268270 </Hint>
269271 </div>
270272 </div>
+12−8
33
44 import type { FoliosSidebar, Result } from "@g1t/contracts";
55
6+import { Button } from "../ui/button";
7+import { Card } from "../ui/card";
68 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
79 import { Field, FieldError, FieldLabel } from "../ui/field";
810 import { Input } from "../ui/input";
144146 />
145147 </Field>
146148 {body && (
147− <div className="rounded-lg border border-line bg-bg/60 p-3">
149+ <Card tone="plain" radius="lg" className="bg-bg/60 p-3">
148150 <p className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted">
149151 <FileText size={13} aria-hidden="true" />
150152 Posts in this thread, as you
151153 </p>
152154 <p className="text-[0.8125rem] leading-snug break-words text-fg-soft">{body}</p>
153− </div>
155+ </Card>
154156 )}
155157 <FieldError>{error}</FieldError>
156158 <DialogFooter>
157− <button
159+ <Button
158160 type="button"
159161 onClick={() => onOpenChange(false)}
160− className="inline-flex h-9 items-center rounded-md border border-line px-3 text-sm font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-surface"
162+ variant="outline"
163+ className="px-3 text-fg/90"
161164 >
162165 Cancel
163− </button>
164− <button
166+ </Button>
167+ <Button
165168 type="submit"
166169 disabled={!body || busy}
167− className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md bg-accent px-3.5 text-sm font-medium text-bg transition-colors hover:bg-accent-hover disabled:opacity-50"
170+ variant="accent"
171+ className="gap-1.5"
168172 >
169173 {busy && <LoaderCircle size={14} className="animate-spin" aria-hidden="true" />}
170174 {busy ? "Asking…" : `Ask @${writer.handle}`}
171− </button>
175+ </Button>
172176 </DialogFooter>
173177 </form>
174178 </DialogContent>
+4−6
44
55 import { type ChecklistItem, dismiss, isDismissed, progress } from "../lib/checklist";
66 import { cn } from "../lib/cn";
7+import { Button } from "./ui/button";
78 import { Hint } from "./ui/hint";
89
910 const storage = () => (typeof window === "undefined" ? null : window.localStorage);
5051 <span className="block h-full rounded-full bg-success" style={{ width: `${(done / total) * 100}%` }} />
5152 </span>
5253 <Hint label="Dismiss for this project">
53− <button
54+ <Button
5455 type="button"
5556 onClick={() => {
5657 dismiss(storage, base);
5758 for (const listener of listeners) listener();
58− }}
59− className="ml-auto rounded-md p-1 text-faint transition-colors hover:bg-raised hover:text-fg"
60− aria-label="Dismiss the checklist for this project"
61− >
59+ }} variant="ghost" size="inline" className="ml-auto p-1 text-faint" aria-label="Dismiss the checklist for this project">
6260 <X size={14} />
63− </button>
61+ </Button>
6462 </Hint>
6563 </div>
6664 <ol className="grid border-t border-line sm:grid-cols-2 lg:grid-cols-3">
+42−37
1616 } from "@g1t/contracts";
1717
1818 import { Badge } from "./ui/badge";
19+import { Card } from "./ui/card";
1920 import { Skeleton } from "./ui/skeleton";
2021 import { UserCard } from "./user-card";
2122
104105 export function CodeownersReportPanel({ report, base, branch }: { report: CodeownersReport | null; base: string; branch: string }) {
105106 if (!report) {
106107 return (
107− <p className="rounded-xl border border-line bg-surface p-4 text-sm text-muted">
108− The CODEOWNERS file could not be read just now. Reload the page to try again.
109− </p>
108+ <Card asChild className="p-4 text-sm text-muted">
109+ <p>
110+ The CODEOWNERS file could not be read just now. Reload the page to try again.
111+ </p>
112+ </Card>
110113 );
111114 }
112115 if (!report.path) {
113116 return (
114− <div className="rounded-xl border border-dashed border-line p-4 text-sm">
117+ <Card tone="plain" className="border-dashed p-4 text-sm">
115118 <p className="font-medium">No CODEOWNERS file</p>
116119 <p className="mt-1 text-muted">
117120 g1t looks on {branch} for the first of these, and asks the owners it names to review changes to their files:
128131 How to write one
129132 </a>
130133 </p>
131− </div>
134+ </Card>
132135 );
133136 }
134137 const path = report.path;
135138 const errors = report.errors.length;
136139 return (
137− <div className="overflow-hidden rounded-xl border border-line">
140+ <Card tone="plain" className="overflow-hidden">
138141 <div className="flex flex-wrap items-center gap-x-3 gap-y-1 bg-surface px-4 py-3 text-sm">
139142 {errors > 0 ? (
140143 <FileWarning size={16} className="shrink-0 text-danger" />
158161 <CodeownersErrorList errors={report.errors} lineHref={(line) => blobHref(base, branch, path, line)} />
159162 </div>
160163 )}
161− </div>
164+ </Card>
162165 );
163166 }
164167
165168 /** The panel's shape while the file is read. */
166169 export function CodeownersReportSkeleton() {
167170 return (
168− <div aria-busy="true" className="flex items-center gap-3 rounded-xl border border-line bg-surface px-4 py-3">
171+ <Card aria-busy="true" className="flex items-center gap-3 px-4 py-3">
169172 <Skeleton className="size-4 rounded-full" />
170173 <Skeleton className="h-3 w-36" />
171174 <Skeleton className="h-3 w-20" />
172175 <Skeleton className="ml-auto h-3 w-14" />
173− </div>
176+ </Card>
174177 );
175178 }
176179
316319 }) {
317320 const fileHref = (line: number) => blobHref(base, branch, owners.path, line);
318321 return (
319− <section className="overflow-hidden rounded-xl border border-line" aria-label="Code owners">
320− <div className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-line bg-surface px-4 py-2.5 text-sm">
321− <span className="flex items-center gap-2 font-medium">
322− <ShieldCheck size={15} className="shrink-0 text-faint" />
323− Code owners
324− </span>
325− <Link to={blobHref(base, branch, owners.path)} className="font-mono text-xs text-muted hover:text-fg hover:underline">
326− {owners.path}
327− </Link>
328− <span className="ml-auto flex items-center gap-2">
329− {owners.errors > 0 && (
330− <Link to={errorsHref} className="text-xs text-danger hover:underline">
331− {owners.errors === 1 ? "1 error" : `${owners.errors} errors`} in the file
332− </Link>
333− )}
334− {owners.required ? <Badge tone="warn">Approval required</Badge> : <Badge>Approval not required</Badge>}
335− </span>
336− </div>
337− {owners.reviews.length === 0 ? (
338− <p className="px-4 py-3 text-sm text-muted">No rule in the file covers what this changes.</p>
339− ) : (
340− <ul className="divide-y divide-line">
341− {owners.reviews.map((review) => (
342− <OwnerReviewRow key={`${review.section ?? ""}:${review.line}`} review={review} fileHref={fileHref} />
343− ))}
344− </ul>
345− )}
346− </section>
322+ <Card asChild tone="plain" className="overflow-hidden">
323+ <section aria-label="Code owners">
324+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-line bg-surface px-4 py-2.5 text-sm">
325+ <span className="flex items-center gap-2 font-medium">
326+ <ShieldCheck size={15} className="shrink-0 text-faint" />
327+ Code owners
328+ </span>
329+ <Link to={blobHref(base, branch, owners.path)} className="font-mono text-xs text-muted hover:text-fg hover:underline">
330+ {owners.path}
331+ </Link>
332+ <span className="ml-auto flex items-center gap-2">
333+ {owners.errors > 0 && (
334+ <Link to={errorsHref} className="text-xs text-danger hover:underline">
335+ {owners.errors === 1 ? "1 error" : `${owners.errors} errors`} in the file
336+ </Link>
337+ )}
338+ {owners.required ? <Badge tone="warn">Approval required</Badge> : <Badge>Approval not required</Badge>}
339+ </span>
340+ </div>
341+ {owners.reviews.length === 0 ? (
342+ <p className="px-4 py-3 text-sm text-muted">No rule in the file covers what this changes.</p>
343+ ) : (
344+ <ul className="divide-y divide-line">
345+ {owners.reviews.map((review) => (
346+ <OwnerReviewRow key={`${review.section ?? ""}:${review.line}`} review={review} fileHref={fileHref} />
347+ ))}
348+ </ul>
349+ )}
350+ </section>
351+ </Card>
347352 );
348353 }
+1−1
99 import { searchHref } from "../lib/search";
1010 import { hitIcon } from "./search";
1111 import type { PaletteCommand } from "./command-palette";
12−import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "./ui/combobox";
12+import { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from "./ui/command";
1313 import { SkeletonRows } from "./ui/skeleton";
1414
1515 /** How long typing has to pause before results are asked for. */
+2−1
33
44 import { type CommitPerson, type ShownCommit, profileHref, shownName } from "../lib/commit-people";
55 import { cn } from "../lib/cn";
6−import { Avatar } from "./ui";
6+
77 import { UserCard } from "./user-card";
8+import { Avatar } from "./ui/avatar";
89
910 /**
1011 * A person on a commit, as an avatar: theirs and their card when the
+32−27
4141 import { Badge } from "./ui/badge";
4242 import { Hint } from "./ui/hint";
4343 import { Input } from "./ui";
44+import { Card } from "./ui/card";
4445
4546 const CATEGORY_ICONS: Record<CategoryFilter, ReactNode> = {
4647 all: <LayoutGrid size={15} />,
190191 </div>
191192
192193 {empty && (
193− <p className="mt-8 rounded-xl border border-dashed border-line px-4 py-8 text-center text-sm text-muted">
194− Nothing matches “{query.trim()}”.{" "}
195− <a
196− href={askHref({ connector: { id: "other", name: query.trim() || "something else" }, scope, workspace, address: CONTACT.support })}
197− className="text-accent hover:underline"
198− >
199− Ask for it
200− </a>
201− , and say what you would use it for.
202− </p>
194+ <Card asChild tone="plain" className="mt-8 border-dashed px-4 py-8 text-center text-sm text-muted">
195+ <p>
196+ Nothing matches “{query.trim()}”.{" "}
197+ <a
198+ href={askHref({ connector: { id: "other", name: query.trim() || "something else" }, scope, workspace, address: CONTACT.support })}
199+ className="text-accent hover:underline"
200+ >
201+ Ask for it
202+ </a>
203+ , and say what you would use it for.
204+ </p>
205+ </Card>
203206 )}
204207
205208 {found.connected.length > 0 && (
360363 /** A short line saying who a page's connections are for, and where the other kind lives. */
361364 export function ScopeNote({ scope, workspace }: { scope: ConnectorScope; workspace: string | null }) {
362365 return (
363− <p className="mb-6 rounded-xl border border-line bg-surface/60 px-4 py-3 text-sm text-muted">
364− {scope === "workspace" ? (
365− <>
366− Connected here, a tool works <span className="text-fg">for everyone in {workspace}</span>: its people and its agents.
367− Your own accounts, such as your calendar, are under{" "}
368− <Link to="/settings/integrations" className="text-accent hover:underline">
369− your integrations
370− </Link>
371− .
372− </>
373− ) : (
374− <>
375− Connected here, a tool works <span className="text-fg">just for you, in every workspace</span> you belong to. Tools
376− the whole team shares are connected by a workspace owner, under the workspace's Integrations.
377− </>
378− )}
379− </p>
366+ <Card asChild tone="plain" className="mb-6 bg-surface/60 px-4 py-3 text-sm text-muted">
367+ <p>
368+ {scope === "workspace" ? (
369+ <>
370+ Connected here, a tool works <span className="text-fg">for everyone in {workspace}</span>: its people and its agents.
371+ Your own accounts, such as your calendar, are under{" "}
372+ <Link to="/settings/integrations" className="text-accent hover:underline">
373+ your integrations
374+ </Link>
375+ .
376+ </>
377+ ) : (
378+ <>
379+ Connected here, a tool works <span className="text-fg">just for you, in every workspace</span> you belong to. Tools
380+ the whole team shares are connected by a workspace owner, under the workspace's Integrations.
381+ </>
382+ )}
383+ </p>
384+ </Card>
380385 );
381386 }
+106−85
4646
4747 import { InlineMarkdown } from "./inline-markdown";
4848 import { SubmitButton, TimeAgo } from "./ui";
49+import { Button } from "./ui/button";
50+import { Card } from "./ui/card";
4951 import { Hint } from "./ui/hint";
5052 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
5153 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
196198 {fetcher.data?.error && <p className="mt-1.5 text-xs text-danger">{fetcher.data.error}</p>}
197199 </div>
198200 <div className="flex shrink-0 items-start gap-1.5">
199− <button
201+ <Button
200202 type="button"
201203 onClick={() => fetcher.submit({ intent: "keep", id: memory.id }, { method: "post", action })}
202− className="inline-flex items-center gap-1 rounded-md bg-fg px-2 py-1 text-xs font-medium text-bg hover:bg-fg-hover"
204+ size="inline"
205+ className="gap-1 px-2 py-1 text-xs"
203206 >
204207 <Check size={12} />
205208 Keep
206− </button>
209+ </Button>
207210 <EditAndKeep memory={memory} action={action} />
208211 <Hint label="Dismiss: never suggested again in these words">
209− <button
212+ <Button
210213 type="button"
211214 aria-label="Dismiss"
212215 onClick={() => fetcher.submit({ intent: "dismiss", id: memory.id }, { method: "post", action })}
213− className="inline-flex items-center gap-1 rounded-md border border-line px-2 py-1 text-xs text-muted hover:border-line-strong hover:text-danger"
216+ variant="outline"
217+ size="inline"
218+ className="gap-1 px-2 py-1 text-xs text-muted hover:text-danger font-normal"
214219 >
215220 <X size={12} />
216221 Dismiss
217− </button>
222+ </Button>
218223 </Hint>
219224 </div>
220225 </li>
227232 return <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">{empty}</p>;
228233 }
229234 return (
230− <ul className="divide-y divide-line rounded-xl border border-line bg-surface">
231− {candidates.map((memory) => (
232− <Candidate key={memory.id} memory={memory} action={action} />
233− ))}
234− </ul>
235+ <Card asChild divided>
236+ <ul>
237+ {candidates.map((memory) => (
238+ <Candidate key={memory.id} memory={memory} action={action} />
239+ ))}
240+ </ul>
241+ </Card>
235242 );
236243 }
237244
269276 );
270277 const edges = catalog.relations.filter((relation) => relation.kind === "depends_on" && at.has(relation.from) && at.has(relation.to));
271278 return (
272− <figure className="rounded-xl border border-line bg-surface p-3">
273− <svg viewBox={`0 0 ${size} ${size}`} className="mx-auto block h-auto w-full max-w-md" role="img" aria-label="How the workspace's projects depend on each other">
274− <defs>
275− <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
276− <path d="M 0 0 L 10 5 L 0 10 z" className="fill-muted" />
277− </marker>
278− </defs>
279− {edges.map((edge) => {
280− const from = at.get(edge.from)!;
281− const to = at.get(edge.to)!;
282− const dx = to.x - from.x;
283− const dy = to.y - from.y;
284− const length = Math.hypot(dx, dy) || 1;
285− const pad = 26;
286− return (
287− <line
288− key={`${edge.from}-${edge.to}`}
289− x1={from.x + (dx / length) * pad}
290− y1={from.y + (dy / length) * pad}
291− x2={to.x - (dx / length) * pad}
292− y2={to.y - (dy / length) * pad}
293− className="stroke-muted"
294− strokeWidth={1.25}
295− markerEnd="url(#arrow)"
296− />
297− );
298− })}
299− {[...at.values()].map(({ x, y, project }) => (
300− <g key={project.id}>
301− <circle cx={x} cy={y} r={20} className="fill-bg stroke-accent" strokeWidth={1.5} />
302− <text x={x} y={y + 36} textAnchor="middle" className="fill-fg text-[11px]">
303− {project.name.length > 16 ? `${project.name.slice(0, 15)}…` : project.name}
304− </text>
305− <text x={x} y={y + 4} textAnchor="middle" className="fill-accent text-[11px] font-semibold">
306− {project.name[0]?.toUpperCase()}
307− </text>
308− </g>
309− ))}
310− </svg>
311− <figcaption className="mt-1 text-center text-xs text-faint">An arrow points from a project to one it uses.</figcaption>
312− </figure>
279+ <Card asChild className="p-3">
280+ <figure>
281+ <svg viewBox={`0 0 ${size} ${size}`} className="mx-auto block h-auto w-full max-w-md" role="img" aria-label="How the workspace's projects depend on each other">
282+ <defs>
283+ <marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
284+ <path d="M 0 0 L 10 5 L 0 10 z" className="fill-muted" />
285+ </marker>
286+ </defs>
287+ {edges.map((edge) => {
288+ const from = at.get(edge.from)!;
289+ const to = at.get(edge.to)!;
290+ const dx = to.x - from.x;
291+ const dy = to.y - from.y;
292+ const length = Math.hypot(dx, dy) || 1;
293+ const pad = 26;
294+ return (
295+ <line
296+ key={`${edge.from}-${edge.to}`}
297+ x1={from.x + (dx / length) * pad}
298+ y1={from.y + (dy / length) * pad}
299+ x2={to.x - (dx / length) * pad}
300+ y2={to.y - (dy / length) * pad}
301+ className="stroke-muted"
302+ strokeWidth={1.25}
303+ markerEnd="url(#arrow)"
304+ />
305+ );
306+ })}
307+ {[...at.values()].map(({ x, y, project }) => (
308+ <g key={project.id}>
309+ <circle cx={x} cy={y} r={20} className="fill-bg stroke-accent" strokeWidth={1.5} />
310+ <text x={x} y={y + 36} textAnchor="middle" className="fill-fg text-[11px]">
311+ {project.name.length > 16 ? `${project.name.slice(0, 15)}…` : project.name}
312+ </text>
313+ <text x={x} y={y + 4} textAnchor="middle" className="fill-accent text-[11px] font-semibold">
314+ {project.name[0]?.toUpperCase()}
315+ </text>
316+ </g>
317+ ))}
318+ </svg>
319+ <figcaption className="mt-1 text-center text-xs text-faint">An arrow points from a project to one it uses.</figcaption>
320+ </figure>
321+ </Card>
313322 );
314323 }
315324
382391 )}
383392 {!kind && !project && <RelationsGraph catalog={catalog} />}
384393 {shown.length === 0 ? (
385− <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">
386− Nothing here yet. The catalog builds itself from each project's default branch: its manifests, docs and
387− workflows, with its deployments and integrations.
388− </p>
394+ <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
395+ <p>
396+ Nothing here yet. The catalog builds itself from each project's default branch: its manifests, docs and
397+ workflows, with its deployments and integrations.
398+ </p>
399+ </Card>
389400 ) : (
390− <ul className="divide-y divide-line rounded-xl border border-line bg-surface">
391− {shown.slice(0, 500).map((entity) => (
392− <EntityRow key={entity.id} entity={entity} names={names} />
393− ))}
394− </ul>
401+ <Card asChild divided>
402+ <ul>
403+ {shown.slice(0, 500).map((entity) => (
404+ <EntityRow key={entity.id} entity={entity} names={names} />
405+ ))}
406+ </ul>
407+ </Card>
395408 )}
396409 {catalog.builtAt && (
397410 <p className="text-xs text-faint">
457470 className="w-full rounded-md border border-line bg-bg py-2 pr-3 pl-9 text-sm outline-none placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
458471 />
459472 </label>
460− <button type="submit" className="rounded-md bg-fg px-3.5 py-2 text-sm font-medium text-bg hover:bg-fg-hover">
473+ <Button type="submit">
461474 {searching ? "Searching…" : "Search"}
462− </button>
475+ </Button>
463476 </Form>
464477 {result &&
465478 (result.hits.length === 0 ? (
466− <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">Nothing matched “{result.query}”.</p>
479+ <Card asChild tone="plain" className="border-dashed px-4 py-6 text-sm text-muted">
480+ <p>Nothing matched “{result.query}”.</p>
481+ </Card>
467482 ) : (
468483 <>
469− <ul className="divide-y divide-line rounded-xl border border-line bg-surface">
470− {result.hits.map((hit) => (
471− <Hit key={`${hit.kind}:${hit.id}`} hit={hit} />
472− ))}
473− </ul>
484+ <Card asChild divided>
485+ <ul>
486+ {result.hits.map((hit) => (
487+ <Hit key={`${hit.kind}:${hit.id}`} hit={hit} />
488+ ))}
489+ </ul>
490+ </Card>
474491 <p className="text-xs text-faint">
475492 {result.mode === "semantic" ? "Ranked by meaning, then by matching words." : "Matched by words: the search index did not answer."}
476493 </p>
504521 <SubmitButton
505522 fetcher={fetcher}
506523 pending="Opening…"
507− className="inline-flex shrink-0 items-center gap-1 rounded-md border border-line px-2 py-1 text-xs text-muted hover:border-accent-dim hover:text-fg disabled:opacity-50"
524+ variant="outline"
525+ size="inline"
526+ className="gap-1 px-2 py-1 text-xs text-muted hover:border-accent-dim font-normal"
508527 >
509528 <Bot size={12} />
510529 Fix with an agent
522541 return (
523542 <div className="grid gap-3 md:grid-cols-2">
524543 {cards.map((card) => (
525− <section key={card.project} className="rounded-xl border border-line bg-surface p-4">
526− <div className="flex items-baseline justify-between gap-3">
527− <Link to={`/${card.repo.namespace}/${card.repo.name}`} className="font-medium hover:text-accent">
528− {card.name}
529− </Link>
530− <span className={`text-xs ${card.passed === card.total ? "text-success" : "text-muted"}`}>
531− {card.passed} of {card.total}
532− </span>
533− </div>
534− <ul className="mt-2">
535− {card.rules.map((rule) => (
536− <Rule key={rule.rule} rule={rule} project={card.project} action={action} />
537− ))}
538− </ul>
539− </section>
544+ <Card asChild key={card.project} className="p-4">
545+ <section>
546+ <div className="flex items-baseline justify-between gap-3">
547+ <Link to={`/${card.repo.namespace}/${card.repo.name}`} className="font-medium hover:text-accent">
548+ {card.name}
549+ </Link>
550+ <span className={`text-xs ${card.passed === card.total ? "text-success" : "text-muted"}`}>
551+ {card.passed} of {card.total}
552+ </span>
553+ </div>
554+ <ul className="mt-2">
555+ {card.rules.map((rule) => (
556+ <Rule key={rule.rule} rule={rule} project={card.project} action={action} />
557+ ))}
558+ </ul>
559+ </section>
560+ </Card>
540561 ))}
541562 </div>
542563 );
+66−63
1212 import { type PointerEvent, useEffect, useMemo, useRef, useState } from "react";
1313
1414 import { type Level, buildCalendar, dayLabel, totalLabel } from "../lib/contribution-calendar";
15+import { Card } from "./ui/card";
1516 import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
1617
1718 /** A square's side and the space between squares, in pixels. */
6566
6667 const heading = totalLabel(total);
6768 return (
68− <section aria-label="Contributions" className="rounded-xl border border-line bg-surface/50 px-4 pt-3.5 pb-3">
69− <h2 className="text-sm font-medium">{heading}</h2>
70− <div ref={scroller} className="mt-3 overflow-x-auto overflow-y-hidden pb-1 [scrollbar-width:thin]">
71− <div className="flex w-max text-[0.625rem] leading-none text-faint">
72− {/* The weekdays, under the months' row. */}
73− <div aria-hidden="true" className="mr-1.5 flex shrink-0 flex-col" style={{ gap: GAP, paddingTop: 16 }}>
74− {WEEKDAYS.map((label, index) => (
75− <span key={index} className="flex items-center" style={{ height: CELL }}>
76− {label}
77− </span>
78− ))}
79− </div>
80− <div
81− role="img"
82− aria-label={heading}
83− className="relative"
84− onPointerOver={over}
85− // A tap's pointer leaves as the finger lifts: its hint stays until the next tap elsewhere.
86− onPointerLeave={(event) => event.pointerType === "mouse" && setHovered(null)}
87− >
88− <div aria-hidden="true" className="relative h-4" style={{ width: calendar.weeks.length * STEP - GAP }}>
89− {calendar.months.map((month) => (
90− <span key={`${month.week}:${month.label}`} className="absolute top-0" style={{ left: month.week * STEP }}>
91− {month.label}
69+ <Card asChild tone="plain" className="bg-surface/50 px-4 pt-3.5 pb-3">
70+ <section aria-label="Contributions">
71+ <h2 className="text-sm font-medium">{heading}</h2>
72+ <div ref={scroller} className="mt-3 overflow-x-auto overflow-y-hidden pb-1 [scrollbar-width:thin]">
73+ <div className="flex w-max text-[0.625rem] leading-none text-faint">
74+ {/* The weekdays, under the months' row. */}
75+ <div aria-hidden="true" className="mr-1.5 flex shrink-0 flex-col" style={{ gap: GAP, paddingTop: 16 }}>
76+ {WEEKDAYS.map((label, index) => (
77+ <span key={index} className="flex items-center" style={{ height: CELL }}>
78+ {label}
9279 </span>
9380 ))}
9481 </div>
95− <div className="flex" style={{ gap: GAP }}>
96− {calendar.weeks.map((week, column) => (
97− <div key={column} className="flex flex-col" style={{ gap: GAP }}>
98− {week.map((day, row) =>
99− day ? (
100− <span
101− key={row}
102− data-date={day.date}
103− data-count={day.count}
104− data-commits={day.commits}
105− className={`block rounded-[2px] ${SHADES[day.level]} ${hovered?.date === day.date ? "ring-1 ring-fg/60" : ""}`}
106− style={{ width: CELL, height: CELL }}
107− />
108− ) : (
109− <span key={row} className="block" style={{ width: CELL, height: CELL }} />
110− ),
111− )}
112− </div>
113− ))}
82+ <div
83+ role="img"
84+ aria-label={heading}
85+ className="relative"
86+ onPointerOver={over}
87+ // A tap's pointer leaves as the finger lifts: its hint stays until the next tap elsewhere.
88+ onPointerLeave={(event) => event.pointerType === "mouse" && setHovered(null)}
89+ >
90+ <div aria-hidden="true" className="relative h-4" style={{ width: calendar.weeks.length * STEP - GAP }}>
91+ {calendar.months.map((month) => (
92+ <span key={`${month.week}:${month.label}`} className="absolute top-0" style={{ left: month.week * STEP }}>
93+ {month.label}
94+ </span>
95+ ))}
96+ </div>
97+ <div className="flex" style={{ gap: GAP }}>
98+ {calendar.weeks.map((week, column) => (
99+ <div key={column} className="flex flex-col" style={{ gap: GAP }}>
100+ {week.map((day, row) =>
101+ day ? (
102+ <span
103+ key={row}
104+ data-date={day.date}
105+ data-count={day.count}
106+ data-commits={day.commits}
107+ className={`block rounded-[2px] ${SHADES[day.level]} ${hovered?.date === day.date ? "ring-1 ring-fg/60" : ""}`}
108+ style={{ width: CELL, height: CELL }}
109+ />
110+ ) : (
111+ <span key={row} className="block" style={{ width: CELL, height: CELL }} />
112+ ),
113+ )}
114+ </div>
115+ ))}
116+ </div>
117+ {/* The one hint, over whichever square the pointer is on. */}
118+ <Tooltip open={hovered != null} onOpenChange={(open) => !open && setHovered(null)}>
119+ <TooltipTrigger asChild>
120+ <span
121+ aria-hidden="true"
122+ className="pointer-events-none absolute"
123+ style={{ left: hovered?.left ?? 0, top: hovered?.top ?? 0, width: CELL, height: CELL }}
124+ />
125+ </TooltipTrigger>
126+ <TooltipContent side="top">{hovered ? dayLabel(hovered.count, hovered.date, hovered.commits) : null}</TooltipContent>
127+ </Tooltip>
114128 </div>
115− {/* The one hint, over whichever square the pointer is on. */}
116− <Tooltip open={hovered != null} onOpenChange={(open) => !open && setHovered(null)}>
117− <TooltipTrigger asChild>
118− <span
119− aria-hidden="true"
120− className="pointer-events-none absolute"
121− style={{ left: hovered?.left ?? 0, top: hovered?.top ?? 0, width: CELL, height: CELL }}
122− />
123− </TooltipTrigger>
124− <TooltipContent side="top">{hovered ? dayLabel(hovered.count, hovered.date, hovered.commits) : null}</TooltipContent>
125− </Tooltip>
126129 </div>
127130 </div>
128− </div>
129− <div aria-hidden="true" className="mt-2 flex items-center justify-end gap-1 text-[0.6875rem] text-faint">
130− <span className="mr-1">Less</span>
131− {([0, 1, 2, 3, 4] as Level[]).map((level) => (
132− <span key={level} className={`block rounded-[2px] ${SHADES[level]}`} style={{ width: CELL, height: CELL }} />
133− ))}
134− <span className="ml-1">More</span>
135− </div>
136− </section>
131+ <div aria-hidden="true" className="mt-2 flex items-center justify-end gap-1 text-[0.6875rem] text-faint">
132+ <span className="mr-1">Less</span>
133+ {([0, 1, 2, 3, 4] as Level[]).map((level) => (
134+ <span key={level} className={`block rounded-[2px] ${SHADES[level]}`} style={{ width: CELL, height: CELL }} />
135+ ))}
136+ <span className="ml-1">More</span>
137+ </div>
138+ </section>
139+ </Card>
137140 );
138141 }
+23−19
44 import { ACCOUNT_RESTORE_DAYS, type AccountDeletion } from "@g1t/contracts";
55
66 import { DangerAction } from "./danger-zone";
7−import { Button, ErrorText } from "./ui";
7+import { ErrorText } from "./ui";
88 import {
99 AlertDialog,
1010 AlertDialogCancel,
1414 AlertDialogHeader,
1515 AlertDialogTitle,
1616 } from "./ui/alert-dialog";
17+import { Button } from "./ui/button";
18+import { Card } from "./ui/card";
1719 import { FieldDescription, FieldLabel, Field as FormField } from "./ui/field";
1820 import { Input as TextInput } from "./ui/input";
1921 import { accountDeletionRefusal, confirmsUsername, whatAccountDeletionTakes } from "../lib/account-deletion";
5658 <DangerAction
5759 title="Delete your account"
5860 action={
59− <Button type="button" variant="danger" disabled={Boolean(refusal)} onClick={() => setOpen(true)}>
61+ <Button type="button" variant="destructive" disabled={Boolean(refusal)} onClick={() => setOpen(true)}>
6062 Delete account
6163 </Button>
6264 }
6668 <span role="status">{refusal}</span>
6769 <ul className="mt-3 space-y-2">
6870 {deletion?.sole_owner_of.map((workspace) => (
69− <li key={workspace.slug} className="rounded-lg border border-line bg-surface px-3 py-2">
70− <p className="text-fg">
71− <span className="font-medium">{workspace.name}</span>{" "}
72− <span className="font-mono text-xs text-faint">{workspace.slug}</span>
73− </p>
74− <p className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs">
75− {workspace.members > 1 ? (
76− <Link to={`/${workspace.slug}/-/members`} className="text-accent underline-offset-4 hover:underline">
77− Make someone else an owner
71+ <Card asChild key={workspace.slug} radius="lg" className="px-3 py-2">
72+ <li>
73+ <p className="text-fg">
74+ <span className="font-medium">{workspace.name}</span>{" "}
75+ <span className="font-mono text-xs text-faint">{workspace.slug}</span>
76+ </p>
77+ <p className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs">
78+ {workspace.members > 1 ? (
79+ <Link to={`/${workspace.slug}/-/members`} className="text-accent underline-offset-4 hover:underline">
80+ Make someone else an owner
81+ </Link>
82+ ) : null}
83+ <Link to={`/${workspace.slug}/-/settings`} className="text-accent underline-offset-4 hover:underline">
84+ Delete the workspace
7885 </Link>
79− ) : null}
80− <Link to={`/${workspace.slug}/-/settings`} className="text-accent underline-offset-4 hover:underline">
81− Delete the workspace
82− </Link>
83− </p>
84− {workspace.billing ? <p className="mt-1 text-xs text-warn">{workspace.billing}</p> : null}
85− </li>
86+ </p>
87+ {workspace.billing ? <p className="mt-1 text-xs text-warn">{workspace.billing}</p> : null}
88+ </li>
89+ </Card>
8690 ))}
8791 </ul>
8892 </>
165169 <ErrorText>{error}</ErrorText>
166170 <AlertDialogFooter>
167171 <AlertDialogCancel type="button">Cancel</AlertDialogCancel>
168− <Button type="submit" variant="danger" disabled={!confirmsUsername(username, confirm) || deleting}>
172+ <Button type="submit" variant="destructive" disabled={!confirmsUsername(username, confirm) || deleting}>
169173 {deleting ? "Deleting…" : "Delete account"}
170174 </Button>
171175 </AlertDialogFooter>
+91−85
3939 } from "../lib/dependency-updates";
4040 import { TimeAgo } from "./ui";
4141 import { Badge, type BadgeTone } from "./ui/badge";
42+import { Button } from "./ui/button";
43+import { Card } from "./ui/card";
4244 import { Hint } from "./ui/hint";
4345
4446 type Done = { ok: boolean; error?: string } | undefined;
9496 { label: "Reviewers", values: entry.reviewers },
9597 ].filter((rule) => rule.values.length > 0);
9698 return (
97− <li className="rounded-lg border border-line p-3">
98− <div className="flex flex-wrap items-start justify-between gap-3">
99− <div className="min-w-0">
100− <p className="flex flex-wrap items-center gap-2 text-sm">
101− <span className="font-mono font-medium text-fg">{entry.ecosystem}</span>
102− <span className="font-mono text-muted wrap-anywhere">{entry.directories.join(", ")}</span>
103− {entry.targetBranch && <span className="font-mono text-xs text-faint">→ {entry.targetBranch}</span>}
104− <Badge tone={status.tone}>{status.label}</Badge>
105− </p>
106− <p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
107− {entry.schedule && (
108− <span className="inline-flex items-center gap-1">
109− <CalendarClock size={12} className="shrink-0" />
110− {entry.schedule}
99+ <Card asChild tone="plain" radius="lg" className="p-3">
100+ <li>
101+ <div className="flex flex-wrap items-start justify-between gap-3">
102+ <div className="min-w-0">
103+ <p className="flex flex-wrap items-center gap-2 text-sm">
104+ <span className="font-mono font-medium text-fg">{entry.ecosystem}</span>
105+ <span className="font-mono text-muted wrap-anywhere">{entry.directories.join(", ")}</span>
106+ {entry.targetBranch && <span className="font-mono text-xs text-faint">→ {entry.targetBranch}</span>}
107+ <Badge tone={status.tone}>{status.label}</Badge>
108+ </p>
109+ <p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
110+ {entry.schedule && (
111+ <span className="inline-flex items-center gap-1">
112+ <CalendarClock size={12} className="shrink-0" />
113+ {entry.schedule}
114+ </span>
115+ )}
116+ {entry.nextRunAt && (
117+ <Hint label={utc(entry.nextRunAt)}>
118+ <span>Next check {timeUntil(entry.nextRunAt)}</span>
119+ </Hint>
120+ )}
121+ <span>
122+ {entry.lastCheckedAt ? (
123+ <>
124+ Last checked <TimeAgo at={entry.lastCheckedAt} />
125+ </>
126+ ) : (
127+ "Not checked yet"
128+ )}
111129 </span>
112− )}
113− {entry.nextRunAt && (
114− <Hint label={utc(entry.nextRunAt)}>
115− <span>Next check {timeUntil(entry.nextRunAt)}</span>
116− </Hint>
117− )}
118− <span>
119− {entry.lastCheckedAt ? (
120− <>
121− Last checked <TimeAgo at={entry.lastCheckedAt} />
122− </>
123− ) : (
124− "Not checked yet"
125− )}
126− </span>
127− {entry.supported && <span>Up to {entry.openPullRequestsLimit} open</span>}
128− </p>
130+ {entry.supported && <span>Up to {entry.openPullRequestsLimit} open</span>}
131+ </p>
132+ </div>
133+ {entry.supported && canCheck && (
134+ <check.Form method="post" action={action} className="shrink-0">
135+ <input type="hidden" name="intent" value="check_updates" />
136+ <input type="hidden" name="entry" value={entry.id} />
137+ <Button
138+ type="submit"
139+ disabled={checking || entry.openPullRequestsLimit === 0}
140+ variant="outline"
141+ size="inline"
142+ className="px-2.5 py-1 text-xs hover:bg-raised font-normal"
143+ >
144+ <RefreshCw size={12} className={checking ? "animate-spin" : ""} />
145+ {checking ? "Checking…" : "Check for updates"}
146+ </Button>
147+ </check.Form>
148+ )}
129149 </div>
130− {entry.supported && canCheck && (
131− <check.Form method="post" action={action} className="shrink-0">
132− <input type="hidden" name="intent" value="check_updates" />
133− <input type="hidden" name="entry" value={entry.id} />
134− <button
135− type="submit"
136− disabled={checking || entry.openPullRequestsLimit === 0}
137− className="inline-flex items-center gap-1.5 rounded-md border border-line px-2.5 py-1 text-xs text-fg/80 transition-colors hover:border-line-strong hover:bg-raised hover:text-fg disabled:opacity-50"
138− >
139− <RefreshCw size={12} className={checking ? "animate-spin" : ""} />
140− {checking ? "Checking…" : "Check for updates"}
141− </button>
142− </check.Form>
143− )}
144− </div>
145− {check.data?.error && <p className="mt-2 text-xs text-danger">{check.data.error}</p>}
146− {entry.lastError ? (
147− <p className="mt-2 flex items-start gap-1.5 text-xs text-danger">
148− <FileWarning size={12} className="mt-0.5 shrink-0" />
149− <span className="wrap-anywhere">
150− <Said text={entry.lastError} />
151− </span>
152− </p>
153− ) : (
154− entry.lastResult && (
155− <p className="mt-2 flex items-start gap-1.5 text-xs text-muted">
156− <CircleCheck size={12} className="mt-0.5 shrink-0 text-success" />
150+ {check.data?.error && <p className="mt-2 text-xs text-danger">{check.data.error}</p>}
151+ {entry.lastError ? (
152+ <p className="mt-2 flex items-start gap-1.5 text-xs text-danger">
153+ <FileWarning size={12} className="mt-0.5 shrink-0" />
157154 <span className="wrap-anywhere">
158− <Said text={entry.lastResult} />
155+ <Said text={entry.lastError} />
159156 </span>
160157 </p>
161− )
162− )}
163− {rules.length > 0 && (
164− <dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs">
165− {rules.map((rule) => (
166− <div key={rule.label} className="contents">
167− <dt className="text-faint">{rule.label}</dt>
168− <dd className="font-mono text-muted wrap-anywhere">{rule.values.join("; ")}</dd>
169− </div>
170− ))}
171− </dl>
172− )}
173− {entry.notes.length > 0 && (
174− <ul className="mt-2 space-y-1">
175− {entry.notes.map((note) => (
176− <li key={note} className="flex items-start gap-1.5 text-xs text-faint">
177− <Info size={12} className="mt-0.5 shrink-0" />
178− <span>
179− <Said text={note} />
158+ ) : (
159+ entry.lastResult && (
160+ <p className="mt-2 flex items-start gap-1.5 text-xs text-muted">
161+ <CircleCheck size={12} className="mt-0.5 shrink-0 text-success" />
162+ <span className="wrap-anywhere">
163+ <Said text={entry.lastResult} />
180164 </span>
181− </li>
182− ))}
183− </ul>
184− )}
185− </li>
165+ </p>
166+ )
167+ )}
168+ {rules.length > 0 && (
169+ <dl className="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-xs">
170+ {rules.map((rule) => (
171+ <div key={rule.label} className="contents">
172+ <dt className="text-faint">{rule.label}</dt>
173+ <dd className="font-mono text-muted wrap-anywhere">{rule.values.join("; ")}</dd>
174+ </div>
175+ ))}
176+ </dl>
177+ )}
178+ {entry.notes.length > 0 && (
179+ <ul className="mt-2 space-y-1">
180+ {entry.notes.map((note) => (
181+ <li key={note} className="flex items-start gap-1.5 text-xs text-faint">
182+ <Info size={12} className="mt-0.5 shrink-0" />
183+ <span>
184+ <Said text={note} />
185+ </span>
186+ </li>
187+ ))}
188+ </ul>
189+ )}
190+ </li>
191+ </Card>
186192 );
187193 }
188194
246252 const pulls = livePulls(state.pulls);
247253 const fileLink = state.path && state.commit ? `${base}/blob/${state.commit}/${state.path}` : null;
248254 return (
249− <div className="rounded-xl border border-line bg-surface p-4">
255+ <Card className="p-4">
250256 {titled && (
251257 <>
252258 <div className="flex flex-wrap items-center justify-between gap-2">
416422 </p>
417423 </section>
418424 )}
419− </div>
425+ </Card>
420426 );
421427 }
+108−97
2222 HighlightedLine,
2323 } from "../lib/diff";
2424 import { Markdown } from "./markdown";
25−import { Avatar, Button, EmptyState, SubmitButton, Textarea, TimeAgo } from "./ui";
25+import { EmptyState, SubmitButton, Textarea, TimeAgo } from "./ui";
26+import { Avatar } from "./ui/avatar";
27+import { Button } from "./ui/button";
28+import { Card } from "./ui/card";
2629 import { Checkbox } from "./ui/checkbox";
2730 import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./ui/sheet";
2831
113116 /** One comment made on a line, shown under that line. */
114117 function LineComment({ comment }: { comment: Comment }) {
115118 return (
116− <div className="rounded-lg border border-line bg-bg font-sans">
119+ <Card tone="bg" radius="lg" className="font-sans">
117120 <p className="flex items-center gap-2 border-b border-line px-3 py-1.5 text-xs text-muted">
118121 <Avatar name={comment.author.username} size={16} />
119122 <span className="font-medium text-fg">{comment.author.username}</span>
122125 <div className="px-3 py-2 text-sm">
123126 <Markdown source={comment.body} />
124127 </div>
125− </div>
128+ </Card>
126129 );
127130 }
128131
171174 <SubmitButton match={{ action: "comment", path, line: String(line) }} pending="Commenting…">
172175 Comment
173176 </SubmitButton>
174− <Button variant="quiet" type="button" onClick={onClose}>
177+ <Button variant="outline" type="button" onClick={onClose}>
175178 Cancel
176179 </Button>
177180 </div>
183186
184187 function CommentButton({ line, onClick }: { line: number; onClick: (event: React.MouseEvent) => void }) {
185188 return (
186− <button
189+ <Button
187190 type="button"
188191 aria-label={`Comment on line ${line}`}
189192 onClick={onClick}
190− className="absolute top-0 -right-2.5 z-10 hidden size-5 items-center justify-center rounded bg-accent text-bg group-hover:flex focus-visible:flex pointer-coarse:-right-1 pointer-coarse:flex pointer-coarse:size-7"
193+ variant="accent" size="icon"
194+ className="absolute top-0 -right-2.5 z-10 hidden size-5 rounded group-hover:flex focus-visible:flex pointer-coarse:-right-1 pointer-coarse:flex pointer-coarse:size-7"
191195 >
192196 <MessageSquarePlus size={12} />
193− </button>
197+ </Button>
194198 );
195199 }
196200
424428 }, [file, collapsed]);
425429 const props = { file: highlighted ?? file, comments, canComment: review?.canComment ?? false };
426430 return (
427− <section
428− ref={section}
429− id={`file-${file.path}`}
430− data-diff-file={file.path}
431− // Lands below the site bar and the change's toolbar, whatever their height.
432− className="scroll-mt-[calc(4rem+var(--diff-toolbar,3rem))] rounded-xl border border-line"
433− >
434− <header
435− // Sticks under the change's toolbar (its height is --diff-toolbar), not behind it.
436− className={`sticky top-[calc(var(--topbar-h)+var(--diff-toolbar,0px))] z-20 flex items-center gap-2 border-line bg-surface/95 px-2 py-2 backdrop-blur sm:gap-2.5 sm:px-3 ${
437− collapsed ? "rounded-xl" : "rounded-t-xl border-b"
438− }`}
431+ <Card asChild tone="plain" className="scroll-mt-[calc(4rem+var(--diff-toolbar,3rem))]">
432+ <section
433+ ref={section}
434+ id={`file-${file.path}`}
435+ data-diff-file={file.path}
436+ // Lands below the site bar and the change's toolbar, whatever their height.
437+
439438 >
440− <button
441− type="button"
442− onClick={onToggle}
443− aria-label={collapsed ? `Show ${file.path}` : `Hide ${file.path}`}
444− aria-expanded={!collapsed}
445− className="-m-1.5 shrink-0 rounded p-2 text-faint hover:bg-raised hover:text-fg sm:m-0 sm:p-0.5"
446− >
447− {collapsed ? <ChevronRight size={15} /> : <ChevronDown size={15} />}
448− </button>
449− <Icon size={15} className={`hidden shrink-0 sm:block ${fileTone(file.status)}`} />
450− {/* A long path is cut at its start, so the file's own name shows. */}
451− <button
452− type="button"
453− onClick={onToggle}
454− dir="rtl"
455− className={`min-w-0 grow truncate text-left font-mono text-[0.8125rem] ${viewed ? "text-muted" : ""}`}
456− >
457− <bdi>{file.path}</bdi>
458− </button>
459− {comments.length > 0 && (
460− <span className="flex items-center gap-1 text-xs text-muted">
461− <MessageSquarePlus size={12} />
462− {comments.length}
463− </span>
464− )}
465− <Stat additions={file.additions} deletions={file.deletions} />
466− {fileBase && file.status !== "deleted" && (
467− <span className="hidden items-center gap-0.5 text-xs sm:flex">
468− <Link to={`${fileBase}/${file.path}`} className="rounded px-1.5 py-0.5 text-muted hover:bg-raised hover:text-fg">
469− View
470− </Link>
471− <Link
472− to={`${fileBase}/${file.path}?blame=1`}
473− className="rounded px-1.5 py-0.5 text-muted hover:bg-raised hover:text-fg"
474− >
475− Blame
476− </Link>
477− </span>
478− )}
479− <label
480− className={`ml-1 flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1.5 text-xs transition-colors select-none sm:py-0.5 ${
481− viewed ? "border-accent/40 bg-accent/10 text-accent" : "border-line text-muted hover:text-fg"
482− }`}
483− >
484− <Checkbox
485− checked={viewed}
486− onCheckedChange={(checked) => onViewed(checked === true)}
487− className="size-3.5 rounded-[4px] [&_svg]:size-2.5"
488− />
489− <span className="sr-only sm:not-sr-only">Viewed</span>
490− </label>
491− </header>
492− {!collapsed && (
493− <div className="overflow-hidden rounded-b-xl">
494− {outdated.length > 0 && (
495− <div className="space-y-2 border-b border-line bg-surface/50 p-3">
496− <p className="text-xs text-faint">On lines that have since changed</p>
497− {outdated.map((comment) => (
498− <LineComment key={comment.id} comment={comment} />
499− ))}
500− </div>
439+ <header
440+ // Sticks under the change's toolbar (its height is --diff-toolbar), not behind it.
441+ className={`sticky top-[calc(var(--topbar-h)+var(--diff-toolbar,0px))] z-20 flex items-center gap-2 border-line bg-surface/95 px-2 py-2 backdrop-blur sm:gap-2.5 sm:px-3 ${
442+ collapsed ? "rounded-xl" : "rounded-t-xl border-b"
443+ }`}>
444+ <Button
445+ type="button"
446+ onClick={onToggle}
447+ aria-label={collapsed ? `Show ${file.path}` : `Hide ${file.path}`}
448+ aria-expanded={!collapsed}
449+ variant="ghost"
450+ size="inline"
451+ className="-m-1.5 rounded p-2 text-faint sm:m-0 sm:p-0.5"
452+ >
453+ {collapsed ? <ChevronRight size={15} /> : <ChevronDown size={15} />}
454+ </Button>
455+ <Icon size={15} className={`hidden shrink-0 sm:block ${fileTone(file.status)}`} />
456+ {/* A long path is cut at its start, so the file's own name shows. */}
457+ <button
458+ type="button"
459+ onClick={onToggle}
460+ dir="rtl"
461+ className={`min-w-0 grow truncate text-left font-mono text-[0.8125rem] ${viewed ? "text-muted" : ""}`}
462+ >
463+ <bdi>{file.path}</bdi>
464+ </button>
465+ {comments.length > 0 && (
466+ <span className="flex items-center gap-1 text-xs text-muted">
467+ <MessageSquarePlus size={12} />
468+ {comments.length}
469+ </span>
501470 )}
502− {file.binary ? (
503− <p className="px-4 py-6 text-sm text-muted">Binary or large file; its contents are not shown.</p>
504− ) : file.hunks.length === 0 ? (
505− <p className="px-4 py-6 text-sm text-muted">No line changes{file.status === "added" ? ": an empty file" : ""}.</p>
506− ) : (
507− <div className="overflow-x-auto">
508− {layout === "split" ? <SplitRows {...props} /> : <UnifiedRows {...props} />}
509− </div>
471+ <Stat additions={file.additions} deletions={file.deletions} />
472+ {fileBase && file.status !== "deleted" && (
473+ <span className="hidden items-center gap-0.5 text-xs sm:flex">
474+ <Link to={`${fileBase}/${file.path}`} className="rounded px-1.5 py-0.5 text-muted hover:bg-raised hover:text-fg">
475+ View
476+ </Link>
477+ <Link
478+ to={`${fileBase}/${file.path}?blame=1`}
479+ className="rounded px-1.5 py-0.5 text-muted hover:bg-raised hover:text-fg"
480+ >
481+ Blame
482+ </Link>
483+ </span>
510484 )}
511− </div>
512− )}
513− </section>
485+ <label
486+ className={`ml-1 flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md border px-2 py-1.5 text-xs transition-colors select-none sm:py-0.5 ${
487+ viewed ? "border-accent/40 bg-accent/10 text-accent" : "border-line text-muted hover:text-fg"
488+ }`}
489+ >
490+ <Checkbox
491+ checked={viewed}
492+ onCheckedChange={(checked) => onViewed(checked === true)}
493+ className="size-3.5 rounded-[4px] [&_svg]:size-2.5"
494+ />
495+ <span className="sr-only sm:not-sr-only">Viewed</span>
496+ </label>
497+ </header>
498+ {!collapsed && (
499+ <div className="overflow-hidden rounded-b-xl">
500+ {outdated.length > 0 && (
501+ <div className="space-y-2 border-b border-line bg-surface/50 p-3">
502+ <p className="text-xs text-faint">On lines that have since changed</p>
503+ {outdated.map((comment) => (
504+ <LineComment key={comment.id} comment={comment} />
505+ ))}
506+ </div>
507+ )}
508+ {file.binary ? (
509+ <p className="px-4 py-6 text-sm text-muted">Binary or large file; its contents are not shown.</p>
510+ ) : file.hunks.length === 0 ? (
511+ <p className="px-4 py-6 text-sm text-muted">No line changes{file.status === "added" ? ": an empty file" : ""}.</p>
512+ ) : (
513+ <div className="overflow-x-auto">
514+ {layout === "split" ? <SplitRows {...props} /> : <UnifiedRows {...props} />}
515+ </div>
516+ )}
517+ </div>
518+ )}
519+ </section>
520+ </Card>
514521 );
515522 }
516523
784791 <div className="ml-auto flex items-center gap-2">
785792 {files.length > 1 && (
786793 // Below xl the tree is not beside the diffs: it opens as a sheet.
787− <button
794+ <Button
788795 type="button"
789796 onClick={() => setPicking(true)}
790− className="flex min-h-9 items-center gap-1.5 rounded-md border border-line px-2.5 text-xs text-muted transition-colors hover:border-line-strong hover:text-fg xl:hidden"
797+ variant="outline"
798+ size="inline"
799+ className="flex min-h-9 px-2.5 text-xs text-muted xl:hidden font-normal"
791800 >
792801 <Files size={13} />
793802 Files
794− </button>
803+ </Button>
795804 )}
796− <button
805+ <Button
797806 type="button"
798807 onClick={() => setCollapsed(allCollapsed ? new Set() : new Set(files.map((file) => file.path)))}
799− className="rounded-md px-2 py-1 text-xs text-muted hover:bg-raised hover:text-fg"
808+ variant="ghost"
809+ size="inline"
810+ className="px-2 py-1 text-xs font-normal"
800811 >
801812 {allCollapsed ? "Expand all" : "Collapse all"}
802− </button>
813+ </Button>
803814 <div className="hidden rounded-md border border-line p-0.5 sm:flex" role="radiogroup" aria-label="Layout">
804815 {(
805816 [
+15−11
66
77 import { AppsLauncher, appIcon, pinnedApp, useAppPins } from "./apps";
88 import { Mark } from "./logo";
9−import { Avatar } from "./ui";
9+
10+import { Avatar } from "./ui/avatar";
11+import { Card } from "./ui/card";
1012 import { Hint } from "./ui/hint";
1113 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
1214 import {
208210 <DialogTitle>Keyboard shortcuts</DialogTitle>
209211 <DialogDescription>Everywhere in g1t, and in Chat.</DialogDescription>
210212 </DialogHeader>
211− <dl className="divide-y divide-line rounded-lg border border-line">
212− {shortcuts(paletteKeyLabel(platform), sidebarKeyLabel(platform)).map(([key, what]) => (
213− <div key={key} className="flex items-center justify-between gap-4 px-3.5 py-2.5 text-sm">
214− <dt className="text-fg-soft">{what}</dt>
215− <dd>
216− <kbd className="rounded-md border border-line-strong bg-raised px-1.5 py-0.5 font-sans text-xs whitespace-nowrap text-fg">{key}</kbd>
217− </dd>
218− </div>
219− ))}
220− </dl>
213+ <Card asChild tone="plain" radius="lg" divided>
214+ <dl>
215+ {shortcuts(paletteKeyLabel(platform), sidebarKeyLabel(platform)).map(([key, what]) => (
216+ <div key={key} className="flex items-center justify-between gap-4 px-3.5 py-2.5 text-sm">
217+ <dt className="text-fg-soft">{what}</dt>
218+ <dd>
219+ <kbd className="rounded-md border border-line-strong bg-raised px-1.5 py-0.5 font-sans text-xs whitespace-nowrap text-fg">{key}</kbd>
220+ </dd>
221+ </div>
222+ ))}
223+ </dl>
224+ </Card>
221225 </DialogContent>
222226 </Dialog>
223227 );
+10−9
1111 import type { AgentEffort, AgentEffortCosts, AgentRecommendation, AgentRecommendations } from "@g1t/contracts";
1212
1313 import { AgentAvatar } from "./agent-avatar";
14−import { BUTTONS } from "./agents/dialogs";
1514 import { Hint } from "./ui/hint";
1615 import { TimeAgo } from "./ui";
16+import { Button } from "./ui/button";
17+import { Card } from "./ui/card";
1718 import { cn } from "../lib/cn";
1819 import { EFFORT_OPTIONS, acceptedLabel, costAt, costLine, effortLabel, savingLabel, totalSaving } from "../lib/effort";
1920 import { money } from "../lib/usage";
3940 const [chosen, setChosen] = useState<AgentEffort>(value);
4041 return (
4142 <div className="min-w-0">
42− <div role="radiogroup" aria-label="Effort" className="grid grid-cols-5 gap-0.5 rounded-lg border border-line bg-bg p-0.5 sm:inline-grid sm:w-auto sm:min-w-[22rem]">
43+ <Card role="radiogroup" aria-label="Effort" tone="bg" radius="lg" className="grid grid-cols-5 gap-0.5 p-0.5 sm:inline-grid sm:w-auto sm:min-w-[22rem]">
4344 {EFFORT_OPTIONS.map((option) => {
4445 const on = chosen === option.key;
4546 return (
6869 </Hint>
6970 );
7071 })}
71− </div>
72+ </Card>
7273 <p className="mt-2 text-xs text-muted">{EFFORT_OPTIONS.find((o) => o.key === chosen)?.about}</p>
7374 {costs !== undefined && <p className="mt-0.5 text-xs text-faint tabular-nums">{costLine(costs, chosen)}</p>}
7475 </div>
143144 <EffortPicker value={value} costs={costs} disabled={!owner} onChange={setPicked} />
144145 {owner && (
145146 <div className="flex flex-wrap items-center gap-3">
146− <button type="submit" disabled={busy || picked === value} className={BUTTONS.PRIMARY}>
147+ <Button type="submit" disabled={busy || picked === value} variant="accent">
147148 {busy ? "Saving…" : "Save effort"}
148− </button>
149+ </Button>
149150 {result?.ok && picked === value && (
150151 <span role="status" className="inline-flex items-center gap-1 text-xs text-success">
151152 <Check size={13} aria-hidden="true" /> Saved as a new version
266267 <fetcher.Form method="post" action={action} className="col-span-2 flex items-center gap-2 sm:col-span-1 sm:self-center">
267268 <input type="hidden" name="intent" value="recommendation" />
268269 <input type="hidden" name="id" value={rec.id} />
269− <button type="submit" name="do" value="apply" disabled={fetcher.state !== "idle"} className={cn(BUTTONS.PRIMARY, "h-8 px-3 py-0 text-xs")}>
270+ <Button type="submit" name="do" value="apply" disabled={fetcher.state !== "idle"} variant="accent" size="sm">
270271 {doing === "apply" ? "Applying…" : "Apply"}
271− </button>
272− <button type="submit" name="do" value="dismiss" disabled={fetcher.state !== "idle"} className={cn(BUTTONS.QUIET, "h-8 px-3 py-0 text-xs")}>
272+ </Button>
273+ <Button type="submit" name="do" value="dismiss" disabled={fetcher.state !== "idle"} variant="outline" size="sm">
273274 {doing === "dismiss" ? "Dismissing…" : "Dismiss"}
274− </button>
275+ </Button>
275276 </fetcher.Form>
276277 ) : (
277278 <p className="col-span-2 text-xs text-faint sm:col-span-1 sm:self-center">An owner can apply it</p>
+7−6
55
66 import { addressActions, backupChoices } from "../lib/emails";
77 import type { EmailActionData } from "../lib/emails.server";
8−import { ErrorText, Field, Input, Pill, SubmitButton, TimeAgo } from "./ui";
8+import { ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
9+import { Badge } from "./ui/badge";
910 import { SelectField } from "./ui/select";
1011 import { SwitchCard } from "./ui/switch";
1112
1516 <Form method="post">
1617 <input type="hidden" name="intent" value={intent} />
1718 <input type="hidden" name="email" value={email} />
18− <SubmitButton variant="quiet" pending={pending} match={{ intent, email }}>
19+ <SubmitButton variant="outline" pending={pending} match={{ intent, email }}>
1920 {label}
2021 </SubmitButton>
2122 </Form>
134135 <div className="min-w-0">
135136 <p className="truncate text-sm">{email.email}</p>
136137 <p className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-faint">
137− {email.primary && <Pill>Primary</Pill>}
138− {email.backup && <Pill>Backup</Pill>}
138+ {email.primary && <Badge size="md">Primary</Badge>}
139+ {email.backup && <Badge size="md">Backup</Badge>}
139140 {email.verified ? (
140141 <span>
141142 Confirmed {email.verifiedAt ? <TimeAgo at={email.verifiedAt} /> : null}
192193 options={[{ value: "", label: "Primary address only" }, ...backups.map((email) => ({ value: email.email, label: email.email }))]}
193194 />
194195 </div>
195− <SubmitButton variant="quiet" pending="Saving…" match={{ intent: "backup-email" }} disabled={backups.length === 0}>
196+ <SubmitButton variant="outline" pending="Saving…" match={{ intent: "backup-email" }} disabled={backups.length === 0}>
196197 Save
197198 </SubmitButton>
198199 </Form>
218219 <p className="text-xs text-faint">
219220 Commits g1t makes for you now carry <span className="font-mono [overflow-wrap:anywhere]">{data.commitEmail}</span>.
220221 </p>
221− <SubmitButton variant="quiet" pending="Saving…" match={{ intent: "email-privacy" }}>
222+ <SubmitButton variant="outline" pending="Saving…" match={{ intent: "email-privacy" }}>
222223 Save
223224 </SubmitButton>
224225 </div>
+11−6
55 import { useEmojiContext } from "./context";
66 import { readRecent, readTone, rememberRecent, rememberTone, useEmojiData } from "./data";
77 import { EmojiGlyph } from "./render";
8+import { Button } from "../ui/button";
89 import { Hint } from "../ui/hint";
910 import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
1011 import { type EmojiPick, SKIN_TONES, customPick, searchEmoji, skinned, unicodePick } from "../../lib/emoji";
121122 </label>
122123 <div className="relative">
123124 <Hint label="Skin tone">
124− <button
125+ <Button
125126 type="button"
126127 aria-label="Skin tone"
127128 aria-expanded={toning}
128129 onClick={() => setToning((open) => !open)}
129− className="flex size-8 items-center justify-center rounded-md text-lg hover:bg-raised"
130+ variant="ghost"
131+ size="icon-sm"
132+ className="text-lg"
130133 >
131134 {SKIN_TONES[tone]}
132− </button>
135+ </Button>
133136 </Hint>
134137 {toning && (
135138 <div role="radiogroup" aria-label="Skin tone" className="absolute top-full right-0 z-10 mt-1 flex gap-0.5 rounded-lg border border-line-strong bg-raised p-1 shadow-xl shadow-black/40">
158161 <nav aria-label="Emoji groups" className="flex shrink-0 items-center gap-0.5 overflow-x-auto border-b border-line px-1.5 py-1 [scrollbar-width:none]">
159162 {sections.map((section) => (
160163 <Hint key={section.key} label={section.title}>
161− <button
164+ <Button
162165 type="button"
163166 aria-label={section.title}
164167 onClick={() => jump(section.key)}
165− className="flex size-7 shrink-0 items-center justify-center rounded-md text-[0.95rem] text-faint grayscale-[0.6] transition hover:bg-raised hover:text-fg hover:grayscale-0"
168+ variant="ghost"
169+ size="icon-xs"
170+ className="text-[0.95rem] text-faint grayscale-[0.6] transition hover:grayscale-0"
166171 >
167172 {section.icon}
168− </button>
173+ </Button>
169174 </Hint>
170175 ))}
171176 </nav>
+16−11
55 import { useEmojiContext } from "./context";
66 import { EmojiPickerPopover } from "./picker";
77 import { EmojiGlyph } from "./render";
8+import { Button } from "../ui/button";
89 import { Hint } from "../ui/hint";
910 import { reactorsLine } from "../../lib/emoji";
1011
6667 if (!react) return null;
6768 const button =
6869 variant === "pill" ? (
69− <button
70+ <Button
7071 type="button"
7172 aria-label="Add a reaction"
72− className="flex h-6 items-center rounded-full border border-line bg-surface px-1.5 text-faint transition-colors hover:border-line-strong hover:bg-raised hover:text-fg"
73+ variant="outline"
74+ className="h-6 rounded-full bg-surface px-1.5 text-faint hover:bg-raised"
7375 >
7476 <SmilePlus size={14} />
75− </button>
77+ </Button>
7678 ) : (
77− <button
79+ <Button
7880 type="button"
7981 aria-label="Add a reaction"
80− className="flex size-7 items-center justify-center rounded-md text-muted hover:bg-raised hover:text-fg"
82+ variant="ghost"
83+ size="icon-xs"
8184 >
8285 <SmilePlus size={15} />
83− </button>
86+ </Button>
8487 );
8588 return (
8689 <EmojiPickerPopover onPick={(emoji) => react(messageId, emoji, true)} side={variant === "pill" ? "top" : "bottom"} align={variant === "pill" ? "start" : "end"}>
106109 return (
107110 <div className="flex items-center justify-between gap-1 px-2 pb-2" aria-label="React">
108111 {QUICK.map((emoji) => (
109− <button
112+ <Button
110113 key={emoji}
111114 type="button"
112115 aria-label={`React with ${emoji}`}
113116 onClick={() => pick(emoji)}
114− className="flex size-11 items-center justify-center rounded-full bg-surface text-[1.375rem] active:bg-raised"
117+ variant="ghost"
118+ size="icon-lg"
119+ className="size-11 rounded-full bg-surface text-[1.375rem] active:bg-raised"
115120 >
116121 {emoji}
117− </button>
122+ </Button>
118123 ))}
119124 <EmojiPickerPopover onPick={pick} side="top" align="end">
120− <button type="button" aria-label="More reactions" className="flex size-11 items-center justify-center rounded-full bg-surface text-muted active:bg-raised">
125+ <Button type="button" aria-label="More reactions" variant="ghost" size="icon" className="size-11 rounded-full bg-surface active:bg-raised">
121126 <SmilePlus size={20} />
122− </button>
127+ </Button>
123128 </EmojiPickerPopover>
124129 </div>
125130 );
+81−70
88 import { loadEmojiData } from "./data";
99 import { CustomEmojiImage } from "./render";
1010 import { MemberAvatar } from "../chat/marks";
11−import { Button, EmptyState, ErrorText, TimeAgo, notACredential } from "../ui";
11+import { EmptyState, ErrorText, TimeAgo, notACredential } from "../ui";
12+import { Button } from "../ui/button";
13+import { Card } from "../ui/card";
1214 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
1315 import { Hint } from "../ui/hint";
1416 import { SelectField } from "../ui/select";
6163 {list.can_upload ? (
6264 <div className="flex gap-2">
6365 {list.emoji.length > 0 && (
64− <Button variant="quiet" onClick={() => setAliasing(true)}>
66+ <Button variant="outline" onClick={() => setAliasing(true)}>
6567 Add an alias
6668 </Button>
6769 )}
8991 {query ? "Matching" : "All emoji"} <span className="text-faint">{shown.length}</span>
9092 </h2>
9193 {shown.length === 0 ? (
92− <p className="rounded-xl border border-line px-4 py-8 text-center text-sm text-muted">No emoji match “{query}”.</p>
94+ <Card asChild tone="plain" className="px-4 py-8 text-center text-sm text-muted">
95+ <p>No emoji match “{query}”.</p>
96+ </Card>
9397 ) : (
9498 <ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
9599 {shown.map((emoji) => (
116120 if (fetcher.data?.ok) forgetCustomEmoji(slug);
117121 }, [fetcher.data, slug]);
118122 return (
119− <li className="group flex items-center gap-3 rounded-xl border border-line bg-surface px-3 py-2.5">
120− <span className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-bg">
121− <CustomEmojiImage name={emoji.name} file={emoji.file} usercontent={usercontent} size={32} />
122− </span>
123− <div className="min-w-0 grow">
124− <p className="truncate font-mono text-sm text-fg">:{emoji.name}:</p>
125− <p className="flex min-w-0 items-center gap-1.5 truncate text-xs text-faint">
126− {emoji.alias_of ? (
127− <span className="truncate">
128− Alias of <span className="font-mono text-muted">:{emoji.alias_of}:</span>
129− </span>
123+ <Card asChild className="group flex items-center gap-3 px-3 py-2.5">
124+ <li>
125+ <span className="flex size-11 shrink-0 items-center justify-center rounded-lg bg-bg">
126+ <CustomEmojiImage name={emoji.name} file={emoji.file} usercontent={usercontent} size={32} />
127+ </span>
128+ <div className="min-w-0 grow">
129+ <p className="truncate font-mono text-sm text-fg">:{emoji.name}:</p>
130+ <p className="flex min-w-0 items-center gap-1.5 truncate text-xs text-faint">
131+ {emoji.alias_of ? (
132+ <span className="truncate">
133+ Alias of <span className="font-mono text-muted">:{emoji.alias_of}:</span>
134+ </span>
135+ ) : (
136+ <>
137+ <MemberAvatar member={emoji.created_by} size={14} />
138+ <span className="truncate">{emoji.created_by.display_name || emoji.created_by.name}</span>
139+ <span aria-hidden>·</span>
140+ <TimeAgo at={emoji.created_at} />
141+ </>
142+ )}
143+ </p>
144+ {fetcher.data && !fetcher.data.ok && <p className="mt-0.5 text-xs text-danger">{fetcher.data.error}</p>}
145+ </div>
146+ {removable &&
147+ (confirm ? (
148+ <div className="flex shrink-0 items-center gap-1">
149+ <Button type="button" onClick={() => setConfirm(false)} variant="ghost" size="inline" className="px-2 py-1 text-xs font-normal">
150+ Keep
151+ </Button>
152+ <Button
153+ type="button"
154+ disabled={busy}
155+ onClick={() => fetcher.submit({ intent: "remove", name: emoji.name }, { method: "post" })}
156+ size="inline"
157+ className="bg-danger/15 px-2 py-1 text-xs text-danger hover:bg-danger/25 disabled:opacity-60"
158+ >
159+ {busy ? "Removing…" : emoji.alias_of ? "Remove" : "Remove with aliases"}
160+ </Button>
161+ </div>
130162 ) : (
131− <>
132− <MemberAvatar member={emoji.created_by} size={14} />
133− <span className="truncate">{emoji.created_by.display_name || emoji.created_by.name}</span>
134− <span aria-hidden>·</span>
135− <TimeAgo at={emoji.created_at} />
136− </>
137− )}
138− </p>
139− {fetcher.data && !fetcher.data.ok && <p className="mt-0.5 text-xs text-danger">{fetcher.data.error}</p>}
140− </div>
141− {removable &&
142− (confirm ? (
143− <div className="flex shrink-0 items-center gap-1">
144− <button type="button" onClick={() => setConfirm(false)} className="rounded-md px-2 py-1 text-xs text-muted hover:bg-raised hover:text-fg">
145− Keep
146− </button>
147− <button
148− type="button"
149− disabled={busy}
150− onClick={() => fetcher.submit({ intent: "remove", name: emoji.name }, { method: "post" })}
151− className="rounded-md bg-danger/15 px-2 py-1 text-xs font-medium text-danger hover:bg-danger/25 disabled:opacity-60"
152− >
153− {busy ? "Removing…" : emoji.alias_of ? "Remove" : "Remove with aliases"}
154− </button>
155− </div>
156− ) : (
157− <Hint label={`Remove :${emoji.name}:`}>
158− <button
159− type="button"
160− aria-label={`Remove :${emoji.name}:`}
161− onClick={() => setConfirm(true)}
162− className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint opacity-100 transition hover:bg-raised hover:text-danger sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100"
163− >
164− <Trash2 size={15} />
165− </button>
166− </Hint>
167− ))}
168− </li>
163+ <Hint label={`Remove :${emoji.name}:`}>
164+ <Button
165+ type="button"
166+ aria-label={`Remove :${emoji.name}:`}
167+ onClick={() => setConfirm(true)}
168+ variant="ghost"
169+ size="icon-sm"
170+ className="text-faint opacity-100 transition hover:text-danger sm:opacity-0 sm:group-hover:opacity-100 sm:focus-visible:opacity-100"
171+ >
172+ <Trash2 size={15} />
173+ </Button>
174+ </Hint>
175+ ))}
176+ </li>
177+ </Card>
169178 );
170179 }
171180
173182 /** Who may add emoji: one of the workspace's chat settings, changed in Settings, Chat. */
174183 function UploadSetting({ slug, value }: { slug: string; value: EmojiUpload }) {
175184 return (
176− <section aria-labelledby="emoji-upload" className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-line p-4">
177− <div>
178− <h2 id="emoji-upload" className="text-sm font-medium text-fg">
179− Who can add emoji: {value === "admins" ? "owners only" : "any member"}
180− </h2>
181− <p className="mt-1 text-xs text-muted">Whoever added an emoji, and owners, can remove it. Who can add them is one of the workspace&apos;s chat settings.</p>
182− </div>
183− <Link
184− to={`/${slug}/-/settings/chat`}
185− className="inline-flex h-8 items-center rounded-md border border-line px-3 text-[0.8125rem] font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-surface"
186− >
187− Chat settings
188− </Link>
189− </section>
185+ <Card asChild tone="plain" className="flex flex-wrap items-center justify-between gap-3 p-4">
186+ <section aria-labelledby="emoji-upload">
187+ <div>
188+ <h2 id="emoji-upload" className="text-sm font-medium text-fg">
189+ Who can add emoji: {value === "admins" ? "owners only" : "any member"}
190+ </h2>
191+ <p className="mt-1 text-xs text-muted">Whoever added an emoji, and owners, can remove it. Who can add them is one of the workspace&apos;s chat settings.</p>
192+ </div>
193+ <Link
194+ to={`/${slug}/-/settings/chat`}
195+ className="inline-flex h-8 items-center rounded-md border border-line px-3 text-[0.8125rem] font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-surface"
196+ >
197+ Chat settings
198+ </Link>
199+ </section>
200+ </Card>
190201 );
191202 }
192203
329340 </label>
330341
331342 {preview && (
332− <div className="rounded-xl border border-line bg-bg p-3" aria-label="Preview">
343+ <Card tone="bg" className="p-3" aria-label="Preview">
333344 <p className="mb-2 text-[0.6875rem] font-semibold tracking-wide text-faint uppercase">Preview</p>
334345 <p className="text-[0.9375rem] text-fg-soft">
335346 Shipping it today <img src={preview} alt={`:${clean || "name"}:`} className="inline-block size-[1.375em] object-contain align-[-0.3em]" />
341352 <img src={preview} alt="" className="size-9 object-contain" />
342353 <span className="ml-auto font-mono text-xs text-faint">:{clean || "name"}:</span>
343354 </div>
344− </div>
355+ </Card>
345356 )}
346357 {fetcher.data && !fetcher.data.ok && fetcher.data.intent === "add" && <ErrorText>{fetcher.data.error}</ErrorText>}
347358
348359 <DialogFooter>
349− <Button variant="quiet" onClick={() => onOpenChange(false)}>
360+ <Button variant="outline" onClick={() => onOpenChange(false)}>
350361 Cancel
351362 </Button>
352363 <Button variant="accent" onClick={submit} disabled={!ready}>
423434 </label>
424435 {fetcher.data && !fetcher.data.ok && fetcher.data.intent === "alias" && <ErrorText>{fetcher.data.error}</ErrorText>}
425436 <DialogFooter>
426− <Button variant="quiet" onClick={() => onOpenChange(false)}>
437+ <Button variant="outline" onClick={() => onOpenChange(false)}>
427438 Cancel
428439 </Button>
429440 <Button
+2−1
77 import { useEffect, useMemo, useState } from "react";
88
99 import { buildTree, canDo, flatten } from "../../lib/folios";
10−import { Button, ErrorText } from "../ui";
10+import { ErrorText } from "../ui";
11+import { Button } from "../ui/button";
1112 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog";
1213 import { SelectField } from "../ui/select";
1314
+8−6
1717
1818 import { citationHref } from "../../../lib/folios";
1919 import { useDrawnTheme } from "../../../lib/theme";
20+import { Button } from "../../ui/button";
21+import { Card } from "../../ui/card";
2022
2123 /** What an embed shows, from the site (`-/artifacts/api?embed=`). */
2224 export type EmbedCard = { kind: "issue" | "pull" | "channel" | "project" | "page" | "link"; title: string; subtitle: string | null; state: string | null; href: string };
118120 render: ({ block, editor }) => {
119121 const [editing, setEditing] = useState(false);
120122 return (
121− <div className="my-1 w-full rounded-lg border border-line bg-surface p-3" contentEditable={false}>
123+ <Card radius="lg" className="my-1 w-full p-3" contentEditable={false}>
122124 <div className="mb-2 flex items-center justify-between text-[0.6875rem] font-medium tracking-wide text-faint uppercase">
123125 <span>Diagram</span>
124126 {editor.isEditable && (
125− <button type="button" onClick={() => setEditing(!editing)} className="rounded px-1.5 py-0.5 normal-case hover:bg-raised hover:text-fg">
127+ <Button type="button" onClick={() => setEditing(!editing)} variant="ghost" size="inline" className="rounded px-1.5 py-0.5 font-normal normal-case text-faint">
126128 {editing ? "Done" : "Edit source"}
127− </button>
129+ </Button>
128130 )}
129131 </div>
130132 {editing && <SourceEditor editable value={block.props.code} placeholder="flowchart LR&#10; A --> B" onChange={(code) => editor.updateBlock(block, { props: { code } })} />}
131133 <div className={editing ? "mt-3" : ""}>
132134 <MermaidView code={block.props.code} />
133135 </div>
134− </div>
136+ </Card>
135137 );
136138 },
137139 },
242244 placeholder="Paste the address of an issue, pull request, channel, project or artifact"
243245 className="min-w-0 grow bg-transparent px-1.5 text-sm text-fg outline-none placeholder:text-faint"
244246 />
245− <button type="submit" className="rounded-md bg-raised px-2.5 py-1 text-xs font-medium text-fg hover:bg-line">
247+ <Button type="submit" variant="secondary" size="inline" className="px-2.5 py-1 text-xs">
246248 Embed
247− </button>
249+ </Button>
248250 </form>
249251 );
250252 }
+11−10
1515 import { canDo, readingTime } from "../../../lib/folios";
1616 import { Markdown } from "../../markdown";
1717 import { TimeAgo } from "../../ui";
18+import { Button } from "../../ui/button";
1819 import { foliosRequest, useFoliosData } from "../actions";
1920 import { FolioGlyph, type FolioBodyProps } from "../kinds";
2021 import { Face } from "../parts";
156157 <div className="group">
157158 {folio.icon && (
158159 <IconPicker value={folio.icon} onChange={(icon) => editable && send("update", { change: { icon } })}>
159− <button type="button" disabled={!editable} aria-label="Change the icon" className="-ml-1 mb-2 rounded-lg p-1 text-5xl leading-none hover:bg-raised disabled:hover:bg-transparent">
160+ <Button type="button" disabled={!editable} aria-label="Change the icon" variant="ghost" size="inline" className="-ml-1 mb-2 rounded-lg p-1 text-5xl leading-none disabled:hover:bg-transparent">
160161 {folio.icon}
161− </button>
162+ </Button>
162163 </IconPicker>
163164 )}
164165 {editable && !trashed && (
165166 <div className="mb-1 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100">
166167 {!folio.icon && (
167168 <IconPicker value={null} onChange={(icon) => send("update", { change: { icon } })}>
168− <button type="button" className="inline-flex h-7 items-center gap-1 rounded-md px-2 text-xs text-faint hover:bg-raised hover:text-fg">
169+ <Button type="button" variant="ghost" size="xs" className="text-faint font-normal">
169170 <SmilePlus size={13} /> Add icon
170− </button>
171+ </Button>
171172 </IconPicker>
172173 )}
173174 </div>
219220 <AlertTriangle size={15} className="shrink-0 text-warn" />
220221 <span className="min-w-0 grow text-fg-soft">Possibly out of date: code this doc cites changed since it was last brought up to date.</span>
221222 {editable && (
222− <button type="button" onClick={() => send("mark_current", {})} className="text-xs font-medium text-warn hover:underline">
223+ <Button type="button" onClick={() => send("mark_current", {})} variant="link" size="inline" className="text-xs text-warn">
223224 It&apos;s current
224− </button>
225+ </Button>
225226 )}
226227 </div>
227228 )}
235236 {suggestions.length} {suggestions.length === 1 ? "suggestion" : "suggestions"} waiting
236237 </span>
237238 {editable && suggestions.length > 1 && (
238− <button type="button" onClick={acceptAll} className="text-xs font-medium text-accent hover:underline">
239+ <Button type="button" onClick={acceptAll} variant="link" size="inline" className="text-xs">
239240 Accept all
240− </button>
241+ </Button>
241242 )}
242243 </div>
243244 <div className="space-y-2">
249250 )}
250251 {suggestions.length > 1 && editable && (
251252 <div className="mt-4 hidden justify-end 2xl:flex">
252− <button type="button" onClick={acceptAll} className="inline-flex h-8 items-center gap-1.5 rounded-md bg-accent/15 px-3 text-xs font-medium text-accent hover:bg-accent/25">
253+ <Button type="button" onClick={acceptAll} variant="link" size="sm" className="bg-accent/15 px-3 text-xs hover:bg-accent/25">
253254 <Check size={13} /> Accept all {suggestions.length} suggestions
254− </button>
255+ </Button>
255256 </div>
256257 )}
257258 </div>
+2−1
66 import { FileCode2, FolderGit2 } from "lucide-react";
77 import { useEffect, useState } from "react";
88
9−import { Button, ErrorText, Field, Input } from "../../ui";
9+import { ErrorText, Field, Input } from "../../ui";
10+import { Button } from "../../ui/button";
1011 import { Combobox } from "../../ui/combobox";
1112 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../ui/dialog";
1213 import { SelectField } from "../../ui/select";
+20−17
66 import { Check, MessageSquare, Sparkles, X } from "lucide-react";
77 import { type ReactNode, useState } from "react";
88
9−import { Button, TimeAgo } from "../../ui";
9+import { TimeAgo } from "../../ui";
10+import { Button } from "../../ui/button";
1011 import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover";
1112 import { Face } from "../parts";
1213
6263 <DiffBlock text={s.after_markdown} tone="add" />
6364 </>
6465 ) : (
65− <button type="button" onClick={() => setOpen(true)} className="text-xs text-accent hover:underline">
66+ <Button type="button" onClick={() => setOpen(true)} variant="link" size="inline" className="text-xs font-normal">
6667 Show the change
67− </button>
68+ </Button>
6869 )}
6970 </div>
7071 {canDecide && (
7172 <footer className="flex gap-2 border-t border-line px-3 py-2">
72− <button type="button" onClick={() => onDecide(s.id, "accept")} className="inline-flex h-7 items-center gap-1 rounded-md bg-success/15 px-2.5 text-xs font-medium text-success hover:bg-success/25">
73+ <Button type="button" onClick={() => onDecide(s.id, "accept")} size="xs" className="bg-success/15 px-2.5 text-success hover:bg-success/25">
7374 <Check size={13} /> Accept
74− </button>
75− <button type="button" onClick={() => onDecide(s.id, "reject")} className="inline-flex h-7 items-center gap-1 rounded-md px-2.5 text-xs text-muted hover:bg-raised hover:text-fg">
75+ </Button>
76+ <Button type="button" onClick={() => onDecide(s.id, "reject")} variant="ghost" size="xs" className="px-2.5 font-normal">
7677 <X size={13} /> Reject
77− </button>
78+ </Button>
7879 </footer>
7980 )}
8081 </article>
117118 }}
118119 >
119120 <input value={custom} onChange={(e) => setCustom(e.target.value)} placeholder="Any emoji" aria-label="Any emoji" className="h-8 min-w-0 grow rounded-md border border-line bg-bg px-2 text-sm outline-none focus:border-accent/60" />
120− <button type="submit" className="h-8 rounded-md bg-raised px-2 text-xs hover:bg-line">
121+ <Button type="submit" variant="secondary" size="xs" className="h-8 font-normal">
121122 Use
122− </button>
123+ </Button>
123124 </form>
124125 {value && (
125− <button
126+ <Button
126127 type="button"
127128 onClick={() => {
128129 onChange(null);
129130 setOpen(false);
130131 }}
131− className="mt-2 text-xs text-faint hover:text-fg"
132+ variant="link"
133+ size="inline"
134+ className="mt-2 text-xs text-faint hover:text-fg font-normal"
132135 >
133136 Remove icon
134− </button>
137+ </Button>
135138 )}
136139 </PopoverContent>
137140 </Popover>
190193 }}
191194 >
192195 <input value={replies[t.id] ?? ""} onChange={(e) => setReplies((r) => ({ ...r, [t.id]: e.target.value }))} placeholder="Reply" aria-label="Reply" className="h-8 min-w-0 grow bg-transparent text-sm outline-none placeholder:text-faint" />
193− <button type="button" onClick={() => onResolve(t.id, !t.resolved)} className="shrink-0 text-xs text-faint hover:text-fg">
196+ <Button type="button" onClick={() => onResolve(t.id, !t.resolved)} variant="link" size="inline" className="text-xs text-faint hover:text-fg font-normal">
194197 {t.resolved ? "Reopen" : "Resolve"}
195− </button>
198+ </Button>
196199 </form>
197200 )}
198201 </li>
206209 <ul className="mt-4 space-y-3">{open.map(thread)}</ul>
207210 {resolved.length > 0 && (
208211 <>
209− <button type="button" onClick={() => setShowResolved(!showResolved)} className="mt-3 text-xs text-faint hover:text-fg">
212+ <Button type="button" onClick={() => setShowResolved(!showResolved)} variant="link" size="inline" className="mt-3 text-xs text-faint hover:text-fg font-normal">
210213 {showResolved ? "Hide" : "Show"} {resolved.length} resolved
211− </button>
214+ </Button>
212215 {showResolved && <ul className="mt-3 space-y-3">{resolved.map(thread)}</ul>}
213216 </>
214217 )}
228231 aria-label="Add a comment"
229232 className="min-h-16 min-w-0 grow resize-y rounded-lg border border-line bg-surface px-3 py-2 text-sm outline-none placeholder:text-faint focus:border-accent/60"
230233 />
231− <Button type="submit" variant="quiet" disabled={!draft.trim()}>
234+ <Button type="submit" variant="outline" disabled={!draft.trim()}>
232235 Comment
233236 </Button>
234237 </form>
+10−7
1111
1212 import { dayGroups } from "../../lib/folios";
1313 import { TimeAgo } from "../ui";
14+import { Card } from "../ui/card";
1415 import { Hint } from "../ui/hint";
1516 import { useFoliosData } from "./actions";
1617 import { FolioThumbnail, KindIcon } from "./kinds";
7273 {days.map((day) => (
7374 <section key={day.key} aria-label={day.label}>
7475 <h3 className="mb-1.5 px-1 text-xs font-medium text-faint">{day.label}</h3>
75− <ul className="divide-y divide-line overflow-hidden rounded-xl border border-line bg-surface">
76− {day.items.map((f) => (
77− <FolioRow key={f.id} slug={slug} folio={f} onError={onError} />
78− ))}
79− </ul>
76+ <Card asChild divided className="overflow-hidden">
77+ <ul>
78+ {day.items.map((f) => (
79+ <FolioRow key={f.id} slug={slug} folio={f} onError={onError} />
80+ ))}
81+ </ul>
82+ </Card>
8083 </section>
8184 ))}
8285 </div>
8689 export function FolioCard({ slug, folio, onError }: { slug: string; folio: Folio; onError?: (message: string) => void }) {
8790 const me = useFoliosData()?.me.key;
8891 return (
89− <div className="group relative flex flex-col overflow-hidden rounded-xl border border-line bg-surface transition-colors hover:border-line-strong">
92+ <Card className="group relative flex flex-col overflow-hidden transition-colors hover:border-line-strong">
9093 <div className="pointer-events-none h-32 shrink-0 border-b border-line">
9194 <FolioThumbnail folio={folio} />
9295 </div>
112115 <FolioMenu slug={slug} folio={folio} onError={onError} />
113116 </span>
114117 </div>
115− </div>
118+ </Card>
116119 );
117120 }
118121
+3−2
1010 import { Link, useNavigate, useRevalidator } from "react-router";
1111
1212 import { canDo } from "../../lib/folios";
13+import { Button } from "../ui/button";
1314 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu";
1415 import { foliosRequest, useFoliosData } from "./actions";
1516 import { MoveDialog, RenameDialog, TemplateDialog } from "./dialogs";
5253 <DropdownMenu>
5354 <DropdownMenuTrigger asChild>
5455 {trigger ?? (
55− <button type="button" aria-label={`More for ${folio.title || "Untitled"}`} className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg max-md:size-10">
56+ <Button type="button" aria-label={`More for ${folio.title || "Untitled"}`} variant="ghost" size="icon-sm" className="text-faint max-md:size-10">
5657 <Ellipsis size={16} />
57− </button>
58+ </Button>
5859 )}
5960 </DropdownMenuTrigger>
6061 <DropdownMenuContent align="end" className="w-56">
+5−3
99 import { Link } from "react-router";
1010
1111 import { AgentAvatar } from "../agent-avatar";
12−import { Avatar } from "../ui";
12+
13+import { Avatar } from "../ui/avatar";
14+import { Card } from "../ui/card";
1315 import { Hint } from "../ui/hint";
1416 import { KindIcon } from "./kinds";
1517
130132
131133 export function TemplateCard({ template, action }: { template: FolioTemplate; action: ReactNode }) {
132134 return (
133− <div className="flex flex-col rounded-xl border border-line bg-surface p-4">
135+ <Card className="flex flex-col p-4">
134136 <span className="flex items-center gap-2">
135137 {template.icon ? (
136138 <span className="text-2xl leading-none" aria-hidden="true">
143145 <span className="mt-3 text-sm font-medium">{template.name}</span>
144146 <span className="mt-1 grow text-xs leading-relaxed text-muted">{template.description || (template.builtin ? "" : `Saved by ${template.created_by?.display_name ?? "your workspace"}.`)}</span>
145147 <div className="mt-3 flex items-center gap-2">{action}</div>
146− </div>
148+ </Card>
147149 );
148150 }
+3−2
1212 import { Link } from "react-router";
1313
1414 import { spacePath } from "../../lib/folios";
15−import { Button, ErrorText } from "../ui";
15+import { ErrorText } from "../ui";
16+import { Button } from "../ui/button";
1617 import { Combobox } from "../ui/combobox";
1718 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog";
1819 import { Hint } from "../ui/hint";
274275 <div className="mt-2 flex items-center justify-between gap-2 border-t border-line pt-4">
275276 <Button
276277 type="button"
277− variant="quiet"
278+ variant="outline"
278279 onClick={async () => {
279280 try {
280281 await navigator.clipboard.writeText(`${window.location.origin}${folio.path}`);
+14−12
88 import type { Folio, FolioRef } from "@g1t/contracts";
99 import { History, MessageSquare, Share2, Star, WifiOff } from "lucide-react";
1010 import { type ReactNode, useState } from "react";
11−import { Link, useRevalidator } from "react-router";
11+import { useRevalidator } from "react-router";
1212
1313 import { spacePath } from "../../lib/folios";
14+import { ButtonLink } from "../ui";
15+import { Button } from "../ui/button";
1416 import { Hint } from "../ui/hint";
1517 import { foliosRequest } from "./actions";
1618 import { FolioGlyph } from "./kinds";
2224 /** Someone here now, as the live document's awareness says. */
2325 export type Presence = { client: number; key: string; name: string; kind: "user" | "agent"; color: string; avatar: string | null; me: boolean };
2426
25−const ICON_BUTTON = "flex size-8 shrink-0 items-center justify-center rounded-md max-md:size-10";
26−
2727 export function FolioHeader({
2828 slug,
2929 folio,
8888 </span>
8989 )}
9090 {actions}
91− <button type="button" onClick={() => setSharing(true)} className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-line px-2.5 text-xs text-fg/85 hover:bg-raised max-md:size-10 max-md:justify-center max-md:px-0" aria-label="Share">
91+ <Button type="button" onClick={() => setSharing(true)} variant="outline" size="sm" className="text-xs text-fg/85 hover:bg-raised max-md:size-10 max-md:justify-center max-md:px-0 font-normal" aria-label="Share">
9292 <Share2 size={13} /> <span className="max-md:hidden">Share</span>
93− </button>
93+ </Button>
9494 {comments && (
9595 <Hint label={comments.open ? "Hide comments" : "Comments"}>
96− <button type="button" onClick={comments.onToggle} aria-pressed={comments.open} aria-label="Comments" className={`${ICON_BUTTON} ${comments.open ? "bg-raised text-fg" : "text-faint hover:bg-raised hover:text-fg"}`}>
96+ <Button type="button" onClick={comments.onToggle} aria-pressed={comments.open} aria-label="Comments" variant="ghost" size="icon-sm" className="text-faint max-md:size-10">
9797 <MessageSquare size={16} />
98− </button>
98+ </Button>
9999 </Hint>
100100 )}
101101 <Hint label="History">
102− <Link to={`${folio.path}/history`} aria-label="History" className={`${ICON_BUTTON} text-faint hover:bg-raised hover:text-fg max-sm:hidden`}>
102+ <ButtonLink to={`${folio.path}/history`} aria-label="History" variant="ghost" size="icon-sm" className="text-faint max-md:size-10 max-sm:hidden">
103103 <History size={16} />
104− </Link>
104+ </ButtonLink>
105105 </Hint>
106106 <Hint label={favorite ? "Remove from Favorites" : "Add to Favorites"}>
107− <button
107+ <Button
108108 type="button"
109109 aria-label={favorite ? "Remove from Favorites" : "Add to Favorites"}
110110 aria-pressed={favorite}
117117 onError(done.error.message);
118118 }
119119 }}
120− className={`${ICON_BUTTON} text-faint hover:bg-raised hover:text-fg max-sm:hidden`}
120+ variant="ghost"
121+ size="icon-sm"
122+ className="text-faint max-md:size-10 max-sm:hidden aria-pressed:bg-transparent aria-pressed:text-faint"
121123 >
122124 <Star size={16} className={favorite ? "fill-current text-warn" : ""} />
123− </button>
125+ </Button>
124126 </Hint>
125127 <FolioMenu slug={slug} folio={{ ...folio, favorite }} page onError={onError} onTrashed={onTrashed} />
126128 <ShareDialog slug={slug} folio={folio} open={sharing} onOpenChange={setSharing} onChanged={() => void revalidate()} />
+15−14
1313 import { NavLink, useLocation, useNavigate, useParams, useRevalidator } from "react-router";
1414
1515 import { buildTree, canDo, pathTo, repoFilePath, repoFolders, spacePath, type RepoFolder, type TreeItem } from "../../lib/folios";
16+import { Button } from "../ui/button";
1617 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu";
1718 import { Hint } from "../ui/hint";
1819 import { Skeleton } from "../ui/skeleton";
221222 await send("move", { folio_id: id, move: { space_id: space.id, parent_id: null, before_id: null } });
222223 }}
223224 >
224− <button type="button" onClick={() => setCollapsed(!collapsed)} aria-expanded={!collapsed} aria-label={collapsed ? `Show ${space.name}` : `Hide ${space.name}`} className="flex size-5 shrink-0 items-center justify-center rounded text-faint hover:bg-line hover:text-fg">
225+ <Button type="button" onClick={() => setCollapsed(!collapsed)} aria-expanded={!collapsed} aria-label={collapsed ? `Show ${space.name}` : `Hide ${space.name}`} variant="ghost" size="icon" className="size-5 rounded text-faint hover:bg-line">
225226 <ChevronRight size={13} className={`transition-transform ${collapsed ? "" : "rotate-90"}`} />
226− </button>
227+ </Button>
227228 <NavLink to={spacePath(slug, space.slug)} end prefetch="intent" className={({ isActive }) => `flex min-w-0 grow items-center gap-1.5 font-medium ${isActive ? "text-fg" : ""}`}>
228229 <span className="flex w-4 shrink-0 justify-center">
229230 <SpaceIcon space={space} size={14} />
314315 return (
315316 <li className="mt-1">
316317 <div className={`${ROW} text-fg-soft`}>
317− <button type="button" onClick={() => setCollapsed(!collapsed)} aria-expanded={!collapsed} aria-label={collapsed ? `Show ${space.repo}'s docs` : `Hide ${space.repo}'s docs`} className="flex size-5 shrink-0 items-center justify-center rounded text-faint hover:bg-line hover:text-fg">
318+ <Button type="button" onClick={() => setCollapsed(!collapsed)} aria-expanded={!collapsed} aria-label={collapsed ? `Show ${space.repo}'s docs` : `Hide ${space.repo}'s docs`} variant="ghost" size="icon" className="size-5 rounded text-faint hover:bg-line">
318319 <ChevronRight size={13} className={`transition-transform ${collapsed ? "" : "rotate-90"}`} />
319− </button>
320+ </Button>
320321 <span className="flex w-4 shrink-0 justify-center">
321322 <FolderGit2 size={14} className="text-faint" aria-hidden="true" />
322323 </span>
366367 <div className="flex h-9 shrink-0 items-center gap-1 pr-1 pl-3">
367368 <h2 className="min-w-0 grow truncate text-xs font-medium text-faint">Artifacts</h2>
368369 <NewMenu onMake={make}>
369− <button type="button" aria-label="New" className="flex h-8 items-center gap-0.5 rounded-md px-1.5 text-faint transition-colors hover:bg-raised hover:text-fg">
370+ <Button type="button" aria-label="New" variant="ghost" className="h-8 gap-0.5 px-1.5 text-faint">
370371 <Plus size={16} />
371372 <ChevronDown size={12} />
372− </button>
373+ </Button>
373374 </NewMenu>
374375 {onClose && (
375− <button type="button" aria-label="Close menu" onClick={onClose} className="flex size-8 shrink-0 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-fg">
376+ <Button type="button" aria-label="Close menu" onClick={onClose} variant="ghost" size="icon-sm" className="text-faint">
376377 <X size={16} />
377− </button>
378+ </Button>
378379 )}
379380 </div>
380381 <nav aria-label="Artifacts" className="min-h-0 grow overflow-y-auto px-2.5 pt-3 pb-4 [scrollbar-width:thin]">
428429 <DropdownMenu>
429430 <Hint label="Browse or make spaces">
430431 <DropdownMenuTrigger asChild>
431− <button type="button" aria-label="Browse or make spaces" className="flex size-6 items-center justify-center rounded text-faint hover:bg-raised hover:text-fg">
432+ <Button type="button" aria-label="Browse or make spaces" variant="ghost" size="icon" className="size-6 rounded text-faint">
432433 <Plus size={13} />
433− </button>
434+ </Button>
434435 </DropdownMenuTrigger>
435436 </Hint>
436437 <DropdownMenuContent align="end" className="w-44">
456457 title="Private"
457458 action={
458459 <Hint label="New private doc">
459− <button type="button" onClick={() => make("doc")} aria-label="New private doc" className="flex size-6 items-center justify-center rounded text-faint hover:bg-raised hover:text-fg">
460+ <Button type="button" onClick={() => make("doc")} aria-label="New private doc" variant="ghost" size="icon" className="size-6 rounded text-faint">
460461 <Plus size={13} />
461− </button>
462+ </Button>
462463 </Hint>
463464 }
464465 >
501502 open={(sidebar.repos ?? []).length > 0}
502503 action={
503504 <Hint label="Show a project's docs">
504− <button type="button" onClick={() => setAddingRepo(true)} aria-label="Show a project's docs" className="flex size-6 items-center justify-center rounded text-faint hover:bg-raised hover:text-fg">
505+ <Button type="button" onClick={() => setAddingRepo(true)} aria-label="Show a project's docs" variant="ghost" size="icon" className="size-6 rounded text-faint">
505506 <Plus size={13} />
506− </button>
507+ </Button>
507508 </Hint>
508509 }
509510 >
+2−1
66 import { DOC_AGENT_MODE_LABELS, DOC_ROLE_LABELS, DOC_SPACE_KIND_LABELS, type DocAgentMode, type DocRole, type DocSpace, type DocSpaceKind } from "@g1t/contracts";
77 import { type FormEvent, useState } from "react";
88
9−import { Button, ErrorText } from "../ui";
9+import { ErrorText } from "../ui";
10+import { Button } from "../ui/button";
1011 import { SelectField } from "../ui/select";
1112
1213 export type SpaceFormValue = {
+3−2
1212
1313 import { linkedArtifacts } from "../../lib/folios";
1414 import { TimeAgo } from "../ui";
15+import { Card } from "../ui/card";
1516 import { KindIcon } from "./kinds";
1617
1718 /** What the site answers for one link (routes/workspace/folios/api.ts `unfurl`); null: not for this viewer. */
6162 if (card === "loading" || card === undefined) return null;
6263 if (card === null) {
6364 return (
64− <div className="flex items-center gap-2.5 rounded-lg border border-line bg-surface px-3 py-2 text-xs text-faint">
65+ <Card radius="lg" className="flex items-center gap-2.5 px-3 py-2 text-xs text-faint">
6566 <Lock size={14} className="shrink-0" />
6667 An artifact you don&apos;t have access to
67− </div>
68+ </Card>
6869 );
6970 }
7071 return (
+3−2
1111 import { money } from "../lib/usage";
1212 import { TimeAgo } from "./ui";
1313 import { Badge } from "./ui/badge";
14+import { Card } from "./ui/card";
1415 import { Hint } from "./ui/hint";
1516 import { Loading, SkeletonLine } from "./ui/skeleton";
1617
9697 /** The table's frame: its heading row, around its body. */
9798 function Frame({ children }: { children: ReactNode }) {
9899 return (
99− <div className="overflow-x-auto rounded-xl border border-line bg-surface">
100+ <Card className="overflow-x-auto">
100101 <table className="w-full">
101102 <thead className="border-b border-line">
102103 <tr>
109110 </thead>
110111 {children}
111112 </table>
112− </div>
113+ </Card>
113114 );
114115 }
115116
+10−9
22 import { siGithub } from "simple-icons";
33
44 import { TimeAgo } from "./ui";
5+import { Button } from "./ui/button";
6+import { Card } from "./ui/card";
57
68 /** GitHub's mark (from Simple Icons), for the buttons that go there. */
79 export function GithubMark({ className = "size-4" }: { className?: string }) {
1820 */
1921 export function ContinueWithGithub({ href, label = "Continue with GitHub" }: { href: string; label?: string }) {
2022 return (
21− <a
22− href={href}
23− className="inline-flex w-full items-center justify-center gap-2 rounded-md border border-line px-3.5 py-2 text-sm font-medium text-fg/90 transition-colors hover:border-line-strong hover:bg-surface hover:text-fg"
24− >
25− <GithubMark />
26− {label}
27− </a>
23+ <Button asChild variant="outline" className="w-full">
24+ <a href={href}>
25+ <GithubMark />
26+ {label}
27+ </a>
28+ </Button>
2829 );
2930 }
3031
4647 */
4748 export function GithubLinkStrip({ link }: { link: GithubRepoLink }) {
4849 return (
49− <div className="flex flex-wrap items-center gap-x-3 gap-y-1 rounded-lg border border-line px-4 py-2.5 text-sm">
50+ <Card tone="plain" radius="lg" className="flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-2.5 text-sm">
5051 <span className="flex min-w-0 items-start gap-2 text-muted">
5152 <GithubMark className="mt-0.5 size-4 shrink-0" />
5253 <span className="min-w-0">
6263 </span>
6364 )}
6465 {link.lastError && <p className="w-full text-xs text-warn">{link.lastError}</p>}
65− </div>
66+ </Card>
6667 );
6768 }
+61−57
1919 import { formatCap, tri, workflowDomainLine } from "../lib/guardrails";
2020 import { formatCost } from "./agents";
2121 import { ErrorText, Input, SubmitButton, TimeAgo } from "./ui";
22+import { Badge } from "./ui/badge";
23+import { Card } from "./ui/card";
2224 import { CheckboxOption } from "./ui/checkbox";
2325 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
2426 import { Textarea } from "./ui/textarea";
5456 children: ReactNode;
5557 }) {
5658 return (
57− <div className="flex flex-col gap-3 rounded-xl border border-line bg-surface p-4 sm:flex-row sm:items-start">
59+ <Card className="flex flex-col gap-3 p-4 sm:flex-row sm:items-start">
5860 <div className="min-w-0 grow">
5961 <p className="text-sm font-medium">{title}</p>
6062 <p className="mt-1 text-sm text-muted">{children}</p>
7173 <SelectItem value="off">{labels[1]}</SelectItem>
7274 </SelectContent>
7375 </Select>
74− </div>
76+ </Card>
7577 );
7678 }
7779
135137 Restricted, a sandbox reaches g1t, the registries below and the domains you list, over HTTP and
136138 HTTPS only. Open, it reaches the whole internet.
137139 </TriSelect>
138− <div className="rounded-xl border border-line bg-surface p-4">
140+ <Card className="p-4">
139141 <p className="text-sm font-medium">Always allowed</p>
140142 <p className="mt-1 text-sm text-muted">
141143 g1t's own hosts, for cloning, pushing, reporting and the model: <Hosts hosts={view.g1tHosts} />
142144 </p>
143− </div>
144− <div className="rounded-xl border border-line bg-surface p-4">
145+ </Card>
146+ <Card className="p-4">
145147 <div className="flex flex-col gap-3 sm:flex-row sm:items-start">
146148 <div className="min-w-0 grow">
147149 <p className="text-sm font-medium">Package registries</p>
169171 />
170172 ))}
171173 </div>
172− </div>
173− <div className="rounded-xl border border-line bg-surface p-4">
174+ </Card>
175+ <Card className="p-4">
174176 <label htmlFor="guardrail-domains" className="text-sm font-medium">
175177 Allowed domains
176178 </label>
192194 From the workspace: <Hosts hosts={inheritedDomains} />
193195 </p>
194196 )}
195− </div>
196− <div className="rounded-xl border border-line bg-surface p-4">
197+ </Card>
198+ <Card className="p-4">
197199 <label htmlFor="guardrail-workflow-domains" className="text-sm font-medium">
198200 Workflow-only domains
199201 </label>
217219 <span className="font-mono break-words">{inheritedWorkflowDomains.map(workflowDomainLine).join("; ")}</span>
218220 </p>
219221 )}
220− </div>
222+ </Card>
221223 </Section>
222224
223225 <Section
242244 {rule.about}
243245 </TriSelect>
244246 ))}
245− <div className="rounded-xl border border-line bg-surface p-4">
247+ <Card className="p-4">
246248 <label htmlFor="guardrail-deny" className="text-sm font-medium">
247249 Also refuse
248250 </label>
265267 From the workspace: <span className="font-mono">{inheritedDeny.join(", ")}</span>
266268 </p>
267269 )}
268− </div>
270+ </Card>
269271 </Section>
270272
271273 <Section
272274 title="Caps"
273275 about="How much one run may cost and how long it may take. A run that reaches either is stopped, and its pull request waits for you."
274276 >
275− <div className="flex flex-col gap-3 rounded-xl border border-line bg-surface p-4 sm:flex-row sm:items-start">
277+ <Card className="flex flex-col gap-3 p-4 sm:flex-row sm:items-start">
276278 <div className="min-w-0 grow">
277279 <label htmlFor="guardrail-budget" className="text-sm font-medium">
278280 Cost per run, in US dollars
289291 defaultValue={own.budgetUsd == null ? "" : String(own.budgetUsd)}
290292 placeholder={base.budgetUsd == null ? "0" : base.budgetUsd.toFixed(2)}
291293 />
292− </div>
293− <div className="rounded-xl border border-line bg-surface p-4">
294+ </Card>
295+ <Card className="p-4">
294296 <p className="text-sm font-medium">Time per run, in minutes</p>
295297 <p className="mt-1 text-sm text-muted">Empty: as {parent}, shown faded.</p>
296298 <div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
306308 </label>
307309 ))}
308310 </div>
309− </div>
311+ </Card>
310312 </Section>
311313 </fieldset>
312314
366368 const budget = run.budgetUsd ?? null;
367369 const costShare = budget && run.costUsd != null ? Math.min(1, run.costUsd / budget) : null;
368370 return (
369− <section className="mt-6 rounded-xl border border-line bg-surface p-4">
370− <div className="flex items-center gap-2 text-sm font-medium">
371− <ShieldCheck size={15} className="text-accent" />
372− Guardrails
373− {run.halted && (
374− <span className="ml-auto rounded-full border border-warn/40 bg-warn/10 px-2 py-0.5 text-xs font-normal text-warn">
375− {run.halted === "abuse"
376− ? "Stopped: unusual CPU use"
377− : `Stopped at its ${run.halted === "budget" ? "cost" : "time"} cap`}
378− </span>
379− )}
380− </div>
381− <div className="mt-3 grid gap-4 text-sm sm:grid-cols-2">
382− {cap != null && (
383− <div>
384− <p className="flex items-center gap-1.5 text-muted">
385− <Clock size={13} />
386− <span className="tabular-nums text-fg" suppressHydrationWarning>
387− {minutes == null ? "—" : `${Math.floor(minutes)}m`}
388− </span>
389− of {cap}m
390− </p>
391− <Meter share={timeShare} tone={timeShare != null && timeShare > 0.85 ? "bg-warn" : "bg-success"} />
392− </div>
393− )}
394− {member && (
395− <div>
396− <p className="flex items-center gap-1.5 text-muted">
397− <Coins size={13} />
398− {spent ? <span className="tabular-nums text-fg">{spent}</span> : <span>Spend</span>}
399− of {formatCap(budget)}
400− </p>
401− <Meter share={costShare} tone={costShare != null && costShare > 0.85 ? "bg-warn" : "bg-success"} />
402− {active && !spent && budget != null && (
403− <p className="mt-1.5 text-xs text-faint">
404− The agent stops itself at the cap. What it spent is reported when the run ends.
371+ <Card asChild className="mt-6 p-4">
372+ <section>
373+ <div className="flex items-center gap-2 text-sm font-medium">
374+ <ShieldCheck size={15} className="text-accent" />
375+ Guardrails
376+ {run.halted && (
377+ <Badge tone="warn" size="md" className="ml-auto">
378+ {run.halted === "abuse"
379+ ? "Stopped: unusual CPU use"
380+ : `Stopped at its ${run.halted === "budget" ? "cost" : "time"} cap`}
381+ </Badge>
382+ )}
383+ </div>
384+ <div className="mt-3 grid gap-4 text-sm sm:grid-cols-2">
385+ {cap != null && (
386+ <div>
387+ <p className="flex items-center gap-1.5 text-muted">
388+ <Clock size={13} />
389+ <span className="tabular-nums text-fg" suppressHydrationWarning>
390+ {minutes == null ? "—" : `${Math.floor(minutes)}m`}
391+ </span>
392+ of {cap}m
393+ </p>
394+ <Meter share={timeShare} tone={timeShare != null && timeShare > 0.85 ? "bg-warn" : "bg-success"} />
395+ </div>
396+ )}
397+ {member && (
398+ <div>
399+ <p className="flex items-center gap-1.5 text-muted">
400+ <Coins size={13} />
401+ {spent ? <span className="tabular-nums text-fg">{spent}</span> : <span>Spend</span>}
402+ of {formatCap(budget)}
405403 </p>
406− )}
407− </div>
408− )}
409− </div>
410− </section>
404+ <Meter share={costShare} tone={costShare != null && costShare > 0.85 ? "bg-warn" : "bg-success"} />
405+ {active && !spent && budget != null && (
406+ <p className="mt-1.5 text-xs text-faint">
407+ The agent stops itself at the cap. What it spent is reported when the run ends.
408+ </p>
409+ )}
410+ </div>
411+ )}
412+ </div>
413+ </section>
414+ </Card>
411415 );
412416 }
+3−5
4848 whenShort,
4949 } from "../lib/home";
5050 import { cn } from "../lib/cn";
51+import { ButtonLink } from "./ui";
5152 import { Skeleton } from "./ui/skeleton";
5253 import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
5354
186187 </div>
187188 )}
188189 </div>
189− <Link
190− to={row.action.to}
191− className="inline-flex h-7 shrink-0 items-center rounded-md border border-line-strong px-2.5 text-xs font-medium whitespace-nowrap text-fg transition-colors hover:border-accent/50 hover:bg-raised"
192− >
190+ <ButtonLink to={row.action.to} variant="outline" size="xs" className="border-line-strong px-2.5 text-fg hover:border-accent/50 hover:bg-raised">
193191 {row.action.label}
194− </Link>
192+ </ButtonLink>
195193 </li>
196194 );
197195 }
+42−33
88 import { SubmitButton } from "./ui";
99 import { Badge, type BadgeTone } from "./ui/badge";
1010 import { Button } from "./ui/button";
11+import { Card } from "./ui/card";
1112 import { Hint } from "./ui/hint";
1213 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu";
1314 import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet";
127128 <fetcher.Form method="post" action={ACTION}>
128129 <Fields intent="undone" id={item.id} />
129130 <Hint label="Move back to Notifications">
130− <SubmitButton fetcher={fetcher} icon aria-label="Move back to Notifications" className={ICON_BUTTON}>
131+ <SubmitButton fetcher={fetcher} icon aria-label="Move back to Notifications" variant="outline" size="icon-xs" className={ICON_BUTTON}>
131132 <Undo2 size={14} />
132133 </SubmitButton>
133134 </Hint>
136137 <fetcher.Form method="post" action={ACTION}>
137138 <Fields intent="done" id={item.id} />
138139 <Hint label="Done">
139− <SubmitButton fetcher={fetcher} icon aria-label="Done" className={ICON_BUTTON}>
140+ <SubmitButton fetcher={fetcher} icon aria-label="Done" variant="outline" size="icon-xs" className={ICON_BUTTON}>
140141 <Check size={14} />
141142 </SubmitButton>
142143 </Hint>
143144 </fetcher.Form>
144145 )}
145146 <DropdownMenu>
146− <DropdownMenuTrigger aria-label="More actions" className={ICON_BUTTON}>
147− <Ellipsis size={14} />
147+ <DropdownMenuTrigger asChild>
148+ <Button variant="outline" size="icon-xs" aria-label="More actions" className={ICON_BUTTON}>
149+ <Ellipsis size={14} />
150+ </Button>
148151 </DropdownMenuTrigger>
149152 <DropdownMenuContent align="end">
150153 <DropdownMenuItem onSelect={() => submit(unread ? "read" : "unread")}>
174177 );
175178 }
176179
177−const ICON_BUTTON =
178− "flex size-7 max-sm:size-10 items-center justify-center rounded-md border border-line bg-surface text-muted transition-colors outline-none hover:border-line-strong hover:text-fg focus-visible:ring-2 focus-visible:ring-accent disabled:opacity-60";
180+/** A row's small action: an outline icon button on the surface, a finger wide on a phone. */
181+const ICON_BUTTON = "bg-surface text-muted max-sm:size-10 disabled:opacity-60";
179182
180183 /** What an empty list says, for its tab. */
181184 export function InboxEmpty({ tab, view }: { tab: InboxTab; view?: "inbox" | "saved" | "done" }) {
182185 const { title, detail } = emptyFor(tab, view);
183186 return (
184− <div className="flex flex-col items-center gap-2 rounded-lg border border-dashed border-line px-6 py-12 text-center">
187+ <Card tone="plain" radius="lg" className="flex flex-col items-center gap-2 border-dashed px-6 py-12 text-center">
185188 <Bell size={20} className="text-faint" aria-hidden="true" />
186189 <p className="text-sm font-medium">{title}</p>
187190 <p className="max-w-64 text-xs text-muted">{detail}</p>
188− </div>
191+ </Card>
189192 );
190193 }
191194
228231 fetcher={fetcher}
229232 disabled={disabled}
230233 pending="Marking…"
231− className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-muted transition-colors hover:bg-raised hover:text-fg disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-muted"
234+ variant="ghost"
235+ size="inline"
236+ className="px-2 py-1 text-xs"
232237 >
233238 <CheckCheck size={14} />
234239 Mark all read
312317 ) : (
313318 <ul aria-busy="true" className="space-y-2">
314319 {Array.from({ length: 4 }, (_, index) => (
315− <li key={index} className="flex gap-3 rounded-lg border border-line p-3">
316− <Skeleton className="size-8 rounded-full" />
317− <div className="flex-1 space-y-2">
318− <Skeleton className="h-3.5 w-3/4" />
319− <Skeleton className="h-3 w-1/2" />
320− </div>
321− </li>
320+ <Card asChild key={index} tone="plain" radius="lg" className="flex gap-3 p-3">
321+ <li>
322+ <Skeleton className="size-8 rounded-full" />
323+ <div className="flex-1 space-y-2">
324+ <Skeleton className="h-3.5 w-3/4" />
325+ <Skeleton className="h-3 w-1/2" />
326+ </div>
327+ </li>
328+ </Card>
322329 ))}
323330 </ul>
324331 )
354361 export function InboxNeedsCard({ items, total }: { items: InboxItem[]; total: number }) {
355362 if (items.length === 0) return null;
356363 return (
357− <section className="rounded-xl border border-line bg-surface p-5">
358− <div className="flex items-center justify-between gap-3">
359− <h2 className="flex items-center gap-2 text-sm font-semibold">
360− <Bell size={14} className="text-warn" aria-hidden="true" />
361− From your notifications
362− <span className="rounded-full bg-warn/15 px-1.5 text-[0.6875rem] font-medium tabular-nums text-warn">{total}</span>
363− </h2>
364− <Link to="/notifications" className="text-xs font-medium text-accent hover:underline">
365− Open Notifications
366− </Link>
367− </div>
368− <ul className="mt-3 space-y-2">
369− {items.map((item) => (
370− <InboxCard key={item.id} item={item} />
371− ))}
372− </ul>
373− </section>
364+ <Card asChild className="p-5">
365+ <section>
366+ <div className="flex items-center justify-between gap-3">
367+ <h2 className="flex items-center gap-2 text-sm font-semibold">
368+ <Bell size={14} className="text-warn" aria-hidden="true" />
369+ From your notifications
370+ <span className="rounded-full bg-warn/15 px-1.5 text-[0.6875rem] font-medium tabular-nums text-warn">{total}</span>
371+ </h2>
372+ <Link to="/notifications" className="text-xs font-medium text-accent hover:underline">
373+ Open Notifications
374+ </Link>
375+ </div>
376+ <ul className="mt-3 space-y-2">
377+ {items.map((item) => (
378+ <InboxCard key={item.id} item={item} />
379+ ))}
380+ </ul>
381+ </section>
382+ </Card>
374383 );
375384 }
+54−50
22
33 import type { WorkspaceInvitation } from "@g1t/contracts";
44
5−import { Avatar, ErrorText, SubmitButton } from "./ui";
5+import { ErrorText, SubmitButton } from "./ui";
6+import { Avatar } from "./ui/avatar";
7+import { Card } from "./ui/card";
68 import { UserCard } from "./user-card";
79 import { invitationLine } from "../lib/invitations";
810
2426 next?: string | null;
2527 }) {
2628 return (
27− <ul className="divide-y divide-line rounded-xl border border-line">
28− {invitations.map((invitation) => (
29− <li key={invitation.id} className="px-4 py-4">
30− <div className="flex flex-wrap items-center gap-3">
31− <Avatar name={invitation.workspace.slug} image={invitation.workspace.avatar} size={32} square />
32− <div className="min-w-0 grow basis-48">
33− <p className="text-sm font-medium">
34− {invitation.workspace.name}{" "}
35− <span className="font-mono text-xs font-normal text-faint">{invitation.workspace.slug}</span>
36− </p>
37− <p className="mt-0.5 text-xs text-muted">
38− {invitation.invitedBy ? (
39− <>
40− <UserCard username={invitation.invitedBy.username}>
41− <span className="font-mono text-fg/90">@{invitation.invitedBy.username}</span>
42− </UserCard>{" "}
43− </>
44− ) : (
45− "The g1t team "
46− )}
47− {invitationLine(invitation)}
48− </p>
29+ <Card asChild tone="plain" divided>
30+ <ul>
31+ {invitations.map((invitation) => (
32+ <li key={invitation.id} className="px-4 py-4">
33+ <div className="flex flex-wrap items-center gap-3">
34+ <Avatar name={invitation.workspace.slug} image={invitation.workspace.avatar} size={32} square />
35+ <div className="min-w-0 grow basis-48">
36+ <p className="text-sm font-medium">
37+ {invitation.workspace.name}{" "}
38+ <span className="font-mono text-xs font-normal text-faint">{invitation.workspace.slug}</span>
39+ </p>
40+ <p className="mt-0.5 text-xs text-muted">
41+ {invitation.invitedBy ? (
42+ <>
43+ <UserCard username={invitation.invitedBy.username}>
44+ <span className="font-mono text-fg/90">@{invitation.invitedBy.username}</span>
45+ </UserCard>{" "}
46+ </>
47+ ) : (
48+ "The g1t team "
49+ )}
50+ {invitationLine(invitation)}
51+ </p>
52+ </div>
53+ <div className="flex items-center gap-2">
54+ <Form method="post">
55+ <input type="hidden" name="intent" value="decline-invitation" />
56+ <input type="hidden" name="id" value={invitation.id} />
57+ <input type="hidden" name="workspace" value={invitation.workspace.name} />
58+ <SubmitButton variant="outline" match={{ intent: "decline-invitation", id: invitation.id }} pending="Declining…">
59+ Decline
60+ </SubmitButton>
61+ </Form>
62+ <Form method="post">
63+ <input type="hidden" name="intent" value="accept-invitation" />
64+ <input type="hidden" name="id" value={invitation.id} />
65+ {next && <input type="hidden" name="next" value={next} />}
66+ <SubmitButton match={{ intent: "accept-invitation", id: invitation.id }} pending="Joining…">
67+ Accept
68+ </SubmitButton>
69+ </Form>
70+ </div>
4971 </div>
50− <div className="flex items-center gap-2">
51− <Form method="post">
52− <input type="hidden" name="intent" value="decline-invitation" />
53− <input type="hidden" name="id" value={invitation.id} />
54− <input type="hidden" name="workspace" value={invitation.workspace.name} />
55− <SubmitButton variant="quiet" match={{ intent: "decline-invitation", id: invitation.id }} pending="Declining…">
56− Decline
57− </SubmitButton>
58− </Form>
59− <Form method="post">
60− <input type="hidden" name="intent" value="accept-invitation" />
61− <input type="hidden" name="id" value={invitation.id} />
62− {next && <input type="hidden" name="next" value={next} />}
63− <SubmitButton match={{ intent: "accept-invitation", id: invitation.id }} pending="Joining…">
64− Accept
65− </SubmitButton>
66− </Form>
67− </div>
68− </div>
69− {errorFor === invitation.id && (
70− <div className="mt-2">
71− <ErrorText>{error}</ErrorText>
72− </div>
73− )}
74− </li>
75− ))}
76− </ul>
72+ {errorFor === invitation.id && (
73+ <div className="mt-2">
74+ <ErrorText>{error}</ErrorText>
75+ </div>
76+ )}
77+ </li>
78+ ))}
79+ </ul>
80+ </Card>
7781 );
7882 }
+3−2
55 import type { Invite, InvitesOverview } from "@g1t/contracts";
66
77 import { CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
8+import { Badge } from "./ui/badge";
89 import { CheckboxOption } from "./ui/checkbox";
910 import { Hint } from "./ui/hint";
1011 import { SelectField } from "./ui/select";
3435 : "An invitation to join this workspace, which they accept or decline. Without an account, it lets them make one first."
3536 }
3637 >
37− <span className="rounded-full border border-line px-2 py-0.5 text-xs text-muted">{kind.label}</span>
38+ <Badge size="md">{kind.label}</Badge>
3839 </Hint>
3940 {(invite.status === "pending" || invite.status === "awaiting_confirmation" || invite.status === "awaiting_answer") && (
4041 <Form method="post" className="ml-auto">
4142 <input type="hidden" name="intent" value="revoke-invite" />
4243 <input type="hidden" name="id" value={invite.id} />
43− <SubmitButton variant="quiet" match={{ intent: "revoke-invite", id: invite.id }} pending="Revoking…">
44+ <SubmitButton variant="outline" match={{ intent: "revoke-invite", id: invite.id }} pending="Revoking…">
4445 Revoke
4546 </SubmitButton>
4647 </Form>
+12−7
77 import { chipStyle, matchLabels, percentDone, tidyColor, tidyLabelName } from "../lib/labels";
88 import { cn } from "../lib/cn";
99 import { ErrorText } from "./ui";
10−import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/combobox";
10+import { Button } from "./ui/button";
11+import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/command";
1112 import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
1213
1314 /** A label as a colored chip. Without a known color it is neutral. */
114115 }}
115116 >
116117 <PopoverTrigger asChild>
117− <button
118+ <Button
118119 type="button"
119120 aria-label="Edit labels"
120− className="rounded-md p-1 text-faint transition-colors hover:bg-raised hover:text-fg"
121+ variant="ghost"
122+ size="inline"
123+ className="p-1 text-faint"
121124 >
122125 <Settings2 size={15} />
123− </button>
126+ </Button>
124127 </PopoverTrigger>
125128 <PopoverContent align="end" className="w-72 p-0">
126129 <Command shouldFilter={false}>
240243 {canEdit && (
241244 <Popover open={open} onOpenChange={setOpen}>
242245 <PopoverTrigger asChild>
243− <button
246+ <Button
244247 type="button"
245248 aria-label="Choose a milestone"
246− className="rounded-md p-1 text-faint transition-colors hover:bg-raised hover:text-fg"
249+ variant="ghost"
250+ size="inline"
251+ className="p-1 text-faint"
247252 >
248253 <Settings2 size={15} />
249− </button>
254+ </Button>
250255 </PopoverTrigger>
251256 <PopoverContent align="end" className="w-72 p-0">
252257 <Command>
+8−8
369369 forge to stitch together.
370370 </p>
371371 <div className="mt-9 flex animate-fade-up flex-wrap items-center justify-center gap-3">
372− <ButtonLink to="/register" variant="primary" large>
372+ <ButtonLink to="/register" size="lg" className="rounded-full">
373373 Start for free
374374 <ArrowRight size={15} />
375375 </ButtonLink>
376− <ButtonLink to="#how" variant="quiet" large>
376+ <ButtonLink to="#how" variant="outline" size="lg" className="rounded-full">
377377 See how it works
378378 </ButtonLink>
379379 </div>
885885 event.
886886 </p>
887887 <div className="mt-6 flex flex-wrap gap-3">
888− <ButtonLink to={`${DOCS}/guides/bring-your-own-agent/`} variant="quiet">
888+ <ButtonLink to={`${DOCS}/guides/bring-your-own-agent/`} variant="outline">
889889 Connect an agent
890890 <ArrowRight size={14} />
891891 </ButtonLink>
892− <ButtonLink to={`${DOCS}/reference/api/`} variant="quiet">
892+ <ButtonLink to={`${DOCS}/reference/api/`} variant="outline">
893893 API reference
894894 </ButtonLink>
895895 </div>
944944 only on g1t.sh.
945945 </p>
946946 <div className="mt-7 flex flex-wrap gap-3">
947− <ButtonLink to="/flagon-io/g1t" variant="quiet">
947+ <ButtonLink to="/flagon-io/g1t" variant="outline">
948948 Browse the source
949949 </ButtonLink>
950− <ButtonLink to={`${DOCS}/guides/self-hosting/`} variant="quiet">
950+ <ButtonLink to={`${DOCS}/guides/self-hosting/`} variant="outline">
951951 Run g1t yourself
952952 </ButtonLink>
953953 </div>
981981 credit when you want agents to work.
982982 </p>
983983 <div className="mt-8 flex flex-wrap justify-center gap-3">
984− <ButtonLink to="/register" variant="primary" large>
984+ <ButtonLink to="/register" size="lg" className="rounded-full">
985985 Create your workspace
986986 <ArrowRight size={15} />
987987 </ButtonLink>
988− <ButtonLink to={`${DOCS}/quickstart/`} variant="quiet" large>
988+ <ButtonLink to={`${DOCS}/quickstart/`} variant="outline" size="lg" className="rounded-full">
989989 Read the quickstart
990990 </ButtonLink>
991991 </div>
+3−2
11 import { Hand, Loader } from "lucide-react";
22
33 import type { Lifecycle, Stage } from "@g1t/contracts";
4+import { Card } from "./ui/card";
45
56 const STEPS = ["Change", "Checks", "Review", "Up to date", "Landing"];
67
8586 }
8687 const current = STEP_OF[stage];
8788 return (
88− <div className="mt-4 rounded-xl border border-line bg-surface px-4 py-3.5 text-sm">
89+ <Card className="mt-4 px-4 py-3.5 text-sm">
8990 <div className="flex items-center gap-2.5">
9091 <Loader size={15} className="shrink-0 animate-spin text-accent motion-reduce:animate-none" />
9192 <p className="font-medium">
116117 })}
117118 </ol>
118119 <p className="mt-2.5 text-muted">{detail}</p>
119− </div>
120+ </Card>
120121 );
121122 }
+6−3
1616 import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
1717 import remarkGfm from "remark-gfm";
1818
19+import { Button } from "./ui/button";
1920 import { Checkbox } from "./ui/checkbox";
2021 import { WeightedLru } from "../lib/content-cache";
2122 import { type AlertKind, G1T_MENTION_HREF, type MarkdownRepo, rehypeAlerts, rehypeReferences } from "../lib/markdown-plugins";
153154 </pre>
154155 <div className="absolute top-2 right-2 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100">
155156 {language && <span className="font-mono text-[0.6875rem] text-faint">{language}</span>}
156− <button
157+ <Button
157158 type="button"
158159 aria-label="Copy"
159160 onClick={() => {
162163 setTimeout(() => setCopied(false), 1500);
163164 });
164165 }}
165− className="rounded-md border border-line bg-raised p-1.5 text-muted hover:text-fg"
166+ variant="outline"
167+ size="inline"
168+ className="bg-raised p-1.5 text-muted"
166169 >
167170 {copied ? <Check size={13} className="text-success" /> : <Copy size={13} />}
168− </button>
171+ </Button>
169172 </div>
170173 </div>
171174 );
+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

+0−0

Binary or large file; its contents are not shown.

This change is too large to show in full.