authorgravatar for 142208891+realhasanshoaib@users.noreply.github.comHasan Shoaib <142208891+realhasanshoaib@users.noreply.github.com> 2026-10-12 05:54:01+04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-12 01:54:01+00:00
logdf78114360b77fc746f5dd5b91d006ca37c51f99
tree0ce0406042011762d5236bbd700871f514c25134
parentfd6d9ddaab241f60a1ad3c2b7b1d836cf871447f
signature Signed by PGP key B5690EEEBB952194

fix(web): sidebar toggle no longer sits under the macOS window buttons (#18193)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

3 files changed, 48 insertions(+), 14 deletions(-)

apps/web/src/components/AppSidebarLayout.tsx+14-5
......@@ -83,7 +83,7 @@ function readInitialThreadSidebarWidth(): number {
8383function SidebarControl() {
8484 const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" });
8585 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
86 const { toggleSidebar } = useSidebar();
86 const { toggleSidebar, isMobile, mobileSheetMounted } = useSidebar();
8787 const isSidebarVisible = useSidebarVisibility();
8888 const environmentIdentificationMode = useEnvironmentIdentificationMode();
8989 const stageBackdropVariant = useSidebarStageBackdropVariant(
......@@ -138,8 +138,12 @@ function SidebarControl() {
138138 // The right-side layout controls carry mr-px (border compensation inside
139139 // the panel), so the trigger mirrors it: both clusters sit one extra pixel
140140 // off their edge and the titlebar reads symmetric.
141 // The sheet owns its trigger until its closing transition unmounts it.
141142 <div
142 className="pointer-events-none fixed left-[var(--workspace-controls-left)] top-[var(--workspace-controls-top)] z-50 ml-px flex h-[var(--workspace-topbar-height)] items-center"
143 className={cn(
144 "pointer-events-none fixed left-[var(--workspace-controls-left)] top-[var(--workspace-controls-top)] z-50 ml-px flex h-[var(--workspace-topbar-height)] items-center",
145 isMobile && mobileSheetMounted && "hidden",
146 )}
143147 data-sidebar-control=""
144148 >
145149 <Tooltip>
......@@ -259,12 +263,15 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
259263 ? getWindowFullscreenState()
260264 : false;
261265 });
266 const windowControlsInsetStyle = (
267 isMacosDesktop && !isWindowFullscreen
268 ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET }
269 : {}
270 ) as CSSProperties;
262271 const sidebarProviderStyle = {
263272 "--sidebar-width": `${clampThreadSidebarWidth(sidebarWidth, sidebarMinimumWidth, sidebarMaximumWidth)}px`,
264273 "--panel-animation-duration": `${panelAnimationDurationMs}ms`,
265 ...(isMacosDesktop && !isWindowFullscreen
266 ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET }
267 : {}),
274 ...windowControlsInsetStyle,
268275 } as CSSProperties;
269276
270277 useEffect(() => {
......@@ -329,6 +336,8 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
329336 storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY,
330337 onResize: setSidebarWidth,
331338 }}
339 // The narrow-window sidebar is a sheet, portaled outside the provider's styles.
340 style={windowControlsInsetStyle}
332341 >
333342 {isOnSettings ? (
334343 <>
apps/web/src/components/sidebar/SidebarChrome.tsx+10-6
......@@ -51,16 +51,20 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({
5151 // The titlebar row, not a padded SidebarHeader: it aligns to the window controls.
5252 <div
5353 className={cn(
54 "relative flex h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center gap-2 px-3 md:pl-0",
54 "relative flex h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center gap-2 pr-3",
5555 isElectron && "drag-region",
5656 )}
5757 >
5858 {backdropVariant ? <SidebarStageBackdrop variant={backdropVariant} /> : null}
59 <SidebarTrigger
60 // Over the stage artwork: the media viewer's control-on-imagery treatment.
61 variant={backdropVariant ? "media-navigation" : "ghost"}
62 className="relative top-auto z-10 translate-y-0 md:hidden"
63 />
59 {/* Match the fixed control's inset and border compensation inside the sheet's safe area. */}
60 <div className="relative top-[calc(var(--workspace-controls-top)-env(safe-area-inset-top))] z-10 ml-[calc(var(--workspace-controls-left)+1px-env(safe-area-inset-left))] flex shrink-0 items-center md:hidden">
61 <SidebarTrigger
62 // Over the stage artwork: the media viewer's control-on-imagery treatment.
63 variant={backdropVariant ? "media-navigation" : "ghost"}
64 className="relative top-auto translate-y-0"
65 aria-label="Toggle main sidebar"
66 />
67 </div>
6468 {/* One visible line: the pill wraps onto the clipped second line once it no longer fits.
6569 The padding keeps the brand's focus ring inside the clip. */}
6670 <div className="relative z-10 flex h-8 min-w-0 flex-1 flex-wrap content-start items-center gap-x-2 overflow-hidden py-0.5">
apps/web/src/components/ui/sidebar.tsx+24-3
......@@ -28,12 +28,15 @@ const SIDEBAR_WIDTH = "16rem";
2828const SIDEBAR_WIDTH_MOBILE = "calc(100vw - var(--spacing(3)))";
2929const SIDEBAR_WIDTH_ICON = "3rem";
3030const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16;
31const SIDEBAR_TITLEBAR_CONTROL_SIZE = "max-sm:[--workspace-titlebar-control-size:--spacing(8)]";
3132
3233type SidebarContextProps = {
3334 state: ResponsiveSidebarState;
3435 open: boolean;
3536 setOpen: (open: boolean) => void;
3637 openMobile: boolean;
38 mobileSheetMounted: boolean;
39 setMobileSheetElement: (element: HTMLDivElement | null) => void;
3740 setOpenMobile: (open: boolean) => void;
3841 isMobile: boolean;
3942 toggleSidebar: () => void;
......@@ -106,6 +109,10 @@ function SidebarProvider({
106109}) {
107110 const isMobile = useIsMobile();
108111 const [openMobile, setOpenMobile] = React.useState(false);
112 const [mobileSheetMounted, setMobileSheetMounted] = React.useState(false);
113 const setMobileSheetElement = React.useCallback((element: HTMLDivElement | null) => {
114 setMobileSheetMounted(element !== null);
115 }, []);
109116
110117 // This is the internal state of the sidebar.
111118 // We use openProp and setOpenProp for control from outside the component.
......@@ -145,12 +152,23 @@ function SidebarProvider({
145152 isMobile,
146153 open,
147154 openMobile,
155 mobileSheetMounted,
156 setMobileSheetElement,
148157 setOpen,
149158 setOpenMobile,
150159 state,
151160 toggleSidebar,
152161 }),
153 [state, open, setOpen, isMobile, openMobile, toggleSidebar],
162 [
163 state,
164 open,
165 setOpen,
166 isMobile,
167 openMobile,
168 mobileSheetMounted,
169 setMobileSheetElement,
170 toggleSidebar,
171 ],
154172 );
155173
156174 return (
......@@ -158,7 +176,8 @@ function SidebarProvider({
158176 <div
159177 // Inset layouts opt into bg-sidebar through className.
160178 className={cn(
161 "group/sidebar-wrapper flex min-h-svh w-full max-sm:[--workspace-titlebar-control-size:--spacing(8)]",
179 "group/sidebar-wrapper flex min-h-svh w-full",
180 SIDEBAR_TITLEBAR_CONTROL_SIZE,
162181 className,
163182 )}
164183 data-sidebar-state={state}
......@@ -194,7 +213,7 @@ function Sidebar({
194213 collapsible?: "offcanvas" | "icon" | "none";
195214 resizable?: boolean | SidebarResizableOptions;
196215}) {
197 const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
216 const { isMobile, state, openMobile, setOpenMobile, setMobileSheetElement } = useSidebar();
198217 const resolvedResizable = React.useMemo<SidebarResolvedResizableOptions | null>(() => {
199218 if (isMobile || collapsible === "none" || !resizable) {
200219 return null;
......@@ -236,6 +255,7 @@ function Sidebar({
236255 <SidebarInstanceContext value={instanceContextValue}>
237256 <Sheet onOpenChange={setOpenMobile} open={openMobile}>
238257 <SheetPopup
258 ref={setMobileSheetElement}
239259 className={cn(
240260 "w-(--sidebar-width) max-w-none bg-sidebar surface-grain p-0 text-sidebar-foreground",
241261 className,
......@@ -258,6 +278,7 @@ function Sidebar({
258278 <div
259279 className={cn(
260280 "flex h-full w-full flex-col pb-safe pt-safe",
281 SIDEBAR_TITLEBAR_CONTROL_SIZE,
261282 side === "left" ? "pl-safe" : "pr-safe",
262283 )}
263284 {...props}