authorgravatar for saiansh2525@gmail.comYash Singh <saiansh2525@gmail.com> 2026-10-10 03:34:14-05:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 08:34:14+00:00
logc77a7b7eebd6ea8a3a63bbcaaa7c1707fe65c86e
treefc7f1ad3f6e5f714631d976b371b7715d4d7fc19
parentbd2346eda2e2c380d1844869c7fd16c279d2190f
signature Signed by PGP key B5690EEEBB952194

fix(web): align compact button touch targets (#17748)


9 files changed, 58 insertions(+), 27 deletions(-)

apps/web/src/components/BranchToolbarBranchSelector.tsx+5-1
...@@ -779,7 +779,11 @@ export function BranchToolbarBranchSelector({...@@ -779,7 +779,11 @@ export function BranchToolbarBranchSelector({
779 )}779 )}
780 />780 />
781 <ComposerContextLabel displayMode={displayMode}>781 <ComposerContextLabel displayMode={displayMode}>
782 <MiddleTruncate value={triggerLabel} className="w-full" />782 <MiddleTruncate
783 value={triggerLabel}
784 variant={displayMode === "panel" ? "cap-aligned" : "default"}
785 className="w-full"
786 />
783 </ComposerContextLabel>787 </ComposerContextLabel>
784 {displayMode !== "panel" ? (788 {displayMode !== "panel" ? (
785 <ChevronDownIcon className="size-3 shrink-0 opacity-50" />789 <ChevronDownIcon className="size-3 shrink-0 opacity-50" />
apps/web/src/components/ComposerContextLabel.tsx+3-1
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import type { ReactNode } from "react";1import type { ReactNode } from "react";
22
3import { cn } from "../lib/utils";3import { cn } from "../lib/utils";
4import { THREAD_DETAILS_PANEL_LABEL_CLASS } from "./chat/threadDetailsPanelStyles";
45
5/** Keeps text measurable while the composer's outer label box collapses. */6/** Keeps text measurable while the composer's outer label box collapses. */
6export function ComposerContextLabel({7export function ComposerContextLabel({
...@@ -16,7 +17,7 @@ export function ComposerContextLabel({...@@ -16,7 +17,7 @@ export function ComposerContextLabel({
16 className={cn(17 className={cn(
17 "min-w-0",18 "min-w-0",
18 displayMode === "panel"19 displayMode === "panel"
19 ? "flex-1 truncate text-left"20 ? "flex-1 overflow-x-clip overflow-y-visible text-left"
20 : "max-w-[240px] group-data-[compact]/composer-context:max-w-0",21 : "max-w-[240px] group-data-[compact]/composer-context:max-w-0",
21 )}22 )}
22 >23 >
...@@ -24,6 +25,7 @@ export function ComposerContextLabel({...@@ -24,6 +25,7 @@ export function ComposerContextLabel({
24 data-composer-label-motion25 data-composer-label-motion
25 className={cn(26 className={cn(
26 "block w-full min-w-0 truncate",27 "block w-full min-w-0 truncate",
28 displayMode === "panel" && THREAD_DETAILS_PANEL_LABEL_CLASS,
27 displayMode === "toolbar" &&29 displayMode === "toolbar" &&
28 "max-w-[240px] transition-opacity duration-180 ease-drawer group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none",30 "max-w-[240px] transition-opacity duration-180 ease-drawer group-data-[compact]/composer-context:opacity-0 motion-reduce:transition-none",
29 )}31 )}
apps/web/src/components/GitActionsControl.tsx+23-18
...@@ -127,6 +127,7 @@ import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";...@@ -127,6 +127,7 @@ import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
127import {127import {
128 THREAD_DETAILS_PANEL_CHEVRON_CLASS,128 THREAD_DETAILS_PANEL_CHEVRON_CLASS,
129 THREAD_DETAILS_PANEL_ICON_CLASS,129 THREAD_DETAILS_PANEL_ICON_CLASS,
130 THREAD_DETAILS_PANEL_LABEL_CLASS,
130 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,131 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,
131 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,132 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,
132} from "./chat/threadDetailsPanelStyles";133} from "./chat/threadDetailsPanelStyles";
...@@ -441,23 +442,25 @@ function GitActionProgressButtonContent({...@@ -441,23 +442,25 @@ function GitActionProgressButtonContent({
441 aria-live="polite"442 aria-live="polite"
442 className={cn(443 className={cn(
443 "grid min-w-0 flex-1 items-center",444 "grid min-w-0 flex-1 items-center",
444 // Pin the title row to the button's minimum content height (min-height445 // Pin the title row to the button's minimum content height so output
445 // minus vertical padding and border) so revealing the output row446 // expands below it. Panel controls leave 24px after padding and border.
446 // extends the button downward without re-centering — the title must447 // No row gap: collapsed output must not add height. The panel's elapsed
447 // not shift. No row gap: the collapsed output row must contribute zero448 // counter uses the menu-chevron slot, so it has no trailing column.
448 // height so the single-line running button matches the static button
449 // exactly. The panel column gap matches the static row's icon-to-label
450 // distance (gap-2.5 plus the label's ml-0.5). In the panel the elapsed
451 // counter renders outside the button (in the menu-chevron slot), so
452 // there is no trailing column.
453 isPanel449 isPanel
454 ? "grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.75rem] gap-x-3"450 ? "grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.5rem] gap-x-3"
455 : "grid-cols-[auto_minmax(0,1fr)_auto] grid-rows-[1.25rem] gap-x-2.5 sm:grid-rows-[1rem]",451 : "grid-cols-[auto_minmax(0,1fr)_auto] grid-rows-[1.25rem] gap-x-2.5 sm:grid-rows-[1rem]",
456 )}452 )}
457 role="status"453 role="status"
458 >454 >
459 <Spinner aria-hidden="true" className="row-start-1 -mx-0.5 shrink-0" />455 <Spinner aria-hidden="true" className="row-start-1 -mx-0.5 shrink-0" />
460 <p className="row-start-1 min-w-0 truncate text-left">{progress.status}</p>456 <p
457 className={cn(
458 "row-start-1 min-w-0 truncate text-left",
459 isPanel && THREAD_DETAILS_PANEL_LABEL_CLASS,
460 )}
461 >
462 {progress.status}
463 </p>
461 {!isPanel ? (464 {!isPanel ? (
462 <GitActionElapsedTime465 <GitActionElapsedTime
463 startedAtMs={progress.startedAtMs}466 startedAtMs={progress.startedAtMs}
...@@ -496,11 +499,13 @@ function GitActionSuccessButtonContent({ success }: { success: InlineGitActionSu...@@ -496,11 +499,13 @@ function GitActionSuccessButtonContent({ success }: { success: InlineGitActionSu
496 return (499 return (
497 <div500 <div
498 aria-live="polite"501 aria-live="polite"
499 className="grid min-w-0 flex-1 grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.75rem] items-center gap-x-3"502 className="grid min-w-0 flex-1 grid-cols-[auto_minmax(0,1fr)] grid-rows-[1.5rem] items-center gap-x-3"
500 role="status"503 role="status"
501 >504 >
502 <CheckIcon aria-hidden="true" className="size-3.5 shrink-0 text-success" />505 <CheckIcon aria-hidden="true" className="size-3.5 shrink-0 text-success" />
503 <p className="min-w-0 truncate text-left">{success.title}</p>506 <p className={cn("min-w-0 truncate text-left", THREAD_DETAILS_PANEL_LABEL_CLASS)}>
507 {success.title}
508 </p>
504 <div509 <div
505 className={cn(510 className={cn(
506 "col-start-2 grid min-w-0 transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none",511 "col-start-2 grid min-w-0 transition-[grid-template-rows,opacity] duration-200 ease-out motion-reduce:transition-none",
...@@ -1704,7 +1709,7 @@ export default function GitActionsControl({...@@ -1704,7 +1709,7 @@ export default function GitActionsControl({
1704 <PopoverTrigger1709 <PopoverTrigger
1705 openOnHover1710 openOnHover
1706 nativeButton={false}1711 nativeButton={false}
1707 render={<span className="block w-max cursor-not-allowed" />}1712 render={<span className="block w-full cursor-not-allowed" />}
1708 >1713 >
1709 <MenuItem1714 <MenuItem
1710 density={presentation === "menu" ? "touch" : "default"}1715 density={presentation === "menu" ? "touch" : "default"}
...@@ -1824,7 +1829,7 @@ export default function GitActionsControl({...@@ -1824,7 +1829,7 @@ export default function GitActionsControl({
1824 onClick={initializeGit}1829 onClick={initializeGit}
1825 >1830 >
1826 <GitBranchPlusIcon className="size-3.5" aria-hidden />1831 <GitBranchPlusIcon className="size-3.5" aria-hidden />
1827 <span className="ml-0.5">1832 <span className={cn("ml-0.5", isPanel && THREAD_DETAILS_PANEL_LABEL_CLASS)}>
1828 {initAction.isPending ? "Initializing..." : "Initialize Git"}1833 {initAction.isPending ? "Initializing..." : "Initialize Git"}
1829 </span>1834 </span>
1830 </ThreadDetailsControl>1835 </ThreadDetailsControl>
...@@ -1882,7 +1887,7 @@ export default function GitActionsControl({...@@ -1882,7 +1887,7 @@ export default function GitActionsControl({
1882 <span1887 <span
1883 className={cn(1888 className={cn(
1884 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",1889 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",
1885 isPanel && "not-sr-only ml-0 truncate",1890 isPanel && cn("not-sr-only ml-0 truncate", THREAD_DETAILS_PANEL_LABEL_CLASS),
1886 )}1891 )}
1887 >1892 >
1888 {quickAction.label}1893 {quickAction.label}
...@@ -1909,7 +1914,7 @@ export default function GitActionsControl({...@@ -1909,7 +1914,7 @@ export default function GitActionsControl({
1909 <span1914 <span
1910 className={cn(1915 className={cn(
1911 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",1916 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",
1912 isPanel && "not-sr-only ml-0 truncate",1917 isPanel && cn("not-sr-only ml-0 truncate", THREAD_DETAILS_PANEL_LABEL_CLASS),
1913 )}1918 )}
1914 >1919 >
1915 {quickAction.label}1920 {quickAction.label}
...@@ -1979,7 +1984,7 @@ export default function GitActionsControl({...@@ -1979,7 +1984,7 @@ export default function GitActionsControl({
1979 onClick={onOpenChanges}1984 onClick={onOpenChanges}
1980 >1985 >
1981 <FileDiffIcon className={THREAD_DETAILS_PANEL_ICON_CLASS} aria-hidden />1986 <FileDiffIcon className={THREAD_DETAILS_PANEL_ICON_CLASS} aria-hidden />
1982 <span className="flex-1 text-left">Changes</span>1987 <span className={cn("flex-1 text-left", THREAD_DETAILS_PANEL_LABEL_CLASS)}>Changes</span>
1983 <span className="flex items-center gap-1 font-mono text-2xs tabular-nums">1988 <span className="flex items-center gap-1 font-mono text-2xs tabular-nums">
1984 <span className="text-success">+{changesTotals?.insertions ?? 0}</span>1989 <span className="text-success">+{changesTotals?.insertions ?? 0}</span>
1985 <span className="text-destructive">-{changesTotals?.deletions ?? 0}</span>1990 <span className="text-destructive">-{changesTotals?.deletions ?? 0}</span>
apps/web/src/components/ProjectScriptsControl.tsx+6-3
...@@ -50,6 +50,7 @@ import { cn } from "~/lib/utils";...@@ -50,6 +50,7 @@ import { cn } from "~/lib/utils";
50import {50import {
51 THREAD_DETAILS_PANEL_CHEVRON_CLASS,51 THREAD_DETAILS_PANEL_CHEVRON_CLASS,
52 THREAD_DETAILS_PANEL_ICON_CLASS,52 THREAD_DETAILS_PANEL_ICON_CLASS,
53 THREAD_DETAILS_PANEL_LABEL_CLASS,
53 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,54 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,
54 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,55 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,
55} from "./chat/threadDetailsPanelStyles";56} from "./chat/threadDetailsPanelStyles";
...@@ -322,7 +323,7 @@ export default function ProjectScriptsControl({...@@ -322,7 +323,7 @@ export default function ProjectScriptsControl({
322 <span323 <span
323 className={cn(324 className={cn(
324 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",325 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",
325 isPanel && "not-sr-only ml-0 truncate",326 isPanel && cn("not-sr-only ml-0 truncate", THREAD_DETAILS_PANEL_LABEL_CLASS),
326 )}327 )}
327 >328 >
328 {primaryScript.name}329 {primaryScript.name}
...@@ -387,7 +388,9 @@ export default function ProjectScriptsControl({...@@ -387,7 +388,9 @@ export default function ProjectScriptsControl({
387 onClick={() => setActionsMenuOpen({ presentation, scripts: false, imports: true })}388 onClick={() => setActionsMenuOpen({ presentation, scripts: false, imports: true })}
388 >389 >
389 <WrenchIcon className={THREAD_DETAILS_PANEL_ICON_CLASS} />390 <WrenchIcon className={THREAD_DETAILS_PANEL_ICON_CLASS} />
390 <span className="min-w-0 truncate">Actions</span>391 <span className={cn("min-w-0 truncate", THREAD_DETAILS_PANEL_LABEL_CLASS)}>
392 Actions
393 </span>
391 </ThreadDetailsControl>394 </ThreadDetailsControl>
392 <span aria-hidden="true" className={THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS} />395 <span aria-hidden="true" className={THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS} />
393 <Menu396 <Menu
...@@ -456,7 +459,7 @@ export default function ProjectScriptsControl({...@@ -456,7 +459,7 @@ export default function ProjectScriptsControl({
456 <span459 <span
457 className={cn(460 className={cn(
458 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",461 "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",
459 isPanel && "not-sr-only ml-0.5",462 isPanel && cn("not-sr-only ml-0.5", THREAD_DETAILS_PANEL_LABEL_CLASS),
460 )}463 )}
461 >464 >
462 {isPanel ? "Add project script" : "Add action"}465 {isPanel ? "Add project script" : "Add action"}
apps/web/src/components/chat/ComposerPrimaryActions.tsx+1-1
...@@ -370,7 +370,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({...@@ -370,7 +370,7 @@ export const ComposerPrimaryActions = memo(function ComposerPrimaryActions({
370 <button370 <button
371 type="button"371 type="button"
372 className={cn(372 className={cn(
373 "flex h-7 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 text-xs tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 [&_svg]:pointer-events-none [&_svg]:size-3.5",373 "flex h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-full border px-2.5 text-xs tabular-nums outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-64 sm:h-8 [&_svg]:pointer-events-none [&_svg]:size-3.5",
374 keepFullHistory374 keepFullHistory
375 ? "border-border text-muted-foreground hover:text-foreground"375 ? "border-border text-muted-foreground hover:text-foreground"
376 : "border-warning/40 text-warning hover:bg-warning/8",376 : "border-warning/40 text-warning hover:bg-warning/8",
apps/web/src/components/chat/OpenInPicker.tsx+2-1
...@@ -64,6 +64,7 @@ import { useAtomCommand } from "~/state/use-atom-command";...@@ -64,6 +64,7 @@ import { useAtomCommand } from "~/state/use-atom-command";
64import {64import {
65 THREAD_DETAILS_PANEL_CHEVRON_CLASS,65 THREAD_DETAILS_PANEL_CHEVRON_CLASS,
66 THREAD_DETAILS_PANEL_ICON_CLASS,66 THREAD_DETAILS_PANEL_ICON_CLASS,
67 THREAD_DETAILS_PANEL_LABEL_CLASS,
67 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,68 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,
68 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,69 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,
69} from "./threadDetailsPanelStyles";70} from "./threadDetailsPanelStyles";
...@@ -421,7 +422,7 @@ export const OpenInPicker = memo(function OpenInPicker({...@@ -421,7 +422,7 @@ export const OpenInPicker = memo(function OpenInPicker({
421 compact422 compact
422 ? "sr-only"423 ? "sr-only"
423 : "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",424 : "sr-only @3xl/header-actions:not-sr-only @3xl/header-actions:ml-0.5",
424 isPanel && "not-sr-only ml-0 min-w-0 truncate",425 isPanel && cn("not-sr-only ml-0 min-w-0 truncate", THREAD_DETAILS_PANEL_LABEL_CLASS),
425 )}426 )}
426 >427 >
427 {primaryLabel}428 {primaryLabel}
apps/web/src/components/chat/ThreadDetailsPrRow.tsx+4-1
...@@ -75,6 +75,7 @@ import { Button } from "../ui/button";...@@ -75,6 +75,7 @@ import { Button } from "../ui/button";
75import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";75import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
76import {76import {
77 THREAD_DETAILS_PANEL_ICON_CLASS,77 THREAD_DETAILS_PANEL_ICON_CLASS,
78 THREAD_DETAILS_PANEL_LABEL_CLASS,
78 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,79 THREAD_DETAILS_PANEL_SPLIT_GROUP_CLASS,
79 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,80 THREAD_DETAILS_PANEL_SPLIT_SEPARATOR_CLASS,
80} from "./threadDetailsPanelStyles";81} from "./threadDetailsPanelStyles";
...@@ -398,7 +399,9 @@ export function ThreadDetailsPrRow({...@@ -398,7 +399,9 @@ export function ThreadDetailsPrRow({
398 }399 }
399 >400 >
400 {icon}401 {icon}
401 <span className="min-w-0 flex-1 truncate text-left">{text}</span>402 <span className={cn("min-w-0 flex-1 truncate text-left", THREAD_DETAILS_PANEL_LABEL_CLASS)}>
403 {text}
404 </span>
402 </TooltipTrigger>405 </TooltipTrigger>
403 {rowTooltip}406 {rowTooltip}
404 </Tooltip>407 </Tooltip>
apps/web/src/components/chat/threadDetailsPanelStyles.ts+5
...@@ -14,6 +14,11 @@ const THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_H...@@ -14,6 +14,11 @@ const THREAD_DETAILS_PANEL_SPLIT_GROUP_SURFACE_CLASS = `${THREAD_DETAILS_PANEL_H
1414
15export const THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS = "gap-2.5 px-2.5 text-left";15export const THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS = "gap-2.5 px-2.5 text-left";
1616
17// Center capitals with icons. Only clip horizontally so truncated labels retain
18// accents and descenders outside the trimmed text box.
19export const THREAD_DETAILS_PANEL_LABEL_CLASS =
20 "[text-box:trim-both_cap_alphabetic] overflow-x-clip overflow-y-visible";
21
17const THREAD_DETAILS_PANEL_CONTROL_CLASS = `h-8 min-w-0 rounded-lg border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80`;22const THREAD_DETAILS_PANEL_CONTROL_CLASS = `h-8 min-w-0 rounded-lg border-transparent ${THREAD_DETAILS_PANEL_ROW_CONTENT_CLASS} text-[13px] font-medium text-foreground/80`;
1823
19export const THREAD_DETAILS_PANEL_ROW_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} w-full justify-start ${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS}`;24export const THREAD_DETAILS_PANEL_ROW_CLASS = `${THREAD_DETAILS_PANEL_CONTROL_CLASS} w-full justify-start ${THREAD_DETAILS_PANEL_ROW_SURFACE_CLASS}`;
apps/web/src/components/ui/middle-truncate.tsx+9-1
...@@ -16,6 +16,7 @@ export function MiddleTruncate({...@@ -16,6 +16,7 @@ export function MiddleTruncate({
16 value,16 value,
17 tail,17 tail,
18 showTitle = true,18 showTitle = true,
19 variant = "default",
19 className,20 className,
20 ...props21 ...props
21}: Omit<ComponentProps<"span">, "children"> & {22}: Omit<ComponentProps<"span">, "children"> & {
...@@ -24,12 +25,19 @@ export function MiddleTruncate({...@@ -24,12 +25,19 @@ export function MiddleTruncate({
24 tail?: number;25 tail?: number;
25 /** The full value on hover. Off where a tooltip already carries it. */26 /** The full value on hover. Off where a tooltip already carries it. */
26 showTitle?: boolean;27 showTitle?: boolean;
28 /** Align capital letters with adjacent icons while preserving accents and descenders. */
29 variant?: "default" | "cap-aligned";
27}) {30}) {
28 const split = splitForMiddleTruncate(value, tail);31 const split = splitForMiddleTruncate(value, tail);
29 return (32 return (
30 <span33 <span
31 {...(showTitle ? { title: value } : {})}34 {...(showTitle ? { title: value } : {})}
32 className={cn("inline-flex min-w-0 max-w-full overflow-hidden whitespace-nowrap", className)}35 className={cn(
36 "inline-flex min-w-0 max-w-full overflow-hidden whitespace-nowrap",
37 variant === "cap-aligned" &&
38 "overflow-x-clip overflow-y-visible [&>span]:[text-box:trim-both_cap_alphabetic] [&>span]:overflow-x-clip [&>span]:overflow-y-visible",
39 className,
40 )}
33 {...props}41 {...props}
34 >42 >
35 {split ? (43 {split ? (