Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ import {
} from "~/components/ui/menu";
import { useBrowserDefaults } from "~/browser/browserDefaults";
import { ScrollArea } from "~/components/ui/scroll-area";
import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
import {
PanelTabCloseButton,
panelTabTitleFadeClassName,
} from "~/components/ui/panel-tab-close-button";
import { faviconUrlForOrigin } from "~/lib/favicon";
import { useTheme } from "~/hooks/useTheme";
import { useDeviceState } from "~/state/device";
Expand Down Expand Up @@ -1216,17 +1219,15 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
onAuxClick={(event) => handleTabAuxClick(event, surface)}
onContextMenu={(event) => void handleTabContextMenu(event, surface)}
className={cn(
"cursor-pointer group/tab flex h-6 max-w-36 shrink-0 items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs",
// min-w-16 keeps a short title's centre clear of the floating close button.
"cursor-pointer group/tab relative flex h-6 max-w-36 min-w-16 shrink-0 items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs",
ownsDesktopTitleBar && "[-webkit-app-region:no-drag]",
active
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
)}
>
<PanelTabCloseButton
label={`Close ${title}`}
onClick={() => props.onCloseSurface(surface)}
>
<span className="relative flex size-4 shrink-0 items-center justify-center">
<SurfaceIcon
surface={surface}
sessions={props.previewSessions}
Expand All @@ -1237,11 +1238,11 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
/>
{pending ? (
<span
className="absolute -right-0.5 -bottom-0.5 size-1.5 rounded-full bg-current"
className="absolute right-0 bottom-0 size-1.5 rounded-full bg-current"
aria-hidden
/>
) : null}
</PanelTabCloseButton>
</span>
{audio === "none" || !audioRuntimeTabId ? null : (
<Tooltip>
<TooltipTrigger
Expand Down Expand Up @@ -1301,7 +1302,10 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
if (surface.kind === "device" && props.onRenameDevice)
setRenamingDevice(surface.id);
}}
className="cursor-pointer flex min-w-0 items-center"
className={cn(
"cursor-pointer flex min-w-0 flex-1 items-center",
!active && panelTabTitleFadeClassName,
)}
onClick={() => props.onActivate(surface)}
>
<span className="truncate">{title}</span>
Expand All @@ -1321,6 +1325,11 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
</TooltipPopup>
</Tooltip>
)}
<PanelTabCloseButton
label={`Close ${title}`}
onClick={() => props.onCloseSurface(surface)}
alwaysVisible={active}
/>
</SortableTab>
);
})}
Expand Down
34 changes: 20 additions & 14 deletions apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ import {
} from "react";
import { Popover, PopoverPopup, PopoverTrigger } from "~/components/ui/popover";
import { Button } from "~/components/ui/button";
import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
import {
PanelTabCloseButton,
panelTabTitleFadeClassName,
} from "~/components/ui/panel-tab-close-button";
import { stackedThreadToast, toastManager } from "~/components/ui/toast";
import { readTextFromClipboard, writeTextToClipboard } from "~/hooks/useCopyToClipboard";
import { cn } from "~/lib/utils";
Expand Down Expand Up @@ -1815,30 +1818,33 @@ export default function ThreadTerminalDrawer({
<div
key={terminalId}
className={cn(
"group/tab flex h-6 w-full items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs",
"group/tab relative flex h-6 w-full items-center gap-0.5 rounded-md pr-2 pl-1.5 text-xs",
isActive
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/60 hover:text-foreground",
)}
>
{canOperateTerminal ? (
<PanelTabCloseButton
label={closeTerminalLabel}
onClick={() => confirmCloseTerminal(terminalId)}
tooltip={closeTerminalLabel}
>
<TerminalSquare className="size-3 shrink-0" />
</PanelTabCloseButton>
) : (
<TerminalSquare className="size-3 shrink-0" />
)}
<span className="flex size-4 shrink-0 items-center justify-center">
<TerminalSquare className="size-3" />
</span>
<button
type="button"
className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left"
className={cn(
"flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left",
canOperateTerminal && !isActive && panelTabTitleFadeClassName,
)}
onClick={() => onActiveTerminalChange(terminalId)}
>
<span className="truncate">{terminalLabel}</span>
</button>
{canOperateTerminal ? (
<PanelTabCloseButton
label={closeTerminalLabel}
onClick={() => confirmCloseTerminal(terminalId)}
tooltip={closeTerminalLabel}
alwaysVisible={isActive}
/>
) : null}
</div>
);
})}
Expand Down
34 changes: 25 additions & 9 deletions apps/web/src/components/ui/panel-tab-close-button.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,48 @@
import { X } from "lucide-react";
import type { ReactNode } from "react";
import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
import { cn } from "~/lib/utils";

interface PanelTabCloseButtonProps {
children: ReactNode;
label: string;
onClick: () => void;
tooltip?: string;
/** Shows the close action at all times, in its own space after the title, as on the active tab. */
alwaysVisible?: boolean;
}

/** Inside a `group/tab` row, swaps the tab identity for its close action on hover or focus. */
/**
* Fades the trailing edge of a tab title that fills its `pr-2` row, while `PanelTabCloseButton`
* floats over it. The clear 8px is the part of the title the button covers. Leave it off rows
* whose close button is `alwaysVisible`, since nothing covers their title.
*/
export const panelTabTitleFadeClassName =
"group-hover/tab:[mask-image:linear-gradient(to_left,transparent_8px,black_20px)] group-has-[:focus-visible]/tab:[mask-image:linear-gradient(to_left,transparent_8px,black_20px)] pointer-coarse:[mask-image:linear-gradient(to_left,transparent_8px,black_20px)]";

/**
* Close action that follows the title in a `relative group/tab` row with `pr-2`. It floats over the
* row's trailing edge, so it takes no space and the row never changes width, and shows on hover or
* keyboard focus, and always on touch screens, which cannot hover. An `alwaysVisible` button takes
* its own space after the title instead, at the same spot 2px from the row edge.
*/
export function PanelTabCloseButton({
children,
label,
onClick,
tooltip,
alwaysVisible = false,
}: PanelTabCloseButtonProps) {
const button = (
<button
type="button"
className="cursor-pointer group/close relative flex size-4 shrink-0 items-center justify-center rounded-sm hover:bg-muted"
className={cn(
"cursor-pointer flex size-3.5 items-center justify-center rounded-sm hover:bg-muted",
alwaysVisible
? "-mr-1.5 shrink-0"
: "pointer-events-none absolute top-1/2 right-0.5 -translate-y-1/2 opacity-0 group-hover/tab:pointer-events-auto group-hover/tab:opacity-100 group-has-[:focus-visible]/tab:pointer-events-auto group-has-[:focus-visible]/tab:opacity-100 pointer-coarse:pointer-events-auto pointer-coarse:opacity-100",
)}
aria-label={label}
onClick={onClick}
>
<span className="relative flex size-3 items-center justify-center group-hover/tab:hidden group-focus-visible/close:hidden">
{children}
</span>
<X className="hidden size-3 group-hover/tab:block group-focus-visible/close:block" />
<X className="size-3" />
</button>
);

Expand Down
Loading