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

View File

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

View File

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

View File

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