From db207f91ac1b3e69f20fc35c716654a0701acef7 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Mar 2026 14:45:15 -0700 Subject: [PATCH 1/7] fix(sidebar): collapsed sidebar shows single icons with hover dropdown menus --- apps/sim/app/_styles/globals.css | 18 +- apps/sim/app/layout.tsx | 1 + .../app/workspace/[workspaceId]/home/home.tsx | 3 + .../workflow-list/workflow-list.tsx | 34 +- .../w/components/sidebar/hooks/index.ts | 1 + .../sidebar/hooks/use-hover-menu.ts | 62 +++ .../w/components/sidebar/sidebar.tsx | 374 +++++++++++++----- .../w/components/sidebar/utils.ts | 30 ++ 8 files changed, 402 insertions(+), 121 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index 605cda4df8..fef5d012f9 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -33,11 +33,27 @@ opacity: 0; } +html[data-sidebar-collapsed] .sidebar-container span, +html[data-sidebar-collapsed] .sidebar-container .text-small { + opacity: 0; +} + .sidebar-container .sidebar-collapse-hide { transition: opacity 60ms ease; } -.sidebar-container[data-collapsed] .sidebar-collapse-hide { +.sidebar-container[data-collapsed] .sidebar-collapse-hide, +html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-hide { + opacity: 0; +} + +.sidebar-container[data-collapsed] .sidebar-collapse-remove, +html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-remove { + display: none; +} + +html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-btn { + width: 0; opacity: 0; } diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index dc3fb76201..c4d5db0301 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -114,6 +114,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) if (isCollapsed) { document.documentElement.style.setProperty('--sidebar-width', '51px'); + document.documentElement.setAttribute('data-sidebar-collapsed', ''); } else { var width = state && state.sidebarWidth; var maxSidebarWidth = window.innerWidth * 0.3; diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index c91d9465e2..522b9ee783 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -16,6 +16,7 @@ import { import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export' import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks' import type { ChatContext } from '@/stores/panel' +import { useSidebarStore } from '@/stores/sidebar/store' import { MessageContent, MothershipView, @@ -166,6 +167,8 @@ export function Home({ chatId }: HomeProps = {}) { const handleResourceEvent = useCallback(() => { if (isResourceCollapsedRef.current) { + const { isCollapsed, toggleCollapsed } = useSidebarStore.getState() + if (!isCollapsed) toggleCollapsed() setIsResourceCollapsed(false) setIsResourceAnimatingIn(true) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx index 86faf7e204..cddb472a6d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx @@ -13,6 +13,10 @@ import { useSidebarDragContextValue, useWorkflowSelection, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { + compareByOrder, + groupWorkflowsByFolder, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' import { useFolders } from '@/hooks/queries/folders' import { useFolderStore } from '@/stores/folders/store' import type { FolderTreeNode } from '@/stores/folders/types' @@ -22,17 +26,6 @@ const TREE_SPACING = { INDENT_PER_LEVEL: 20, } as const -function compareByOrder( - a: T, - b: T -): number { - if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder - const timeA = a.createdAt?.getTime() ?? 0 - const timeB = b.createdAt?.getTime() ?? 0 - if (timeA !== timeB) return timeA - timeB - return a.id.localeCompare(b.id) -} - interface WorkflowListProps { workspaceId: string workflowId: string | undefined @@ -129,21 +122,10 @@ export const WorkflowList = memo(function WorkflowList({ return activeWorkflow?.folderId || null }, [workflowId, regularWorkflows, isLoading, foldersLoading]) - const workflowsByFolder = useMemo(() => { - const grouped = regularWorkflows.reduce( - (acc, workflow) => { - const folderId = workflow.folderId || 'root' - if (!acc[folderId]) acc[folderId] = [] - acc[folderId].push(workflow) - return acc - }, - {} as Record - ) - for (const folderId of Object.keys(grouped)) { - grouped[folderId].sort(compareByOrder) - } - return grouped - }, [regularWorkflows]) + const workflowsByFolder = useMemo( + () => groupWorkflowsByFolder(regularWorkflows), + [regularWorkflows] + ) const orderedWorkflowIds = useMemo(() => { const ids: string[] = [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts index 5c23e7afe0..bdffa08fdf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/index.ts @@ -4,6 +4,7 @@ export { type DropIndicator, useDragDrop } from './use-drag-drop' export { useFolderExpand } from './use-folder-expand' export { useFolderOperations } from './use-folder-operations' export { useFolderSelection } from './use-folder-selection' +export { useHoverMenu } from './use-hover-menu' export { useItemDrag } from './use-item-drag' export { useItemRename } from './use-item-rename' export { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts new file mode 100644 index 0000000000..cb7cb60461 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +const CLOSE_DELAY_MS = 150 + +const preventAutoFocus = (e: Event) => e.preventDefault() + +/** + * Manages hover-triggered dropdown menu state. + * Provides handlers for trigger and content mouse events with a delay + * to prevent flickering when moving between trigger and content. + */ +export function useHoverMenu() { + const [isOpen, setIsOpen] = useState(false) + const closeTimerRef = useRef | null>(null) + + const cancelClose = useCallback(() => { + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current) + closeTimerRef.current = null + } + }, []) + + useEffect(() => { + return () => { + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current) + } + } + }, []) + + const scheduleClose = useCallback(() => { + cancelClose() + closeTimerRef.current = setTimeout(() => setIsOpen(false), CLOSE_DELAY_MS) + }, [cancelClose]) + + const open = useCallback(() => { + cancelClose() + setIsOpen(true) + }, [cancelClose]) + + const close = useCallback(() => { + cancelClose() + setIsOpen(false) + }, [cancelClose]) + + const triggerProps = useMemo( + () => ({ onMouseEnter: open, onMouseLeave: scheduleClose }) as const, + [open, scheduleClose] + ) + + const contentProps = useMemo( + () => + ({ + onMouseEnter: cancelClose, + onMouseLeave: scheduleClose, + onCloseAutoFocus: preventAutoFocus, + }) as const, + [cancelClose, scheduleClose] + ) + + return { isOpen, close, triggerProps, contentProps } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 9469033299..e9ce5d9c9a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { MoreHorizontal } from 'lucide-react' +import { Folder, MoreHorizontal } from 'lucide-react' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' import { @@ -12,6 +12,9 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, DropdownMenuTrigger, FolderPlus, Home, @@ -50,31 +53,132 @@ import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/ import { useContextMenu, useFolderOperations, + useHoverMenu, useSidebarResize, useTaskSelection, useWorkflowOperations, useWorkspaceManagement, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { groupWorkflowsByFolder } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' import { useDuplicateWorkspace, useExportWorkspace, useImportWorkflow, useImportWorkspace, } from '@/app/workspace/[workspaceId]/w/hooks' +import { useFolders } from '@/hooks/queries/folders' import { useDeleteTask, useDeleteTasks, useRenameTask, useTasks } from '@/hooks/queries/tasks' import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useTaskEvents } from '@/hooks/use-task-events' import { SIDEBAR_WIDTH } from '@/stores/constants' import { useFolderStore } from '@/stores/folders/store' +import type { FolderTreeNode } from '@/stores/folders/types' import { useSearchModalStore } from '@/stores/modals/search/store' import { useSidebarStore } from '@/stores/sidebar/store' +import type { WorkflowMetadata } from '@/stores/workflows/registry/types' const logger = createLogger('Sidebar') +interface CollapsedSidebarMenuProps { + icon: React.ReactNode + hover: ReturnType + onClick?: () => void + children: React.ReactNode + className?: string +} + +function CollapsedSidebarMenu({ + icon, + hover, + onClick, + children, + className, +}: CollapsedSidebarMenuProps) { + return ( +
+ { + if (!open) hover.close() + }} + modal={false} + > +
+ + + +
+ + {children} + +
+
+ ) +} + +function renderCollapsedFolderItems( + nodes: FolderTreeNode[], + grouped: Record, + wsId: string +): React.ReactNode[] { + return nodes.map((folder) => { + const folderWorkflows = grouped[folder.id] || [] + const hasChildren = folder.children.length > 0 || folderWorkflows.length > 0 + + if (!hasChildren) { + return ( + + + {folder.name} + + ) + } + + return ( + + + + {folder.name} + + + {renderCollapsedFolderItems(folder.children, grouped, wsId)} + {folderWorkflows.map((workflow) => ( + + +
+ {workflow.name} + + + ))} + + + ) + }) +} + function SidebarItemSkeleton() { return ( -
+
) @@ -270,6 +374,7 @@ export const Sidebar = memo(function Sidebar() { const timer = setTimeout(() => setShowCollapsedContent(true), 200) return () => clearTimeout(timer) } + document.documentElement.removeAttribute('data-sidebar-collapsed') setShowCollapsedContent(false) }, [isCollapsed]) @@ -356,6 +461,20 @@ export const Sidebar = memo(function Sidebar() { workspaceId, }) + useFolders(workspaceId) + const folders = useFolderStore((s) => s.folders) + + const folderTree = useMemo( + () => (isCollapsed && workspaceId ? useFolderStore.getState().getFolderTree(workspaceId) : []), + // eslint-disable-next-line react-hooks/exhaustive-deps + [isCollapsed, workspaceId, folders] + ) + + const workflowsByFolder = useMemo( + () => (isCollapsed ? groupWorkflowsByFolder(regularWorkflows) : {}), + [isCollapsed, regularWorkflows] + ) + const [activeNavItemHref, setActiveNavItemHref] = useState(null) const { isOpen: isNavContextMenuOpen, @@ -632,6 +751,8 @@ export const Sidebar = memo(function Sidebar() { const [visibleTaskCount, setVisibleTaskCount] = useState(5) const [renamingTaskId, setRenamingTaskId] = useState(null) const [renameValue, setRenameValue] = useState('') + const tasksHover = useHoverMenu() + const workflowsHover = useHoverMenu() const renameInputRef = useRef(null) const renameCanceledRef = useRef(false) @@ -960,7 +1081,7 @@ export const Sidebar = memo(function Sidebar() { type='button' onClick={toggleCollapsed} className={cn( - 'ml-auto flex h-[30px] items-center justify-center overflow-hidden rounded-[8px] transition-all duration-200 hover:bg-[var(--surface-active)]', + 'sidebar-collapse-btn ml-auto flex h-[30px] items-center justify-center overflow-hidden rounded-[8px] transition-all duration-200 hover:bg-[var(--surface-active)]', isCollapsed ? 'w-0 opacity-0' : 'w-[30px] opacity-100' )} aria-label='Collapse sidebar' @@ -1023,13 +1144,11 @@ export const Sidebar = memo(function Sidebar() { {/* Workspace */}
-
-
- Workspace + {!isCollapsed && ( +
+
Workspace
-
+ )}
{workspaceNavItems.map((item) => ( {/* Tasks */}
-
-
-
- All tasks + {isCollapsed ? ( + + } + hover={tasksHover} + onClick={() => router.push(`/workspace/${workspaceId}/home`)} + > + {tasksLoading ? ( + + + Loading... + + ) : tasks.length === 0 ? ( + No tasks yet + ) : ( + tasks.map((task) => ( + + + + {task.name} + + + )) + )} + + ) : ( +
+
+
+
+ All tasks +
+
+ + + + + +

New task

+
+
+
+
- {!isCollapsed && ( -
- - - - - -

New task

-
-
-
- )} + + See more + + )} + + )} +
-
-
- {tasksLoading ? ( - + )} +
+ + {/* Workflows */} + {isCollapsed ? ( + + } + hover={workflowsHover} + onClick={handleCreateWorkflow} + className='mt-[14px]' + > + {workflowsLoading && regularWorkflows.length === 0 ? ( + + + Loading... + + ) : regularWorkflows.length === 0 ? ( + No workflows yet ) : ( <> - {tasks.slice(0, visibleTaskCount).map((task) => { - const isCurrentRoute = task.id !== 'new' && pathname === task.href - const isRenaming = renamingTaskId === task.id - const isSelected = task.id !== 'new' && selectedTasks.has(task.id) - - if (!isCollapsed && isRenaming) { - return ( + {renderCollapsedFolderItems(folderTree, workflowsByFolder, workspaceId)} + {(workflowsByFolder.root || []).map((workflow) => ( + +
- - setRenameValue(e.target.value)} - onKeyDown={handleRenameKeyDown} - onBlur={handleSaveTaskRename} - className='min-w-0 flex-1 border-none bg-transparent font-base text-[14px] text-[var(--text-body)] outline-none' - /> -
- ) - } - - return ( - - ) - })} - {tasks.length > visibleTaskCount && ( - - )} + className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]' + style={{ + backgroundColor: workflow.color, + borderColor: `${workflow.color}60`, + backgroundClip: 'padding-box', + }} + /> + {workflow.name} + +
+ ))} )} -
-
- - {/* Workflows */} - {!isCollapsed && ( -
+ + ) : ( +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts new file mode 100644 index 0000000000..ecabf89b45 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts @@ -0,0 +1,30 @@ +import type { WorkflowMetadata } from '@/stores/workflows/registry/types' + +export function compareByOrder( + a: T, + b: T +): number { + if (a.sortOrder !== b.sortOrder) return a.sortOrder - b.sortOrder + const timeA = a.createdAt?.getTime() ?? 0 + const timeB = b.createdAt?.getTime() ?? 0 + if (timeA !== timeB) return timeA - timeB + return a.id.localeCompare(b.id) +} + +export function groupWorkflowsByFolder( + workflows: WorkflowMetadata[] +): Record { + const grouped = workflows.reduce( + (acc, workflow) => { + const folderId = workflow.folderId || 'root' + if (!acc[folderId]) acc[folderId] = [] + acc[folderId].push(workflow) + return acc + }, + {} as Record + ) + for (const key of Object.keys(grouped)) { + grouped[key].sort(compareByOrder) + } + return grouped +} From 1a42d956f4b3650fafbc36295f67d06423225335 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Mar 2026 14:51:13 -0700 Subject: [PATCH 2/7] fix(sidebar): truncate long names in collapsed dropdown menus --- .../[workspaceId]/w/components/sidebar/sidebar.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index e9ce5d9c9a..a076089fb7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -115,13 +115,7 @@ function CollapsedSidebarMenu({
- + {children} @@ -153,7 +147,7 @@ function renderCollapsedFolderItems( {folder.name} - + {renderCollapsedFolderItems(folder.children, grouped, wsId)} {folderWorkflows.map((workflow) => ( @@ -1192,7 +1186,7 @@ export const Sidebar = memo(function Sidebar() { - {task.name} + {task.name} )) From 5fee2304d2eeb1009bbf8cf8f3c088316978af6a Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Mar 2026 14:55:57 -0700 Subject: [PATCH 3/7] =?UTF-8?q?fix(sidebar):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20extract=20components,=20fix=20reactive=20subscripti?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../collapsed-sidebar-menu.tsx | 117 ++++++++++++++++++ .../w/components/sidebar/components/index.ts | 4 + .../w/components/sidebar/sidebar.tsx | 111 ++--------------- 3 files changed, 132 insertions(+), 100 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx new file mode 100644 index 0000000000..41ef56e1b5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx @@ -0,0 +1,117 @@ +import { Folder } from 'lucide-react' +import Link from 'next/link' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/emcn' +import { cn } from '@/lib/core/utils/cn' +import type { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import type { FolderTreeNode } from '@/stores/folders/types' +import type { WorkflowMetadata } from '@/stores/workflows/registry/types' + +interface CollapsedSidebarMenuProps { + icon: React.ReactNode + hover: ReturnType + onClick?: () => void + children: React.ReactNode + className?: string +} + +export function CollapsedSidebarMenu({ + icon, + hover, + onClick, + children, + className, +}: CollapsedSidebarMenuProps) { + return ( +
+ { + if (!open) hover.close() + }} + modal={false} + > +
+ + + +
+ + {children} + +
+
+ ) +} + +export function CollapsedFolderItems({ + nodes, + workflowsByFolder, + workspaceId, +}: { + nodes: FolderTreeNode[] + workflowsByFolder: Record + workspaceId: string +}) { + return ( + <> + {nodes.map((folder) => { + const folderWorkflows = workflowsByFolder[folder.id] || [] + const hasChildren = folder.children.length > 0 || folderWorkflows.length > 0 + + if (!hasChildren) { + return ( + + + {folder.name} + + ) + } + + return ( + + + + {folder.name} + + + + {folderWorkflows.map((workflow) => ( + + +
+ {workflow.name} + + + ))} + + + ) + })} + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts index a5a1fbb087..f122ee5de6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/index.ts @@ -1,3 +1,7 @@ +export { + CollapsedFolderItems, + CollapsedSidebarMenu, +} from './collapsed-sidebar-menu/collapsed-sidebar-menu' export { HelpModal } from './help-modal/help-modal' export { NavItemContextMenu } from './nav-item-context-menu' export { SearchModal } from './search-modal/search-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index a076089fb7..3a5deb6beb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { Folder, MoreHorizontal } from 'lucide-react' +import { MoreHorizontal } from 'lucide-react' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' import { @@ -12,9 +12,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, DropdownMenuTrigger, FolderPlus, Home, @@ -41,6 +38,8 @@ import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/provide import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils' import { + CollapsedFolderItems, + CollapsedSidebarMenu, HelpModal, NavItemContextMenu, SearchModal, @@ -73,103 +72,11 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useTaskEvents } from '@/hooks/use-task-events' import { SIDEBAR_WIDTH } from '@/stores/constants' import { useFolderStore } from '@/stores/folders/store' -import type { FolderTreeNode } from '@/stores/folders/types' import { useSearchModalStore } from '@/stores/modals/search/store' import { useSidebarStore } from '@/stores/sidebar/store' -import type { WorkflowMetadata } from '@/stores/workflows/registry/types' const logger = createLogger('Sidebar') -interface CollapsedSidebarMenuProps { - icon: React.ReactNode - hover: ReturnType - onClick?: () => void - children: React.ReactNode - className?: string -} - -function CollapsedSidebarMenu({ - icon, - hover, - onClick, - children, - className, -}: CollapsedSidebarMenuProps) { - return ( -
- { - if (!open) hover.close() - }} - modal={false} - > -
- - - -
- - {children} - -
-
- ) -} - -function renderCollapsedFolderItems( - nodes: FolderTreeNode[], - grouped: Record, - wsId: string -): React.ReactNode[] { - return nodes.map((folder) => { - const folderWorkflows = grouped[folder.id] || [] - const hasChildren = folder.children.length > 0 || folderWorkflows.length > 0 - - if (!hasChildren) { - return ( - - - {folder.name} - - ) - } - - return ( - - - - {folder.name} - - - {renderCollapsedFolderItems(folder.children, grouped, wsId)} - {folderWorkflows.map((workflow) => ( - - -
- {workflow.name} - - - ))} - - - ) - }) -} - function SidebarItemSkeleton() { return (
@@ -457,11 +364,11 @@ export const Sidebar = memo(function Sidebar() { useFolders(workspaceId) const folders = useFolderStore((s) => s.folders) + const getFolderTree = useFolderStore((s) => s.getFolderTree) const folderTree = useMemo( - () => (isCollapsed && workspaceId ? useFolderStore.getState().getFolderTree(workspaceId) : []), - // eslint-disable-next-line react-hooks/exhaustive-deps - [isCollapsed, workspaceId, folders] + () => (isCollapsed && workspaceId ? getFolderTree(workspaceId) : []), + [isCollapsed, workspaceId, folders, getFolderTree] ) const workflowsByFolder = useMemo( @@ -1305,7 +1212,11 @@ export const Sidebar = memo(function Sidebar() { No workflows yet ) : ( <> - {renderCollapsedFolderItems(folderTree, workflowsByFolder, workspaceId)} + {(workflowsByFolder.root || []).map((workflow) => ( From cbd33eb0aac86745743359f5c5dd1cf83628033f Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Mar 2026 15:09:15 -0700 Subject: [PATCH 4/7] fix(sidebar): support touch/keyboard for collapsed menus, document auto-collapse --- apps/sim/app/workspace/[workspaceId]/home/home.tsx | 1 + .../collapsed-sidebar-menu/collapsed-sidebar-menu.tsx | 3 ++- .../[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 522b9ee783..64ef65819f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -167,6 +167,7 @@ export function Home({ chatId }: HomeProps = {}) { const handleResourceEvent = useCallback(() => { if (isResourceCollapsedRef.current) { + /** Auto-collapse sidebar to give resource panel maximum width for immersive experience */ const { isCollapsed, toggleCollapsed } = useSidebarStore.getState() if (!isCollapsed) toggleCollapsed() setIsResourceCollapsed(false) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx index 41ef56e1b5..22736c5754 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx @@ -34,7 +34,8 @@ export function CollapsedSidebarMenu({ { - if (!open) hover.close() + if (open) hover.open() + else hover.close() }} modal={false} > diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts index cb7cb60461..e48fc4c25d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-hover-menu.ts @@ -58,5 +58,5 @@ export function useHoverMenu() { [cancelClose, scheduleClose] ) - return { isOpen, close, triggerProps, contentProps } + return { isOpen, open, close, triggerProps, contentProps } } From c9f838aa06027ea1afb17b86b323b06f77fe1b98 Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Mar 2026 15:15:50 -0700 Subject: [PATCH 5/7] fix(sidebar): remove dead CSS selector for sidebar-collapse-remove --- apps/sim/app/_styles/globals.css | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sim/app/_styles/globals.css b/apps/sim/app/_styles/globals.css index fef5d012f9..fccd7600ee 100644 --- a/apps/sim/app/_styles/globals.css +++ b/apps/sim/app/_styles/globals.css @@ -47,7 +47,6 @@ html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-hide { opacity: 0; } -.sidebar-container[data-collapsed] .sidebar-collapse-remove, html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-remove { display: none; } From e6fde83d23f08309554b1053af456139675073fb Mon Sep 17 00:00:00 2001 From: waleed Date: Sat, 14 Mar 2026 15:18:23 -0700 Subject: [PATCH 6/7] fix(sidebar): add aria-label to collapsed menu trigger buttons --- .../collapsed-sidebar-menu/collapsed-sidebar-menu.tsx | 3 +++ .../workspace/[workspaceId]/w/components/sidebar/sidebar.tsx | 2 ++ 2 files changed, 5 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx index 22736c5754..ff61e2ed52 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx @@ -18,6 +18,7 @@ interface CollapsedSidebarMenuProps { icon: React.ReactNode hover: ReturnType onClick?: () => void + ariaLabel?: string children: React.ReactNode className?: string } @@ -26,6 +27,7 @@ export function CollapsedSidebarMenu({ icon, hover, onClick, + ariaLabel, children, className, }: CollapsedSidebarMenuProps) { @@ -43,6 +45,7 @@ export function CollapsedSidebarMenu({