| author | |
| committer | |
| log | 669168d08aa36c3b41f05d78605cdedb0bb1fce1 |
| tree | 0f0cb5a00adab90cec16c2fce6f8408491f3d36d |
| parent | 9bd663be7fc67ebde62ea0268b53b2e9e48ef3a1 |
- 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 controls3 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 | Open | 1480 | <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 | } |
| 123 | 123 | ||
| 124 | function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) { | 124 | function 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 | } |
| 135 | 136 | ||
| 136 | function buildGitActionProgressStages(input: { | 137 | function 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.disabled | 193 | 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); | ||
| 199 | 196 | ||
| 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 | <PopoverTrigger | 452 | <PopoverTrigger |
| 453 | openOnHover | ||
| 456 | render={ | 454 | render={ |
| 457 | <Button | 455 | <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 | <Menu | 482 | <Menu> |
| 487 | onOpenChange={(open) => { | ||
| 488 | if (open) void refreshGitStatus(); | ||
| 489 | if (!open) setOpenDisabledMenuItemId(null); | ||
| 490 | }} | ||
| 491 | > | ||
| 492 | <MenuTrigger | 483 | <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 | <Popover | 498 | <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 | <PopoverTrigger | 499 | <PopoverTrigger |
| 513 | render={ | 500 | openOnHover |
| 514 | <span | 501 | 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"; |
| 4 | import { Terminal, type ITheme } from "@xterm/xterm"; | 4 | import { Terminal, type ITheme } from "@xterm/xterm"; |
| 5 | import { | 5 | import { |
| 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"; |
| 14 | import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover"; | ||
| 13 | import { | 15 | import { |
| 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 | } |
| 421 | 423 | ||
| 424 | interface TerminalActionButtonProps { | ||
| 425 | label: string; | ||
| 426 | className: string; | ||
| 427 | onClick: () => void; | ||
| 428 | children: ReactNode; | ||
| 429 | } | ||
| 430 | |||
| 431 | function 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 | |||
| 422 | export default function ThreadTerminalDrawer({ | 459 | export 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 | <button | 694 | <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 | <button | 702 | <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 | <button | 710 | <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 | <button | 779 | <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 | <button | 786 | <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 | <button | 793 | <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> |
| 777 | 802 |