Linear mode arrangement tweaks (#8853)

Planning to keep updates smaller and more contained in the interest of
collaboration and velocity
- The breadcrumb hamburger menu that provides workflow options is now
displayed in linear mode
- As part of this change, the reka-ui popover component now accepts
primvevue format MenuItems
- I prefer the format I had, but this makes transitioning stuff easier.
- The simplified linear history is moved to always be horizontal and
shown beneath previews.
- The label has been removed from the "Give Feedback" button on desktop
so it does not overlap
- The full side toolbar is displayed in linear mode
  - This is temporary,  but it gets the dead code pruned out now.
- Lays some groundwork for selecting an asset from the assets panel to
also select the item in the main linear panel
- The api `promptQueued` event can now optionally include a promptIds,
which list the ids for all jobs that were queued together as part of
that batch
- Update the max for the `number of generations` field to respect the
recently updated cloud limits

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/e632679c-d727-4882-841b-09e99a2f81a4"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/a9bcd809-c314-49bd-a479-2448d1a88456"/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8853-Linear-mode-arrangement-tweaks-3066d73d365081589355ef753513900b)
by [Unito](https://www.unito.io)
This commit is contained in:
AustinMroz
2026-02-20 03:17:19 -08:00
committed by GitHub
parent 7bf9d51d1d
commit 306fb94cf5
6 changed files with 206 additions and 304 deletions

View File

@@ -1,6 +1,7 @@
<template>
<SidebarTabTemplate
:title="isInFolderView ? '' : $t('sideToolbar.mediaAssets.title')"
v-bind="$attrs"
>
<template #alt-title>
<div
@@ -272,6 +273,8 @@ const { activeJobsCount } = storeToRefs(queueStore)
const executionStore = useExecutionStore()
const settingStore = useSettingStore()
const emit = defineEmits<{ assetSelected: [asset: AssetItem] }>()
const activeTab = ref<'input' | 'output'>('output')
const folderJobId = ref<string | null>(null)
const folderExecutionTime = ref<number | undefined>(undefined)
@@ -530,6 +533,7 @@ watch(
function handleAssetSelect(asset: AssetItem, assets?: AssetItem[]) {
const assetList = assets ?? visibleAssets.value
const index = assetList.findIndex((a) => a.id === asset.id)
emit('assetSelected', asset)
handleAssetClick(asset, index, assetList)
}

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { MenuItem } from 'primevue/menuitem'
import {
PopoverArrow,
PopoverContent,
@@ -15,7 +16,7 @@ defineOptions({
})
defineProps<{
entries?: { label: string; action?: () => void; icon?: string }[][]
entries?: MenuItem[]
icon?: string
to?: string | HTMLElement
}>()
@@ -40,33 +41,34 @@ defineProps<{
>
<slot>
<div class="flex flex-col p-1">
<section
v-for="(entryGroup, index) in entries ?? []"
:key="index"
class="flex flex-col border-b-2 last:border-none border-border-subtle"
>
<template v-for="item in entries ?? []" :key="item.label">
<div
v-for="{ label, action, icon } in entryGroup"
:key="label"
v-if="item.separator"
class="border-b w-full border-border-subtle"
/>
<div
v-else
:class="
cn(
'flex flex-row gap-4 p-2 rounded-sm my-1',
action &&
'cursor-pointer hover:bg-secondary-background-hover'
item.disabled
? 'opacity-50 pointer-events-none'
: item.command &&
'cursor-pointer hover:bg-secondary-background-hover'
)
"
@click="
() => {
if (!action) return
action()
(e) => {
if (!item.command || item.disabled) return
item.command({ originalEvent: e, item })
close()
}
"
>
<i v-if="icon" :class="icon" />
{{ label }}
<i v-if="item.icon" :class="item.icon" />
{{ item.label }}
</div>
</section>
</template>
</div>
</slot>
<PopoverArrow class="fill-base-background stroke-border-subtle" />

View File

@@ -11,7 +11,7 @@ import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
@@ -30,6 +30,7 @@ const { t } = useI18n()
const commandStore = useCommandStore()
const executionStore = useExecutionStore()
const { batchCount } = storeToRefs(useQueueSettingsStore())
const settingStore = useSettingStore()
const { isActiveSubscription } = useBillingContext()
const workflowStore = useWorkflowStore()
@@ -101,7 +102,11 @@ const partitionedNodes = computed(() => {
})
const batchCountWidget: SimplifiedWidget<number> = {
options: { precision: 0, min: 1, max: isCloud ? 4 : 99 },
options: {
precision: 0,
min: 1,
max: settingStore.get('Comfy.QueueButton.BatchCountLimit')
},
value: 1,
name: t('linearMode.runCount'),
type: 'number'

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue'
import { whenever } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { downloadFile } from '@/base/common/downloadUtil'
@@ -12,6 +13,7 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow
import { extractWorkflowFromAsset } from '@/platform/workflow/utils/workflowExtractionUtil'
import ImagePreview from '@/renderer/extensions/linearMode/ImagePreview.vue'
import LinearWelcome from '@/renderer/extensions/linearMode/LinearWelcome.vue'
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
// Lazy-loaded to avoid pulling THREE.js into the main bundle
const Preview3d = () => import('@/renderer/extensions/linearMode/Preview3d.vue')
import VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'
@@ -21,6 +23,7 @@ import {
} from '@/renderer/extensions/linearMode/mediaTypes'
import type { StatItem } from '@/renderer/extensions/linearMode/mediaTypes'
import { app } from '@/scripts/app'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import type { ResultItemImpl } from '@/stores/queueStore'
import { formatDuration } from '@/utils/dateTimeUtil'
import { collectAllNodes } from '@/utils/graphTraversalUtil'
@@ -28,15 +31,22 @@ import { executeWidgetsCallback } from '@/utils/litegraphUtil'
const { t, d } = useI18n()
const mediaActions = useMediaAssetActions()
const nodeOutputStore = useNodeOutputStore()
const { runButtonClick, selectedItem, selectedOutput } = defineProps<{
latentPreview?: string
const { runButtonClick } = defineProps<{
runButtonClick?: (e: Event) => void
selectedItem?: AssetItem
selectedOutput?: ResultItemImpl
mobile?: boolean
}>()
const selectedItem = ref<AssetItem>()
const selectedOutput = ref<ResultItemImpl>()
const canShowPreview = ref(true)
const latentPreview = ref<string>()
whenever(
() => nodeOutputStore.latestPreview[0],
() => (latentPreview.value = nodeOutputStore.latestPreview[0])
)
const dateOptions = {
month: 'short',
day: 'numeric',
@@ -55,16 +65,17 @@ function formatTime(time?: string) {
}
const itemStats = computed<StatItem[]>(() => {
if (!selectedItem) return []
const user_metadata = getOutputAssetMetadata(selectedItem.user_metadata)
if (!selectedItem.value) return []
const user_metadata = getOutputAssetMetadata(selectedItem.value.user_metadata)
if (!user_metadata) return []
const { allOutputs } = user_metadata
return [
{ content: formatTime(selectedItem.created_at) },
{ content: formatTime(selectedItem.value.created_at) },
{ content: formatDuration(user_metadata.executionTimeInSeconds) },
allOutputs && { content: t('g.asset', allOutputs.length) },
(selectedOutput && mediaTypes[getMediaType(selectedOutput)]) ?? {}
(selectedOutput.value && mediaTypes[getMediaType(selectedOutput.value)]) ??
{}
].filter((i) => !!i)
})
@@ -89,7 +100,7 @@ async function loadWorkflow(item: AssetItem | undefined) {
async function rerun(e: Event) {
if (!runButtonClick) return
await loadWorkflow(selectedItem)
await loadWorkflow(selectedItem.value)
//FIXME don't use timeouts here
//Currently seeds fail to properly update even with timeouts?
await new Promise((r) => setTimeout(r, 500))
@@ -136,28 +147,28 @@ async function rerun(e: Event) {
</Button>
<Popover
:entries="[
[
{
icon: 'icon-[lucide--download]',
label: t('linearMode.downloadAll'),
action: () => downloadAsset(selectedItem!)
}
],
[
{
icon: 'icon-[lucide--trash-2]',
label: t('queue.jobMenu.deleteAsset'),
action: () => mediaActions.deleteAssets(selectedItem!)
}
]
{
icon: 'icon-[lucide--download]',
label: t('linearMode.downloadAll'),
command: () => downloadAsset(selectedItem!)
},
{ separator: true },
{
icon: 'icon-[lucide--trash-2]',
label: t('queue.jobMenu.deleteAsset'),
command: () => mediaActions.deleteAssets(selectedItem!)
}
]"
/>
</div>
</section>
<ImagePreview
v-if="latentPreview ?? getMediaType(selectedOutput) === 'images'"
v-if="
(canShowPreview && latentPreview) ||
getMediaType(selectedOutput) === 'images'
"
:mobile
:src="latentPreview ?? selectedOutput!.url"
:src="(canShowPreview && latentPreview) || selectedOutput!.url"
/>
<VideoPreview
v-else-if="getMediaType(selectedOutput) === 'video'"
@@ -180,4 +191,12 @@ async function rerun(e: Event) {
:model-url="selectedOutput!.url"
/>
<LinearWelcome v-else />
<OutputHistory
@update-selection="
(event) => {
;[selectedItem, selectedOutput, canShowPreview] = event
latentPreview = undefined
}
"
/>
</template>

View File

@@ -2,24 +2,17 @@
import {
useAsyncState,
useEventListener,
useInfiniteScroll,
useScroll
useInfiniteScroll
} from '@vueuse/core'
import { computed, ref, toRaw, toValue, useTemplateRef, watch } from 'vue'
import type { MaybeRef } from 'vue'
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
import SidebarIcon from '@/components/sidebar/SidebarIcon.vue'
import SidebarTemplatesButton from '@/components/sidebar/SidebarTemplatesButton.vue'
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
import Button from '@/components/ui/button/Button.vue'
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useSettingStore } from '@/platform/settings/settingStore'
import {
getMediaType,
mediaTypes
@@ -27,10 +20,8 @@ import {
import type { NodeExecutionOutput, ResultItem } from '@/schemas/apiSchema'
import { getJobDetail } from '@/services/jobOutputCache'
import { useQueueStore, ResultItemImpl } from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { cn } from '@/utils/tailwindUtil'
const displayWorkflows = ref(false)
const outputs = useMediaAssets('output')
const {
progressBarContainerClass,
@@ -40,26 +31,15 @@ const {
} = useProgressBarBackground()
const { totalPercent, currentNodePercent } = useQueueProgress()
const queueStore = useQueueStore()
const settingStore = useSettingStore()
const workflowTab = useWorkspaceStore()
.getSidebarTabs()
.find((w) => w.id === 'workflows')
void outputs.fetchMediaList()
defineProps<{
scrollResetButtonTo?: string | HTMLElement
mobile?: boolean
}>()
const emit = defineEmits<{
updateSelection: [
selection: [AssetItem | undefined, ResultItemImpl | undefined, boolean]
]
}>()
defineExpose({ onWheel })
const selectedIndex = ref<[number, number]>([-1, 0])
function doEmit() {
@@ -72,17 +52,9 @@ function doEmit() {
}
const outputsRef = useTemplateRef('outputsRef')
const { reset: resetInfiniteScroll } = useInfiniteScroll(
outputsRef,
outputs.loadMore,
{ canLoadMore: () => outputs.hasMore.value }
)
function resetOutputsScroll() {
//TODO need to also prune outputs entries?
resetInfiniteScroll()
outputsRef.value?.scrollTo(0, 0)
}
const { y: outputScrollState } = useScroll(outputsRef)
useInfiniteScroll(outputsRef, outputs.loadMore, {
canLoadMore: () => outputs.hasMore.value
})
watch(selectedIndex, () => {
const [index, key] = selectedIndex.value
@@ -208,26 +180,31 @@ function gotoPreviousOutput() {
let pointer = new CanvasPointer(document.body)
let scrollOffset = 0
function onWheel(e: WheelEvent) {
if (!e.ctrlKey && !e.metaKey) return
e.preventDefault()
e.stopPropagation()
useEventListener(
document.body,
'wheel',
function (e: WheelEvent) {
if (!e.ctrlKey && !e.metaKey) return
e.preventDefault()
e.stopPropagation()
if (!pointer.isTrackpadGesture(e)) {
if (e.deltaY > 0) gotoNextOutput()
else gotoPreviousOutput()
return
}
scrollOffset += e.deltaY
while (scrollOffset >= 60) {
scrollOffset -= 60
gotoNextOutput()
}
while (scrollOffset <= -60) {
scrollOffset += 60
gotoPreviousOutput()
}
}
if (!pointer.isTrackpadGesture(e)) {
if (e.deltaY > 0) gotoNextOutput()
else gotoPreviousOutput()
return
}
scrollOffset += e.deltaY
while (scrollOffset >= 60) {
scrollOffset -= 60
gotoNextOutput()
}
while (scrollOffset <= -60) {
scrollOffset += 60
gotoPreviousOutput()
}
},
{ capture: true, passive: false }
)
useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
if (
@@ -244,138 +221,80 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
})
</script>
<template>
<div
:class="
cn(
'min-w-38 flex bg-comfy-menu-bg md:h-full border-border-subtle',
settingStore.get('Comfy.Sidebar.Location') === 'right'
? 'flex-row-reverse border-l'
: 'md:border-r'
)
"
v-bind="$attrs"
<article
ref="outputsRef"
data-testid="linear-outputs"
class="h-24 p-3 overflow-x-auto overflow-y-clip border-node-component-border flex items-center contain-size md:mx-15"
>
<div
v-if="!mobile"
class="h-full flex flex-col w-14 shrink-0 overflow-hidden items-center p-2"
<section
v-if="
queueStore.runningTasks.length > 0 || queueStore.pendingTasks.length > 0
"
data-testid="linear-job"
class="py-3 h-24 aspect-square px-1 relative"
>
<template v-if="workflowTab">
<SidebarIcon
:icon="workflowTab.icon"
:icon-badge="workflowTab.iconBadge"
:tooltip="workflowTab.tooltip"
:label="workflowTab.label || workflowTab.title"
:class="workflowTab.id + '-tab-button'"
:selected="displayWorkflows"
:is-small="settingStore.get('Comfy.Sidebar.Size') === 'small'"
@click="displayWorkflows = !displayWorkflows"
/>
</template>
<SidebarTemplatesButton />
<div class="flex-1" />
<ModeToggle />
</div>
<div class="border-border-subtle md:border-r" />
<WorkflowsSidebarTab v-if="displayWorkflows" class="min-w-50 grow-1" />
<article
v-else
ref="outputsRef"
data-testid="linear-outputs"
class="h-24 md:h-full min-w-24 grow-1 p-3 overflow-x-auto overflow-y-clip md:overflow-y-auto md:overflow-x-clip md:border-r-1 border-node-component-border flex md:flex-col items-center contain-size"
>
<section
<i
v-if="queueStore.runningTasks.length > 0"
class="icon-[lucide--loader-circle] size-full animate-spin"
/>
<i v-else class="icon-[lucide--ellipsis] size-full animate-pulse" />
<div
v-if="
queueStore.runningTasks.length > 0 ||
queueStore.pendingTasks.length > 0
queueStore.runningTasks.length + queueStore.pendingTasks.length > 1
"
data-testid="linear-job"
class="py-3 not-md:h-24 md:w-full aspect-square px-1 relative"
>
<i
v-if="queueStore.runningTasks.length > 0"
class="icon-[lucide--loader-circle] size-full animate-spin"
/>
<i v-else class="icon-[lucide--ellipsis] size-full animate-pulse" />
<div
v-if="
queueStore.runningTasks.length + queueStore.pendingTasks.length > 1
"
class="absolute top-0 right-0 p-1 min-w-5 h-5 flex justify-center items-center rounded-full bg-primary-background text-text-primary"
v-text="
queueStore.runningTasks.length + queueStore.pendingTasks.length
"
/>
<div class="absolute -bottom-1 w-full h-3 rounded-sm overflow-clip">
<div :class="progressBarContainerClass">
<div
:class="progressBarPrimaryClass"
:style="progressPercentStyle(totalPercent)"
/>
<div
:class="progressBarSecondaryClass"
:style="progressPercentStyle(currentNodePercent)"
/>
</div>
</div>
</section>
<template v-for="(item, index) in outputs.media.value" :key="index">
<div
class="border-border-subtle not-md:border-l md:border-t first:border-none not-md:h-21 md:w-full m-3"
/>
<template v-for="(output, key) in toValue(allOutputs(item))" :key>
<img
v-if="getMediaType(output) === 'images'"
:class="
cn(
'p-1 rounded-lg aspect-square object-cover not-md:h-20 md:w-full',
index === selectedIndex[0] &&
key === selectedIndex[1] &&
'border-2'
)
"
:data-output-index="index"
:src="output.url"
@click="selectedIndex = [index, key]"
class="absolute top-0 right-0 p-1 min-w-5 h-5 flex justify-center items-center rounded-full bg-primary-background text-text-primary"
v-text="queueStore.runningTasks.length + queueStore.pendingTasks.length"
/>
<div class="absolute -bottom-1 w-full h-3 rounded-sm overflow-clip">
<div :class="progressBarContainerClass">
<div
:class="progressBarPrimaryClass"
:style="progressPercentStyle(totalPercent)"
/>
<div
v-else
:class="progressBarSecondaryClass"
:style="progressPercentStyle(currentNodePercent)"
/>
</div>
</div>
</section>
<template v-for="(item, index) in outputs.media.value" :key="index">
<div class="border-border-subtle border-l first:border-none h-21 m-3" />
<template v-for="(output, key) in toValue(allOutputs(item))" :key>
<img
v-if="getMediaType(output) === 'images'"
:class="
cn(
'p-1 rounded-lg aspect-square object-cover h-20',
index === selectedIndex[0] &&
key === selectedIndex[1] &&
'border-2'
)
"
:data-output-index="index"
:src="output.url"
@click="selectedIndex = [index, key]"
/>
<div
v-else
:class="
cn(
'p-1 rounded-lg aspect-square w-full',
index === selectedIndex[0] &&
key === selectedIndex[1] &&
'border-2'
)
"
:data-output-index="index"
@click="selectedIndex = [index, key]"
>
<i
:class="
cn(
'p-1 rounded-lg aspect-square w-full',
index === selectedIndex[0] &&
key === selectedIndex[1] &&
'border-2'
)
cn(mediaTypes[getMediaType(output)]?.iconClass, 'size-full')
"
:data-output-index="index"
@click="selectedIndex = [index, key]"
>
<i
:class="
cn(mediaTypes[getMediaType(output)]?.iconClass, 'size-full')
"
/>
</div>
</template>
/>
</div>
</template>
</article>
</div>
<Teleport
v-if="outputScrollState && scrollResetButtonTo"
:to="scrollResetButtonTo"
>
<Button
:class="
cn(
'p-3 size-10 bg-base-foreground',
settingStore.get('Comfy.Sidebar.Location') === 'left'
? 'left-4'
: 'right-4'
)
"
@click="resetOutputsScroll"
>
<i class="icon-[lucide--arrow-up] size-4 bg-base-background" />
</Button>
</Teleport>
</template>
</article>
</template>

View File

@@ -1,44 +1,38 @@
<script setup lang="ts">
import {
breakpointsTailwind,
unrefElement,
useBreakpoints,
whenever
} from '@vueuse/core'
import { breakpointsTailwind, unrefElement, useBreakpoints } from '@vueuse/core'
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { ref, useTemplateRef } from 'vue'
import { computed, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import ExtensionSlot from '@/components/common/ExtensionSlot.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 Button from '@/components/ui/button/Button.vue'
import Popover from '@/components/ui/Popover.vue'
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { useWorkflowActionsMenu } from '@/composables/useWorkflowActionsMenu'
import { useSettingStore } from '@/platform/settings/settingStore'
import LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue'
import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'
import MobileMenu from '@/renderer/extensions/linearMode/MobileMenu.vue'
import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import type { ResultItemImpl } from '@/stores/queueStore'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
const { t } = useI18n()
const nodeOutputStore = useNodeOutputStore()
const settingStore = useSettingStore()
const workspaceStore = useWorkspaceStore()
const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')
const hasPreview = ref(false)
whenever(
() => nodeOutputStore.latestPreview[0],
() => (hasPreview.value = true)
)
const activeTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
const selectedItem = ref<AssetItem>()
const selectedOutput = ref<ResultItemImpl>()
const canShowPreview = ref(true)
const outputHistoryRef = useTemplateRef('outputHistoryRef')
const { menuItems } = useWorkflowActionsMenu(
() => useCommandStore().execute('Comfy.RenameWorkflow'),
{ isRoot: true }
)
const topLeftRef = useTemplateRef('topLeftRef')
const topRightRef = useTemplateRef('topRightRef')
@@ -47,10 +41,7 @@ const bottomRightRef = useTemplateRef('bottomRightRef')
const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
</script>
<template>
<div
class="absolute w-full h-full"
@wheel.capture="(e: WheelEvent) => outputHistoryRef?.onWheel(e)"
>
<div class="absolute w-full h-full">
<div class="workflow-tabs-container pointer-events-auto h-9.5 w-full">
<div class="flex h-full items-center">
<WorkflowTabs />
@@ -64,29 +55,10 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
<MobileMenu />
<div class="flex flex-col text-muted-foreground">
<LinearPreview
:latent-preview="
canShowPreview && hasPreview
? nodeOutputStore.latestPreview[0]
: undefined
"
:run-button-click="linearWorkflowRef?.runButtonClick"
:selected-item
:selected-output
mobile
/>
</div>
<OutputHistory
ref="outputHistoryRef"
mobile
@update-selection="
([item, output, canShow]) => {
selectedItem = item
selectedOutput = output
canShowPreview = canShow
hasPreview = false
}
"
/>
<LinearControls ref="linearWorkflowRef" mobile />
<div class="text-base-foreground flex items-center gap-4">
<div class="border-r border-border-subtle mr-auto">
@@ -99,7 +71,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
<Splitter
v-else
class="h-[calc(100%-38px)] w-full bg-comfy-menu-secondary-bg"
:pt="{ gutter: { class: 'bg-transparent w-4 -mx-3' } }"
:pt="{ gutter: { class: 'bg-transparent w-4 -mx-1' } }"
@resizestart="({ originalEvent }) => originalEvent.preventDefault()"
>
<SplitterPanel
@@ -107,50 +79,41 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
:size="1"
class="min-w-min outline-none"
>
<OutputHistory
<div
v-if="settingStore.get('Comfy.Sidebar.Location') === 'left'"
ref="outputHistoryRef"
:scroll-reset-button-to="unrefElement(bottomLeftRef) ?? undefined"
@update-selection="
([item, output, canShow]) => {
selectedItem = item
selectedOutput = output
canShowPreview = canShow
hasPreview = false
}
"
/>
class="flex h-full border-border-subtle border-r"
>
<SideToolbar />
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
</div>
<LinearControls
v-else
ref="linearWorkflowRef"
:toast-to="unrefElement(bottomLeftRef) ?? undefined"
:notes-to="unrefElement(topLeftRef) ?? undefined"
/>
<div />
</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"
>
<LinearPreview
:latent-preview="
canShowPreview && hasPreview
? nodeOutputStore.latestPreview[0]
: undefined
"
:run-button-click="linearWorkflowRef?.runButtonClick"
:selected-item
:selected-output
/>
<div ref="topLeftRef" class="absolute z-21 top-4 left-4" />
<LinearPreview :run-button-click="linearWorkflowRef?.runButtonClick" />
<div ref="topLeftRef" class="absolute z-21 top-4 left-4">
<Popover :entries="menuItems" align="start">
<template #button>
<Button size="icon" variant="textonly">
<i class="icon-[lucide--menu]" />
</Button>
</template>
</Popover>
</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
class="absolute z-20 bottom-4 right-4 text-base-foreground flex items-center gap-4"
>
<div v-text="t('linearMode.beta')" />
<TypeformPopoverButton
data-tf-widget="gmVqFi8l"
:align="
@@ -172,20 +135,10 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
:toast-to="unrefElement(bottomRightRef) ?? undefined"
:notes-to="unrefElement(topRightRef) ?? undefined"
/>
<OutputHistory
v-else
ref="outputHistoryRef"
:scroll-reset-button-to="unrefElement(bottomRightRef) ?? undefined"
@update-selection="
([item, output, canShow]) => {
selectedItem = item
selectedOutput = output
canShowPreview = canShow
hasPreview = false
}
"
/>
<div />
<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>
</SplitterPanel>
</Splitter>
</div>