mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 14:27:40 +00:00
feat(ui): Implement experimental UI layer with versioned architecture
- Reorganize components into v1/v2 versioned structure - Add common components for shared UI elements - Introduce composables for reusable logic - Restructure views into v1/v2 directories - Remove old component structure in favor of versioned approach - Update router and UI store for new architecture
This commit is contained in:
@@ -165,19 +165,58 @@ const stats = SystemStatsSchema.parse(response.data)
|
||||
|
||||
### 9. File Organization
|
||||
|
||||
This project supports **two parallel interface versions**:
|
||||
- **Interface 1.0 (v1)**: Legacy UI/UX - compatible with current ComfyUI
|
||||
- **Interface 2.0 (v2)**: Experimental UI/UX - new design patterns
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # Reusable Vue components
|
||||
│ ├── common/ # Generic components (buttons, inputs)
|
||||
│ ├── layout/ # Layout components (sidebar, header)
|
||||
│ └── [feature]/ # Feature-specific components
|
||||
├── composables/ # Vue composition functions
|
||||
├── services/ # API clients and external services
|
||||
├── stores/ # Pinia stores
|
||||
├── types/ # TypeScript type definitions
|
||||
├── utils/ # Pure utility functions
|
||||
├── views/ # Page/route components
|
||||
└── assets/ # Static assets and CSS
|
||||
├── components/
|
||||
│ ├── common/ # Shared components across both versions
|
||||
│ ├── v1/ # Interface 1.0 components
|
||||
│ │ ├── canvas/ # Canvas components
|
||||
│ │ ├── layout/ # Layout components
|
||||
│ │ └── [feature]/ # Feature-specific components
|
||||
│ └── v2/ # Interface 2.0 components (experimental)
|
||||
│ ├── canvas/ # Canvas components
|
||||
│ ├── dialogs/ # Dialog components
|
||||
│ ├── layout/ # Layout components
|
||||
│ ├── nodes/ # Node components
|
||||
│ │ └── widgets/ # Widget components
|
||||
│ └── workspace/ # Workspace components
|
||||
├── composables/
|
||||
│ ├── common/ # Shared composables
|
||||
│ ├── v1/ # V1-specific composables
|
||||
│ └── v2/ # V2-specific composables
|
||||
├── services/ # API clients (shared)
|
||||
├── stores/ # Pinia stores (shared)
|
||||
├── types/ # TypeScript types (shared)
|
||||
├── utils/ # Utility functions (shared)
|
||||
├── data/ # Static data files
|
||||
├── views/
|
||||
│ ├── v1/ # Interface 1.0 views
|
||||
│ └── v2/ # Interface 2.0 views
|
||||
│ ├── workspace/ # Workspace sub-views
|
||||
│ └── project/ # Project sub-views
|
||||
└── assets/
|
||||
└── css/ # Stylesheets
|
||||
```
|
||||
|
||||
**Import Patterns:**
|
||||
```typescript
|
||||
// V2 components
|
||||
import CanvasView from '@/components/v2/canvas/CanvasView.vue'
|
||||
|
||||
// V1 components
|
||||
import CanvasView from '@/components/v1/canvas/CanvasView.vue'
|
||||
|
||||
// Common/shared components
|
||||
import Button from '@/components/common/Button.vue'
|
||||
|
||||
// Shared services, stores, types (version-agnostic)
|
||||
import { useComfyStore } from '@/stores/comfyStore'
|
||||
import { comfyApi } from '@/services/comfyApi'
|
||||
import type { NodeDefinition } from '@/types/node'
|
||||
```
|
||||
|
||||
### 10. Naming Conventions
|
||||
|
||||
35
ComfyUI_vibe/src/components.d.ts
vendored
35
ComfyUI_vibe/src/components.d.ts
vendored
@@ -7,26 +7,23 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CanvasBottomBar: typeof import('./components/canvas/CanvasBottomBar.vue')['default']
|
||||
CanvasLeftSidebar: typeof import('./components/canvas/CanvasLeftSidebar.vue')['default']
|
||||
CanvasTabBar: typeof import('./components/canvas/CanvasTabBar.vue')['default']
|
||||
FlowNode: typeof import('./components/nodes/FlowNode.vue')['default']
|
||||
NodeHeader: typeof import('./components/nodes/NodeHeader.vue')['default']
|
||||
NodeSlots: typeof import('./components/nodes/NodeSlots.vue')['default']
|
||||
NodeWidgets: typeof import('./components/nodes/NodeWidgets.vue')['default']
|
||||
CanvasBottomBar: typeof import('./components/v2/canvas/CanvasBottomBar.vue')['default']
|
||||
CanvasLeftSidebar: typeof import('./components/v2/canvas/CanvasLeftSidebar.vue')['default']
|
||||
CanvasTabBar: typeof import('./components/v2/canvas/CanvasTabBar.vue')['default']
|
||||
FlowNode: typeof import('./components/v2/nodes/FlowNode.vue')['default']
|
||||
NodeHeader: typeof import('./components/v2/nodes/NodeHeader.vue')['default']
|
||||
NodeSlots: typeof import('./components/v2/nodes/NodeSlots.vue')['default']
|
||||
NodeWidgets: typeof import('./components/v2/nodes/NodeWidgets.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SlotDot: typeof import('./components/nodes/SlotDot.vue')['default']
|
||||
WidgetColor: typeof import('./components/nodes/widgets/WidgetColor.vue')['default']
|
||||
WidgetNumber: typeof import('./components/nodes/widgets/WidgetNumber.vue')['default']
|
||||
WidgetSelect: typeof import('./components/nodes/widgets/WidgetSelect.vue')['default']
|
||||
WidgetSlider: typeof import('./components/nodes/widgets/WidgetSlider.vue')['default']
|
||||
WidgetText: typeof import('./components/nodes/widgets/WidgetText.vue')['default']
|
||||
WidgetToggle: typeof import('./components/nodes/widgets/WidgetToggle.vue')['default']
|
||||
WorkspaceLayout: typeof import('./components/layout/WorkspaceLayout.vue')['default']
|
||||
WorkspaceSidebar: typeof import('./components/layout/WorkspaceSidebar.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
Tooltip: typeof import('primevue/tooltip')['default']
|
||||
SlotDot: typeof import('./components/v2/nodes/SlotDot.vue')['default']
|
||||
WidgetColor: typeof import('./components/v2/nodes/widgets/WidgetColor.vue')['default']
|
||||
WidgetNumber: typeof import('./components/v2/nodes/widgets/WidgetNumber.vue')['default']
|
||||
WidgetSelect: typeof import('./components/v2/nodes/widgets/WidgetSelect.vue')['default']
|
||||
WidgetSlider: typeof import('./components/v2/nodes/widgets/WidgetSlider.vue')['default']
|
||||
WidgetText: typeof import('./components/v2/nodes/widgets/WidgetText.vue')['default']
|
||||
WidgetToggle: typeof import('./components/v2/nodes/widgets/WidgetToggle.vue')['default']
|
||||
WorkspaceLayout: typeof import('./components/v2/layout/WorkspaceLayout.vue')['default']
|
||||
WorkspaceSidebar: typeof import('./components/v2/layout/WorkspaceSidebar.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
0
ComfyUI_vibe/src/components/common/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/common/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/canvas/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/canvas/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/dialogs/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/dialogs/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/layout/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/layout/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/nodes/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/nodes/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/workspace/.gitkeep
Normal file
0
ComfyUI_vibe/src/components/v1/workspace/.gitkeep
Normal file
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUiStore } from '@/stores/uiStore'
|
||||
|
||||
interface CanvasTab {
|
||||
id: string
|
||||
@@ -10,6 +11,7 @@ interface CanvasTab {
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const uiStore = useUiStore()
|
||||
|
||||
const tabs = ref<CanvasTab[]>([
|
||||
{ id: 'workflow-1', name: 'Main Workflow', isActive: true },
|
||||
@@ -80,6 +82,14 @@ function closeTab(tabId: string, event: MouseEvent): void {
|
||||
<i class="pi pi-cog menu-item-icon" />
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<div class="menu-divider" />
|
||||
<button class="menu-item" @click="uiStore.toggleInterfaceVersion()">
|
||||
<i class="pi pi-sparkles menu-item-icon" />
|
||||
<span>Experimental</span>
|
||||
<div :class="['toggle-switch', { active: uiStore.interfaceVersion === 'v2' }]">
|
||||
<div class="toggle-knob" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -227,6 +237,33 @@ function closeTab(tabId: string, event: MouseEvent): void {
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
.toggle-switch {
|
||||
margin-left: auto;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: #3f3f46;
|
||||
border-radius: 10px;
|
||||
padding: 2px;
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-switch.active {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.toggle-knob {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.toggle-switch.active .toggle-knob {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
0
ComfyUI_vibe/src/composables/common/.gitkeep
Normal file
0
ComfyUI_vibe/src/composables/common/.gitkeep
Normal file
0
ComfyUI_vibe/src/composables/v1/.gitkeep
Normal file
0
ComfyUI_vibe/src/composables/v1/.gitkeep
Normal file
0
ComfyUI_vibe/src/composables/v2/.gitkeep
Normal file
0
ComfyUI_vibe/src/composables/v2/.gitkeep
Normal file
@@ -2,61 +2,62 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
// Interface 2.0 (Experimental) Routes
|
||||
const v2Routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'auth',
|
||||
component: () => import('./views/AuthView.vue')
|
||||
component: () => import('./views/v2/AuthView.vue')
|
||||
},
|
||||
{
|
||||
path: '/home',
|
||||
name: 'home',
|
||||
component: () => import('./views/HomeView.vue')
|
||||
component: () => import('./views/v2/HomeView.vue')
|
||||
},
|
||||
{
|
||||
path: '/:workspaceId',
|
||||
component: () => import('./views/WorkspaceView.vue'),
|
||||
component: () => import('./views/v2/WorkspaceView.vue'),
|
||||
props: true,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'workspace-dashboard',
|
||||
component: () => import('./views/workspace/DashboardView.vue')
|
||||
component: () => import('./views/v2/workspace/DashboardView.vue')
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
name: 'workspace-projects',
|
||||
component: () => import('./views/workspace/ProjectsView.vue')
|
||||
component: () => import('./views/v2/workspace/ProjectsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'canvases',
|
||||
name: 'workspace-canvases',
|
||||
component: () => import('./views/workspace/CanvasesView.vue')
|
||||
component: () => import('./views/v2/workspace/CanvasesView.vue')
|
||||
},
|
||||
{
|
||||
path: 'workflows',
|
||||
name: 'workspace-workflows',
|
||||
component: () => import('./views/workspace/WorkflowsView.vue')
|
||||
component: () => import('./views/v2/workspace/WorkflowsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
name: 'workspace-assets',
|
||||
component: () => import('./views/workspace/AssetsView.vue')
|
||||
component: () => import('./views/v2/workspace/AssetsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'models',
|
||||
name: 'workspace-models',
|
||||
component: () => import('./views/workspace/ModelsView.vue')
|
||||
component: () => import('./views/v2/workspace/ModelsView.vue')
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'workspace-settings',
|
||||
component: () => import('./views/workspace/SettingsView.vue')
|
||||
component: () => import('./views/v2/workspace/SettingsView.vue')
|
||||
},
|
||||
{
|
||||
path: ':projectId',
|
||||
name: 'project-detail',
|
||||
component: () => import('./views/workspace/ProjectDetailView.vue'),
|
||||
component: () => import('./views/v2/workspace/ProjectDetailView.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
@@ -64,11 +65,17 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/:workspaceId/:projectId/:canvasId',
|
||||
name: 'canvas',
|
||||
component: () => import('./views/CanvasView.vue'),
|
||||
component: () => import('./views/v2/CanvasView.vue'),
|
||||
props: true
|
||||
}
|
||||
]
|
||||
|
||||
// Interface 1.0 (Legacy) Routes - TODO: Add when v1 views are created
|
||||
// const v1Routes: RouteRecordRaw[] = []
|
||||
|
||||
// Currently using v2 routes
|
||||
const routes: RouteRecordRaw[] = v2Routes
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export type InterfaceVersion = 'v1' | 'v2'
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const interface2Enabled = ref(false)
|
||||
// Interface version: v1 = legacy, v2 = experimental
|
||||
const interfaceVersion = ref<InterfaceVersion>('v2')
|
||||
const leftSidebarOpen = ref(true)
|
||||
const rightSidebarOpen = ref(false)
|
||||
|
||||
function toggleInterface2(): void {
|
||||
interface2Enabled.value = !interface2Enabled.value
|
||||
// Computed for backwards compatibility
|
||||
const interface2Enabled = computed(() => interfaceVersion.value === 'v2')
|
||||
|
||||
function setInterfaceVersion(version: InterfaceVersion): void {
|
||||
interfaceVersion.value = version
|
||||
}
|
||||
|
||||
function toggleInterfaceVersion(): void {
|
||||
interfaceVersion.value = interfaceVersion.value === 'v1' ? 'v2' : 'v1'
|
||||
}
|
||||
|
||||
function toggleLeftSidebar(): void {
|
||||
@@ -19,10 +29,12 @@ export const useUiStore = defineStore('ui', () => {
|
||||
}
|
||||
|
||||
return {
|
||||
interfaceVersion,
|
||||
interface2Enabled,
|
||||
leftSidebarOpen,
|
||||
rightSidebarOpen,
|
||||
toggleInterface2,
|
||||
setInterfaceVersion,
|
||||
toggleInterfaceVersion,
|
||||
toggleLeftSidebar,
|
||||
toggleRightSidebar,
|
||||
}
|
||||
|
||||
0
ComfyUI_vibe/src/views/v1/.gitkeep
Normal file
0
ComfyUI_vibe/src/views/v1/.gitkeep
Normal file
@@ -5,10 +5,10 @@ import { Background } from '@vue-flow/background'
|
||||
import '@vue-flow/core/dist/style.css'
|
||||
import '@vue-flow/core/dist/theme-default.css'
|
||||
|
||||
import CanvasTabBar from '@/components/canvas/CanvasTabBar.vue'
|
||||
import CanvasLeftSidebar from '@/components/canvas/CanvasLeftSidebar.vue'
|
||||
import CanvasBottomBar from '@/components/canvas/CanvasBottomBar.vue'
|
||||
import { FlowNode } from '@/components/nodes'
|
||||
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 { FlowNode } from '@/components/v2/nodes'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useUiStore } from '@/stores/uiStore'
|
||||
|
||||
@@ -53,7 +53,7 @@ function createNodeData(
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcut: X to toggle interface
|
||||
// Keyboard shortcut: X to toggle interface version
|
||||
function handleKeydown(event: KeyboardEvent): void {
|
||||
if (
|
||||
event.target instanceof HTMLInputElement ||
|
||||
@@ -63,7 +63,7 @@ function handleKeydown(event: KeyboardEvent): void {
|
||||
}
|
||||
|
||||
if (event.key.toLowerCase() === 'x') {
|
||||
uiStore.toggleInterface2()
|
||||
uiStore.toggleInterfaceVersion()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,34 +286,6 @@ function toggleCollapsed(): void {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Legend (bottom left) -->
|
||||
<div class="absolute bottom-20 left-4 z-10 flex flex-col gap-1 rounded-lg bg-zinc-900/90 p-3 text-xs backdrop-blur">
|
||||
<div class="font-medium text-zinc-300 mb-1">Slot Types</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background: #b39ddb" />
|
||||
<span class="text-zinc-400">MODEL</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background: #ffcc80" />
|
||||
<span class="text-zinc-400">CLIP</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background: #ef5350" />
|
||||
<span class="text-zinc-400">VAE</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background: #ff80ab" />
|
||||
<span class="text-zinc-400">LATENT</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background: #ffab40" />
|
||||
<span class="text-zinc-400">CONDITIONING</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-2.5 w-2.5 rounded-full" style="background: #64b5f6" />
|
||||
<span class="text-zinc-400">IMAGE</span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Right sidebar - Node Properties -->
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import WorkspaceLayout from '@/components/layout/WorkspaceLayout.vue'
|
||||
import WorkspaceLayout from '@/components/v2/layout/WorkspaceLayout.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const workspaceId = computed(() => route.params.workspaceId as string)
|
||||
Reference in New Issue
Block a user