Skip to content

Commit

Hover hints in components are the site's tooltip, not the browser's title

Truncated commit messages and run steps, check and outcome badges, assignees, branch drift, scope chips, icon buttons and the merge box's copy and re-run buttons now use Hint. What the title said only to a hover is kept for screen readers where it was the only place it lived (sr-only text, aria-hidden on what it replaces). The landing page's provider tiles drop their title: the name is printed under each.

syntaqxcommitted Parent0ead720Browse files
25 files+419−3310/25 viewed
+7−3
99
1010 import type { CommitStatus, Conclusion, LogChunk, WorkflowNote } from "@g1t/contracts";
1111
12+import { Hint } from "./ui/hint";
13+
1214 type Standing = { status: string; conclusion: Conclusion | null };
1315
1416 /** One icon for where a run, job or step stands. */
258260 return (
259261 <li key={status.context}>
260262 {path ? (
261− <Link to={path} className="flex items-center gap-2 hover:text-fg" title={status.description ?? undefined}>
262− {row}
263− </Link>
263+ <Hint label={status.description}>
264+ <Link to={path} className="flex items-center gap-2 hover:text-fg">
265+ {row}
266+ </Link>
267+ </Hint>
264268 ) : (
265269 <span className="flex items-center gap-2">{row}</span>
266270 )}
+6−7
55
66 import { Idle } from "./agents";
77 import { TimeAgo } from "./ui";
8+import { Hint } from "./ui/hint";
89
910 const ROLE: Record<RepoInstructions["files"][number]["role"], string> = {
1011 root: "Every run",
6263 <span className="ml-auto flex items-center gap-2 text-xs text-faint">
6364 {file.lastChanged && (
6465 <>
65− <Link
66− to={`${base}/commit/${file.lastChanged.commit}`}
67− className="font-mono hover:text-fg"
68− title={file.lastChanged.message}
69− >
70− {file.lastChanged.commit.slice(0, 7)}
71− </Link>
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>
7271 <span>by {file.lastChanged.author}</span>
7372 <TimeAgo at={file.lastChanged.at} />
7473 </>
+31−23
2424 import { RunAudit } from "./audit";
2525 import { STAGE_LABEL, StageDots } from "./lifecycle";
2626 import { Avatar, SubmitButton, TimeAgo } from "./ui";
27+import { Hint } from "./ui/hint";
2728 import {
2829 AlertDialog,
2930 AlertDialogAction,
259260 <RunStatusBadge status={run.status} />
260261 </div>
261262 {run.step && (
262− <p className={`mt-2.5 truncate font-mono text-xs ${active ? "text-fg/85" : "text-muted"}`} title={inlinePlain(shownStep(run.step))}>
263− {active && <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />}
264− <InlineMarkdown text={shownStep(run.step)} />
265− </p>
263+ <Hint label={inlinePlain(shownStep(run.step))}>
264+ <p className={`mt-2.5 truncate font-mono text-xs ${active ? "text-fg/85" : "text-muted"}`}>
265+ {active && <span className="mr-1.5 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />}
266+ <InlineMarkdown text={shownStep(run.step)} />
267+ </p>
268+ </Hint>
266269 )}
267270 {run.status === "failed" && run.error && <p className="mt-1.5 text-xs text-danger">{run.error}</p>}
268271 <div className="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-muted">
271274 {run.startedAt || active ? <Elapsed from={run.startedAt ?? run.createdAt} to={run.finishedAt} /> : "—"}
272275 </span>
273276 {cost && (
274− <span className="flex items-center gap-1" title={active ? "So far, as the harness reports it" : "As the harness reported it"}>
275− <Coins size={12} />
276− {cost}
277− {active && " so far"}
278− </span>
277+ <Hint label={active ? "So far, as the harness reports it" : "As the harness reported it"}>
278+ <span className="flex items-center gap-1">
279+ <Coins size={12} />
280+ {cost}
281+ {active && " so far"}
282+ </span>
283+ </Hint>
279284 )}
280285 {run.model && <span className="font-mono">{run.model}</span>}
281286 {run.startedBy && <span>started by {run.startedBy}</span>}
437442 <RunStatusBadge status={current.status} />
438443 </div>
439444 {current.step && (
440− <p className="mt-3 truncate rounded-lg bg-bg px-3 py-2 font-mono text-xs text-fg/85 ring-1 ring-line" title={inlinePlain(shownStep(current.step))}>
441− {active && <span className="mr-2 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />}
442− <InlineMarkdown text={shownStep(current.step)} />
443− </p>
445+ <Hint label={inlinePlain(shownStep(current.step))}>
446+ <p className="mt-3 truncate rounded-lg bg-bg px-3 py-2 font-mono text-xs text-fg/85 ring-1 ring-line">
447+ {active && <span className="mr-2 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />}
448+ <InlineMarkdown text={shownStep(current.step)} />
449+ </p>
450+ </Hint>
444451 )}
445452 {confidence && <ConfidenceLine confidence={confidence} />}
446453 <div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-muted">
476483 export function AgentBadge({ run }: { run: AgentRun | undefined }) {
477484 if (!run) return null;
478485 return (
479− <span
480− title={run.step ? inlinePlain(shownStep(run.step)) : undefined}
481− className="inline-flex shrink-0 items-center gap-1 rounded-full border border-merged/40 bg-merged/10 px-2 py-0.5 text-xs text-merged"
482− >
483− <Loader2 size={11} className="animate-spin" />
484− {RUN_KIND_LABEL[run.kind]}
485− </span>
486+ <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-merged/10 px-2 py-0.5 text-xs text-merged">
488+ <Loader2 size={11} className="animate-spin" />
489+ {RUN_KIND_LABEL[run.kind]}
490+ </span>
491+ </Hint>
486492 );
487493 }
488494
529535 const run = data?.runs.find((run) => run.number === number && isActiveRun(run.status));
530536 if (!run?.step) return null;
531537 return (
532− <span className="mt-0.5 block truncate font-mono text-[0.6875rem] text-faint" title={inlinePlain(shownStep(run.step))}>
533− {RUN_KIND_LABEL[run.kind]}: <InlineMarkdown text={shownStep(run.step)} />
534− </span>
538+ <Hint label={inlinePlain(shownStep(run.step))}>
539+ <span className="mt-0.5 block truncate font-mono text-[0.6875rem] text-faint">
540+ {RUN_KIND_LABEL[run.kind]}: <InlineMarkdown text={shownStep(run.step)} />
541+ </span>
542+ </Hint>
535543 );
536544 }
+26−16
1111
1212 import { actionLabel, actorLabel, ruleLabel, targetLabel } from "../lib/audit";
1313 import { Avatar, TimeAgo } from "./ui";
14+import { Hint } from "./ui/hint";
1415 import { Loading, SkeletonRows } from "./ui/skeleton";
1516
1617 function clock(at: string): string {
2122 export function OutcomeMark({ entry }: { entry: AuditEntry }) {
2223 const denied = entry.outcome === "denied";
2324 return (
24− <span
25− title={`${entry.outcome}: ${ruleLabel(entry.rule)} (${entry.rule})`}
26− className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs ${
27− denied ? "bg-danger/10 text-danger ring-1 ring-danger/30" : "bg-raised text-muted ring-1 ring-line"
28− }`}
29− >
30− {denied ? <ShieldAlert size={11} /> : <ShieldCheck size={11} />}
31− {entry.outcome}
32− </span>
25+ <Hint label={`${entry.outcome}: ${ruleLabel(entry.rule)} (${entry.rule})`}>
26+ <span
27+ className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs ${
28+ denied ? "bg-danger/10 text-danger ring-1 ring-danger/30" : "bg-raised text-muted ring-1 ring-line"
29+ }`}
30+ >
31+ {denied ? <ShieldAlert size={11} /> : <ShieldCheck size={11} />}
32+ {entry.outcome}
33+ </span>
34+ </Hint>
3335 );
3436 }
3537
6971 <span className="truncate font-mono text-xs text-muted">{targetLabel(entry)}</span>
7072 </div>
7173 <p className="mt-1 flex flex-wrap gap-x-3 text-xs text-faint">
72− <span title={entry.rule}>{ruleLabel(entry.rule)}</span>
74+ <Hint label={entry.rule}>
75+ <span>{ruleLabel(entry.rule)}</span>
76+ </Hint>
7377 <span>{entry.surface.toUpperCase()}</span>
7478 {entry.result && entry.result !== "ok" && <span>result: {entry.result}</span>}
7579 {entry.runId && entry.repo && (
8387 </Link>
8488 )}
8589 {entry.credentialId && (
86− <span className="font-mono" title="Credential">
87− {entry.credentialId}
88− </span>
90+ <Hint label="Credential">
91+ <span className="font-mono">
92+ <span className="sr-only">Credential </span>
93+ {entry.credentialId}
94+ </span>
95+ </Hint>
8996 )}
90− <span className="font-mono" title="Request id">
91− {entry.requestId}
92− </span>
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>
93103 </p>
94104 {entry.outcome === "denied" && entry.message && <p className="mt-1 text-xs text-danger">{entry.message}</p>}
95105 </div>
+13−10
33 import { useFetcher } from "react-router";
44
55 import { ErrorText } from "./ui";
6+import { Hint } from "./ui/hint";
67 import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "./ui/combobox";
78 import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
89
4748 if (next && names.state === "idle" && !names.data) names.load(branchesUrl);
4849 }}
4950 >
50− <PopoverTrigger asChild>
51− <button
52− type="button"
53− title="Change the branch it merges into"
54− 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"
55− >
56− {shown}
57− <Pencil size={12} className="text-faint group-hover:text-fg" />
58− </button>
59− </PopoverTrigger>
51+ <Hint label="Change the branch it merges into">
52+ <PopoverTrigger asChild>
53+ <button
54+ 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+ >
57+ {shown}
58+ <Pencil size={12} className="text-faint group-hover:text-fg" />
59+ <span className="sr-only">Change the branch it merges into</span>
60+ </button>
61+ </PopoverTrigger>
62+ </Hint>
6063 <PopoverContent align="start" className="w-72 p-0">
6164 <Command>
6265 <CommandInput placeholder="Find a branch to merge into" />
+44−38
77 import { CheckBadge } from "./checks";
88 import { host } from "./deploy";
99 import { Avatar, TimeAgo } from "./ui";
10+import { Hint } from "./ui/hint";
11+
12+function drift({ ahead, behind }: Drift, main: string): string {
13+ return `${ahead} ${ahead === 1 ? "commit" : "commits"} ahead of ${main}, ${behind} behind`;
14+}
1015
1116 export type ActiveBranch = {
1217 name: string;
3641 <Avatar name={branch.commit.author} size={13} />
3742 <span className="shrink-0">{branch.commit.author}</span>
3843 <span className="text-faint">·</span>
39− <Link
40− to={`${base}/commit/${branch.commit.hash}`}
41− className="min-w-0 truncate hover:text-fg"
42− title={branch.commit.message}
43− >
44− {branch.commit.message}
45− </Link>
44+ <Hint label={branch.commit.message}>
45+ <Link to={`${base}/commit/${branch.commit.hash}`} className="min-w-0 truncate hover:text-fg">
46+ {branch.commit.message}
47+ </Link>
48+ </Hint>
4649 <span className="shrink-0 text-faint">
4750 · <TimeAgo at={branch.commit.at} />
4851 </span>
5154 </span>
5255 <span className="ml-6.5 flex shrink-0 flex-wrap items-center gap-2 text-xs sm:ml-0">
5356 {branch.drift && (
54− <span
55− 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"
56− title={`${branch.drift.ahead} ${branch.drift.ahead === 1 ? "commit" : "commits"} ahead of ${main}, ${branch.drift.behind} behind`}
57− >
58− <span className="inline-flex items-center gap-0.5">
59− <ArrowUp size={11} className={branch.drift.ahead > 0 ? "text-accent" : "text-faint"} />
60− {branch.drift.ahead}
57+ <Hint label={drift(branch.drift, main)}>
58+ <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">
59+ <span className="sr-only">{drift(branch.drift, main)}</span>
60+ <span className="inline-flex items-center gap-0.5" aria-hidden>
61+ <ArrowUp size={11} className={branch.drift.ahead > 0 ? "text-accent" : "text-faint"} />
62+ {branch.drift.ahead}
63+ </span>
64+ <span className="inline-flex items-center gap-0.5" aria-hidden>
65+ <ArrowDown size={11} className={branch.drift.behind > 0 ? "text-warn" : "text-faint"} />
66+ {branch.drift.behind}
67+ </span>
6168 </span>
62− <span className="inline-flex items-center gap-0.5">
63− <ArrowDown size={11} className={branch.drift.behind > 0 ? "text-warn" : "text-faint"} />
64− {branch.drift.behind}
65− </span>
66− </span>
69+ </Hint>
6770 )}
6871 {branch.pull ? (
69− <Link
70− to={`${base}/pull/${branch.pull.number}`}
71− 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"
72− title={branch.pull.title}
73− >
74− <GitPullRequest size={12} className={branch.pull.draft ? "text-faint" : "text-accent"} />#{branch.pull.number}
75− <CheckBadge status={branch.pull.checkStatus} />
76− </Link>
72+ <Hint label={branch.pull.title}>
73+ <Link
74+ to={`${base}/pull/${branch.pull.number}`}
75+ 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"
76+ >
77+ <GitPullRequest size={12} className={branch.pull.draft ? "text-faint" : "text-accent"} />#{branch.pull.number}
78+ <span className="sr-only">{branch.pull.title}</span>
79+ <CheckBadge status={branch.pull.checkStatus} />
80+ </Link>
81+ </Hint>
7782 ) : branch.drift?.ahead === 0 ? (
7883 // Nothing here that the default branch lacks: no pull request to open.
79− <span className="px-1.5 py-0.5 text-faint" title={`Every commit on ${branch.name} is already on ${main}.`}>
80− Nothing to merge
81− </span>
84+ <Hint label={`Every commit on ${branch.name} is already on ${main}.`}>
85+ <span className="px-1.5 py-0.5 text-faint">Nothing to merge</span>
86+ </Hint>
8287 ) : (
8388 <Link
8489 to={`${base}/pulls/new?branch=${encodeURIComponent(branch.name)}`}
8893 </Link>
8994 )}
9095 {branch.preview && (
91− <a
92− href={branch.preview}
93− 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"
94− title={host(branch.preview)}
95− >
96− <Globe size={12} className="shrink-0 text-merged" />
97− Preview
98− </a>
96+ <Hint label={host(branch.preview)}>
97+ <a
98+ href={branch.preview}
99+ 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"
100+ >
101+ <Globe size={12} className="shrink-0 text-merged" />
102+ Preview
103+ </a>
104+ </Hint>
99105 )}
100106 </span>
101107 </li>
+14−12
44
55 import { type ChecklistItem, dismiss, isDismissed, progress } from "../lib/checklist";
66 import { cn } from "../lib/cn";
7+import { Hint } from "./ui/hint";
78
89 const storage = () => (typeof window === "undefined" ? null : window.localStorage);
910 const listeners = new Set<() => void>();
4849 <span className="hidden h-1.5 w-24 overflow-hidden rounded-full bg-line sm:block" aria-hidden>
4950 <span className="block h-full rounded-full bg-accent" style={{ width: `${(done / total) * 100}%` }} />
5051 </span>
51− <button
52− type="button"
53− onClick={() => {
54− dismiss(storage, base);
55− for (const listener of listeners) listener();
56− }}
57− className="ml-auto rounded-md p-1 text-faint transition-colors hover:bg-raised hover:text-fg"
58− aria-label="Dismiss the checklist for this project"
59− title="Dismiss for this project"
60− >
61− <X size={14} />
62− </button>
52+ <Hint label="Dismiss for this project">
53+ <button
54+ type="button"
55+ onClick={() => {
56+ dismiss(storage, base);
57+ 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+ >
62+ <X size={14} />
63+ </button>
64+ </Hint>
6365 </div>
6466 <ol className="grid border-t border-line sm:grid-cols-2 lg:grid-cols-3">
6567 {items.map((item) => (
+8−4
22
33 import type { CheckStatus } from "@g1t/contracts";
44
5+import { Hint } from "./ui/hint";
6+
57 /**
68 * What a pull request's own check status says. A pull request's checks are
79 * the workflows run on it (see the merge box); this is set when the merge
2729 export function CheckBadge({ status }: { status: CheckStatus | null }) {
2830 if (!status) return null;
2931 return (
30− <span className="flex shrink-0 items-center gap-1 text-xs text-muted" title={LABELS[status]}>
31− <CheckIcon status={status} size={13} />
32− <span className="sr-only">{LABELS[status]}</span>
33− </span>
32+ <Hint label={LABELS[status]}>
33+ <span className="flex shrink-0 items-center gap-1 text-xs text-muted">
34+ <CheckIcon status={status} size={13} />
35+ <span className="sr-only">{LABELS[status]}</span>
36+ </span>
37+ </Hint>
3438 );
3539 }
+22−19
4646
4747 import { InlineMarkdown } from "./inline-markdown";
4848 import { SubmitButton, TimeAgo } from "./ui";
49+import { Hint } from "./ui/hint";
4950 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
5051 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
5152
204205 Keep
205206 </button>
206207 <EditAndKeep memory={memory} action={action} />
207− <button
208− type="button"
209− aria-label="Dismiss"
210− title="Dismiss: never suggested again in these words"
211− onClick={() => fetcher.submit({ intent: "dismiss", id: memory.id }, { method: "post", action })}
212− 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"
213− >
214− <X size={12} />
215− Dismiss
216− </button>
208+ <Hint label="Dismiss: never suggested again in these words">
209+ <button
210+ type="button"
211+ aria-label="Dismiss"
212+ 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"
214+ >
215+ <X size={12} />
216+ Dismiss
217+ </button>
218+ </Hint>
217219 </div>
218220 </li>
219221 );
498500 <input type="hidden" name="intent" value="fix" />
499501 <input type="hidden" name="project" value={project} />
500502 <input type="hidden" name="rule" value={rule.rule} />
501− <SubmitButton
502− fetcher={fetcher}
503− pending="Opening…"
504− title={`Opens “${rule.fix.title}” and puts an agent on it`}
505− 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"
506− >
507− <Bot size={12} />
508− Fix with an agent
509− </SubmitButton>
503+ <Hint label={`Opens “${rule.fix.title}” and puts an agent on it`}>
504+ <SubmitButton
505+ fetcher={fetcher}
506+ 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"
508+ >
509+ <Bot size={12} />
510+ Fix with an agent
511+ </SubmitButton>
512+ </Hint>
510513 </fetcher.Form>
511514 )}
512515 </li>
+4−1
3939 } from "../lib/dependency-updates";
4040 import { TimeAgo } from "./ui";
4141 import { Badge, type BadgeTone } from "./ui/badge";
42+import { Hint } from "./ui/hint";
4243
4344 type Done = { ok: boolean; error?: string } | undefined;
4445
110111 </span>
111112 )}
112113 {entry.nextRunAt && (
113− <span title={utc(entry.nextRunAt)}>Next check {timeUntil(entry.nextRunAt)}</span>
114+ <Hint label={utc(entry.nextRunAt)}>
115+ <span>Next check {timeUntil(entry.nextRunAt)}</span>
116+ </Hint>
114117 )}
115118 <span>
116119 {entry.lastCheckedAt ? (
+11−6
66
77 import { SubmitButton } from "./ui";
88 import { Badge, type BadgeTone } from "./ui/badge";
9+import { Hint } from "./ui/hint";
910 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu";
1011 import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from "./ui/sheet";
1112 import { Skeleton } from "./ui/skeleton";
124125 {item.doneAt ? (
125126 <fetcher.Form method="post" action={ACTION}>
126127 <Fields intent="undone" id={item.id} />
127− <SubmitButton fetcher={fetcher} icon aria-label="Move back to the inbox" title="Move back to the inbox" className={ICON_BUTTON}>
128− <Undo2 size={14} />
129− </SubmitButton>
128+ <Hint label="Move back to the inbox">
129+ <SubmitButton fetcher={fetcher} icon aria-label="Move back to the inbox" className={ICON_BUTTON}>
130+ <Undo2 size={14} />
131+ </SubmitButton>
132+ </Hint>
130133 </fetcher.Form>
131134 ) : (
132135 <fetcher.Form method="post" action={ACTION}>
133136 <Fields intent="done" id={item.id} />
134− <SubmitButton fetcher={fetcher} icon aria-label="Done" title="Done" className={ICON_BUTTON}>
135− <Check size={14} />
136− </SubmitButton>
137+ <Hint label="Done">
138+ <SubmitButton fetcher={fetcher} icon aria-label="Done" className={ICON_BUTTON}>
139+ <Check size={14} />
140+ </SubmitButton>
141+ </Hint>
137142 </fetcher.Form>
138143 )}
139144 <DropdownMenu>
+1−5
528528 <div className="space-y-6 lg:order-1">
529529 <div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
530530 {STACK.map((provider) => (
531− <div
532− key={provider}
533− className="flex flex-col items-center gap-2 rounded-xl bg-surface px-2 py-3 ring-1 ring-line"
534− title={PROVIDERS[provider].label}
535− >
531+ <div key={provider} className="flex flex-col items-center gap-2 rounded-xl bg-surface px-2 py-3 ring-1 ring-line">
536532 <ProviderMark provider={provider} size={30} />
537533 <span className="w-full text-center text-xs leading-tight text-muted">{PROVIDERS[provider].label}</span>
538534 </div>
+12−10
2323 } from "./ui/alert-dialog";
2424 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
2525 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
26+import { Hint } from "./ui/hint";
2627 import { Switch } from "./ui/switch";
2728
2829 const KIND: Record<MemoryKind, { label: string; about: string; tone: string }> = {
167168 </div>
168169 {editable && (
169170 <div className="flex shrink-0 items-start gap-0.5">
170− <button
171− type="button"
172− aria-label={pinned ? "Unpin" : "Pin"}
173− title={pinned ? "Unpin" : "Pin: given to every agent first"}
174− onClick={() => fetcher.submit({ intent: "update", id: memory.id, pinned: String(!pinned) }, { method: "post", action })}
175− disabled={fetcher.state !== "idle"}
176− className="rounded p-1 text-faint transition-colors hover:bg-raised hover:text-fg disabled:opacity-50"
177− >
178− {pinned ? <PinOff size={13} /> : <Pin size={13} />}
179− </button>
171+ <Hint label={pinned ? "Unpin" : "Pin: given to every agent first"}>
172+ <button
173+ type="button"
174+ aria-label={pinned ? "Unpin" : "Pin"}
175+ onClick={() => fetcher.submit({ intent: "update", id: memory.id, pinned: String(!pinned) }, { method: "post", action })}
176+ disabled={fetcher.state !== "idle"}
177+ className="rounded p-1 text-faint transition-colors hover:bg-raised hover:text-fg disabled:opacity-50"
178+ >
179+ {pinned ? <PinOff size={13} /> : <Pin size={13} />}
180+ </button>
181+ </Hint>
180182 <EditMemory memory={memory} action={action} />
181183 <AlertDialog>
182184 <AlertDialogTrigger aria-label="Forget" className="rounded p-1 text-faint transition-colors hover:bg-raised hover:text-danger">
+37−33
2929 import { duration } from "./actions";
3030 import { Elapsed, type Live, useRuns } from "./agents";
3131 import { Button, CopyLine, ErrorText, SubmitButton, TimeAgo } from "./ui";
32+import { Hint } from "./ui/hint";
3233 import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
3334 import { SkeletonLine } from "./ui/skeleton";
3435
128129 }
129130 return (
130131 <div className="relative">
131− <button
132− type="button"
133− aria-label="Copy the output"
134− title="Copy the output"
135− className="absolute top-1.5 right-3 z-10 rounded-md border border-line bg-surface p-1.5 text-faint transition-colors hover:text-fg"
136− onClick={() => {
137− void navigator.clipboard.writeText(stripAnsi(body));
138− setCopied(true);
139− setTimeout(() => setCopied(false), 1500);
140− }}
141− >
142− {copied ? <Check size={13} className="text-accent" /> : <Copy size={13} />}
143− </button>
132+ <Hint label="Copy the output">
133+ <button
134+ type="button"
135+ aria-label="Copy the output"
136+ className="absolute top-1.5 right-3 z-10 rounded-md border border-line bg-surface p-1.5 text-faint transition-colors hover:text-fg"
137+ onClick={() => {
138+ void navigator.clipboard.writeText(stripAnsi(body));
139+ setCopied(true);
140+ setTimeout(() => setCopied(false), 1500);
141+ }}
142+ >
143+ {copied ? <Check size={13} className="text-accent" /> : <Copy size={13} />}
144+ </button>
145+ </Hint>
144146 {cut && (
145147 <p className="border-b border-line px-3 py-1.5 text-xs text-faint">
146148 Showing the end of the output. Earlier lines were not kept.
416418 <Form method="post" key={status.context}>
417419 <input type="hidden" name="action" value="rerun-workflow" />
418420 <input type="hidden" name="run" value={runIdOf(status) ?? ""} />
419− <SubmitButton
420− variant="quiet"
421− match={{ action: "rerun-workflow", run: runIdOf(status) ?? "" }}
422− pending="Re-running…"
423− title={`Re-run the failed jobs of ${status.context}`}
424− >
425− <RotateCw size={13} />
426− Re-run failed jobs{failedRuns.length > 1 ? ` of ${status.context}` : ""}
427− </SubmitButton>
421+ <Hint label={`Re-run the failed jobs of ${status.context}`}>
422+ <SubmitButton
423+ variant="quiet"
424+ match={{ action: "rerun-workflow", run: runIdOf(status) ?? "" }}
425+ pending="Re-running…"
426+ >
427+ <RotateCw size={13} />
428+ Re-run failed jobs{failedRuns.length > 1 ? ` of ${status.context}` : ""}
429+ </SubmitButton>
430+ </Hint>
428431 </Form>
429432 ))}
430433 </div>
850853 is pushed.
851854 </p>
852855 {update.paths.length > 0 && (
853− <p className="mt-1 truncate font-mono text-xs text-faint" title={update.paths.join(", ")}>
854− {update.paths.slice(0, 5).join(", ")}
855− {update.paths.length > 5 && ` and ${update.paths.length - 5} more`}
856− </p>
856+ <Hint label={update.paths.join(", ")}>
857+ <p className="mt-1 truncate font-mono text-xs text-faint">
858+ {update.paths.slice(0, 5).join(", ")}
859+ {update.paths.length > 5 && ` and ${update.paths.length - 5} more`}
860+ </p>
861+ </Hint>
857862 )}
858− <p
859− className="mt-2 truncate rounded-lg bg-bg px-3 py-2 font-mono text-xs text-fg/85 ring-1 ring-line"
860− title={run?.step ?? undefined}
861− >
862− <span className="mr-2 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
863− {run?.step ?? (run ? "Starting a sandbox…" : "Waiting for a sandbox…")}
864− </p>
863+ <Hint label={run?.step}>
864+ <p className="mt-2 truncate rounded-lg bg-bg px-3 py-2 font-mono text-xs text-fg/85 ring-1 ring-line">
865+ <span className="mr-2 inline-block size-1.5 animate-pulse rounded-full bg-merged align-middle" />
866+ {run?.step ?? (run ? "Starting a sandbox…" : "Waiting for a sandbox…")}
867+ </p>
868+ </Hint>
865869 <p className="mt-2 flex flex-wrap items-center gap-x-3 text-xs text-muted">
866870 <Elapsed from={new Date(startedAt).toISOString()} />
867871 <Link to={session} className="hover:text-fg">
+11−8
3636 import { Unavailable } from "./mission";
3737 import { TokenUsagePanel } from "./token-usage";
3838 import { Avatar, SubmitButton, TimeAgo } from "./ui";
39+import { Hint } from "./ui/hint";
3940 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu";
4041 import type { Loaded } from "../routes/home";
4142
413414
414415 function Stat({ label, value, hint, dot, title }: { label: string; value: string; hint: ReactNode; dot?: string; title?: string }) {
415416 return (
416− <div className="min-w-0 bg-surface px-4 py-3.5 sm:px-5 sm:py-4" title={title}>
417− <p className="flex items-center gap-1.5 truncate text-xs text-muted">
418− {dot && <span className={cn("size-1.5 shrink-0 rounded-full", dot)} />}
419− {label}
420− </p>
421− <p className="mt-1 font-display text-2xl font-semibold tracking-tight tabular-nums sm:text-[1.75rem]">{value}</p>
422− <p className="mt-0.5 truncate text-xs text-faint">{hint}</p>
423− </div>
417+ <Hint label={title}>
418+ <div className="min-w-0 bg-surface px-4 py-3.5 sm:px-5 sm:py-4">
419+ <p className="flex items-center gap-1.5 truncate text-xs text-muted">
420+ {dot && <span className={cn("size-1.5 shrink-0 rounded-full", dot)} />}
421+ {label}
422+ </p>
423+ <p className="mt-1 font-display text-2xl font-semibold tracking-tight tabular-nums sm:text-[1.75rem]">{value}</p>
424+ <p className="mt-0.5 truncate text-xs text-faint">{hint}</p>
425+ </div>
426+ </Hint>
424427 );
425428 }
426429
+22−14
3333 import { cn } from "../lib/cn";
3434 import { type ActivityGroup, type Need, type NeedKind, type Verb, isAgent, sparkPoints } from "../lib/mission";
3535 import { Avatar, TimeAgo } from "./ui";
36+import { Hint } from "./ui/hint";
3637 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
3738
3839 // --- Panels -------------------------------------------------------------------
127128 const look = NEED[need.kind];
128129 return (
129130 <li key={need.key} className="flex items-start gap-3 px-4 py-3">
130− <span className={cn("mt-0.5 shrink-0", look.tone)} title={look.label}>
131− {look.icon}
132− </span>
131+ <Hint label={look.label}>
132+ <span className={cn("mt-0.5 shrink-0", look.tone)}>
133+ {look.icon}
134+ <span className="sr-only">{look.label}</span>
135+ </span>
136+ </Hint>
133137 <span className="min-w-0 grow">
134138 <Link to={need.to} prefetch="intent" className="block truncate text-sm font-medium hover:text-accent">
135139 {need.title}
447451 </span>
448452 )}
449453 </div>
450− <p className="mt-1.5 truncate text-[0.6875rem] text-faint" title={hint}>
451− {hint}
452− </p>
454+ <Hint label={hint}>
455+ <p className="mt-1.5 truncate text-[0.6875rem] text-faint">{hint}</p>
456+ </Hint>
453457 </>
454458 );
455459 const box = "block rounded-xl border border-line bg-surface p-4 transition-colors";
491495 {Array.from({ length: slots - shown.length }, (_, i) => (
492496 <span key={`empty-${i}`} className="h-2 flex-1 rounded-sm bg-line/60" />
493497 ))}
494− {shown.map((build) => (
495− <Link
496− key={build.id}
497− to={`${base}/deployments/${build.id}`}
498− title={`${build.kind === "production" ? "Production" : "Preview"} · ${build.commit.slice(0, 7)} · ${build.status} · ${new Date(build.createdAt).toLocaleString("en-US")}`}
499− className={cn("flex-1 rounded-sm transition-opacity hover:opacity-80", STRIP_TONE[build.status], build.kind === "production" ? "h-8" : "h-5")}
500− />
501− ))}
498+ {shown.map((build) => {
499+ const about = `${build.kind === "production" ? "Production" : "Preview"} · ${build.commit.slice(0, 7)} · ${build.status} · ${new Date(build.createdAt).toLocaleString("en-US")}`;
500+ return (
501+ <Hint key={build.id} label={about}>
502+ <Link
503+ to={`${base}/deployments/${build.id}`}
504+ aria-label={about}
505+ className={cn("flex-1 rounded-sm transition-opacity hover:opacity-80", STRIP_TONE[build.status], build.kind === "production" ? "h-8" : "h-5")}
506+ />
507+ </Hint>
508+ );
509+ })}
502510 </div>
503511 );
504512 }
+4−3
1010 import { type Connection, MODEL_TASKS, type ModelRoute, type ModelTask, type Provider, PROVIDERS } from "@g1t/contracts";
1111
1212 import { Field, Input, SubmitButton } from "./ui";
13+import { Hint } from "./ui/hint";
1314 import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectSeparator, SelectTrigger, SelectValue } from "./ui/select";
1415
1516 type Group = "labs" | "platforms" | "any";
216217 <ProviderMark provider={provider} size={28} />
217218 <span className="min-w-0 grow">
218219 <span className="block truncate text-sm font-medium">{PROVIDERS[provider].label}</span>
219− <span className="block truncate text-xs text-muted" title={blurb(provider)}>
220− {blurb(provider)}
221− </span>
220+ <Hint label={blurb(provider)}>
221+ <span className="block truncate text-xs text-muted">{blurb(provider)}</span>
222+ </Hint>
222223 </span>
223224 <ChevronRight size={14} className="shrink-0 text-faint transition-transform group-hover:translate-x-0.5" />
224225 </Link>
+6−7
1515 import { Avatar, CopyLine, TimeAgo, notACredential } from "./ui";
1616 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "./ui/dropdown-menu";
1717 import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
18+import { Hint } from "./ui/hint";
1819 import { SkeletonLine } from "./ui/skeleton";
1920
2021 function encodePath(path: string): string {
256257 </Link>
257258 <span className="hidden min-w-0 sm:block">
258259 {commit ? (
259− <Link
260− to={`${base}/commit/${commit.hash}`}
261− className="block animate-fade-in truncate text-muted hover:text-fg hover:underline"
262− title={commit.message.split("\n")[0]}
263− >
264− {commit.message.split("\n")[0]}
265− </Link>
260+ <Hint label={commit.message.split("\n")[0]}>
261+ <Link to={`${base}/commit/${commit.hash}`} className="block animate-fade-in truncate text-muted hover:text-fg hover:underline">
262+ {commit.message.split("\n")[0]}
263+ </Link>
264+ </Hint>
266265 ) : last === undefined ? (
267266 <SkeletonLine width="10rem" barClassName="max-w-full" />
268267 ) : null}
+7−3
1414 import type { SecretsAction, SecretsData } from "../lib/secrets.server";
1515 import { ButtonLink, EmptyState, ErrorText, SubmitButton, TimeAgo } from "./ui";
1616 import { CheckboxOption } from "./ui/checkbox";
17+import { Hint } from "./ui/hint";
1718 import { RadioCard, RadioGroup, RadioOption } from "./ui/radio-group";
1819 import { Select, SelectContent, SelectItem, SelectSeparator, SelectTrigger, SelectValue } from "./ui/select";
1920
183184 ) : (
184185 <>
185186 {row.projects.length > 0 && (
186− <span className="mr-1 text-xs text-faint" title={row.projects.join(", ")}>
187− {row.projects.length} {row.projects.length === 1 ? "project" : "projects"}
188− </span>
187+ <Hint label={row.projects.join(", ")}>
188+ <span className="mr-1 text-xs text-faint">
189+ {row.projects.length} {row.projects.length === 1 ? "project" : "projects"}
190+ <span className="sr-only">: {row.projects.join(", ")}</span>
191+ </span>
192+ </Hint>
189193 )}
190194 {manage && (
191195 <>
+12−10
2525 import { SeverityBadge } from "./security";
2626 import { TimeAgo } from "./ui";
2727 import { Badge } from "./ui/badge";
28+import { Hint } from "./ui/hint";
2829 import { RadioGroup, RadioOption } from "./ui/radio-group";
2930 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
3031
390391 }
391392 return (
392393 <span className="flex flex-col items-end gap-1">
393− <button
394− type="button"
395− disabled={fetcher.state !== "idle"}
396− onClick={() => fetcher.submit({ intent: "fix", id }, { method: "post", action })}
397− className={`${SMALL_BUTTON} inline-flex items-center gap-1.5`}
398− title="Opens an issue assigned to g1t; its run is charged as agent usage"
399− >
400− <Sparkles size={13} />
401− {fetcher.state !== "idle" ? "Opening…" : "Fix with g1t"}
402− </button>
394+ <Hint label="Opens an issue assigned to g1t; its run is charged as agent usage">
395+ <button
396+ type="button"
397+ disabled={fetcher.state !== "idle"}
398+ onClick={() => fetcher.submit({ intent: "fix", id }, { method: "post", action })}
399+ className={`${SMALL_BUTTON} inline-flex items-center gap-1.5`}
400+ >
401+ <Sparkles size={13} />
402+ {fetcher.state !== "idle" ? "Opening…" : "Fix with g1t"}
403+ </button>
404+ </Hint>
403405 {fetcher.data?.error && <span className="text-xs text-danger">{fetcher.data.error}</span>}
404406 </span>
405407 );
+22−9
5454 } from "../lib/security-alerts";
5555 import { Avatar, TimeAgo } from "./ui";
5656 import { Badge, type BadgeTone } from "./ui/badge";
57+import { Hint } from "./ui/hint";
5758 import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
5859 import { RadioGroup, RadioOption } from "./ui/radio-group";
5960
345346 <Link to={`${base}/security/secret-scanning/${finding.id}`} className="text-sm font-medium first-letter:uppercase hover:underline">
346347 {finding.label}
347348 </Link>
348− <Badge tone={badge.tone} title={badge.about}>
349− {badge.label}
350− </Badge>
351− {finding.testValue && <Badge title={finding.testValue}>Likely test value</Badge>}
349+ <Hint label={badge.about}>
350+ <Badge tone={badge.tone}>{badge.label}</Badge>
351+ </Hint>
352+ {finding.testValue && (
353+ <Hint label={finding.testValue}>
354+ <Badge>Likely test value</Badge>
355+ </Hint>
356+ )}
352357 {finding.validity === "active" && (
353− <Badge tone="danger" title="Its issuer says it still works">
354− Active
355− </Badge>
358+ <Hint label="Its issuer says it still works">
359+ <Badge tone="danger">Active</Badge>
360+ </Hint>
356361 )}
357− {finding.validity === "inactive" && <Badge title="Its issuer refused it: revoked or expired">Inactive</Badge>}
358− {finding.bypass && <Badge tone="warn" title={`Bypassed by ${finding.bypass.by}`}>Bypassed</Badge>}
362+ {finding.validity === "inactive" && (
363+ <Hint label="Its issuer refused it: revoked or expired">
364+ <Badge>Inactive</Badge>
365+ </Hint>
366+ )}
367+ {finding.bypass && (
368+ <Hint label={`Bypassed by ${finding.bypass.by}`}>
369+ <Badge tone="warn">Bypassed</Badge>
370+ </Hint>
371+ )}
359372 {finding.state === "dismissed" && finding.dismissedReason && <Badge>{dismissLabel(finding.dismissedReason)}</Badge>}
360373 </div>
361374 <p className="mt-1 truncate font-mono text-xs">
+47−46
2626 normalizeScopes,
2727 } from "../lib/token-scopes";
2828 import { Badge } from "./ui/badge";
29+import { Hint } from "./ui/hint";
2930 import { CONTROL } from "./ui/input";
3031
3132 // Choosing what a token or an application may do: a classic checklist.
4748 const by = impliedBy(ticked, scope);
4849 const checked = by !== null || ticked.includes(scope);
4950 return (
50− <label
51− className={cn("flex min-w-0 items-start gap-2.5 py-1", by ? "cursor-default" : "cursor-pointer")}
52− title={by ? `Included in ${by}` : undefined}
53− >
54− <input
55− type="checkbox"
56− name="scope"
57− value={scope}
58− checked={checked}
59− disabled={by !== null}
60− onChange={(event) => onToggle(scope, event.target.checked)}
61− className={cn("mt-0.5 size-4 shrink-0", isDangerous(scope) ? "accent-danger" : "accent-accent")}
62− />
63− <span className="min-w-0">
64− <span className={cn("block font-mono text-[0.8125rem]", isDangerous(scope) ? "text-danger" : "text-fg", by && "opacity-60")}>
65− {scope}
51+ <Hint label={by ? `Included in ${by}` : undefined}>
52+ <label className={cn("flex min-w-0 items-start gap-2.5 py-1", by ? "cursor-default" : "cursor-pointer")}>
53+ <input
54+ type="checkbox"
55+ name="scope"
56+ value={scope}
57+ checked={checked}
58+ disabled={by !== null}
59+ onChange={(event) => onToggle(scope, event.target.checked)}
60+ className={cn("mt-0.5 size-4 shrink-0", isDangerous(scope) ? "accent-danger" : "accent-accent")}
61+ />
62+ <span className="min-w-0">
63+ <span className={cn("block font-mono text-[0.8125rem]", isDangerous(scope) ? "text-danger" : "text-fg", by && "opacity-60")}>
64+ {scope}
65+ </span>
66+ <span className="block text-xs leading-snug text-faint">{describeScope(scope)}</span>
67+ {by && <span className="sr-only">Included in {by}</span>}
6668 </span>
67− <span className="block text-xs leading-snug text-faint">{describeScope(scope)}</span>
68− </span>
69− </label>
69+ </label>
70+ </Hint>
7071 );
7172 }
7273
126127 <div className="flex flex-wrap items-center gap-1.5">
127128 <span className="mr-1 text-sm font-medium text-muted">Scopes</span>
128129 {PRESETS.filter((option) => allowFull || option.id !== "full").map((option) => (
129− <button
130− key={option.id}
131− type="button"
132− aria-pressed={preset === option.id}
133− title={option.description}
134− onClick={() => choosePreset(option.id)}
135− className={cn(
136− "rounded-full border px-2.5 py-0.5 text-xs transition-colors",
137− preset === option.id
138− ? option.id === "full"
139− ? "border-danger/50 bg-danger/10 text-danger"
140− : "border-accent/50 bg-accent/10 text-accent"
141− : "border-line text-muted hover:border-line-strong hover:text-fg",
142− )}
143− >
144− {option.label}
145− </button>
130+ <Hint key={option.id} label={option.description}>
131+ <button
132+ type="button"
133+ aria-pressed={preset === option.id}
134+ onClick={() => choosePreset(option.id)}
135+ className={cn(
136+ "rounded-full border px-2.5 py-0.5 text-xs transition-colors",
137+ preset === option.id
138+ ? option.id === "full"
139+ ? "border-danger/50 bg-danger/10 text-danger"
140+ : "border-accent/50 bg-accent/10 text-accent"
141+ : "border-line text-muted hover:border-line-strong hover:text-fg",
142+ )}
143+ >
144+ {option.label}
145+ </button>
146+ </Hint>
146147 ))}
147148 <span className="ml-auto text-xs text-faint">
148149 {count === null ? "Everything you can do" : count === 1 ? "1 scope" : `${count} scopes`}
241242 <div className={cn("mt-1.5 flex flex-wrap items-center gap-1.5", className)}>
242243 <Badge tone={tone}>{summary}</Badge>
243244 {scopes.map((scope) => (
244− <span
245− key={scope}
246− title={describeScope(scope)}
247− className={cn(
248− "rounded border px-1.5 py-px font-mono text-[0.6875rem]",
249− isDangerous(scope) ? "border-danger/40 text-danger" : "border-line text-muted",
250− )}
251− >
252− {scope}
253− </span>
245+ <Hint key={scope} label={describeScope(scope)}>
246+ <span
247+ className={cn(
248+ "rounded border px-1.5 py-px font-mono text-[0.6875rem]",
249+ isDangerous(scope) ? "border-danger/40 text-danger" : "border-line text-muted",
250+ )}
251+ >
252+ {scope}
253+ </span>
254+ </Hint>
254255 ))}
255256 </div>
256257 );
+16−13
33
44 import { cn } from "../lib/cn";
55 import { usd } from "../lib/mission-control";
6+import { Hint } from "./ui/hint";
67 import {
78 HEAT_LEVELS,
89 type MixPart,
109110
110111 function Tile({ label, value, hint, title }: { label: string; value: string; hint?: string; title?: string }) {
111112 return (
112− <div className="rounded-lg border border-line bg-bg/40 px-3 py-2.5" title={title}>
113− <dt className="text-[0.6875rem] text-muted">{label}</dt>
114− <dd className="mt-0.5 text-lg font-semibold tracking-tight tabular-nums">
115− {value}
116− {hint && <span className="ml-1 text-xs font-normal text-faint">{hint}</span>}
117− </dd>
118− </div>
113+ <Hint label={title}>
114+ <div className="rounded-lg border border-line bg-bg/40 px-3 py-2.5">
115+ <dt className="text-[0.6875rem] text-muted">{label}</dt>
116+ <dd className="mt-0.5 text-lg font-semibold tracking-tight tabular-nums">
117+ {value}
118+ {hint && <span className="ml-1 text-xs font-normal text-faint">{hint}</span>}
119+ </dd>
120+ </div>
121+ </Hint>
119122 );
120123 }
121124
179182 <h3 className="text-xs font-medium text-fg-soft">Token mix</h3>
180183 <div className="mt-2 flex h-2.5 gap-[2px] overflow-hidden rounded" role="img" aria-label={parts.map((p) => `${p.label} ${Math.round(p.share * 100)}%`).join(", ")}>
181184 {parts.map((part) => (
182− <span
183− key={part.key}
184− title={`${part.label}: ${compactTokens(part.tokens)} (${Math.round(part.share * 100)}%)`}
185− className="h-full first:rounded-l last:rounded-r"
186− style={{ width: `${part.share * 100}%`, minWidth: 3, background: MIX_COLOR[part.key] }}
187− />
185+ <Hint key={part.key} label={`${part.label}: ${compactTokens(part.tokens)} (${Math.round(part.share * 100)}%)`}>
186+ <span
187+ className="h-full first:rounded-l last:rounded-r"
188+ style={{ width: `${part.share * 100}%`, minWidth: 3, background: MIX_COLOR[part.key] }}
189+ />
190+ </Hint>
188191 ))}
189192 </div>
190193 <ul className="mt-2.5 grid grid-cols-2 gap-x-3 gap-y-1.5 text-[0.6875rem]">
+13−11
1111 import type { WebhooksAction, WebhooksData } from "../lib/webhooks.server";
1212 import { CopyLine, EmptyState, ErrorText, Field, Input, SubmitButton, TimeAgo } from "./ui";
1313 import { CheckboxOption } from "./ui/checkbox";
14+import { Hint } from "./ui/hint";
1415 import { RadioGroup, RadioOption } from "./ui/radio-group";
1516
1617 /** The events, in groups people recognise. */
177178 /** One webhook's action, as an icon: a spinner in its place while that action goes. */
178179 function IconButton({ intent, id, label, children }: { intent: string; id: string; label: string; children: React.ReactNode }) {
179180 return (
180− <SubmitButton
181− icon
182− name="intent"
183− value={intent}
184− match={{ id }}
185− title={label}
186− aria-label={label}
187− className="rounded-md p-2 text-muted transition-colors hover:bg-raised hover:text-fg disabled:opacity-50"
188− >
189− {children}
190− </SubmitButton>
181+ <Hint label={label}>
182+ <SubmitButton
183+ icon
184+ name="intent"
185+ value={intent}
186+ match={{ id }}
187+ aria-label={label}
188+ className="rounded-md p-2 text-muted transition-colors hover:bg-raised hover:text-fg disabled:opacity-50"
189+ >
190+ {children}
191+ </SubmitButton>
192+ </Hint>
191193 );
192194 }
193195
+23−20
1111 import { MentionTextarea } from "./mention-textarea";
1212 import { Avatar, SubmitButton, TimeAgo } from "./ui";
1313 import { CheckboxOption } from "./ui/checkbox";
14+import { Hint } from "./ui/hint";
1415
1516 export { IssueIcon, PullIcon };
1617
5758 if (people.length === 0) return null;
5859 const shown = people.slice(0, 3);
5960 return (
60− <span
61− className="flex shrink-0 items-center"
62− title={`Assigned to ${people.join(", ")}`}
63− >
64− {shown.map((name, index) => (
65− <span key={name} className={`rounded-full ring-2 ring-bg ${index > 0 ? "-ml-1.5" : ""}`}>
66− <Avatar name={name} size={18} />
67− </span>
68− ))}
69− {people.length > shown.length && (
70− <span className="ml-1 text-xs text-faint">+{people.length - shown.length}</span>
71− )}
72− </span>
61+ <Hint label={`Assigned to ${people.join(", ")}`}>
62+ <span className="flex shrink-0 items-center">
63+ <span className="sr-only">Assigned to {people.join(", ")}</span>
64+ {shown.map((name, index) => (
65+ <span key={name} className={`rounded-full ring-2 ring-bg ${index > 0 ? "-ml-1.5" : ""}`} aria-hidden>
66+ <Avatar name={name} size={18} />
67+ </span>
68+ ))}
69+ {people.length > shown.length && (
70+ <span className="ml-1 text-xs text-faint" aria-hidden>
71+ +{people.length - shown.length}
72+ </span>
73+ )}
74+ </span>
75+ </Hint>
7376 );
7477 }
7578
7679 /** The agent working on an issue now. */
7780 export function Assignee({ agent }: { agent: string }) {
7881 return (
79− <span
80− className="inline-flex shrink-0 items-center gap-1 rounded-full border border-accent/35 bg-accent/10 px-2 py-px text-xs font-medium text-accent"
81− title={`Assigned to ${agent}`}
82− >
83− <Bot size={12} />
84− {agent}
85− </span>
82+ <Hint label={`Assigned to ${agent}`}>
83+ <span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-accent/35 bg-accent/10 px-2 py-px text-xs font-medium text-accent">
84+ <Bot size={12} />
85+ <span className="sr-only">Assigned to </span>
86+ {agent}
87+ </span>
88+ </Hint>
8689 );
8790 }
8891