From aecb2d4bdc4a8c1a97ba1cf20c360d2d2fb0fbb3 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 4 Nov 2025 18:57:49 -0500 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20mobile=20toggl?= =?UTF-8?q?e=20for=20Review=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Floating Action Button (FAB) to access the right sidebar (Costs/Review panels) on mobile devices where it was previously inaccessible. Changes: - Add FAB button in bottom-right corner (mobile only) - Implement slide-in animation from right (mirrors left sidebar) - Add backdrop overlay with click-to-close - Add close button inside sidebar for explicit dismissal - Update mobile styles to use fixed positioning with transform The right sidebar now slides in from the right on mobile when the FAB is clicked, providing access to the Review and Costs panels that were previously hidden on narrow screens. _Generated with `cmux`_ Change-Id: I16419aef58f515366f5dec90f3e9d27cc368069c Signed-off-by: Test --- src/components/RightSidebar.tsx | 251 +++++++++++++++++++------------- 1 file changed, 151 insertions(+), 100 deletions(-) diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index 3eb7cd7a5..5d94ff054 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -52,9 +52,11 @@ const SidebarContainer: React.FC = ({ "bg-separator border-l border-border-light flex flex-col overflow-hidden flex-shrink-0", customWidth ? "" : "transition-[width] duration-200", collapsed && "sticky right-0 z-10 shadow-[-2px_0_4px_rgba(0,0,0,0.2)]", - "max-md:border-l-0 max-md:border-t max-md:border-border-light", - collapsed && "max-md:w-0 max-md:absolute max-md:bottom-0", - !collapsed && "max-md:w-full max-md:relative max-md:max-h-[50vh]" + // Mobile: slide in from right (similar to left sidebar pattern) + "max-md:fixed max-md:right-0 max-md:top-0 max-md:h-screen max-md:transition-transform max-md:duration-300", + collapsed && "max-md:translate-x-full max-md:shadow-none", + !collapsed && + "max-md:translate-x-0 max-md:w-full max-md:max-w-md max-md:z-[999] max-md:shadow-[-2px_0_8px_rgba(0,0,0,0.5)] max-md:border-l max-md:border-border-light" )} style={{ width }} role={role} @@ -186,112 +188,161 @@ const RightSidebarComponent: React.FC = ({ const verticalMeter = showMeter ? : null; return ( - - {/* Full view when not collapsed */} -
- {/* Render meter when Review tab is active */} - {selectedTab === "review" && ( -
- {verticalMeter} -
- )} + <> + {/* FAB - Floating Action Button for mobile, only visible when collapsed */} + {showCollapsed && ( + + )} - {/* Render resize handle to right of meter when Review tab is active */} - {selectedTab === "review" && onStartResize && ( -
onStartResize(e as unknown as React.MouseEvent)} - /> - )} + {/* Backdrop overlay - only on mobile when sidebar is expanded */} + {!showCollapsed && ( +
setShowCollapsed(true)} + aria-hidden="true" + /> + )} -
-
- - - - {formatKeybind(KEYBINDS.COSTS_TAB)} - - - + + {/* Full view when not collapsed */} +
+ {/* Render meter when Review tab is active */} + {selectedTab === "review" && ( +
+ {verticalMeter} +
+ )} + + {/* Render resize handle to right of meter when Review tab is active */} + {selectedTab === "review" && onStartResize && ( +
onStartResize(e as unknown as React.MouseEvent)} + /> + )} + +
+
+ {/* Close button - only visible on mobile */} - - {formatKeybind(KEYBINDS.REVIEW_TAB)} - - -
-
- {selectedTab === "costs" && ( -
- -
- )} - {selectedTab === "review" && ( -
- -
- )} + + + + + {formatKeybind(KEYBINDS.COSTS_TAB)} + + + + + + {formatKeybind(KEYBINDS.REVIEW_TAB)} + + +
+
+ {selectedTab === "costs" && ( +
+ +
+ )} + {selectedTab === "review" && ( +
+ +
+ )} +
-
- {/* Render meter in collapsed view when sidebar is collapsed */} -
{verticalMeter}
-
+ {/* Render meter in collapsed view when sidebar is collapsed */} +
{verticalMeter}
+ + ); }; From 51761ec88aea1d1407886ae74008dfe13f9701ce Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 4 Nov 2025 18:59:46 -0500 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20correct=20Tailwind=20?= =?UTF-8?q?CSS=20classnames=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix ESLint warnings for Tailwind CSS classnames ordering. _Generated with `cmux`_ Change-Id: I2a9cc2f02ccbf3cce2a4c20cb8f6fccfffb12457 Signed-off-by: Test --- src/components/RightSidebar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index 5d94ff054..d057f0830 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -211,7 +211,7 @@ const RightSidebarComponent: React.FC = ({ {/* Backdrop overlay - only on mobile when sidebar is expanded */} {!showCollapsed && (
setShowCollapsed(true)} aria-hidden="true" /> @@ -247,7 +247,7 @@ const RightSidebarComponent: React.FC = ({
From ebcf022ae6b125e25f00da2ed0c078ce57886f71 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 5 Nov 2025 07:28:11 -0500 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20swipe=20gestur?= =?UTF-8?q?es=20for=20mobile=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add touch gesture support for opening/closing the right sidebar on mobile: - Swipe left from right edge (last 50px) to open sidebar - Swipe right from anywhere to close sidebar - Gestures must be horizontal, at least 50px, and fast (< 300ms) - Provides native app-like interaction for mobile users This complements the FAB button with a more natural gesture-based interaction pattern commonly found in mobile apps. _Generated with `cmux`_ Change-Id: I8b78d1b9d32056a1b02adde4cd17e17375eecfaa Signed-off-by: Test --- src/components/RightSidebar.tsx | 63 +++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index d057f0830..eb21edb5e 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -187,6 +187,69 @@ const RightSidebarComponent: React.FC = ({ const showMeter = showCollapsed || selectedTab === "review"; const verticalMeter = showMeter ? : null; + // Swipe gesture detection for mobile - right-to-left swipe to open sidebar + React.useEffect(() => { + // Only enable swipe on mobile when sidebar is collapsed + if (typeof window === "undefined") return; + + let touchStartX = 0; + let touchStartY = 0; + let touchStartTime = 0; + + const handleTouchStart = (e: TouchEvent) => { + // Only detect swipes from right edge (last ~50px of screen) + const touch = e.touches[0]; + if (!touch) return; + + const screenWidth = window.innerWidth; + if (touch.clientX < screenWidth - 50) return; // Not from right edge + + touchStartX = touch.clientX; + touchStartY = touch.clientY; + touchStartTime = Date.now(); + }; + + const handleTouchEnd = (e: TouchEvent) => { + const touch = e.changedTouches[0]; + if (!touch) return; + + const touchEndX = touch.clientX; + const touchEndY = touch.clientY; + const touchEndTime = Date.now(); + + // Calculate swipe distance and direction + const deltaX = touchEndX - touchStartX; + const deltaY = touchEndY - touchStartY; + const duration = touchEndTime - touchStartTime; + + // Swipe must be: + // 1. Horizontal (more X movement than Y) + // 2. At least 50px distance + // 3. Fast enough (< 300ms) + const isLeftSwipe = deltaX < -50; // Right to left + const isRightSwipe = deltaX > 50; // Left to right + const isHorizontal = Math.abs(deltaX) > Math.abs(deltaY); + const isFastEnough = duration < 300; + + // Open sidebar on left swipe from right edge when collapsed + if (isLeftSwipe && isHorizontal && isFastEnough && showCollapsed) { + setShowCollapsed(false); + } + // Close sidebar on right swipe when open (from anywhere on screen) + else if (isRightSwipe && isHorizontal && isFastEnough && !showCollapsed) { + setShowCollapsed(true); + } + }; + + window.addEventListener("touchstart", handleTouchStart, { passive: true }); + window.addEventListener("touchend", handleTouchEnd, { passive: true }); + + return () => { + window.removeEventListener("touchstart", handleTouchStart); + window.removeEventListener("touchend", handleTouchEnd); + }; + }, [showCollapsed, setShowCollapsed]); + return ( <> {/* FAB - Floating Action Button for mobile, only visible when collapsed */} From 8a861631cc7d81147c12d6ac37c88a2354e4e606 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 5 Nov 2025 07:49:47 -0500 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20move=20mobile=20?= =?UTF-8?q?sidebar=20toggle=20to=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace bottom-right FAB with a header button for better accessibility: - Add panel icon button next to terminal button in WorkspaceHeader - Only visible on mobile (max-md breakpoint) - Remove FAB that was blocking chat input area - Keep swipe gestures and backdrop overlay - Expose sidebar open function via callback prop chain The header button is more discoverable and doesn't interfere with other mobile UI elements like the chat input. _Generated with `cmux`_ Change-Id: Ib26964982a9872748bb43041a39f787f12974de1 Signed-off-by: Test --- src/components/AIView.tsx | 10 ++++++++++ src/components/RightSidebar.tsx | 29 ++++++++++------------------- src/components/WorkspaceHeader.tsx | 20 ++++++++++++++++++++ vite.config.ts | 2 +- 4 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/components/AIView.tsx b/src/components/AIView.tsx index 13991ffeb..57e386a91 100644 --- a/src/components/AIView.tsx +++ b/src/components/AIView.tsx @@ -102,6 +102,12 @@ const AIViewInner: React.FC = ({ chatInputAPI.current?.appendText(note); }, []); + // Ref to store the sidebar open function (for mobile header button) + const openRightSidebarRef = useRef<(() => void) | null>(null); + const handleOpenRightSidebar = useCallback(() => { + openRightSidebarRef.current?.(); + }, []); + // Thinking level state from context const { thinkingLevel: currentWorkspaceThinking, setThinkingLevel } = useThinking(); @@ -328,6 +334,7 @@ const AIViewInner: React.FC = ({ branch={branch} namedWorkspacePath={namedWorkspacePath} runtimeConfig={runtimeConfig} + onOpenRightSidebar={handleOpenRightSidebar} />
@@ -482,6 +489,9 @@ const AIViewInner: React.FC = ({ onStartResize={isReviewTabActive ? startResize : undefined} // Pass resize handler when Review active isResizing={isResizing} // Pass resizing state onReviewNote={handleReviewNote} // Pass review note handler to append to chat + onMountOpenCallback={(openFn) => { + openRightSidebarRef.current = openFn; + }} />
); diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index eb21edb5e..20fd30eaf 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -85,6 +85,8 @@ interface RightSidebarProps { isResizing?: boolean; /** Callback when user adds a review note from Code Review tab */ onReviewNote?: (note: string) => void; + /** Callback to expose the open sidebar function (for mobile header button) */ + onMountOpenCallback?: (openFn: () => void) => void; } const RightSidebarComponent: React.FC = ({ @@ -96,6 +98,7 @@ const RightSidebarComponent: React.FC = ({ onStartResize, isResizing = false, onReviewNote, + onMountOpenCallback, }) => { // Global tab preference (not per-workspace) const [selectedTab, setSelectedTab] = usePersistedState("right-sidebar-tab", "costs"); @@ -187,6 +190,13 @@ const RightSidebarComponent: React.FC = ({ const showMeter = showCollapsed || selectedTab === "review"; const verticalMeter = showMeter ? : null; + // Expose open function to parent (for mobile header button) + React.useEffect(() => { + if (onMountOpenCallback) { + onMountOpenCallback(() => setShowCollapsed(false)); + } + }, [onMountOpenCallback, setShowCollapsed]); + // Swipe gesture detection for mobile - right-to-left swipe to open sidebar React.useEffect(() => { // Only enable swipe on mobile when sidebar is collapsed @@ -252,25 +262,6 @@ const RightSidebarComponent: React.FC = ({ return ( <> - {/* FAB - Floating Action Button for mobile, only visible when collapsed */} - {showCollapsed && ( - - )} - {/* Backdrop overlay - only on mobile when sidebar is expanded */} {!showCollapsed && (
void; } export const WorkspaceHeader: React.FC = ({ @@ -21,6 +22,7 @@ export const WorkspaceHeader: React.FC = ({ branch, namedWorkspacePath, runtimeConfig, + onOpenRightSidebar, }) => { const gitStatus = useGitStatus(workspaceId); const handleOpenTerminal = useCallback(() => { @@ -56,6 +58,24 @@ export const WorkspaceHeader: React.FC = ({ Open in terminal ({formatKeybind(KEYBINDS.OPEN_TERMINAL)}) + + {/* Open sidebar button - only visible on mobile */} + {onOpenRightSidebar && ( + + + + Open review panel + + + )}
); diff --git a/vite.config.ts b/vite.config.ts index 26ef9f70e..7b9377613 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -85,7 +85,7 @@ export default defineConfig(({ mode }) => ({ host: devServerHost, // Configurable via CMUX_VITE_HOST (defaults to 127.0.0.1 for security) port: devServerPort, strictPort: true, - allowedHosts: devServerHost === "0.0.0.0" ? undefined : ["localhost", "127.0.0.1"], + allowedHosts: devServerHost === "0.0.0.0" ? [".ts.net"] : ["localhost", "127.0.0.1"], sourcemapIgnoreList: () => false, // Show all sources in DevTools }, preview: { From cdf522ccd1ed4cafb987c30d4fbc8a3bc8818135 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 5 Nov 2025 08:01:37 -0500 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20maintain=20manual=20o?= =?UTF-8?q?pen=20state=20for=20mobile=20review=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prevent the mobile review panel from immediately auto-collapsing after the user taps the new header button: - Track manual expansions to disable hysteresis auto-collapse until the user dismisses the panel - Wire the mobile header button through AIView -> RightSidebar to open in manual mode - Reuse the same close helper across overlay, close button, and swipe gestures - Remove the bottom FAB entirely in favor of the header entrypoint This fixes the flicker where the panel opened and instantly closed on mobile devices. _Generated with `cmux`_ Change-Id: Id6fbe1a850808355c16a88c76bd43958c522f0dd Signed-off-by: Test --- src/components/RightSidebar.tsx | 174 +++++++++++++++++++------------- 1 file changed, 103 insertions(+), 71 deletions(-) diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index 20fd30eaf..6013decf4 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -167,35 +167,63 @@ const RightSidebarComponent: React.FC = ({ false ); + // Single render point for VerticalTokenMeter + // Shows when: (1) collapsed, OR (2) Review tab is active + const showMeter = showCollapsed || selectedTab === "review"; + const verticalMeter = showMeter ? : null; + + // Track manual expansion to prevent auto-collapse immediately after user opens sidebar + const manualExpandRef = React.useRef(false); + + const openSidebar = React.useCallback( + (manual: boolean) => { + manualExpandRef.current = manual; + setShowCollapsed(false); + }, + [setShowCollapsed] + ); + + const closeSidebar = React.useCallback(() => { + manualExpandRef.current = false; + setShowCollapsed(true); + }, [setShowCollapsed]); + + // Expose open function to parent (for mobile header button) + React.useEffect(() => { + if (onMountOpenCallback) { + onMountOpenCallback(() => openSidebar(true)); + } + }, [onMountOpenCallback, openSidebar]); + + const openSidebarAuto = React.useCallback(() => openSidebar(false), [openSidebar]); + const openSidebarManual = React.useCallback(() => openSidebar(true), [openSidebar]); + React.useEffect(() => { // Never collapse when Review tab is active - code review needs space if (selectedTab === "review") { if (showCollapsed) { - setShowCollapsed(false); + openSidebarAuto(); } + manualExpandRef.current = false; + return; + } + + // If user manually expanded on mobile, keep sidebar open until they close it + if (manualExpandRef.current) { return; } - // Normal hysteresis for Costs/Tools tabs if (chatAreaWidth <= COLLAPSE_THRESHOLD) { - setShowCollapsed(true); + if (!showCollapsed) { + closeSidebar(); + } } else if (chatAreaWidth >= EXPAND_THRESHOLD) { - setShowCollapsed(false); + if (showCollapsed) { + openSidebarAuto(); + } } // Between thresholds: maintain current state (no change) - }, [chatAreaWidth, selectedTab, showCollapsed, setShowCollapsed]); - - // Single render point for VerticalTokenMeter - // Shows when: (1) collapsed, OR (2) Review tab is active - const showMeter = showCollapsed || selectedTab === "review"; - const verticalMeter = showMeter ? : null; - - // Expose open function to parent (for mobile header button) - React.useEffect(() => { - if (onMountOpenCallback) { - onMountOpenCallback(() => setShowCollapsed(false)); - } - }, [onMountOpenCallback, setShowCollapsed]); + }, [chatAreaWidth, selectedTab, showCollapsed, closeSidebar, openSidebarAuto]); // Swipe gesture detection for mobile - right-to-left swipe to open sidebar React.useEffect(() => { @@ -243,11 +271,11 @@ const RightSidebarComponent: React.FC = ({ // Open sidebar on left swipe from right edge when collapsed if (isLeftSwipe && isHorizontal && isFastEnough && showCollapsed) { - setShowCollapsed(false); + openSidebarManual(); } // Close sidebar on right swipe when open (from anywhere on screen) else if (isRightSwipe && isHorizontal && isFastEnough && !showCollapsed) { - setShowCollapsed(true); + closeSidebar(); } }; @@ -258,7 +286,7 @@ const RightSidebarComponent: React.FC = ({ window.removeEventListener("touchstart", handleTouchStart); window.removeEventListener("touchend", handleTouchEnd); }; - }, [showCollapsed, setShowCollapsed]); + }, [showCollapsed, closeSidebar, openSidebarManual]); return ( <> @@ -266,7 +294,7 @@ const RightSidebarComponent: React.FC = ({ {!showCollapsed && (
setShowCollapsed(true)} + onClick={closeSidebar} aria-hidden="true" /> )} @@ -301,68 +329,72 @@ const RightSidebarComponent: React.FC = ({
{/* Close button - only visible on mobile */} - - - - {formatKeybind(KEYBINDS.COSTS_TAB)} - - - - - - {formatKeybind(KEYBINDS.REVIEW_TAB)} - - +
+ + + + {formatKeybind(KEYBINDS.COSTS_TAB)} + + + + + + {formatKeybind(KEYBINDS.REVIEW_TAB)} + + +
Date: Wed, 5 Nov 2025 11:14:10 -0500 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20prevent=20mobile=20ba?= =?UTF-8?q?ckdrop=20from=20intercepting=20review=20taps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure taps inside the expanded mobile review panel do not register as backdrop clicks: - Forward ref SidebarContainer to measure actual rendered width - Track viewport and sidebar widths to size the backdrop hit area - Limit backdrop click target to the space outside the panel - Keep interior close controls and swipe gestures intact _Generated with `cmux`_ Change-Id: I5817e2a466875325839ca3059d47f43b8ce94ed8 Signed-off-by: Test --- src/components/RightSidebar.tsx | 152 +++++++++++++++++++++++--------- 1 file changed, 112 insertions(+), 40 deletions(-) diff --git a/src/components/RightSidebar.tsx b/src/components/RightSidebar.tsx index 6013decf4..8e0f9d63c 100644 --- a/src/components/RightSidebar.tsx +++ b/src/components/RightSidebar.tsx @@ -20,6 +20,7 @@ interface SidebarContainerProps { role: string; "aria-label": string; } +const MOBILE_DEFAULT_WIDTH = 360; /** * SidebarContainer - Main sidebar wrapper with dynamic width @@ -30,42 +31,40 @@ interface SidebarContainerProps { * 3. wide - Auto-calculated max width for Review tab (when not resizing) * 4. default (300px) - Costs/Tools tabs */ -const SidebarContainer: React.FC = ({ - collapsed, - wide, - customWidth, - children, - role, - "aria-label": ariaLabel, -}) => { - const width = collapsed - ? "20px" - : customWidth - ? `${customWidth}px` - : wide - ? "min(1200px, calc(100vw - 400px))" - : "300px"; +const SidebarContainer = React.forwardRef( + ({ collapsed, wide, customWidth, children, role, "aria-label": ariaLabel }, ref) => { + const width = collapsed + ? "20px" + : customWidth + ? `${customWidth}px` + : wide + ? "min(1200px, calc(100vw - 400px))" + : "300px"; + + return ( +
+ {children} +
+ ); + } +); - return ( -
- {children} -
- ); -}; +SidebarContainer.displayName = "SidebarContainer"; type TabType = "costs" | "review"; @@ -174,6 +173,9 @@ const RightSidebarComponent: React.FC = ({ // Track manual expansion to prevent auto-collapse immediately after user opens sidebar const manualExpandRef = React.useRef(false); + const sidebarRef = React.useRef(null); + const [measuredSidebarWidth, setMeasuredSidebarWidth] = React.useState(0); + const [viewportWidth, setViewportWidth] = React.useState(0); const openSidebar = React.useCallback( (manual: boolean) => { @@ -226,6 +228,68 @@ const RightSidebarComponent: React.FC = ({ }, [chatAreaWidth, selectedTab, showCollapsed, closeSidebar, openSidebarAuto]); // Swipe gesture detection for mobile - right-to-left swipe to open sidebar + React.useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const updateViewportWidth = () => { + setViewportWidth(window.innerWidth); + }; + + updateViewportWidth(); + window.addEventListener("resize", updateViewportWidth); + return () => window.removeEventListener("resize", updateViewportWidth); + }, []); + + React.useEffect(() => { + const element = sidebarRef.current; + if (!element) { + return; + } + + const updateMeasuredWidth = () => { + setMeasuredSidebarWidth(element.getBoundingClientRect().width); + }; + + updateMeasuredWidth(); + + if (typeof ResizeObserver === "undefined") { + return; + } + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setMeasuredSidebarWidth(entry.contentRect.width); + } + }); + + observer.observe(element); + return () => observer.disconnect(); + }, [showCollapsed, selectedTab, width]); + + const effectiveSidebarWidth = React.useMemo(() => { + if (!viewportWidth) { + return null; + } + + const candidate = + (measuredSidebarWidth > 0 ? measuredSidebarWidth : undefined) ?? + (typeof width === "number" && width > 0 ? width : undefined) ?? + MOBILE_DEFAULT_WIDTH; + const sanitized = candidate > 0 ? candidate : MOBILE_DEFAULT_WIDTH; + return Math.min(sanitized, viewportWidth); + }, [measuredSidebarWidth, width, viewportWidth]); + + const overlayClickableWidth = React.useMemo(() => { + if (showCollapsed || !viewportWidth) { + return 0; + } + + const sidebarWidthForCalc = effectiveSidebarWidth ?? MOBILE_DEFAULT_WIDTH; + return Math.max(viewportWidth - sidebarWidthForCalc, 0); + }, [effectiveSidebarWidth, showCollapsed, viewportWidth]); + React.useEffect(() => { // Only enable swipe on mobile when sidebar is collapsed if (typeof window === "undefined") return; @@ -292,14 +356,22 @@ const RightSidebarComponent: React.FC = ({ <> {/* Backdrop overlay - only on mobile when sidebar is expanded */} {!showCollapsed && ( -