diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index 00d5c05ef089029f04a18ed6fbb64b5d3a89b1ca..fd56ee8ca5ac9de20a7cf284361ff8465a048b0f 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -83,7 +83,7 @@ function readInitialThreadSidebarWidth(): number { function SidebarControl() { const usagePageOpen = useLocation({ select: (location) => location.pathname === "/usage" }); const keybindings = useAtomValue(primaryServerKeybindingsAtom); - const { toggleSidebar } = useSidebar(); + const { toggleSidebar, isMobile, mobileSheetMounted } = useSidebar(); const isSidebarVisible = useSidebarVisibility(); const environmentIdentificationMode = useEnvironmentIdentificationMode(); const stageBackdropVariant = useSidebarStageBackdropVariant( @@ -138,8 +138,12 @@ function SidebarControl() { // The right-side layout controls carry mr-px (border compensation inside // the panel), so the trigger mirrors it: both clusters sit one extra pixel // off their edge and the titlebar reads symmetric. + // The sheet owns its trigger until its closing transition unmounts it.
@@ -259,12 +263,15 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { ? getWindowFullscreenState() : false; }); + const windowControlsInsetStyle = ( + isMacosDesktop && !isWindowFullscreen + ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } + : {} + ) as CSSProperties; const sidebarProviderStyle = { "--sidebar-width": `${clampThreadSidebarWidth(sidebarWidth, sidebarMinimumWidth, sidebarMaximumWidth)}px`, "--panel-animation-duration": `${panelAnimationDurationMs}ms`, - ...(isMacosDesktop && !isWindowFullscreen - ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } - : {}), + ...windowControlsInsetStyle, } as CSSProperties; useEffect(() => { @@ -329,6 +336,8 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) { storageKey: THREAD_SIDEBAR_WIDTH_STORAGE_KEY, onResize: setSidebarWidth, }} + // The narrow-window sidebar is a sheet, portaled outside the provider's styles. + style={windowControlsInsetStyle} > {isOnSettings ? ( <> diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index 716f96ceecd238aa7f439881c9eee2c5486462d2..ae05529374c38f0c2c304e8ceedeec94828962d6 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -51,16 +51,20 @@ export const SidebarChromeHeader = memo(function SidebarChromeHeader({ // The titlebar row, not a padded SidebarHeader: it aligns to the window controls.
{backdropVariant ? : null} - + {/* Match the fixed control's inset and border compensation inside the sheet's safe area. */} +
+ +
{/* One visible line: the pill wraps onto the clipped second line once it no longer fits. The padding keeps the brand's focus ring inside the clip. */}
diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index c1bd6b277c5d8c021d52b91efc64f01f193733af..a22e3c44bbdd50ecbf79777afa9380c5ee4c34c5 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -28,12 +28,15 @@ const SIDEBAR_WIDTH = "16rem"; const SIDEBAR_WIDTH_MOBILE = "calc(100vw - var(--spacing(3)))"; const SIDEBAR_WIDTH_ICON = "3rem"; const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16; +const SIDEBAR_TITLEBAR_CONTROL_SIZE = "max-sm:[--workspace-titlebar-control-size:--spacing(8)]"; type SidebarContextProps = { state: ResponsiveSidebarState; open: boolean; setOpen: (open: boolean) => void; openMobile: boolean; + mobileSheetMounted: boolean; + setMobileSheetElement: (element: HTMLDivElement | null) => void; setOpenMobile: (open: boolean) => void; isMobile: boolean; toggleSidebar: () => void; @@ -106,6 +109,10 @@ function SidebarProvider({ }) { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); + const [mobileSheetMounted, setMobileSheetMounted] = React.useState(false); + const setMobileSheetElement = React.useCallback((element: HTMLDivElement | null) => { + setMobileSheetMounted(element !== null); + }, []); // This is the internal state of the sidebar. // We use openProp and setOpenProp for control from outside the component. @@ -145,12 +152,23 @@ function SidebarProvider({ isMobile, open, openMobile, + mobileSheetMounted, + setMobileSheetElement, setOpen, setOpenMobile, state, toggleSidebar, }), - [state, open, setOpen, isMobile, openMobile, toggleSidebar], + [ + state, + open, + setOpen, + isMobile, + openMobile, + mobileSheetMounted, + setMobileSheetElement, + toggleSidebar, + ], ); return ( @@ -158,7 +176,8 @@ function SidebarProvider({
(() => { if (isMobile || collapsible === "none" || !resizable) { return null; @@ -236,6 +255,7 @@ function Sidebar({