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 {...@@ -83,7 +83,7 @@ function readInitialThreadSidebarWidth(): number {
83function SidebarControl() {83function SidebarControl() {
84 const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" });84 const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" });
85 const keybindings = useAtomValue(primaryServerKeybindingsAtom);85 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
86 const { toggleSidebar } = useSidebar();86 const { toggleSidebar, isMobile, mobileSheetMounted } = useSidebar();
87 const isSidebarVisible = useSidebarVisibility();87 const isSidebarVisible = useSidebarVisibility();
88 const environmentIdentificationMode = useEnvironmentIdentificationMode();88 const environmentIdentificationMode = useEnvironmentIdentificationMode();
89 const stageBackdropVariant = useSidebarStageBackdropVariant(89 const stageBackdropVariant = useSidebarStageBackdropVariant(
...@@ -138,8 +138,12 @@ function SidebarControl() {...@@ -138,8 +138,12 @@ function SidebarControl() {
138 // The right-side layout controls carry mr-px (border compensation inside138 // The right-side layout controls carry mr-px (border compensation inside
139 // the panel), so the trigger mirrors it: both clusters sit one extra pixel139 // the panel), so the trigger mirrors it: both clusters sit one extra pixel
140 // off their edge and the titlebar reads symmetric.140 // off their edge and the titlebar reads symmetric.
141 // The sheet owns its trigger until its closing transition unmounts it.
141 <div142 <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 )}
143 data-sidebar-control=""147 data-sidebar-control=""
144 >148 >
145 <Tooltip>149 <Tooltip>
...@@ -259,12 +263,15 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {...@@ -259,12 +263,15 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
259 ? getWindowFullscreenState()263 ? getWindowFullscreenState()
260 : false;264 : false;
261 });265 });
266 const windowControlsInsetStyle = (
267 isMacosDesktop && !isWindowFullscreen
268 ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET }
269 : {}
270 ) as CSSProperties;
262 const sidebarProviderStyle = {271 const sidebarProviderStyle = {
263 "--sidebar-width": `${clampThreadSidebarWidth(sidebarWidth, sidebarMinimumWidth, sidebarMaximumWidth)}px`,272 "--sidebar-width": `${clampThreadSidebarWidth(sidebarWidth, sidebarMinimumWidth, sidebarMaximumWidth)}px`,
264 "--panel-animation-duration": `${panelAnimationDurationMs}ms`,273 "--panel-animation-duration": `${panelAnimationDurationMs}ms`,
265 ...(isMacosDesktop && !isWindowFullscreen274 ...windowControlsInsetStyle,
266 ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET }
267 : {}),
268 } as CSSProperties;275 } as CSSProperties;
269276
270 useEffect(() => {277 useEffect(() => {
...@@ -329,6 +336,8 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {...@@ -329,6 +336,8 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
329 storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY,336 storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY,
330 onResize: setSidebarWidth,337 onResize: setSidebarWidth,
331 }}338 }}
339 // The narrow-window sidebar is a sheet, portaled outside the provider's styles.
340 style={windowControlsInsetStyle}
332 >341 >
333 {isOnSettings ? (342 {isOnSettings ? (
334 <>343 <>
apps/web/src/components/sidebar/SidebarChrome.tsx+10-6
...@@ -51,16 +51,20 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({...@@ -51,16 +51,20 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({
51 // The titlebar row, not a padded SidebarHeader: it aligns to the window controls.51 // The titlebar row, not a padded SidebarHeader: it aligns to the window controls.
52 <div52 <div
53 className={cn(53 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",
55 isElectron && "drag-region",55 isElectron && "drag-region",
56 )}56 )}
57 >57 >
58 {backdropVariant ? <SidebarStageBackdrop variant={backdropVariant} /> : null}58 {backdropVariant ? <SidebarStageBackdrop variant={backdropVariant} /> : null}
59 <SidebarTrigger59 {/* Match the fixed control's inset and border compensation inside the sheet's safe area. */}
60 // Over the stage artwork: the media viewer's control-on-imagery treatment.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 variant={backdropVariant ? "media-navigation" : "ghost"}61 <SidebarTrigger
62 className="relative top-auto z-10 translate-y-0 md:hidden"62 // Over the stage artwork: the media viewer's control-on-imagery treatment.
63 />63 variant={backdropVariant ? "media-navigation" : "ghost"}
64 className="relative top-auto translate-y-0"
65 aria-label="Toggle main sidebar"
66 />
67 </div>
64 {/* One visible line: the pill wraps onto the clipped second line once it no longer fits.68 {/* One visible line: the pill wraps onto the clipped second line once it no longer fits.
65 The padding keeps the brand's focus ring inside the clip. */}69 The padding keeps the brand's focus ring inside the clip. */}
66 <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">70 <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";...@@ -28,12 +28,15 @@ const SIDEBAR_WIDTH = "16rem";
28const SIDEBAR_WIDTH_MOBILE = "calc(100vw - var(--spacing(3)))";28const SIDEBAR_WIDTH_MOBILE = "calc(100vw - var(--spacing(3)))";
29const SIDEBAR_WIDTH_ICON = "3rem";29const SIDEBAR_WIDTH_ICON = "3rem";
30const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16;30const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16;
31const SIDEBAR_TITLEBAR_CONTROL_SIZE = "max-sm:[--workspace-titlebar-control-size:--spacing(8)]";
3132
32type SidebarContextProps = {33type SidebarContextProps = {
33 state: ResponsiveSidebarState;34 state: ResponsiveSidebarState;
34 open: boolean;35 open: boolean;
35 setOpen: (open: boolean) => void;36 setOpen: (open: boolean) => void;
36 openMobile: boolean;37 openMobile: boolean;
38 mobileSheetMounted: boolean;
39 setMobileSheetElement: (element: HTMLDivElement | null) => void;
37 setOpenMobile: (open: boolean) => void;40 setOpenMobile: (open: boolean) => void;
38 isMobile: boolean;41 isMobile: boolean;
39 toggleSidebar: () => void;42 toggleSidebar: () => void;
...@@ -106,6 +109,10 @@ function SidebarProvider({...@@ -106,6 +109,10 @@ function SidebarProvider({
106}) {109}) {
107 const isMobile = useIsMobile();110 const isMobile = useIsMobile();
108 const [openMobile, setOpenMobile] = React.useState(false);111 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
110 // This is the internal state of the sidebar.117 // This is the internal state of the sidebar.
111 // We use openProp and setOpenProp for control from outside the component.118 // We use openProp and setOpenProp for control from outside the component.
...@@ -145,12 +152,23 @@ function SidebarProvider({...@@ -145,12 +152,23 @@ function SidebarProvider({
145 isMobile,152 isMobile,
146 open,153 open,
147 openMobile,154 openMobile,
155 mobileSheetMounted,
156 setMobileSheetElement,
148 setOpen,157 setOpen,
149 setOpenMobile,158 setOpenMobile,
150 state,159 state,
151 toggleSidebar,160 toggleSidebar,
152 }),161 }),
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 ],
154 );172 );
155173
156 return (174 return (
...@@ -158,7 +176,8 @@ function SidebarProvider({...@@ -158,7 +176,8 @@ function SidebarProvider({
158 <div176 <div
159 // Inset layouts opt into bg-sidebar through className.177 // Inset layouts opt into bg-sidebar through className.
160 className={cn(178 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,
162 className,181 className,
163 )}182 )}
164 data-sidebar-state={state}183 data-sidebar-state={state}
...@@ -194,7 +213,7 @@ function Sidebar({...@@ -194,7 +213,7 @@ function Sidebar({
194 collapsible?: "offcanvas" | "icon" | "none";213 collapsible?: "offcanvas" | "icon" | "none";
195 resizable?: boolean | SidebarResizableOptions;214 resizable?: boolean | SidebarResizableOptions;
196}) {215}) {
197 const { isMobile, state, openMobile, setOpenMobile } = useSidebar();216 const { isMobile, state, openMobile, setOpenMobile, setMobileSheetElement } = useSidebar();
198 const resolvedResizable = React.useMemo<SidebarResolvedResizableOptions | null>(() => {217 const resolvedResizable = React.useMemo<SidebarResolvedResizableOptions | null>(() => {
199 if (isMobile || collapsible === "none" || !resizable) {218 if (isMobile || collapsible === "none" || !resizable) {
200 return null;219 return null;
...@@ -236,6 +255,7 @@ function Sidebar({...@@ -236,6 +255,7 @@ function Sidebar({
236 <SidebarInstanceContext value={instanceContextValue}>255 <SidebarInstanceContext value={instanceContextValue}>
237 <Sheet onOpenChange={setOpenMobile} open={openMobile}>256 <Sheet onOpenChange={setOpenMobile} open={openMobile}>
238 <SheetPopup257 <SheetPopup
258 ref={setMobileSheetElement}
239 className={cn(259 className={cn(
240 "w-(--sidebar-width) max-w-none bg-sidebar surface-grain p-0 text-sidebar-foreground",260 "w-(--sidebar-width) max-w-none bg-sidebar surface-grain p-0 text-sidebar-foreground",
241 className,261 className,
...@@ -258,6 +278,7 @@ function Sidebar({...@@ -258,6 +278,7 @@ function Sidebar({
258 <div278 <div
259 className={cn(279 className={cn(
260 "flex h-full w-full flex-col pb-safe pt-safe",280 "flex h-full w-full flex-col pb-safe pt-safe",
281 SIDEBAR_TITLEBAR_CONTROL_SIZE,
261 side === "left" ? "pl-safe" : "pr-safe",282 side === "left" ? "pl-safe" : "pr-safe",
262 )}283 )}
263 {...props}284 {...props}