rework minimap, toolbox, and menu designs with unified theming (#6038)

## Summary

This PR redesigns the graph canvas interface components including
minimap, toolbox, and menu systems with updated spacing, colors, and
interaction patterns - using the design tokens directly from Figma,
which can be used elsewhere going forward.

There are some other changes to the designs, outlined
[here](https://www.notion.so/comfy-org/Update-Minimap-Menu-v2-2886d73d365080e88e12f8df027019c0):

- [x]  Update/standardize the padding between viewport and toolbox
- [x] Update toolbox component’s style to match the other floating menus
style (border radius, height, padding and follow theme colors)
- [x]  Expose the minimap button
- [x]  Remove the focus button and delete it’s keybinding
- [x]  Group the hand and the default cursor buttons


https://github.com/user-attachments/assets/92542e60-c32d-4a21-a6f6-e72837a70b17

## Review Focus

New CSS variables for cross-component theming consistency and
CanvasModeSelector component extraction for improved code organization.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6038-rework-minimap-toolbox-and-menu-designs-with-unified-theming-28b6d73d36508191a0c6cf8036d965c4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Christian Byrne
2025-10-14 14:26:07 -07:00
committed by GitHub
parent 2058967761
commit 10af2300fa
43 changed files with 289 additions and 385 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -39,15 +39,15 @@ test.describe('Graph Canvas Menu', () => {
)
})
test('Focus mode button is clickable and has correct test id', async ({
test('Toggle minimap button is clickable and has correct test id', async ({
comfyPage
}) => {
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
await expect(focusButton).toBeVisible()
await expect(focusButton).toBeEnabled()
const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapButton).toBeVisible()
await expect(minimapButton).toBeEnabled()
// Test that the button can be clicked without error
await focusButton.click()
await minimapButton.click()
await comfyPage.nextFrame()
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -3,10 +3,10 @@ import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import {
type ComfyPage,
comfyPageFixture as test,
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
@@ -786,16 +786,8 @@ test.describe('Viewport settings', () => {
// Screenshot the canvas element
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await toggleButton.click()
// close zoom menu
await zoomControlsButton.click()
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.menu.topbar.saveWorkflow('Workflow A')

View File

@@ -35,12 +35,6 @@ test.describe('Minimap', () => {
})
test('Validate minimap toggle button state', async ({ comfyPage }) => {
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(toggleButton).toBeVisible()
@@ -51,13 +45,6 @@ test.describe('Minimap', () => {
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapContainer).toBeVisible()
@@ -67,22 +54,10 @@ test.describe('Minimap', () => {
await expect(minimapContainer).not.toBeVisible()
// Open zoom controls dropdown again
await zoomControlsButton.click()
await comfyPage.nextFrame()
await expect(toggleButton).toContainText('Show Minimap')
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).toBeVisible()
// Open zoom controls dropdown again to verify button text
await zoomControlsButton.click()
await comfyPage.nextFrame()
await expect(toggleButton).toContainText('Hide Minimap')
})
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -24,11 +24,14 @@
--text-xxs: 0.625rem;
--text-xxs--line-height: calc(1 / 0.625);
/* Spacing */
--spacing-xs: 8px;
/* Font Families */
--font-inter: 'Inter', sans-serif;
/* Palette Colors */
--color-charcoal-100: #55565e;
--color-charcoal-100: #171718;
--color-charcoal-200: #494a50;
--color-charcoal-300: #3c3d42;
--color-charcoal-400: #313235;
@@ -146,8 +149,15 @@
--accent-primary: var(--color-charcoal-700);
--backdrop: var(--color-white);
--button-hover-surface: var(--color-gray-200);
--button-active-surface: var(--color-gray-400);
--dialog-surface: var(--color-neutral-200);
--interface-panel-surface: var(--color-pure-white);
--interface-stroke: var(--color-gray-300);
--nav-background: var(--color-pure-white);
--node-border: var(--color-gray-300);
--node-component-border: var(--color-gray-400);
--node-component-disabled: var(--color-alpha-stone-100-20);
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-stone-200);
@@ -159,7 +169,7 @@
--node-component-slot-dot-outline: var(--color-black);
--node-component-slot-text: var(--color-stone-200);
--node-component-surface-highlight: var(--color-stone-100);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-hovered: var(--color-gray-200);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-white);
--node-component-tooltip: var(--color-charcoal-700);
@@ -170,18 +180,27 @@
from var(--color-zinc-500) r g b / 10%
);
--node-component-widget-skeleton-surface: var(--color-zinc-300);
--node-component-disabled: var(--color-alpha-stone-100-20);
--node-divider: var(--color-sand-100);
--node-icon-disabled: var(--color-alpha-gray-500-50);
--node-stroke: var(--color-gray-400);
--node-stroke-selected: var(--color-accent-primary);
--node-stroke-error: var(--color-error);
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-stone-100);
--text-primary: var(--color-charcoal-700);
--input-surface: rgba(0, 0, 0, 0.15);
}
.dark-theme {
--accent-primary: var(--color-pure-white);
--backdrop: var(--color-neutral-900);
--button-hover-surface: var(--color-charcoal-600);
--button-active-surface: var(--color-charcoal-600);
--dialog-surface: var(--color-neutral-700);
--interface-panel-surface: var(--color-charcoal-100);
--interface-stroke: var(--color-charcoal-400);
--nav-background: var(--color-charcoal-100);
--node-border: var(--color-charcoal-500);
--node-component-border: var(--color-stone-200);
--node-component-border-error: var(--color-danger-100);
--node-component-border-executing: var(--color-blue-500);
@@ -194,7 +213,7 @@
--node-component-slot-dot-outline: var(--color-white);
--node-component-slot-text: var(--color-slate-200);
--node-component-surface-highlight: var(--color-slate-100);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-hovered: var(--color-charcoal-600);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-charcoal-800);
--node-component-tooltip: var(--color-white);
@@ -202,16 +221,26 @@
--node-component-tooltip-surface: var(--color-charcoal-800);
--node-component-widget-skeleton-surface: var(--color-zinc-800);
--node-component-disabled: var(--color-alpha-charcoal-600-30);
--node-divider: var(--color-charcoal-500);
--node-icon-disabled: var(--color-alpha-stone-100-20);
--node-stroke: var(--color-stone-200);
--node-stroke-selected: var(--color-pure-white);
--node-stroke-error: var(--color-error);
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-slate-100);
--text-primary: var(--color-pure-white);
--input-surface: rgba(130, 130, 130, 0.1);
}
@theme inline {
--color-backdrop: var(--backdrop);
--color-button-hover-surface: var(--button-hover-surface);
--color-button-active-surface: var(--button-active-surface);
--color-dialog-surface: var(--dialog-surface);
--color-interface-panel-surface: var(--interface-panel-surface);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
--color-node-border: var(--node-border);
--color-node-component-border: var(--node-component-border);
--color-node-component-executing: var(--node-component-executing);
--color-node-component-header: var(--node-component-header);
@@ -244,11 +273,15 @@
--node-component-widget-skeleton-surface
);
--color-node-component-disabled: var(--node-component-disabled);
--color-node-divider: var(--node-divider);
--color-node-icon-disabled: var(--node-icon-disabled);
--color-node-stroke: var(--node-stroke);
--color-node-stroke-selected: var(--node-stroke-selected);
--color-node-stroke-error: var(--node-stroke-error);
--color-node-stroke-executing: var(--node-stroke-executing);
--color-text-secondary: var(--text-secondary);
--color-text-primary: var(--text-primary);
--color-input-surface: var(--input-surface);
}
@custom-variant dark-theme {

View File

@@ -0,0 +1,126 @@
<template>
<Button
ref="buttonRef"
severity="secondary"
class="group h-8 rounded-none! bg-interface-panel-surface p-0 transition-none! hover:rounded-lg! hover:bg-button-hover-surface!"
:style="buttonStyles"
@click="toggle"
>
<template #default>
<div class="flex items-center gap-1 pr-0.5">
<div
class="rounded-lg bg-button-active-surface p-2 group-hover:bg-button-hover-surface"
>
<i :class="currentModeIcon" class="block h-4 w-4" />
</div>
<i class="icon-[lucide--chevron-down] block h-4 w-4 pr-1.5" />
</div>
</template>
</Button>
<Popover
ref="popover"
:auto-z-index="true"
:base-z-index="1000"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="popoverPt"
>
<div class="flex flex-col gap-1">
<div
class="flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="setMode('select')"
>
<div class="flex items-center gap-2">
<i class="icon-[lucide--mouse-pointer-2] h-4 w-4" />
<span>{{ $t('graphCanvasMenu.select') }}</span>
</div>
<span class="text-[9px] text-text-primary">{{
unlockCommandText
}}</span>
</div>
<div
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="setMode('hand')"
>
<div class="flex items-center gap-2">
<i class="icon-[lucide--hand] h-4 w-4" />
<span>{{ $t('graphCanvasMenu.hand') }}</span>
</div>
<span class="text-[9px] text-text-primary">{{ lockCommandText }}</span>
</div>
</div>
</Popover>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
interface Props {
buttonStyles?: Record<string, string>
}
defineProps<Props>()
const buttonRef = ref<InstanceType<typeof Button>>()
const popover = ref<InstanceType<typeof Popover>>()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const isCanvasReadOnly = computed(() => canvasStore.canvas?.read_only ?? false)
const currentModeIcon = computed(() =>
isCanvasReadOnly.value
? 'icon-[lucide--hand]'
: 'icon-[lucide--mouse-pointer-2]'
)
const unlockCommandText = computed(() =>
commandStore
.formatKeySequence(commandStore.getCommand('Comfy.Canvas.Unlock'))
.toUpperCase()
)
const lockCommandText = computed(() =>
commandStore
.formatKeySequence(commandStore.getCommand('Comfy.Canvas.Lock'))
.toUpperCase()
)
const toggle = (event: Event) => {
const el = (buttonRef.value as any)?.$el || buttonRef.value
popover.value?.toggle(event, el)
}
const setMode = (mode: 'select' | 'hand') => {
if (mode === 'select' && isCanvasReadOnly.value) {
void commandStore.execute('Comfy.Canvas.Unlock')
} else if (mode === 'hand' && !isCanvasReadOnly.value) {
void commandStore.execute('Comfy.Canvas.Lock')
}
popover.value?.hide()
}
const popoverPt = computed(() => ({
root: {
class: 'absolute z-50 -translate-y-2'
},
content: {
class: [
'mb-2 text-text-primary',
'shadow-lg border border-node-border',
'bg-nav-background',
'rounded-lg',
'p-2 px-3',
'min-w-39',
'select-none'
]
}
}))
</script>

View File

@@ -10,41 +10,15 @@
></div>
<ButtonGroup
class="p-buttongroup-vertical absolute right-2 bottom-4 p-1 md:right-4"
class="absolute right-2 bottom-2 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>
<Button
v-tooltip.top="selectTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
severity="secondary"
:aria-label="selectTooltip"
:pressed="isCanvasReadOnly"
icon="i-material-symbols:pan-tool-outline"
:class="selectButtonClass"
@click="() => commandStore.execute('Comfy.Canvas.Unlock')"
>
<template #icon>
<i class="icon-[lucide--mouse-pointer-2]" />
</template>
</Button>
<CanvasModeSelector
:button-styles="stringifiedMinimapStyles.buttonStyles"
/>
<Button
v-tooltip.top="handTooltip"
severity="secondary"
:aria-label="handTooltip"
:pressed="isCanvasUnlocked"
:class="handButtonClass"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.Lock')"
>
<template #icon>
<i class="icon-[lucide--hand]" />
</template>
</Button>
<!-- vertical line with bg E1DED5 -->
<div class="mx-2 my-1 w-px bg-[#E1DED5] dark-theme:bg-[#2E3037]" />
<div class="h-[27px] w-[1px] self-center bg-node-divider" />
<Button
v-tooltip.top="fitViewTooltip"
@@ -52,11 +26,11 @@
icon="pi pi-expand"
:aria-label="fitViewTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
class="hover:bg-[#E7E6E6]! dark-theme:hover:bg-[#444444]!"
class="h-8 w-8 bg-interface-panel-surface p-0 hover:bg-button-hover-surface!"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
<i class="icon-[lucide--focus]" />
<i class="icon-[lucide--focus] h-4 w-4" />
</template>
</Button>
@@ -71,26 +45,26 @@
:style="stringifiedMinimapStyles.buttonStyles"
@click="toggleModal"
>
<span class="inline-flex text-xs">
<span class="inline-flex items-center gap-1 px-2 text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i class="icon-[lucide--chevron-down]" />
<i class="icon-[lucide--chevron-down] h-4 w-4" />
</span>
</Button>
<div class="mx-2 my-1 w-px bg-[#E1DED5] dark-theme:bg-[#2E3037]" />
<div class="h-[27px] w-[1px] self-center bg-node-divider" />
<Button
ref="focusButton"
v-tooltip.top="focusModeTooltip"
ref="minimapButton"
v-tooltip.top="minimapTooltip"
severity="secondary"
:aria-label="focusModeTooltip"
data-testid="focus-mode-button"
:aria-label="minimapTooltip"
data-testid="toggle-minimap-button"
:style="stringifiedMinimapStyles.buttonStyles"
:class="focusButtonClass"
@click="() => commandStore.execute('Workspace.ToggleFocusMode')"
:class="minimapButtonClass"
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
>
<template #icon>
<i class="icon-[lucide--lightbulb]" />
<i class="icon-[lucide--map] h-4 w-4" />
</template>
</Button>
@@ -111,7 +85,7 @@
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i class="icon-[lucide--route-off]" />
<i class="icon-[lucide--route-off] h-4 w-4" />
</template>
</Button>
</ButtonGroup>
@@ -131,8 +105,8 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import CanvasModeSelector from './CanvasModeSelector.vue'
import ZoomControlsModal from './modals/ZoomControlsModal.vue'
const { t } = useI18n()
@@ -141,21 +115,16 @@ const { formatKeySequence } = useCommandStore()
const canvasStore = useCanvasStore()
const settingStore = useSettingStore()
const canvasInteractions = useCanvasInteractions()
const workspaceStore = useWorkspaceStore()
const minimap = useMinimap()
const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
useZoomControls()
const stringifiedMinimapStyles = computed(() => {
const buttonGroupKeys = ['backgroundColor', 'borderRadius', '']
const buttonKeys = ['backgroundColor', 'borderRadius']
const buttonGroupKeys = ['borderRadius']
const buttonKeys = ['borderRadius']
const additionalButtonStyles = {
border: 'none',
width: '35px',
height: '35px',
'margin-right': '2px',
'margin-left': '2px'
border: 'none'
}
const containerStyles = minimap.containerStyles.value
@@ -176,72 +145,56 @@ const stringifiedMinimapStyles = computed(() => {
})
// Computed properties for reactive states
const isCanvasReadOnly = computed(() => canvasStore.canvas?.read_only ?? false)
const isCanvasUnlocked = computed(() => !isCanvasReadOnly.value)
const linkHidden = computed(
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
)
// Computed properties for command text
const unlockCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Comfy.Canvas.Unlock')
).toUpperCase()
)
const lockCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.Lock')).toUpperCase()
)
const fitViewCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Comfy.Canvas.FitView')
).toUpperCase()
)
const focusCommandText = computed(() =>
const minimapCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Workspace.ToggleFocusMode')
commandStore.getCommand('Comfy.Canvas.ToggleMinimap')
).toUpperCase()
)
// Computed properties for button classes and states
const selectButtonClass = computed(() =>
isCanvasUnlocked.value
? 'not-active:dark-theme:bg-[#262729]! not-active:bg-[#E7E6E6]!'
: ''
)
const handButtonClass = computed(() =>
isCanvasReadOnly.value
? 'not-active:dark-theme:bg-[#262729]! not-active:bg-[#E7E6E6]!'
: ''
)
const zoomButtonClass = computed(() => [
'w-16!',
isModalVisible.value
? 'not-active:dark-theme:bg-[#262729]! not-active:bg-[#E7E6E6]!'
: '',
'dark-theme:hover:bg-[#262729]! hover:bg-[#E7E6E6]!'
'bg-interface-panel-surface',
isModalVisible.value ? 'not-active:bg-button-active-surface!' : '',
'hover:bg-button-hover-surface!',
'p-0',
'h-8',
'w-15'
])
const focusButtonClass = computed(() => ({
'dark-theme:hover:bg-[#262729]! hover:bg-[#E7E6E6]!': true,
'not-active:dark-theme:bg-[#262729]! not-active:bg-[#E7E6E6]!':
workspaceStore.focusMode
const minimapButtonClass = computed(() => ({
'bg-interface-panel-surface': true,
'hover:bg-button-hover-surface!': true,
'not-active:bg-button-active-surface!': settingStore.get(
'Comfy.Minimap.Visible'
),
'p-0': true,
'w-8': true,
'h-8': true
}))
// Computed properties for tooltip and aria-label texts
const selectTooltip = computed(
() => `${t('graphCanvasMenu.select')} (${unlockCommandText.value})`
)
const handTooltip = computed(
() => `${t('graphCanvasMenu.hand')} (${lockCommandText.value})`
)
const fitViewTooltip = computed(
() => `${t('graphCanvasMenu.fitView')} (${fitViewCommandText.value})`
)
const focusModeTooltip = computed(
() => `${t('graphCanvasMenu.focusMode')} (${focusCommandText.value})`
)
const fitViewTooltip = computed(() => {
const label = t('graphCanvasMenu.fitView')
const shortcut = fitViewCommandText.value
return shortcut ? `${label} (${shortcut})` : label
})
const minimapTooltip = computed(() => {
const label = settingStore.get('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
const shortcut = minimapCommandText.value
return shortcut ? `${label} (${shortcut})` : label
})
const linkVisibilityTooltip = computed(() =>
linkHidden.value
? t('graphCanvasMenu.showLinks')
@@ -253,10 +206,12 @@ const linkVisibilityAriaLabel = computed(() =>
: t('graphCanvasMenu.hideLinks')
)
const linkVisibleClass = computed(() => [
linkHidden.value
? 'not-active:dark-theme:bg-[#262729]! not-active:bg-[#E7E6E6]!'
: '',
'dark-theme:hover:bg-[#262729]! hover:bg-[#E7E6E6]!'
'bg-interface-panel-surface',
linkHidden.value ? 'not-active:bg-button-active-surface!' : '',
'hover:bg-button-hover-surface!',
'p-0',
'w-8',
'h-8'
])
onMounted(() => {
@@ -267,19 +222,3 @@ onBeforeUnmount(() => {
canvasStore.cleanupScaleSync()
})
</script>
<style scoped>
.p-buttongroup-vertical {
display: flex;
flex-direction: row;
z-index: 1200;
border-radius: var(--p-button-border-radius);
overflow: hidden;
border: 1px solid var(--p-panel-border-color);
}
.p-buttongroup-vertical .p-button {
margin: 0;
border-radius: 0;
}
</style>

View File

@@ -7,11 +7,10 @@
<Transition name="slide-up">
<Panel
v-if="visible"
class="selection-toolbox pointer-events-auto rounded-lg"
:style="`backgroundColor: ${containerStyles.backgroundColor};`"
class="selection-toolbox pointer-events-auto rounded-lg border border-interface-stroke bg-interface-panel-surface"
:pt="{
header: 'hidden',
content: 'p-1 h-10 flex flex-row gap-1'
content: 'p-2 h-12 flex flex-row gap-1'
}"
@wheel="canvasInteractions.forwardEventToCanvas"
>
@@ -65,7 +64,6 @@ import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionTo
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useExtensionService } from '@/services/extensionService'
import { useCommandStore } from '@/stores/commandStore'
import type { ComfyCommandImpl } from '@/stores/commandStore'
@@ -78,8 +76,6 @@ const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const extensionService = useExtensionService()
const canvasInteractions = useCanvasInteractions()
const minimap = useMinimap()
const containerStyles = minimap.containerStyles
const toolboxRef = ref<HTMLElement | undefined>()
const { visible } = useSelectionToolboxPosition(toolboxRef)

View File

@@ -1,118 +1,51 @@
<template>
<div
v-if="visible"
class="absolute right-2 bottom-[66px] z-1300 flex w-[250px] justify-center border-0! bg-inherit! md:right-11"
class="absolute right-2 bottom-[66px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
>
<div
class="w-4/5 rounded-lg border border-gray-200 bg-white p-4 shadow-lg dark-theme:border-gray-700 dark-theme:bg-[#2b2b2b]"
class="w-4/5 rounded-lg border border-node-border bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
:style="filteredMinimapStyles"
@click.stop
>
<div>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:bg-transparent! focus:bg-transparent! active:bg-transparent!'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
<div class="flex flex-col gap-1">
<div
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@mousedown="startRepeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<template #default>
<span class="block text-sm font-medium">{{
$t('graphCanvasMenu.zoomIn')
}}</span>
<span class="block text-sm text-gray-500">{{
zoomInCommandText
}}</span>
</template>
</Button>
<span class="font-medium">{{ $t('graphCanvasMenu.zoomIn') }}</span>
<span class="text-[9px] text-text-primary">{{
zoomInCommandText
}}</span>
</div>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:bg-transparent! focus:bg-transparent! active:bg-transparent!'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
<div
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@mousedown="startRepeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<template #default>
<span class="block text-sm font-medium">{{
$t('graphCanvasMenu.zoomOut')
}}</span>
<span class="block text-sm text-gray-500">{{
zoomOutCommandText
}}</span>
</template>
</Button>
<span class="font-medium">{{ $t('graphCanvasMenu.zoomOut') }}</span>
<span class="text-[9px] text-text-primary">{{
zoomOutCommandText
}}</span>
</div>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:bg-transparent! focus:bg-transparent! active:bg-transparent!'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
<div
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="executeCommand('Comfy.Canvas.FitView')"
>
<template #default>
<span class="block text-sm font-medium">{{
$t('zoomControls.zoomToFit')
}}</span>
<span class="block text-sm text-gray-500">{{
zoomToFitCommandText
}}</span>
</template>
</Button>
<hr class="mb-1 border-[#E1DED5] dark-theme:border-[#2E3037]" />
<Button
severity="secondary"
text
data-testid="toggle-minimap-button"
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:bg-transparent! focus:bg-transparent! active:bg-transparent!'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@click="executeCommand('Comfy.Canvas.ToggleMinimap')"
>
<template #default>
<span class="block text-sm font-medium">{{
minimapToggleText
}}</span>
<span class="block text-sm text-gray-500">{{
showMinimapCommandText
}}</span>
</template>
</Button>
<hr class="mt-1 border-[#E1DED5] dark-theme:border-[#2E3037]" />
<span class="font-medium">{{ $t('zoomControls.zoomToFit') }}</span>
<span class="text-[9px] text-text-primary">{{
zoomToFitCommandText
}}</span>
</div>
<div
ref="zoomInputContainer"
class="zoomInputContainer flex items-center rounded bg-[#E7E6E6] p-2 px-2 focus-within:bg-[#F3F3F3] dark-theme:bg-[#8282821A]"
class="zoomInputContainer flex items-center gap-1 rounded bg-input-surface p-2"
>
<InputNumber
ref="zoomInput"
@@ -122,12 +55,12 @@
:show-buttons="false"
:use-grouping="false"
:unstyled="true"
input-class="flex-1 bg-transparent border-none outline-hidden text-sm shadow-none my-0 "
input-class="bg-transparent border-none outline-hidden text-sm shadow-none my-0 w-full"
fluid
@input="applyZoom"
@keyup.enter="applyZoom"
/>
<span class="-ml-4 text-sm text-gray-500">%</span>
<span class="flex-shrink-0 text-sm text-text-primary">%</span>
</div>
</div>
</div>
@@ -136,18 +69,14 @@
<script setup lang="ts">
import type { InputNumberInputEvent } from 'primevue'
import { Button, InputNumber } from 'primevue'
import { InputNumber } from 'primevue'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
const { t } = useI18n()
const minimap = useMinimap()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const { formatKeySequence } = useCommandStore()
@@ -158,19 +87,8 @@ interface Props {
const props = defineProps<Props>()
const emit = defineEmits<{
close: []
}>()
const interval = ref<number | null>(null)
// Computed properties for reactive states
const minimapToggleText = computed(() =>
settingStore.get('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
)
const applyZoom = (val: InputNumberInputEvent) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
@@ -181,9 +99,6 @@ const applyZoom = (val: InputNumberInputEvent) => {
const executeCommand = (command: string) => {
void commandStore.execute(command)
if (command === 'Comfy.Canvas.ToggleMinimap') {
emit('close')
}
}
const startRepeat = (command: string) => {
@@ -215,9 +130,6 @@ const zoomOutCommandText = computed(() =>
const zoomToFitCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))
)
const showMinimapCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ToggleMinimap'))
)
const zoomInput = ref<InstanceType<typeof InputNumber> | null>(null)
const zoomInputContainer = ref<HTMLDivElement | null>(null)
@@ -236,10 +148,6 @@ watch(
</script>
<style>
.zoomInputContainer:focus-within {
border: 1px solid rgb(204 204 204);
}
.dark-theme .zoomInputContainer:focus-within {
border: 1px solid rgb(204 204 204);
border: 1px solid var(--color-pure-white);
}
</style>

View File

@@ -28,7 +28,6 @@
:key="`submenu-${option.label}`"
:ref="(el) => setSubmenuRef(`submenu-${option.label}`, el)"
:option="option"
:container-styles="containerStyles"
@submenu-click="handleSubmenuClick"
/>
</div>
@@ -55,7 +54,6 @@ import type {
} from '@/composables/graph/useMoreOptionsMenu'
import { useSubmenuPositioning } from '@/composables/graph/useSubmenuPositioning'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import MenuOptionItem from './MenuOptionItem.vue'
import SubmenuPopover from './SubmenuPopover.vue'
@@ -75,8 +73,6 @@ const currentSubmenu = ref<string | null>(null)
const { menuOptions, menuOptionsWithSubmenu, bump } = useMoreOptionsMenu()
const { toggleSubmenu, hideAllSubmenus } = useSubmenuPositioning()
const canvasInteractions = useCanvasInteractions()
const minimap = useMinimap()
const containerStyles = minimap.containerStyles
let lastLogTs = 0
const LOG_INTERVAL = 120 // ms
@@ -264,11 +260,9 @@ const pt = computed(() => ({
content: {
class: [
'mt-2 text-neutral dark-theme:text-white rounded-lg',
'shadow-lg border border-zinc-200 dark-theme:border-zinc-700'
],
style: {
backgroundColor: containerStyles.value.backgroundColor
}
'shadow-lg border border-zinc-200 dark-theme:border-zinc-700',
'bg-interface-panel-surface'
]
}
}))

View File

@@ -56,13 +56,6 @@ import { useNodeCustomization } from '@/composables/graph/useNodeCustomization'
interface Props {
option: MenuOption
containerStyles: {
width: string
height: string
backgroundColor: string
border: string
borderRadius: string
}
}
interface Emits {
@@ -117,11 +110,9 @@ const submenuPt = computed(() => ({
content: {
class: [
'text-neutral dark-theme:text-white rounded-lg',
'shadow-lg border border-zinc-200 dark-theme:border-zinc-700'
],
style: {
backgroundColor: props.containerStyles.backgroundColor
}
'shadow-lg border border-zinc-200 dark-theme:border-zinc-700',
'bg-interface-panel-surface'
]
}
}))
</script>

View File

@@ -162,12 +162,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
commandId: 'Workspace.ToggleBottomPanelTab.logs-terminal'
},
{
combo: {
key: 'f'
},
commandId: 'Workspace.ToggleFocusMode'
},
{
combo: {
key: 'e',

View File

@@ -2,7 +2,7 @@
<div
v-if="visible && initialized"
ref="minimapRef"
class="minimap-main-container absolute right-2 bottom-[66px] z-1000 flex md:right-11"
class="minimap-main-container absolute right-2 bottom-[66px] z-1000 flex"
>
<MiniMapPanel
v-if="showOptionsPanel"
@@ -17,11 +17,11 @@
<div
ref="containerRef"
class="litegraph-minimap relative"
class="litegraph-minimap relative bg-interface-panel-surface"
:style="containerStyles"
>
<Button
class="absolute z-10"
class="absolute top-1 left-1 z-10 hover:bg-button-hover-surface!"
size="small"
text
severity="secondary"
@@ -32,7 +32,7 @@
</template>
</Button>
<Button
class="absolute right-0 z-10"
class="absolute top-1 right-1 z-10 hover:bg-button-hover-surface!"
size="small"
text
severity="secondary"
@@ -45,7 +45,7 @@
</Button>
<hr
class="absolute top-5 h-px border-0 bg-node-component-border"
class="absolute top-7 h-px border-0 bg-node-component-border"
:style="{
width: containerStyles.width
}"

View File

@@ -1,6 +1,6 @@
<template>
<div
class="minimap-panel mr-2 flex flex-col gap-3 p-3 text-sm"
class="minimap-panel mr-2 flex flex-col gap-2 bg-interface-panel-surface p-3 text-sm"
:style="panelStyles"
>
<div class="flex items-center gap-2">

View File

@@ -25,7 +25,7 @@ export function useMinimapSettings() {
settingStore.get('Comfy.Minimap.RenderErrorState')
)
const width = 250
const width = 253
const height = 200
// Theme-aware colors
@@ -36,16 +36,14 @@ export function useMinimapSettings() {
const containerStyles = computed(() => ({
width: `${width}px`,
height: `${height}px`,
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
border: '1px solid var(--interface-stroke)',
borderRadius: '8px'
}))
const panelStyles = computed(() => ({
width: `210px`,
height: `${height}px`,
backgroundColor: isLightTheme.value ? '#FAF9F5' : '#15161C',
border: `1px solid ${isLightTheme.value ? '#ccc' : '#333'}`,
border: '1px solid var(--interface-stroke)',
borderRadius: '8px'
}))

View File

@@ -76,7 +76,7 @@ describe('ZoomControlsModal', () => {
it('should execute zoom in command when zoom in button is clicked', async () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const buttons = wrapper.findAll('.cursor-pointer')
const zoomInButton = buttons.find((btn) =>
btn.text().includes('graphCanvasMenu.zoomIn')
)
@@ -90,7 +90,7 @@ describe('ZoomControlsModal', () => {
it('should execute zoom out command when zoom out button is clicked', async () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const buttons = wrapper.findAll('.cursor-pointer')
const zoomOutButton = buttons.find((btn) =>
btn.text().includes('graphCanvasMenu.zoomOut')
)
@@ -104,7 +104,7 @@ describe('ZoomControlsModal', () => {
it('should execute fit view command when fit view button is clicked', async () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const buttons = wrapper.findAll('.cursor-pointer')
const fitViewButton = buttons.find((btn) =>
btn.text().includes('zoomControls.zoomToFit')
)
@@ -115,34 +115,6 @@ describe('ZoomControlsModal', () => {
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.FitView')
})
it('should emit close when minimap toggle button is clicked', async () => {
const wrapper = createWrapper()
const minimapButton = wrapper.find('[data-testid="toggle-minimap-button"]')
expect(minimapButton.exists()).toBe(true)
await minimapButton.trigger('click')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.ToggleMinimap')
expect(wrapper.emitted('close')).toBeTruthy()
expect(wrapper.emitted('close')).toHaveLength(1)
})
it('should not emit close when other command buttons are clicked', async () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const fitViewButton = buttons.find((btn) =>
btn.text().includes('zoomControls.zoomToFit')
)
expect(fitViewButton).toBeDefined()
await fitViewButton!.trigger('click')
expect(mockExecute).toHaveBeenCalledWith('Comfy.Canvas.FitView')
expect(wrapper.emitted('close')).toBeFalsy()
})
it('should call setAppZoomFromPercentage with valid zoom input values', async () => {
const wrapper = createWrapper()
@@ -169,26 +141,10 @@ describe('ZoomControlsModal', () => {
expect(mockSetAppZoom).not.toHaveBeenCalled()
})
it('should display "Hide Minimap" when minimap is visible', () => {
mockSettingGet.mockReturnValue(true)
const wrapper = createWrapper()
const minimapButton = wrapper.find('[data-testid="toggle-minimap-button"]')
expect(minimapButton.text()).toContain('zoomControls.hideMinimap')
})
it('should display "Show Minimap" when minimap is hidden', () => {
mockSettingGet.mockReturnValue(false)
const wrapper = createWrapper()
const minimapButton = wrapper.find('[data-testid="toggle-minimap-button"]')
expect(minimapButton.text()).toContain('zoomControls.showMinimap')
})
it('should display keyboard shortcuts for commands', () => {
const wrapper = createWrapper()
const buttons = wrapper.findAll('button')
const buttons = wrapper.findAll('.cursor-pointer')
expect(buttons.length).toBeGreaterThan(0)
// Each command button should show the keyboard shortcut

View File

@@ -898,10 +898,9 @@ describe('useMinimap', () => {
const minimap = useMinimap()
const styles = minimap.containerStyles.value
expect(styles.width).toBe('250px')
expect(styles.width).toBe('253px')
expect(styles.height).toBe('200px')
expect(styles.backgroundColor).toBe('#15161C')
expect(styles.border).toBe('1px solid #333')
expect(styles.border).toBe('1px solid var(--interface-stroke)')
expect(styles.borderRadius).toBe('8px')
})
})

View File

@@ -55,10 +55,10 @@ describe('useMinimapSettings', () => {
const settings = useMinimapSettings()
const styles = settings.containerStyles.value
expect(styles.width).toBe('250px')
expect(styles.width).toBe('253px')
expect(styles.height).toBe('200px')
expect(styles.backgroundColor).toBe('#15161C') // dark theme color
expect(styles.border).toBe('1px solid #333')
expect(styles.border).toBe('1px solid var(--interface-stroke)')
expect(styles.borderRadius).toBe('8px')
})
it('should generate light theme container styles', () => {
@@ -74,8 +74,10 @@ describe('useMinimapSettings', () => {
const settings = useMinimapSettings()
const styles = settings.containerStyles.value
expect(styles.backgroundColor).toBe('#FAF9F5') // light theme color
expect(styles.border).toBe('1px solid #ccc')
expect(styles.width).toBe('253px')
expect(styles.height).toBe('200px')
expect(styles.border).toBe('1px solid var(--interface-stroke)')
expect(styles.borderRadius).toBe('8px')
})
it('should generate panel styles based on theme', () => {
@@ -91,8 +93,9 @@ describe('useMinimapSettings', () => {
const settings = useMinimapSettings()
const styles = settings.panelStyles.value
expect(styles.backgroundColor).toBe('#15161C')
expect(styles.border).toBe('1px solid #333')
expect(styles.width).toBe('210px')
expect(styles.height).toBe('200px')
expect(styles.border).toBe('1px solid var(--interface-stroke)')
expect(styles.borderRadius).toBe('8px')
})