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() {...@@ -1476,8 +1476,8 @@ function OpenInPicker() {
1476 return (1476 return (
1477 <Group aria-label="Subscription actions">1477 <Group aria-label="Subscription actions">
1478 <Button size="xs" variant="outline" onClick={() => openInEditor(lastEditor)}>1478 <Button size="xs" variant="outline" onClick={() => openInEditor(lastEditor)}>
1479 {primaryOption?.Icon && <primaryOption.Icon aria-hidden="true" className="size-3" />}1479 {primaryOption?.Icon && <primaryOption.Icon aria-hidden="true" className="size-3.5" />}
1480 Open1480 <span className="ml-0.5">Open</span>
1481 </Button>1481 </Button>
1482 <GroupSeparator />1482 <GroupSeparator />
1483 <Menu>1483 <Menu>
apps/web/src/components/GitActionsControl.tsx+16-34
...@@ -122,15 +122,16 @@ function GitActionItemIcon({ icon }: { icon: GitActionIconName }) {...@@ -122,15 +122,16 @@ function GitActionItemIcon({ icon }: { icon: GitActionIconName }) {
122}122}
123123
124function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {124function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
125 if (quickAction.kind === "open_pr") return <GitHubIcon className="size-4" />;125 const iconClassName = "size-3.5";
126 if (quickAction.kind === "run_pull") return <InfoIcon className="size-4" />;126 if (quickAction.kind === "open_pr") return <GitHubIcon className={iconClassName} />;
127 if (quickAction.kind === "run_pull") return <InfoIcon className={iconClassName} />;
127 if (quickAction.kind === "run_action") {128 if (quickAction.kind === "run_action") {
128 if (quickAction.action === "commit") return <GitCommitIcon className="size-4" />;129 if (quickAction.action === "commit") return <GitCommitIcon className={iconClassName} />;
129 if (quickAction.action === "commit_push") return <CloudUploadIcon className="size-4" />;130 if (quickAction.action === "commit_push") return <CloudUploadIcon className={iconClassName} />;
130 return <GitHubIcon className="size-4" />;131 return <GitHubIcon className={iconClassName} />;
131 }132 }
132 if (quickAction.label === "Commit") return <GitCommitIcon className="size-4" />;133 if (quickAction.label === "Commit") return <GitCommitIcon className={iconClassName} />;
133 return <InfoIcon className="size-4" />;134 return <InfoIcon className={iconClassName} />;
134}135}
135136
136function buildGitActionProgressStages(input: {137function buildGitActionProgressStages(input: {
...@@ -192,10 +193,6 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -192,10 +193,6 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
192 const quickActionDisabledReason = quickAction.disabled193 const quickActionDisabledReason = quickAction.disabled
193 ? (quickAction.hint ?? "This action is currently unavailable.")194 ? (quickAction.hint ?? "This action is currently unavailable.")
194 : null;195 : null;
195 const [isQuickActionPopoverOpen, setIsQuickActionPopoverOpen] = useState(false);
196 const [openDisabledMenuItemId, setOpenDisabledMenuItemId] = useState<
197 GitActionMenuItem["id"] | null
198 >(null);
199196
200 const refreshGitStatus = useCallback(async () => {197 const refreshGitStatus = useCallback(async () => {
201 if (!api || !gitCwd) return;198 if (!api || !gitCwd) return;
...@@ -451,21 +448,20 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -451,21 +448,20 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
451 ) : (448 ) : (
452 <Group aria-label="Git actions">449 <Group aria-label="Git actions">
453 {quickActionDisabledReason ? (450 {quickActionDisabledReason ? (
454 <Popover open={isQuickActionPopoverOpen} onOpenChange={setIsQuickActionPopoverOpen}>451 <Popover>
455 <PopoverTrigger452 <PopoverTrigger
453 openOnHover
456 render={454 render={
457 <Button455 <Button
458 variant="outline"456 variant="outline"
459 size="xs"457 size="xs"
460 disabled={isGitActionRunning || quickAction.disabled}458 disabled={isGitActionRunning || quickAction.disabled}
461 className="cursor-not-allowed disabled:cursor-not-allowed disabled:pointer-events-auto"459 className="cursor-not-allowed disabled:cursor-not-allowed disabled:pointer-events-auto"
462 onMouseEnter={() => setIsQuickActionPopoverOpen(true)}
463 onMouseLeave={() => setIsQuickActionPopoverOpen(false)}
464 />460 />
465 }461 }
466 >462 >
467 <GitQuickActionIcon quickAction={quickAction} />463 <GitQuickActionIcon quickAction={quickAction} />
468 {quickAction.label}464 <span className="ml-0.5">{quickAction.label}</span>
469 </PopoverTrigger>465 </PopoverTrigger>
470 <PopoverPopup tooltipStyle side="bottom" align="start">466 <PopoverPopup tooltipStyle side="bottom" align="start">
471 {quickActionDisabledReason}467 {quickActionDisabledReason}
...@@ -479,16 +475,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -479,16 +475,11 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
479 onClick={runQuickAction}475 onClick={runQuickAction}
480 >476 >
481 <GitQuickActionIcon quickAction={quickAction} />477 <GitQuickActionIcon quickAction={quickAction} />
482 {quickAction.label}478 <span className="ml-0.5">{quickAction.label}</span>
483 </Button>479 </Button>
484 )}480 )}
485 <GroupSeparator />481 <GroupSeparator />
486 <Menu482 <Menu>
487 onOpenChange={(open) => {
488 if (open) void refreshGitStatus();
489 if (!open) setOpenDisabledMenuItemId(null);
490 }}
491 >
492 <MenuTrigger483 <MenuTrigger
493 render={<Button aria-label="Git action options" size="icon-xs" variant="outline" />}484 render={<Button aria-label="Git action options" size="icon-xs" variant="outline" />}
494 disabled={isGitActionRunning}485 disabled={isGitActionRunning}
...@@ -504,19 +495,10 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp...@@ -504,19 +495,10 @@ export default function GitActionsControl({ api, gitCwd }: GitActionsControlProp
504 );495 );
505 if (item.disabled && disabledReason) {496 if (item.disabled && disabledReason) {
506 return (497 return (
507 <Popover498 <Popover key={`${item.id}-${item.label}`}>
508 key={`${item.id}-${item.label}`}
509 open={openDisabledMenuItemId === item.id}
510 onOpenChange={(open) => setOpenDisabledMenuItemId(open ? item.id : null)}
511 >
512 <PopoverTrigger499 <PopoverTrigger
513 render={500 openOnHover
514 <span501 render={<span className="inline-flex w-full cursor-not-allowed" />}
515 className="inline-flex w-full cursor-not-allowed"
516 onMouseEnter={() => setOpenDisabledMenuItemId(item.id)}
517 onMouseLeave={() => setOpenDisabledMenuItemId(null)}
518 />
519 }
520 >502 >
521 <MenuItem disabled>503 <MenuItem disabled>
522 <GitActionItemIcon icon={item.icon} />504 <GitActionItemIcon icon={item.icon} />
apps/web/src/components/ThreadTerminalDrawer.tsx+55-30
...@@ -4,12 +4,14 @@ import { type NativeApi } from "@t3tools/contracts";...@@ -4,12 +4,14 @@ import { type NativeApi } from "@t3tools/contracts";
4import { Terminal, type ITheme } from "@xterm/xterm";4import { Terminal, type ITheme } from "@xterm/xterm";
5import {5import {
6 type PointerEvent as ReactPointerEvent,6 type PointerEvent as ReactPointerEvent,
7 type ReactNode,
7 useCallback,8 useCallback,
8 useEffect,9 useEffect,
9 useMemo,10 useMemo,
10 useRef,11 useRef,
11 useState,12 useState,
12} from "react";13} from "react";
14import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
13import {15import {
14 extractTerminalLinks,16 extractTerminalLinks,
15 isTerminalLinkActivation,17 isTerminalLinkActivation,
...@@ -419,6 +421,41 @@ interface ThreadTerminalDrawerProps {...@@ -419,6 +421,41 @@ interface ThreadTerminalDrawerProps {
419 onHeightChange: (height: number) => void;421 onHeightChange: (height: number) => void;
420}422}
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
422export default function ThreadTerminalDrawer({459export default function ThreadTerminalDrawer({
423 api,460 api,
424 threadId,461 threadId,
...@@ -654,35 +691,29 @@ export default function ThreadTerminalDrawer({...@@ -654,35 +691,29 @@ export default function ThreadTerminalDrawer({
654 {!hasTerminalSidebar && (691 {!hasTerminalSidebar && (
655 <div className="pointer-events-none absolute right-2 top-2 z-20">692 <div className="pointer-events-none absolute right-2 top-2 z-20">
656 <div className="pointer-events-auto inline-flex items-center overflow-hidden rounded-md border border-border/80 bg-background/70">693 <div className="pointer-events-auto inline-flex items-center overflow-hidden rounded-md border border-border/80 bg-background/70">
657 <button694 <TerminalActionButton
658 type="button"
659 className="p-1 text-foreground/90 transition-colors hover:bg-accent"695 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
660 onClick={onSplitTerminal}696 onClick={onSplitTerminal}
661 aria-label="Split Terminal"697 label="Split Terminal"
662 title="Split Terminal"
663 >698 >
664 <SquareSplitHorizontal className="size-3.25" />699 <SquareSplitHorizontal className="size-3.25" />
665 </button>700 </TerminalActionButton>
666 <div className="h-4 w-px bg-border/80" />701 <div className="h-4 w-px bg-border/80" />
667 <button702 <TerminalActionButton
668 type="button"
669 className="p-1 text-foreground/90 transition-colors hover:bg-accent"703 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
670 onClick={onNewTerminal}704 onClick={onNewTerminal}
671 aria-label="New Terminal"705 label="New Terminal"
672 title="New Terminal"
673 >706 >
674 <Plus className="size-3.25" />707 <Plus className="size-3.25" />
675 </button>708 </TerminalActionButton>
676 <div className="h-4 w-px bg-border/80" />709 <div className="h-4 w-px bg-border/80" />
677 <button710 <TerminalActionButton
678 type="button"
679 className="p-1 text-foreground/90 transition-colors hover:bg-accent"711 className="p-1 text-foreground/90 transition-colors hover:bg-accent"
680 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}712 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}
681 aria-label="Close Terminal"713 label="Close Terminal"
682 title="Close Terminal"
683 >714 >
684 <Trash2 className="size-3.25" />715 <Trash2 className="size-3.25" />
685 </button>716 </TerminalActionButton>
686 </div>717 </div>
687 </div>718 </div>
688 )}719 )}
...@@ -745,33 +776,27 @@ export default function ThreadTerminalDrawer({...@@ -745,33 +776,27 @@ export default function ThreadTerminalDrawer({
745 <aside className="flex w-36 min-w-36 flex-col border border-border/70 bg-muted/10">776 <aside className="flex w-36 min-w-36 flex-col border border-border/70 bg-muted/10">
746 <div className="flex h-[22px] items-stretch justify-end border-b border-border/70">777 <div className="flex h-[22px] items-stretch justify-end border-b border-border/70">
747 <div className="inline-flex h-full items-stretch">778 <div className="inline-flex h-full items-stretch">
748 <button779 <TerminalActionButton
749 type="button"
750 className="inline-flex h-full items-center px-1 text-foreground/90 transition-colors hover:bg-accent/70"780 className="inline-flex h-full items-center px-1 text-foreground/90 transition-colors hover:bg-accent/70"
751 onClick={onSplitTerminal}781 onClick={onSplitTerminal}
752 aria-label="Split Terminal"782 label="Split Terminal"
753 title="Split Terminal"
754 >783 >
755 <SquareSplitHorizontal className="size-3.25" />784 <SquareSplitHorizontal className="size-3.25" />
756 </button>785 </TerminalActionButton>
757 <button786 <TerminalActionButton
758 type="button"
759 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"787 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"
760 onClick={onNewTerminal}788 onClick={onNewTerminal}
761 aria-label="New Terminal"789 label="New Terminal"
762 title="New Terminal"
763 >790 >
764 <Plus className="size-3.25" />791 <Plus className="size-3.25" />
765 </button>792 </TerminalActionButton>
766 <button793 <TerminalActionButton
767 type="button"
768 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"794 className="inline-flex h-full items-center border-l border-border/70 px-1 text-foreground/90 transition-colors hover:bg-accent/70"
769 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}795 onClick={() => onCloseTerminal(resolvedActiveTerminalId)}
770 aria-label="Close Terminal"796 label="Close Terminal"
771 title="Close Terminal"
772 >797 >
773 <Trash2 className="size-3.25" />798 <Trash2 className="size-3.25" />
774 </button>799 </TerminalActionButton>
775 </div>800 </div>
776 </div>801 </div>
777802