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:
pythongosssss
2026-02-23 18:05:52 +00:00
committed by GitHub
parent d601aba721
commit 0f4bceafdd
5 changed files with 160 additions and 27 deletions

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

View File

@@ -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.", "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.", "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." "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": { "missingNodes": {

View File

@@ -39,7 +39,6 @@ const appModeStore = useAppModeStore()
const props = defineProps<{ const props = defineProps<{
toastTo?: string | HTMLElement toastTo?: string | HTMLElement
notesTo?: string | HTMLElement
mobile?: boolean mobile?: boolean
}>() }>()
@@ -194,11 +193,10 @@ defineExpose({ runButtonClick })
<div class="flex-1" /> <div class="flex-1" />
<Popover <Popover
v-if="partitionedNodes[0].length" 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" class="overflow-y-auto overflow-x-clip max-h-(--reka-popover-content-available-height) z-100"
:reference="notesTo" side="bottom"
side="left" :side-offset="-8"
:to="notesTo"
> >
<template #button> <template #button>
<Button variant="muted-textonly"> <Button variant="muted-textonly">
@@ -312,9 +310,4 @@ defineExpose({ runButtonClick })
<span v-text="t('queue.jobAddedToQueue')" /> <span v-text="t('queue.jobAddedToQueue')" />
</div> </div>
</Teleport> </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> </template>

View File

@@ -7,6 +7,7 @@ export const useAppModeStore = defineStore('appMode', () => {
const mode = ref<AppMode>('graph') const mode = ref<AppMode>('graph')
const builderSaving = ref(false) const builderSaving = ref(false)
const hasOutputs = ref(true) const hasOutputs = ref(true)
const enableAppBuilder = ref(false)
const isBuilderMode = computed( const isBuilderMode = computed(
() => mode.value === 'builder:select' || mode.value === 'builder:arrange' () => mode.value === 'builder:select' || mode.value === 'builder:arrange'
@@ -23,6 +24,7 @@ export const useAppModeStore = defineStore('appMode', () => {
return { return {
mode: readonly(mode), mode: readonly(mode),
enableAppBuilder: readonly(enableAppBuilder),
isBuilderMode, isBuilderMode,
isAppMode, isAppMode,
isGraphMode, isGraphMode,

View File

@@ -10,10 +10,9 @@ import SplitterPanel from 'primevue/splitterpanel'
import { computed, ref, useTemplateRef } from 'vue' import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import AppModeToolbar from '@/components/appMode/AppModeToolbar.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue' import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
import ModeToggle from '@/components/sidebar/ModeToggle.vue' import ModeToggle from '@/components/sidebar/ModeToggle.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue' import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue' import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.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 { useNodeOutputStore } from '@/stores/imagePreviewStore'
import type { ResultItemImpl } from '@/stores/queueStore' import type { ResultItemImpl } from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore' import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useAppModeStore } from '@/stores/appModeStore'
const { t } = useI18n() const { t } = useI18n()
const nodeOutputStore = useNodeOutputStore() const nodeOutputStore = useNodeOutputStore()
const settingStore = useSettingStore() const settingStore = useSettingStore()
const workspaceStore = useWorkspaceStore() const workspaceStore = useWorkspaceStore()
const appModeStore = useAppModeStore()
const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md') const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')
const activeTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab) 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) const hasPreview = ref(false)
whenever( whenever(
@@ -45,8 +55,6 @@ const selectedItem = ref<AssetItem>()
const selectedOutput = ref<ResultItemImpl>() const selectedOutput = ref<ResultItemImpl>()
const canShowPreview = ref(true) const canShowPreview = ref(true)
const topLeftRef = useTemplateRef('topLeftRef')
const topRightRef = useTemplateRef('topRightRef')
const bottomLeftRef = useTemplateRef('bottomLeftRef') const bottomLeftRef = useTemplateRef('bottomLeftRef')
const bottomRightRef = useTemplateRef('bottomRightRef') const bottomRightRef = useTemplateRef('bottomRightRef')
const linearWorkflowRef = useTemplateRef('linearWorkflowRef') const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
@@ -93,28 +101,27 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
@resizestart="({ originalEvent }) => originalEvent.preventDefault()" @resizestart="({ originalEvent }) => originalEvent.preventDefault()"
> >
<SplitterPanel <SplitterPanel
v-if="hasLeftPanel"
id="linearLeftPanel" id="linearLeftPanel"
:size="1" :size="1"
class="min-w-min outline-none" class="min-w-min outline-none"
> >
<div <div
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'" v-if="sidebarOnLeft && activeTab"
class="flex h-full border-border-subtle border-r" class="flex h-full border-border-subtle border-r"
> >
<SideToolbar /> <ExtensionSlot :extension="activeTab" />
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
</div> </div>
<LinearControls <LinearControls
v-else v-else
ref="linearWorkflowRef" ref="linearWorkflowRef"
:toast-to="unrefElement(bottomLeftRef) ?? undefined" :toast-to="unrefElement(bottomLeftRef) ?? undefined"
:notes-to="unrefElement(topLeftRef) ?? undefined"
/> />
</SplitterPanel> </SplitterPanel>
<SplitterPanel <SplitterPanel
id="linearCenterPanel" id="linearCenterPanel"
:size="98" :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 <LinearPreview
:latent-preview=" :latent-preview="
@@ -126,10 +133,9 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
:selected-item :selected-item
:selected-output :selected-output
/> />
<div ref="topLeftRef" class="absolute z-21 top-4 left-4"> <div class="absolute z-21 top-1 left-1">
<WorkflowActionsDropdown source="app_mode_menu_selected" /> <AppModeToolbar v-if="!appModeStore.isBuilderMode" />
</div> </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="bottomLeftRef" class="absolute z-20 bottom-4 left-4" />
<div ref="bottomRightRef" class="absolute z-20 bottom-24 right-4" /> <div ref="bottomRightRef" class="absolute z-20 bottom-24 right-4" />
<div <div
@@ -146,19 +152,21 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
</div> </div>
</SplitterPanel> </SplitterPanel>
<SplitterPanel <SplitterPanel
v-if="hasRightPanel"
id="linearRightPanel" id="linearRightPanel"
:size="1" :size="1"
class="min-w-min outline-none" class="min-w-min outline-none"
> >
<LinearControls <LinearControls
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'" v-if="sidebarOnLeft"
ref="linearWorkflowRef" ref="linearWorkflowRef"
:toast-to="unrefElement(bottomRightRef) ?? undefined" :toast-to="unrefElement(bottomRightRef) ?? undefined"
:notes-to="unrefElement(topRightRef) ?? undefined"
/> />
<div v-else class="flex h-full border-border-subtle border-l"> <div
<ExtensionSlot v-if="activeTab" :extension="activeTab" /> v-else-if="activeTab"
<SideToolbar class="border-border-subtle border-l" /> class="flex h-full border-border-subtle border-l"
>
<ExtensionSlot :extension="activeTab" />
</div> </div>
</SplitterPanel> </SplitterPanel>
</Splitter> </Splitter>