mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-24 08:44:06 +00:00
App mode - builder toolbar - 6 (#9029)
## Summary A toolbar for builder mode, hides various UI elements when in builder mode ## Screenshots (if applicable) Toolbar <img width="706" height="166" alt="image" src="https://github.com/user-attachments/assets/1f0b08b5-1661-4ed5-96bb-feecc73ca701" /> With disabled save and output required popover <img width="711" height="299" alt="image" src="https://github.com/user-attachments/assets/4a93aaf8-d850-4afe-ab9f-4abd44a25420" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9029-App-mode-builder-toolbar-6-30d6d73d365081e3aef5c90033ba347d) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -99,6 +99,10 @@
|
||||
--color-magenta-300: #ceaac9;
|
||||
--color-magenta-700: #6a246a;
|
||||
|
||||
--color-ocean-300: #badde8;
|
||||
--color-ocean-600: #2f687a;
|
||||
--color-ocean-900: #253236;
|
||||
|
||||
--color-danger-100: #c02323;
|
||||
--color-danger-200: #d62952;
|
||||
|
||||
@@ -155,6 +159,7 @@
|
||||
--comfy-menu-bg: #353535;
|
||||
--comfy-menu-secondary-bg: #292929;
|
||||
--comfy-topbar-height: 2.5rem;
|
||||
--workflow-tabs-height: 2.375rem;
|
||||
--comfy-input-bg: #222;
|
||||
--input-text: #ddd;
|
||||
--descrip-text: #999;
|
||||
@@ -219,6 +224,10 @@
|
||||
--interface-panel-surface: var(--color-white);
|
||||
--interface-stroke: var(--color-smoke-300);
|
||||
|
||||
--interface-builder-mode-background: var(--color-ocean-300);
|
||||
--interface-builder-mode-button-background: var(--color-ocean-600);
|
||||
--interface-builder-mode-button-foreground: var(--color-white);
|
||||
|
||||
--nav-background: var(--color-white);
|
||||
|
||||
--node-border: var(--color-smoke-300);
|
||||
@@ -378,6 +387,10 @@
|
||||
--interface-panel-surface: var(--color-charcoal-800);
|
||||
--interface-stroke: var(--color-charcoal-400);
|
||||
|
||||
--interface-builder-mode-background: var(--color-ocean-900);
|
||||
--interface-builder-mode-button-background: var(--color-ocean-600);
|
||||
--interface-builder-mode-button-foreground: var(--color-white);
|
||||
|
||||
--nav-background: var(--color-charcoal-800);
|
||||
|
||||
--node-border: var(--color-charcoal-500);
|
||||
@@ -512,6 +525,15 @@
|
||||
--color-comfy-menu-bg: var(--comfy-menu-bg);
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
|
||||
--color-interface-builder-mode-background: var(
|
||||
--interface-builder-mode-background
|
||||
);
|
||||
--color-interface-builder-mode-button-background: var(
|
||||
--interface-builder-mode-button-background
|
||||
);
|
||||
--color-interface-builder-mode-button-foreground: var(
|
||||
--interface-builder-mode-button-foreground
|
||||
);
|
||||
--color-interface-stroke: var(--interface-stroke);
|
||||
--color-nav-background: var(--nav-background);
|
||||
--color-node-border: var(--node-border);
|
||||
|
||||
105
src/components/builder/BuilderToolbar.vue
Normal file
105
src/components/builder/BuilderToolbar.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<nav
|
||||
class="fixed top-[calc(var(--workflow-tabs-height)+var(--spacing)*1.5)] left-1/2 z-[1000] -translate-x-1/2"
|
||||
:aria-label="t('builderToolbar.label')"
|
||||
>
|
||||
<div
|
||||
class="inline-flex items-center gap-1 rounded-2xl border border-border-default bg-base-background p-2 shadow-interface"
|
||||
>
|
||||
<template
|
||||
v-for="(step, index) in [selectStep, arrangeStep]"
|
||||
:key="step.id"
|
||||
>
|
||||
<button
|
||||
:class="
|
||||
cn(
|
||||
stepClasses,
|
||||
activeStep === step.id && 'bg-interface-builder-mode-background',
|
||||
activeStep !== step.id &&
|
||||
'hover:bg-secondary-background bg-transparent'
|
||||
)
|
||||
"
|
||||
:aria-current="activeStep === step.id ? 'step' : undefined"
|
||||
@click="appModeStore.setMode(step.id)"
|
||||
>
|
||||
<StepBadge :step :index :model-value="activeStep" />
|
||||
<StepLabel :step />
|
||||
</button>
|
||||
|
||||
<div class="mx-1 h-px w-4 bg-border-default" role="separator" />
|
||||
</template>
|
||||
|
||||
<!-- Save -->
|
||||
<ConnectOutputPopover
|
||||
v-if="!appModeStore.hasOutputs"
|
||||
:is-select-active="activeStep === 'builder:select'"
|
||||
@switch="appModeStore.setMode('builder:select')"
|
||||
>
|
||||
<button :class="cn(stepClasses, 'opacity-30 bg-transparent')">
|
||||
<StepBadge :step="saveStep" :index="2" :model-value="activeStep" />
|
||||
<StepLabel :step="saveStep" />
|
||||
</button>
|
||||
</ConnectOutputPopover>
|
||||
<button
|
||||
v-else
|
||||
:class="
|
||||
cn(
|
||||
stepClasses,
|
||||
activeStep === 'save'
|
||||
? 'bg-interface-builder-mode-background'
|
||||
: 'hover:bg-secondary-background bg-transparent'
|
||||
)
|
||||
"
|
||||
@click="appModeStore.setBuilderSaving(true)"
|
||||
>
|
||||
<StepBadge :step="saveStep" :index="2" :model-value="activeStep" />
|
||||
<StepLabel :step="saveStep" />
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import type { AppMode } from '@/stores/appModeStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import ConnectOutputPopover from './ConnectOutputPopover.vue'
|
||||
import StepBadge from './StepBadge.vue'
|
||||
import StepLabel from './StepLabel.vue'
|
||||
import type { BuilderToolbarStep } from './types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const appModeStore = useAppModeStore()
|
||||
|
||||
const activeStep = computed(() =>
|
||||
appModeStore.isBuilderSaving ? 'save' : appModeStore.mode
|
||||
)
|
||||
|
||||
const stepClasses =
|
||||
'inline-flex h-14 min-h-8 cursor-pointer items-center gap-3 rounded-lg py-2 pr-4 pl-2 transition-colors border-none'
|
||||
|
||||
const selectStep: BuilderToolbarStep<AppMode> = {
|
||||
id: 'builder:select',
|
||||
title: t('builderToolbar.select'),
|
||||
subtitle: t('builderToolbar.selectDescription'),
|
||||
icon: 'icon-[lucide--mouse-pointer-click]'
|
||||
}
|
||||
|
||||
const arrangeStep: BuilderToolbarStep<AppMode> = {
|
||||
id: 'builder:arrange',
|
||||
title: t('builderToolbar.arrange'),
|
||||
subtitle: t('builderToolbar.arrangeDescription'),
|
||||
icon: 'icon-[lucide--layout-panel-left]'
|
||||
}
|
||||
|
||||
const saveStep: BuilderToolbarStep<'save'> = {
|
||||
id: 'save',
|
||||
title: t('builderToolbar.save'),
|
||||
subtitle: t('builderToolbar.saveDescription'),
|
||||
icon: 'icon-[lucide--cloud-upload]'
|
||||
}
|
||||
</script>
|
||||
78
src/components/builder/ConnectOutputPopover.vue
Normal file
78
src/components/builder/ConnectOutputPopover.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<PopoverRoot>
|
||||
<PopoverTrigger as-child>
|
||||
<slot />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
:side-offset="8"
|
||||
:collision-padding="10"
|
||||
class="z-[1001] w-80 rounded-xl border border-border-default bg-base-background shadow-interface will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade"
|
||||
>
|
||||
<div class="flex h-12 items-center justify-between px-4">
|
||||
<h3 class="text-sm font-medium text-base-foreground">
|
||||
{{ t('builderToolbar.connectOutput') }}
|
||||
</h3>
|
||||
<PopoverClose
|
||||
:aria-label="t('g.close')"
|
||||
class="flex cursor-pointer appearance-none items-center justify-center border-none bg-transparent p-0 text-muted-foreground hover:text-base-foreground"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</PopoverClose>
|
||||
</div>
|
||||
<div class="border-t border-border-default" />
|
||||
<p class="mt-3 px-4 text-xs text-muted-foreground leading-relaxed">
|
||||
{{ t('builderToolbar.connectOutputBody1') }}
|
||||
</p>
|
||||
<p
|
||||
v-if="!isSelectActive"
|
||||
class="mt-2 px-4 text-xs text-muted-foreground leading-relaxed"
|
||||
>
|
||||
{{ t('builderToolbar.connectOutputBody2') }}
|
||||
</p>
|
||||
<div class="mt-4 flex items-center justify-end gap-2 px-4 pb-4">
|
||||
<template v-if="isSelectActive">
|
||||
<PopoverClose as-child>
|
||||
<Button variant="secondary" size="md">
|
||||
{{ t('g.ok') }}
|
||||
</Button>
|
||||
</PopoverClose>
|
||||
</template>
|
||||
<template v-else>
|
||||
<PopoverClose as-child>
|
||||
<Button variant="muted-textonly" size="md">
|
||||
{{ t('g.cancel') }}
|
||||
</Button>
|
||||
</PopoverClose>
|
||||
<PopoverClose as-child>
|
||||
<Button variant="secondary" size="md" @click="emit('switch')">
|
||||
{{ t('builderToolbar.switchToSelect') }}
|
||||
</Button>
|
||||
</PopoverClose>
|
||||
</template>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</PopoverRoot>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
PopoverClose,
|
||||
PopoverContent,
|
||||
PopoverRoot,
|
||||
PopoverTrigger
|
||||
} from 'reka-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
const { isSelectActive = false } = defineProps<{
|
||||
isSelectActive?: boolean
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
switch: []
|
||||
}>()
|
||||
</script>
|
||||
22
src/components/builder/StepBadge.vue
Normal file
22
src/components/builder/StepBadge.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex size-10 shrink-0 items-center justify-center rounded-lg bg-interface-builder-mode-button-background text-interface-builder-mode-button-foreground"
|
||||
>
|
||||
<i v-if="modelValue === step.id" :class="cn(step.icon, 'size-5')" />
|
||||
<span v-else class="text-sm font-bold">
|
||||
{{ index + 1 }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import type { BuilderToolbarStep } from './types'
|
||||
|
||||
defineProps<{
|
||||
step: BuilderToolbarStep
|
||||
index: number
|
||||
modelValue: string
|
||||
}>()
|
||||
</script>
|
||||
20
src/components/builder/StepLabel.vue
Normal file
20
src/components/builder/StepLabel.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-0.5 text-left">
|
||||
<span class="text-sm font-medium text-base-foreground">
|
||||
{{ step.title }}
|
||||
</span>
|
||||
<span
|
||||
class="hidden whitespace-nowrap text-xs text-muted-foreground sm:inline"
|
||||
>
|
||||
{{ step.subtitle }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BuilderToolbarStep } from './types'
|
||||
|
||||
const { step } = defineProps<{
|
||||
step: BuilderToolbarStep
|
||||
}>()
|
||||
</script>
|
||||
6
src/components/builder/types.ts
Normal file
6
src/components/builder/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface BuilderToolbarStep<T extends string = string> {
|
||||
id: T
|
||||
title: string
|
||||
subtitle: string
|
||||
icon: string
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<template v-if="showUI" #workflow-tabs>
|
||||
<div
|
||||
v-if="workflowTabsPosition === 'Topbar'"
|
||||
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
|
||||
class="workflow-tabs-container pointer-events-auto relative w-full h-(--workflow-tabs-height)"
|
||||
>
|
||||
<!-- Native drag area for Electron -->
|
||||
<div
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showUI" #side-toolbar>
|
||||
<template v-if="showUI && !appModeStore.isBuilderMode" #side-toolbar>
|
||||
<SideToolbar />
|
||||
</template>
|
||||
<template v-if="showUI" #side-bar-panel>
|
||||
@@ -31,19 +31,27 @@
|
||||
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showUI" #topmenu>
|
||||
<template v-if="showUI && !appModeStore.isBuilderMode" #topmenu>
|
||||
<TopMenuSection />
|
||||
</template>
|
||||
<template v-if="showUI" #bottom-panel>
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template v-if="showUI" #right-side-panel>
|
||||
<NodePropertiesPanel />
|
||||
<NodePropertiesPanel v-if="!appModeStore.isBuilderMode" />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
||||
<GraphCanvasMenu
|
||||
v-if="canvasMenuEnabled && !appModeStore.isBuilderMode"
|
||||
class="pointer-events-auto"
|
||||
/>
|
||||
<MiniMap
|
||||
v-if="comfyAppReady && minimapEnabled && betaMenuEnabled"
|
||||
v-if="
|
||||
comfyAppReady &&
|
||||
minimapEnabled &&
|
||||
betaMenuEnabled &&
|
||||
!appModeStore.isBuilderMode
|
||||
"
|
||||
class="pointer-events-auto"
|
||||
/>
|
||||
</template>
|
||||
@@ -174,6 +182,7 @@ import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isNativeWindow } from '@/utils/envUtil'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
@@ -194,6 +203,7 @@ const nodeSearchboxPopoverRef = shallowRef<InstanceType<
|
||||
const settingStore = useSettingStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const appModeStore = useAppModeStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const executionStore = useExecutionStore()
|
||||
|
||||
@@ -3218,5 +3218,18 @@
|
||||
"poseToVideo": "Pose to video",
|
||||
"cannyToVideo": "Canny to video",
|
||||
"depthToVideo": "Depth to video"
|
||||
},
|
||||
"builderToolbar": {
|
||||
"label": "App Builder",
|
||||
"select": "Select",
|
||||
"selectDescription": "Choose inputs/outputs",
|
||||
"arrange": "Preview",
|
||||
"arrangeDescription": "Review app layout",
|
||||
"save": "Save",
|
||||
"saveDescription": "Save and finish",
|
||||
"connectOutput": "Connect an output",
|
||||
"connectOutputBody1": "Your app needs at least one output to be connected before it can be saved.",
|
||||
"connectOutputBody2": "Switch to the 'Select' step and click on output nodes to add them here.",
|
||||
"switchToSelect": "Switch to Select"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { readonly, computed, ref } from 'vue'
|
||||
|
||||
type AppMode = 'graph' | 'app' | 'builder:select' | 'builder:arrange'
|
||||
export type AppMode = 'graph' | 'app' | 'builder:select' | 'builder:arrange'
|
||||
|
||||
export const useAppModeStore = defineStore('appMode', () => {
|
||||
const mode = ref<AppMode>('graph')
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<GraphCanvas @ready="onGraphReady" />
|
||||
</div>
|
||||
<LinearView v-if="linearMode" />
|
||||
<BuilderToolbar v-if="appModeStore.isBuilderMode" />
|
||||
</div>
|
||||
|
||||
<GlobalToast />
|
||||
@@ -68,6 +69,7 @@ import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { setupAutoQueueHandler } from '@/services/autoQueueService'
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
@@ -84,6 +86,7 @@ import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
|
||||
import { electronAPI } from '@/utils/envUtil'
|
||||
import BuilderToolbar from '@/components/builder/BuilderToolbar.vue'
|
||||
import LinearView from '@/views/LinearView.vue'
|
||||
import ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue'
|
||||
|
||||
@@ -100,6 +103,7 @@ const queueStore = useQueueStore()
|
||||
const assetsStore = useAssetsStore()
|
||||
const versionCompatibilityStore = useVersionCompatibilityStore()
|
||||
const graphCanvasContainerRef = ref<HTMLDivElement | null>(null)
|
||||
const appModeStore = useAppModeStore()
|
||||
const { linearMode } = storeToRefs(useCanvasStore())
|
||||
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
@@ -43,7 +43,9 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
</script>
|
||||
<template>
|
||||
<div class="absolute w-full h-full">
|
||||
<div class="workflow-tabs-container pointer-events-auto h-9.5 w-full">
|
||||
<div
|
||||
class="workflow-tabs-container pointer-events-auto h-(--workflow-tabs-height) w-full"
|
||||
>
|
||||
<div class="flex h-full items-center">
|
||||
<WorkflowTabs />
|
||||
<TopbarBadges />
|
||||
@@ -51,7 +53,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
</div>
|
||||
<div
|
||||
v-if="mobileDisplay"
|
||||
class="justify-center border-border-subtle border-t overflow-y-scroll h-[calc(100%-38px)] bg-comfy-menu-bg"
|
||||
class="justify-center border-border-subtle border-t overflow-y-scroll h-[calc(100%-var(--workflow-tabs-height))] bg-comfy-menu-bg"
|
||||
>
|
||||
<MobileMenu />
|
||||
<LinearProgressBar class="w-full" />
|
||||
|
||||
Reference in New Issue
Block a user