Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
fix(sidebar): collapsed sidebar shows single icons with hover dropdown menus#3588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
db207f91a42d955fee230cbd33ebc9f838ae6fde83e8f731eFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,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() | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| setIsResourceCollapsed(false) | ||
| setIsResourceAnimatingIn(true) | ||
| } | ||
| 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() | ||
| }} | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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> | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| </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 |
|---|---|---|
| @@ -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 } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.