feat(workspace): Add Recents/Trash pages and move Linear/Node buttons to header

- Add RecentsView with recently accessed items list
- Add TrashView with multi-select, restore, and delete actions
- Add Recents and Trash menu items to workspace sidebar
- Move Linear/Node buttons from sidebar to page headers (right side)
- Add Linear/Node buttons to all workspace views (Dashboard, Workflows, Assets, Models)
- Remove Create section from sidebar navigation
- Add routes for recents and trash pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
orkhanart
2025-11-29 06:48:33 -08:00
parent 5e178bb2ce
commit a3a12999a9
15 changed files with 759 additions and 250 deletions

View File

@@ -30,6 +30,8 @@ declare module 'vue' {
LinearStepCard: typeof import('./components/linear/LinearStepCard.vue')['default']
LinearTemplateCard: typeof import('./components/linear/LinearTemplateCard.vue')['default']
LinearTemplateSelector: typeof import('./components/linear/LinearTemplateSelector.vue')['default']
LinearTopBar: typeof import('./components/linear/LinearTopBar.vue')['default']
LinearTopNavbar: typeof import('./components/linear/LinearTopNavbar.vue')['default']
LinearWorkflowSidebar: typeof import('./components/linear/LinearWorkflowSidebar.vue')['default']
LinearWorkspace: typeof import('./components/linear/LinearWorkspace.vue')['default']
ModelsTab: typeof import('./components/v2/workspace/ModelsTab.vue')['default']

View File

@@ -1,215 +1,234 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useLinearModeStore } from '@/stores/linearModeStore'
import type { LinearOutput } from '@/types/linear'
const store = useLinearModeStore()
const activeTab = ref<'queue' | 'history'>('queue')
const outputs = computed(() => store.outputs)
const isGenerating = computed(() => store.isGenerating)
const currentWorkflow = computed(() => store.currentWorkflow)
// Mock queue items
const queueItems = computed(() => {
if (!isGenerating.value || !currentWorkflow.value) return []
// Mock batches for demo - each batch is a generation session with multiple outputs
const batches = ref([
{
id: 'batch-1',
prompt: 'A mystical forest with glowing mushrooms and fairy lights, cinematic lighting, 8k',
model: 'Gen-4 Turbo',
duration: '5s',
createdAt: '2 min ago',
settings: { seed: 123456, steps: 30, cfg: 7.5 },
outputs: [
{ id: '1a', url: 'https://picsum.photos/seed/forest1/400/400', type: 'image' },
{ id: '1b', url: 'https://picsum.photos/seed/forest2/400/400', type: 'image' },
{ id: '1c', url: 'https://picsum.photos/seed/forest3/400/400', type: 'video' },
{ id: '1d', url: 'https://picsum.photos/seed/forest4/400/400', type: 'image' },
],
},
{
id: 'batch-2',
prompt: 'Cyberpunk city at night with neon lights and rain reflections',
model: 'Gen-4',
duration: '10s',
createdAt: '15 min ago',
settings: { seed: 789012, steps: 25, cfg: 8 },
outputs: [
{ id: '2a', url: 'https://picsum.photos/seed/cyber1/400/400', type: 'video' },
{ id: '2b', url: 'https://picsum.photos/seed/cyber2/400/400', type: 'image' },
],
},
{
id: 'batch-3',
prompt: 'Portrait of a woman with dramatic lighting, studio photography',
model: 'Gen-4 Turbo',
duration: '5s',
createdAt: '1 hour ago',
settings: { seed: 345678, steps: 30, cfg: 7 },
outputs: [
{ id: '3a', url: 'https://picsum.photos/seed/portrait1/400/400', type: 'image' },
{ id: '3b', url: 'https://picsum.photos/seed/portrait2/400/400', type: 'image' },
{ id: '3c', url: 'https://picsum.photos/seed/portrait3/400/400', type: 'image' },
],
},
{
id: 'batch-4',
prompt: 'Abstract fluid art in blue and gold, macro photography',
model: 'Flash 2.5',
duration: '5s',
createdAt: '3 hours ago',
settings: { seed: 901234, steps: 20, cfg: 6.5 },
outputs: [
{ id: '4a', url: 'https://picsum.photos/seed/abstract1/400/400', type: 'image' },
],
},
])
return [
{
id: currentWorkflow.value.id,
name: currentWorkflow.value.templateName,
status: 'running' as const,
progress: store.executionProgress,
currentStep: currentWorkflow.value.currentStepIndex + 1,
totalSteps: currentWorkflow.value.steps.length,
},
]
// Current generation progress
const queueItem = computed(() => {
if (!isGenerating.value || !currentWorkflow.value) return null
return {
id: currentWorkflow.value.id,
name: currentWorkflow.value.templateName,
progress: store.executionProgress,
}
})
function formatTime(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
hour: '2-digit',
minute: '2-digit',
}).format(date)
function copyPrompt(prompt: string): void {
navigator.clipboard.writeText(prompt)
}
function handleDownload(output: LinearOutput): void {
const link = document.createElement('a')
link.href = output.url
link.download = output.filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
function deleteBatch(batchId: string): void {
const index = batches.value.findIndex(b => b.id === batchId)
if (index > -1) {
batches.value.splice(index, 1)
}
}
function handleDelete(outputId: string): void {
store.deleteOutput(outputId)
function downloadAll(batchId: string): void {
console.log('Download all from batch:', batchId)
}
function handleClearHistory(): void {
store.clearOutputs()
function reuseSettings(batchId: string): void {
console.log('Reuse settings from batch:', batchId)
}
</script>
<template>
<!-- Main content area - takes remaining space -->
<!-- Main content area - Batch gallery of creations -->
<main class="flex h-full flex-1 flex-col bg-zinc-950">
<!-- Tabs -->
<div class="flex border-b border-zinc-800">
<button
:class="[
'flex-1 px-4 py-2.5 text-xs font-medium transition-colors',
activeTab === 'queue'
? 'border-b-2 border-blue-600 text-zinc-100'
: 'text-zinc-500 hover:text-zinc-300'
]"
@click="activeTab = 'queue'"
>
Queue
<span
v-if="queueItems.length"
class="ml-1.5 rounded-full bg-blue-600 px-1.5 py-0.5 text-[10px] text-white"
>
{{ queueItems.length }}
</span>
</button>
<button
:class="[
'flex-1 px-4 py-2.5 text-xs font-medium transition-colors',
activeTab === 'history'
? 'border-b-2 border-blue-600 text-zinc-100'
: 'text-zinc-500 hover:text-zinc-300'
]"
@click="activeTab = 'history'"
>
History
<span
v-if="outputs.length"
class="ml-1.5 rounded bg-zinc-700 px-1.5 py-0.5 text-[10px] text-zinc-400"
>
{{ outputs.length }}
</span>
</button>
</div>
<!-- Queue View -->
<div v-if="activeTab === 'queue'" class="flex-1 overflow-y-auto">
<!-- Active Queue Items -->
<div v-if="queueItems.length" class="p-3">
<div
v-for="item in queueItems"
:key="item.id"
class="rounded-lg border border-zinc-800 bg-zinc-800/50 p-3"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="h-2 w-2 animate-pulse rounded-full bg-blue-500" />
<span class="text-xs font-medium text-zinc-200">{{ item.name }}</span>
</div>
<span class="text-[10px] text-zinc-500">
Step {{ item.currentStep }}/{{ item.totalSteps }}
</span>
<div class="flex-1 overflow-y-auto">
<!-- Currently Generating Batch -->
<div v-if="queueItem" class="border-b border-zinc-800 p-6">
<div class="mb-4 flex items-center gap-3">
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-blue-600">
<i class="pi pi-spin pi-spinner text-sm text-white" />
</div>
<!-- Progress -->
<div class="mt-2">
<div class="h-1 overflow-hidden rounded-full bg-zinc-700">
<div
class="h-full rounded-full bg-blue-600 transition-all duration-300"
:style="{ width: `${item.progress}%` }"
/>
</div>
<div class="mt-1 text-right text-[10px] text-zinc-500">
{{ Math.round(item.progress) }}%
</div>
<div class="flex-1">
<div class="text-sm font-medium text-zinc-200">Generating...</div>
<div class="text-xs text-zinc-500">{{ Math.round(queueItem.progress) }}% complete</div>
</div>
</div>
<div class="h-1.5 overflow-hidden rounded-full bg-zinc-800">
<div
class="h-full rounded-full bg-blue-500 transition-all duration-300"
:style="{ width: `${queueItem.progress}%` }"
/>
</div>
</div>
<!-- Empty Queue -->
<!-- Batch Sections -->
<div
v-else
class="flex flex-col items-center justify-center py-12 text-zinc-500"
v-for="batch in batches"
:key="batch.id"
class="border-b border-zinc-800 p-6"
>
<i class="pi pi-clock mb-2 text-2xl" />
<span class="text-xs">Queue is empty</span>
<p class="mt-1 text-center text-[10px] text-zinc-600">
Generated images will appear here
</p>
</div>
</div>
<!-- Batch Header -->
<div class="mb-4 flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<!-- Prompt -->
<p class="text-sm leading-relaxed text-zinc-200">
{{ batch.prompt }}
</p>
<!-- Meta Info -->
<div class="mt-2 flex flex-wrap items-center gap-3 text-[11px] text-zinc-500">
<span class="flex items-center gap-1">
<i class="pi pi-box text-[10px]" />
{{ batch.model }}
</span>
<span class="flex items-center gap-1">
<i class="pi pi-clock text-[10px]" />
{{ batch.duration }}
</span>
<span class="flex items-center gap-1">
<i class="pi pi-history text-[10px]" />
{{ batch.createdAt }}
</span>
<span class="text-zinc-600"></span>
<span class="text-zinc-600">
Seed: {{ batch.settings.seed }} · Steps: {{ batch.settings.steps }} · CFG: {{ batch.settings.cfg }}
</span>
</div>
</div>
<!-- History View -->
<div v-else class="flex flex-1 flex-col overflow-hidden">
<!-- History Header -->
<div
v-if="outputs.length"
class="flex items-center justify-between border-b border-zinc-800 px-3 py-2"
>
<span class="text-[10px] text-zinc-500">{{ outputs.length }} generations</span>
<button
class="text-[10px] text-zinc-500 transition-colors hover:text-red-400"
@click="handleClearHistory"
>
Clear all
</button>
</div>
<!-- Action Buttons -->
<div class="flex shrink-0 items-center gap-1">
<button
v-tooltip.bottom="'Copy prompt'"
class="flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="copyPrompt(batch.prompt)"
>
<i class="pi pi-copy text-sm" />
</button>
<button
v-tooltip.bottom="'Reuse settings'"
class="flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="reuseSettings(batch.id)"
>
<i class="pi pi-replay text-sm" />
</button>
<button
v-tooltip.bottom="'Download all'"
class="flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="downloadAll(batch.id)"
>
<i class="pi pi-download text-sm" />
</button>
<button
v-tooltip.bottom="'Delete batch'"
class="flex h-8 w-8 items-center justify-center rounded-lg text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-red-400"
@click="deleteBatch(batch.id)"
>
<i class="pi pi-trash text-sm" />
</button>
</div>
</div>
<!-- History Grid -->
<div v-if="outputs.length" class="flex-1 overflow-y-auto p-4">
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
<!-- Outputs Grid -->
<div class="flex flex-wrap gap-4">
<div
v-for="output in outputs"
v-for="output in batch.outputs"
:key="output.id"
class="group relative aspect-square overflow-hidden rounded-lg bg-zinc-800"
class="group relative h-48 w-48 cursor-pointer overflow-hidden rounded-xl bg-zinc-900 transition-all hover:ring-2 hover:ring-blue-500/50"
>
<img
:src="output.thumbnailUrl ?? output.url"
:alt="output.filename"
class="h-full w-full object-cover transition-transform group-hover:scale-105"
:src="output.url"
:alt="batch.prompt"
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<!-- Hover Overlay -->
<!-- Video indicator -->
<div
class="absolute inset-0 flex flex-col justify-between bg-gradient-to-t from-black/80 via-transparent to-black/40 p-2 opacity-0 transition-opacity group-hover:opacity-100"
v-if="output.type === 'video'"
class="absolute left-2 top-2 flex h-5 w-5 items-center justify-center rounded-full bg-black/70"
>
<div class="flex justify-end">
<button
class="flex h-6 w-6 items-center justify-center rounded bg-black/50 text-zinc-300 transition-colors hover:bg-red-600 hover:text-white"
@click="handleDelete(output.id)"
>
<i class="pi pi-trash text-[10px]" />
</button>
</div>
<i class="pi pi-play text-[8px] text-white" />
</div>
<div>
<div class="flex items-center justify-between">
<span class="text-[10px] text-zinc-300">
{{ formatTime(output.createdAt) }}
</span>
<button
class="flex h-6 w-6 items-center justify-center rounded bg-black/50 text-zinc-300 transition-colors hover:bg-blue-600 hover:text-white"
@click="handleDownload(output)"
>
<i class="pi pi-download text-[10px]" />
</button>
</div>
<!-- Hover Overlay -->
<div class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity group-hover:opacity-100">
<div class="flex gap-1">
<button class="flex h-7 w-7 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-sm transition-colors hover:bg-white/30">
<i class="pi pi-eye text-xs" />
</button>
<button class="flex h-7 w-7 items-center justify-center rounded-full bg-white/20 text-white backdrop-blur-sm transition-colors hover:bg-white/30">
<i class="pi pi-download text-xs" />
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Empty History -->
<!-- Empty State -->
<div
v-else
class="flex flex-1 flex-col items-center justify-center text-zinc-500"
v-if="batches.length === 0 && !queueItem"
class="flex h-full flex-col items-center justify-center p-12 text-zinc-500"
>
<i class="pi pi-images mb-2 text-2xl" />
<span class="text-xs">No history yet</span>
<p class="mt-1 text-center text-[10px] text-zinc-600">
Your creations will appear here
<div class="flex h-20 w-20 items-center justify-center rounded-2xl bg-zinc-900">
<i class="pi pi-images text-3xl" />
</div>
<h3 class="mt-4 text-sm font-medium text-zinc-300">No creations yet</h3>
<p class="mt-1 text-center text-xs text-zinc-600">
Your generated images and videos will appear here
</p>
</div>
</div>

View File

@@ -39,7 +39,6 @@ function selectTab(tab: LinearTab): void {
@click="selectTab(tab.id)"
>
<i :class="['pi', tab.icon, 'text-base']" />
<span class="mt-0.5 text-[8px] font-medium uppercase tracking-wide">{{ tab.label }}</span>
</button>
</div>

View File

@@ -0,0 +1,170 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUiStore } from '@/stores/uiStore'
const router = useRouter()
const uiStore = useUiStore()
const sessionName = ref('Untitled session')
const credits = ref(4625)
const showMenu = ref(false)
function handleLogoClick(): void {
showMenu.value = !showMenu.value
}
function goToWorkspace(): void {
showMenu.value = false
router.push({ name: 'workspace-dashboard', params: { workspaceId: 'default' } })
}
function goToProjects(): void {
showMenu.value = false
router.push({ name: 'workspace-projects', params: { workspaceId: 'default' } })
}
function goToSettings(): void {
showMenu.value = false
router.push({ name: 'workspace-settings', params: { workspaceId: 'default' } })
}
function signOut(): void {
showMenu.value = false
router.push('/')
}
function toggleExperimentalUI(): void {
uiStore.toggleInterfaceVersion()
}
</script>
<template>
<header class="flex h-10 shrink-0 items-center gap-1 border-b border-zinc-800 bg-zinc-950 px-2 select-none">
<!-- Logo Section with Dropdown -->
<div class="relative">
<button
class="flex items-center gap-1 rounded-md px-2 py-1.5 text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-100"
@click="handleLogoClick"
>
<img src="/assets/images/comfy-logo-mono.svg" alt="Comfy" class="h-5 w-5" />
<i class="pi pi-chevron-down text-[10px] opacity-70" />
</button>
<!-- Dropdown Menu -->
<div v-if="showMenu" class="absolute left-0 top-full z-[100] mt-1 min-w-[240px] rounded-lg border border-zinc-800 bg-zinc-900 p-1 shadow-xl">
<!-- File Section -->
<div class="px-3 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500">File</div>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800">
<i class="pi pi-file w-4 text-sm text-zinc-500" />
<span class="flex-1">New Session</span>
<span class="text-[11px] text-zinc-600">Ctrl+N</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800">
<i class="pi pi-folder-open w-4 text-sm text-zinc-500" />
<span class="flex-1">Open...</span>
<span class="text-[11px] text-zinc-600">Ctrl+O</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800">
<i class="pi pi-save w-4 text-sm text-zinc-500" />
<span class="flex-1">Save</span>
<span class="text-[11px] text-zinc-600">Ctrl+S</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800">
<i class="pi pi-download w-4 text-sm text-zinc-500" />
<span>Export...</span>
</button>
<div class="mx-2 my-1 h-px bg-zinc-800" />
<!-- Workspace Section -->
<div class="px-3 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500">Workspace</div>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800" @click="goToWorkspace">
<i class="pi pi-home w-4 text-sm text-zinc-500" />
<span>Dashboard</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800" @click="goToProjects">
<i class="pi pi-folder w-4 text-sm text-zinc-500" />
<span>Projects</span>
</button>
<div class="mx-2 my-1 h-px bg-zinc-800" />
<!-- Account Section -->
<div class="px-3 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500">Account</div>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800" @click="goToSettings">
<i class="pi pi-cog w-4 text-sm text-zinc-500" />
<span>Settings</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-200 transition-colors hover:bg-zinc-800" @click="toggleExperimentalUI">
<i class="pi pi-sparkles w-4 text-sm text-zinc-500" />
<span class="flex-1">Experimental UI</span>
<div
class="h-5 w-9 rounded-full p-0.5 transition-colors"
:class="uiStore.interfaceVersion === 'v2' ? 'bg-blue-500' : 'bg-zinc-700'"
>
<div
class="h-4 w-4 rounded-full bg-white transition-transform"
:class="uiStore.interfaceVersion === 'v2' ? 'translate-x-4' : 'translate-x-0'"
/>
</div>
</button>
<div class="mx-2 my-1 h-px bg-zinc-800" />
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-red-400 transition-colors hover:bg-red-500/10" @click="signOut">
<i class="pi pi-sign-out w-4 text-sm text-red-400" />
<span>Sign out</span>
</button>
</div>
<!-- Backdrop -->
<div v-if="showMenu" class="fixed inset-0 z-[99]" @click="showMenu = false" />
</div>
<!-- Divider -->
<div class="mx-1 h-5 w-px bg-zinc-800" />
<!-- Home Button -->
<button
v-tooltip.bottom="{ value: 'Home', showDelay: 50 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-100"
@click="goToWorkspace"
>
<i class="pi pi-home text-base" />
</button>
<!-- Divider -->
<div class="mx-1 h-5 w-px bg-zinc-800" />
<!-- Session Name / Tabs Area -->
<div class="flex flex-1 items-center">
<div class="flex items-center gap-1">
<span class="rounded-md bg-zinc-800 px-3 py-1.5 text-xs text-zinc-100">{{ sessionName }}</span>
<button class="p-1 text-zinc-500 transition-colors hover:text-zinc-300">
<i class="pi pi-ellipsis-h text-xs" />
</button>
</div>
</div>
<!-- Right Section -->
<div class="flex items-center gap-2">
<button
v-tooltip.bottom="{ value: 'Help', showDelay: 50 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-100"
>
<i class="pi pi-question-circle text-sm" />
</button>
<button
v-tooltip.bottom="{ value: 'Open in new window', showDelay: 50 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-100"
>
<i class="pi pi-external-link text-sm" />
</button>
<span class="text-xs text-zinc-400">{{ credits.toLocaleString() }} credits</span>
<button class="rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500">
Upgrade
</button>
</div>
</header>
</template>

View File

@@ -1,4 +1,5 @@
// Linear Mode Components - Runway-style 2-Column Layout
export { default as LinearTopBar } from './LinearTopBar.vue'
export { default as LinearIconSidebar } from './LinearIconSidebar.vue'
export { default as LinearCreationPanel } from './LinearCreationPanel.vue'
export { default as LinearHistoryPanel } from './LinearHistoryPanel.vue'

View File

@@ -27,16 +27,11 @@ const router = useRouter()
const isTeam = computed(() => props.workspaceId === 'team')
const userMenuGroups = computed<MenuGroup[]>(() => [
{
label: 'Create',
items: [
{ label: 'Linear Mode', icon: 'pi pi-bolt', route: `/${props.workspaceId}/create` }
]
},
{
label: 'Overview',
items: [
{ label: 'Dashboard', icon: 'pi pi-home', route: `/${props.workspaceId}` },
{ label: 'Recents', icon: 'pi pi-clock', route: `/${props.workspaceId}/recents` },
{ label: 'Projects', icon: 'pi pi-folder', route: `/${props.workspaceId}/projects` },
{ label: 'Canvases', icon: 'pi pi-objects-column', route: `/${props.workspaceId}/canvases` }
]
@@ -46,22 +41,18 @@ const userMenuGroups = computed<MenuGroup[]>(() => [
items: [
{ label: 'Workflows', icon: 'pi pi-sitemap', route: `/${props.workspaceId}/workflows` },
{ label: 'Assets', icon: 'pi pi-images', route: `/${props.workspaceId}/assets` },
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` }
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` },
{ label: 'Trash', icon: 'pi pi-trash', route: `/${props.workspaceId}/trash` }
]
}
])
const teamMenuGroups = computed<MenuGroup[]>(() => [
{
label: 'Create',
items: [
{ label: 'Linear Mode', icon: 'pi pi-bolt', route: `/${props.workspaceId}/create` }
]
},
{
label: 'Overview',
items: [
{ label: 'Dashboard', icon: 'pi pi-home', route: `/${props.workspaceId}` },
{ label: 'Recents', icon: 'pi pi-clock', route: `/${props.workspaceId}/recents` },
{ label: 'Projects', icon: 'pi pi-folder', route: `/${props.workspaceId}/projects` },
{ label: 'Canvases', icon: 'pi pi-objects-column', route: `/${props.workspaceId}/canvases` }
]
@@ -71,7 +62,8 @@ const teamMenuGroups = computed<MenuGroup[]>(() => [
items: [
{ label: 'Workflows', icon: 'pi pi-sitemap', route: `/${props.workspaceId}/workflows` },
{ label: 'Assets', icon: 'pi pi-images', route: `/${props.workspaceId}/assets` },
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` }
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` },
{ label: 'Trash', icon: 'pi pi-trash', route: `/${props.workspaceId}/trash` }
]
},
{

View File

@@ -1,18 +1,26 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
interface Props {
title: string
subtitle?: string
actionLabel?: string
actionIcon?: string
showCreateButtons?: boolean
}
const props = withDefaults(defineProps<Props>(), {
actionIcon: 'pi pi-plus'
actionIcon: 'pi pi-plus',
showCreateButtons: true
})
const emit = defineEmits<{
action: []
}>()
const route = useRoute()
const workspaceId = computed(() => route.params.workspaceId as string || 'default')
</script>
<template>
@@ -25,13 +33,33 @@ const emit = defineEmits<{
{{ props.subtitle }}
</p>
</div>
<button
v-if="props.actionLabel"
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
@click="emit('action')"
>
<i :class="[props.actionIcon, 'text-xs']" />
{{ props.actionLabel }}
</button>
<div class="flex items-center gap-2">
<!-- Create Buttons -->
<template v-if="props.showCreateButtons">
<RouterLink
:to="`/${workspaceId}/create`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-bolt text-xs" />
Linear
</RouterLink>
<RouterLink
:to="`/${workspaceId}/canvas`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-share-alt text-xs" />
Node
</RouterLink>
</template>
<!-- Action Button -->
<button
v-if="props.actionLabel"
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
@click="emit('action')"
>
<i :class="[props.actionIcon, 'text-xs']" />
{{ props.actionLabel }}
</button>
</div>
</div>
</template>

View File

@@ -49,6 +49,16 @@ const v2Routes: RouteRecordRaw[] = [
name: 'workspace-models',
component: () => import('./views/v2/workspace/ModelsView.vue')
},
{
path: 'recents',
name: 'workspace-recents',
component: () => import('./views/v2/workspace/RecentsView.vue')
},
{
path: 'trash',
name: 'workspace-trash',
component: () => import('./views/v2/workspace/TrashView.vue')
},
{
path: 'settings',
name: 'workspace-settings',

View File

@@ -1,51 +1,24 @@
<script setup lang="ts">
import { ref } from 'vue'
import LinearIconSidebar from '@/components/linear/LinearIconSidebar.vue'
import LinearCreationPanel from '@/components/linear/LinearCreationPanel.vue'
import LinearHistoryPanel from '@/components/linear/LinearHistoryPanel.vue'
const sessionName = ref('Untitled session')
const credits = ref(4625)
import LinearTopBar from '@/components/linear/LinearTopBar.vue'
</script>
<template>
<div class="linear-view flex h-screen bg-zinc-950">
<!-- Left Icon Sidebar (Chat, Tool, Apps, Workflow) -->
<LinearIconSidebar />
<div class="linear-view flex h-screen flex-col bg-zinc-950">
<!-- Top Bar with Logo, Home, Session Name, Credits -->
<LinearTopBar />
<!-- Left Creation Panel (prompt, upload, settings, generate) -->
<LinearCreationPanel />
<!-- Main Content -->
<div class="flex flex-1 overflow-hidden">
<!-- Left Icon Sidebar (Chat, Tool, Apps, Workflow) -->
<LinearIconSidebar />
<!-- Right Main Area (queue/history) -->
<div class="flex flex-1 flex-col">
<!-- Top Bar -->
<header class="flex h-12 shrink-0 items-center justify-between border-b border-zinc-800 bg-zinc-950 px-4">
<div />
<!-- Left Creation Panel (prompt, upload, settings, generate) -->
<LinearCreationPanel />
<!-- Center: Session Name -->
<div class="flex items-center gap-1">
<span class="text-sm text-zinc-300">{{ sessionName }}</span>
<button class="p-1 text-zinc-500 transition-colors hover:text-zinc-300">
<i class="pi pi-ellipsis-h text-xs" />
</button>
</div>
<!-- Right: Credits + Upgrade -->
<div class="flex items-center gap-3">
<button class="text-zinc-500 transition-colors hover:text-zinc-300">
<i class="pi pi-question-circle text-sm" />
</button>
<button class="text-zinc-500 transition-colors hover:text-zinc-300">
<i class="pi pi-external-link text-sm" />
</button>
<span class="text-xs text-zinc-400">{{ credits.toLocaleString() }} credits</span>
<button class="rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-blue-500">
Upgrade
</button>
</div>
</header>
<!-- Main Content Area -->
<!-- Right Main Area (queue/history) -->
<LinearHistoryPanel />
</div>
</div>

View File

@@ -89,12 +89,28 @@ function getAssetIcon(type: string): string {
{{ assets.length }} files
</p>
</div>
<button
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<i class="pi pi-upload text-xs" />
Upload
</button>
<div class="flex items-center gap-2">
<RouterLink
:to="`/${workspaceId}/create`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-bolt text-xs" />
Linear
</RouterLink>
<RouterLink
:to="`/${workspaceId}/canvas`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-share-alt text-xs" />
Node
</RouterLink>
<button
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<i class="pi pi-upload text-xs" />
Upload
</button>
</div>
</div>
<!-- Search, Filter, Sort & View Toggle -->

View File

@@ -34,13 +34,31 @@ const starterTemplates = [
<template>
<div class="p-6">
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
{{ isTeam ? 'Team Dashboard' : 'Dashboard' }}
</h1>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
Welcome back, {{ workspaceId }}
</p>
<div class="mb-6 flex items-start justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
{{ isTeam ? 'Team Dashboard' : 'Dashboard' }}
</h1>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
Welcome back, {{ workspaceId }}
</p>
</div>
<div class="flex items-center gap-2">
<RouterLink
:to="`/${workspaceId}/create`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-bolt text-xs" />
Linear
</RouterLink>
<RouterLink
:to="`/${workspaceId}/canvas`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-share-alt text-xs" />
Node
</RouterLink>
</div>
</div>
<!-- Quick Actions -->

View File

@@ -101,12 +101,28 @@ function getModelColor(type: string): string {
{{ models.length }} models installed
</p>
</div>
<button
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<i class="pi pi-plus text-xs" />
Add Model
</button>
<div class="flex items-center gap-2">
<RouterLink
:to="`/${workspaceId}/create`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-bolt text-xs" />
Linear
</RouterLink>
<RouterLink
:to="`/${workspaceId}/canvas`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-share-alt text-xs" />
Node
</RouterLink>
<button
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<i class="pi pi-plus text-xs" />
Add Model
</button>
</div>
</div>
<!-- Search, Filter, Sort & View Toggle -->

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { ref } from 'vue'
import WorkspaceViewHeader from '@/components/v2/workspace/WorkspaceViewHeader.vue'
interface RecentItem {
id: string
name: string
type: 'canvas' | 'workflow' | 'asset' | 'project'
icon: string
updatedAt: string
thumbnail?: string
}
const recentItems = ref<RecentItem[]>([
{ id: '1', name: 'Portrait Generation', type: 'canvas', icon: 'pi-objects-column', updatedAt: '2 minutes ago' },
{ id: '2', name: 'SDXL Workflow', type: 'workflow', icon: 'pi-sitemap', updatedAt: '15 minutes ago' },
{ id: '3', name: 'Product Shots', type: 'project', icon: 'pi-folder', updatedAt: '1 hour ago' },
{ id: '4', name: 'reference_image.png', type: 'asset', icon: 'pi-image', updatedAt: '2 hours ago' },
{ id: '5', name: 'Inpainting Canvas', type: 'canvas', icon: 'pi-objects-column', updatedAt: '3 hours ago' },
{ id: '6', name: 'ControlNet Pipeline', type: 'workflow', icon: 'pi-sitemap', updatedAt: '5 hours ago' },
{ id: '7', name: 'Marketing Assets', type: 'project', icon: 'pi-folder', updatedAt: 'Yesterday' },
{ id: '8', name: 'logo_v2.png', type: 'asset', icon: 'pi-image', updatedAt: 'Yesterday' },
])
function getTypeLabel(type: string): string {
const labels: Record<string, string> = {
canvas: 'Canvas',
workflow: 'Workflow',
asset: 'Asset',
project: 'Project'
}
return labels[type] || type
}
function getTypeColor(type: string): string {
const colors: Record<string, string> = {
canvas: 'bg-blue-500/20 text-blue-400',
workflow: 'bg-purple-500/20 text-purple-400',
asset: 'bg-green-500/20 text-green-400',
project: 'bg-amber-500/20 text-amber-400'
}
return colors[type] || 'bg-zinc-500/20 text-zinc-400'
}
</script>
<template>
<div class="p-6">
<WorkspaceViewHeader
title="Recents"
subtitle="Recently accessed items"
:show-create-buttons="true"
/>
<div class="space-y-2">
<div
v-for="item in recentItems"
:key="item.id"
class="flex items-center gap-4 rounded-lg border border-zinc-200 bg-white p-4 transition-colors hover:border-zinc-300 hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-800/50"
>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
<i :class="['pi', item.icon, 'text-lg text-zinc-500 dark:text-zinc-400']" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ item.name }}</span>
<span :class="['rounded px-1.5 py-0.5 text-[10px] font-medium', getTypeColor(item.type)]">
{{ getTypeLabel(item.type) }}
</span>
</div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ item.updatedAt }}</p>
</div>
<button class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300">
<i class="pi pi-ellipsis-v text-sm" />
</button>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,171 @@
<script setup lang="ts">
import { ref } from 'vue'
import WorkspaceViewHeader from '@/components/v2/workspace/WorkspaceViewHeader.vue'
interface TrashItem {
id: string
name: string
type: 'canvas' | 'workflow' | 'asset' | 'project'
icon: string
deletedAt: string
expiresIn: string
}
const trashItems = ref<TrashItem[]>([
{ id: '1', name: 'Old Canvas Draft', type: 'canvas', icon: 'pi-objects-column', deletedAt: '2 days ago', expiresIn: '28 days' },
{ id: '2', name: 'Test Workflow', type: 'workflow', icon: 'pi-sitemap', deletedAt: '5 days ago', expiresIn: '25 days' },
{ id: '3', name: 'unused_asset.png', type: 'asset', icon: 'pi-image', deletedAt: '1 week ago', expiresIn: '23 days' },
{ id: '4', name: 'Archived Project', type: 'project', icon: 'pi-folder', deletedAt: '2 weeks ago', expiresIn: '16 days' },
])
const selectedItems = ref<Set<string>>(new Set())
function toggleSelect(id: string): void {
if (selectedItems.value.has(id)) {
selectedItems.value.delete(id)
} else {
selectedItems.value.add(id)
}
}
function selectAll(): void {
if (selectedItems.value.size === trashItems.value.length) {
selectedItems.value.clear()
} else {
trashItems.value.forEach(item => selectedItems.value.add(item.id))
}
}
function restoreSelected(): void {
trashItems.value = trashItems.value.filter(item => !selectedItems.value.has(item.id))
selectedItems.value.clear()
}
function deleteSelected(): void {
trashItems.value = trashItems.value.filter(item => !selectedItems.value.has(item.id))
selectedItems.value.clear()
}
function emptyTrash(): void {
trashItems.value = []
selectedItems.value.clear()
}
function getTypeLabel(type: string): string {
const labels: Record<string, string> = {
canvas: 'Canvas',
workflow: 'Workflow',
asset: 'Asset',
project: 'Project'
}
return labels[type] || type
}
</script>
<template>
<div class="p-6">
<WorkspaceViewHeader
title="Trash"
subtitle="Items are permanently deleted after 30 days"
:show-create-buttons="false"
/>
<!-- Actions Bar -->
<div v-if="trashItems.length > 0" class="mb-4 flex items-center justify-between rounded-lg border border-zinc-200 bg-zinc-50 px-4 py-2 dark:border-zinc-800 dark:bg-zinc-900">
<div class="flex items-center gap-3">
<label class="flex items-center gap-2 text-sm text-zinc-600 dark:text-zinc-400">
<input
type="checkbox"
:checked="selectedItems.size === trashItems.length"
:indeterminate="selectedItems.size > 0 && selectedItems.size < trashItems.length"
class="h-4 w-4 rounded border-zinc-300 dark:border-zinc-600"
@change="selectAll"
/>
Select all
</label>
<template v-if="selectedItems.size > 0">
<span class="text-sm text-zinc-500">{{ selectedItems.size }} selected</span>
<button
class="flex items-center gap-1.5 rounded-md bg-zinc-200 px-2 py-1 text-xs font-medium text-zinc-700 transition-colors hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-600"
@click="restoreSelected"
>
<i class="pi pi-refresh text-[10px]" />
Restore
</button>
<button
class="flex items-center gap-1.5 rounded-md bg-red-500/10 px-2 py-1 text-xs font-medium text-red-500 transition-colors hover:bg-red-500/20"
@click="deleteSelected"
>
<i class="pi pi-trash text-[10px]" />
Delete forever
</button>
</template>
</div>
<button
class="flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-red-500 transition-colors hover:bg-red-500/10"
@click="emptyTrash"
>
<i class="pi pi-trash text-[10px]" />
Empty trash
</button>
</div>
<!-- Trash Items -->
<div v-if="trashItems.length > 0" class="space-y-2">
<div
v-for="item in trashItems"
:key="item.id"
:class="[
'flex items-center gap-4 rounded-lg border p-4 transition-colors',
selectedItems.has(item.id)
? 'border-blue-500 bg-blue-50 dark:border-blue-500 dark:bg-blue-500/10'
: 'border-zinc-200 bg-white hover:border-zinc-300 hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-800/50'
]"
>
<input
type="checkbox"
:checked="selectedItems.has(item.id)"
class="h-4 w-4 rounded border-zinc-300 dark:border-zinc-600"
@change="toggleSelect(item.id)"
/>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
<i :class="['pi', item.icon, 'text-lg text-zinc-400']" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ item.name }}</span>
<span class="rounded bg-zinc-200 px-1.5 py-0.5 text-[10px] font-medium text-zinc-600 dark:bg-zinc-700 dark:text-zinc-400">
{{ getTypeLabel(item.type) }}
</span>
</div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">
Deleted {{ item.deletedAt }} · Expires in {{ item.expiresIn }}
</p>
</div>
<div class="flex items-center gap-1">
<button
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
title="Restore"
>
<i class="pi pi-refresh text-sm" />
</button>
<button
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10"
title="Delete forever"
>
<i class="pi pi-trash text-sm" />
</button>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="flex flex-col items-center justify-center rounded-lg border border-dashed border-zinc-300 py-16 dark:border-zinc-700">
<div class="flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800">
<i class="pi pi-trash text-2xl text-zinc-400" />
</div>
<h3 class="mt-4 text-lg font-medium text-zinc-900 dark:text-zinc-100">Trash is empty</h3>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Deleted items will appear here</p>
</div>
</div>
</template>

View File

@@ -71,12 +71,28 @@ const filteredWorkflows = computed(() => {
{{ workflows.length }} saved workflows
</p>
</div>
<button
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<i class="pi pi-upload text-xs" />
Import Workflow
</button>
<div class="flex items-center gap-2">
<RouterLink
:to="`/${workspaceId}/create`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-bolt text-xs" />
Linear
</RouterLink>
<RouterLink
:to="`/${workspaceId}/canvas`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-share-alt text-xs" />
Node
</RouterLink>
<button
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<i class="pi pi-upload text-xs" />
Import Workflow
</button>
</div>
</div>
<!-- Search, Sort & View Toggle -->