Skip to content
Closed
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
30 changes: 28 additions & 2 deletions apps/web/src/components/chat/PanelLayoutControls.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PanelBottomIcon, PanelRightIcon, SquareMenuIcon } from "lucide-react";
import { Maximize2, Minimize2 } from "lucide";
import { MorphIcon } from "~/components/MorphIcon";
import { memo, type ReactNode } from "react";
import { memo, type ReactNode, useLayoutEffect, useRef } from "react";

import { cn } from "~/lib/utils";

Expand All @@ -10,6 +10,9 @@ import { PopoverCreateHandle, PopoverTrigger } from "../ui/popover";
import { Toggle } from "../ui/toggle";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";

/** Matches the right inset of `ThreadPanelHeaderControl`. */
const THREAD_PANEL_HEADER_INSET_PX = 12;

export interface PanelLayoutControlsProps {
showThreadPanelControl?: boolean;
showTerminalControl?: boolean;
Expand Down Expand Up @@ -63,7 +66,7 @@ export const PanelLayoutControls = memo(function PanelLayoutControls({
);
const threadPanelControl = (
<Tooltip>
<TooltipTrigger render={<span className="flex shrink-0" />}>
<TooltipTrigger render={<span className="flex shrink-0" data-thread-panel-toggle />}>
{threadPanelPresentation === "popover" ? (
<PopoverTrigger handle={threadPanelPopoverHandle} render={threadPanelToggle} />
) : (
Expand Down Expand Up @@ -182,8 +185,31 @@ export function WorkspaceTitlebarControls(props: {
readonly onToggleMaximized: () => void;
readonly children: ReactNode;
}) {
const { rightPanelOpen, maximized } = props;
const controlsRef = useRef<HTMLDivElement | null>(null);
const previous = useRef({ rightPanelOpen, maximized });
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- starts the toggle's slide before the commit that swapped it in paints; the animation is cancelled on change or unmount.
useLayoutEffect(() => {
const { rightPanelOpen: wasOpen, maximized: wasMaximized } = previous.current;
previous.current = { rightPanelOpen, maximized };
if (!wasOpen || rightPanelOpen || wasMaximized) return;
const toggle = controlsRef.current?.querySelector("[data-thread-panel-toggle]");
const panel = toggle?.ownerDocument.querySelector("[data-preview-panel-mode='inline']");
const timing = panel?.getAnimations()[0]?.effect?.getTiming();
if (!toggle || !panel || !timing) return;
// The header's toggle was just removed, so slide this one in from where it sat.
const headerRight = panel.getBoundingClientRect().left - THREAD_PANEL_HEADER_INSET_PX;
const offset = headerRight - toggle.getBoundingClientRect().right;
const animation = toggle.animate(
[{ transform: `translateX(${offset}px)` }, { transform: "none" }],
{ ...timing, easing: getComputedStyle(panel).transitionTimingFunction },
);
return () => animation.cancel();
}, [rightPanelOpen, maximized]);

return (
<div
ref={controlsRef}
className="pointer-events-none fixed top-[var(--workspace-controls-top)] right-[var(--workspace-controls-right)] z-50 mr-px flex h-[var(--workspace-topbar-height)] items-center gap-1 [-webkit-app-region:no-drag]"
data-workspace-titlebar-controls
>
Expand Down
Loading