3e676e3bb2
* feat(web): double-click tab to toggle maximize Adds a dblclick gesture on dockview tabs that toggles maximize/restore on the containing group, mirroring the existing maximize button. Preview tabs keep their promote-to-pin shortcut: first dblclick pins, then dblclick maximizes. Sidebar group is excluded. * fix(web): add cursor-pointer to tab wrappers for consistency * fix(web): add cursor-pointer to plan-tab wrapper * fix(web): make tab dblclick maximize resilient to layout rebuild Refactor the dblclick-to-maximize handler so it reads the panel's group ID and store state at call time instead of from a render-time closure, and guards against firing while the dockview store is mid-layout-restore. This addresses oscillation when double-clicking a session tab on a task that has multiple sessions: the maximize call now stops event propagation, respects isRestoringLayout, and always uses the live api.group.id, so it remains correct even after dockview rebuilds groups via fromJSON.
161 lines
5.2 KiB
TypeScript
161 lines
5.2 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback } from "react";
|
|
import { DockviewDefaultTab, type IDockviewPanelHeaderProps } from "dockview-react";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuSeparator,
|
|
ContextMenuTrigger,
|
|
} from "@kandev/ui/context-menu";
|
|
import { cn } from "@/lib/utils";
|
|
import { useDockviewStore } from "@/lib/state/dockview-store";
|
|
import type { PreviewType } from "@/lib/state/dockview-panel-actions";
|
|
import { useTabMaximizeOnDoubleClick } from "./use-tab-maximize";
|
|
|
|
/**
|
|
* Middle-click to close any tab (preview or pinned).
|
|
* Call `event.preventDefault()` to suppress the browser autoscroll gesture.
|
|
*/
|
|
export function useMiddleClickClose(
|
|
api: IDockviewPanelHeaderProps["api"],
|
|
containerApi: IDockviewPanelHeaderProps["containerApi"],
|
|
) {
|
|
return useCallback(
|
|
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
if (event.button !== 1) return;
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const panel = containerApi.getPanel(api.id);
|
|
if (panel) containerApi.removePanel(panel);
|
|
},
|
|
[api, containerApi],
|
|
);
|
|
}
|
|
|
|
function useTabContextActions(
|
|
api: IDockviewPanelHeaderProps["api"],
|
|
containerApi: IDockviewPanelHeaderProps["containerApi"],
|
|
) {
|
|
const handleClose = useCallback(() => {
|
|
const panel = containerApi.getPanel(api.id);
|
|
if (panel) containerApi.removePanel(panel);
|
|
}, [api, containerApi]);
|
|
|
|
const handleCloseOthers = useCallback(() => {
|
|
const toClose = api.group.panels.filter(
|
|
(p) => p.id !== api.id && p.id !== "chat" && !p.id.startsWith("session:"),
|
|
);
|
|
for (const panel of toClose) containerApi.removePanel(panel);
|
|
}, [api, containerApi]);
|
|
|
|
return { handleClose, handleCloseOthers };
|
|
}
|
|
|
|
/**
|
|
* Preview tab: italic title + double-click to pin + middle-click to close.
|
|
* One per preview type (file-editor / file-diff / commit-detail).
|
|
*/
|
|
function PreviewTab(props: IDockviewPanelHeaderProps & { type: PreviewType }) {
|
|
const { api, containerApi, type } = props;
|
|
const promote = useDockviewStore((s) => s.promotePreviewToPinned);
|
|
const onMouseDown = useMiddleClickClose(api, containerApi);
|
|
const { handleClose, handleCloseOthers } = useTabContextActions(api, containerApi);
|
|
const handleMaximizeDblClick = useTabMaximizeOnDoubleClick(api);
|
|
const isPromoted = (props.params as Record<string, unknown> | undefined)?.promoted === true;
|
|
|
|
const onDoubleClick = useCallback(
|
|
(event: React.MouseEvent) => {
|
|
if (!isPromoted) {
|
|
promote(type);
|
|
return;
|
|
}
|
|
handleMaximizeDblClick(event);
|
|
},
|
|
[promote, type, isPromoted, handleMaximizeDblClick],
|
|
);
|
|
|
|
const handleKeepOpen = useCallback(() => {
|
|
promote(type);
|
|
}, [promote, type]);
|
|
|
|
return (
|
|
<ContextMenu>
|
|
<ContextMenuTrigger asChild>
|
|
<div
|
|
className={cn(
|
|
"flex h-full items-center cursor-pointer select-none",
|
|
!isPromoted && "italic",
|
|
)}
|
|
onMouseDown={onMouseDown}
|
|
onDoubleClick={onDoubleClick}
|
|
title={isPromoted ? undefined : "Double-click to keep this tab open"}
|
|
data-testid={`preview-tab-${type}`}
|
|
>
|
|
<DockviewDefaultTab {...props} />
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent>
|
|
<ContextMenuItem className="cursor-pointer" onSelect={handleClose}>
|
|
Close
|
|
</ContextMenuItem>
|
|
<ContextMenuItem className="cursor-pointer" onSelect={handleCloseOthers}>
|
|
Close Others
|
|
</ContextMenuItem>
|
|
{!isPromoted && (
|
|
<>
|
|
<ContextMenuSeparator />
|
|
<ContextMenuItem className="cursor-pointer" onSelect={handleKeepOpen}>
|
|
Keep Open
|
|
</ContextMenuItem>
|
|
</>
|
|
)}
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
);
|
|
}
|
|
|
|
export function PreviewFileTab(props: IDockviewPanelHeaderProps) {
|
|
return <PreviewTab {...props} type="file-editor" />;
|
|
}
|
|
export function PreviewDiffTab(props: IDockviewPanelHeaderProps) {
|
|
return <PreviewTab {...props} type="file-diff" />;
|
|
}
|
|
export function PreviewCommitTab(props: IDockviewPanelHeaderProps) {
|
|
return <PreviewTab {...props} type="commit-detail" />;
|
|
}
|
|
|
|
/**
|
|
* Default (non-preview) tab for pinned file/diff/commit panels.
|
|
* Adds middle-click-to-close and a right-click context menu.
|
|
*/
|
|
export function PinnedDefaultTab(props: IDockviewPanelHeaderProps) {
|
|
const { api, containerApi } = props;
|
|
const onMouseDown = useMiddleClickClose(api, containerApi);
|
|
const { handleClose, handleCloseOthers } = useTabContextActions(api, containerApi);
|
|
const onDoubleClick = useTabMaximizeOnDoubleClick(api);
|
|
|
|
return (
|
|
<ContextMenu>
|
|
<ContextMenuTrigger asChild>
|
|
<div
|
|
className="flex h-full items-center cursor-pointer select-none"
|
|
onMouseDown={onMouseDown}
|
|
onDoubleClick={onDoubleClick}
|
|
>
|
|
<DockviewDefaultTab {...props} />
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent>
|
|
<ContextMenuItem className="cursor-pointer" onSelect={handleClose}>
|
|
Close
|
|
</ContextMenuItem>
|
|
<ContextMenuItem className="cursor-pointer" onSelect={handleCloseOthers}>
|
|
Close Others
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
);
|
|
}
|