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:
orkhanart
2025-11-28 18:23:26 -08:00
parent 136c9edfbe
commit 67af617868
44 changed files with 147 additions and 83 deletions

View File

@@ -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

View File

@@ -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']
}
}

View 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;

View File

View File

View 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

View File

@@ -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,
}

View File

View 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 -->

View File

@@ -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)