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