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.
| 11 | 11 | allows, | |
| 12 | 12 | } from "@g1t/contracts"; | |
| 13 | 13 | ||
| 14 | + | import { Card } from "./ui/card"; | |
| 14 | 15 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| 15 | 16 | ||
| 16 | 17 | /** A repository role picker, each role with what it is for. */ | |
| ⋯ | |||
| 50 | 51 | return ( | |
| 51 | 52 | <> | |
| 52 | 53 | {/* 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"> | |
| 67 | 70 | <table className="w-full min-w-[34rem] text-sm"> | |
| 68 | 71 | <thead> | |
| 69 | 72 | <tr className="border-b border-line bg-surface text-left text-xs text-muted"> | |
| ⋯ | |||
| 99 | 102 | })} | |
| 100 | 103 | </tbody> | |
| 101 | 104 | </table> | |
| 102 | − | </div> | |
| 105 | + | </Card> | |
| 103 | 106 | </> | |
| 104 | 107 | ); | |
| 105 | 108 | } | |
| 22 | 22 | <Form method="post" className="ml-auto"> | |
| 23 | 23 | <input type="hidden" name="intent" value={intent} /> | |
| 24 | 24 | <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 }}> | |
| 26 | 26 | {label} | |
| 27 | 27 | </SubmitButton> | |
| 28 | 28 | </Form> |
| 9 | 9 | ||
| 10 | 10 | import type { CommitStatus, Conclusion, LogChunk, WorkflowNote } from "@g1t/contracts"; | |
| 11 | 11 | ||
| 12 | + | import { Card } from "./ui/card"; | |
| 12 | 13 | import { Hint } from "./ui/hint"; | |
| 13 | 14 | import { type Line, blocks, highlight, searchLog } from "../lib/log-lines"; | |
| 14 | 15 | ||
| ⋯ | |||
| 236 | 237 | const sorted = [...notes].sort((a, b) => order[a.severity] - order[b.severity]); | |
| 237 | 238 | const blocking = notes.filter((n) => n.severity === "unsupported").length; | |
| 238 | 239 | 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> | |
| 259 | 262 | ); | |
| 260 | 263 | } | |
| 261 | 264 | ||
| ⋯ | |||
| 270 | 273 | const failed = statuses.filter((s) => s.state === "failure" || s.state === "error").length; | |
| 271 | 274 | const pending = statuses.filter((s) => s.state === "pending").length; | |
| 272 | 275 | 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> | |
| 302 | 307 | ); | |
| 303 | 308 | } | |
| 14 | 14 | ||
| 15 | 15 | import type { AgentMessage, G1tEvent } from "@g1t/contracts"; | |
| 16 | 16 | ||
| 17 | − | import { Avatar, TimeAgo } from "./ui"; | |
| 17 | + | import { TimeAgo } from "./ui"; | |
| 18 | + | import { Avatar } from "./ui/avatar"; | |
| 18 | 19 | ||
| 19 | 20 | /** Whether an actor is g1t itself: its agent or its machinery. */ | |
| 20 | 21 | const isG1t = (actor: string | null): actor is string => actor === "g1t"; |
| 34 | 34 | </p> | |
| 35 | 35 | {canAdd && ( | |
| 36 | 36 | <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…"> | |
| 38 | 38 | <Plus size={14} /> | |
| 39 | 39 | Add CI | |
| 40 | 40 | </SubmitButton> |
| 1 | 1 | import { useMemo } from "react"; | |
| 2 | 2 | ||
| 3 | 3 | import { G1tMark } from "./orchestrator"; | |
| 4 | − | import { Avatar } from "./ui"; | |
| 4 | + | ||
| 5 | 5 | import { COLUMNS, ROWS, creature } from "../lib/pixel-creature"; | |
| 6 | + | import { Avatar } from "./ui/avatar"; | |
| 6 | 7 | ||
| 7 | 8 | /** What an agent's face is drawn from. */ | |
| 8 | 9 | export type AgentLike = { |
| 6 | 6 | ||
| 7 | 7 | import { cn } from "../lib/cn"; | |
| 8 | 8 | import type { NotStarted } from "../lib/delegate"; | |
| 9 | − | import { Avatar } from "./ui"; | |
| 9 | + | ||
| 10 | + | import { Avatar } from "./ui/avatar"; | |
| 11 | + | import { Button } from "./ui/button"; | |
| 10 | 12 | import { CONTROL } from "./ui/input"; | |
| 11 | 13 | import { SelectField } from "./ui/select"; | |
| 12 | 14 | ||
| ⋯ | |||
| 153 | 155 | <Avatar name="g1t" size={16} /> | |
| 154 | 156 | <span className="truncate">g1t · no model to choose</span> | |
| 155 | 157 | </span> | |
| 156 | − | <button | |
| 158 | + | <Button | |
| 157 | 159 | type="submit" | |
| 158 | 160 | 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" | |
| 160 | 163 | > | |
| 161 | 164 | {busy ? <LoaderCircle size={14} className="animate-spin" /> : <Sparkles size={14} />} | |
| 162 | 165 | {busy ? "Starting…" : "Put an agent on it"} | |
| 163 | − | </button> | |
| 166 | + | </Button> | |
| 164 | 167 | </div> | |
| 165 | 168 | </fetcher.Form> | |
| 166 | 169 | </div> | |
| 5 | 5 | ||
| 6 | 6 | import { Idle } from "./agents"; | |
| 7 | 7 | import { TimeAgo } from "./ui"; | |
| 8 | + | import { Card } from "./ui/card"; | |
| 8 | 9 | import { Hint } from "./ui/hint"; | |
| 9 | 10 | ||
| 10 | 11 | const ROLE: Record<RepoInstructions["files"][number]["role"], string> = { | |
| ⋯ | |||
| 53 | 54 | ) : ( | |
| 54 | 55 | <ul className="mt-3 space-y-3"> | |
| 55 | 56 | {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> | |
| 85 | 88 | ))} | |
| 86 | 89 | </ul> | |
| 87 | 90 | )} | |
| 3 | 3 | ||
| 4 | 4 | import type { AgentRouting, ModelTier, SubagentDef } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | + | import { Button } from "./ui/button"; | |
| 7 | + | import { Card } from "./ui/card"; | |
| 6 | 8 | import { Hint } from "./ui/hint"; | |
| 7 | 9 | import { Input } from "./ui/input"; | |
| 8 | 10 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| ⋯ | |||
| 158 | 160 | /> | |
| 159 | 161 | {!locked && ( | |
| 160 | 162 | <Hint label="Remove"> | |
| 161 | − | <button | |
| 163 | + | <Button | |
| 162 | 164 | type="button" | |
| 163 | 165 | aria-label={`Remove responsibility ${index + 1}`} | |
| 164 | 166 | 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" | |
| 166 | 170 | > | |
| 167 | 171 | <X size={14} /> | |
| 168 | − | </button> | |
| 172 | + | </Button> | |
| 169 | 173 | </Hint> | |
| 170 | 174 | )} | |
| 171 | 175 | </li> | |
| 172 | 176 | ))} | |
| 173 | 177 | </ul> | |
| 174 | 178 | {!locked && items.length < MAX_RESPONSIBILITIES && ( | |
| 175 | − | <button | |
| 179 | + | <Button | |
| 176 | 180 | type="button" | |
| 177 | 181 | 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" | |
| 179 | 185 | > | |
| 180 | 186 | <Plus size={14} /> | |
| 181 | 187 | Add a responsibility | |
| 182 | − | </button> | |
| 188 | + | </Button> | |
| 183 | 189 | )} | |
| 184 | 190 | </div> | |
| 185 | 191 | </> | |
| ⋯ | |||
| 211 | 217 | <input type="hidden" name="subagents" value={JSON.stringify(list)} /> | |
| 212 | 218 | <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> | |
| 213 | 219 | {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> | |
| 215 | 223 | ) : ( | |
| 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> | |
| 252 | 266 | )} | |
| 253 | − | <button | |
| 267 | + | <Button | |
| 254 | 268 | type="button" | |
| 255 | 269 | onClick={() => { | |
| 256 | 270 | setError(null); | |
| 257 | 271 | setEditing({ index: null, draft: { ...BLANK_SUBAGENT, routing: clampRouting(BLANK_SUBAGENT.routing, routing) } }); | |
| 258 | 272 | }} | |
| 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" | |
| 260 | 276 | > | |
| 261 | 277 | <Plus size={14} /> | |
| 262 | 278 | Add a subagent | |
| 263 | − | </button> | |
| 279 | + | </Button> | |
| 264 | 280 | <Sheet open={editing != null} onOpenChange={(open) => !open && setEditing(null)}> | |
| 265 | 281 | <SheetContent side="right" className="w-[28rem] max-w-[100vw]"> | |
| 266 | 282 | {editing && ( | |
| ⋯ | |||
| 342 | 358 | </div> | |
| 343 | 359 | <div className="flex items-center gap-3 border-t border-line px-5 py-3"> | |
| 344 | 360 | {error && <p className="text-sm text-danger">{error}</p>} | |
| 345 | − | <button | |
| 361 | + | <Button | |
| 346 | 362 | type="button" | |
| 347 | 363 | 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" | |
| 349 | 366 | > | |
| 350 | 367 | 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"> | |
| 353 | 370 | {editing.index == null ? "Add" : "Done"} | |
| 354 | − | </button> | |
| 371 | + | </Button> | |
| 355 | 372 | </div> | |
| 356 | 373 | </div> | |
| 357 | 374 | )} | |
| 13 | 13 | } from "../lib/agent-setup"; | |
| 14 | 14 | import { useAddresses } from "../lib/addresses"; | |
| 15 | 15 | import { cn } from "../lib/cn"; | |
| 16 | + | import { Button } from "./ui/button"; | |
| 17 | + | import { Card } from "./ui/card"; | |
| 16 | 18 | ||
| 17 | 19 | /** The agent this browser chose last; every toggle on the page shares it. */ | |
| 18 | 20 | export function useAgentChoice(): [AgentId, (id: AgentId) => void] { | |
| ⋯ | |||
| 54 | 56 | ||
| 55 | 57 | return ( | |
| 56 | 58 | <div className={className}> | |
| 57 | − | <div className="@container overflow-hidden rounded-lg border border-line bg-surface"> | |
| 59 | + | <Card radius="lg" className="@container overflow-hidden"> | |
| 58 | 60 | <div | |
| 59 | 61 | role="tablist" | |
| 60 | 62 | aria-label="Coding agent" | |
| ⋯ | |||
| 99 | 101 | : agent.code} | |
| 100 | 102 | </code> | |
| 101 | 103 | </pre> | |
| 102 | − | <button | |
| 104 | + | <Button | |
| 103 | 105 | type="button" | |
| 104 | 106 | aria-label={copied ? "Copied" : `Copy the ${agent.label} ${agent.file ? "config" : "command"}`} | |
| 105 | 107 | 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" | |
| 107 | 111 | > | |
| 108 | 112 | {copied ? <Check size={14} className="text-success" /> : <Copy size={14} />} | |
| 109 | − | </button> | |
| 113 | + | </Button> | |
| 110 | 114 | </div> | |
| 111 | − | </div> | |
| 115 | + | </Card> | |
| 112 | 116 | {hint && ( | |
| 113 | 117 | <p className="mt-2 text-xs leading-5 text-muted"> | |
| 114 | 118 | {agent.then.split("`").map((part, index) => | |
| 7 | 7 | import { Link } from "react-router"; | |
| 8 | 8 | ||
| 9 | 9 | import { PixelCreature } from "./agent-avatar"; | |
| 10 | + | import { ButtonLink } from "./ui"; | |
| 11 | + | import { buttonVariants } from "./ui/button"; | |
| 12 | + | import { Card } from "./ui/card"; | |
| 10 | 13 | import { Hint } from "./ui/hint"; | |
| 11 | 14 | import { type TemplateListing, startPath, templatePath } from "../lib/agent-templates"; | |
| 12 | 15 | import { cn } from "../lib/cn"; | |
| 13 | 16 | ||
| 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`; | |
| 17 | 17 | ||
| 18 | 18 | /** Starting an agent from a template: the new-agent form with it chosen, for owners; a word on who can, for everyone else. */ | |
| 19 | 19 | export function StartAction({ listing, slug, owner, className }: { listing: TemplateListing; slug: string; owner: boolean; className?: string }) { | |
| 20 | 20 | if (!owner) { | |
| 21 | 21 | return ( | |
| 22 | 22 | <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)}> | |
| 24 | 24 | Owners start agents | |
| 25 | 25 | </span> | |
| 26 | 26 | </Hint> | |
| 27 | 27 | ); | |
| 28 | 28 | } | |
| 29 | 29 | 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}> | |
| 31 | 31 | <Plus size={14} /> | |
| 32 | 32 | {listing.agents.length > 0 ? "Start another" : "Start from this"} | |
| 33 | − | </Link> | |
| 33 | + | </ButtonLink> | |
| 34 | 34 | ); | |
| 35 | 35 | } | |
| 36 | 36 | ||
| ⋯ | |||
| 38 | 38 | export function TemplateCard({ listing, slug, owner }: { listing: TemplateListing; slug: string; owner: boolean }) { | |
| 39 | 39 | const { template, agents } = listing; | |
| 40 | 40 | 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} />} | |
| 53 | 70 | </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> | |
| 71 | 73 | ); | |
| 72 | 74 | } | |
| 7 | 7 | import { type AgentLike, AgentAvatar, PixelCreature } from "./agent-avatar"; | |
| 8 | 8 | import { StatusDot, statusLabel } from "./chat/marks"; | |
| 9 | 9 | import { isOrchestrator } from "./orchestrator"; | |
| 10 | + | import { Button } from "./ui/button"; | |
| 11 | + | import { Card } from "./ui/card"; | |
| 10 | 12 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| 11 | 13 | import { Skeleton } from "./ui/skeleton"; | |
| 12 | 14 | import { AUTONOMY, type AgentDraft, PRESETS, TIER_LABELS, cleanHandle, dollarsField } from "../lib/agent-form"; | |
| ⋯ | |||
| 529 | 531 | /> | |
| 530 | 532 | {!locked && ( | |
| 531 | 533 | <Hint label="Another name"> | |
| 532 | − | <button | |
| 534 | + | <Button | |
| 533 | 535 | type="button" | |
| 534 | 536 | aria-label="Another name" | |
| 535 | 537 | 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" | |
| 537 | 540 | > | |
| 538 | 541 | <Dices size={17} /> | |
| 539 | − | </button> | |
| 542 | + | </Button> | |
| 540 | 543 | </Hint> | |
| 541 | 544 | )} | |
| 542 | 545 | </div> | |
| ⋯ | |||
| 616 | 619 | {spaces.length ? ( | |
| 617 | 620 | <div className="grid gap-1.5 sm:grid-cols-2"> | |
| 618 | 621 | {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> | |
| 627 | 629 | ))} | |
| 628 | 630 | </div> | |
| 629 | 631 | ) : ( | |
| ⋯ | |||
| 728 | 730 | </Select> | |
| 729 | 731 | </div> | |
| 730 | 732 | </div> | |
| 731 | − | <div className="rounded-lg border border-line"> | |
| 733 | + | <Card tone="plain" radius="lg"> | |
| 732 | 734 | <button | |
| 733 | 735 | type="button" | |
| 734 | 736 | onClick={() => setAdvanced(!advanced)} | |
| ⋯ | |||
| 745 | 747 | </Label> | |
| 746 | 748 | <input id="pinned" name="pinned" defaultValue={draft.routing.pinned ?? ""} placeholder="provider/model" className={`${FIELD} font-mono`} autoComplete="off" data-1p-ignore /> | |
| 747 | 749 | </div> | |
| 748 | − | </div> | |
| 750 | + | </Card> | |
| 749 | 751 | </FormSection> | |
| 750 | 752 | ||
| 751 | 753 | <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."> | |
| ⋯ | |||
| 789 | 791 | ||
| 790 | 792 | <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"> | |
| 791 | 793 | {Object.keys(e).length > 0 && <p className="mr-auto text-sm text-danger">Check the fields marked above.</p>} | |
| 792 | − | <button | |
| 794 | + | <Button | |
| 793 | 795 | type="submit" | |
| 794 | 796 | 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" | |
| 796 | 799 | > | |
| 797 | 800 | <Sparkles size={15} /> | |
| 798 | 801 | {busy ? "Saving…" : submit} | |
| 799 | − | </button> | |
| 802 | + | </Button> | |
| 800 | 803 | </div> | |
| 801 | 804 | </Form> | |
| 802 | 805 | ); | |
| 23 | 23 | ||
| 24 | 24 | import { RunAudit } from "./audit"; | |
| 25 | 25 | import { STAGE_LABEL, StageDots } from "./lifecycle"; | |
| 26 | − | import { Avatar, SubmitButton, TimeAgo } from "./ui"; | |
| 26 | + | import { SubmitButton, TimeAgo } from "./ui"; | |
| 27 | 27 | import { Hint } from "./ui/hint"; | |
| 28 | 28 | import { | |
| 29 | 29 | AlertDialog, | |
| ⋯ | |||
| 36 | 36 | AlertDialogTitle, | |
| 37 | 37 | AlertDialogTrigger, | |
| 38 | 38 | } from "./ui/alert-dialog"; | |
| 39 | + | import { Avatar } from "./ui/avatar"; | |
| 40 | + | import { Badge } from "./ui/badge"; | |
| 41 | + | import { Card } from "./ui/card"; | |
| 39 | 42 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; | |
| 40 | 43 | import { shownStep } from "../lib/agent-step"; | |
| 41 | 44 | import { inlinePlain } from "../lib/inline-markdown"; | |
| ⋯ | |||
| 199 | 202 | <div className="flex justify-end"> | |
| 200 | 203 | <SubmitButton | |
| 201 | 204 | 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…"> | |
| 205 | 206 | Send | |
| 206 | 207 | </SubmitButton> | |
| 207 | 208 | </div> | |
| ⋯ | |||
| 484 | 485 | if (!run) return null; | |
| 485 | 486 | return ( | |
| 486 | 487 | <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"> | |
| 488 | 489 | <Loader2 size={11} className="animate-spin" /> | |
| 489 | 490 | {RUN_KIND_LABEL[run.kind]} | |
| 490 | − | </span> | |
| 491 | + | </Badge> | |
| 491 | 492 | </Hint> | |
| 492 | 493 | ); | |
| 493 | 494 | } | |
| ⋯ | |||
| 507 | 508 | /** Nothing running, said plainly. */ | |
| 508 | 509 | export function Idle({ children }: { children: ReactNode }) { | |
| 509 | 510 | 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> | |
| 515 | 518 | ); | |
| 516 | 519 | } | |
| 517 | 520 | ||
| 6 | 6 | import { ABILITY_LEVEL_LABELS } from "@g1t/contracts/abilities"; | |
| 7 | 7 | ||
| 8 | 8 | import { cn } from "../../lib/cn"; | |
| 9 | − | import { Button, ButtonLink, SubmitButton } from "../ui"; | |
| 9 | + | import { ButtonLink, SubmitButton } from "../ui"; | |
| 10 | + | import { Button } from "../ui/button"; | |
| 10 | 11 | import { Badge } from "../ui/badge"; | |
| 11 | 12 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog"; | |
| 12 | 13 | import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field"; | |
| ⋯ | |||
| 168 | 169 | )} | |
| 169 | 170 | </div> | |
| 170 | 171 | <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>)} | |
| 172 | 173 | {integration && source.connected && source.href && ( | |
| 173 | 174 | <Link to={source.href} className="text-xs text-muted underline-offset-2 hover:underline"> | |
| 174 | 175 | Manage | |
| ⋯ | |||
| 180 | 181 | <input type="hidden" name="intent" value="mcp_refresh" /> | |
| 181 | 182 | <input type="hidden" name="server" value={source.id} /> | |
| 182 | 183 | <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`}> | |
| 184 | 185 | <RefreshCw size={14} /> | |
| 185 | 186 | </SubmitButton> | |
| 186 | 187 | </Hint> | |
| ⋯ | |||
| 189 | 190 | <input type="hidden" name="intent" value="mcp_remove" /> | |
| 190 | 191 | <input type="hidden" name="server" value={source.id} /> | |
| 191 | 192 | <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}`}> | |
| 193 | 194 | <Trash2 size={14} /> | |
| 194 | 195 | </SubmitButton> | |
| 195 | 196 | </Hint> | |
| ⋯ | |||
| 218 | 219 | return ( | |
| 219 | 220 | <Dialog open={open} onOpenChange={setOpen}> | |
| 220 | 221 | <DialogTrigger asChild> | |
| 221 | − | <Button type="button" variant="quiet" disabled={count >= max}> | |
| 222 | + | <Button type="button" variant="outline" disabled={count >= max}> | |
| 222 | 223 | <Plus size={14} /> | |
| 223 | 224 | Add a server | |
| 224 | 225 | </Button> | |
| ⋯ | |||
| 244 | 245 | </Field> | |
| 245 | 246 | <FieldError>{error}</FieldError> | |
| 246 | 247 | <DialogFooter> | |
| 247 | − | <Button type="button" variant="quiet" onClick={() => setOpen(false)}> | |
| 248 | + | <Button type="button" variant="outline" onClick={() => setOpen(false)}> | |
| 248 | 249 | Cancel | |
| 249 | 250 | </Button> | |
| 250 | 251 | <SubmitButton variant="accent" fetcher={fetcher} match={{ intent: "mcp_add" }} busy={busy} pending="Listing its tools…"> | |
| 13 | 13 | import { TeamPicker } from "../agent-role"; | |
| 14 | 14 | import { AgentAvatar, PixelCreature } from "../agent-avatar"; | |
| 15 | 15 | import { Markdown } from "../markdown"; | |
| 16 | + | import { Alert } from "../ui/alert"; | |
| 16 | 17 | import { Badge } from "../ui/badge"; | |
| 18 | + | import { Button } from "../ui/button"; | |
| 19 | + | import { Card } from "../ui/card"; | |
| 17 | 20 | import { Checkbox } from "../ui/checkbox"; | |
| 18 | 21 | import { Hint } from "../ui/hint"; | |
| 19 | 22 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; | |
| ⋯ | |||
| 24 | 27 | ||
| 25 | 28 | const FIELD = | |
| 26 | 29 | "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 | − | ||
| 31 | 30 | /** What the builder's actions answer. */ | |
| 32 | 31 | export type BuilderAnswer = | |
| 33 | 32 | | { intent: "draft"; ok: true; proposal: AgentProposal } | |
| ⋯ | |||
| 130 | 129 | )} | |
| 131 | 130 | <span className="grow" /> | |
| 132 | 131 | <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"> | |
| 134 | 133 | <Sparkles size={14} className={busy ? "animate-pulse motion-reduce:animate-none" : ""} /> | |
| 135 | 134 | {busy ? "Drafting…" : "Draft it"} | |
| 136 | − | </button> | |
| 135 | + | </Button> | |
| 137 | 136 | </div> | |
| 138 | 137 | </div> | |
| 139 | 138 | </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 | + | )} | |
| 141 | 144 | <p className="mt-3 text-xs leading-relaxed text-muted"> | |
| 142 | 145 | {scope === "personal" | |
| 143 | 146 | ? "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." | |
| ⋯ | |||
| 264 | 267 | <div className="relative shrink-0"> | |
| 265 | 268 | <AgentAvatar agent={{ handle: value.handle, avatar_seed: seed }} size={56} /> | |
| 266 | 269 | <Hint label="Another face"> | |
| 267 | − | <button | |
| 270 | + | <Button | |
| 268 | 271 | type="button" | |
| 269 | 272 | aria-label="Another face" | |
| 270 | 273 | 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" | |
| 272 | 276 | > | |
| 273 | 277 | <Dices size={13} /> | |
| 274 | − | </button> | |
| 278 | + | </Button> | |
| 275 | 279 | </Hint> | |
| 276 | 280 | </div> | |
| 277 | 281 | <div className="grid min-w-0 grow gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,0.8fr)]"> | |
| ⋯ | |||
| 281 | 285 | <input id="b-name" value={value.display_name} onChange={(e) => rename(e.target.value)} className={FIELD} autoComplete="off" data-1p-ignore required /> | |
| 282 | 286 | {ideas.length > 1 && ( | |
| 283 | 287 | <Hint label="Another name"> | |
| 284 | − | <button | |
| 288 | + | <Button | |
| 285 | 289 | type="button" | |
| 286 | 290 | aria-label="Another name" | |
| 287 | 291 | onClick={() => { | |
| ⋯ | |||
| 289 | 293 | setIdea(next); | |
| 290 | 294 | rename(ideas[next]!); | |
| 291 | 295 | }} | |
| 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" | |
| 293 | 298 | > | |
| 294 | 299 | <Shuffle size={15} /> | |
| 295 | − | </button> | |
| 300 | + | </Button> | |
| 296 | 301 | </Hint> | |
| 297 | 302 | )} | |
| 298 | 303 | </div> | |
| ⋯ | |||
| 377 | 382 | {FOUNDATIONAL_SKILLS.map((skill) => { | |
| 378 | 383 | const on = !off.has(skill.id); | |
| 379 | 384 | 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> | |
| 400 | 404 | ); | |
| 401 | 405 | })} | |
| 402 | 406 | </div> | |
| ⋯ | |||
| 404 | 408 | ||
| 405 | 409 | {hints.length > 0 && ( | |
| 406 | 410 | <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> | |
| 425 | 431 | </Section> | |
| 426 | 432 | )} | |
| 427 | 433 | ||
| ⋯ | |||
| 432 | 438 | > | |
| 433 | 439 | <ul className="space-y-2"> | |
| 434 | 440 | {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> | |
| 441 | 449 | ))} | |
| 442 | 450 | </ul> | |
| 443 | 451 | </Section> | |
| ⋯ | |||
| 549 | 557 | </div> | |
| 550 | 558 | {turns.length > 0 && ( | |
| 551 | 559 | <Hint label="Start over"> | |
| 552 | − | <button | |
| 560 | + | <Button | |
| 553 | 561 | type="button" | |
| 554 | 562 | aria-label="Start over" | |
| 555 | 563 | onClick={() => { | |
| 556 | 564 | setTurns([]); | |
| 557 | 565 | onReset?.(); | |
| 558 | 566 | }} | |
| 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" | |
| 560 | 570 | > | |
| 561 | 571 | <RotateCcw size={14} /> | |
| 562 | − | </button> | |
| 572 | + | </Button> | |
| 563 | 573 | </Hint> | |
| 564 | 574 | )} | |
| 565 | 575 | </div> | |
| ⋯ | |||
| 591 | 601 | {name} is typing… | |
| 592 | 602 | </div> | |
| 593 | 603 | )} | |
| 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 | + | )} | |
| 595 | 609 | </div> | |
| 596 | 610 | <form | |
| 597 | 611 | className="flex items-end gap-2 border-t border-line p-2.5" | |
| ⋯ | |||
| 618 | 632 | placeholder={`Message ${name}`} | |
| 619 | 633 | 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" | |
| 620 | 634 | /> | |
| 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"> | |
| 622 | 636 | <ArrowUp size={16} /> | |
| 623 | − | </button> | |
| 637 | + | </Button> | |
| 624 | 638 | </form> | |
| 625 | 639 | {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>} | |
| 626 | 640 | </div> | |
| ⋯ | |||
| 641 | 655 | {teams.map((slug) => ( | |
| 642 | 656 | <input key={slug} type="hidden" name="teams" value={slug} /> | |
| 643 | 657 | ))} | |
| 644 | − | <button type="button" onClick={onEditAll} className={QUIET}> | |
| 658 | + | <Button type="button" onClick={onEditAll} variant="outline"> | |
| 645 | 659 | 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"> | |
| 648 | 662 | <Check size={15} /> | |
| 649 | 663 | {busy ? "Creating…" : `Create ${definition.display_name.trim() || "agent"}`} | |
| 650 | − | </button> | |
| 664 | + | </Button> | |
| 651 | 665 | </Form> | |
| 652 | 666 | ); | |
| 653 | 667 | } | |
| ⋯ | |||
| 707 | 721 | placeholder={`Answer in Spanish, and keep replies under five sentences`} | |
| 708 | 722 | className={`${FIELD} grow resize-y`} | |
| 709 | 723 | /> | |
| 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"> | |
| 711 | 725 | <Sparkles size={14} className={busy ? "animate-pulse motion-reduce:animate-none" : ""} /> | |
| 712 | 726 | {busy ? "Drafting…" : "Draft the change"} | |
| 713 | − | </button> | |
| 727 | + | </Button> | |
| 714 | 728 | </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 | + | )} | |
| 716 | 734 | </fetcher.Form> | |
| 717 | 735 | {shown && ( | |
| 718 | 736 | <div className="border-t border-line p-4 sm:p-5"> | |
| ⋯ | |||
| 723 | 741 | </span> | |
| 724 | 742 | </p> | |
| 725 | 743 | <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 | + | )} | |
| 727 | 749 | <save.Form method="post" className="mt-4 flex flex-wrap items-center justify-end gap-2"> | |
| 728 | 750 | <input type="hidden" name="intent" value="apply" /> | |
| 729 | 751 | <input type="hidden" name="changes" value={JSON.stringify(shown.changes)} /> | |
| 730 | 752 | <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"> | |
| 732 | 754 | 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"> | |
| 735 | 757 | <Check size={15} /> | |
| 736 | 758 | {save.state !== "idle" ? "Saving…" : `Save as version ${agent.version + 1}`} | |
| 737 | − | </button> | |
| 759 | + | </Button> | |
| 738 | 760 | </save.Form> | |
| 739 | 761 | </div> | |
| 740 | 762 | )} | |
| ⋯ | |||
| 745 | 767 | /** Each field a change touches, before and after; the job and duties as a line diff. */ | |
| 746 | 768 | export function ChangeList({ rows }: { rows: ChangeRow[] }) { | |
| 747 | 769 | 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> | |
| 785 | 809 | ); | |
| 786 | 810 | } | |
| 787 | 811 | ||
| ⋯ | |||
| 811 | 835 | {mine ? "your" : `@${agent.personal_owner}'s`} direct messages stays behind with the personal agent, which is archived. | |
| 812 | 836 | </p> | |
| 813 | 837 | <div className="flex flex-wrap gap-2"> | |
| 814 | − | <button type="submit" disabled={busy} className={PRIMARY}> | |
| 838 | + | <Button type="submit" disabled={busy} variant="accent"> | |
| 815 | 839 | <Users size={14} /> | |
| 816 | 840 | {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"> | |
| 819 | 843 | Cancel | |
| 820 | − | </button> | |
| 844 | + | </Button> | |
| 821 | 845 | </div> | |
| 822 | 846 | </Form> | |
| 823 | 847 | ) : ( | |
| 824 | − | <button type="button" onClick={() => setAsking(true)} className={QUIET}> | |
| 848 | + | <Button type="button" onClick={() => setAsking(true)} variant="outline"> | |
| 825 | 849 | <Users size={14} /> | |
| 826 | 850 | Promote to a workspace agent… | |
| 827 | − | </button> | |
| 851 | + | </Button> | |
| 828 | 852 | )} | |
| 829 | 853 | </div> | |
| 830 | 854 | )} | |
| 13 | 13 | AlertDialogTitle, | |
| 14 | 14 | AlertDialogTrigger, | |
| 15 | 15 | } from "../ui/alert-dialog"; | |
| 16 | + | import { Button } from "../ui/button"; | |
| 17 | + | import { Card } from "../ui/card"; | |
| 16 | 18 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog"; | |
| 17 | 19 | import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field"; | |
| 18 | 20 | import { Input, InputAddon, InputGroup } from "../ui/input"; | |
| ⋯ | |||
| 34 | 36 | return { fetcher, open, setOpen, error, busy: fetcher.state !== "idle" }; | |
| 35 | 37 | } | |
| 36 | 38 | ||
| 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"; | |
| 39 | 39 | ||
| 40 | 40 | /** A dollars field: `$` before it, posted as dollars. */ | |
| 41 | 41 | export function DollarsInput({ id, name, defaultValue, placeholder, required }: { id: string; name: string; defaultValue: string; placeholder?: string; required?: boolean }) { | |
| ⋯ | |||
| 83 | 83 | </Field> | |
| 84 | 84 | <FieldError>{error}</FieldError> | |
| 85 | 85 | <DialogFooter> | |
| 86 | − | <button type="button" className={QUIET} onClick={() => setOpen(false)}> | |
| 86 | + | <Button type="button" variant="outline" onClick={() => setOpen(false)}> | |
| 87 | 87 | Cancel | |
| 88 | − | </button> | |
| 89 | − | <button type="submit" className={PRIMARY} disabled={busy}> | |
| 88 | + | </Button> | |
| 89 | + | <Button type="submit" variant="accent" disabled={busy}> | |
| 90 | 90 | {busy ? "Saving…" : "Save budget"} | |
| 91 | − | </button> | |
| 91 | + | </Button> | |
| 92 | 92 | </DialogFooter> | |
| 93 | 93 | </fetcher.Form> | |
| 94 | 94 | </DialogContent> | |
| ⋯ | |||
| 117 | 117 | {session.visible ? <>“{session.title}”</> : <PrivateTitle />} stopped at its cap. A new cap lets it go on; it stops again there. | |
| 118 | 118 | </DialogDescription> | |
| 119 | 119 | </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> | |
| 123 | 125 | <fetcher.Form method="post" action={`/${slug}/-/agents/${session.agent_handle}/sessions/${session.id}`} className="grid gap-5"> | |
| 124 | 126 | <input type="hidden" name="intent" value="approve" /> | |
| 125 | 127 | <Field> | |
| ⋯ | |||
| 129 | 131 | </Field> | |
| 130 | 132 | <FieldError>{error}</FieldError> | |
| 131 | 133 | <DialogFooter> | |
| 132 | − | <button type="button" className={QUIET} onClick={() => setOpen(false)}> | |
| 134 | + | <Button type="button" variant="outline" onClick={() => setOpen(false)}> | |
| 133 | 135 | Cancel | |
| 134 | − | </button> | |
| 135 | − | <button type="submit" className={PRIMARY} disabled={busy}> | |
| 136 | + | </Button> | |
| 137 | + | <Button type="submit" variant="accent" disabled={busy}> | |
| 136 | 138 | {busy ? "Approving…" : "Approve and go on"} | |
| 137 | − | </button> | |
| 139 | + | </Button> | |
| 138 | 140 | </DialogFooter> | |
| 139 | 141 | </fetcher.Form> | |
| 140 | 142 | </DialogContent> | |
| ⋯ | |||
| 181 | 183 | ))} | |
| 182 | 184 | <AlertDialogFooter> | |
| 183 | 185 | <AlertDialogCancel type="button">Cancel</AlertDialogCancel> | |
| 184 | − | <button | |
| 186 | + | <Button | |
| 185 | 187 | 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"> | |
| 189 | 189 | {busy ? "Working…" : confirm} | |
| 190 | − | </button> | |
| 190 | + | </Button> | |
| 191 | 191 | </AlertDialogFooter> | |
| 192 | 192 | </fetcher.Form> | |
| 193 | 193 | </AlertDialogContent> | |
| ⋯ | |||
| 195 | 195 | ); | |
| 196 | 196 | } | |
| 197 | 197 | ||
| 198 | − | export const BUTTONS = { PRIMARY, QUIET }; | |
| 8 | 8 | import { Badge } from "../ui/badge"; | |
| 9 | 9 | import { Hint } from "../ui/hint"; | |
| 10 | 10 | import { TimeAgo } from "../ui"; | |
| 11 | + | import { Card } from "../ui/card"; | |
| 11 | 12 | import { cn } from "../../lib/cn"; | |
| 12 | 13 | import { money } from "../../lib/usage"; | |
| 13 | 14 | import { kindLabel, meterTone, monthDays, sessionStatus, shareOf, shortDay, topSlices, whereLabel } from "./format"; | |
| ⋯ | |||
| 150 | 151 | /** A live session as a card: the agent, what it is doing, how far and what it has cost. */ | |
| 151 | 152 | export function SessionCard({ slug, session, action }: { slug: string; session: AgentSession; action?: ReactNode }) { | |
| 152 | 153 | 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} /> | |
| 162 | 166 | </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>} | |
| 175 | 180 | </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> | |
| 180 | 183 | ); | |
| 181 | 184 | } | |
| 182 | 185 | ||
| ⋯ | |||
| 286 | 289 | /** A quiet empty state inside a panel or a tab. */ | |
| 287 | 290 | export function Quiet({ title, children, icon }: { title: string; children?: ReactNode; icon?: ReactNode }) { | |
| 288 | 291 | 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"> | |
| 290 | 293 | {icon && <span className="mx-auto mb-3 flex size-10 items-center justify-center rounded-xl bg-raised text-muted">{icon}</span>} | |
| 291 | 294 | <p className="text-sm font-medium">{title}</p> | |
| 292 | 295 | {children && <div className="mx-auto mt-1 max-w-md text-sm text-muted">{children}</div>} | |
| 293 | − | </div> | |
| 296 | + | </Card> | |
| 294 | 297 | ); | |
| 295 | 298 | } | |
| 2 | 2 | ||
| 3 | 3 | import { type NewRoutine, ROUTINE_EVENTS, type RoutineEvent, type RoutineSchedule } from "@g1t/contracts"; | |
| 4 | 4 | ||
| 5 | + | import { Button } from "../ui/button"; | |
| 6 | + | import { Card } from "../ui/card"; | |
| 5 | 7 | import { CheckboxOption } from "../ui/checkbox"; | |
| 6 | 8 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog"; | |
| 7 | 9 | import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field"; | |
| ⋯ | |||
| 9 | 11 | import { SelectField } from "../ui/select"; | |
| 10 | 12 | import { Switch } from "../ui/switch"; | |
| 11 | 13 | import { Textarea } from "../ui/textarea"; | |
| 12 | − | import { BUTTONS, useDialogFetcher } from "./dialogs"; | |
| 14 | + | import { useDialogFetcher } from "./dialogs"; | |
| 13 | 15 | import { timeField, weekdayName } from "./format"; | |
| 14 | 16 | ||
| 15 | 17 | /** A conversation a routine can post in. */ | |
| ⋯ | |||
| 82 | 84 | /> | |
| 83 | 85 | </Field> | |
| 84 | 86 | ||
| 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"> | |
| 109 | 102 | <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} | |
| 113 | 106 | size="sm" | |
| 114 | − | aria-label="Day of the week" | |
| 107 | + | aria-label="How often" | |
| 115 | 108 | /> | |
| 116 | 109 | </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} | |
| 129 | 165 | /> | |
| 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> | |
| 146 | 174 | )} | |
| 147 | 175 | </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> | |
| 175 | 179 | {scheduled ? null : <input type="hidden" name="every" value="none" />} | |
| 176 | 180 | ||
| 177 | 181 | <Field> | |
| ⋯ | |||
| 192 | 196 | </Field> | |
| 193 | 197 | <FieldError>{error}</FieldError> | |
| 194 | 198 | <DialogFooter> | |
| 195 | − | <button type="button" className={BUTTONS.QUIET} onClick={() => setOpen(false)}> | |
| 199 | + | <Button type="button" variant="outline" onClick={() => setOpen(false)}> | |
| 196 | 200 | 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)}> | |
| 199 | 203 | {busy ? "Saving…" : draft.id ? "Save routine" : "Add routine"} | |
| 200 | − | </button> | |
| 204 | + | </Button> | |
| 201 | 205 | </DialogFooter> | |
| 202 | 206 | </fetcher.Form> | |
| 203 | 207 | </DialogContent> | |
| 9 | 9 | import type { LibrarySkill, SkillAttachment, SkillLibrary, SkillOrigin, SkillScope } from "@g1t/contracts"; | |
| 10 | 10 | ||
| 11 | 11 | import { Badge } from "../ui/badge"; | |
| 12 | + | import { Button } from "../ui/button"; | |
| 12 | 13 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog"; | |
| 13 | 14 | import { Field, FieldDescription, FieldError, FieldLabel } from "../ui/field"; | |
| 14 | 15 | import { Hint } from "../ui/hint"; | |
| 15 | 16 | import { SelectField } from "../ui/select"; | |
| 16 | − | import { BUTTONS, useDialogFetcher } from "./dialogs"; | |
| 17 | + | import { useDialogFetcher } from "./dialogs"; | |
| 17 | 18 | ||
| 18 | 19 | /** `/acme/-/agents/skills`, or one skill's page. */ | |
| 19 | 20 | export function skillsPath(slug: string, name?: string, rest = ""): string { | |
| ⋯ | |||
| 148 | 149 | </Field> | |
| 149 | 150 | <FieldError>{error}</FieldError> | |
| 150 | 151 | <DialogFooter> | |
| 151 | − | <button type="button" className={BUTTONS.QUIET} onClick={() => setOpen(false)}> | |
| 152 | + | <Button type="button" variant="outline" onClick={() => setOpen(false)}> | |
| 152 | 153 | Cancel | |
| 153 | − | </button> | |
| 154 | − | <button type="submit" className={BUTTONS.PRIMARY} disabled={busy || !where}> | |
| 154 | + | </Button> | |
| 155 | + | <Button type="submit" variant="accent" disabled={busy || !where}> | |
| 155 | 156 | {busy ? "Attaching…" : "Attach"} | |
| 156 | − | </button> | |
| 157 | + | </Button> | |
| 157 | 158 | </DialogFooter> | |
| 158 | 159 | </fetcher.Form> | |
| 159 | 160 | </DialogContent> | |
| ⋯ | |||
| 189 | 190 | </Field> | |
| 190 | 191 | <FieldError>{error}</FieldError> | |
| 191 | 192 | <DialogFooter> | |
| 192 | − | <button type="button" className={BUTTONS.QUIET} onClick={() => setOpen(false)}> | |
| 193 | + | <Button type="button" variant="outline" onClick={() => setOpen(false)}> | |
| 193 | 194 | Cancel | |
| 194 | − | </button> | |
| 195 | − | <button type="submit" className={BUTTONS.PRIMARY} disabled={busy || !name}> | |
| 195 | + | </Button> | |
| 196 | + | <Button type="submit" variant="accent" disabled={busy || !name}> | |
| 196 | 197 | {busy ? "Attaching…" : "Attach"} | |
| 197 | − | </button> | |
| 198 | + | </Button> | |
| 198 | 199 | </DialogFooter> | |
| 199 | 200 | </fetcher.Form> | |
| 200 | 201 | </DialogContent> | |
| 8 | 8 | import { Link } from "react-router"; | |
| 9 | 9 | ||
| 10 | 10 | import { analyticsHere, choice, choose, consentRequired, onChoice } from "../lib/analytics-consent"; | |
| 11 | + | import { Button } from "./ui/button"; | |
| 11 | 12 | ||
| 12 | 13 | /** Whether this visitor is asked at all, known only once the page is in the browser. */ | |
| 13 | 14 | export function useAsksFirst(): boolean { | |
| ⋯ | |||
| 33 | 34 | </Link> | |
| 34 | 35 | </p> | |
| 35 | 36 | <div className="mt-3 flex gap-2"> | |
| 36 | − | <button | |
| 37 | + | <Button | |
| 37 | 38 | type="button" | |
| 38 | 39 | 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" | |
| 40 | 42 | > | |
| 41 | 43 | Allow | |
| 42 | − | </button> | |
| 43 | − | <button | |
| 44 | + | </Button> | |
| 45 | + | <Button | |
| 44 | 46 | type="button" | |
| 45 | 47 | 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" | |
| 47 | 51 | > | |
| 48 | 52 | No thanks | |
| 49 | − | </button> | |
| 53 | + | </Button> | |
| 50 | 54 | </div> | |
| 51 | 55 | </section> | |
| 52 | 56 | ); | |
| 4 | 4 | ||
| 5 | 5 | import { ConnectorMark } from "./connectors"; | |
| 6 | 6 | import { TierBadge } from "./marketplace"; | |
| 7 | + | import { Card } from "./ui/card"; | |
| 7 | 8 | import { Skeleton } from "./ui/skeleton"; | |
| 8 | 9 | import { type BuiltinApp, type InstalledAppData, type PinnableApp, appPinFromForm, installedAppOf, withPin } from "../lib/apps"; | |
| 9 | 10 | import { cn } from "../lib/cn"; | |
| ⋯ | |||
| 196 | 197 | <NoApps slug={slug} onOpen={onClose} compact /> | |
| 197 | 198 | ) : ( | |
| 198 | 199 | <> | |
| 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> | |
| 210 | 213 | {apps.length === 0 ? ( | |
| 211 | 214 | <p className="px-2 py-6 text-center text-sm text-muted">No installed app matches.</p> | |
| 212 | 215 | ) : ( | |
| 1 | 1 | import { ArrowUp, AtSign, BookMarked, ChevronDown, MessageSquare, Plus, Sparkles } from "lucide-react"; | |
| 2 | 2 | import type { ReactNode } from "react"; | |
| 3 | 3 | ||
| 4 | + | import { Badge } from "./ui/badge"; | |
| 5 | + | import { Card } from "./ui/card"; | |
| 4 | 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| 5 | 7 | ||
| 6 | 8 | /** | |
| ⋯ | |||
| 12 | 14 | export function AskComposer({ children }: { children?: ReactNode }) { | |
| 13 | 15 | return ( | |
| 14 | 16 | <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"> | |
| 16 | 18 | <p className="flex items-center gap-2 px-4 pt-3 text-sm font-medium text-fg"> | |
| 17 | 19 | <Sparkles size={14} className="text-accent" aria-hidden="true" /> | |
| 18 | 20 | 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> | |
| 20 | 22 | </p> | |
| 21 | 23 | <label htmlFor="ask-g1t" className="sr-only"> | |
| 22 | 24 | Ask the agent (coming soon) | |
| ⋯ | |||
| 46 | 48 | <ArrowUp size={15} /> | |
| 47 | 49 | </Soon> | |
| 48 | 50 | </div> | |
| 49 | − | </div> | |
| 51 | + | </Card> | |
| 50 | 52 | {children && <div className="flex flex-wrap items-center gap-2">{children}</div>} | |
| 51 | 53 | </section> | |
| 52 | 54 | ); | |
| 12 | 12 | import { CheckStateIcon } from "./commit-checks"; | |
| 13 | 13 | import { ChangeSize } from "./work"; | |
| 14 | 14 | import { PullIcon } from "./work-icons"; | |
| 15 | + | import { Badge } from "./ui/badge"; | |
| 16 | + | import { Card } from "./ui/card"; | |
| 15 | 17 | import { SkeletonLine } from "./ui/skeleton"; | |
| 16 | 18 | ||
| 17 | 19 | function ReviewIcon({ state }: { state: Attempt["review"]["state"] }) { | |
| ⋯ | |||
| 37 | 39 | </Link> | |
| 38 | 40 | )} | |
| 39 | 41 | {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> | |
| 41 | 43 | )} | |
| 42 | 44 | <span className="ml-auto shrink-0"> | |
| 43 | 45 | <ChangeSize files={attempt.files} /> | |
| ⋯ | |||
| 76 | 78 | return ( | |
| 77 | 79 | <Suspense | |
| 78 | 80 | 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> | |
| 80 | 82 | <SkeletonLine className="text-sm" barClassName="w-48" /> | |
| 81 | − | </div> | |
| 83 | + | </Card> | |
| 82 | 84 | } | |
| 83 | 85 | > | |
| 84 | 86 | <Await resolve={attempts} errorElement={null}> | |
| 85 | 87 | {(found) => | |
| 86 | 88 | 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> | |
| 101 | 105 | ) | |
| 102 | 106 | } | |
| 103 | 107 | </Await> | |
| 10 | 10 | import type { AuditEntry } from "@g1t/contracts"; | |
| 11 | 11 | ||
| 12 | 12 | 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"; | |
| 14 | 16 | import { Hint } from "./ui/hint"; | |
| 15 | 17 | import { Loading, SkeletonRows } from "./ui/skeleton"; | |
| 16 | 18 | ||
| ⋯ | |||
| 58 | 60 | /** The workspace's log, one row an entry, newest first. */ | |
| 59 | 61 | export function AuditTable({ entries, base }: { entries: AuditEntry[]; base: string }) { | |
| 60 | 62 | 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"> | |
| 91 | 101 | <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} | |
| 94 | 104 | </span> | |
| 95 | 105 | </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> | |
| 112 | 116 | ); | |
| 113 | 117 | } | |
| 114 | 118 | ||
| ⋯ | |||
| 134 | 138 | {denied > 0 && <span className="text-danger"> · {denied} refused</span>} | |
| 135 | 139 | {compact && entries.length > shown.length && ` · the latest ${shown.length}`} | |
| 136 | 140 | </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> | |
| 151 | 152 | </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> | |
| 158 | 164 | </div> | |
| 159 | 165 | ); | |
| 160 | 166 | } | |
| 3 | 3 | import { Form } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | 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"; | |
| 7 | 8 | ||
| 8 | 9 | /** | |
| 9 | 10 | * Uploading an avatar, as GitHub's settings do: the one shown now, a | |
| ⋯ | |||
| 63 | 64 | {image && ( | |
| 64 | 65 | <Form method="post"> | |
| 65 | 66 | <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}> | |
| 67 | 68 | <Trash2 size={15} /> | |
| 68 | 69 | Remove | |
| 69 | 70 | </SubmitButton> | |
| 3 | 3 | import { useFetcher } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import { ErrorText } from "./ui"; | |
| 6 | + | import { Button } from "./ui/button"; | |
| 6 | 7 | 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"; | |
| 8 | 9 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; | |
| 9 | 10 | ||
| 10 | 11 | /** | |
| ⋯ | |||
| 50 | 51 | > | |
| 51 | 52 | <Hint label="Change the branch it merges into"> | |
| 52 | 53 | <PopoverTrigger asChild> | |
| 53 | − | <button | |
| 54 | + | <Button | |
| 54 | 55 | 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" | |
| 56 | 59 | > | |
| 57 | 60 | {shown} | |
| 58 | 61 | <Pencil size={12} className="text-faint group-hover:text-fg" /> | |
| 59 | 62 | <span className="sr-only">Change the branch it merges into</span> | |
| 60 | − | </button> | |
| 63 | + | </Button> | |
| 61 | 64 | </PopoverTrigger> | |
| 62 | 65 | </Hint> | |
| 63 | 66 | <PopoverContent align="start" className="w-72 p-0"> | |
| 181 | 181 | Keep the plan | |
| 182 | 182 | </SubmitButton> | |
| 183 | 183 | ) : status.kind === "paid" && subscription ? ( | |
| 184 | − | <SubmitButton variant="quiet" name="intent" value="cancel" pending="Saving…"> | |
| 184 | + | <SubmitButton variant="outline" name="intent" value="cancel" pending="Saving…"> | |
| 185 | 185 | Downgrade to free at the period's end | |
| 186 | 186 | </SubmitButton> | |
| 187 | 187 | ) : null} | |
| ⋯ | |||
| 342 | 342 | At most <Dollar name="monthly" label="Monthly maximum in dollars" defaultValue={whole(r.monthlyMaxMicros)} /> a month | |
| 343 | 343 | {(r.reloadedMicros ?? 0) > 0 && <span className="text-faint">· {money(r.reloadedMicros ?? 0)} reloaded this month</span>} | |
| 344 | 344 | </div> | |
| 345 | − | <SubmitButton variant="quiet" match={{ intent: "ai-reload" }} pending="Saving…"> | |
| 345 | + | <SubmitButton variant="outline" match={{ intent: "ai-reload" }} pending="Saving…"> | |
| 346 | 346 | Save auto-reload | |
| 347 | 347 | </SubmitButton> | |
| 348 | 348 | <p className="text-xs text-faint">A reload that cannot be charged turns auto-reload off and tells the owners by email.</p> | |
| ⋯ | |||
| 403 | 403 | <span className="block text-xs text-muted">Webhook, told of each alert (optional)</span> | |
| 404 | 404 | <input name="webhook" type="url" placeholder="https://hooks.example.com/g1t" defaultValue={limit.budgetWebhook ?? ""} className={`mt-1 ${FIELD} max-w-md`} /> | |
| 405 | 405 | </label> | |
| 406 | − | <SubmitButton variant="quiet" match={{ intent: "budget" }} pending="Saving…"> | |
| 406 | + | <SubmitButton variant="outline" match={{ intent: "budget" }} pending="Saving…"> | |
| 407 | 407 | Save alerts | |
| 408 | 408 | </SubmitButton> | |
| 409 | 409 | </Form> | |
| ⋯ | |||
| 430 | 430 | aside={ | |
| 431 | 431 | owner && enabled ? ( | |
| 432 | 432 | <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…"> | |
| 434 | 434 | Manage in Stripe | |
| 435 | 435 | <ArrowUpRight size={14} /> | |
| 436 | 436 | </SubmitButton> | |
| ⋯ | |||
| 633 | 633 | </label> | |
| 634 | 634 | {!disabled && ( | |
| 635 | 635 | <div className="flex items-end"> | |
| 636 | − | <SubmitButton variant="quiet" match={{ intent: "details" }} pending="Saving…"> | |
| 636 | + | <SubmitButton variant="outline" match={{ intent: "details" }} pending="Saving…"> | |
| 637 | 637 | Save invoice details | |
| 638 | 638 | </SubmitButton> | |
| 639 | 639 | </div> | |
| 30 | 30 | wholeDollars, | |
| 31 | 31 | } from "../lib/billing"; | |
| 32 | 32 | import { ErrorText, SubmitButton } from "./ui"; | |
| 33 | + | import { Badge } from "./ui/badge"; | |
| 33 | 34 | import { RadioGroup, RadioGroupItem } from "./ui/radio-group"; | |
| 34 | 35 | ||
| 35 | 36 | /** An error the action returned, for the section it belongs to. */ | |
| ⋯ | |||
| 287 | 288 | </SubmitButton> | |
| 288 | 289 | ) : null} | |
| 289 | 290 | {(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…"> | |
| 291 | 292 | {status.kind === "past_due" ? "Update payment on Stripe" : "Manage on Stripe"} | |
| 292 | 293 | <ArrowUpRight size={14} /> | |
| 293 | 294 | </SubmitButton> | |
| 294 | 295 | )} | |
| 295 | 296 | {status.kind === "paid" && subscription && ( | |
| 296 | − | <SubmitButton variant="quiet" name="intent" value="cancel" pending="Saving…"> | |
| 297 | + | <SubmitButton variant="outline" name="intent" value="cancel" pending="Saving…"> | |
| 297 | 298 | End at the end of the period | |
| 298 | 299 | </SubmitButton> | |
| 299 | 300 | )} | |
| ⋯ | |||
| 401 | 402 | {verified && left > 0 && <Meter label="Trial used" used={Math.max(0, trialMicros - left)} of={trialMicros} />} | |
| 402 | 403 | {!verified && enabled && owner && ( | |
| 403 | 404 | <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…"> | |
| 405 | 406 | <ShieldCheck size={14} /> | |
| 406 | 407 | Check a card | |
| 407 | 408 | </SubmitButton> | |
| ⋯ | |||
| 429 | 430 | icon={<Gauge size={16} />} | |
| 430 | 431 | title="Spend limit" | |
| 431 | 432 | 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} | |
| 433 | 434 | > | |
| 434 | 435 | {limit.account.startsWith("ent_") && ( | |
| 435 | 436 | <p className="mt-2 text-sm text-muted"> | |
| ⋯ | |||
| 517 | 518 | </RadioGroup> | |
| 518 | 519 | </fieldset> | |
| 519 | 520 | <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…"> | |
| 521 | 522 | Save limit | |
| 522 | 523 | </SubmitButton> | |
| 523 | 524 | <a href="#raise" className="text-sm text-muted hover:text-fg"> | |
| ⋯ | |||
| 596 | 597 | className={`${CONTROL} w-full`} | |
| 597 | 598 | /> | |
| 598 | 599 | </label> | |
| 599 | − | <SubmitButton variant="quiet" match={{ intent: "request", kind: "limit" }} pending="Sending…"> | |
| 600 | + | <SubmitButton variant="outline" match={{ intent: "request", kind: "limit" }} pending="Sending…"> | |
| 600 | 601 | Send the request | |
| 601 | 602 | </SubmitButton> | |
| 602 | 603 | <ErrorText>{error}</ErrorText> | |
| ⋯ | |||
| 636 | 637 | <li key={grant.id} className={`px-3.5 py-2.5 text-sm ${grant.state === "open" ? "" : "text-muted"}`}> | |
| 637 | 638 | <p className="flex flex-wrap items-center gap-x-2 gap-y-1"> | |
| 638 | 639 | <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> | |
| 640 | 641 | </p> | |
| 641 | 642 | <p className="mt-0.5 text-xs text-faint"> | |
| 642 | 643 | {grant.kind === "refund" && grant.refundFor ? `For ${grant.refundFor}. ` : ""} | |
| ⋯ | |||
| 682 | 683 | <input type="hidden" name="intent" value="prepay" /> | |
| 683 | 684 | <div className="flex flex-wrap items-center gap-2"> | |
| 684 | 685 | {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…"> | |
| 686 | 687 | {wholeDollars(amount * 1_000_000)} | |
| 687 | 688 | </SubmitButton> | |
| 688 | 689 | ))} | |
| ⋯ | |||
| 758 | 759 | </span> | |
| 759 | 760 | <DollarInput name="issue" label="Issue cap in dollars" defaultValue={String(entitlements.issueCapMicros / 1_000_000)} /> | |
| 760 | 761 | </label> | |
| 761 | − | <SubmitButton variant="quiet" match={{ intent: "caps" }} pending="Saving…"> | |
| 762 | + | <SubmitButton variant="outline" match={{ intent: "caps" }} pending="Saving…"> | |
| 762 | 763 | Save caps | |
| 763 | 764 | </SubmitButton> | |
| 764 | 765 | </Form> | |
| ⋯ | |||
| 795 | 796 | placeholder="An agent kept retrying a failing check on Tuesday night." | |
| 796 | 797 | className={`${CONTROL} w-full`} | |
| 797 | 798 | /> | |
| 798 | − | <SubmitButton variant="quiet" match={{ intent: "request", kind: "overage" }} pending="Sending…"> | |
| 799 | + | <SubmitButton variant="outline" match={{ intent: "request", kind: "overage" }} pending="Sending…"> | |
| 799 | 800 | Tell g1t | |
| 800 | 801 | </SubmitButton> | |
| 801 | 802 | <ErrorText>{error}</ErrorText> | |
| 18 | 18 | import { CommitAvatars, CommitNames } from "./commit-person"; | |
| 19 | 19 | import { Markdown } from "./markdown"; | |
| 20 | 20 | import { TimeAgo } from "./ui"; | |
| 21 | + | import { Button } from "./ui/button"; | |
| 21 | 22 | import { Skeleton } from "./ui/skeleton"; | |
| 22 | 23 | ||
| 23 | 24 | /** | |
| ⋯ | |||
| 204 | 205 | <span className="grow text-sm font-medium"> | |
| 205 | 206 | Why line{picked.start === picked.end ? ` ${picked.start}` : `s ${picked.start}–${picked.end}`} | |
| 206 | 207 | </span> | |
| 207 | − | <button | |
| 208 | + | <Button | |
| 208 | 209 | type="button" | |
| 209 | 210 | onClick={onClose} | |
| 210 | 211 | 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" | |
| 212 | 215 | > | |
| 213 | 216 | <X size={14} /> | |
| 214 | − | </button> | |
| 217 | + | </Button> | |
| 215 | 218 | </header> | |
| 216 | 219 | {!current ? ( | |
| 217 | 220 | <div className="space-y-3 p-4" aria-busy={loading}> | |
| 10 | 10 | import { type ChecksSource, CommitChecksBadge } from "./commit-checks"; | |
| 11 | 11 | import { host } from "./deploy"; | |
| 12 | 12 | import { TimeAgo } from "./ui"; | |
| 13 | + | import { Card } from "./ui/card"; | |
| 13 | 14 | import { Hint } from "./ui/hint"; | |
| 14 | 15 | ||
| 15 | 16 | function drift({ ahead, behind }: Drift, main: string): string { | |
| ⋯ | |||
| 39 | 40 | checks?: ChecksSource; | |
| 40 | 41 | }) { | |
| 41 | 42 | 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" /> | |
| 80 | 60 | </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} /> | |
| 84 | 70 | </span> | |
| 85 | 71 | </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 | + | ) : ( | |
| 90 | 107 | <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" | |
| 93 | 110 | > | |
| 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 | |
| 97 | 112 | </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> | |
| 127 | 130 | ); | |
| 128 | 131 | } | |
| 6 | 6 | ||
| 7 | 7 | import { AgentPill } from "./marks"; | |
| 8 | 8 | 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"; | |
| 10 | 13 | import { Hint } from "../ui/hint"; | |
| 11 | 14 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; | |
| 12 | 15 | import { Switch } from "../ui/switch"; | |
| ⋯ | |||
| 85 | 88 | [slug], | |
| 86 | 89 | ); | |
| 87 | 90 | } | |
| 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"; | |
| 93 | 91 | ||
| 94 | 92 | /** Why someone may not make a channel: the workspace keeps it to its owners. */ | |
| 95 | 93 | export const OWNERS_ONLY_CHANNELS = "Only workspace owners can create channels here."; | |
| ⋯ | |||
| 140 | 138 | return ( | |
| 141 | 139 | <Hint label={OWNERS_ONLY_CHANNELS} disabled> | |
| 142 | 140 | {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"> | |
| 144 | 142 | <Plus size={14} /> | |
| 145 | − | </button> | |
| 143 | + | </Button> | |
| 146 | 144 | ) : ( | |
| 147 | − | <button type="button" disabled className={`${QUIET_BUTTON} opacity-50`}> | |
| 145 | + | <Button type="button" disabled variant="outline" className="opacity-50"> | |
| 148 | 146 | <Lock size={14} /> | |
| 149 | 147 | Create a channel | |
| 150 | − | </button> | |
| 148 | + | </Button> | |
| 151 | 149 | )} | |
| 152 | 150 | </Hint> | |
| 153 | 151 | ); | |
| ⋯ | |||
| 156 | 154 | <> | |
| 157 | 155 | {variant === "icon" ? ( | |
| 158 | 156 | <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"> | |
| 160 | 158 | <Plus size={14} /> | |
| 161 | − | </button> | |
| 159 | + | </Button> | |
| 162 | 160 | </Hint> | |
| 163 | 161 | ) : ( | |
| 164 | − | <button type="button" onClick={() => setOpen(true)} className={QUIET_BUTTON}> | |
| 162 | + | <Button type="button" onClick={() => setOpen(true)} variant="outline"> | |
| 165 | 163 | <Hash size={15} /> | |
| 166 | 164 | Create a channel | |
| 167 | − | </button> | |
| 165 | + | </Button> | |
| 168 | 166 | )} | |
| 169 | 167 | <Dialog open={open} onOpenChange={setOpen}> | |
| 170 | 168 | <DialogContent> | |
| ⋯ | |||
| 210 | 208 | className="h-10 rounded-md border border-line bg-bg px-3 text-sm outline-none placeholder:text-faint focus:border-accent-dim" | |
| 211 | 209 | /> | |
| 212 | 210 | </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> | |
| 227 | 227 | </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> | |
| 231 | 231 | {error && <p className="text-sm text-danger">{error}</p>} | |
| 232 | 232 | <DialogFooter> | |
| 233 | − | <button type="button" onClick={() => setOpen(false)} className={QUIET_BUTTON}> | |
| 233 | + | <Button type="button" onClick={() => setOpen(false)} variant="outline"> | |
| 234 | 234 | Cancel | |
| 235 | − | </button> | |
| 236 | − | <button | |
| 235 | + | </Button> | |
| 236 | + | <Button | |
| 237 | 237 | type="submit" | |
| 238 | 238 | 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" | |
| 240 | 240 | > | |
| 241 | 241 | {busy ? "Creating…" : "Create channel"} | |
| 242 | − | </button> | |
| 242 | + | </Button> | |
| 243 | 243 | </DialogFooter> | |
| 244 | 244 | </form> | |
| 245 | 245 | </DialogContent> | |
| ⋯ | |||
| 261 | 261 | <> | |
| 262 | 262 | {variant === "fab" ? ( | |
| 263 | 263 | // A phone's list: the round button above the tab bar. | |
| 264 | − | <button | |
| 264 | + | <Button | |
| 265 | 265 | type="button" | |
| 266 | 266 | aria-label="New message" | |
| 267 | 267 | 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" | |
| 269 | 270 | > | |
| 270 | 271 | <SquarePen size={22} /> | |
| 271 | − | </button> | |
| 272 | + | </Button> | |
| 272 | 273 | ) : variant === "icon" ? ( | |
| 273 | 274 | <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"> | |
| 275 | 276 | <SquarePen size={15} /> | |
| 276 | − | </button> | |
| 277 | + | </Button> | |
| 277 | 278 | </Hint> | |
| 278 | 279 | ) : ( | |
| 279 | − | <button type="button" onClick={() => setOpen(true)} className={QUIET_BUTTON}> | |
| 280 | + | <Button type="button" onClick={() => setOpen(true)} variant="outline"> | |
| 280 | 281 | <SquarePen size={15} /> | |
| 281 | 282 | {children ?? "New message"} | |
| 282 | − | </button> | |
| 283 | + | </Button> | |
| 283 | 284 | )} | |
| 284 | 285 | <Dialog open={open} onOpenChange={setOpen}> | |
| 285 | 286 | <DialogContent className="max-w-md gap-0 p-0 max-sm:pb-[env(safe-area-inset-bottom)]" showClose={false}> | |
| ⋯ | |||
| 350 | 351 | <span key={person.key} className="flex h-6 items-center gap-1.5 rounded-md bg-raised pr-1 pl-1 text-[0.8125rem]"> | |
| 351 | 352 | {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} />} | |
| 352 | 353 | {person.display} | |
| 353 | − | <button | |
| 354 | + | <Button | |
| 354 | 355 | type="button" | |
| 355 | 356 | aria-label={`Remove ${person.display}`} | |
| 356 | 357 | 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" | |
| 358 | 361 | > | |
| 359 | 362 | <X size={12} /> | |
| 360 | − | </button> | |
| 363 | + | </Button> | |
| 361 | 364 | </span> | |
| 362 | 365 | ))} | |
| 363 | 366 | <input | |
| ⋯ | |||
| 424 | 427 | <div className="flex items-center justify-between gap-3 border-t border-line px-5 py-3"> | |
| 425 | 428 | <span className="min-w-0 truncate text-sm text-danger">{error}</span> | |
| 426 | 429 | <div className="flex shrink-0 gap-2"> | |
| 427 | − | <button type="button" onClick={onDone} className={QUIET_BUTTON}> | |
| 430 | + | <Button type="button" onClick={onDone} variant="outline"> | |
| 428 | 431 | Cancel | |
| 429 | − | </button> | |
| 430 | − | <button | |
| 432 | + | </Button> | |
| 433 | + | <Button | |
| 431 | 434 | type="button" | |
| 432 | 435 | disabled={busy || chosen.length === 0} | |
| 433 | 436 | 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" | |
| 435 | 438 | > | |
| 436 | 439 | {busy ? "Opening…" : "Open conversation"} | |
| 437 | − | </button> | |
| 440 | + | </Button> | |
| 438 | 441 | </div> | |
| 439 | 442 | </div> | |
| 440 | 443 | </div> | |
| 40 | 40 | AlertDialogTitle, | |
| 41 | 41 | } from "../ui/alert-dialog"; | |
| 42 | 42 | import { Badge, type BadgeTone } from "../ui/badge"; | |
| 43 | + | import { ButtonLink } from "../ui"; | |
| 44 | + | import { Button, type ButtonVariant } from "../ui/button"; | |
| 43 | 45 | import { InputAddon, InputGroup } from "../ui/input"; | |
| 44 | 46 | import { cn } from "../../lib/cn"; | |
| 45 | 47 | import { type CardChip, actionMode, cardChip, foldsBody, inputValue, moneyInitial, shownActions } from "../../lib/card-actions"; | |
| ⋯ | |||
| 171 | 173 | {toast.ok ? <CircleCheck size={16} /> : <CircleAlert size={16} />} | |
| 172 | 174 | </span> | |
| 173 | 175 | <p className="min-w-0 grow">{toast.message}</p> | |
| 174 | − | <button | |
| 176 | + | <Button | |
| 175 | 177 | type="button" | |
| 176 | 178 | aria-label="Dismiss" | |
| 177 | 179 | 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" | |
| 179 | 182 | > | |
| 180 | 183 | <X size={14} /> | |
| 181 | − | </button> | |
| 184 | + | </Button> | |
| 182 | 185 | </div> | |
| 183 | 186 | ))} | |
| 184 | 187 | </section> | |
| ⋯ | |||
| 186 | 189 | } | |
| 187 | 190 | ||
| 188 | 191 | // ── 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"; | |
| 192 | 192 | ||
| 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" }; | |
| 198 | 195 | ||
| 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") }; | |
| 201 | 198 | } | |
| 202 | 199 | ||
| 203 | 200 | /** Sends a card's action; says what happened in a toast; true when it was done. */ | |
| ⋯ | |||
| 352 | 349 | const target = actionMode(action) === "link" ? cardHref(action.href, slug, code) : null; | |
| 353 | 350 | if (target) { | |
| 354 | 351 | return ( | |
| 355 | − | <Link key={action.id} to={target} className={actionClass(action)}> | |
| 352 | + | <ButtonLink key={action.id} to={target} {...actionProps(action)}> | |
| 356 | 353 | {action.label} | |
| 357 | 354 | <ArrowUpRight size={14} aria-hidden="true" className="-mr-0.5 text-faint" /> | |
| 358 | − | </Link> | |
| 355 | + | </ButtonLink> | |
| 359 | 356 | ); | |
| 360 | 357 | } | |
| 361 | 358 | const working = busy === action.id; | |
| 362 | 359 | const opens = !!action.input; | |
| 363 | 360 | return ( | |
| 364 | − | <button | |
| 361 | + | <Button | |
| 365 | 362 | key={action.id} | |
| 366 | 363 | ref={(element) => { | |
| 367 | 364 | if (element) triggers.current.set(action.id, element); | |
| ⋯ | |||
| 374 | 371 | aria-controls={opens && asking === action.id ? panelId : undefined} | |
| 375 | 372 | aria-haspopup={action.confirm && !opens ? "dialog" : undefined} | |
| 376 | 373 | onClick={() => press(action)} | |
| 377 | − | className={actionClass(action, opens && asking === action.id)} | |
| 374 | + | {...actionProps(action, opens && asking === action.id)} | |
| 378 | 375 | > | |
| 379 | 376 | {working && <LoaderCircle size={14} aria-hidden="true" className="-ml-0.5 animate-spin" />} | |
| 380 | 377 | {action.label} | |
| 381 | − | </button> | |
| 378 | + | </Button> | |
| 382 | 379 | ); | |
| 383 | 380 | })} | |
| 384 | 381 | </div> | |
| ⋯ | |||
| 453 | 450 | )} | |
| 454 | 451 | </div> | |
| 455 | 452 | {(folded || open) && ( | |
| 456 | − | <button | |
| 453 | + | <Button | |
| 457 | 454 | type="button" | |
| 458 | 455 | aria-expanded={open} | |
| 459 | 456 | aria-controls={id} | |
| 460 | 457 | 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" | |
| 462 | 461 | > | |
| 463 | 462 | {open ? "Show less" : "Show more"} | |
| 464 | − | </button> | |
| 463 | + | </Button> | |
| 465 | 464 | )} | |
| 466 | 465 | </div> | |
| 467 | 466 | ); | |
| ⋯ | |||
| 589 | 588 | /> | |
| 590 | 589 | )} | |
| 591 | 590 | <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({})}> | |
| 593 | 592 | 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" })}> | |
| 596 | 595 | {busy && <LoaderCircle size={14} aria-hidden="true" className="-ml-0.5 animate-spin" />} | |
| 597 | 596 | {action.label} | |
| 598 | − | </button> | |
| 597 | + | </Button> | |
| 599 | 598 | </div> | |
| 600 | 599 | </div> | |
| 601 | 600 | {invalid ? ( | |
| 40 | 40 | import { AgentPill, MemberAvatar } from "./marks"; | |
| 41 | 41 | import { PersonStatusEmoji, PersonStatusLine, PresenceSummary, WithPresence } from "../presence"; | |
| 42 | 42 | import { CardContext, MemberCard, type PersonCard, personCard } from "./profile-card"; | |
| 43 | − | import { Avatar } from "../ui"; | |
| 43 | + | ||
| 44 | 44 | import { localTime } from "../../lib/time-zone"; | |
| 45 | 45 | import { BottomSheet, SheetRow, useBack, useSwipeBack } from "../mobile"; | |
| 46 | 46 | import { MessageText, type TextContext } from "./text"; | |
| ⋯ | |||
| 58 | 58 | AlertDialogHeader, | |
| 59 | 59 | AlertDialogTitle, | |
| 60 | 60 | } from "../ui/alert-dialog"; | |
| 61 | + | import { Avatar } from "../ui/avatar"; | |
| 61 | 62 | import { Badge } from "../ui/badge"; | |
| 62 | 63 | import { Hint } from "../ui/hint"; | |
| 63 | 64 | import { Button } from "../ui/button"; | |
| 65 | + | import { Card } from "../ui/card"; | |
| 64 | 66 | import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; | |
| 65 | 67 | import { type Mentionable, type ShownMessage, channelPath, mentionNames, mergeMessages, shownHandle, shownName, timeline } from "../../lib/chat"; | |
| 66 | 68 | import { codeAccessPath } from "../../lib/workspace-nav"; | |
| ⋯ | |||
| 638 | 640 | <div className="mx-auto flex min-h-full max-w-[56rem] flex-col justify-end px-2 pt-6 pb-3 sm:px-4"> | |
| 639 | 641 | {older ? ( | |
| 640 | 642 | <div className="flex justify-center pb-4"> | |
| 641 | − | <button | |
| 643 | + | <Button | |
| 642 | 644 | type="button" | |
| 643 | 645 | 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" | |
| 645 | 649 | > | |
| 646 | 650 | {loadingOlder ? "Loading…" : "Load earlier messages"} | |
| 647 | − | </button> | |
| 651 | + | </Button> | |
| 648 | 652 | </div> | |
| 649 | 653 | ) : ( | |
| 650 | 654 | <ConversationStart data={view} others={others} /> | |
| ⋯ | |||
| 678 | 682 | <div className="mx-auto max-w-[56rem]"> | |
| 679 | 683 | <TypingLine members={typers} /> | |
| 680 | 684 | {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"> | |
| 682 | 686 | <p className="flex items-center gap-2 text-sm text-muted"> | |
| 683 | 687 | <Archive size={15} className="shrink-0 text-faint" /> | |
| 684 | 688 | <span> | |
| ⋯ | |||
| 686 | 690 | </span> | |
| 687 | 691 | </p> | |
| 688 | 692 | {canManage && ( | |
| 689 | − | <button | |
| 693 | + | <Button | |
| 690 | 694 | type="button" | |
| 691 | 695 | 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" | |
| 693 | 698 | > | |
| 694 | 699 | <ArchiveRestore size={15} /> | |
| 695 | 700 | Unarchive | |
| 696 | − | </button> | |
| 701 | + | </Button> | |
| 697 | 702 | )} | |
| 698 | − | </div> | |
| 703 | + | </Card> | |
| 699 | 704 | ) : joined ? ( | |
| 700 | 705 | <Composer draftKey={data.channel.id} placeholder={placeholder} people={people} onSend={(body) => void post(body, null)} onTyping={onTyping} autoFocus /> | |
| 701 | 706 | ) : ( | |
| 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"> | |
| 703 | 708 | <p className="text-sm text-muted"> | |
| 704 | 709 | You're reading <span className="font-medium text-fg">#{name}</span>. Join to post and get its notifications. | |
| 705 | 710 | </p> | |
| 706 | − | <button | |
| 711 | + | <Button | |
| 707 | 712 | type="button" | |
| 708 | 713 | 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" | |
| 710 | 715 | > | |
| 711 | 716 | Join channel | |
| 712 | − | </button> | |
| 713 | − | </div> | |
| 717 | + | </Button> | |
| 718 | + | </Card> | |
| 714 | 719 | )} | |
| 715 | 720 | </div> | |
| 716 | 721 | </div> | |
| ⋯ | |||
| 978 | 983 | return ( | |
| 979 | 984 | <div role="separator" aria-label={label} className="relative my-3 flex items-center justify-center"> | |
| 980 | 985 | <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> | |
| 982 | 987 | {label} | |
| 983 | − | </span> | |
| 988 | + | </Badge> | |
| 984 | 989 | </div> | |
| 985 | 990 | ); | |
| 986 | 991 | } | |
| ⋯ | |||
| 1066 | 1071 | ) : ( | |
| 1067 | 1072 | <ul className="space-y-3"> | |
| 1068 | 1073 | {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> | |
| 1077 | 1084 | ))} | |
| 1078 | 1085 | </ul> | |
| 1079 | 1086 | )} | |
| ⋯ | |||
| 1284 | 1291 | {message.failed && ( | |
| 1285 | 1292 | <p className="mt-0.5 text-xs text-danger"> | |
| 1286 | 1293 | 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"> | |
| 1288 | 1295 | Try again | |
| 1289 | − | </button> | |
| 1296 | + | </Button> | |
| 1290 | 1297 | </p> | |
| 1291 | 1298 | )} | |
| 1292 | 1299 | {!inThread && message.reply_count > 0 && onThread && ( | |
| 1293 | − | <button | |
| 1300 | + | <Button | |
| 1294 | 1301 | type="button" | |
| 1295 | 1302 | 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" | |
| 1297 | 1306 | > | |
| 1298 | 1307 | <MessageSquareText size={14} /> | |
| 1299 | 1308 | {message.reply_count} {message.reply_count === 1 ? "reply" : "replies"} | |
| ⋯ | |||
| 1302 | 1311 | Last reply {clock(message.last_reply_at, zone)} | |
| 1303 | 1312 | </span> | |
| 1304 | 1313 | )} | |
| 1305 | − | </button> | |
| 1314 | + | </Button> | |
| 1306 | 1315 | )} | |
| 1307 | 1316 | </div> | |
| 1308 | 1317 | {!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"> | |
| 1310 | 1319 | <AddReaction messageId={message.id} /> | |
| 1311 | 1320 | <Hint label="Reply in thread"> | |
| 1312 | − | <button | |
| 1321 | + | <Button | |
| 1313 | 1322 | type="button" | |
| 1314 | 1323 | aria-label="Reply in thread" | |
| 1315 | 1324 | 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" | |
| 1317 | 1327 | > | |
| 1318 | 1328 | <MessageSquareText size={15} /> | |
| 1319 | − | </button> | |
| 1329 | + | </Button> | |
| 1320 | 1330 | </Hint> | |
| 1321 | 1331 | {onCopyLink && <MoreActions onCopyLink={onCopyLink} onWriteUp={onWriteUp} onEdit={onEdit} />} | |
| 1322 | − | </div> | |
| 1332 | + | </Card> | |
| 1323 | 1333 | )} | |
| 1324 | 1334 | </article> | |
| 1325 | 1335 | ); | |
| ⋯ | |||
| 1372 | 1382 | return ( | |
| 1373 | 1383 | <DropdownMenu> | |
| 1374 | 1384 | <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> | |
| 1380 | 1389 | </DropdownMenuTrigger> | |
| 1381 | 1390 | </Hint> | |
| 1382 | 1391 | <DropdownMenuContent align="end"> | |
| ⋯ | |||
| 1432 | 1441 | 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)]" | |
| 1433 | 1442 | > | |
| 1434 | 1443 | <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"> | |
| 1436 | 1445 | <ChevronLeft size={22} /> | |
| 1437 | − | </button> | |
| 1446 | + | </Button> | |
| 1438 | 1447 | <h2 className="text-[0.9375rem] font-semibold">{title}</h2> | |
| 1439 | 1448 | {subtitle && <span className="truncate text-sm text-faint">{subtitle}</span>} | |
| 1440 | 1449 | <div className="ml-auto flex shrink-0 items-center gap-1"> | |
| 1441 | 1450 | {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"> | |
| 1443 | 1452 | <X size={16} /> | |
| 1444 | − | </button> | |
| 1453 | + | </Button> | |
| 1445 | 1454 | </div> | |
| 1446 | 1455 | </div> | |
| 1447 | 1456 | <div className="flex min-h-0 grow flex-col">{children}</div> | |
| ⋯ | |||
| 1578 | 1587 | /> | |
| 1579 | 1588 | </span> | |
| 1580 | 1589 | <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"> | |
| 1582 | 1591 | 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"> | |
| 1585 | 1594 | {busy ? "Saving…" : "Save"} | |
| 1586 | − | </button> | |
| 1595 | + | </Button> | |
| 1587 | 1596 | </div> | |
| 1588 | 1597 | </form> | |
| 1589 | 1598 | ); | |
| 1590 | 1599 | const editButton = (field: "name" | "topic", label: string) => ( | |
| 1591 | − | <button | |
| 1600 | + | <Button | |
| 1592 | 1601 | type="button" | |
| 1593 | 1602 | onClick={() => start(field)} | |
| 1594 | 1603 | 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" | |
| 1596 | 1607 | > | |
| 1597 | 1608 | Edit | |
| 1598 | − | </button> | |
| 1609 | + | </Button> | |
| 1599 | 1610 | ); | |
| 1600 | 1611 | return ( | |
| 1601 | 1612 | <> | |
| 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> | |
| 1620 | 1633 | {canManage && !general && ( | |
| 1621 | 1634 | <button | |
| 1622 | 1635 | type="button" | |
| ⋯ | |||
| 1769 | 1782 | return ( | |
| 1770 | 1783 | <Popover open={open} onOpenChange={setOpen}> | |
| 1771 | 1784 | <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"> | |
| 1773 | 1786 | <UserPlus size={13} /> | |
| 1774 | 1787 | Add | |
| 1775 | − | </button> | |
| 1788 | + | </Button> | |
| 1776 | 1789 | </PopoverTrigger> | |
| 1777 | 1790 | <PopoverContent align="end" className="w-72 p-1.5"> | |
| 1778 | 1791 | <input | |
| 23 | 23 | import { type KeyboardEvent as ReactKeyboardEvent, type ReactNode, type RefObject, useEffect, useId, useMemo, useRef, useState } from "react"; | |
| 24 | 24 | ||
| 25 | 25 | import { AgentPill, MemberAvatar } from "./marks"; | |
| 26 | + | import { Button } from "../ui/button"; | |
| 26 | 27 | import { Hint } from "../ui/hint"; | |
| 27 | 28 | import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; | |
| 28 | 29 | // `:shortcode` completion and the emoji picker (components/emoji). | |
| ⋯ | |||
| 622 | 623 | </button> | |
| 623 | 624 | </Hint> | |
| 624 | 625 | <Hint label="Mention someone"> | |
| 625 | − | <button | |
| 626 | + | <Button | |
| 626 | 627 | type="button" | |
| 627 | 628 | aria-label="Mention someone" | |
| 628 | 629 | disabled={disabled || !editor} | |
| ⋯ | |||
| 638 | 639 | .run(); | |
| 639 | 640 | look(editor); | |
| 640 | 641 | }} | |
| 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" | |
| 642 | 645 | > | |
| 643 | 646 | <AtSign size={15} /> | |
| 644 | − | </button> | |
| 647 | + | </Button> | |
| 645 | 648 | </Hint> | |
| 646 | 649 | <EmojiPickerPopover side="top" align="start" onPick={(picked) => editor?.chain().focus().insertContent({ type: "text", text: picked }).run()}> | |
| 647 | 650 | <Hint label="Add an emoji"> | |
| 648 | − | <button | |
| 651 | + | <Button | |
| 649 | 652 | type="button" | |
| 650 | 653 | aria-label="Add an emoji" | |
| 651 | 654 | 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" | |
| 653 | 658 | > | |
| 654 | 659 | <Smile size={15} /> | |
| 655 | − | </button> | |
| 660 | + | </Button> | |
| 656 | 661 | </Hint> | |
| 657 | 662 | </EmojiPickerPopover> | |
| 658 | 663 | {editing ? ( | |
| ⋯ | |||
| 661 | 666 | <kbd className="font-sans">Esc</kbd> to cancel · <kbd className="font-sans">Enter</kbd> to save | |
| 662 | 667 | </span> | |
| 663 | 668 | <span className="ml-auto flex items-center gap-1.5"> | |
| 664 | − | <button | |
| 669 | + | <Button | |
| 665 | 670 | type="button" | |
| 666 | 671 | 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" | |
| 668 | 675 | > | |
| 669 | 676 | Cancel | |
| 670 | − | </button> | |
| 671 | − | <button | |
| 677 | + | </Button> | |
| 678 | + | <Button | |
| 672 | 679 | type="button" | |
| 673 | 680 | disabled={disabled || !editor || state?.empty} | |
| 674 | 681 | 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" | |
| 676 | 685 | > | |
| 677 | 686 | Save | |
| 678 | − | </button> | |
| 687 | + | </Button> | |
| 679 | 688 | </span> | |
| 680 | 689 | </> | |
| 681 | 690 | ) : ( | |
| ⋯ | |||
| 683 | 692 | <span className="ml-1 hidden text-[0.6875rem] text-faint sm:inline"> | |
| 684 | 693 | <kbd className="font-sans">Enter</kbd> to send · <kbd className="font-sans">Shift+Enter</kbd> for a new line | |
| 685 | 694 | </span> | |
| 686 | − | <button | |
| 695 | + | <Button | |
| 687 | 696 | type="button" | |
| 688 | 697 | aria-label="Send" | |
| 689 | 698 | disabled={disabled || !editor || state?.empty !== false} | |
| 690 | 699 | onMouseDown={(event) => event.preventDefault()} | |
| 691 | 700 | 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" | |
| 693 | 704 | > | |
| 694 | 705 | <ArrowUp size={16} strokeWidth={2.4} /> | |
| 695 | − | </button> | |
| 706 | + | </Button> | |
| 696 | 707 | </> | |
| 697 | 708 | )} | |
| 698 | 709 | </div> | |
| ⋯ | |||
| 706 | 717 | return ( | |
| 707 | 718 | <Popover> | |
| 708 | 719 | <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> | |
| 715 | 724 | </PopoverTrigger> | |
| 716 | 725 | </Hint> | |
| 717 | 726 | <PopoverContent side="top" align="end" className="w-80 p-3" onOpenAutoFocus={(event) => event.preventDefault()}> | |
| ⋯ | |||
| 823 | 832 | </div> | |
| 824 | 833 | <div className="flex items-center gap-1.5 pt-0.5"> | |
| 825 | 834 | {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"> | |
| 827 | 836 | <Unlink size={14} /> | |
| 828 | 837 | Remove link | |
| 829 | − | </button> | |
| 838 | + | </Button> | |
| 830 | 839 | )} | |
| 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"> | |
| 832 | 841 | 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"> | |
| 835 | 844 | {initial.existing ? "Save" : "Add link"} | |
| 836 | − | </button> | |
| 845 | + | </Button> | |
| 837 | 846 | </div> | |
| 838 | 847 | </form> | |
| 839 | 848 | ); | |
| 3 | 3 | import { useParams } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import { CreateChannelButton, NewMessageButton } from "./actions"; | |
| 6 | + | import { Card } from "../ui/card"; | |
| 6 | 7 | ||
| 7 | 8 | /** A calm full-height panel for a conversation that cannot be shown. */ | |
| 8 | 9 | function Panel({ icon, title, children }: { icon: ReactNode; title: string; children: ReactNode }) { | |
| 9 | 10 | return ( | |
| 10 | 11 | <div className="flex h-(--page-h) items-center justify-center px-6"> | |
| 11 | 12 | <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> | |
| 13 | 14 | <h1 className="mt-4 text-base font-semibold">{title}</h1> | |
| 14 | 15 | <div className="mt-1.5 text-sm text-muted">{children}</div> | |
| 15 | 16 | </div> |
| 4 | 4 | ||
| 5 | 5 | import { AgentAvatar } from "../agent-avatar"; | |
| 6 | 6 | import { WithPresence } from "../presence"; | |
| 7 | − | import { Avatar } from "../ui"; | |
| 7 | + | ||
| 8 | 8 | import { cn } from "../../lib/cn"; | |
| 9 | + | import { Avatar } from "../ui/avatar"; | |
| 9 | 10 | ||
| 10 | 11 | /** | |
| 11 | 12 | * A workspace agent's face when it has no picture: a sparkle on a lavender |
| 10 | 10 | import { BottomSheet } from "../mobile"; | |
| 11 | 11 | import { PresenceSummary, WithPresence } from "../presence"; | |
| 12 | 12 | 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"; | |
| 14 | 17 | import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; | |
| 15 | 18 | import { Skeleton } from "../ui/skeleton"; | |
| 16 | 19 | import { formatDollars } from "../../lib/agent-form"; | |
| ⋯ | |||
| 77 | 80 | }; | |
| 78 | 81 | } | |
| 79 | 82 | ||
| 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"; | |
| 82 | 83 | ||
| 83 | 84 | function PersonBody({ username, display, avatar, ctx, open, close }: { username: string; display: string; avatar: string | null; ctx: CardContextValue; open: boolean; close: () => void }) { | |
| 84 | 85 | const card = usePersonCard(ctx.slug, username, open); | |
| ⋯ | |||
| 133 | 134 | )} | |
| 134 | 135 | </div> | |
| 135 | 136 | <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"> | |
| 137 | 138 | <MessageSquare size={15} /> | |
| 138 | 139 | {message.busy ? "Opening…" : "Message"} | |
| 139 | − | </button> | |
| 140 | − | <button | |
| 140 | + | </Button> | |
| 141 | + | <Button | |
| 141 | 142 | type="button" | |
| 142 | 143 | onClick={() => { | |
| 143 | 144 | close(); | |
| 144 | 145 | ctx.onViewProfile(username); | |
| 145 | 146 | }} | |
| 146 | − | className={QUIET} | |
| 147 | − | > | |
| 147 | + | variant="outline" className="grow rounded-lg max-md:h-11"> | |
| 148 | 148 | <UserRound size={15} /> | |
| 149 | 149 | View profile | |
| 150 | − | </button> | |
| 150 | + | </Button> | |
| 151 | 151 | </div> | |
| 152 | 152 | </div> | |
| 153 | 153 | ); | |
| ⋯ | |||
| 192 | 192 | )} | |
| 193 | 193 | </div> | |
| 194 | 194 | <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"> | |
| 196 | 196 | <MessageSquare size={15} /> | |
| 197 | 197 | {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"> | |
| 200 | 200 | Profile | |
| 201 | − | </Link> | |
| 201 | + | </ButtonLink> | |
| 202 | 202 | </div> | |
| 203 | 203 | </div> | |
| 204 | 204 | ); | |
| 11 | 11 | import { AgentAvatar } from "../agent-avatar"; | |
| 12 | 12 | import { PersonStatusEmoji } from "../presence"; | |
| 13 | 13 | import { isOrchestrator } from "../orchestrator"; | |
| 14 | + | import { Button } from "../ui/button"; | |
| 14 | 15 | import { Hint } from "../ui/hint"; | |
| 15 | 16 | import { Skeleton } from "../ui/skeleton"; | |
| 16 | 17 | import { type ChatFilter, agentDmOf, channelPath, filterEntries, sections } from "../../lib/chat"; | |
| ⋯ | |||
| 372 | 373 | function PinButton({ pinned, onPin, label }: { pinned: boolean; onPin: () => void; label: string }) { | |
| 373 | 374 | return ( | |
| 374 | 375 | <Hint label={pinned ? "Unpin" : "Pin to the top"}> | |
| 375 | − | <button | |
| 376 | + | <Button | |
| 376 | 377 | type="button" | |
| 377 | 378 | aria-label={`${pinned ? "Unpin" : "Pin"} ${label}`} | |
| 378 | 379 | onClick={(event) => { | |
| ⋯ | |||
| 380 | 381 | event.stopPropagation(); | |
| 381 | 382 | onPin(); | |
| 382 | 383 | }} | |
| 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" | |
| 384 | 386 | > | |
| 385 | 387 | {pinned ? <PinOff size={13} /> : <Pin size={13} />} | |
| 386 | − | </button> | |
| 388 | + | </Button> | |
| 387 | 389 | </Hint> | |
| 388 | 390 | ); | |
| 389 | 391 | } | |
| 3 | 3 | import { Link } from "react-router"; | |
| 4 | 4 | ||
| 5 | 5 | import { MemberCard } from "./profile-card"; | |
| 6 | + | import { Button } from "../ui/button"; | |
| 6 | 7 | import { Hint } from "../ui/hint"; | |
| 7 | 8 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table"; | |
| 8 | 9 | import { type Block, type Span, blocks, onlyEmoji } from "../../lib/chat"; | |
| ⋯ | |||
| 252 | 253 | <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"> | |
| 253 | 254 | {language && <span className="font-mono text-[0.6875rem] text-faint pointer-coarse:hidden">{language}</span>} | |
| 254 | 255 | <Hint label={copied ? "Copied" : "Copy code"}> | |
| 255 | − | <button | |
| 256 | + | <Button | |
| 256 | 257 | type="button" | |
| 257 | 258 | aria-label="Copy code" | |
| 258 | 259 | onClick={() => { | |
| ⋯ | |||
| 261 | 262 | setTimeout(() => setCopied(false), 1500); | |
| 262 | 263 | }); | |
| 263 | 264 | }} | |
| 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" | |
| 265 | 267 | > | |
| 266 | 268 | {copied ? <Check size={12} className="text-success" /> : <Copy size={12} />} | |
| 267 | − | </button> | |
| 269 | + | </Button> | |
| 268 | 270 | </Hint> | |
| 269 | 271 | </div> | |
| 270 | 272 | </div> | |
| 3 | 3 | ||
| 4 | 4 | import type { FoliosSidebar, Result } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | + | import { Button } from "../ui/button"; | |
| 7 | + | import { Card } from "../ui/card"; | |
| 6 | 8 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; | |
| 7 | 9 | import { Field, FieldError, FieldLabel } from "../ui/field"; | |
| 8 | 10 | import { Input } from "../ui/input"; | |
| ⋯ | |||
| 144 | 146 | /> | |
| 145 | 147 | </Field> | |
| 146 | 148 | {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"> | |
| 148 | 150 | <p className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted"> | |
| 149 | 151 | <FileText size={13} aria-hidden="true" /> | |
| 150 | 152 | Posts in this thread, as you | |
| 151 | 153 | </p> | |
| 152 | 154 | <p className="text-[0.8125rem] leading-snug break-words text-fg-soft">{body}</p> | |
| 153 | − | </div> | |
| 155 | + | </Card> | |
| 154 | 156 | )} | |
| 155 | 157 | <FieldError>{error}</FieldError> | |
| 156 | 158 | <DialogFooter> | |
| 157 | − | <button | |
| 159 | + | <Button | |
| 158 | 160 | type="button" | |
| 159 | 161 | 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" | |
| 161 | 164 | > | |
| 162 | 165 | Cancel | |
| 163 | − | </button> | |
| 164 | − | <button | |
| 166 | + | </Button> | |
| 167 | + | <Button | |
| 165 | 168 | type="submit" | |
| 166 | 169 | 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" | |
| 168 | 172 | > | |
| 169 | 173 | {busy && <LoaderCircle size={14} className="animate-spin" aria-hidden="true" />} | |
| 170 | 174 | {busy ? "Asking…" : `Ask @${writer.handle}`} | |
| 171 | − | </button> | |
| 175 | + | </Button> | |
| 172 | 176 | </DialogFooter> | |
| 173 | 177 | </form> | |
| 174 | 178 | </DialogContent> | |
| 4 | 4 | ||
| 5 | 5 | import { type ChecklistItem, dismiss, isDismissed, progress } from "../lib/checklist"; | |
| 6 | 6 | import { cn } from "../lib/cn"; | |
| 7 | + | import { Button } from "./ui/button"; | |
| 7 | 8 | import { Hint } from "./ui/hint"; | |
| 8 | 9 | ||
| 9 | 10 | const storage = () => (typeof window === "undefined" ? null : window.localStorage); | |
| ⋯ | |||
| 50 | 51 | <span className="block h-full rounded-full bg-success" style={{ width: `${(done / total) * 100}%` }} /> | |
| 51 | 52 | </span> | |
| 52 | 53 | <Hint label="Dismiss for this project"> | |
| 53 | − | <button | |
| 54 | + | <Button | |
| 54 | 55 | type="button" | |
| 55 | 56 | onClick={() => { | |
| 56 | 57 | dismiss(storage, base); | |
| 57 | 58 | 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"> | |
| 62 | 60 | <X size={14} /> | |
| 63 | − | </button> | |
| 61 | + | </Button> | |
| 64 | 62 | </Hint> | |
| 65 | 63 | </div> | |
| 66 | 64 | <ol className="grid border-t border-line sm:grid-cols-2 lg:grid-cols-3"> | |
| 16 | 16 | } from "@g1t/contracts"; | |
| 17 | 17 | ||
| 18 | 18 | import { Badge } from "./ui/badge"; | |
| 19 | + | import { Card } from "./ui/card"; | |
| 19 | 20 | import { Skeleton } from "./ui/skeleton"; | |
| 20 | 21 | import { UserCard } from "./user-card"; | |
| 21 | 22 | ||
| ⋯ | |||
| 104 | 105 | export function CodeownersReportPanel({ report, base, branch }: { report: CodeownersReport | null; base: string; branch: string }) { | |
| 105 | 106 | if (!report) { | |
| 106 | 107 | 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> | |
| 110 | 113 | ); | |
| 111 | 114 | } | |
| 112 | 115 | if (!report.path) { | |
| 113 | 116 | 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"> | |
| 115 | 118 | <p className="font-medium">No CODEOWNERS file</p> | |
| 116 | 119 | <p className="mt-1 text-muted"> | |
| 117 | 120 | g1t looks on {branch} for the first of these, and asks the owners it names to review changes to their files: | |
| ⋯ | |||
| 128 | 131 | How to write one | |
| 129 | 132 | </a> | |
| 130 | 133 | </p> | |
| 131 | − | </div> | |
| 134 | + | </Card> | |
| 132 | 135 | ); | |
| 133 | 136 | } | |
| 134 | 137 | const path = report.path; | |
| 135 | 138 | const errors = report.errors.length; | |
| 136 | 139 | return ( | |
| 137 | − | <div className="overflow-hidden rounded-xl border border-line"> | |
| 140 | + | <Card tone="plain" className="overflow-hidden"> | |
| 138 | 141 | <div className="flex flex-wrap items-center gap-x-3 gap-y-1 bg-surface px-4 py-3 text-sm"> | |
| 139 | 142 | {errors > 0 ? ( | |
| 140 | 143 | <FileWarning size={16} className="shrink-0 text-danger" /> | |
| ⋯ | |||
| 158 | 161 | <CodeownersErrorList errors={report.errors} lineHref={(line) => blobHref(base, branch, path, line)} /> | |
| 159 | 162 | </div> | |
| 160 | 163 | )} | |
| 161 | − | </div> | |
| 164 | + | </Card> | |
| 162 | 165 | ); | |
| 163 | 166 | } | |
| 164 | 167 | ||
| 165 | 168 | /** The panel's shape while the file is read. */ | |
| 166 | 169 | export function CodeownersReportSkeleton() { | |
| 167 | 170 | 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"> | |
| 169 | 172 | <Skeleton className="size-4 rounded-full" /> | |
| 170 | 173 | <Skeleton className="h-3 w-36" /> | |
| 171 | 174 | <Skeleton className="h-3 w-20" /> | |
| 172 | 175 | <Skeleton className="ml-auto h-3 w-14" /> | |
| 173 | − | </div> | |
| 176 | + | </Card> | |
| 174 | 177 | ); | |
| 175 | 178 | } | |
| 176 | 179 | ||
| ⋯ | |||
| 316 | 319 | }) { | |
| 317 | 320 | const fileHref = (line: number) => blobHref(base, branch, owners.path, line); | |
| 318 | 321 | 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> | |
| 347 | 352 | ); | |
| 348 | 353 | } | |
| 9 | 9 | import { searchHref } from "../lib/search"; | |
| 10 | 10 | import { hitIcon } from "./search"; | |
| 11 | 11 | 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"; | |
| 13 | 13 | import { SkeletonRows } from "./ui/skeleton"; | |
| 14 | 14 | ||
| 15 | 15 | /** How long typing has to pause before results are asked for. */ |
| 3 | 3 | ||
| 4 | 4 | import { type CommitPerson, type ShownCommit, profileHref, shownName } from "../lib/commit-people"; | |
| 5 | 5 | import { cn } from "../lib/cn"; | |
| 6 | − | import { Avatar } from "./ui"; | |
| 6 | + | ||
| 7 | 7 | import { UserCard } from "./user-card"; | |
| 8 | + | import { Avatar } from "./ui/avatar"; | |
| 8 | 9 | ||
| 9 | 10 | /** | |
| 10 | 11 | * A person on a commit, as an avatar: theirs and their card when the |
| 41 | 41 | import { Badge } from "./ui/badge"; | |
| 42 | 42 | import { Hint } from "./ui/hint"; | |
| 43 | 43 | import { Input } from "./ui"; | |
| 44 | + | import { Card } from "./ui/card"; | |
| 44 | 45 | ||
| 45 | 46 | const CATEGORY_ICONS: Record<CategoryFilter, ReactNode> = { | |
| 46 | 47 | all: <LayoutGrid size={15} />, | |
| ⋯ | |||
| 190 | 191 | </div> | |
| 191 | 192 | ||
| 192 | 193 | {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> | |
| 203 | 206 | )} | |
| 204 | 207 | ||
| 205 | 208 | {found.connected.length > 0 && ( | |
| ⋯ | |||
| 360 | 363 | /** A short line saying who a page's connections are for, and where the other kind lives. */ | |
| 361 | 364 | export function ScopeNote({ scope, workspace }: { scope: ConnectorScope; workspace: string | null }) { | |
| 362 | 365 | 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> | |
| 380 | 385 | ); | |
| 381 | 386 | } | |
| 46 | 46 | ||
| 47 | 47 | import { InlineMarkdown } from "./inline-markdown"; | |
| 48 | 48 | import { SubmitButton, TimeAgo } from "./ui"; | |
| 49 | + | import { Button } from "./ui/button"; | |
| 50 | + | import { Card } from "./ui/card"; | |
| 49 | 51 | import { Hint } from "./ui/hint"; | |
| 50 | 52 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; | |
| 51 | 53 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| ⋯ | |||
| 196 | 198 | {fetcher.data?.error && <p className="mt-1.5 text-xs text-danger">{fetcher.data.error}</p>} | |
| 197 | 199 | </div> | |
| 198 | 200 | <div className="flex shrink-0 items-start gap-1.5"> | |
| 199 | − | <button | |
| 201 | + | <Button | |
| 200 | 202 | type="button" | |
| 201 | 203 | 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" | |
| 203 | 206 | > | |
| 204 | 207 | <Check size={12} /> | |
| 205 | 208 | Keep | |
| 206 | − | </button> | |
| 209 | + | </Button> | |
| 207 | 210 | <EditAndKeep memory={memory} action={action} /> | |
| 208 | 211 | <Hint label="Dismiss: never suggested again in these words"> | |
| 209 | − | <button | |
| 212 | + | <Button | |
| 210 | 213 | type="button" | |
| 211 | 214 | aria-label="Dismiss" | |
| 212 | 215 | 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" | |
| 214 | 219 | > | |
| 215 | 220 | <X size={12} /> | |
| 216 | 221 | Dismiss | |
| 217 | − | </button> | |
| 222 | + | </Button> | |
| 218 | 223 | </Hint> | |
| 219 | 224 | </div> | |
| 220 | 225 | </li> | |
| ⋯ | |||
| 227 | 232 | return <p className="rounded-xl border border-dashed border-line px-4 py-6 text-sm text-muted">{empty}</p>; | |
| 228 | 233 | } | |
| 229 | 234 | 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> | |
| 235 | 242 | ); | |
| 236 | 243 | } | |
| 237 | 244 | ||
| ⋯ | |||
| 269 | 276 | ); | |
| 270 | 277 | const edges = catalog.relations.filter((relation) => relation.kind === "depends_on" && at.has(relation.from) && at.has(relation.to)); | |
| 271 | 278 | 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> | |
| 313 | 322 | ); | |
| 314 | 323 | } | |
| 315 | 324 | ||
| ⋯ | |||
| 382 | 391 | )} | |
| 383 | 392 | {!kind && !project && <RelationsGraph catalog={catalog} />} | |
| 384 | 393 | {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> | |
| 389 | 400 | ) : ( | |
| 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> | |
| 395 | 408 | )} | |
| 396 | 409 | {catalog.builtAt && ( | |
| 397 | 410 | <p className="text-xs text-faint"> | |
| ⋯ | |||
| 457 | 470 | 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" | |
| 458 | 471 | /> | |
| 459 | 472 | </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"> | |
| 461 | 474 | {searching ? "Searching…" : "Search"} | |
| 462 | − | </button> | |
| 475 | + | </Button> | |
| 463 | 476 | </Form> | |
| 464 | 477 | {result && | |
| 465 | 478 | (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> | |
| 467 | 482 | ) : ( | |
| 468 | 483 | <> | |
| 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> | |
| 474 | 491 | <p className="text-xs text-faint"> | |
| 475 | 492 | {result.mode === "semantic" ? "Ranked by meaning, then by matching words." : "Matched by words: the search index did not answer."} | |
| 476 | 493 | </p> | |
| ⋯ | |||
| 504 | 521 | <SubmitButton | |
| 505 | 522 | fetcher={fetcher} | |
| 506 | 523 | 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" | |
| 508 | 527 | > | |
| 509 | 528 | <Bot size={12} /> | |
| 510 | 529 | Fix with an agent | |
| ⋯ | |||
| 522 | 541 | return ( | |
| 523 | 542 | <div className="grid gap-3 md:grid-cols-2"> | |
| 524 | 543 | {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> | |
| 540 | 561 | ))} | |
| 541 | 562 | </div> | |
| 542 | 563 | ); | |
| 12 | 12 | import { type PointerEvent, useEffect, useMemo, useRef, useState } from "react"; | |
| 13 | 13 | ||
| 14 | 14 | import { type Level, buildCalendar, dayLabel, totalLabel } from "../lib/contribution-calendar"; | |
| 15 | + | import { Card } from "./ui/card"; | |
| 15 | 16 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| 16 | 17 | ||
| 17 | 18 | /** A square's side and the space between squares, in pixels. */ | |
| ⋯ | |||
| 65 | 66 | ||
| 66 | 67 | const heading = totalLabel(total); | |
| 67 | 68 | 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} | |
| 92 | 79 | </span> | |
| 93 | 80 | ))} | |
| 94 | 81 | </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> | |
| 114 | 128 | </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> | |
| 126 | 129 | </div> | |
| 127 | 130 | </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> | |
| 137 | 140 | ); | |
| 138 | 141 | } | |
| 4 | 4 | import { ACCOUNT_RESTORE_DAYS, type AccountDeletion } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import { DangerAction } from "./danger-zone"; | |
| 7 | − | import { Button, ErrorText } from "./ui"; | |
| 7 | + | import { ErrorText } from "./ui"; | |
| 8 | 8 | import { | |
| 9 | 9 | AlertDialog, | |
| 10 | 10 | AlertDialogCancel, | |
| ⋯ | |||
| 14 | 14 | AlertDialogHeader, | |
| 15 | 15 | AlertDialogTitle, | |
| 16 | 16 | } from "./ui/alert-dialog"; | |
| 17 | + | import { Button } from "./ui/button"; | |
| 18 | + | import { Card } from "./ui/card"; | |
| 17 | 19 | import { FieldDescription, FieldLabel, Field as FormField } from "./ui/field"; | |
| 18 | 20 | import { Input as TextInput } from "./ui/input"; | |
| 19 | 21 | import { accountDeletionRefusal, confirmsUsername, whatAccountDeletionTakes } from "../lib/account-deletion"; | |
| ⋯ | |||
| 56 | 58 | <DangerAction | |
| 57 | 59 | title="Delete your account" | |
| 58 | 60 | 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)}> | |
| 60 | 62 | Delete account | |
| 61 | 63 | </Button> | |
| 62 | 64 | } | |
| ⋯ | |||
| 66 | 68 | <span role="status">{refusal}</span> | |
| 67 | 69 | <ul className="mt-3 space-y-2"> | |
| 68 | 70 | {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 | |
| 78 | 85 | </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> | |
| 86 | 90 | ))} | |
| 87 | 91 | </ul> | |
| 88 | 92 | </> | |
| ⋯ | |||
| 165 | 169 | <ErrorText>{error}</ErrorText> | |
| 166 | 170 | <AlertDialogFooter> | |
| 167 | 171 | <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}> | |
| 169 | 173 | {deleting ? "Deleting…" : "Delete account"} | |
| 170 | 174 | </Button> | |
| 171 | 175 | </AlertDialogFooter> | |
| 39 | 39 | } from "../lib/dependency-updates"; | |
| 40 | 40 | import { TimeAgo } from "./ui"; | |
| 41 | 41 | import { Badge, type BadgeTone } from "./ui/badge"; | |
| 42 | + | import { Button } from "./ui/button"; | |
| 43 | + | import { Card } from "./ui/card"; | |
| 42 | 44 | import { Hint } from "./ui/hint"; | |
| 43 | 45 | ||
| 44 | 46 | type Done = { ok: boolean; error?: string } | undefined; | |
| ⋯ | |||
| 94 | 96 | { label: "Reviewers", values: entry.reviewers }, | |
| 95 | 97 | ].filter((rule) => rule.values.length > 0); | |
| 96 | 98 | 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 | + | )} | |
| 111 | 129 | </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 | + | )} | |
| 129 | 149 | </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" /> | |
| 157 | 154 | <span className="wrap-anywhere"> | |
| 158 | − | <Said text={entry.lastResult} /> | |
| 155 | + | <Said text={entry.lastError} /> | |
| 159 | 156 | </span> | |
| 160 | 157 | </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} /> | |
| 180 | 164 | </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> | |
| 186 | 192 | ); | |
| 187 | 193 | } | |
| 188 | 194 | ||
| ⋯ | |||
| 246 | 252 | const pulls = livePulls(state.pulls); | |
| 247 | 253 | const fileLink = state.path && state.commit ? `${base}/blob/${state.commit}/${state.path}` : null; | |
| 248 | 254 | return ( | |
| 249 | − | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 255 | + | <Card className="p-4"> | |
| 250 | 256 | {titled && ( | |
| 251 | 257 | <> | |
| 252 | 258 | <div className="flex flex-wrap items-center justify-between gap-2"> | |
| ⋯ | |||
| 416 | 422 | </p> | |
| 417 | 423 | </section> | |
| 418 | 424 | )} | |
| 419 | − | </div> | |
| 425 | + | </Card> | |
| 420 | 426 | ); | |
| 421 | 427 | } | |
| 22 | 22 | HighlightedLine, | |
| 23 | 23 | } from "../lib/diff"; | |
| 24 | 24 | 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"; | |
| 26 | 29 | import { Checkbox } from "./ui/checkbox"; | |
| 27 | 30 | import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "./ui/sheet"; | |
| 28 | 31 | ||
| ⋯ | |||
| 113 | 116 | /** One comment made on a line, shown under that line. */ | |
| 114 | 117 | function LineComment({ comment }: { comment: Comment }) { | |
| 115 | 118 | return ( | |
| 116 | − | <div className="rounded-lg border border-line bg-bg font-sans"> | |
| 119 | + | <Card tone="bg" radius="lg" className="font-sans"> | |
| 117 | 120 | <p className="flex items-center gap-2 border-b border-line px-3 py-1.5 text-xs text-muted"> | |
| 118 | 121 | <Avatar name={comment.author.username} size={16} /> | |
| 119 | 122 | <span className="font-medium text-fg">{comment.author.username}</span> | |
| ⋯ | |||
| 122 | 125 | <div className="px-3 py-2 text-sm"> | |
| 123 | 126 | <Markdown source={comment.body} /> | |
| 124 | 127 | </div> | |
| 125 | − | </div> | |
| 128 | + | </Card> | |
| 126 | 129 | ); | |
| 127 | 130 | } | |
| 128 | 131 | ||
| ⋯ | |||
| 171 | 174 | <SubmitButton match={{ action: "comment", path, line: String(line) }} pending="Commenting…"> | |
| 172 | 175 | Comment | |
| 173 | 176 | </SubmitButton> | |
| 174 | − | <Button variant="quiet" type="button" onClick={onClose}> | |
| 177 | + | <Button variant="outline" type="button" onClick={onClose}> | |
| 175 | 178 | Cancel | |
| 176 | 179 | </Button> | |
| 177 | 180 | </div> | |
| ⋯ | |||
| 183 | 186 | ||
| 184 | 187 | function CommentButton({ line, onClick }: { line: number; onClick: (event: React.MouseEvent) => void }) { | |
| 185 | 188 | return ( | |
| 186 | − | <button | |
| 189 | + | <Button | |
| 187 | 190 | type="button" | |
| 188 | 191 | aria-label={`Comment on line ${line}`} | |
| 189 | 192 | 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" | |
| 191 | 195 | > | |
| 192 | 196 | <MessageSquarePlus size={12} /> | |
| 193 | − | </button> | |
| 197 | + | </Button> | |
| 194 | 198 | ); | |
| 195 | 199 | } | |
| 196 | 200 | ||
| ⋯ | |||
| 424 | 428 | }, [file, collapsed]); | |
| 425 | 429 | const props = { file: highlighted ?? file, comments, canComment: review?.canComment ?? false }; | |
| 426 | 430 | 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 | + | ||
| 439 | 438 | > | |
| 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> | |
| 501 | 470 | )} | |
| 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> | |
| 510 | 484 | )} | |
| 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> | |
| 514 | 521 | ); | |
| 515 | 522 | } | |
| 516 | 523 | ||
| ⋯ | |||
| 784 | 791 | <div className="ml-auto flex items-center gap-2"> | |
| 785 | 792 | {files.length > 1 && ( | |
| 786 | 793 | // Below xl the tree is not beside the diffs: it opens as a sheet. | |
| 787 | − | <button | |
| 794 | + | <Button | |
| 788 | 795 | type="button" | |
| 789 | 796 | 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" | |
| 791 | 800 | > | |
| 792 | 801 | <Files size={13} /> | |
| 793 | 802 | Files | |
| 794 | − | </button> | |
| 803 | + | </Button> | |
| 795 | 804 | )} | |
| 796 | − | <button | |
| 805 | + | <Button | |
| 797 | 806 | type="button" | |
| 798 | 807 | 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" | |
| 800 | 811 | > | |
| 801 | 812 | {allCollapsed ? "Expand all" : "Collapse all"} | |
| 802 | − | </button> | |
| 813 | + | </Button> | |
| 803 | 814 | <div className="hidden rounded-md border border-line p-0.5 sm:flex" role="radiogroup" aria-label="Layout"> | |
| 804 | 815 | {( | |
| 805 | 816 | [ | |
| 6 | 6 | ||
| 7 | 7 | import { AppsLauncher, appIcon, pinnedApp, useAppPins } from "./apps"; | |
| 8 | 8 | import { Mark } from "./logo"; | |
| 9 | − | import { Avatar } from "./ui"; | |
| 9 | + | ||
| 10 | + | import { Avatar } from "./ui/avatar"; | |
| 11 | + | import { Card } from "./ui/card"; | |
| 10 | 12 | import { Hint } from "./ui/hint"; | |
| 11 | 13 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog"; | |
| 12 | 14 | import { | |
| ⋯ | |||
| 208 | 210 | <DialogTitle>Keyboard shortcuts</DialogTitle> | |
| 209 | 211 | <DialogDescription>Everywhere in g1t, and in Chat.</DialogDescription> | |
| 210 | 212 | </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> | |
| 221 | 225 | </DialogContent> | |
| 222 | 226 | </Dialog> | |
| 223 | 227 | ); | |
| 11 | 11 | import type { AgentEffort, AgentEffortCosts, AgentRecommendation, AgentRecommendations } from "@g1t/contracts"; | |
| 12 | 12 | ||
| 13 | 13 | import { AgentAvatar } from "./agent-avatar"; | |
| 14 | − | import { BUTTONS } from "./agents/dialogs"; | |
| 15 | 14 | import { Hint } from "./ui/hint"; | |
| 16 | 15 | import { TimeAgo } from "./ui"; | |
| 16 | + | import { Button } from "./ui/button"; | |
| 17 | + | import { Card } from "./ui/card"; | |
| 17 | 18 | import { cn } from "../lib/cn"; | |
| 18 | 19 | import { EFFORT_OPTIONS, acceptedLabel, costAt, costLine, effortLabel, savingLabel, totalSaving } from "../lib/effort"; | |
| 19 | 20 | import { money } from "../lib/usage"; | |
| ⋯ | |||
| 39 | 40 | const [chosen, setChosen] = useState<AgentEffort>(value); | |
| 40 | 41 | return ( | |
| 41 | 42 | <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]"> | |
| 43 | 44 | {EFFORT_OPTIONS.map((option) => { | |
| 44 | 45 | const on = chosen === option.key; | |
| 45 | 46 | return ( | |
| ⋯ | |||
| 68 | 69 | </Hint> | |
| 69 | 70 | ); | |
| 70 | 71 | })} | |
| 71 | − | </div> | |
| 72 | + | </Card> | |
| 72 | 73 | <p className="mt-2 text-xs text-muted">{EFFORT_OPTIONS.find((o) => o.key === chosen)?.about}</p> | |
| 73 | 74 | {costs !== undefined && <p className="mt-0.5 text-xs text-faint tabular-nums">{costLine(costs, chosen)}</p>} | |
| 74 | 75 | </div> | |
| ⋯ | |||
| 143 | 144 | <EffortPicker value={value} costs={costs} disabled={!owner} onChange={setPicked} /> | |
| 144 | 145 | {owner && ( | |
| 145 | 146 | <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"> | |
| 147 | 148 | {busy ? "Saving…" : "Save effort"} | |
| 148 | − | </button> | |
| 149 | + | </Button> | |
| 149 | 150 | {result?.ok && picked === value && ( | |
| 150 | 151 | <span role="status" className="inline-flex items-center gap-1 text-xs text-success"> | |
| 151 | 152 | <Check size={13} aria-hidden="true" /> Saved as a new version | |
| ⋯ | |||
| 266 | 267 | <fetcher.Form method="post" action={action} className="col-span-2 flex items-center gap-2 sm:col-span-1 sm:self-center"> | |
| 267 | 268 | <input type="hidden" name="intent" value="recommendation" /> | |
| 268 | 269 | <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"> | |
| 270 | 271 | {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"> | |
| 273 | 274 | {doing === "dismiss" ? "Dismissing…" : "Dismiss"} | |
| 274 | − | </button> | |
| 275 | + | </Button> | |
| 275 | 276 | </fetcher.Form> | |
| 276 | 277 | ) : ( | |
| 277 | 278 | <p className="col-span-2 text-xs text-faint sm:col-span-1 sm:self-center">An owner can apply it</p> | |
| 5 | 5 | ||
| 6 | 6 | import { addressActions, backupChoices } from "../lib/emails"; | |
| 7 | 7 | 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"; | |
| 9 | 10 | import { SelectField } from "./ui/select"; | |
| 10 | 11 | import { SwitchCard } from "./ui/switch"; | |
| 11 | 12 | ||
| ⋯ | |||
| 15 | 16 | <Form method="post"> | |
| 16 | 17 | <input type="hidden" name="intent" value={intent} /> | |
| 17 | 18 | <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 }}> | |
| 19 | 20 | {label} | |
| 20 | 21 | </SubmitButton> | |
| 21 | 22 | </Form> | |
| ⋯ | |||
| 134 | 135 | <div className="min-w-0"> | |
| 135 | 136 | <p className="truncate text-sm">{email.email}</p> | |
| 136 | 137 | <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>} | |
| 139 | 140 | {email.verified ? ( | |
| 140 | 141 | <span> | |
| 141 | 142 | Confirmed {email.verifiedAt ? <TimeAgo at={email.verifiedAt} /> : null} | |
| ⋯ | |||
| 192 | 193 | options={[{ value: "", label: "Primary address only" }, ...backups.map((email) => ({ value: email.email, label: email.email }))]} | |
| 193 | 194 | /> | |
| 194 | 195 | </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}> | |
| 196 | 197 | Save | |
| 197 | 198 | </SubmitButton> | |
| 198 | 199 | </Form> | |
| ⋯ | |||
| 218 | 219 | <p className="text-xs text-faint"> | |
| 219 | 220 | Commits g1t makes for you now carry <span className="font-mono [overflow-wrap:anywhere]">{data.commitEmail}</span>. | |
| 220 | 221 | </p> | |
| 221 | − | <SubmitButton variant="quiet" pending="Saving…" match={{ intent: "email-privacy" }}> | |
| 222 | + | <SubmitButton variant="outline" pending="Saving…" match={{ intent: "email-privacy" }}> | |
| 222 | 223 | Save | |
| 223 | 224 | </SubmitButton> | |
| 224 | 225 | </div> | |
| 5 | 5 | import { useEmojiContext } from "./context"; | |
| 6 | 6 | import { readRecent, readTone, rememberRecent, rememberTone, useEmojiData } from "./data"; | |
| 7 | 7 | import { EmojiGlyph } from "./render"; | |
| 8 | + | import { Button } from "../ui/button"; | |
| 8 | 9 | import { Hint } from "../ui/hint"; | |
| 9 | 10 | import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; | |
| 10 | 11 | import { type EmojiPick, SKIN_TONES, customPick, searchEmoji, skinned, unicodePick } from "../../lib/emoji"; | |
| ⋯ | |||
| 121 | 122 | </label> | |
| 122 | 123 | <div className="relative"> | |
| 123 | 124 | <Hint label="Skin tone"> | |
| 124 | − | <button | |
| 125 | + | <Button | |
| 125 | 126 | type="button" | |
| 126 | 127 | aria-label="Skin tone" | |
| 127 | 128 | aria-expanded={toning} | |
| 128 | 129 | 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" | |
| 130 | 133 | > | |
| 131 | 134 | {SKIN_TONES[tone]} | |
| 132 | − | </button> | |
| 135 | + | </Button> | |
| 133 | 136 | </Hint> | |
| 134 | 137 | {toning && ( | |
| 135 | 138 | <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"> | |
| ⋯ | |||
| 158 | 161 | <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]"> | |
| 159 | 162 | {sections.map((section) => ( | |
| 160 | 163 | <Hint key={section.key} label={section.title}> | |
| 161 | − | <button | |
| 164 | + | <Button | |
| 162 | 165 | type="button" | |
| 163 | 166 | aria-label={section.title} | |
| 164 | 167 | 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" | |
| 166 | 171 | > | |
| 167 | 172 | {section.icon} | |
| 168 | − | </button> | |
| 173 | + | </Button> | |
| 169 | 174 | </Hint> | |
| 170 | 175 | ))} | |
| 171 | 176 | </nav> | |
| 5 | 5 | import { useEmojiContext } from "./context"; | |
| 6 | 6 | import { EmojiPickerPopover } from "./picker"; | |
| 7 | 7 | import { EmojiGlyph } from "./render"; | |
| 8 | + | import { Button } from "../ui/button"; | |
| 8 | 9 | import { Hint } from "../ui/hint"; | |
| 9 | 10 | import { reactorsLine } from "../../lib/emoji"; | |
| 10 | 11 | ||
| ⋯ | |||
| 66 | 67 | if (!react) return null; | |
| 67 | 68 | const button = | |
| 68 | 69 | variant === "pill" ? ( | |
| 69 | − | <button | |
| 70 | + | <Button | |
| 70 | 71 | type="button" | |
| 71 | 72 | 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" | |
| 73 | 75 | > | |
| 74 | 76 | <SmilePlus size={14} /> | |
| 75 | − | </button> | |
| 77 | + | </Button> | |
| 76 | 78 | ) : ( | |
| 77 | − | <button | |
| 79 | + | <Button | |
| 78 | 80 | type="button" | |
| 79 | 81 | 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" | |
| 81 | 84 | > | |
| 82 | 85 | <SmilePlus size={15} /> | |
| 83 | − | </button> | |
| 86 | + | </Button> | |
| 84 | 87 | ); | |
| 85 | 88 | return ( | |
| 86 | 89 | <EmojiPickerPopover onPick={(emoji) => react(messageId, emoji, true)} side={variant === "pill" ? "top" : "bottom"} align={variant === "pill" ? "start" : "end"}> | |
| ⋯ | |||
| 106 | 109 | return ( | |
| 107 | 110 | <div className="flex items-center justify-between gap-1 px-2 pb-2" aria-label="React"> | |
| 108 | 111 | {QUICK.map((emoji) => ( | |
| 109 | − | <button | |
| 112 | + | <Button | |
| 110 | 113 | key={emoji} | |
| 111 | 114 | type="button" | |
| 112 | 115 | aria-label={`React with ${emoji}`} | |
| 113 | 116 | 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" | |
| 115 | 120 | > | |
| 116 | 121 | {emoji} | |
| 117 | − | </button> | |
| 122 | + | </Button> | |
| 118 | 123 | ))} | |
| 119 | 124 | <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"> | |
| 121 | 126 | <SmilePlus size={20} /> | |
| 122 | − | </button> | |
| 127 | + | </Button> | |
| 123 | 128 | </EmojiPickerPopover> | |
| 124 | 129 | </div> | |
| 125 | 130 | ); | |
| 8 | 8 | import { loadEmojiData } from "./data"; | |
| 9 | 9 | import { CustomEmojiImage } from "./render"; | |
| 10 | 10 | 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"; | |
| 12 | 14 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; | |
| 13 | 15 | import { Hint } from "../ui/hint"; | |
| 14 | 16 | import { SelectField } from "../ui/select"; | |
| ⋯ | |||
| 61 | 63 | {list.can_upload ? ( | |
| 62 | 64 | <div className="flex gap-2"> | |
| 63 | 65 | {list.emoji.length > 0 && ( | |
| 64 | − | <Button variant="quiet" onClick={() => setAliasing(true)}> | |
| 66 | + | <Button variant="outline" onClick={() => setAliasing(true)}> | |
| 65 | 67 | Add an alias | |
| 66 | 68 | </Button> | |
| 67 | 69 | )} | |
| ⋯ | |||
| 89 | 91 | {query ? "Matching" : "All emoji"} <span className="text-faint">{shown.length}</span> | |
| 90 | 92 | </h2> | |
| 91 | 93 | {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> | |
| 93 | 97 | ) : ( | |
| 94 | 98 | <ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3"> | |
| 95 | 99 | {shown.map((emoji) => ( | |
| ⋯ | |||
| 116 | 120 | if (fetcher.data?.ok) forgetCustomEmoji(slug); | |
| 117 | 121 | }, [fetcher.data, slug]); | |
| 118 | 122 | 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> | |
| 130 | 162 | ) : ( | |
| 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> | |
| 169 | 178 | ); | |
| 170 | 179 | } | |
| 171 | 180 | ||
| ⋯ | |||
| 173 | 182 | /** Who may add emoji: one of the workspace's chat settings, changed in Settings, Chat. */ | |
| 174 | 183 | function UploadSetting({ slug, value }: { slug: string; value: EmojiUpload }) { | |
| 175 | 184 | 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'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'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> | |
| 190 | 201 | ); | |
| 191 | 202 | } | |
| 192 | 203 | ||
| ⋯ | |||
| 329 | 340 | </label> | |
| 330 | 341 | ||
| 331 | 342 | {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"> | |
| 333 | 344 | <p className="mb-2 text-[0.6875rem] font-semibold tracking-wide text-faint uppercase">Preview</p> | |
| 334 | 345 | <p className="text-[0.9375rem] text-fg-soft"> | |
| 335 | 346 | Shipping it today <img src={preview} alt={`:${clean || "name"}:`} className="inline-block size-[1.375em] object-contain align-[-0.3em]" /> | |
| ⋯ | |||
| 341 | 352 | <img src={preview} alt="" className="size-9 object-contain" /> | |
| 342 | 353 | <span className="ml-auto font-mono text-xs text-faint">:{clean || "name"}:</span> | |
| 343 | 354 | </div> | |
| 344 | − | </div> | |
| 355 | + | </Card> | |
| 345 | 356 | )} | |
| 346 | 357 | {fetcher.data && !fetcher.data.ok && fetcher.data.intent === "add" && <ErrorText>{fetcher.data.error}</ErrorText>} | |
| 347 | 358 | ||
| 348 | 359 | <DialogFooter> | |
| 349 | − | <Button variant="quiet" onClick={() => onOpenChange(false)}> | |
| 360 | + | <Button variant="outline" onClick={() => onOpenChange(false)}> | |
| 350 | 361 | Cancel | |
| 351 | 362 | </Button> | |
| 352 | 363 | <Button variant="accent" onClick={submit} disabled={!ready}> | |
| ⋯ | |||
| 423 | 434 | </label> | |
| 424 | 435 | {fetcher.data && !fetcher.data.ok && fetcher.data.intent === "alias" && <ErrorText>{fetcher.data.error}</ErrorText>} | |
| 425 | 436 | <DialogFooter> | |
| 426 | − | <Button variant="quiet" onClick={() => onOpenChange(false)}> | |
| 437 | + | <Button variant="outline" onClick={() => onOpenChange(false)}> | |
| 427 | 438 | Cancel | |
| 428 | 439 | </Button> | |
| 429 | 440 | <Button | |
| 7 | 7 | import { useEffect, useMemo, useState } from "react"; | |
| 8 | 8 | ||
| 9 | 9 | import { buildTree, canDo, flatten } from "../../lib/folios"; | |
| 10 | − | import { Button, ErrorText } from "../ui"; | |
| 10 | + | import { ErrorText } from "../ui"; | |
| 11 | + | import { Button } from "../ui/button"; | |
| 11 | 12 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog"; | |
| 12 | 13 | import { SelectField } from "../ui/select"; | |
| 13 | 14 |
| 17 | 17 | ||
| 18 | 18 | import { citationHref } from "../../../lib/folios"; | |
| 19 | 19 | import { useDrawnTheme } from "../../../lib/theme"; | |
| 20 | + | import { Button } from "../../ui/button"; | |
| 21 | + | import { Card } from "../../ui/card"; | |
| 20 | 22 | ||
| 21 | 23 | /** What an embed shows, from the site (`-/artifacts/api?embed=`). */ | |
| 22 | 24 | export type EmbedCard = { kind: "issue" | "pull" | "channel" | "project" | "page" | "link"; title: string; subtitle: string | null; state: string | null; href: string }; | |
| ⋯ | |||
| 118 | 120 | render: ({ block, editor }) => { | |
| 119 | 121 | const [editing, setEditing] = useState(false); | |
| 120 | 122 | 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}> | |
| 122 | 124 | <div className="mb-2 flex items-center justify-between text-[0.6875rem] font-medium tracking-wide text-faint uppercase"> | |
| 123 | 125 | <span>Diagram</span> | |
| 124 | 126 | {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"> | |
| 126 | 128 | {editing ? "Done" : "Edit source"} | |
| 127 | − | </button> | |
| 129 | + | </Button> | |
| 128 | 130 | )} | |
| 129 | 131 | </div> | |
| 130 | 132 | {editing && <SourceEditor editable value={block.props.code} placeholder="flowchart LR A --> B" onChange={(code) => editor.updateBlock(block, { props: { code } })} />} | |
| 131 | 133 | <div className={editing ? "mt-3" : ""}> | |
| 132 | 134 | <MermaidView code={block.props.code} /> | |
| 133 | 135 | </div> | |
| 134 | − | </div> | |
| 136 | + | </Card> | |
| 135 | 137 | ); | |
| 136 | 138 | }, | |
| 137 | 139 | }, | |
| ⋯ | |||
| 242 | 244 | placeholder="Paste the address of an issue, pull request, channel, project or artifact" | |
| 243 | 245 | className="min-w-0 grow bg-transparent px-1.5 text-sm text-fg outline-none placeholder:text-faint" | |
| 244 | 246 | /> | |
| 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"> | |
| 246 | 248 | Embed | |
| 247 | − | </button> | |
| 249 | + | </Button> | |
| 248 | 250 | </form> | |
| 249 | 251 | ); | |
| 250 | 252 | } | |
| 15 | 15 | import { canDo, readingTime } from "../../../lib/folios"; | |
| 16 | 16 | import { Markdown } from "../../markdown"; | |
| 17 | 17 | import { TimeAgo } from "../../ui"; | |
| 18 | + | import { Button } from "../../ui/button"; | |
| 18 | 19 | import { foliosRequest, useFoliosData } from "../actions"; | |
| 19 | 20 | import { FolioGlyph, type FolioBodyProps } from "../kinds"; | |
| 20 | 21 | import { Face } from "../parts"; | |
| ⋯ | |||
| 156 | 157 | <div className="group"> | |
| 157 | 158 | {folio.icon && ( | |
| 158 | 159 | <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"> | |
| 160 | 161 | {folio.icon} | |
| 161 | − | </button> | |
| 162 | + | </Button> | |
| 162 | 163 | </IconPicker> | |
| 163 | 164 | )} | |
| 164 | 165 | {editable && !trashed && ( | |
| 165 | 166 | <div className="mb-1 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100"> | |
| 166 | 167 | {!folio.icon && ( | |
| 167 | 168 | <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"> | |
| 169 | 170 | <SmilePlus size={13} /> Add icon | |
| 170 | − | </button> | |
| 171 | + | </Button> | |
| 171 | 172 | </IconPicker> | |
| 172 | 173 | )} | |
| 173 | 174 | </div> | |
| ⋯ | |||
| 219 | 220 | <AlertTriangle size={15} className="shrink-0 text-warn" /> | |
| 220 | 221 | <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> | |
| 221 | 222 | {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"> | |
| 223 | 224 | It's current | |
| 224 | − | </button> | |
| 225 | + | </Button> | |
| 225 | 226 | )} | |
| 226 | 227 | </div> | |
| 227 | 228 | )} | |
| ⋯ | |||
| 235 | 236 | {suggestions.length} {suggestions.length === 1 ? "suggestion" : "suggestions"} waiting | |
| 236 | 237 | </span> | |
| 237 | 238 | {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"> | |
| 239 | 240 | Accept all | |
| 240 | − | </button> | |
| 241 | + | </Button> | |
| 241 | 242 | )} | |
| 242 | 243 | </div> | |
| 243 | 244 | <div className="space-y-2"> | |
| ⋯ | |||
| 249 | 250 | )} | |
| 250 | 251 | {suggestions.length > 1 && editable && ( | |
| 251 | 252 | <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"> | |
| 253 | 254 | <Check size={13} /> Accept all {suggestions.length} suggestions | |
| 254 | − | </button> | |
| 255 | + | </Button> | |
| 255 | 256 | </div> | |
| 256 | 257 | )} | |
| 257 | 258 | </div> | |
| 6 | 6 | import { FileCode2, FolderGit2 } from "lucide-react"; | |
| 7 | 7 | import { useEffect, useState } from "react"; | |
| 8 | 8 | ||
| 9 | − | import { Button, ErrorText, Field, Input } from "../../ui"; | |
| 9 | + | import { ErrorText, Field, Input } from "../../ui"; | |
| 10 | + | import { Button } from "../../ui/button"; | |
| 10 | 11 | import { Combobox } from "../../ui/combobox"; | |
| 11 | 12 | import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../../ui/dialog"; | |
| 12 | 13 | import { SelectField } from "../../ui/select"; |
| 6 | 6 | import { Check, MessageSquare, Sparkles, X } from "lucide-react"; | |
| 7 | 7 | import { type ReactNode, useState } from "react"; | |
| 8 | 8 | ||
| 9 | − | import { Button, TimeAgo } from "../../ui"; | |
| 9 | + | import { TimeAgo } from "../../ui"; | |
| 10 | + | import { Button } from "../../ui/button"; | |
| 10 | 11 | import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; | |
| 11 | 12 | import { Face } from "../parts"; | |
| 12 | 13 | ||
| ⋯ | |||
| 62 | 63 | <DiffBlock text={s.after_markdown} tone="add" /> | |
| 63 | 64 | </> | |
| 64 | 65 | ) : ( | |
| 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"> | |
| 66 | 67 | Show the change | |
| 67 | − | </button> | |
| 68 | + | </Button> | |
| 68 | 69 | )} | |
| 69 | 70 | </div> | |
| 70 | 71 | {canDecide && ( | |
| 71 | 72 | <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"> | |
| 73 | 74 | <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"> | |
| 76 | 77 | <X size={13} /> Reject | |
| 77 | − | </button> | |
| 78 | + | </Button> | |
| 78 | 79 | </footer> | |
| 79 | 80 | )} | |
| 80 | 81 | </article> | |
| ⋯ | |||
| 117 | 118 | }} | |
| 118 | 119 | > | |
| 119 | 120 | <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"> | |
| 121 | 122 | Use | |
| 122 | − | </button> | |
| 123 | + | </Button> | |
| 123 | 124 | </form> | |
| 124 | 125 | {value && ( | |
| 125 | − | <button | |
| 126 | + | <Button | |
| 126 | 127 | type="button" | |
| 127 | 128 | onClick={() => { | |
| 128 | 129 | onChange(null); | |
| 129 | 130 | setOpen(false); | |
| 130 | 131 | }} | |
| 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" | |
| 132 | 135 | > | |
| 133 | 136 | Remove icon | |
| 134 | − | </button> | |
| 137 | + | </Button> | |
| 135 | 138 | )} | |
| 136 | 139 | </PopoverContent> | |
| 137 | 140 | </Popover> | |
| ⋯ | |||
| 190 | 193 | }} | |
| 191 | 194 | > | |
| 192 | 195 | <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"> | |
| 194 | 197 | {t.resolved ? "Reopen" : "Resolve"} | |
| 195 | − | </button> | |
| 198 | + | </Button> | |
| 196 | 199 | </form> | |
| 197 | 200 | )} | |
| 198 | 201 | </li> | |
| ⋯ | |||
| 206 | 209 | <ul className="mt-4 space-y-3">{open.map(thread)}</ul> | |
| 207 | 210 | {resolved.length > 0 && ( | |
| 208 | 211 | <> | |
| 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"> | |
| 210 | 213 | {showResolved ? "Hide" : "Show"} {resolved.length} resolved | |
| 211 | − | </button> | |
| 214 | + | </Button> | |
| 212 | 215 | {showResolved && <ul className="mt-3 space-y-3">{resolved.map(thread)}</ul>} | |
| 213 | 216 | </> | |
| 214 | 217 | )} | |
| ⋯ | |||
| 228 | 231 | aria-label="Add a comment" | |
| 229 | 232 | 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" | |
| 230 | 233 | /> | |
| 231 | − | <Button type="submit" variant="quiet" disabled={!draft.trim()}> | |
| 234 | + | <Button type="submit" variant="outline" disabled={!draft.trim()}> | |
| 232 | 235 | Comment | |
| 233 | 236 | </Button> | |
| 234 | 237 | </form> | |
| 11 | 11 | ||
| 12 | 12 | import { dayGroups } from "../../lib/folios"; | |
| 13 | 13 | import { TimeAgo } from "../ui"; | |
| 14 | + | import { Card } from "../ui/card"; | |
| 14 | 15 | import { Hint } from "../ui/hint"; | |
| 15 | 16 | import { useFoliosData } from "./actions"; | |
| 16 | 17 | import { FolioThumbnail, KindIcon } from "./kinds"; | |
| ⋯ | |||
| 72 | 73 | {days.map((day) => ( | |
| 73 | 74 | <section key={day.key} aria-label={day.label}> | |
| 74 | 75 | <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> | |
| 80 | 83 | </section> | |
| 81 | 84 | ))} | |
| 82 | 85 | </div> | |
| ⋯ | |||
| 86 | 89 | export function FolioCard({ slug, folio, onError }: { slug: string; folio: Folio; onError?: (message: string) => void }) { | |
| 87 | 90 | const me = useFoliosData()?.me.key; | |
| 88 | 91 | 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"> | |
| 90 | 93 | <div className="pointer-events-none h-32 shrink-0 border-b border-line"> | |
| 91 | 94 | <FolioThumbnail folio={folio} /> | |
| 92 | 95 | </div> | |
| ⋯ | |||
| 112 | 115 | <FolioMenu slug={slug} folio={folio} onError={onError} /> | |
| 113 | 116 | </span> | |
| 114 | 117 | </div> | |
| 115 | − | </div> | |
| 118 | + | </Card> | |
| 116 | 119 | ); | |
| 117 | 120 | } | |
| 118 | 121 | ||
| 10 | 10 | import { Link, useNavigate, useRevalidator } from "react-router"; | |
| 11 | 11 | ||
| 12 | 12 | import { canDo } from "../../lib/folios"; | |
| 13 | + | import { Button } from "../ui/button"; | |
| 13 | 14 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu"; | |
| 14 | 15 | import { foliosRequest, useFoliosData } from "./actions"; | |
| 15 | 16 | import { MoveDialog, RenameDialog, TemplateDialog } from "./dialogs"; | |
| ⋯ | |||
| 52 | 53 | <DropdownMenu> | |
| 53 | 54 | <DropdownMenuTrigger asChild> | |
| 54 | 55 | {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"> | |
| 56 | 57 | <Ellipsis size={16} /> | |
| 57 | − | </button> | |
| 58 | + | </Button> | |
| 58 | 59 | )} | |
| 59 | 60 | </DropdownMenuTrigger> | |
| 60 | 61 | <DropdownMenuContent align="end" className="w-56"> | |
| 9 | 9 | import { Link } from "react-router"; | |
| 10 | 10 | ||
| 11 | 11 | import { AgentAvatar } from "../agent-avatar"; | |
| 12 | − | import { Avatar } from "../ui"; | |
| 12 | + | ||
| 13 | + | import { Avatar } from "../ui/avatar"; | |
| 14 | + | import { Card } from "../ui/card"; | |
| 13 | 15 | import { Hint } from "../ui/hint"; | |
| 14 | 16 | import { KindIcon } from "./kinds"; | |
| 15 | 17 | ||
| ⋯ | |||
| 130 | 132 | ||
| 131 | 133 | export function TemplateCard({ template, action }: { template: FolioTemplate; action: ReactNode }) { | |
| 132 | 134 | return ( | |
| 133 | − | <div className="flex flex-col rounded-xl border border-line bg-surface p-4"> | |
| 135 | + | <Card className="flex flex-col p-4"> | |
| 134 | 136 | <span className="flex items-center gap-2"> | |
| 135 | 137 | {template.icon ? ( | |
| 136 | 138 | <span className="text-2xl leading-none" aria-hidden="true"> | |
| ⋯ | |||
| 143 | 145 | <span className="mt-3 text-sm font-medium">{template.name}</span> | |
| 144 | 146 | <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> | |
| 145 | 147 | <div className="mt-3 flex items-center gap-2">{action}</div> | |
| 146 | − | </div> | |
| 148 | + | </Card> | |
| 147 | 149 | ); | |
| 148 | 150 | } | |
| 12 | 12 | import { Link } from "react-router"; | |
| 13 | 13 | ||
| 14 | 14 | import { spacePath } from "../../lib/folios"; | |
| 15 | − | import { Button, ErrorText } from "../ui"; | |
| 15 | + | import { ErrorText } from "../ui"; | |
| 16 | + | import { Button } from "../ui/button"; | |
| 16 | 17 | import { Combobox } from "../ui/combobox"; | |
| 17 | 18 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "../ui/dialog"; | |
| 18 | 19 | import { Hint } from "../ui/hint"; | |
| ⋯ | |||
| 274 | 275 | <div className="mt-2 flex items-center justify-between gap-2 border-t border-line pt-4"> | |
| 275 | 276 | <Button | |
| 276 | 277 | type="button" | |
| 277 | − | variant="quiet" | |
| 278 | + | variant="outline" | |
| 278 | 279 | onClick={async () => { | |
| 279 | 280 | try { | |
| 280 | 281 | await navigator.clipboard.writeText(`${window.location.origin}${folio.path}`); | |
| 8 | 8 | import type { Folio, FolioRef } from "@g1t/contracts"; | |
| 9 | 9 | import { History, MessageSquare, Share2, Star, WifiOff } from "lucide-react"; | |
| 10 | 10 | import { type ReactNode, useState } from "react"; | |
| 11 | − | import { Link, useRevalidator } from "react-router"; | |
| 11 | + | import { useRevalidator } from "react-router"; | |
| 12 | 12 | ||
| 13 | 13 | import { spacePath } from "../../lib/folios"; | |
| 14 | + | import { ButtonLink } from "../ui"; | |
| 15 | + | import { Button } from "../ui/button"; | |
| 14 | 16 | import { Hint } from "../ui/hint"; | |
| 15 | 17 | import { foliosRequest } from "./actions"; | |
| 16 | 18 | import { FolioGlyph } from "./kinds"; | |
| ⋯ | |||
| 22 | 24 | /** Someone here now, as the live document's awareness says. */ | |
| 23 | 25 | export type Presence = { client: number; key: string; name: string; kind: "user" | "agent"; color: string; avatar: string | null; me: boolean }; | |
| 24 | 26 | ||
| 25 | − | const ICON_BUTTON = "flex size-8 shrink-0 items-center justify-center rounded-md max-md:size-10"; | |
| 26 | − | ||
| 27 | 27 | export function FolioHeader({ | |
| 28 | 28 | slug, | |
| 29 | 29 | folio, | |
| ⋯ | |||
| 88 | 88 | </span> | |
| 89 | 89 | )} | |
| 90 | 90 | {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"> | |
| 92 | 92 | <Share2 size={13} /> <span className="max-md:hidden">Share</span> | |
| 93 | − | </button> | |
| 93 | + | </Button> | |
| 94 | 94 | {comments && ( | |
| 95 | 95 | <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"> | |
| 97 | 97 | <MessageSquare size={16} /> | |
| 98 | − | </button> | |
| 98 | + | </Button> | |
| 99 | 99 | </Hint> | |
| 100 | 100 | )} | |
| 101 | 101 | <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"> | |
| 103 | 103 | <History size={16} /> | |
| 104 | − | </Link> | |
| 104 | + | </ButtonLink> | |
| 105 | 105 | </Hint> | |
| 106 | 106 | <Hint label={favorite ? "Remove from Favorites" : "Add to Favorites"}> | |
| 107 | − | <button | |
| 107 | + | <Button | |
| 108 | 108 | type="button" | |
| 109 | 109 | aria-label={favorite ? "Remove from Favorites" : "Add to Favorites"} | |
| 110 | 110 | aria-pressed={favorite} | |
| ⋯ | |||
| 117 | 117 | onError(done.error.message); | |
| 118 | 118 | } | |
| 119 | 119 | }} | |
| 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" | |
| 121 | 123 | > | |
| 122 | 124 | <Star size={16} className={favorite ? "fill-current text-warn" : ""} /> | |
| 123 | − | </button> | |
| 125 | + | </Button> | |
| 124 | 126 | </Hint> | |
| 125 | 127 | <FolioMenu slug={slug} folio={{ ...folio, favorite }} page onError={onError} onTrashed={onTrashed} /> | |
| 126 | 128 | <ShareDialog slug={slug} folio={folio} open={sharing} onOpenChange={setSharing} onChanged={() => void revalidate()} /> | |
| 13 | 13 | import { NavLink, useLocation, useNavigate, useParams, useRevalidator } from "react-router"; | |
| 14 | 14 | ||
| 15 | 15 | import { buildTree, canDo, pathTo, repoFilePath, repoFolders, spacePath, type RepoFolder, type TreeItem } from "../../lib/folios"; | |
| 16 | + | import { Button } from "../ui/button"; | |
| 16 | 17 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu"; | |
| 17 | 18 | import { Hint } from "../ui/hint"; | |
| 18 | 19 | import { Skeleton } from "../ui/skeleton"; | |
| ⋯ | |||
| 221 | 222 | await send("move", { folio_id: id, move: { space_id: space.id, parent_id: null, before_id: null } }); | |
| 222 | 223 | }} | |
| 223 | 224 | > | |
| 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"> | |
| 225 | 226 | <ChevronRight size={13} className={`transition-transform ${collapsed ? "" : "rotate-90"}`} /> | |
| 226 | − | </button> | |
| 227 | + | </Button> | |
| 227 | 228 | <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" : ""}`}> | |
| 228 | 229 | <span className="flex w-4 shrink-0 justify-center"> | |
| 229 | 230 | <SpaceIcon space={space} size={14} /> | |
| ⋯ | |||
| 314 | 315 | return ( | |
| 315 | 316 | <li className="mt-1"> | |
| 316 | 317 | <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"> | |
| 318 | 319 | <ChevronRight size={13} className={`transition-transform ${collapsed ? "" : "rotate-90"}`} /> | |
| 319 | − | </button> | |
| 320 | + | </Button> | |
| 320 | 321 | <span className="flex w-4 shrink-0 justify-center"> | |
| 321 | 322 | <FolderGit2 size={14} className="text-faint" aria-hidden="true" /> | |
| 322 | 323 | </span> | |
| ⋯ | |||
| 366 | 367 | <div className="flex h-9 shrink-0 items-center gap-1 pr-1 pl-3"> | |
| 367 | 368 | <h2 className="min-w-0 grow truncate text-xs font-medium text-faint">Artifacts</h2> | |
| 368 | 369 | <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"> | |
| 370 | 371 | <Plus size={16} /> | |
| 371 | 372 | <ChevronDown size={12} /> | |
| 372 | − | </button> | |
| 373 | + | </Button> | |
| 373 | 374 | </NewMenu> | |
| 374 | 375 | {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"> | |
| 376 | 377 | <X size={16} /> | |
| 377 | − | </button> | |
| 378 | + | </Button> | |
| 378 | 379 | )} | |
| 379 | 380 | </div> | |
| 380 | 381 | <nav aria-label="Artifacts" className="min-h-0 grow overflow-y-auto px-2.5 pt-3 pb-4 [scrollbar-width:thin]"> | |
| ⋯ | |||
| 428 | 429 | <DropdownMenu> | |
| 429 | 430 | <Hint label="Browse or make spaces"> | |
| 430 | 431 | <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"> | |
| 432 | 433 | <Plus size={13} /> | |
| 433 | − | </button> | |
| 434 | + | </Button> | |
| 434 | 435 | </DropdownMenuTrigger> | |
| 435 | 436 | </Hint> | |
| 436 | 437 | <DropdownMenuContent align="end" className="w-44"> | |
| ⋯ | |||
| 456 | 457 | title="Private" | |
| 457 | 458 | action={ | |
| 458 | 459 | <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"> | |
| 460 | 461 | <Plus size={13} /> | |
| 461 | − | </button> | |
| 462 | + | </Button> | |
| 462 | 463 | </Hint> | |
| 463 | 464 | } | |
| 464 | 465 | > | |
| ⋯ | |||
| 501 | 502 | open={(sidebar.repos ?? []).length > 0} | |
| 502 | 503 | action={ | |
| 503 | 504 | <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"> | |
| 505 | 506 | <Plus size={13} /> | |
| 506 | − | </button> | |
| 507 | + | </Button> | |
| 507 | 508 | </Hint> | |
| 508 | 509 | } | |
| 509 | 510 | > | |
| 6 | 6 | import { DOC_AGENT_MODE_LABELS, DOC_ROLE_LABELS, DOC_SPACE_KIND_LABELS, type DocAgentMode, type DocRole, type DocSpace, type DocSpaceKind } from "@g1t/contracts"; | |
| 7 | 7 | import { type FormEvent, useState } from "react"; | |
| 8 | 8 | ||
| 9 | − | import { Button, ErrorText } from "../ui"; | |
| 9 | + | import { ErrorText } from "../ui"; | |
| 10 | + | import { Button } from "../ui/button"; | |
| 10 | 11 | import { SelectField } from "../ui/select"; | |
| 11 | 12 | ||
| 12 | 13 | export type SpaceFormValue = { |
| 12 | 12 | ||
| 13 | 13 | import { linkedArtifacts } from "../../lib/folios"; | |
| 14 | 14 | import { TimeAgo } from "../ui"; | |
| 15 | + | import { Card } from "../ui/card"; | |
| 15 | 16 | import { KindIcon } from "./kinds"; | |
| 16 | 17 | ||
| 17 | 18 | /** What the site answers for one link (routes/workspace/folios/api.ts `unfurl`); null: not for this viewer. */ | |
| ⋯ | |||
| 61 | 62 | if (card === "loading" || card === undefined) return null; | |
| 62 | 63 | if (card === null) { | |
| 63 | 64 | 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"> | |
| 65 | 66 | <Lock size={14} className="shrink-0" /> | |
| 66 | 67 | An artifact you don't have access to | |
| 67 | − | </div> | |
| 68 | + | </Card> | |
| 68 | 69 | ); | |
| 69 | 70 | } | |
| 70 | 71 | return ( | |
| 11 | 11 | import { money } from "../lib/usage"; | |
| 12 | 12 | import { TimeAgo } from "./ui"; | |
| 13 | 13 | import { Badge } from "./ui/badge"; | |
| 14 | + | import { Card } from "./ui/card"; | |
| 14 | 15 | import { Hint } from "./ui/hint"; | |
| 15 | 16 | import { Loading, SkeletonLine } from "./ui/skeleton"; | |
| 16 | 17 | ||
| ⋯ | |||
| 96 | 97 | /** The table's frame: its heading row, around its body. */ | |
| 97 | 98 | function Frame({ children }: { children: ReactNode }) { | |
| 98 | 99 | return ( | |
| 99 | − | <div className="overflow-x-auto rounded-xl border border-line bg-surface"> | |
| 100 | + | <Card className="overflow-x-auto"> | |
| 100 | 101 | <table className="w-full"> | |
| 101 | 102 | <thead className="border-b border-line"> | |
| 102 | 103 | <tr> | |
| ⋯ | |||
| 109 | 110 | </thead> | |
| 110 | 111 | {children} | |
| 111 | 112 | </table> | |
| 112 | − | </div> | |
| 113 | + | </Card> | |
| 113 | 114 | ); | |
| 114 | 115 | } | |
| 115 | 116 | ||
| 2 | 2 | import { siGithub } from "simple-icons"; | |
| 3 | 3 | ||
| 4 | 4 | import { TimeAgo } from "./ui"; | |
| 5 | + | import { Button } from "./ui/button"; | |
| 6 | + | import { Card } from "./ui/card"; | |
| 5 | 7 | ||
| 6 | 8 | /** GitHub's mark (from Simple Icons), for the buttons that go there. */ | |
| 7 | 9 | export function GithubMark({ className = "size-4" }: { className?: string }) { | |
| ⋯ | |||
| 18 | 20 | */ | |
| 19 | 21 | export function ContinueWithGithub({ href, label = "Continue with GitHub" }: { href: string; label?: string }) { | |
| 20 | 22 | 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> | |
| 28 | 29 | ); | |
| 29 | 30 | } | |
| 30 | 31 | ||
| ⋯ | |||
| 46 | 47 | */ | |
| 47 | 48 | export function GithubLinkStrip({ link }: { link: GithubRepoLink }) { | |
| 48 | 49 | 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"> | |
| 50 | 51 | <span className="flex min-w-0 items-start gap-2 text-muted"> | |
| 51 | 52 | <GithubMark className="mt-0.5 size-4 shrink-0" /> | |
| 52 | 53 | <span className="min-w-0"> | |
| ⋯ | |||
| 62 | 63 | </span> | |
| 63 | 64 | )} | |
| 64 | 65 | {link.lastError && <p className="w-full text-xs text-warn">{link.lastError}</p>} | |
| 65 | − | </div> | |
| 66 | + | </Card> | |
| 66 | 67 | ); | |
| 67 | 68 | } | |
| 19 | 19 | import { formatCap, tri, workflowDomainLine } from "../lib/guardrails"; | |
| 20 | 20 | import { formatCost } from "./agents"; | |
| 21 | 21 | import { ErrorText, Input, SubmitButton, TimeAgo } from "./ui"; | |
| 22 | + | import { Badge } from "./ui/badge"; | |
| 23 | + | import { Card } from "./ui/card"; | |
| 22 | 24 | import { CheckboxOption } from "./ui/checkbox"; | |
| 23 | 25 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| 24 | 26 | import { Textarea } from "./ui/textarea"; | |
| ⋯ | |||
| 54 | 56 | children: ReactNode; | |
| 55 | 57 | }) { | |
| 56 | 58 | 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"> | |
| 58 | 60 | <div className="min-w-0 grow"> | |
| 59 | 61 | <p className="text-sm font-medium">{title}</p> | |
| 60 | 62 | <p className="mt-1 text-sm text-muted">{children}</p> | |
| ⋯ | |||
| 71 | 73 | <SelectItem value="off">{labels[1]}</SelectItem> | |
| 72 | 74 | </SelectContent> | |
| 73 | 75 | </Select> | |
| 74 | − | </div> | |
| 76 | + | </Card> | |
| 75 | 77 | ); | |
| 76 | 78 | } | |
| 77 | 79 | ||
| ⋯ | |||
| 135 | 137 | Restricted, a sandbox reaches g1t, the registries below and the domains you list, over HTTP and | |
| 136 | 138 | HTTPS only. Open, it reaches the whole internet. | |
| 137 | 139 | </TriSelect> | |
| 138 | − | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 140 | + | <Card className="p-4"> | |
| 139 | 141 | <p className="text-sm font-medium">Always allowed</p> | |
| 140 | 142 | <p className="mt-1 text-sm text-muted"> | |
| 141 | 143 | g1t's own hosts, for cloning, pushing, reporting and the model: <Hosts hosts={view.g1tHosts} /> | |
| 142 | 144 | </p> | |
| 143 | − | </div> | |
| 144 | − | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 145 | + | </Card> | |
| 146 | + | <Card className="p-4"> | |
| 145 | 147 | <div className="flex flex-col gap-3 sm:flex-row sm:items-start"> | |
| 146 | 148 | <div className="min-w-0 grow"> | |
| 147 | 149 | <p className="text-sm font-medium">Package registries</p> | |
| ⋯ | |||
| 169 | 171 | /> | |
| 170 | 172 | ))} | |
| 171 | 173 | </div> | |
| 172 | − | </div> | |
| 173 | − | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 174 | + | </Card> | |
| 175 | + | <Card className="p-4"> | |
| 174 | 176 | <label htmlFor="guardrail-domains" className="text-sm font-medium"> | |
| 175 | 177 | Allowed domains | |
| 176 | 178 | </label> | |
| ⋯ | |||
| 192 | 194 | From the workspace: <Hosts hosts={inheritedDomains} /> | |
| 193 | 195 | </p> | |
| 194 | 196 | )} | |
| 195 | − | </div> | |
| 196 | − | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 197 | + | </Card> | |
| 198 | + | <Card className="p-4"> | |
| 197 | 199 | <label htmlFor="guardrail-workflow-domains" className="text-sm font-medium"> | |
| 198 | 200 | Workflow-only domains | |
| 199 | 201 | </label> | |
| ⋯ | |||
| 217 | 219 | <span className="font-mono break-words">{inheritedWorkflowDomains.map(workflowDomainLine).join("; ")}</span> | |
| 218 | 220 | </p> | |
| 219 | 221 | )} | |
| 220 | − | </div> | |
| 222 | + | </Card> | |
| 221 | 223 | </Section> | |
| 222 | 224 | ||
| 223 | 225 | <Section | |
| ⋯ | |||
| 242 | 244 | {rule.about} | |
| 243 | 245 | </TriSelect> | |
| 244 | 246 | ))} | |
| 245 | − | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 247 | + | <Card className="p-4"> | |
| 246 | 248 | <label htmlFor="guardrail-deny" className="text-sm font-medium"> | |
| 247 | 249 | Also refuse | |
| 248 | 250 | </label> | |
| ⋯ | |||
| 265 | 267 | From the workspace: <span className="font-mono">{inheritedDeny.join(", ")}</span> | |
| 266 | 268 | </p> | |
| 267 | 269 | )} | |
| 268 | − | </div> | |
| 270 | + | </Card> | |
| 269 | 271 | </Section> | |
| 270 | 272 | ||
| 271 | 273 | <Section | |
| 272 | 274 | title="Caps" | |
| 273 | 275 | 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." | |
| 274 | 276 | > | |
| 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"> | |
| 276 | 278 | <div className="min-w-0 grow"> | |
| 277 | 279 | <label htmlFor="guardrail-budget" className="text-sm font-medium"> | |
| 278 | 280 | Cost per run, in US dollars | |
| ⋯ | |||
| 289 | 291 | defaultValue={own.budgetUsd == null ? "" : String(own.budgetUsd)} | |
| 290 | 292 | placeholder={base.budgetUsd == null ? "0" : base.budgetUsd.toFixed(2)} | |
| 291 | 293 | /> | |
| 292 | − | </div> | |
| 293 | − | <div className="rounded-xl border border-line bg-surface p-4"> | |
| 294 | + | </Card> | |
| 295 | + | <Card className="p-4"> | |
| 294 | 296 | <p className="text-sm font-medium">Time per run, in minutes</p> | |
| 295 | 297 | <p className="mt-1 text-sm text-muted">Empty: as {parent}, shown faded.</p> | |
| 296 | 298 | <div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3"> | |
| ⋯ | |||
| 306 | 308 | </label> | |
| 307 | 309 | ))} | |
| 308 | 310 | </div> | |
| 309 | − | </div> | |
| 311 | + | </Card> | |
| 310 | 312 | </Section> | |
| 311 | 313 | </fieldset> | |
| 312 | 314 | ||
| ⋯ | |||
| 366 | 368 | const budget = run.budgetUsd ?? null; | |
| 367 | 369 | const costShare = budget && run.costUsd != null ? Math.min(1, run.costUsd / budget) : null; | |
| 368 | 370 | 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)} | |
| 405 | 403 | </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> | |
| 411 | 415 | ); | |
| 412 | 416 | } | |
| 48 | 48 | whenShort, | |
| 49 | 49 | } from "../lib/home"; | |
| 50 | 50 | import { cn } from "../lib/cn"; | |
| 51 | + | import { ButtonLink } from "./ui"; | |
| 51 | 52 | import { Skeleton } from "./ui/skeleton"; | |
| 52 | 53 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; | |
| 53 | 54 | ||
| ⋯ | |||
| 186 | 187 | </div> | |
| 187 | 188 | )} | |
| 188 | 189 | </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"> | |
| 193 | 191 | {row.action.label} | |
| 194 | − | </Link> | |
| 192 | + | </ButtonLink> | |
| 195 | 193 | </li> | |
| 196 | 194 | ); | |
| 197 | 195 | } | |
| 8 | 8 | import { SubmitButton } from "./ui"; | |
| 9 | 9 | import { Badge, type BadgeTone } from "./ui/badge"; | |
| 10 | 10 | import { Button } from "./ui/button"; | |
| 11 | + | import { Card } from "./ui/card"; | |
| 11 | 12 | import { Hint } from "./ui/hint"; | |
| 12 | 13 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu"; | |
| 13 | 14 | import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet"; | |
| ⋯ | |||
| 127 | 128 | <fetcher.Form method="post" action={ACTION}> | |
| 128 | 129 | <Fields intent="undone" id={item.id} /> | |
| 129 | 130 | <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}> | |
| 131 | 132 | <Undo2 size={14} /> | |
| 132 | 133 | </SubmitButton> | |
| 133 | 134 | </Hint> | |
| ⋯ | |||
| 136 | 137 | <fetcher.Form method="post" action={ACTION}> | |
| 137 | 138 | <Fields intent="done" id={item.id} /> | |
| 138 | 139 | <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}> | |
| 140 | 141 | <Check size={14} /> | |
| 141 | 142 | </SubmitButton> | |
| 142 | 143 | </Hint> | |
| 143 | 144 | </fetcher.Form> | |
| 144 | 145 | )} | |
| 145 | 146 | <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> | |
| 148 | 151 | </DropdownMenuTrigger> | |
| 149 | 152 | <DropdownMenuContent align="end"> | |
| 150 | 153 | <DropdownMenuItem onSelect={() => submit(unread ? "read" : "unread")}> | |
| ⋯ | |||
| 174 | 177 | ); | |
| 175 | 178 | } | |
| 176 | 179 | ||
| 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"; | |
| 179 | 182 | ||
| 180 | 183 | /** What an empty list says, for its tab. */ | |
| 181 | 184 | export function InboxEmpty({ tab, view }: { tab: InboxTab; view?: "inbox" | "saved" | "done" }) { | |
| 182 | 185 | const { title, detail } = emptyFor(tab, view); | |
| 183 | 186 | 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"> | |
| 185 | 188 | <Bell size={20} className="text-faint" aria-hidden="true" /> | |
| 186 | 189 | <p className="text-sm font-medium">{title}</p> | |
| 187 | 190 | <p className="max-w-64 text-xs text-muted">{detail}</p> | |
| 188 | − | </div> | |
| 191 | + | </Card> | |
| 189 | 192 | ); | |
| 190 | 193 | } | |
| 191 | 194 | ||
| ⋯ | |||
| 228 | 231 | fetcher={fetcher} | |
| 229 | 232 | disabled={disabled} | |
| 230 | 233 | 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" | |
| 232 | 237 | > | |
| 233 | 238 | <CheckCheck size={14} /> | |
| 234 | 239 | Mark all read | |
| ⋯ | |||
| 312 | 317 | ) : ( | |
| 313 | 318 | <ul aria-busy="true" className="space-y-2"> | |
| 314 | 319 | {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> | |
| 322 | 329 | ))} | |
| 323 | 330 | </ul> | |
| 324 | 331 | ) | |
| ⋯ | |||
| 354 | 361 | export function InboxNeedsCard({ items, total }: { items: InboxItem[]; total: number }) { | |
| 355 | 362 | if (items.length === 0) return null; | |
| 356 | 363 | 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> | |
| 374 | 383 | ); | |
| 375 | 384 | } | |
| 2 | 2 | ||
| 3 | 3 | import type { WorkspaceInvitation } from "@g1t/contracts"; | |
| 4 | 4 | ||
| 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"; | |
| 6 | 8 | import { UserCard } from "./user-card"; | |
| 7 | 9 | import { invitationLine } from "../lib/invitations"; | |
| 8 | 10 | ||
| ⋯ | |||
| 24 | 26 | next?: string | null; | |
| 25 | 27 | }) { | |
| 26 | 28 | 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> | |
| 49 | 71 | </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> | |
| 77 | 81 | ); | |
| 78 | 82 | } | |
| 5 | 5 | import type { Invite, InvitesOverview } from "@g1t/contracts"; | |
| 6 | 6 | ||
| 7 | 7 | import { CopyLine, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui"; | |
| 8 | + | import { Badge } from "./ui/badge"; | |
| 8 | 9 | import { CheckboxOption } from "./ui/checkbox"; | |
| 9 | 10 | import { Hint } from "./ui/hint"; | |
| 10 | 11 | import { SelectField } from "./ui/select"; | |
| ⋯ | |||
| 34 | 35 | : "An invitation to join this workspace, which they accept or decline. Without an account, it lets them make one first." | |
| 35 | 36 | } | |
| 36 | 37 | > | |
| 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> | |
| 38 | 39 | </Hint> | |
| 39 | 40 | {(invite.status === "pending" || invite.status === "awaiting_confirmation" || invite.status === "awaiting_answer") && ( | |
| 40 | 41 | <Form method="post" className="ml-auto"> | |
| 41 | 42 | <input type="hidden" name="intent" value="revoke-invite" /> | |
| 42 | 43 | <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…"> | |
| 44 | 45 | Revoke | |
| 45 | 46 | </SubmitButton> | |
| 46 | 47 | </Form> | |
| 7 | 7 | import { chipStyle, matchLabels, percentDone, tidyColor, tidyLabelName } from "../lib/labels"; | |
| 8 | 8 | import { cn } from "../lib/cn"; | |
| 9 | 9 | 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"; | |
| 11 | 12 | import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; | |
| 12 | 13 | ||
| 13 | 14 | /** A label as a colored chip. Without a known color it is neutral. */ | |
| ⋯ | |||
| 114 | 115 | }} | |
| 115 | 116 | > | |
| 116 | 117 | <PopoverTrigger asChild> | |
| 117 | − | <button | |
| 118 | + | <Button | |
| 118 | 119 | type="button" | |
| 119 | 120 | 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" | |
| 121 | 124 | > | |
| 122 | 125 | <Settings2 size={15} /> | |
| 123 | − | </button> | |
| 126 | + | </Button> | |
| 124 | 127 | </PopoverTrigger> | |
| 125 | 128 | <PopoverContent align="end" className="w-72 p-0"> | |
| 126 | 129 | <Command shouldFilter={false}> | |
| ⋯ | |||
| 240 | 243 | {canEdit && ( | |
| 241 | 244 | <Popover open={open} onOpenChange={setOpen}> | |
| 242 | 245 | <PopoverTrigger asChild> | |
| 243 | − | <button | |
| 246 | + | <Button | |
| 244 | 247 | type="button" | |
| 245 | 248 | 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" | |
| 247 | 252 | > | |
| 248 | 253 | <Settings2 size={15} /> | |
| 249 | − | </button> | |
| 254 | + | </Button> | |
| 250 | 255 | </PopoverTrigger> | |
| 251 | 256 | <PopoverContent align="end" className="w-72 p-0"> | |
| 252 | 257 | <Command> | |
| 369 | 369 | forge to stitch together. | |
| 370 | 370 | </p> | |
| 371 | 371 | <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"> | |
| 373 | 373 | Start for free | |
| 374 | 374 | <ArrowRight size={15} /> | |
| 375 | 375 | </ButtonLink> | |
| 376 | − | <ButtonLink to="#how" variant="quiet" large> | |
| 376 | + | <ButtonLink to="#how" variant="outline" size="lg" className="rounded-full"> | |
| 377 | 377 | See how it works | |
| 378 | 378 | </ButtonLink> | |
| 379 | 379 | </div> | |
| ⋯ | |||
| 885 | 885 | event. | |
| 886 | 886 | </p> | |
| 887 | 887 | <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"> | |
| 889 | 889 | Connect an agent | |
| 890 | 890 | <ArrowRight size={14} /> | |
| 891 | 891 | </ButtonLink> | |
| 892 | − | <ButtonLink to={`${DOCS}/reference/api/`} variant="quiet"> | |
| 892 | + | <ButtonLink to={`${DOCS}/reference/api/`} variant="outline"> | |
| 893 | 893 | API reference | |
| 894 | 894 | </ButtonLink> | |
| 895 | 895 | </div> | |
| ⋯ | |||
| 944 | 944 | only on g1t.sh. | |
| 945 | 945 | </p> | |
| 946 | 946 | <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"> | |
| 948 | 948 | Browse the source | |
| 949 | 949 | </ButtonLink> | |
| 950 | − | <ButtonLink to={`${DOCS}/guides/self-hosting/`} variant="quiet"> | |
| 950 | + | <ButtonLink to={`${DOCS}/guides/self-hosting/`} variant="outline"> | |
| 951 | 951 | Run g1t yourself | |
| 952 | 952 | </ButtonLink> | |
| 953 | 953 | </div> | |
| ⋯ | |||
| 981 | 981 | credit when you want agents to work. | |
| 982 | 982 | </p> | |
| 983 | 983 | <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"> | |
| 985 | 985 | Create your workspace | |
| 986 | 986 | <ArrowRight size={15} /> | |
| 987 | 987 | </ButtonLink> | |
| 988 | − | <ButtonLink to={`${DOCS}/quickstart/`} variant="quiet" large> | |
| 988 | + | <ButtonLink to={`${DOCS}/quickstart/`} variant="outline" size="lg" className="rounded-full"> | |
| 989 | 989 | Read the quickstart | |
| 990 | 990 | </ButtonLink> | |
| 991 | 991 | </div> | |
| 1 | 1 | import { Hand, Loader } from "lucide-react"; | |
| 2 | 2 | ||
| 3 | 3 | import type { Lifecycle, Stage } from "@g1t/contracts"; | |
| 4 | + | import { Card } from "./ui/card"; | |
| 4 | 5 | ||
| 5 | 6 | const STEPS = ["Change", "Checks", "Review", "Up to date", "Landing"]; | |
| 6 | 7 | ||
| ⋯ | |||
| 85 | 86 | } | |
| 86 | 87 | const current = STEP_OF[stage]; | |
| 87 | 88 | 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"> | |
| 89 | 90 | <div className="flex items-center gap-2.5"> | |
| 90 | 91 | <Loader size={15} className="shrink-0 animate-spin text-accent motion-reduce:animate-none" /> | |
| 91 | 92 | <p className="font-medium"> | |
| ⋯ | |||
| 116 | 117 | })} | |
| 117 | 118 | </ol> | |
| 118 | 119 | <p className="mt-2.5 text-muted">{detail}</p> | |
| 119 | − | </div> | |
| 120 | + | </Card> | |
| 120 | 121 | ); | |
| 121 | 122 | } | |
| 16 | 16 | import rehypeSanitize, { defaultSchema } from "rehype-sanitize"; | |
| 17 | 17 | import remarkGfm from "remark-gfm"; | |
| 18 | 18 | ||
| 19 | + | import { Button } from "./ui/button"; | |
| 19 | 20 | import { Checkbox } from "./ui/checkbox"; | |
| 20 | 21 | import { WeightedLru } from "../lib/content-cache"; | |
| 21 | 22 | import { type AlertKind, G1T_MENTION_HREF, type MarkdownRepo, rehypeAlerts, rehypeReferences } from "../lib/markdown-plugins"; | |
| ⋯ | |||
| 153 | 154 | </pre> | |
| 154 | 155 | <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"> | |
| 155 | 156 | {language && <span className="font-mono text-[0.6875rem] text-faint">{language}</span>} | |
| 156 | − | <button | |
| 157 | + | <Button | |
| 157 | 158 | type="button" | |
| 158 | 159 | aria-label="Copy" | |
| 159 | 160 | onClick={() => { | |
| ⋯ | |||
| 162 | 163 | setTimeout(() => setCopied(false), 1500); | |
| 163 | 164 | }); | |
| 164 | 165 | }} | |
| 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" | |
| 166 | 169 | > | |
| 167 | 170 | {copied ? <Check size={13} className="text-success" /> : <Copy size={13} />} | |
| 168 | − | </button> | |
| 171 | + | </Button> | |
| 169 | 172 | </div> | |
| 170 | 173 | </div> | |
| 171 | 174 | ); | |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.