Files
ComfyUI_frontend/src/components/graph/modals/ZoomControlsModal.vue
Christian Byrne 10af2300fa 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>
2025-10-14 14:26:07 -07:00

154 lines
4.6 KiB
Vue

<template>
<div
v-if="visible"
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-node-border bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
:style="filteredMinimapStyles"
@click.stop
>
<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"
>
<span class="font-medium">{{ $t('graphCanvasMenu.zoomIn') }}</span>
<span class="text-[9px] text-text-primary">{{
zoomInCommandText
}}</span>
</div>
<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"
>
<span class="font-medium">{{ $t('graphCanvasMenu.zoomOut') }}</span>
<span class="text-[9px] text-text-primary">{{
zoomOutCommandText
}}</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="executeCommand('Comfy.Canvas.FitView')"
>
<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 gap-1 rounded bg-input-surface p-2"
>
<InputNumber
ref="zoomInput"
:default-value="canvasStore.appScalePercentage"
:min="1"
:max="1000"
:show-buttons="false"
:use-grouping="false"
:unstyled="true"
input-class="bg-transparent border-none outline-hidden text-sm shadow-none my-0 w-full"
fluid
@input="applyZoom"
@keyup.enter="applyZoom"
/>
<span class="flex-shrink-0 text-sm text-text-primary">%</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { InputNumberInputEvent } from 'primevue'
import { InputNumber } from 'primevue'
import { computed, nextTick, ref, watch } from 'vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
const minimap = useMinimap()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const { formatKeySequence } = useCommandStore()
interface Props {
visible: boolean
}
const props = defineProps<Props>()
const interval = ref<number | null>(null)
const applyZoom = (val: InputNumberInputEvent) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
return
}
canvasStore.setAppZoomFromPercentage(inputValue)
}
const executeCommand = (command: string) => {
void commandStore.execute(command)
}
const startRepeat = (command: string) => {
if (interval.value) return
const cmd = () => commandStore.execute(command)
void cmd()
interval.value = window.setInterval(cmd, 100)
}
const stopRepeat = () => {
if (interval.value) {
clearInterval(interval.value)
interval.value = null
}
}
const filteredMinimapStyles = computed(() => {
return {
...minimap.containerStyles.value,
height: undefined,
width: undefined
}
})
const zoomInCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomIn'))
)
const zoomOutCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomOut'))
)
const zoomToFitCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))
)
const zoomInput = ref<InstanceType<typeof InputNumber> | null>(null)
const zoomInputContainer = ref<HTMLDivElement | null>(null)
watch(
() => props.visible,
async (newVal) => {
if (newVal) {
await nextTick()
const input = zoomInputContainer.value?.querySelector(
'input'
) as HTMLInputElement
input?.focus()
}
}
)
</script>
<style>
.zoomInputContainer:focus-within {
border: 1px solid var(--color-pure-white);
}
</style>