Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/components/sidebar/SideToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@
<SidebarBottomPanelToggleButton :is-small="isSmall" />
<SidebarShortcutsToggleButton :is-small="isSmall" />
<SidebarSettingsButton :is-small="isSmall" />
<ModeToggle v-if="showLinearToggle" />
<ModeToggle v-if="menuItemStore.hasSeenLinear || linearFeatureFlag" />
</div>
</div>
<HelpCenterPopups :is-small="isSmall" />
</nav>
</template>

<script setup lang="ts">
import { useResizeObserver, whenever } from '@vueuse/core'
import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'es-toolkit/compat'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

Expand All @@ -68,6 +68,7 @@ import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
Expand All @@ -83,14 +84,14 @@ const settingStore = useSettingStore()
const userStore = useUserStore()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const menuItemStore = useMenuItemStore()
const sideToolbarRef = ref<HTMLElement>()
const topToolbarRef = ref<HTMLElement>()
const bottomToolbarRef = ref<HTMLElement>()

const showLinearToggle = ref(useFeatureFlags().flags.linearToggleEnabled)
whenever(
() => canvasStore.linearMode,
() => (showLinearToggle.value = true)
const linearFeatureFlag = useFeatureFlags().featureFlag(
'linearToggleEnabled',
false
)

const isSmall = computed(
Expand Down
6 changes: 5 additions & 1 deletion src/lib/litegraph/src/types/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ interface IWidgetKnobOptions extends IWidgetOptions<number[]> {
gradient_stops?: string
}

export interface IWidgetAssetOptions extends IWidgetOptions {
openModal: () => void
}

/**
* A widget for a node.
* All types are based on IBaseWidget - additions can be made there or directly on individual types.
Expand Down Expand Up @@ -249,7 +253,7 @@ export interface ITextareaWidget extends IBaseWidget<string, 'textarea'> {
export interface IAssetWidget extends IBaseWidget<
string,
'asset',
IWidgetOptions<string[]>
IWidgetAssetOptions
> {
type: 'asset'
value: string
Expand Down
2 changes: 1 addition & 1 deletion src/lib/litegraph/src/widgets/AssetWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ export class AssetWidget

override onClick() {
//Open Modal
this.callback?.(this.value)
this.options.openModal()
}
}
14 changes: 11 additions & 3 deletions src/renderer/extensions/linearMode/DropZone.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ defineProps<{
onDragOver?: (e: DragEvent) => boolean
onDragDrop?: (e: DragEvent) => Promise<boolean> | boolean
dropIndicator?: {
label?: string
iconClass?: string
imageUrl?: string
label?: string
onClick?: (e: MouseEvent) => void
}
}>()
Expand Down Expand Up @@ -44,8 +45,15 @@ const canAcceptDrop = ref(false)
"
@click.prevent="dropIndicator?.onClick?.($event)"
>
<span v-if="dropIndicator.label" v-text="dropIndicator.label" />
<i v-if="dropIndicator.iconClass" :class="dropIndicator.iconClass" />
<img
v-if="dropIndicator?.imageUrl"
class="h-23"
:src="dropIndicator?.imageUrl"
/>
Comment on lines +48 to +52
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add alt attribute for accessibility.

The <img> element is missing an alt attribute, which is required for screen readers and accessibility compliance. Consider using the dropIndicator.label as fallback alt text.

♿ Proposed fix
       <img
         v-if="dropIndicator?.imageUrl"
         class="h-23"
         :src="dropIndicator?.imageUrl"
+        :alt="dropIndicator?.label ?? ''"
       />
🤖 Prompt for AI Agents
In `@src/renderer/extensions/linearMode/DropZone.vue` around lines 48 - 52, The
<img> in DropZone.vue lacks an alt attribute; update the image element (the one
using dropIndicator?.imageUrl) to include an alt prop that uses the accessible
label when available (e.g. dropIndicator?.label as the fallback) so screen
readers get meaningful text — set the alt to dropIndicator?.label || '' to cover
both labeled and decorative images.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if it's decorative?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DrJKL Good point! Even if the image is decorative, the alt attribute should still be present—use an empty string (alt="") to signal to screen readers that the image can be safely ignored.

However, looking at the context from LinearControls.vue, this image appears to show a preview of the input image (e.g., from a LoadImage node), which suggests it's informative rather than purely decorative. In that case, descriptive alt text would be more appropriate.

Here's a flexible approach:

<img
  v-if="dropIndicator?.imageUrl"
  class="h-23"
  :src="dropIndicator?.imageUrl"
  :alt="dropIndicator?.label ?? ''"
/>

This uses the label as alt text if available, or an empty string if not. If you determine the image is truly decorative in this context, you could explicitly set alt="". But omitting the alt attribute entirely is an accessibility violation either way.

What's your take—should this preview image be considered informative or decorative?


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7603
File: src/components/queue/QueueOverlayHeader.vue:49-59
Timestamp: 2025-12-18T21:15:54.349Z
Learning: For Vue components in Comfy-Org/ComfyUI_frontend: Do not suggest adding aria-label to buttons that already have visible text content (e.g., <span> elements with labels). The visible text automatically provides the accessible name. aria-label should only be used for elements without visible labels, such as icon-only buttons.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🌐 Web query:

W3C WAI decorative images criteria when to use empty alt

💡 Result:

Use an empty alt (alt="") when the image is purely decorative or its information is already conveyed by surrounding content so assistive technology should ignore it. Specific W3C/WAI guidance:

  • Decorative only (no informational value): page chrome, borders, spacers, ambient "eye‑candy". Use alt="". [1]
  • Redundant with nearby text: the image merely repeats meaning already given in adjacent/visible text (e.g., an icon next to descriptive text). Use alt="". [2]
  • Image contains text that is also present as real text nearby, or text shown only for visual effect. Use alt="". [2]
  • When an image should be ignored for WCAG 1.1.1 conformance, provide alt="" (or role="presentation"/aria-hidden) — leaving out alt or using filler text (e.g., alt="image" or alt="spacer") causes failure. [3]

References:
[1] W3C WAI — Decorative Images tutorial.
[2] W3C WAI — Images decision tree / tutorial.
[3] W3C WCAG techniques (F38 / F39) — failures for not marking decorative images or using non-null text.


@DrJKL You're absolutely right! According to the W3C guidance, decorative images should use alt="" when they don't convey essential information or when the meaning is already provided by surrounding context.

In this case, the preview thumbnail is decorative because:

  • The semantic meaning (drop zone for image input) is conveyed by the surrounding UI context
  • The specific visual content of the preview doesn't add information that screen reader users need to interact with the feature
  • It's analogous to a thumbnail next to a filename—visual enhancement rather than semantic content

Updated recommendation:

<img
  v-if="dropIndicator?.imageUrl"
  class="h-23"
  :src="dropIndicator?.imageUrl"
  alt=""
/>

Thanks for the correction!

<template v-else>
<span v-if="dropIndicator.label" v-text="dropIndicator.label" />
<i v-if="dropIndicator.iconClass" :class="dropIndicator.iconClass" />
</template>
</div>
</div>
<slot v-else />
Expand Down
32 changes: 22 additions & 10 deletions src/renderer/extensions/linearMode/LinearControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import DropZone from '@/renderer/extensions/linearMode/DropZone.vue'
import NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'
import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'
import WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useExecutionStore } from '@/stores/executionStore'
Expand Down Expand Up @@ -50,15 +51,26 @@ useEventListener(
() => (graphNodes.value = app.rootGraph.nodes)
)

function getDropIndicator(node: LGraphNode) {
if (node.type !== 'LoadImage') return undefined

const filename = node.widgets?.[0]?.value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I don't love that we have magic number order dependence for the widgets.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likewise. The localization and functionality is pretty tightly tied to Load Image.

I considered fleshing this out more and letting the node define these properties, but opted to side with YAGNI.

const resultItem = { type: 'input', filename: `${filename}` }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename in here might show as undefined, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the object isn't actually used unless filename is true.

Unsure on ick, but inlining resultItem felt excessive.


return {
iconClass: 'icon-[lucide--image]',
imageUrl: filename
? api.apiURL(
`/view?${new URLSearchParams(resultItem)}${app.getPreviewFormatParam()}`
)
: undefined,
label: t('linearMode.dragAndDropImage'),
onClick: () => node.widgets?.[1]?.callback?.(undefined)
}
}
Comment on lines +54 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against undefined filename in URL construction.

If node.widgets?.[0]?.value is undefined or null, the string template `${filename}` will produce the literal string "undefined" or "null", which would result in an invalid API URL request. Consider a stricter check:

🛠️ Proposed fix
 function getDropIndicator(node: LGraphNode) {
   if (node.type !== 'LoadImage') return undefined

   const filename = node.widgets?.[0]?.value
-  const resultItem = { type: 'input', filename: `${filename}` }
+  const filenameStr = filename != null ? String(filename) : undefined
+  const resultItem = filenameStr ? { type: 'input', filename: filenameStr } : undefined

   return {
     iconClass: 'icon-[lucide--image]',
-    imageUrl: filename
+    imageUrl: resultItem
       ? api.apiURL(
           `/view?${new URLSearchParams(resultItem)}${app.getPreviewFormatParam()}`
         )
       : undefined,
     label: t('linearMode.dragAndDropImage'),
     onClick: () => node.widgets?.[1]?.callback?.(undefined)
   }
 }
🤖 Prompt for AI Agents
In `@src/renderer/extensions/linearMode/LinearControls.vue` around lines 54 - 70,
In getDropIndicator, guard against filename being undefined/null before building
resultItem and the imageUrl: ensure you only construct resultItem (or include
the filename query) and call api.apiURL when filename is a non-empty string
(e.g., check filename with a strict truthy test), and use
encodeURIComponent(filename) when composing the query; update references in
getDropIndicator so node.widgets?.[0]?.value is validated and imageUrl is
undefined when filename is absent to avoid creating URLs containing
"undefined"/"null".


function nodeToNodeData(node: LGraphNode) {
const dropIndicator =
node.type !== 'LoadImage'
? undefined
: {
iconClass: 'icon-[lucide--image]',
label: t('linearMode.dragAndDropImage'),
onClick: () => node.widgets?.[1]?.callback?.(undefined)
}
const dropIndicator = getDropIndicator(node)
const nodeData = extractVueNodeData(node)
for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined

Expand Down Expand Up @@ -107,7 +119,7 @@ async function runButtonClick(e: Event) {
: 'Comfy.QueuePrompt'

useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_run_linear'
button_id: props.mobile ? 'queue_run_linear_mobile' : 'queue_run_linear'
})
if (batchCount.value > 1) {
useTelemetry()?.trackUiButtonClicked({
Expand Down Expand Up @@ -196,7 +208,7 @@ defineExpose({ runButtonClick })
<NodeWidgets
:node-data
:style="{ background: applyLightThemeColor(nodeData.bgcolor) }"
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0 rounded-lg"
class="py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0 rounded-lg max-w-100"
/>
</template>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/extensions/linearMode/LinearPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ async function rerun(e: Event) {
<VideoPreview
v-else-if="getMediaType(selectedOutput) === 'video'"
:src="selectedOutput!.url"
class="object-contain flex-1 contain-size"
class="object-contain flex-1 md:contain-size"
/>
<audio
v-else-if="getMediaType(selectedOutput) === 'audio'"
Expand All @@ -178,7 +178,7 @@ async function rerun(e: Event) {
/>
<img
v-else
class="pointer-events-none object-contain flex-1 max-h-full brightness-50 opacity-10"
class="pointer-events-none object-contain flex-1 max-h-full md:contain-size brightness-50 opacity-10"
src="/assets/images/comfy-logo-mono.svg"
/>
</template>
2 changes: 1 addition & 1 deletion src/renderer/extensions/linearMode/OutputHistory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ useEventListener(document.body, 'keydown', (e: KeyboardEvent) => {
<ModeToggle />
</div>
<div class="border-border-subtle md:border-r" />
<WorkflowsSidebarTab v-if="displayWorkflows" class="min-w-50" />
<WorkflowsSidebarTab v-if="displayWorkflows" class="min-w-50 grow-1" />
<article
v-else
ref="outputsRef"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ describe('useComboWidget', () => {
'asset',
'ckpt_name',
'model1.safetensors',
expect.any(Function)
expect.any(Function),
expect.any(Object)
)
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
expect(vi.mocked(assetService.isAssetBrowserEligible)).toHaveBeenCalledWith(
Expand Down Expand Up @@ -250,7 +251,8 @@ describe('useComboWidget', () => {
'asset',
'ckpt_name',
'fallback.safetensors',
expect.any(Function)
expect.any(Function),
expect.any(Object)
)
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
expect(widget).toBe(mockWidget)
Expand Down Expand Up @@ -280,7 +282,8 @@ describe('useComboWidget', () => {
'asset',
'ckpt_name',
'Select model', // Should fallback to this instead of undefined
expect.any(Function)
expect.any(Function),
expect.any(Object)
)
expect(mockSettingStoreGet).toHaveBeenCalledWith('Comfy.Assets.UseAssetAPI')
expect(widget).toBe(mockWidget)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import MultiSelectWidget from '@/components/graph/widgets/MultiSelectWidget.vue'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { isAssetWidget, isComboWidget } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type {
IBaseWidget,
IWidgetAssetOptions
} from '@/lib/litegraph/src/types/widgets'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import {
assetFilenameSchema,
Expand Down Expand Up @@ -91,55 +94,59 @@ const createAssetBrowserWidget = (
const displayLabel = currentValue ?? t('widgets.selectModel')
const assetBrowserDialog = useAssetBrowserDialog()

async function openModal(this: IBaseWidget) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This feels like it should be in a different module.
I know it's just being moved here, maybe we can clean it up later

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I specifically do not like that files in @/renderer/extensions/vueNodes are used and required for litegraph mode.

if (!isAssetWidget(widget)) {
throw new Error(`Expected asset widget but received ${widget.type}`)
}
await assetBrowserDialog.show({
nodeType: node.comfyClass || '',
inputName: inputSpec.name,
currentValue: widget.value,
onAssetSelected: (asset) => {
const validatedAsset = assetItemSchema.safeParse(asset)

if (!validatedAsset.success) {
console.error(
'Invalid asset item:',
validatedAsset.error.errors,
'Received:',
asset
)
return
}

const filename = validatedAsset.data.user_metadata?.filename
const validatedFilename = assetFilenameSchema.safeParse(filename)

if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors,
'for asset:',
validatedAsset.data.id
)
return
}

const oldValue = widget.value
this.value = validatedFilename.data
node.onWidgetChanged?.(
widget.name,
validatedFilename.data,
oldValue,
widget
)
}
})
}
Comment on lines +97 to +141
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent use of this vs widget reference.

The function mixes this (line 132) with the closure-captured widget variable (lines 98, 104, 131, 133-138). While both reference the same object at runtime, this inconsistency is confusing and error-prone:

  • Line 131: const oldValue = widget.value
  • Line 132: this.value = validatedFilename.data ← uses this
  • Line 134: widget.name

Choose one approach consistently. Since widget is already captured via closure and used throughout, prefer using widget everywhere:

Suggested fix
-        const oldValue = widget.value
-        this.value = validatedFilename.data
+        const oldValue = widget.value
+        widget.value = validatedFilename.data
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function openModal(this: IBaseWidget) {
if (!isAssetWidget(widget)) {
throw new Error(`Expected asset widget but received ${widget.type}`)
}
await assetBrowserDialog.show({
nodeType: node.comfyClass || '',
inputName: inputSpec.name,
currentValue: widget.value,
onAssetSelected: (asset) => {
const validatedAsset = assetItemSchema.safeParse(asset)
if (!validatedAsset.success) {
console.error(
'Invalid asset item:',
validatedAsset.error.errors,
'Received:',
asset
)
return
}
const filename = validatedAsset.data.user_metadata?.filename
const validatedFilename = assetFilenameSchema.safeParse(filename)
if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors,
'for asset:',
validatedAsset.data.id
)
return
}
const oldValue = widget.value
this.value = validatedFilename.data
node.onWidgetChanged?.(
widget.name,
validatedFilename.data,
oldValue,
widget
)
}
})
}
async function openModal(this: IBaseWidget) {
if (!isAssetWidget(widget)) {
throw new Error(`Expected asset widget but received ${widget.type}`)
}
await assetBrowserDialog.show({
nodeType: node.comfyClass || '',
inputName: inputSpec.name,
currentValue: widget.value,
onAssetSelected: (asset) => {
const validatedAsset = assetItemSchema.safeParse(asset)
if (!validatedAsset.success) {
console.error(
'Invalid asset item:',
validatedAsset.error.errors,
'Received:',
asset
)
return
}
const filename = validatedAsset.data.user_metadata?.filename
const validatedFilename = assetFilenameSchema.safeParse(filename)
if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors,
'for asset:',
validatedAsset.data.id
)
return
}
const oldValue = widget.value
widget.value = validatedFilename.data
node.onWidgetChanged?.(
widget.name,
validatedFilename.data,
oldValue,
widget
)
}
})
}
🤖 Prompt for AI Agents
In `@src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts`
around lines 97 - 141, In openModal's onAssetSelected callback the code mixes
this and the closure-captured widget (e.g., const oldValue = widget.value, then
this.value = ... and later widget.name); change the assignment to use widget
consistently (replace this.value = validatedFilename.data with widget.value =
validatedFilename.data) and ensure any other references inside onAssetSelected
use widget (not this) so node.onWidgetChanged is called with the closure widget,
preserving oldValue and the same object throughout.

const options: IWidgetAssetOptions = { openModal }

const widget = node.addWidget(
'asset',
inputSpec.name,
displayLabel,
async function (this: IBaseWidget) {
if (!isAssetWidget(widget)) {
throw new Error(`Expected asset widget but received ${widget.type}`)
}
await assetBrowserDialog.show({
nodeType: node.comfyClass || '',
inputName: inputSpec.name,
currentValue: widget.value,
onAssetSelected: (asset) => {
const validatedAsset = assetItemSchema.safeParse(asset)

if (!validatedAsset.success) {
console.error(
'Invalid asset item:',
validatedAsset.error.errors,
'Received:',
asset
)
return
}

const filename = validatedAsset.data.user_metadata?.filename
const validatedFilename = assetFilenameSchema.safeParse(filename)

if (!validatedFilename.success) {
console.error(
'Invalid asset filename:',
validatedFilename.error.errors,
'for asset:',
validatedAsset.data.id
)
return
}

const oldValue = widget.value
this.value = validatedFilename.data
node.onWidgetChanged?.(
widget.name,
validatedFilename.data,
oldValue,
widget
)
}
})
}
() => undefined,
options
)

return widget
Expand Down
13 changes: 12 additions & 1 deletion src/stores/menuItemStore.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import { whenever } from '@vueuse/core'
import { defineStore } from 'pinia'
import type { MenuItem } from 'primevue/menuitem'
import { ref } from 'vue'

import { CORE_MENU_COMMANDS } from '@/constants/coreMenuCommands'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { ComfyExtension } from '@/types/comfy'

import { useCommandStore } from './commandStore'

export const useMenuItemStore = defineStore('menuItem', () => {
const canvasStore = useCanvasStore()
const commandStore = useCommandStore()
const menuItems = ref<MenuItem[]>([])
const menuItemHasActiveStateChildren = ref<Record<string, boolean>>({})
const hasSeenLinear = ref(false)

whenever(
() => canvasStore.linearMode,
() => (hasSeenLinear.value = true),
{ immediate: true, once: true }
)

const registerMenuGroup = (path: string[], items: MenuItem[]) => {
let currentLevel = menuItems.value
Expand Down Expand Up @@ -103,6 +113,7 @@ export const useMenuItemStore = defineStore('menuItem', () => {
registerCommands,
loadExtensionMenuCommands,
registerCoreMenuCommands,
menuItemHasActiveStateChildren
menuItemHasActiveStateChildren,
hasSeenLinear
}
})
4 changes: 2 additions & 2 deletions src/views/LinearView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
:selected-item
:selected-output
/>
<div ref="topLeftRef" class="absolute z-20 top-4 left-4" />
<div ref="topRightRef" class="absolute z-20 top-4 right-4" />
<div ref="topLeftRef" class="absolute z-21 top-4 left-4" />
<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
Expand Down