improvement(sidebar): ui/ux

This commit is contained in:
Emir Karabeg
2025-04-04 20:13:53 -07:00
parent c50e924772
commit aa97b4bd9f
3 changed files with 150 additions and 142 deletions
@@ -1,126 +0,0 @@
'use client'
import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { Folder, LucideIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { NavItem } from '../nav-item/nav-item'
interface FolderSectionProps {
title: string
workflows: WorkflowMetadata[]
defaultOpen?: boolean
icon?: LucideIcon
}
export function FolderSection({
title,
workflows,
defaultOpen = true,
icon: Icon = Folder,
}: FolderSectionProps) {
const [isOpen, setIsOpen] = useState(defaultOpen)
const pathname = usePathname()
// Auto-expand the folder if the current path is one of the workflows inside it
useEffect(() => {
const isCurrentPathInThisFolder = workflows.some((workflow) => pathname === `/w/${workflow.id}`)
if (isCurrentPathInThisFolder) {
setIsOpen(true)
}
}, [pathname, workflows])
if (workflows.length === 0) {
return null
}
// Calculate height based on number of workflows, with a maximum of 4
const navItemHeight = 32 // Height of each NavItem in pixels
const gapSize = 12 // Gap between items (3 in rem units = ~12px)
const maxItems = 4
// Calculate height for a single item (no gaps)
const singleItemHeight = navItemHeight
// Calculate heights for different number of items
const twoItemsHeight = navItemHeight * 2 + gapSize
const threeItemsHeight = navItemHeight * 3 + gapSize * 2
const fourItemsHeight = navItemHeight * 4 + gapSize * 3
// Max height is always the height of 4 items
const maxHeight = fourItemsHeight
const needsScroll = workflows.length > maxItems
// Get exact height based on number of workflows (up to 4)
const getExactHeight = () => {
switch (Math.min(workflows.length, maxItems)) {
case 1:
return singleItemHeight
case 2:
return twoItemsHeight
case 3:
return threeItemsHeight
case 4:
return fourItemsHeight
default:
return 'auto'
}
}
return (
<div className="flex flex-col items-center w-full">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(!isOpen)}
className={`flex !h-9 !w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8 ${
isOpen ? 'bg-accent' : ''
}`}
>
<Icon className="!h-5 !w-5" />
<span className="sr-only">{title}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right">{title}</TooltipContent>
</Tooltip>
{isOpen && (
<div className="mt-3 flex flex-col items-center w-full">
<div
className={`flex flex-col items-center w-full ${
needsScroll ? 'overflow-y-auto [&::-webkit-scrollbar]:hidden' : ''
}`}
style={{
height: workflows.length <= maxItems ? `${getExactHeight()}px` : `${maxHeight}px`,
minHeight: workflows.length === 1 ? `${singleItemHeight}px` : undefined,
maxHeight: `${maxHeight}px`,
scrollbarWidth: 'none',
msOverflowStyle: 'none',
}}
>
<ul className="flex flex-col items-center gap-3 w-full">
{workflows.map((workflow) => (
<li key={workflow.id} className="flex justify-center w-full h-8 flex-shrink-0">
<NavItem href={`/w/${workflow.id}`} label={workflow.name}>
<div
className="h-4 w-4 rounded-full flex-shrink-0"
style={{
backgroundColor:
workflow.color ||
(workflow.marketplaceData?.status === 'temp' ? '#808080' : '#3972F6'),
}}
/>
</NavItem>
</li>
))}
</ul>
</div>
</div>
)}
</div>
)
}
@@ -0,0 +1,138 @@
'use client'
import { useEffect, useState } from 'react'
import { usePathname } from 'next/navigation'
import { Folder, LucideIcon, Store } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { NavItem } from '../nav-item/nav-item'
interface WorkflowFolderProps {
title: string
workflows: WorkflowMetadata[]
defaultOpen?: boolean
icon?: LucideIcon
maxItems?: number
hasMarketplace?: boolean
}
interface WorkflowFoldersProps {
regularWorkflows: WorkflowMetadata[]
marketplaceWorkflows: WorkflowMetadata[]
}
/**
* WorkflowFolder component for rendering a collapsible group of workflows
*/
function WorkflowFolder({
title,
workflows,
defaultOpen = true,
icon: Icon = Folder,
maxItems,
hasMarketplace = false,
}: WorkflowFolderProps) {
const [isOpen, setIsOpen] = useState(defaultOpen)
const pathname = usePathname()
// Auto-expand the folder if the current path is one of the workflows inside it
useEffect(() => {
const isCurrentPathInThisFolder = workflows.some((workflow) => pathname === `/w/${workflow.id}`)
if (isCurrentPathInThisFolder) {
setIsOpen(true)
}
}, [pathname, workflows])
if (workflows.length === 0) {
return null
}
// Filter workflows if maxItems is specified
const displayedWorkflows = maxItems ? workflows.slice(0, maxItems) : workflows
return (
<div className="flex flex-col items-center w-full">
{/* Folder Button - This stays fixed */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => setIsOpen(!isOpen)}
className={`flex !h-9 !w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8 ${
isOpen ? 'bg-accent' : ''
}`}
>
<Icon className="!h-5 !w-5" />
<span className="sr-only">{title}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="right">{title}</TooltipContent>
</Tooltip>
{/* Workflow items with separate scrollable container */}
{isOpen && (
<div className="mt-3 w-full flex items-center flex-col">
<div
className={`w-full flex flex-col items-center gap-3 ${hasMarketplace ? 'max-h-[calc(240px+0.5rem)] overflow-y-auto pb-2 scrollbar-none' : ''}`}
>
{displayedWorkflows.map((workflow) => (
<li key={workflow.id} className="flex justify-center w-full h-8 flex-shrink-0">
<NavItem key={workflow.id} href={`/w/${workflow.id}`} label={workflow.name}>
<div
className="h-4 w-4 rounded-full flex-shrink-0"
style={{
backgroundColor:
workflow.color ||
(workflow.marketplaceData?.status === 'temp' ? '#808080' : '#3972F6'),
}}
/>
</NavItem>
</li>
))}
</div>
</div>
)}
</div>
)
}
/**
* WorkflowFolders - main component for sidebar workflow organization
* Handles both regular and marketplace workflows in a single unified component
*/
export function WorkflowFolders({ regularWorkflows, marketplaceWorkflows }: WorkflowFoldersProps) {
const hasMarketplace = marketplaceWorkflows.length > 0
// Limit regular workflows to 7 when marketplace workflows exist
const maxRegularWorkflows = hasMarketplace ? 7 : undefined
return (
<div className="flex flex-col w-full space-y-6">
{/* Regular Workflows Section */}
<div>
<WorkflowFolder
title="My Workflows"
workflows={regularWorkflows}
defaultOpen={true}
maxItems={maxRegularWorkflows}
hasMarketplace={hasMarketplace}
/>
</div>
{/* Marketplace Workflows Section - Only when marketplace workflows exist */}
{hasMarketplace && (
<div>
<WorkflowFolder
title="Marketplace"
workflows={marketplaceWorkflows}
defaultOpen={true}
icon={Store}
hasMarketplace={true}
/>
</div>
)}
</div>
)
}
+12 -16
View File
@@ -10,10 +10,10 @@ import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { WorkflowMetadata } from '@/stores/workflows/registry/types'
import { FolderSection } from './components/folder-section/folder-section'
import { HelpModal } from './components/help-modal/help-modal'
import { NavItem } from './components/nav-item/nav-item'
import { SettingsModal } from './components/settings-modal/settings-modal'
import { WorkflowFolders } from './components/workflow-folders/workflow-folders'
export function Sidebar() {
const { workflows, createWorkflow } = useWorkflowRegistry()
@@ -85,7 +85,8 @@ export function Sidebar() {
return (
<aside className="fixed inset-y-0 left-0 z-10 hidden w-14 flex-col border-r bg-background sm:flex">
<nav className="flex flex-col items-center gap-4 px-2 py-5">
{/* Top navigation - Logo and Add button */}
<nav className="flex-shrink-0 flex flex-col items-center gap-4 px-2 py-5">
{/* Sim Studio Logo */}
<Link
href="/w/1"
@@ -112,23 +113,18 @@ export function Sidebar() {
</Tooltip>
</nav>
{/* Scrollable workflows section */}
<nav className="flex-1 overflow-y-auto px-2 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:'none'] [scrollbar-width:'none']">
<div className="flex flex-col items-center gap-4">
{/* My Workflows folder */}
<FolderSection title="My Workflows" workflows={regularWorkflows} defaultOpen={true} />
{/* Marketplace Workflows folder */}
<FolderSection
title="Marketplace"
workflows={tempWorkflows}
defaultOpen={true}
icon={Store}
{/* Workflow sections - This area scrolls */}
<div className="flex-1 overflow-y-auto px-2 scrollbar-none">
<div className="pb-2">
<WorkflowFolders
regularWorkflows={regularWorkflows}
marketplaceWorkflows={tempWorkflows}
/>
</div>
</nav>
</div>
<nav className="flex flex-col items-center gap-4 px-2 py-[18px]">
{/* Bottom navigation - Always visible */}
<nav className="flex-shrink-0 flex flex-col items-center gap-4 px-2 py-[18px]">
{/* Marketplace */}
<Tooltip>
<TooltipTrigger asChild>