Skip to content
17 changes: 16 additions & 1 deletion apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,26 @@
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;
}

html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-remove {
display: none;
}

html[data-sidebar-collapsed] .sidebar-container .sidebar-collapse-btn {
width: 0;
opacity: 0;
}

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -166,6 +167,9 @@ 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)
setIsResourceAnimatingIn(true)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
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<typeof useHoverMenu>
onClick?: () => void
ariaLabel?: string
children: React.ReactNode
className?: string
}

export function CollapsedSidebarMenu({
icon,
hover,
onClick,
ariaLabel,
children,
className,
}: CollapsedSidebarMenuProps) {
return (
<div className={cn('flex flex-col px-[8px]', className)}>
<DropdownMenu
open={hover.isOpen}
onOpenChange={(open) => {
if (open) hover.open()
else hover.close()
}}
modal={false}
>
<div {...hover.triggerProps}>
<DropdownMenuTrigger asChild>
<button
type='button'
aria-label={ariaLabel}
className='mx-[2px] flex h-[30px] items-center rounded-[8px] px-[8px] hover:bg-[var(--surface-active)]'
onClick={onClick}
>
{icon}
</button>
</DropdownMenuTrigger>
</div>
<DropdownMenuContent side='right' align='start' sideOffset={8} {...hover.contentProps}>
{children}
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}

export function CollapsedFolderItems({
nodes,
workflowsByFolder,
workspaceId,
}: {
nodes: FolderTreeNode[]
workflowsByFolder: Record<string, WorkflowMetadata[]>
workspaceId: string
}) {
return (
<>
{nodes.map((folder) => {
const folderWorkflows = workflowsByFolder[folder.id] || []
const hasChildren = folder.children.length > 0 || folderWorkflows.length > 0

if (!hasChildren) {
return (
<DropdownMenuItem key={folder.id} disabled>
<Folder className='h-[14px] w-[14px]' />
<span className='truncate'>{folder.name}</span>
</DropdownMenuItem>
)
}

return (
<DropdownMenuSub key={folder.id}>
<DropdownMenuSubTrigger>
<Folder className='h-[14px] w-[14px]' />
<span className='truncate'>{folder.name}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<CollapsedFolderItems
nodes={folder.children}
workflowsByFolder={workflowsByFolder}
workspaceId={workspaceId}
/>
{folderWorkflows.map((workflow) => (
<DropdownMenuItem key={workflow.id} asChild>
<Link href={`/workspace/${workspaceId}/w/${workflow.id}`}>
<div
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
style={{
backgroundColor: workflow.color,
borderColor: `${workflow.color}60`,
backgroundClip: 'padding-box',
}}
/>
<span className='truncate'>{workflow.name}</span>
</Link>
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
})}
</>
)
}
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -22,17 +26,6 @@ const TREE_SPACING = {
INDENT_PER_LEVEL: 20,
} as const

function compareByOrder<T extends { sortOrder: number; createdAt?: Date; id: string }>(
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
Expand Down Expand Up @@ -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<string, WorkflowMetadata[]>
)
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[] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof setTimeout> | 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, open, close, triggerProps, contentProps }
}
Loading
Loading