Skip to content
Open
Show file tree
Hide file tree
Changes from 11 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
33 changes: 31 additions & 2 deletions src/components/LiteGraphCanvasSplitterOverlay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<Splitter
key="main-splitter-stable"
class="splitter-overlay flex-1 overflow-hidden"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
:state-key="sidebarStateKey || 'main-splitter'"
:pt:gutter="getSplitterGutterClasses"
:state-key="mainSplitterStateKey"
state-storage="local"
>
<SplitterPanel
Expand Down Expand Up @@ -80,6 +80,16 @@
name="side-bar-panel"
/>
</SplitterPanel>

<!-- Right Side Panel - independent of sidebar -->
<SplitterPanel
v-if="rightSidePanelVisible"
class="right-side-panel pointer-events-auto"
:min-size="15"
:size="20"
>
<slot name="right-side-panel" />
</SplitterPanel>
</Splitter>
</div>
</div>
Expand All @@ -92,9 +102,11 @@ import { computed } from 'vue'

import { useSettingStore } from '@/platform/settings/settingStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'

const settingStore = useSettingStore()
const rightSidePanelStore = useRightSidePanelStore()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
Expand All @@ -109,6 +121,7 @@ const sidebarPanelVisible = computed(
const bottomPanelVisible = computed(
() => useBottomPanelStore().bottomPanelVisible
)
const rightSidePanelVisible = computed(() => rightSidePanelStore.isOpen)
const activeSidebarTabId = computed(
() => useSidebarTabStore().activeSidebarTabId
)
Expand All @@ -120,6 +133,18 @@ const sidebarStateKey = computed(() => {
// When no tab is active, use a default key to maintain state
return activeSidebarTabId.value ?? 'default-sidebar'
})

const mainSplitterStateKey = computed(() => {
const baseKey = sidebarStateKey.value || 'main-splitter'
return rightSidePanelVisible.value ? `${baseKey}-with-right-panel` : baseKey
})

// Hide handles when both panels are hidden
const getSplitterGutterClasses = computed(() => {
return !sidebarPanelVisible.value && !rightSidePanelVisible.value
? 'hidden'
: ''
})
</script>

<style scoped>
Expand All @@ -139,6 +164,10 @@ const sidebarStateKey = computed(() => {
background-color: var(--bg-color);
}

.right-side-panel {
background-color: var(--bg-color);
}

.bottom-panel {
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
Expand Down
26 changes: 26 additions & 0 deletions src/components/TopMenuSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@
</IconButton>
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" />
<IconButton
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="transparent"
size="sm"
class="mr-2 transition-colors duration-200 ease-in-out hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-pressed="isRightSidePanelOpen"
:aria-label="t('rightSidePanel.togglePanel')"
@click="toggleRightSidePanel"
>
<i
class="icon-[lucide--panel-right] block size-4 text-muted-foreground"
/>
</IconButton>
Comment on lines +47 to +60
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove aria-pressed or keep button always visible for consistency.

The button is only rendered when the panel is closed (v-if="!isRightSidePanelOpen"), which means aria-pressed will always be false when the button is visible. This doesn't follow accessibility best practices for toggle buttons.

Consider one of these approaches:

  1. Recommended: Remove the aria-pressed attribute since the button's visibility already indicates state
  2. Alternative: Keep the button always visible (like the queue history button on lines 23-44) and toggle aria-pressed between true/false

Apply this diff to remove the unnecessary aria-pressed attribute:

         <IconButton
           v-if="!isRightSidePanelOpen"
           v-tooltip.bottom="rightSidePanelTooltipConfig"
           type="transparent"
           size="sm"
           class="mr-2 transition-colors duration-200 ease-in-out hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
-          :aria-pressed="isRightSidePanelOpen"
           :aria-label="t('rightSidePanel.togglePanel')"
           @click="toggleRightSidePanel"
         >
📝 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
<IconButton
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="transparent"
size="sm"
class="mr-2 transition-colors duration-200 ease-in-out hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-pressed="isRightSidePanelOpen"
:aria-label="t('rightSidePanel.togglePanel')"
@click="toggleRightSidePanel"
>
<i
class="icon-[lucide--panel-right] block size-4 text-muted-foreground"
/>
</IconButton>
<IconButton
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="transparent"
size="sm"
class="mr-2 transition-colors duration-200 ease-in-out hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-label="t('rightSidePanel.togglePanel')"
@click="toggleRightSidePanel"
>
<i
class="icon-[lucide--panel-right] block size-4 text-muted-foreground"
/>
</IconButton>
🤖 Prompt for AI Agents
In src/components/TopMenuSection.vue around lines 47 to 60, the IconButton is
conditionally rendered only when the right side panel is closed so aria-pressed
will always be false; remove the unnecessary :aria-pressed binding to avoid a
misleading toggle attribute, keeping the rest of the props and event handler
intact (or alternatively render the button unconditionally and keep
:aria-pressed toggling if you prefer the always-visible pattern).

</div>
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
Expand All @@ -68,10 +82,12 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { app } from '@/scripts/app'
import { useQueueStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'

const workspaceStore = useWorkspaceStore()
const rightSidePanelStore = useRightSidePanelStore()
const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron()
const { t } = useI18n()
Expand All @@ -88,6 +104,16 @@ const queueHistoryButtonBackgroundClass = computed(() =>
: 'bg-secondary-background'
)

// Right side panel toggle
const isRightSidePanelOpen = computed(() => rightSidePanelStore.isOpen)
const rightSidePanelTooltipConfig = computed(() =>
buildTooltipConfig(t('rightSidePanel.togglePanel'))
)

const toggleRightSidePanel = () => {
rightSidePanelStore.togglePanel()
}

// Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>()
onMounted(() => {
Expand Down
4 changes: 4 additions & 0 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
<template v-if="showUI" #bottom-panel>
<BottomPanel />
</template>
<template v-if="showUI" #right-side-panel>
<NodePropertiesPanel />
</template>
<template #graph-canvas-panel>
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
<MiniMap
Expand Down Expand Up @@ -110,6 +113,7 @@ import NodeTooltip from '@/components/graph/NodeTooltip.vue'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
import NodePropertiesPanel from '@/components/rightSidePanel/NodePropertiesPanel.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
Expand Down
230 changes: 230 additions & 0 deletions src/components/node/NodeHelpContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<template>
<div class="node-help-content mx-auto w-full">
<ProgressSpinner
v-if="isLoading"
class="m-auto"
:aria-label="$t('g.loading')"
/>
<!-- Markdown fetched successfully -->
<div
v-else-if="!error"
class="markdown-content"
v-html="renderedHelpHtml"
/>
<!-- Fallback: markdown not found or fetch error -->
<div v-else class="fallback-content space-y-6 text-sm">
<p v-if="node.description">
<strong>{{ $t('g.description') }}:</strong> {{ node.description }}
</p>

<div v-if="inputList.length">
<p>
<strong>{{ $t('nodeHelpPage.inputs') }}:</strong>
</p>
<!-- Using plain HTML table instead of DataTable for consistent styling with markdown content -->
<table class="overflow-x-auto">
<thead>
<tr>
<th>{{ $t('g.name') }}</th>
<th>{{ $t('nodeHelpPage.type') }}</th>
<th>{{ $t('g.description') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="input in inputList" :key="input.name">
<td>
<code>{{ input.name }}</code>
</td>
<td>{{ input.type }}</td>
<td>{{ input.tooltip || '-' }}</td>
</tr>
</tbody>
</table>
</div>

<div v-if="outputList.length">
<p>
<strong>{{ $t('nodeHelpPage.outputs') }}:</strong>
</p>
<table class="overflow-x-auto">
<thead>
<tr>
<th>{{ $t('g.name') }}</th>
<th>{{ $t('nodeHelpPage.type') }}</th>
<th>{{ $t('g.description') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="output in outputList" :key="output.name">
<td>
<code>{{ output.name }}</code>
</td>
<td>{{ output.type }}</td>
<td>{{ output.tooltip || '-' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import ProgressSpinner from 'primevue/progressspinner'
import { computed } from 'vue'

import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'

const { node } = defineProps<{ node: ComfyNodeDefImpl }>()

const nodeHelpStore = useNodeHelpStore()
const { renderedHelpHtml, isLoading, error } = storeToRefs(nodeHelpStore)

const inputList = computed(() =>
Object.values(node.inputs).map((spec) => ({
name: spec.name,
type: spec.type,
tooltip: spec.tooltip || ''
}))
)

const outputList = computed(() =>
node.outputs.map((spec) => ({
name: spec.name,
type: spec.type,
tooltip: spec.tooltip || ''
}))
)
</script>

<style scoped>
@reference './../../assets/css/style.css';

.node-help-content :deep(:is(img, video)) {
@apply max-w-full h-auto block mb-4;
}

.markdown-content,
.fallback-content {
@apply text-sm overflow-visible;
}

.markdown-content :deep(h1),
.fallback-content h1 {
@apply text-[22px] font-bold mt-8 mb-4 first:mt-0;
}

.markdown-content :deep(h2),
.fallback-content h2 {
@apply text-[18px] font-bold mt-8 mb-4 first:mt-0;
}

.markdown-content :deep(h3),
.fallback-content h3 {
@apply text-[16px] font-bold mt-8 mb-4 first:mt-0;
}

.markdown-content :deep(h4),
.markdown-content :deep(h5),
.markdown-content :deep(h6),
.fallback-content h4,
.fallback-content h5,
.fallback-content h6 {
@apply mt-8 mb-4 first:mt-0;
}

.markdown-content :deep(td),
.fallback-content td {
color: var(--drag-text);
}

.markdown-content :deep(a),
.fallback-content a {
color: var(--drag-text);
text-decoration: underline;
}

.markdown-content :deep(th),
.fallback-content th {
color: var(--fg-color);
}

.markdown-content :deep(ul),
.markdown-content :deep(ol),
.fallback-content ul,
.fallback-content ol {
@apply pl-8 my-2;
}

.markdown-content :deep(ul ul),
.markdown-content :deep(ol ol),
.markdown-content :deep(ul ol),
.markdown-content :deep(ol ul),
.fallback-content ul ul,
.fallback-content ol ol,
.fallback-content ul ol,
.fallback-content ol ul {
@apply pl-6 my-2;
}

.markdown-content :deep(li),
.fallback-content li {
@apply my-2;
}

.markdown-content :deep(*:first-child),
.fallback-content > *:first-child {
@apply mt-0;
}

.markdown-content :deep(code),
.fallback-content code {
color: var(--code-text-color);
background-color: var(--code-bg-color);
@apply rounded px-1.5 py-0.5;
}

.markdown-content :deep(table),
.fallback-content table {
@apply w-full border-collapse;
}

.markdown-content :deep(th),
.markdown-content :deep(td),
.fallback-content th,
.fallback-content td {
@apply px-2 py-2;
}

.markdown-content :deep(tr),
.fallback-content tr {
border-bottom: 1px solid var(--content-bg);
}

.markdown-content :deep(tr:last-child),
.fallback-content tr:last-child {
border-bottom: none;
}

.markdown-content :deep(thead),
.fallback-content thead {
border-bottom: 1px solid var(--p-text-color);
}

.markdown-content :deep(pre),
.fallback-content pre {
@apply rounded p-4 my-4 overflow-x-auto;
background-color: var(--code-block-bg-color);

code {
@apply bg-transparent p-0;
color: var(--p-text-color);
}
}

.markdown-content :deep(table) {
@apply overflow-x-auto;
}
</style>
Loading
Loading