Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/web-ui/src/app/layout/WorkspaceBody.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,27 @@ $_workbench-gap: 6px;
width: 0;
overflow: hidden;
pointer-events: none;

// Hover-to-expand flyout (issue #1039): when the collapsed sidebar is
// hovered, the full nav panel becomes a floating overlay so the user can
// peek at content without committing to a permanent expand.
&.is-hover-expanded {
position: absolute;
top: 0;
bottom: $_workbench-gap;
left: $_workbench-gap;
z-index: $z-dropdown;
flex: none;
flex-basis: auto;
width: var(--nav-width);
min-width: 0;
overflow: visible;
pointer-events: auto;
border-radius: $size-radius-md;
box-shadow: var(--bf-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.24));
background: var(--bf-appearance-token-color-bg-primary);
transition: width 150ms ease, opacity 150ms ease;
}
}
}

Expand Down Expand Up @@ -120,6 +141,10 @@ $_workbench-gap: 6px;
left: calc(#{$_workbench-gap} + 72px);
}

.bitfun-app-layout--macos .bitfun-workspace-body__nav-area.is-collapsed.is-hover-expanded {
left: calc(#{$_workbench-gap} + 72px);
}

.bitfun-app-layout--macos .bitfun-workspace-body__nav-area.is-collapsed + .bitfun-workspace-body__scene-area {
.bitfun-scene-bar {
padding-left: calc(#{$_nav-collapsed-width} + #{$size-gap-1} + 72px);
Expand All @@ -143,4 +168,8 @@ $_workbench-gap: 6px;
.bitfun-workspace-body__nav-divider::after {
transition: none;
}

.bitfun-workspace-body__nav-area.is-collapsed.is-hover-expanded {
transition: none;
}
}
69 changes: 66 additions & 3 deletions src/web-ui/src/app/layout/WorkspaceBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const NAV_DEFAULT_WIDTH = 240;
const NAV_MIN_WIDTH = 240;
const NAV_MAX_WIDTH = 480;
const COLLAPSE_THRESHOLD = 64;
const HOVER_EXPAND_DELAY = 200;
const HOVER_COLLAPSE_DELAY = 200;

interface WorkspaceBodyProps {
className?: string;
Expand All @@ -49,16 +51,36 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
const { state, toggleLeftPanel } = useApp();
const isNavCollapsed = state.layout.leftPanelCollapsed;
const [navWidth, setNavWidth] = useState(NAV_DEFAULT_WIDTH);
const [isHoverExpanded, setIsHoverExpanded] = useState(false);
const navAreaRef = useRef<HTMLDivElement>(null);
const navDividerRef = useRef<HTMLDivElement>(null);
// Active drag cleanup, so window listeners never leak if we unmount mid-drag.
const dragCleanupRef = useRef<(() => void) | null>(null);
// Hover-expand/collapse timeout, shared between collapsed-nav and nav-area.
const hoverTimeoutRef = useRef<number | null>(null);

// Clear any pending hover timeout (expand or collapse).
const clearHoverTimeout = useCallback(() => {
if (hoverTimeoutRef.current !== null) {
clearTimeout(hoverTimeoutRef.current);
hoverTimeoutRef.current = null;
}
}, []);

// When the panel is permanently expanded (toggleLeftPanel), reset hover state.
useEffect(() => {
if (!isNavCollapsed) {
clearHoverTimeout();
setIsHoverExpanded(false);
}
}, [isNavCollapsed, clearHoverTimeout]);

useEffect(() => {
return () => {
dragCleanupRef.current?.();
clearHoverTimeout();
};
}, []);
}, [clearHoverTimeout]);

const handleNavCollapseDragStart = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
if (event.button !== 0 || isNavCollapsed) return;
Expand Down Expand Up @@ -130,6 +152,39 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
dragCleanupRef.current = cleanup;
}, [isNavCollapsed, navWidth, toggleLeftPanel]);

// --- Hover-to-expand (issue #1039) ---
// When the sidebar is collapsed, hovering the collapsed strip temporarily
// expands the nav panel as a floating overlay. A shared timeout lets the
// cursor move between the collapsed strip and the flyout without collapsing.

const scheduleHoverExpand = useCallback(() => {
clearHoverTimeout();
hoverTimeoutRef.current = window.setTimeout(() => {
setIsHoverExpanded(true);
hoverTimeoutRef.current = null;
}, HOVER_EXPAND_DELAY);
}, [clearHoverTimeout]);

const cancelHoverExpand = useCallback(() => {
clearHoverTimeout();
}, [clearHoverTimeout]);

const scheduleHoverCollapse = useCallback(() => {
clearHoverTimeout();
hoverTimeoutRef.current = window.setTimeout(() => {
setIsHoverExpanded(false);
hoverTimeoutRef.current = null;
}, HOVER_COLLAPSE_DELAY);
}, [clearHoverTimeout]);

// Entering either the collapsed strip or the flyout itself keeps it open.
const handleNavHoverEnter = useCallback(() => {
clearHoverTimeout();
setIsHoverExpanded(true);
}, [clearHoverTimeout]);

const showHoverFlyout = isNavCollapsed && isHoverExpanded;

return (
<div
className={`bitfun-workspace-body${isEntering ? ' is-entering' : ''}${isExiting ? ' is-exiting' : ''} ${className}`}
Expand All @@ -138,19 +193,27 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
data-bf-state={isNavCollapsed ? 'collapsed' : undefined}
>
{isNavCollapsed && (
<div className="bitfun-workspace-body__collapsed-nav" data-bf-scene="workbench" data-bf-part="collapsedNav">
<div
className="bitfun-workspace-body__collapsed-nav"
data-bf-scene="workbench"
data-bf-part="collapsedNav"
onMouseEnter={scheduleHoverExpand}
onMouseLeave={cancelHoverExpand}
>
<NavBar isCollapsed onExpandNav={toggleLeftPanel} onMaximize={onMaximize} />
</div>
)}

{/* Left: nav history bar + navigation sidebar — always rendered for slide animation */}
<div
ref={navAreaRef}
className={`bitfun-workspace-body__nav-area${isNavCollapsed ? ' is-collapsed' : ''}`}
className={`bitfun-workspace-body__nav-area${isNavCollapsed ? ' is-collapsed' : ''}${showHoverFlyout ? ' is-hover-expanded' : ''}`}
style={isNavCollapsed ? undefined : { '--nav-width': `${navWidth}px` } as React.CSSProperties}
data-bf-scene="workbench"
data-bf-part="navArea"
data-bf-state={isNavCollapsed ? 'collapsed' : undefined}
onMouseEnter={showHoverFlyout ? handleNavHoverEnter : undefined}
onMouseLeave={showHoverFlyout ? scheduleHoverCollapse : undefined}
>
<NavBar onExpandNav={toggleLeftPanel} onMaximize={onMaximize} />
<NavPanel className="bitfun-workspace-body__nav-panel" />
Expand Down