Skip to content

Commit fee10de

Browse files
feat(lint): ban native title tooltips and migrate to styled Tooltip (#7209)
1 parent bab4b6f commit fee10de

27 files changed

Lines changed: 730 additions & 355 deletions

apps/web/src/browser/BrowserDeviceToolbar.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { useState } from "react";
1212

1313
import { Button } from "~/components/ui/button";
1414
import { Input } from "~/components/ui/input";
15+
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
1516
import {
1617
Select,
1718
SelectGroup,
@@ -296,26 +297,34 @@ export function BrowserDeviceToolbar({
296297
/>
297298
</form>
298299

299-
<Button
300-
variant="ghost"
301-
size="icon-xs"
302-
type="button"
303-
aria-label={
304-
aspectRatio === null ? "Lock viewport aspect ratio" : "Unlock viewport aspect ratio"
305-
}
306-
aria-pressed={aspectRatio !== null}
307-
title={aspectRatio === null ? "Lock aspect ratio" : "Unlock aspect ratio"}
308-
className={cn(aspectRatio !== null && "bg-accent text-foreground")}
309-
disabled={pending || !customValid}
310-
onPointerDown={(event) => event.preventDefault()}
311-
onClick={toggleAspectRatio}
312-
>
313-
{aspectRatio === null ? (
314-
<Unlink2 className={cn(aspectRatio !== null && "text-foreground")} />
315-
) : (
316-
<Link2 className={cn(aspectRatio !== null && "text-foreground")} />
317-
)}
318-
</Button>
300+
<Tooltip>
301+
<TooltipTrigger
302+
render={
303+
<Button
304+
variant="ghost"
305+
size="icon-xs"
306+
type="button"
307+
aria-label={
308+
aspectRatio === null ? "Lock viewport aspect ratio" : "Unlock viewport aspect ratio"
309+
}
310+
aria-pressed={aspectRatio !== null}
311+
className={cn(aspectRatio !== null && "bg-accent text-foreground")}
312+
disabled={pending || !customValid}
313+
onPointerDown={(event) => event.preventDefault()}
314+
onClick={toggleAspectRatio}
315+
/>
316+
}
317+
>
318+
{aspectRatio === null ? (
319+
<Unlink2 className={cn(aspectRatio !== null && "text-foreground")} />
320+
) : (
321+
<Link2 className={cn(aspectRatio !== null && "text-foreground")} />
322+
)}
323+
</TooltipTrigger>
324+
<TooltipPopup side="top">
325+
{aspectRatio === null ? "Lock aspect ratio" : "Unlock aspect ratio"}
326+
</TooltipPopup>
327+
</Tooltip>
319328
<Button
320329
variant="ghost"
321330
size="icon-xs"

apps/web/src/components/ChatView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6348,7 +6348,6 @@ function ChatViewContent(props: ChatViewProps) {
63486348
>
63496349
<Button
63506350
aria-label="Scroll to end"
6351-
title="Scroll to end"
63526351
onClick={() => scrollToEnd(true)}
63536352
className="pointer-events-auto gap-1.5 rounded-full px-3 text-muted-foreground hover:text-foreground"
63546353
size="xs"

apps/web/src/components/ConnectionStatusDot.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export function ConnectionStatusDot({
5959
const dot = (
6060
<button
6161
type="button"
62-
title={tooltipText}
6362
aria-label={tooltipText}
6463
className="relative flex size-3 shrink-0 cursor-help items-center justify-center rounded-full outline-hidden"
6564
>

apps/web/src/components/DiffPanel.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -583,11 +583,19 @@ export default function DiffPanel({
583583
{selectedTurnId === null && selectedGitScope === "branch" && selectedGitSource?.baseRef && (
584584
<div
585585
className="flex min-w-0 max-w-full items-center gap-2 overflow-hidden text-xs text-muted-foreground"
586-
title={`${selectedGitSource.headRef ?? "HEAD"}${selectedGitSource.baseRef}`}
587586
aria-label={`Comparing ${selectedGitSource.headRef ?? "HEAD"} against ${selectedGitSource.baseRef}`}
588587
>
589-
<span className="min-w-0 max-w-48 truncate">{selectedGitSource.headRef ?? "HEAD"}</span>
590-
<ArrowRightIcon className="size-3.5 shrink-0 opacity-70" />
588+
<Tooltip>
589+
<TooltipTrigger render={<span className="flex min-w-0 items-center gap-2" />}>
590+
<span className="min-w-0 max-w-48 truncate">
591+
{selectedGitSource.headRef ?? "HEAD"}
592+
</span>
593+
<ArrowRightIcon className="size-3.5 shrink-0 opacity-70" />
594+
</TooltipTrigger>
595+
<TooltipPopup side="top">
596+
{`${selectedGitSource.headRef ?? "HEAD"}${selectedGitSource.baseRef}`}
597+
</TooltipPopup>
598+
</Tooltip>
591599
<Combobox
592600
items={baseRefItems}
593601
filteredItems={filteredBaseRefItems}
@@ -677,12 +685,20 @@ export default function DiffPanel({
677685
/>
678686
</div>
679687
) : choice.remote ? (
680-
<span
681-
className="flex justify-end text-muted-foreground"
682-
title="Remote only"
683-
>
684-
<CheckIcon aria-hidden="true" className="size-3" />
685-
</span>
688+
<Tooltip>
689+
<TooltipTrigger
690+
render={
691+
<span className="flex justify-end text-muted-foreground">
692+
<CheckIcon
693+
role="img"
694+
aria-label="Remote only"
695+
className="size-3"
696+
/>
697+
</span>
698+
}
699+
/>
700+
<TooltipPopup side="top">Remote only</TooltipPopup>
701+
</Tooltip>
686702
) : null}
687703
</div>
688704
</ComboboxItem>

apps/web/src/components/ServerUpdateAction.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useAtomCommand } from "~/state/use-atom-command";
1111
import { manualServerUpdateCommand } from "~/versionSkew";
1212
import { Button } from "./ui/button";
1313
import { toastManager } from "./ui/toast";
14+
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
1415

1516
// The wire "installing" stage is a sub-second launcher handoff, so the UI
1617
// folds it into the download phase; everything after the handoff is the
@@ -45,9 +46,12 @@ export function ServerUpdateProgress({
4546
return (
4647
<div className="mt-1 flex min-w-0 items-center gap-2 text-xs text-destructive" role="alert">
4748
<span className="size-1.5 shrink-0 rounded-full bg-destructive" aria-hidden="true" />
48-
<span className="min-w-0 truncate" title={state.message}>
49-
{state.message}
50-
</span>
49+
<Tooltip>
50+
<TooltipTrigger render={<span className="min-w-0 truncate">{state.message}</span>} />
51+
<TooltipPopup side="top" className="max-w-80">
52+
{state.message}
53+
</TooltipPopup>
54+
</Tooltip>
5155
</div>
5256
);
5357
}

apps/web/src/components/Sidebar.tsx

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -537,15 +537,21 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
537537
{props.projectTitle}
538538
</span>
539539
<span className="ml-auto flex h-5 min-w-5 shrink-0 items-center justify-end">
540-
<button
541-
type="button"
542-
aria-label="Discard draft"
543-
title="Discard draft"
544-
onClick={handleDiscard}
545-
className="pointer-events-none inline-flex cursor-pointer items-center rounded-md bg-transparent px-1 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/sidebar-row:pointer-events-auto group-hover/sidebar-row:opacity-100"
546-
>
547-
<XIcon className="size-3" />
548-
</button>
540+
<Tooltip>
541+
<TooltipTrigger
542+
render={
543+
<button
544+
type="button"
545+
aria-label="Discard draft"
546+
onClick={handleDiscard}
547+
className="pointer-events-none inline-flex cursor-pointer items-center rounded-md bg-transparent px-1 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:pointer-events-auto focus-visible:opacity-100 group-hover/sidebar-row:pointer-events-auto group-hover/sidebar-row:opacity-100"
548+
>
549+
<XIcon className="size-3" />
550+
</button>
551+
}
552+
/>
553+
<TooltipPopup side="top">Discard draft</TooltipPopup>
554+
</Tooltip>
549555
</span>
550556
</div>
551557
<div className="mt-0.5 truncate text-sm font-medium text-foreground/90">{preview}</div>
@@ -1247,16 +1253,22 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
12471253
) : isWoke ? (
12481254
// A wake can land straight in the settled tail (e.g. PR
12491255
// merged while snoozed); the signal must survive the trip.
1250-
<button
1251-
type="button"
1252-
aria-label="Dismiss Woke notification"
1253-
title="Dismiss Woke notification"
1254-
onClick={handleAcknowledgeWokeClick}
1255-
className="inline-flex cursor-pointer items-center gap-1 rounded-sm text-xs font-medium text-amber-700 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring dark:text-amber-300"
1256-
>
1257-
<AlarmClockIcon aria-hidden className="size-3" />
1258-
<span role="status">Woke</span>
1259-
</button>
1256+
<Tooltip>
1257+
<TooltipTrigger
1258+
render={
1259+
<button
1260+
type="button"
1261+
aria-label="Dismiss Woke notification"
1262+
onClick={handleAcknowledgeWokeClick}
1263+
className="inline-flex cursor-pointer items-center gap-1 rounded-sm text-xs font-medium text-amber-700 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring dark:text-amber-300"
1264+
>
1265+
<AlarmClockIcon aria-hidden className="size-3" />
1266+
<span role="status">Woke</span>
1267+
</button>
1268+
}
1269+
/>
1270+
<TooltipPopup side="top">Dismiss Woke notification</TooltipPopup>
1271+
</Tooltip>
12601272
) : (
12611273
<span className="text-xs">
12621274
{variantAction === "unsettle"
@@ -1414,19 +1426,25 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
14141426
>
14151427
{topStatus ? (
14161428
isWokeStatus ? (
1417-
<button
1418-
type="button"
1419-
aria-label="Dismiss Woke notification"
1420-
title="Dismiss Woke notification"
1421-
onClick={handleAcknowledgeWokeClick}
1422-
className={cn(
1423-
"inline-flex cursor-pointer items-center gap-1 rounded-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring",
1424-
topStatus.className,
1425-
)}
1426-
>
1427-
<AlarmClockIcon aria-hidden className="size-4 shrink-0" />
1428-
<span role="status">{topStatus.label}</span>
1429-
</button>
1429+
<Tooltip>
1430+
<TooltipTrigger
1431+
render={
1432+
<button
1433+
type="button"
1434+
aria-label="Dismiss Woke notification"
1435+
onClick={handleAcknowledgeWokeClick}
1436+
className={cn(
1437+
"inline-flex cursor-pointer items-center gap-1 rounded-sm font-medium outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring",
1438+
topStatus.className,
1439+
)}
1440+
>
1441+
<AlarmClockIcon aria-hidden className="size-4 shrink-0" />
1442+
<span role="status">{topStatus.label}</span>
1443+
</button>
1444+
}
1445+
/>
1446+
<TooltipPopup side="top">Dismiss Woke notification</TooltipPopup>
1447+
</Tooltip>
14301448
) : (
14311449
<span
14321450
className={cn(

apps/web/src/components/chat/ChangedFilesTree.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -174,21 +174,26 @@ export const ChangedFilesCard = memo(function ChangedFilesCard(props: {
174174
</p>
175175
<div className="mt-2 flex flex-wrap items-center gap-1.5">
176176
{previewFiles.map((file) => (
177-
<button
178-
key={file.path}
179-
type="button"
180-
title={file.path}
181-
className="inline-flex max-w-48 items-center gap-1 rounded-md border border-border/70 bg-background/45 px-1.5 py-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
182-
onClick={() => onOpenTurnDiff(turnId, file.path)}
183-
>
184-
<PierreEntryIcon
185-
pathValue={file.path}
186-
kind="file"
187-
theme={resolvedTheme}
188-
className="size-3 shrink-0 text-muted-foreground/70"
189-
/>
190-
<span className="truncate">{changedFileName(file.path)}</span>
191-
</button>
177+
<Tooltip key={file.path}>
178+
<TooltipTrigger
179+
render={
180+
<button
181+
type="button"
182+
className="inline-flex max-w-48 items-center gap-1 rounded-md border border-border/70 bg-background/45 px-1.5 py-1 font-mono text-[10px] text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
183+
onClick={() => onOpenTurnDiff(turnId, file.path)}
184+
/>
185+
}
186+
>
187+
<PierreEntryIcon
188+
pathValue={file.path}
189+
kind="file"
190+
theme={resolvedTheme}
191+
className="size-3 shrink-0 text-muted-foreground/70"
192+
/>
193+
<span className="truncate">{changedFileName(file.path)}</span>
194+
</TooltipTrigger>
195+
<TooltipPopup side="top">{file.path}</TooltipPopup>
196+
</Tooltip>
192197
))}
193198
<button
194199
type="button"

apps/web/src/components/chat/ComposerPreviewAnnotationCards.test.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ describe("ComposerPreviewAnnotationCards", () => {
3737
);
3838

3939
expect(markup).toContain("Make this headline feel intentional.");
40-
expect(markup).toContain('title="1 region"');
41-
expect(markup).toContain('title="1 style change"');
40+
expect(markup.match(/data-slot="tooltip-trigger"/g)).toHaveLength(2);
41+
expect(markup).not.toContain('title="1 region"');
42+
expect(markup).not.toContain('title="1 style change"');
4243
expect(markup).not.toContain("Welcome");
4344
expect(markup).not.toContain("localhost:3000");
4445
expect(markup).not.toContain("Preview annotation");

apps/web/src/components/chat/ComposerPreviewAnnotationCards.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ComposerImageAttachment } from "~/composerDraftStore";
66
import { formatElementContextLabel, normalizeElementContextSelection } from "~/lib/elementContext";
77
import { cn } from "~/lib/utils";
88
import { Button } from "../ui/button";
9+
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
910

1011
interface ComposerPreviewAnnotationCardsProps {
1112
annotations: ReadonlyArray<PreviewAnnotationPayload>;
@@ -16,14 +17,19 @@ interface ComposerPreviewAnnotationCardsProps {
1617
}
1718

1819
function TargetStat(props: { icon: ReactNode; count: number; label: string }) {
20+
const tooltipText = `${props.count} ${props.label}${props.count === 1 ? "" : "s"}`;
1921
return (
20-
<span
21-
className="inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground"
22-
title={`${props.count} ${props.label}${props.count === 1 ? "" : "s"}`}
23-
>
24-
{props.icon}
25-
{props.count}
26-
</span>
22+
<Tooltip>
23+
<TooltipTrigger
24+
render={
25+
<span className="inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground">
26+
{props.icon}
27+
{props.count}
28+
</span>
29+
}
30+
/>
31+
<TooltipPopup side="top">{tooltipText}</TooltipPopup>
32+
</Tooltip>
2733
);
2834
}
2935

0 commit comments

Comments
 (0)