mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-24 16:54:03 +00:00
App mode - App mode toolbar - 3 (#9025)
## Summary Adds a new toolbar for app mode ## Changes - **What**: Adds new toolbar with builder mode disabled ## Screenshots (if applicable) <img width="172" height="220" alt="image" src="https://github.com/user-attachments/assets/16f3cf61-bcbe-4b4d-a169-01c934140354" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9025-App-mode-App-mode-toolbar-3-30d6d73d365081549af6fdd1937b098f) by [Unito](https://www.unito.io)
This commit is contained in:
126
src/components/appMode/AppModeToolbar.vue
Normal file
126
src/components/appMode/AppModeToolbar.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const appModeStore = useAppModeStore()
|
||||
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
||||
|
||||
const isAssetsActive = computed(
|
||||
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'assets'
|
||||
)
|
||||
const isWorkflowsActive = computed(
|
||||
() => workspaceStore.sidebarTab.activeSidebarTab?.id === 'workflows'
|
||||
)
|
||||
|
||||
function enterBuilderMode() {
|
||||
appModeStore.setMode('builder:select')
|
||||
}
|
||||
|
||||
function openAssets() {
|
||||
void commandStore.execute('Workspace.ToggleSidebarTab.assets')
|
||||
}
|
||||
|
||||
function showApps() {
|
||||
void commandStore.execute('Workspace.ToggleSidebarTab.workflows')
|
||||
}
|
||||
|
||||
function openTemplates() {
|
||||
useWorkflowTemplateSelectorDialog().show('sidebar')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2 pointer-events-auto">
|
||||
<WorkflowActionsDropdown source="app_mode_toolbar">
|
||||
<template #button>
|
||||
<Button
|
||||
v-tooltip.right="{
|
||||
value: t('sideToolbar.labels.menu'),
|
||||
...tooltipOptions
|
||||
}"
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('sideToolbar.labels.menu')"
|
||||
class="h-10 rounded-lg pl-3 pr-2 gap-1 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
|
||||
>
|
||||
<i class="icon-[lucide--panels-top-left] size-4" />
|
||||
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</template>
|
||||
</WorkflowActionsDropdown>
|
||||
|
||||
<Button
|
||||
v-if="appModeStore.enableAppBuilder"
|
||||
v-tooltip.right="{
|
||||
value: t('linearMode.appModeToolbar.appBuilder'),
|
||||
...tooltipOptions
|
||||
}"
|
||||
variant="secondary"
|
||||
size="unset"
|
||||
:aria-label="t('linearMode.appModeToolbar.appBuilder')"
|
||||
class="size-10 rounded-lg"
|
||||
@click="enterBuilderMode"
|
||||
>
|
||||
<i class="icon-[lucide--hammer] size-4" />
|
||||
</Button>
|
||||
|
||||
<div
|
||||
class="flex flex-col w-10 rounded-lg bg-secondary-background overflow-hidden"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.right="{
|
||||
value: t('sideToolbar.mediaAssets.title'),
|
||||
...tooltipOptions
|
||||
}"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="t('sideToolbar.mediaAssets.title')"
|
||||
:class="
|
||||
cn('size-10', isAssetsActive && 'bg-secondary-background-hover')
|
||||
"
|
||||
@click="openAssets"
|
||||
>
|
||||
<i class="icon-[comfy--image-ai-edit] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip.right="{
|
||||
value: t('linearMode.appModeToolbar.apps'),
|
||||
...tooltipOptions
|
||||
}"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="t('linearMode.appModeToolbar.apps')"
|
||||
:class="
|
||||
cn('size-10', isWorkflowsActive && 'bg-secondary-background-hover')
|
||||
"
|
||||
@click="showApps"
|
||||
>
|
||||
<i class="icon-[lucide--panels-top-left] size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-tooltip.right="{
|
||||
value: t('sideToolbar.templates'),
|
||||
...tooltipOptions
|
||||
}"
|
||||
variant="textonly"
|
||||
size="unset"
|
||||
:aria-label="t('sideToolbar.templates')"
|
||||
class="size-10"
|
||||
@click="openTemplates"
|
||||
>
|
||||
<i class="icon-[comfy--template] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2946,6 +2946,10 @@
|
||||
"layout": "On the left, you'll see your generated images, videos, and outputs. On the right, just the controls you need. Everything complex stays out of sight.",
|
||||
"sharing": "Sharing is easy: create your workflow, open App Mode, right-click the tab, and export. When others open your file, it launches straight into this clean view. You can share powerful workflows as simple tools without anyone needing to understand node graphs.",
|
||||
"widget": "If you want to control which settings appear, convert your top-level nodes into a subgraph, then use widget promotion in the toolbox above it to choose what's exposed."
|
||||
},
|
||||
"appModeToolbar": {
|
||||
"appBuilder": "App builder",
|
||||
"apps": "Apps"
|
||||
}
|
||||
},
|
||||
"missingNodes": {
|
||||
|
||||
@@ -39,7 +39,6 @@ const appModeStore = useAppModeStore()
|
||||
|
||||
const props = defineProps<{
|
||||
toastTo?: string | HTMLElement
|
||||
notesTo?: string | HTMLElement
|
||||
mobile?: boolean
|
||||
}>()
|
||||
|
||||
@@ -194,11 +193,10 @@ defineExpose({ runButtonClick })
|
||||
<div class="flex-1" />
|
||||
<Popover
|
||||
v-if="partitionedNodes[0].length"
|
||||
align="start"
|
||||
align="end"
|
||||
class="overflow-y-auto overflow-x-clip max-h-(--reka-popover-content-available-height) z-100"
|
||||
:reference="notesTo"
|
||||
side="left"
|
||||
:to="notesTo"
|
||||
side="bottom"
|
||||
:side-offset="-8"
|
||||
>
|
||||
<template #button>
|
||||
<Button variant="muted-textonly">
|
||||
@@ -312,9 +310,4 @@ defineExpose({ runButtonClick })
|
||||
<span v-text="t('queue.jobAddedToQueue')" />
|
||||
</div>
|
||||
</Teleport>
|
||||
<Teleport v-if="false" defer :to="notesTo">
|
||||
<div
|
||||
class="bg-base-background text-muted-foreground flex flex-col w-90 gap-2 rounded-2xl border-1 border-border-subtle py-3"
|
||||
></div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -7,6 +7,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
const mode = ref<AppMode>('graph')
|
||||
const builderSaving = ref(false)
|
||||
const hasOutputs = ref(true)
|
||||
const enableAppBuilder = ref(false)
|
||||
|
||||
const isBuilderMode = computed(
|
||||
() => mode.value === 'builder:select' || mode.value === 'builder:arrange'
|
||||
@@ -23,6 +24,7 @@ export const useAppModeStore = defineStore('appMode', () => {
|
||||
|
||||
return {
|
||||
mode: readonly(mode),
|
||||
enableAppBuilder: readonly(enableAppBuilder),
|
||||
isBuilderMode,
|
||||
isAppMode,
|
||||
isGraphMode,
|
||||
|
||||
@@ -10,10 +10,9 @@ import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import AppModeToolbar from '@/components/appMode/AppModeToolbar.vue'
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
||||
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
|
||||
@@ -25,15 +24,26 @@ import MobileMenu from '@/renderer/extensions/linearMode/MobileMenu.vue'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const settingStore = useSettingStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const appModeStore = useAppModeStore()
|
||||
|
||||
const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')
|
||||
|
||||
const activeTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
const sidebarOnLeft = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Location') === 'left'
|
||||
)
|
||||
const hasLeftPanel = computed(
|
||||
() => (sidebarOnLeft.value && activeTab.value) || !sidebarOnLeft.value
|
||||
)
|
||||
const hasRightPanel = computed(
|
||||
() => sidebarOnLeft.value || (!sidebarOnLeft.value && activeTab.value)
|
||||
)
|
||||
|
||||
const hasPreview = ref(false)
|
||||
whenever(
|
||||
@@ -45,8 +55,6 @@ const selectedItem = ref<AssetItem>()
|
||||
const selectedOutput = ref<ResultItemImpl>()
|
||||
const canShowPreview = ref(true)
|
||||
|
||||
const topLeftRef = useTemplateRef('topLeftRef')
|
||||
const topRightRef = useTemplateRef('topRightRef')
|
||||
const bottomLeftRef = useTemplateRef('bottomLeftRef')
|
||||
const bottomRightRef = useTemplateRef('bottomRightRef')
|
||||
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
@@ -93,28 +101,27 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
@resizestart="({ originalEvent }) => originalEvent.preventDefault()"
|
||||
>
|
||||
<SplitterPanel
|
||||
v-if="hasLeftPanel"
|
||||
id="linearLeftPanel"
|
||||
:size="1"
|
||||
class="min-w-min outline-none"
|
||||
>
|
||||
<div
|
||||
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'"
|
||||
v-if="sidebarOnLeft && activeTab"
|
||||
class="flex h-full border-border-subtle border-r"
|
||||
>
|
||||
<SideToolbar />
|
||||
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
|
||||
<ExtensionSlot :extension="activeTab" />
|
||||
</div>
|
||||
<LinearControls
|
||||
v-else
|
||||
ref="linearWorkflowRef"
|
||||
:toast-to="unrefElement(bottomLeftRef) ?? undefined"
|
||||
:notes-to="unrefElement(topLeftRef) ?? undefined"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
id="linearCenterPanel"
|
||||
:size="98"
|
||||
class="flex flex-col min-w-min gap-4 mx-2 px-10 pt-8 pb-4 relative text-muted-foreground outline-none"
|
||||
class="flex flex-col min-w-min gap-4 px-10 pt-8 pb-4 relative text-muted-foreground outline-none"
|
||||
>
|
||||
<LinearPreview
|
||||
:latent-preview="
|
||||
@@ -126,10 +133,9 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
:selected-item
|
||||
:selected-output
|
||||
/>
|
||||
<div ref="topLeftRef" class="absolute z-21 top-4 left-4">
|
||||
<WorkflowActionsDropdown source="app_mode_menu_selected" />
|
||||
<div class="absolute z-21 top-1 left-1">
|
||||
<AppModeToolbar v-if="!appModeStore.isBuilderMode" />
|
||||
</div>
|
||||
<div ref="topRightRef" class="absolute z-21 top-4 right-4" />
|
||||
<div ref="bottomLeftRef" class="absolute z-20 bottom-4 left-4" />
|
||||
<div ref="bottomRightRef" class="absolute z-20 bottom-24 right-4" />
|
||||
<div
|
||||
@@ -146,19 +152,21 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
v-if="hasRightPanel"
|
||||
id="linearRightPanel"
|
||||
:size="1"
|
||||
class="min-w-min outline-none"
|
||||
>
|
||||
<LinearControls
|
||||
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'"
|
||||
v-if="sidebarOnLeft"
|
||||
ref="linearWorkflowRef"
|
||||
:toast-to="unrefElement(bottomRightRef) ?? undefined"
|
||||
:notes-to="unrefElement(topRightRef) ?? undefined"
|
||||
/>
|
||||
<div v-else class="flex h-full border-border-subtle border-l">
|
||||
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
|
||||
<SideToolbar class="border-border-subtle border-l" />
|
||||
<div
|
||||
v-else-if="activeTab"
|
||||
class="flex h-full border-border-subtle border-l"
|
||||
>
|
||||
<ExtensionSlot :extension="activeTab" />
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
|
||||
Reference in New Issue
Block a user