| 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,7 +83,7 @@ function readInitialThreadSidebarWidth(): number { |
| 83 | function SidebarControl() { | 83 | function 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 inside | 138 | // The right-side layout controls carry mr-px (border compensation inside |
| 139 | // the panel), so the trigger mirrors it: both clusters sit one extra pixel | 139 | // 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 | <div | 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 | 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 && !isWindowFullscreen | 274 | ...windowControlsInsetStyle, |
| 266 | ? { "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } | ||
| 267 | : {}), | ||
| 268 | } as CSSProperties; | 275 | } as CSSProperties; |
| 269 | 276 | ||
| 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 | <div | 52 | <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 | <SidebarTrigger | 59 | {/* 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"; |
| 28 | const SIDEBAR_WIDTH_MOBILE = "calc(100vw - var(--spacing(3)))"; | 28 | const SIDEBAR_WIDTH_MOBILE = "calc(100vw - var(--spacing(3)))"; |
| 29 | const SIDEBAR_WIDTH_ICON = "3rem"; | 29 | const SIDEBAR_WIDTH_ICON = "3rem"; |
| 30 | const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16; | 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 | type SidebarContextProps = { | 33 | type 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 | }, []); | ||
| 109 | 116 | ||
| 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 | ); |
| 155 | 173 | ||
| 156 | return ( | 174 | return ( |
| ... | @@ -158,7 +176,8 @@ function SidebarProvider({ | ... | @@ -158,7 +176,8 @@ function SidebarProvider({ |
| 158 | <div | 176 | <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 | <SheetPopup | 257 | <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 | <div | 278 | <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} |