mirror of
https://github.com/simstudioai/sim.git
synced 2026-10-12 23:33:47 +08:00
improvement(sidebar): ui/ux
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user