Skip to content
Source
Sign inSign up
Open History to see the last commit

PanelLayoutControls.tsx

231 lines7.7 KB

1import {
2 PanelBottomIcon,
3 PanelRightIcon,
4 SquareMenuIcon,
5 Maximize2Icon,
6 Minimize2Icon,
7} from "lucide-react";
8import { memo, type ReactNode } from "react";
9​
10import { cn } from "~/lib/utils";
11​
12import type { ThreadPanelPresentation } from "../../rightPanelLayout";
13import { PopoverCreateHandle, PopoverTrigger } from "../ui/popover";
14import { useMobileSidebarSheetMounted } from "../ui/sidebar";
15import { Toggle } from "../ui/toggle";
16import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
17​
18export interface PanelLayoutControlsProps {
19 showThreadPanelControl?: boolean;
20 showTerminalControl?: boolean;
21 showRightPanelControl?: boolean;
22 terminalAvailable: boolean;
23 terminalOpen: boolean;
24 terminalShortcutLabel: string | null;
25 threadPanelOpen: boolean;
26 threadPanelPresentation: ThreadPanelPresentation;
27 threadPanelPopoverHandle?: ReturnType<typeof PopoverCreateHandle>;
28 threadPanelShortcutLabel: string | null;
29 rightPanelAvailable: boolean;
30 rightPanelOpen: boolean;
31 rightPanelShortcutLabel: string | null;
32 rightPanelUnavailableLabel?: string;
33 onToggleTerminal: () => void;
34 onToggleThreadPanel: () => void;
35 onToggleRightPanel: () => void;
37​
38export const PanelLayoutControls = memo(function PanelLayoutControls({
39 showThreadPanelControl = true,
40 showTerminalControl = true,
41 showRightPanelControl = true,
42 terminalAvailable,
43 terminalOpen,
44 terminalShortcutLabel,
45 threadPanelOpen,
46 threadPanelPresentation,
47 threadPanelPopoverHandle,
48 threadPanelShortcutLabel,
49 rightPanelAvailable,
50 rightPanelOpen,
51 rightPanelShortcutLabel,
52 rightPanelUnavailableLabel = "Right panel is unavailable",
53 onToggleTerminal,
54 onToggleThreadPanel,
55 onToggleRightPanel,
56}: PanelLayoutControlsProps) {
57 const threadPanelToggle = (
58 <Toggle
59 className="relative shrink-0 [-webkit-app-region:no-drag]"
60 pressed={threadPanelOpen}
61 onPressedChange={threadPanelPresentation === "popover" ? undefined : onToggleThreadPanel}
62 aria-label="Toggle thread details panel"
63 variant="ghost"
64 size="sm"
65 >
66 <SquareMenuIcon className="size-4" />
67 </Toggle>
68 );
69 const threadPanelControl = (
70 <Tooltip>
71 <TooltipTrigger render={<span className="flex shrink-0" />}>
72 {threadPanelPresentation === "popover" ? (
73 <PopoverTrigger handle={threadPanelPopoverHandle} render={threadPanelToggle} />
74 ) : (
75 threadPanelToggle
76 )}
77 </TooltipTrigger>
78 <TooltipPopup side="bottom">
79 Toggle thread details
80 {threadPanelShortcutLabel ? ` (${threadPanelShortcutLabel})` : ""}
81 </TooltipPopup>
82 </Tooltip>
83 );
84​
85 return (
86 <div
87 className="flex h-full shrink-0 items-center gap-1 [-webkit-app-region:no-drag]"
88 data-panel-layout-controls
89 >
90 {showThreadPanelControl ? threadPanelControl : null}
91 {showTerminalControl ? (
92 <Tooltip>
93 <TooltipTrigger render={<span className="flex shrink-0" />}>
94 <Toggle
95 className="shrink-0 [-webkit-app-region:no-drag]"
96 pressed={terminalOpen}
97 onPressedChange={onToggleTerminal}
98 aria-label="Toggle terminal drawer"
99 variant="ghost"
100 size="sm"
101 disabled={!terminalAvailable}
102 >
103 <PanelBottomIcon className="size-4" />
104 </Toggle>
105 </TooltipTrigger>
106 <TooltipPopup side="bottom">
107 {terminalAvailable
108 ? `Toggle terminal drawer${terminalShortcutLabel ? ` (${terminalShortcutLabel})` : ""}`
109 : "Terminal drawer is unavailable"}
110 </TooltipPopup>
111 </Tooltip>
112 ) : null}
113 {showRightPanelControl ? (
114 <Tooltip>
115 <TooltipTrigger render={<span className="flex shrink-0" />}>
116 <Toggle
117 className="shrink-0 [-webkit-app-region:no-drag]"
118 pressed={rightPanelOpen}
119 onPressedChange={onToggleRightPanel}
120 aria-label="Toggle right panel"
121 data-right-panel-toggle
122 variant="ghost"
123 size="sm"
124 disabled={!rightPanelAvailable}
125 >
126 <PanelRightIcon className="size-4" />
127 </Toggle>
128 </TooltipTrigger>
129 <TooltipPopup side="bottom">
130 {rightPanelAvailable
131 ? `Toggle right panel${rightPanelShortcutLabel ? ` (${rightPanelShortcutLabel})` : ""}`
132 : rightPanelUnavailableLabel}
133 </TooltipPopup>
134 </Tooltip>
135 ) : null}
136 </div>
137 );
138});
139​
140export const RightPanelMaximizeControl = memo(function RightPanelMaximizeControl({
141 maximized,
142 onToggle,
143}: {
144 maximized: boolean;
145 onToggle: () => void;
146}) {
147 const label = maximized ? "Restore panel size" : "Maximize panel";
148 return (
149 <Tooltip>
150 <TooltipTrigger
151 render={
152 <Toggle
153 className="shrink-0 [-webkit-app-region:no-drag]"
154 pressed={maximized}
155 onPressedChange={onToggle}
156 aria-label={label}
157 variant="ghost"
158 size="sm"
159 >
160 {maximized ? (
161 <Minimize2Icon className="size-4" />
162 ) : (
163 <Maximize2Icon className="size-4" />
164 )}
165 </Toggle>
166 }
167 />
168 <TooltipPopup side="bottom">{label}</TooltipPopup>
169 </Tooltip>
170 );
171});
172​
173/** The thread panel toggle on a header whose right side the inline right panel covers. */
174export function ThreadPanelHeaderControl(props: PanelLayoutControlsProps) {
175 const mobileSidebarSheetMounted = useMobileSidebarSheetMounted();
176 return (
177 <div
178 className={cn(
179 "absolute top-[var(--workspace-controls-top)] right-[calc(env(safe-area-inset-right)+0.75rem)] z-50 flex h-[var(--workspace-topbar-height)] items-center [-webkit-app-region:no-drag]",
180 mobileSidebarSheetMounted && "hidden",
181 )}
182 data-workspace-titlebar-controls
183 >
184 <PanelLayoutControls {...props} showTerminalControl={false} showRightPanelControl={false} />
185 </div>
186 );
188​
189/** The window's top-right panel controls, with the maximize toggle while the panel is open. */
190export function WorkspaceTitlebarControls(props: {
191 readonly showMaximizeControl: boolean;
192 readonly rightPanelOpen: boolean;
193 readonly animationsActive: boolean;
194 readonly maximized: boolean;
195 readonly onToggleMaximized: () => void;
196 readonly children: ReactNode;
197}) {
198 const mobileSidebarSheetMounted = useMobileSidebarSheetMounted();
199 return (
200 <div
201 className={cn(
202 "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]",
203 mobileSidebarSheetMounted && "hidden",
204 )}
205 data-workspace-titlebar-controls
206 >
207 {props.showMaximizeControl ? (
208 <span
209 aria-hidden={!props.rightPanelOpen}
210 // Fading out on close would show it beside the thread details toggle that replaces it.
211 className={cn(
212 "flex shrink-0",
213 props.rightPanelOpen &&
214 props.animationsActive &&
215 "motion-safe:transition-opacity motion-safe:duration-(--panel-animation-duration) motion-safe:ease-out",
216 props.rightPanelOpen
217 ? "pointer-events-auto opacity-100"
218 : "pointer-events-none absolute right-full mr-1 opacity-0",
219 )}
220 inert={!props.rightPanelOpen}
221 >
222 <RightPanelMaximizeControl
223 maximized={props.maximized}
224 onToggle={props.onToggleMaximized}
225 />
226 </span>
227 ) : null}
228 <div className="pointer-events-auto flex h-full items-center">{props.children}</div>
229 </div>
230 );