Skip to content

Commit db207f9

Browse files
committed
fix(sidebar): collapsed sidebar shows single icons with hover dropdown menus
1 parent 5ba3118 commit db207f9

File tree

8 files changed

+402
-121
lines changed

8 files changed

+402
-121
lines changed

apps/sim/app/_styles/globals.css

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,27 @@
3333
opacity: 0;
3434
}
3535

36+
html[data-sidebar-collapsed] .sidebar-container span,
37+
html[data-sidebar-collapsed] .sidebar-container .text-small {
38+
opacity: 0;
39+
}
40+
3641
.sidebar-container .sidebar-collapse-hide {
3742
transition: opacity 60ms ease;
3843
}
3944

40-
.sidebar-container[data-collapsed] .sidebar-collapse-hide {
45+
.sidebar-container[data-collapsed] .sidebar-collapse-hide,
46+
html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-hide {
47+
opacity: 0;
48+
}
49+
50+
.sidebar-container[data-collapsed] .sidebar-collapse-remove,
51+
html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-remove {
52+
display: none;
53+
}
54+
55+
html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-btn {
56+
width: 0;
4157
opacity: 0;
4258
}
4359

apps/sim/app/layout.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
114114
115115
if (isCollapsed) {
116116
document.documentElement.style.setProperty('--sidebar-width', '51px');
117+
document.documentElement.setAttribute('data-sidebar-collapsed', '');
117118
} else {
118119
var width = state && state.sidebarWidth;
119120
var maxSidebarWidth = window.innerWidth * 0.3;

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export'
1717
import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
1818
import type { ChatContext } from '@/stores/panel'
19+
import { useSidebarStore } from '@/stores/sidebar/store'
1920
import {
2021
MessageContent,
2122
MothershipView,
@@ -166,6 +167,8 @@ export function Home({ chatId }: HomeProps = {}) {
166167

167168
const handleResourceEvent = useCallback(() => {
168169
if (isResourceCollapsedRef.current) {
170+
const { isCollapsed, toggleCollapsed } = useSidebarStore.getState()
171+
if (!isCollapsed) toggleCollapsed()
169172
setIsResourceCollapsed(false)
170173
setIsResourceAnimatingIn(true)
171174
}

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
useSidebarDragContextValue,
1414
useWorkflowSelection,
1515
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
16+
import {
17+
compareByOrder,
18+
groupWorkflowsByFolder,
19+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
1620
import { useFolders } from '@/hooks/queries/folders'
1721
import { useFolderStore } from '@/stores/folders/store'
1822
import type { FolderTreeNode } from '@/stores/folders/types'
@@ -22,17 +26,6 @@ const TREE_SPACING = {
2226
INDENT_PER_LEVEL: 20,
2327
} as const
2428

25-
function compareByOrder<T extends { sortOrder: number; createdAt?: Date; id: string }>(
26-
a: T,
27-
b: T
28-
): number {
29-
if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder
30-
const timeA = a.createdAt?.getTime() ?? 0
31-
const timeB = b.createdAt?.getTime() ?? 0
32-
if (timeA !== timeB) return timeA - timeB
33-
return a.id.localeCompare(b.id)
34-
}
35-
3629
interface WorkflowListProps {
3730
workspaceId: string
3831
workflowId: string | undefined
@@ -129,21 +122,10 @@ export const WorkflowList = memo(function WorkflowList({
129122
return activeWorkflow?.folderId || null
130123
}, [workflowId, regularWorkflows, isLoading, foldersLoading])
131124

132-
const workflowsByFolder = useMemo(() => {
133-
const grouped = regularWorkflows.reduce(
134-
(acc, workflow) => {
135-
const folderId = workflow.folderId || 'root'
136-
if (!acc[folderId]) acc[folderId] = []
137-
acc[folderId].push(workflow)
138-
return acc
139-
},
140-
{} as Record<string, WorkflowMetadata[]>
141-
)
142-
for (const folderId of Object.keys(grouped)) {
143-
grouped[folderId].sort(compareByOrder)
144-
}
145-
return grouped
146-
}, [regularWorkflows])
125+
const workflowsByFolder = useMemo(
126+
() => groupWorkflowsByFolder(regularWorkflows),
127+
[regularWorkflows]
128+
)
147129

148130
const orderedWorkflowIds = useMemo(() => {
149131
const ids: string[] = []

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export { type DropIndicator, useDragDrop } from './use-drag-drop'
44
export { useFolderExpand } from './use-folder-expand'
55
export { useFolderOperations } from './use-folder-operations'
66
export { useFolderSelection } from './use-folder-selection'
7+
export { useHoverMenu } from './use-hover-menu'
78
export { useItemDrag } from './use-item-drag'
89
export { useItemRename } from './use-item-rename'
910
export {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2+
3+
const CLOSE_DELAY_MS = 150
4+
5+
const preventAutoFocus = (e: Event) => e.preventDefault()
6+
7+
/**
8+
* Manages hover-triggered dropdown menu state.
9+
* Provides handlers for trigger and content mouse events with a delay
10+
* to prevent flickering when moving between trigger and content.
11+
*/
12+
export function useHoverMenu() {
13+
const [isOpen, setIsOpen] = useState(false)
14+
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
15+
16+
const cancelClose = useCallback(() => {
17+
if (closeTimerRef.current) {
18+
clearTimeout(closeTimerRef.current)
19+
closeTimerRef.current = null
20+
}
21+
}, [])
22+
23+
useEffect(() => {
24+
return () => {
25+
if (closeTimerRef.current) {
26+
clearTimeout(closeTimerRef.current)
27+
}
28+
}
29+
}, [])
30+
31+
const scheduleClose = useCallback(() => {
32+
cancelClose()
33+
closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_DELAY_MS)
34+
}, [cancelClose])
35+
36+
const open = useCallback(() => {
37+
cancelClose()
38+
setIsOpen(true)
39+
}, [cancelClose])
40+
41+
const close = useCallback(() => {
42+
cancelClose()
43+
setIsOpen(false)
44+
}, [cancelClose])
45+
46+
const triggerProps = useMemo(
47+
() => ({ onMouseEnter: open, onMouseLeave: scheduleClose }) as const,
48+
[open, scheduleClose]
49+
)
50+
51+
const contentProps = useMemo(
52+
() =>
53+
({
54+
onMouseEnter: cancelClose,
55+
onMouseLeave: scheduleClose,
56+
onCloseAutoFocus: preventAutoFocus,
57+
}) as const,
58+
[cancelClose, scheduleClose]
59+
)
60+
61+
return { isOpen, close, triggerProps, contentProps }
62+
}

0 commit comments

Comments
 (0)