authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-14 23:31:20-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-14 23:35:48-08:00
log669168d08aa36c3b41f05d78605cdedb0bb1fce1
tree0f0cb5a00adab90cec16c2fce6f8408491f3d36d
parent9bd663be7fc67ebde62ea0268b53b2e9e48ef3a1

Polish action button icons and hover tooltips

- Standardize quick action icon sizing and label spacing in chat and git controls - Replace manual popover hover state handling with `openOnHover` triggers - Add reusable terminal action button wrapper with hover tooltips for split/new/close controls

3 files changed, 73 insertions(+), 66 deletions(-)

apps/web/src/components/ChatView.tsx+2-2
......@@ -1476,8 +1476,8 @@ function OpenInPicker() {
14761476 return (
14771477 <Group aria-label="Subscription actions">
14781478 <Button size="xs" variant="outline" onClick={() => openInEditor(lastEditor)}>
1479 {primaryOption?.Icon && <primaryOption.Icon aria-hidden="true" className="size-3" />}
1480 Open
1479 {primaryOption?.Icon && <primaryOption.Icon aria-hidden="true" className="size-3.5" />}
1480 <span className="ml-0.5">Open</span>
14811481 </Button>
14821482 <GroupSeparator />
14831483 <Menu>
apps/web/src/components/GitActionsControl.tsx+16-34
......@@ -122,15 +122,16 @@ function GitActionItemIcon({ icon }: { icon: GitActionIconName }) {
122122}
123123
124124function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
125 if (quickAction.kind === "open_pr") return <GitHubIcon className="size-4" />;
126 if (quickAction.kind === "run_pull") return <InfoIcon className="size-4" />;
125 const iconClassName = "size-3.5";
126 if (quickAction.kind === "open_pr") return <GitHubIcon className={iconClassName} />;
127 if (quickAction.kind === "run_pull") return <InfoIcon className={iconClassName} />;
127128 if (quickAction.kind === "run_action") {
128 if (quickAction.action === "commit") return <GitCommitIcon className="size-4" />;
129 if (quickAction.action === "commit_push") return <CloudUploadIcon className="size-4" />;
130 return <GitHubIcon className="size-4" />;
129 if (quickAction.action === "commit") return <GitCommitIcon className={iconClassName} />;
130 if (quickAction.action === "commit_push") return <CloudUploadIcon className={iconClassName} />;
131 return <GitHubIcon className={iconClassName} />;
131132 }
132 if (quickAction.label === "Commit") return <GitCommitIcon className="size-4" />;
133 return <InfoIcon className="size-4" />;
133 if (quickAction.label === "Commit") return <GitCommitIcon className={iconClassName} />;
134 return <InfoIcon className={iconClassName} />;
134135}
135136
136137function buildGitActionProgressStages(input: {
......@@ -192,10 +193,6 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
192193 const quickActionDisabledReason = quickAction.disabled
193194 ? (quickAction.hint ?? "This action is currently unavailable.")
194195 : null;
195 const [isQuickActionPopoverOpen, setIsQuickActionPopoverOpen] = useState(false);
196 const [openDisabledMenuItemId, setOpenDisabledMenuItemId] = useState<
197 GitActionMenuItem["id"] | null
198 >(null);
199196
200197 const refreshGitStatus = useCallback(async () => {
201198 if (!api || !gitCwd) return;
......@@ -451,21 +448,20 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
451448 ) : (
452449 <Group aria-label="Git actions">
453450 {quickActionDisabledReason ? (
454 <Popover open={isQuickActionPopoverOpen} onOpenChange={setIsQuickActionPopoverOpen}>
451 <Popover>
455452 <PopoverTrigger
453 openOnHover
456454 render={
457455 <Button
458456 variant="outline"
459457 size="xs"
460458 disabled={isGitActionRunning || quickAction.disabled}
461459 className="cursor-not-allowed disabled:cursor-not-allowed disabled:pointer-events-auto"
462 onMouseEnter={() => setIsQuickActionPopoverOpen(true)}
463 onMouseLeave={() => setIsQuickActionPopoverOpen(false)}
464460 />
465461 }
466462 >
467463 <GitQuickActionIcon quickAction={quickAction} />
468 {quickAction.label}
464 <span className="ml-0.5">{quickAction.label}</span>
469465 </PopoverTrigger>
470466 <PopoverPopup tooltipStyle side="bottom" align="start">
471467 {quickActionDisabledReason}
......@@ -479,16 +475,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
479475 onClick={runQuickAction}
480476 >
481477 <GitQuickActionIcon quickAction={quickAction} />
482 {quickAction.label}
478 <span className="ml-0.5">{quickAction.label}</span>
483479 </Button>
484480 )}
485481 <GroupSeparator />
486 <Menu
487 onOpenChange={(open) => {
488 if (open) void refreshGitStatus();
489 if (!open) setOpenDisabledMenuItemId(null);
490 }}
491 >
482 <Menu>
492483 <MenuTrigger
493484 render={<Button aria-label="Git action options" size="icon-xs" variant="outline" />}
494485 disabled={isGitActionRunning}
......@@ -504,19 +495,10 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
504495 );
505496 if (item.disabled && disabledReason) {
506497 return (
507 <Popover
508 key={`${item.id}-${item.label}`}
509 open={openDisabledMenuItemId === item.id}
510 onOpenChange={(open) => setOpenDisabledMenuItemId(open ? item.id : null)}
511 >
498 <Popover key={`${item.id}-${item.label}`}>
512499 <PopoverTrigger
513 render={
514 <span
515 className="inline-flex w-full cursor-not-allowed"
516 onMouseEnter={() => setOpenDisabledMenuItemId(item.id)}
517 onMouseLeave={() => setOpenDisabledMenuItemId(null)}
518 />
519 }
500 openOnHover
501 render={<span className="inline-flex w-full cursor-not-allowed" />}
520502 >
521503 <MenuItem disabled>
522504 <GitActionItemIcon icon={item.icon} />
apps/web/src/components/ThreadTerminalDrawer.tsx+55-30
......@@ -4,12 +4,14 @@ import { type NativeApi } from "@t3tools/contracts";
44import { Terminal, type ITheme } from "@xterm/xterm";
55import {
66 type PointerEvent as ReactPointerEvent,
7 type ReactNode,
78 useCallback,
89 useEffect,
910 useMemo,
1011 useRef,
1112 useState,
1213} from "react";
14import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
1315import {
1416 extractTerminalLinks,
1517 isTerminalLinkActivation,
......@@ -419,6 +421,41 @@ interface ThreadTerminalDrawerProps {
419421 onHeightChange: (height: number) => void;
420422}
421423
424interface TerminalActionButtonProps {
425 label: string;
426 className: string;
427 onClick: () => void;
428 children: ReactNode;
429}
430
431function TerminalActionButton({ label, className, onClick, children }: TerminalActionButtonProps) {
432 return (
433 <Popover>
434 <PopoverTrigger openOnHover
435 render={
436 <button
437 type="button"
438 className={className}
439 onClick={onClick}
440 aria-label={label}
441 />
442 }
443 >
444 {children}
445 </PopoverTrigger>
446 <PopoverPopup
447 tooltipStyle
448 side="bottom"
449 sideOffset={6}
450 align="center"
451 className="pointer-events-none select-none"
452 >
453 {label}
454 </PopoverPopup>
455 </Popover>
456 );
457}
458
422459export default function ThreadTerminalDrawer({
423460 api,
424461 threadId,
......@@ -654,35 +691,29 @@ export default function ThreadTerminalDrawer({
654691 {!hasTerminalSidebar && (
655692 <div className="pointer-events-none absolute right-2 top-2 z-20">
656693 <div className="pointer-events-auto inline-flex items-center overflow-hidden rounded-md border border-border/80 bg-background/70">
657 <button
658 type="button"
694 <TerminalActionButton
659695 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
660696 onClick={onSplitTerminal}
661 aria-label="Split Terminal"
662 title="Split Terminal"
697 label="Split Terminal"
663698 >
664699 <SquareSplitHorizontal className="size-3.25" />
665 </button>
700 </TerminalActionButton>
666701 <div className="h-4 w-px bg-border/80" />
667 <button
668 type="button"
702 <TerminalActionButton
669703 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
670704 onClick={onNewTerminal}
671 aria-label="New Terminal"
672 title="New Terminal"
705 label="New Terminal"
673706 >
674707 <Plus className="size-3.25" />
675 </button>
708 </TerminalActionButton>
676709 <div className="h-4 w-px bg-border/80" />
677 <button
678 type="button"
710 <TerminalActionButton
679711 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
680712 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}
681 aria-label="Close Terminal"
682 title="Close Terminal"
713 label="Close Terminal"
683714 >
684715 <Trash2 className="size-3.25" />
685 </button>
716 </TerminalActionButton>
686717 </div>
687718 </div>
688719 )}
......@@ -745,33 +776,27 @@ export default function ThreadTerminalDrawer({
745776 <aside className="flex w-36 min-w-36 flex-col border border-border/70 bg-muted/10">
746777 <div className="flex h-[22px] items-stretch justify-end border-b border-border/70">
747778 <div className="inline-flex h-full items-stretch">
748 <button
749 type="button"
779 <TerminalActionButton
750780 className="inline-flex h-full items-center px-1 text-foreground/90 transition-colors hover:bg-accent/70"
751781 onClick={onSplitTerminal}
752 aria-label="Split Terminal"
753 title="Split Terminal"
782 label="Split Terminal"
754783 >
755784 <SquareSplitHorizontal className="size-3.25" />
756 </button>
757 <button
758 type="button"
785 </TerminalActionButton>
786 <TerminalActionButton
759787 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"
760788 onClick={onNewTerminal}
761 aria-label="New Terminal"
762 title="New Terminal"
789 label="New Terminal"
763790 >
764791 <Plus className="size-3.25" />
765 </button>
766 <button
767 type="button"
792 </TerminalActionButton>
793 <TerminalActionButton
768794 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"
769795 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}
770 aria-label="Close Terminal"
771 title="Close Terminal"
796 label="Close Terminal"
772797 >
773798 <Trash2 className="size-3.25" />
774 </button>
799 </TerminalActionButton>
775800 </div>
776801 </div>
777802