feat(canvas): Add run controls, share dialog, and right toolbar

- Add CanvasRunControls with Run dropdown (Run, Run on Change), Queue button
- Add CanvasShareDialog with Figma-like sharing UI (invite, permissions, link)
- Add CanvasRightToolbar with zoom and fit controls
- Move run button from tab bar to canvas area (top-right)
- Integrate share dialog with tab bar share button

🤖 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 11:25:08 -08:00
parent fff3984477
commit bb49cca003
11 changed files with 622 additions and 23 deletions

View File

@@ -11,6 +11,9 @@ declare module 'vue' {
CanvasBottomBar: typeof import('./components/v2/canvas/CanvasBottomBar.vue')['default']
CanvasLeftSidebar: typeof import('./components/v2/canvas/CanvasLeftSidebar.vue')['default']
CanvasLogoMenu: typeof import('./components/v2/canvas/CanvasLogoMenu.vue')['default']
CanvasRightToolbar: typeof import('./components/v2/canvas/CanvasRightToolbar.vue')['default']
CanvasRunControls: typeof import('./components/v2/canvas/CanvasRunControls.vue')['default']
CanvasShareDialog: typeof import('./components/v2/canvas/CanvasShareDialog.vue')['default']
CanvasTabBar: typeof import('./components/v2/canvas/CanvasTabBar.vue')['default']
CanvasTabs: typeof import('./components/v2/canvas/CanvasTabs.vue')['default']
CreateProjectDialog: typeof import('./components/v2/workspace/CreateProjectDialog.vue')['default']

View File

@@ -11,7 +11,7 @@ function handleTabClick(tabId: Exclude<SidebarTabId, null>): void {
</script>
<template>
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-zinc-900 py-2">
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-black py-2">
<!-- Tab buttons -->
<div class="flex flex-col gap-1">
<button

View File

@@ -83,7 +83,7 @@ function setFilter(value: string): void {
<template>
<aside
class="border-r border-zinc-800 bg-zinc-900/95 transition-all duration-200"
class="border-r border-zinc-800 bg-black/95 transition-all duration-200"
:class="sidebarPanelExpanded ? 'w-80' : 'w-0 overflow-hidden'"
>
<!-- Library Tab - Full custom layout -->
@@ -136,7 +136,7 @@ function setFilter(value: string): void {
</button>
<div
v-if="showFilterMenu"
class="absolute left-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute left-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in filterOptions"
@@ -162,7 +162,7 @@ function setFilter(value: string): void {
</button>
<div
v-if="showSortMenu"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in sortOptions"

View File

@@ -146,7 +146,7 @@ const mockRecents = [
<!-- Expandable Panel (above tabs) -->
<div
v-if="bottomPanelExpanded"
class="bottom-panel flex overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900/95 shadow-2xl backdrop-blur transition-all duration-300"
class="bottom-panel flex overflow-hidden rounded-xl border border-zinc-800 bg-black/95 shadow-2xl backdrop-blur transition-all duration-300"
:style="{
width: isExtended ? 'calc(100vw - 100px)' : '720px',
maxWidth: isExtended ? '1400px' : '720px',
@@ -157,7 +157,7 @@ const mockRecents = [
<!-- Left Sidebar -->
<div
v-if="showSidebar"
class="flex w-48 shrink-0 flex-col border-r border-zinc-800 bg-zinc-900/50"
class="flex w-48 shrink-0 flex-col border-r border-zinc-800 bg-black/50"
>
<div class="p-3">
<div class="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">Categories</div>
@@ -431,7 +431,7 @@ const mockRecents = [
</div>
<!-- Bottom Tab Bar -->
<div class="flex items-center gap-1 rounded-lg border border-zinc-800 bg-zinc-900/90 px-2 py-1.5 backdrop-blur">
<div class="flex items-center gap-1 rounded-lg border border-zinc-800 bg-black/90 px-2 py-1.5 backdrop-blur">
<!-- Tab buttons -->
<button
v-for="tab in BOTTOM_BAR_TABS"
@@ -447,6 +447,23 @@ const mockRecents = [
>
<i :class="[tab.icon, 'text-base']" />
</button>
<!-- Divider -->
<div class="mx-1 h-5 w-px bg-zinc-700" />
<!-- Settings & Shortcuts -->
<button
v-tooltip.top="{ value: 'Keyboard Shortcuts', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
>
<i class="pi pi-bolt text-base" />
</button>
<button
v-tooltip.top="{ value: 'Settings', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
>
<i class="pi pi-cog text-base" />
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Props {
orientation?: 'vertical' | 'horizontal'
}
const props = withDefaults(defineProps<Props>(), {
orientation: 'vertical'
})
const emit = defineEmits<{
fitView: []
zoomIn: []
zoomOut: []
}>()
// Tool mode: 'select' or 'pan'
const toolMode = ref<'select' | 'pan'>('select')
// Zoom level (percentage)
const zoomLevel = ref(75)
// Toggle states
const showMinimap = ref(false)
const showLinks = ref(true)
const isVertical = computed(() => props.orientation === 'vertical')
const tooltipPos = computed(() => isVertical.value ? 'left' : 'top')
function setToolMode(mode: 'select' | 'pan'): void {
toolMode.value = mode
}
function handleFitView(): void {
emit('fitView')
}
function handleZoomIn(): void {
zoomLevel.value = Math.min(400, zoomLevel.value + 25)
emit('zoomIn')
}
function handleZoomOut(): void {
zoomLevel.value = Math.max(10, zoomLevel.value - 25)
emit('zoomOut')
}
function toggleMinimap(): void {
showMinimap.value = !showMinimap.value
}
function toggleLinks(): void {
showLinks.value = !showLinks.value
}
</script>
<template>
<div
class="absolute z-10"
:class="isVertical ? 'right-4 top-1/2 -translate-y-1/2' : 'bottom-4 right-4'"
>
<div
class="flex items-center gap-1 rounded-lg border border-zinc-800 bg-black/90 p-1.5 backdrop-blur"
:class="isVertical ? 'flex-col' : 'flex-row'"
>
<!-- Select / Pan Toggle -->
<button
v-tooltip:[tooltipPos]="{ value: 'Select', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="toolMode === 'select' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="setToolMode('select')"
>
<i class="pi pi-arrow-up-left text-base" />
</button>
<button
v-tooltip:[tooltipPos]="{ value: 'Pan', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="toolMode === 'pan' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="setToolMode('pan')"
>
<i class="pi pi-arrows-alt text-base" />
</button>
<!-- Divider -->
<div :class="isVertical ? 'my-1 h-px w-5 bg-zinc-700' : 'mx-1 h-5 w-px bg-zinc-700'" />
<!-- Fit to Screen -->
<button
v-tooltip:[tooltipPos]="{ value: 'Fit to Screen', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="handleFitView"
>
<i class="pi pi-expand text-base" />
</button>
<!-- Divider -->
<div :class="isVertical ? 'my-1 h-px w-5 bg-zinc-700' : 'mx-1 h-5 w-px bg-zinc-700'" />
<!-- Zoom Controls -->
<button
v-tooltip:[tooltipPos]="{ value: 'Zoom In', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="handleZoomIn"
>
<i class="pi pi-plus text-sm" />
</button>
<div
v-tooltip:[tooltipPos]="{ value: 'Zoom Level', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center text-[10px] font-medium text-zinc-400"
>
{{ zoomLevel }}%
</div>
<button
v-tooltip:[tooltipPos]="{ value: 'Zoom Out', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="handleZoomOut"
>
<i class="pi pi-minus text-sm" />
</button>
<!-- Divider -->
<div :class="isVertical ? 'my-1 h-px w-5 bg-zinc-700' : 'mx-1 h-5 w-px bg-zinc-700'" />
<!-- Minimap Toggle -->
<button
v-tooltip:[tooltipPos]="{ value: showMinimap ? 'Hide Minimap' : 'Show Minimap', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="showMinimap ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="toggleMinimap"
>
<i class="pi pi-map text-base" />
</button>
<!-- Links Toggle -->
<button
v-tooltip:[tooltipPos]="{ value: showLinks ? 'Hide Links' : 'Show Links', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="showLinks ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="toggleLinks"
>
<i class="pi pi-link text-base" />
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { ref } from 'vue'
import Popover from 'primevue/popover'
const runMenu = ref<InstanceType<typeof Popover> | null>(null)
const isRunning = ref(false)
const queueCount = ref(0)
function toggleRunMenu(event: Event): void {
runMenu.value?.toggle(event)
}
function runWorkflow(): void {
isRunning.value = true
setTimeout(() => {
isRunning.value = false
}, 2000)
}
function runOnChange(): void {
// Toggle run on change mode
}
function addToQueue(): void {
queueCount.value++
}
function clearQueue(): void {
queueCount.value = 0
}
</script>
<template>
<div class="absolute right-4 top-4 z-10 flex items-center gap-2">
<!-- Queue indicator -->
<div
v-if="queueCount > 0"
class="flex items-center gap-1.5 rounded-md bg-amber-500/20 px-2.5 py-1.5 text-xs font-medium text-amber-400"
>
<i class="pi pi-list text-[10px]" />
<span>{{ queueCount }} in queue</span>
<button
class="ml-1 rounded p-0.5 transition-colors hover:bg-amber-500/20"
@click="clearQueue"
>
<i class="pi pi-times text-[10px]" />
</button>
</div>
<!-- Add to Queue -->
<button
v-tooltip.bottom="{ value: 'Add to Queue', showDelay: 50 }"
class="flex h-8 items-center gap-1.5 rounded-md bg-zinc-800/80 px-3 text-sm text-zinc-300 shadow-sm backdrop-blur transition-colors hover:bg-zinc-700 hover:text-white"
@click="addToQueue"
>
<i class="pi pi-plus text-xs" />
<span>Queue</span>
</button>
<!-- Run Button with Dropdown -->
<div class="relative flex">
<button
class="flex h-8 items-center gap-1.5 rounded-l-md bg-blue-600 px-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-500"
:disabled="isRunning"
@click="runWorkflow"
>
<i :class="['text-xs', isRunning ? 'pi pi-spin pi-spinner' : 'pi pi-play']" />
<span>{{ isRunning ? 'Running...' : 'Run' }}</span>
</button>
<button
class="flex h-8 items-center rounded-r-md border-l border-blue-500 bg-blue-600 px-1.5 text-white shadow-sm transition-colors hover:bg-blue-500"
@click="toggleRunMenu"
>
<i class="pi pi-chevron-down text-[10px]" />
</button>
<!-- Run Menu Popover -->
<Popover ref="runMenu" append-to="self">
<div class="flex w-48 flex-col py-1">
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="runWorkflow(); runMenu?.hide()"
>
<i class="pi pi-play text-xs text-blue-500" />
<span>Run Workflow</span>
<span class="ml-auto text-xs text-zinc-400"></span>
</button>
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="runOnChange(); runMenu?.hide()"
>
<i class="pi pi-sync text-xs text-green-500" />
<span>Run on Change</span>
</button>
<div class="my-1 h-px bg-zinc-200 dark:bg-zinc-700" />
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="addToQueue(); runMenu?.hide()"
>
<i class="pi pi-plus text-xs text-amber-500" />
<span>Add to Queue</span>
<span class="ml-auto text-xs text-zinc-400"></span>
</button>
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="clearQueue(); runMenu?.hide()"
>
<i class="pi pi-trash text-xs text-red-500" />
<span>Clear Queue</span>
</button>
</div>
</Popover>
</div>
</div>
</template>

View File

@@ -0,0 +1,298 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import Select from 'primevue/select'
interface SharedUser {
id: string
name: string
email: string
avatar?: string
initials: string
role: 'owner' | 'editor' | 'viewer'
color: string
}
const props = defineProps<{
visible: boolean
workflowName?: string
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
const inviteEmail = ref('')
const inviteRole = ref<'editor' | 'viewer'>('editor')
const linkCopied = ref(false)
const linkAccess = ref<'restricted' | 'anyone'>('restricted')
const linkPermission = ref<'viewer' | 'editor'>('viewer')
const roleOptions = [
{ label: 'Can edit', value: 'editor' },
{ label: 'Can view', value: 'viewer' }
]
const linkAccessOptions = [
{ label: 'Restricted', value: 'restricted', description: 'Only people with access can open' },
{ label: 'Anyone with the link', value: 'anyone', description: 'Anyone on the internet with the link can view' }
]
const sharedUsers = ref<SharedUser[]>([
{ id: '1', name: 'John Doe', email: 'john@example.com', initials: 'JD', role: 'owner', color: 'bg-blue-600' },
{ id: '2', name: 'Sarah Wilson', email: 'sarah@example.com', initials: 'SW', role: 'editor', color: 'bg-purple-600' },
{ id: '3', name: 'Mike Chen', email: 'mike@example.com', initials: 'MC', role: 'viewer', color: 'bg-green-600' }
])
const shareLink = computed(() => {
return `https://comfy.app/share/${props.workflowName?.toLowerCase().replace(/\s+/g, '-') || 'workflow'}`
})
function copyLink(): void {
navigator.clipboard.writeText(shareLink.value)
linkCopied.value = true
setTimeout(() => {
linkCopied.value = false
}, 2000)
}
function inviteUser(): void {
if (!inviteEmail.value) return
const newUser: SharedUser = {
id: Date.now().toString(),
name: inviteEmail.value.split('@')[0] || 'User',
email: inviteEmail.value,
initials: inviteEmail.value.substring(0, 2).toUpperCase(),
role: inviteRole.value,
color: ['bg-pink-600', 'bg-amber-600', 'bg-cyan-600', 'bg-indigo-600'][Math.floor(Math.random() * 4)] || 'bg-zinc-600'
}
sharedUsers.value.push(newUser)
inviteEmail.value = ''
}
function updateUserRole(userId: string, role: 'editor' | 'viewer'): void {
const user = sharedUsers.value.find(u => u.id === userId)
if (user && user.role !== 'owner') {
user.role = role
}
}
function removeUser(userId: string): void {
const index = sharedUsers.value.findIndex(u => u.id === userId)
if (index > -1 && sharedUsers.value[index]?.role !== 'owner') {
sharedUsers.value.splice(index, 1)
}
}
function getRoleLabel(role: string): string {
switch (role) {
case 'owner': return 'Owner'
case 'editor': return 'Can edit'
case 'viewer': return 'Can view'
default: return role
}
}
</script>
<template>
<Dialog
v-model:visible="dialogVisible"
modal
:draggable="false"
:closable="true"
:pt="{
root: { class: 'bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-zinc-200 dark:border-zinc-800 w-[480px]' },
header: { class: 'p-0' },
content: { class: 'p-0' },
mask: { class: 'backdrop-blur-sm bg-black/50' }
}"
>
<template #header>
<div class="flex w-full items-center justify-between border-b border-zinc-200 px-5 py-4 dark:border-zinc-800">
<div>
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-100">Share "{{ workflowName || 'Workflow' }}"</h2>
<p class="mt-0.5 text-sm text-zinc-500 dark:text-zinc-400">Invite others to collaborate</p>
</div>
</div>
</template>
<div class="p-5">
<!-- Invite Section -->
<div class="mb-5">
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">
Invite people
</label>
<div class="flex gap-2">
<div class="relative flex-1">
<i class="pi pi-envelope absolute left-3 top-1/2 -translate-y-1/2 text-sm text-zinc-400" />
<InputText
v-model="inviteEmail"
placeholder="Enter email address"
class="w-full rounded-lg border border-zinc-300 bg-white py-2.5 pl-9 pr-3 text-sm text-zinc-900 placeholder-zinc-400 outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500"
@keyup.enter="inviteUser"
/>
</div>
<Select
v-model="inviteRole"
:options="roleOptions"
option-label="label"
option-value="value"
class="w-32"
:pt="{
root: { class: 'border border-zinc-300 dark:border-zinc-700 rounded-lg' },
label: { class: 'text-sm py-2.5 px-3' }
}"
/>
<button
class="rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-50"
:disabled="!inviteEmail"
@click="inviteUser"
>
Invite
</button>
</div>
</div>
<!-- People with access -->
<div class="mb-5">
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">
People with access
</label>
<div class="max-h-48 space-y-1 overflow-y-auto rounded-lg border border-zinc-200 dark:border-zinc-800">
<div
v-for="user in sharedUsers"
:key="user.id"
class="group flex items-center gap-3 px-3 py-2.5 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
>
<!-- Avatar -->
<div
:class="[
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white',
user.color
]"
>
{{ user.initials }}
</div>
<!-- User info -->
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ user.name }}
<span v-if="user.role === 'owner'" class="ml-1 text-xs text-zinc-400">(you)</span>
</p>
<p class="truncate text-xs text-zinc-500 dark:text-zinc-400">{{ user.email }}</p>
</div>
<!-- Role selector / Remove -->
<div class="flex items-center gap-1">
<Select
v-if="user.role !== 'owner'"
:model-value="user.role"
:options="roleOptions"
option-label="label"
option-value="value"
class="w-28"
:pt="{
root: { class: 'border-0 bg-transparent' },
label: { class: 'text-xs py-1 px-2 text-zinc-500 dark:text-zinc-400' },
trigger: { class: 'w-4' }
}"
@update:model-value="updateUserRole(user.id, $event)"
/>
<span v-else class="px-2 text-xs text-zinc-400">{{ getRoleLabel(user.role) }}</span>
<button
v-if="user.role !== 'owner'"
class="rounded p-1 text-zinc-400 opacity-0 transition-all hover:bg-zinc-200 hover:text-zinc-600 group-hover:opacity-100 dark:hover:bg-zinc-700 dark:hover:text-zinc-300"
@click="removeUser(user.id)"
>
<i class="pi pi-times text-xs" />
</button>
</div>
</div>
</div>
</div>
<!-- Link sharing section -->
<div class="rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-800/50">
<div class="mb-3 flex items-start gap-3">
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-zinc-200 dark:bg-zinc-700">
<i class="pi pi-link text-zinc-600 dark:text-zinc-400" />
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<Select
v-model="linkAccess"
:options="linkAccessOptions"
option-label="label"
option-value="value"
class="flex-1"
:pt="{
root: { class: 'border-0 bg-transparent' },
label: { class: 'text-sm font-medium py-0 px-0 text-zinc-900 dark:text-zinc-100' },
trigger: { class: 'w-4' }
}"
/>
</div>
<p class="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{{ linkAccess === 'restricted' ? 'Only people with access can open' : 'Anyone on the internet with the link' }}
</p>
</div>
<Select
v-if="linkAccess === 'anyone'"
v-model="linkPermission"
:options="roleOptions"
option-label="label"
option-value="value"
class="w-28"
:pt="{
root: { class: 'border border-zinc-300 dark:border-zinc-700 rounded-lg' },
label: { class: 'text-xs py-1.5 px-2' }
}"
/>
</div>
<!-- Copy link -->
<div class="flex gap-2">
<div class="flex-1 truncate rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400">
{{ shareLink }}
</div>
<button
:class="[
'flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
linkCopied
? 'bg-green-600 text-white'
: 'bg-zinc-900 text-white hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200'
]"
@click="copyLink"
>
<i :class="['text-xs', linkCopied ? 'pi pi-check' : 'pi pi-copy']" />
{{ linkCopied ? 'Copied!' : 'Copy link' }}
</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-between border-t border-zinc-200 px-5 py-4 dark:border-zinc-800">
<button class="flex items-center gap-1.5 text-sm text-zinc-500 transition-colors hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200">
<i class="pi pi-cog text-xs" />
Advanced settings
</button>
<button
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500"
@click="dialogVisible = false"
>
Done
</button>
</div>
</Dialog>
</template>

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import CanvasLogoMenu from './CanvasLogoMenu.vue'
import CanvasTabs, { type CanvasTab } from './CanvasTabs.vue'
import CanvasShareDialog from './CanvasShareDialog.vue'
const router = useRouter()
const showShareDialog = ref(false)
const tabs = ref<CanvasTab[]>([
{ id: 'workflow-1', name: 'Main Workflow', isActive: true },
@@ -51,6 +53,10 @@ function createNewTab(): void {
})
selectTab(newId)
}
const activeWorkflowName = computed(() => {
return tabs.value.find(t => t.id === activeTabId.value)?.name || 'Workflow'
})
</script>
<template>
@@ -97,15 +103,16 @@ function createNewTab(): void {
<button
v-tooltip.bottom="{ value: 'Share', 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="showShareDialog = true"
>
<i class="pi pi-share-alt text-sm" />
</button>
<button
v-tooltip.bottom="{ value: 'Run Workflow', showDelay: 50 }"
class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-600 text-white transition-colors hover:bg-blue-500"
>
<i class="pi pi-play text-sm" />
</button>
</div>
<!-- Share Dialog -->
<CanvasShareDialog
v-model:visible="showShareDialog"
:workflow-name="activeWorkflowName"
/>
</div>
</template>

View File

@@ -131,7 +131,7 @@ const filteredWorkflows = computed(() => {
</button>
<div
v-if="showFilterMenu"
class="absolute left-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute left-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in filterOptions"
@@ -157,7 +157,7 @@ const filteredWorkflows = computed(() => {
</button>
<div
v-if="showSortMenu"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in sortOptions"
@@ -177,7 +177,7 @@ const filteredWorkflows = computed(() => {
<!-- Content -->
<div class="flex-1 overflow-y-auto p-2">
<!-- Team Header Card -->
<div class="mb-3 rounded-lg border border-zinc-800 bg-zinc-900 p-2.5">
<div class="mb-3 rounded-lg border border-zinc-800 bg-black p-2.5">
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg text-lg font-bold"

View File

@@ -34,7 +34,7 @@ function handleNodeLeave(): void {
<template>
<!-- Level 1: Category Icon Bar -->
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-zinc-900 py-2">
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-black py-2">
<div class="flex flex-1 flex-col gap-0.5 overflow-y-auto scrollbar-hide">
<button
v-for="category in NODE_CATEGORIES"
@@ -60,17 +60,17 @@ function handleNodeLeave(): void {
<div class="mt-auto flex flex-col gap-1 pt-2">
<button
v-tooltip.right="{ value: 'Settings', showDelay: 50 }"
v-tooltip.right="{ 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-200"
>
<i class="pi pi-cog text-xs" />
<i class="pi pi-question-circle text-xs" />
</button>
</div>
</nav>
<!-- Level 2: Subcategory Panel -->
<aside
class="border-r border-zinc-800 bg-zinc-900/98 transition-all duration-200 ease-out"
class="border-r border-zinc-800 bg-black/98 transition-all duration-200 ease-out"
:class="nodePanelExpanded ? 'w-72' : 'w-0 overflow-hidden'"
>
<div v-if="nodePanelExpanded && activeNodeCategoryData" class="flex h-full w-72 flex-col">
@@ -156,7 +156,7 @@ function handleNodeLeave(): void {
<Transition name="fade">
<div
v-if="hoveredNode && nodePanelExpanded"
class="pointer-events-none fixed z-50 ml-2 w-64 rounded-lg border border-zinc-700 bg-zinc-900 p-3 shadow-xl"
class="pointer-events-none fixed z-50 ml-2 w-64 rounded-lg border border-zinc-700 bg-black p-3 shadow-xl"
:style="{ top: `${previewPosition.top}px`, left: 'calc(48px + 288px + 8px)' }"
>
<div class="mb-2 flex items-center gap-2">

View File

@@ -8,6 +8,8 @@ import '@vue-flow/core/dist/theme-default.css'
import CanvasTabBar from '@/components/v2/canvas/CanvasTabBar.vue'
import CanvasLeftSidebar from '@/components/v2/canvas/CanvasLeftSidebar.vue'
import CanvasBottomBar from '@/components/v2/canvas/CanvasBottomBar.vue'
import CanvasRightToolbar from '@/components/v2/canvas/CanvasRightToolbar.vue'
import CanvasRunControls from '@/components/v2/canvas/CanvasRunControls.vue'
import NodePropertiesPanel from '@/components/v2/canvas/NodePropertiesPanel.vue'
import { FlowNode } from '@/components/v2/nodes'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -36,7 +38,7 @@ onMounted(() => {
})
// Vue Flow
const { onNodeClick, onPaneClick, fitView } = useVueFlow()
const { onNodeClick, onPaneClick, fitView, zoomIn, zoomOut } = useVueFlow()
// Center the workflow on mount with 50% zoom
onMounted(() => {
@@ -109,6 +111,14 @@ function closePropertiesPanel(): void {
<!-- Interface 2.0: Floating bottom bar -->
<CanvasBottomBar v-if="isInterface2" />
<!-- Right toolbar: vertical for v2, horizontal for v1 -->
<CanvasRightToolbar
:orientation="isInterface2 ? 'vertical' : 'horizontal'"
@fit-view="fitView({ padding: 0.3 })"
@zoom-in="zoomIn()"
@zoom-out="zoomOut()"
/>
<!-- Workflow name -->
<div class="absolute left-4 top-4 z-10">
<span
@@ -117,6 +127,9 @@ function closePropertiesPanel(): void {
{{ props.canvasId }}
</span>
</div>
<!-- Run Controls (top-right) -->
<CanvasRunControls />
</main>
<!-- Right sidebar - Node Properties -->