Modal Standardization (#4784)

This commit is contained in:
Jin Yi
2025-08-18 09:41:15 +09:00
committed by GitHub
parent b1057f164b
commit ceac8f3741
35 changed files with 533 additions and 16 deletions

View File

@@ -0,0 +1,15 @@
<template>
<button
class="flex justify-center items-center outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white w-8 h-8 rounded-lg cursor-pointer"
role="button"
@click="onClick"
>
<slot></slot>
</button>
</template>
<script setup lang="ts">
const { onClick } = defineProps<{
onClick: () => void
}>()
</script>

View File

@@ -0,0 +1,67 @@
<template>
<BaseWidgetLayout>
<template #leftPanel>
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
<template #header-icon>
<i-lucide:puzzle class="text-neutral" />
</template>
<template #header-title>
<span class="text-neutral text-base">{{ t('g.title') }}</span>
</template>
</LeftSidePanel>
</template>
<template #header>
<!-- here -->
</template>
<template #content>
<!-- here -->
</template>
<template #rightPanel>
<RightSidePanel></RightSidePanel>
</template>
</BaseWidgetLayout>
</template>
<script setup lang="ts">
import { provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
import BaseWidgetLayout from './layout/BaseWidgetLayout.vue'
import LeftSidePanel from './panel/LeftSidePanel.vue'
import RightSidePanel from './panel/RightSidePanel.vue'
const { t } = useI18n()
const { onClose } = defineProps<{
onClose: () => void
}>()
provide(OnCloseKey, onClose)
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
{ id: 'installed', label: 'Installed' },
{
title: 'TAGS',
items: [
{ id: 'tag-sd15', label: 'SD 1.5' },
{ id: 'tag-sdxl', label: 'SDXL' },
{ id: 'tag-utility', label: 'Utility' }
]
},
{
title: 'CATEGORIES',
items: [
{ id: 'cat-models', label: 'Models' },
{ id: 'cat-nodes', label: 'Nodes' }
]
}
])
const selectedNavItem = ref<string | null>('installed')
</script>

View File

@@ -0,0 +1,176 @@
<template>
<div
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-100 dark-theme:bg-zinc-800"
>
<IconButton
v-show="!isRightPanelOpen && hasRightPanel"
class="absolute top-4 right-16 z-10 transition-opacity duration-200"
:class="{
'opacity-0 pointer-events-none': isRightPanelOpen || !hasRightPanel
}"
@click="toggleRightPanel"
>
<i-lucide:panel-right class="text-sm" />
</IconButton>
<IconButton
class="absolute top-4 right-6 z-10 transition-opacity duration-200"
@click="closeDialog"
>
<i class="pi pi-times text-sm"></i>
</IconButton>
<div class="flex w-full h-full">
<Transition name="slide-panel">
<nav
v-if="$slots.leftPanel && showLeftPanel"
:class="[
PANEL_SIZES.width,
PANEL_SIZES.minWidth,
PANEL_SIZES.maxWidth
]"
>
<slot name="leftPanel"></slot>
</nav>
</Transition>
<div class="flex-1 flex bg-zinc-100 dark-theme:bg-neutral-900">
<div class="w-full h-full flex flex-col">
<header
v-if="$slots.header"
class="w-full h-16 px-6 py-4 flex justify-between gap-2"
>
<div class="flex-1 flex gap-2 flex-shrink-0">
<IconButton v-if="!notMobile" @click="toggleLeftPanel">
<i-lucide:panel-left v-if="!showLeftPanel" class="text-sm" />
<i-lucide:panel-left-close v-else class="text-sm" />
</IconButton>
<slot name="header"></slot>
</div>
<div class="flex justify-end gap-2 min-w-20">
<slot name="header-right-area"></slot>
<IconButton
v-if="isRightPanelOpen && hasRightPanel"
@click="toggleRightPanel"
>
<i-lucide:panel-right-close class="text-sm" />
</IconButton>
</div>
</header>
<main class="flex-1">
<slot name="content"></slot>
</main>
</div>
<Transition name="slide-panel-right">
<aside
v-if="hasRightPanel && isRightPanelOpen"
class="w-1/4 min-w-40 max-w-80"
>
<slot name="rightPanel"></slot>
</aside>
</Transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useBreakpoints } from '@vueuse/core'
import { computed, inject, ref, useSlots, watch } from 'vue'
import IconButton from '@/components/custom/button/IconButton.vue'
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
const BREAKPOINTS = { sm: 480 }
const PANEL_SIZES = {
width: 'w-1/3',
minWidth: 'min-w-40',
maxWidth: 'max-w-56'
}
const slots = useSlots()
const closeDialog = inject(OnCloseKey, () => {})
const breakpoints = useBreakpoints(BREAKPOINTS)
const notMobile = breakpoints.greater('sm')
const isLeftPanelOpen = ref<boolean>(true)
const isRightPanelOpen = ref<boolean>(false)
const mobileMenuOpen = ref<boolean>(false)
const hasRightPanel = computed(() => !!slots.rightPanel)
watch(notMobile, (isDesktop) => {
if (!isDesktop) {
mobileMenuOpen.value = false
}
})
const showLeftPanel = computed(() => {
const shouldShow = notMobile.value
? isLeftPanelOpen.value
: mobileMenuOpen.value
return shouldShow
})
const toggleLeftPanel = () => {
if (notMobile.value) {
isLeftPanelOpen.value = !isLeftPanelOpen.value
} else {
mobileMenuOpen.value = !mobileMenuOpen.value
}
}
const toggleRightPanel = () => {
isRightPanelOpen.value = !isRightPanelOpen.value
}
</script>
<style scoped>
.base-widget-layout {
height: 80vh;
width: 90vw;
max-width: 1280px;
aspect-ratio: 20/13;
}
@media (min-width: 1450px) {
.base-widget-layout {
max-width: 1724px;
}
}
/* Fade transition for buttons */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* Slide transition for left panel */
.slide-panel-enter-active,
.slide-panel-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
backface-visibility: hidden;
}
.slide-panel-enter-from,
.slide-panel-leave-to {
transform: translateX(-100%);
}
/* Slide transition for right panel */
.slide-panel-right-enter-active,
.slide-panel-right-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
backface-visibility: hidden;
}
.slide-panel-right-enter-from,
.slide-panel-right-leave-to {
transform: translateX(100%);
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div
class="flex items-center gap-2 px-4 py-2 text-xs rounded-md transition-colors cursor-pointer"
:class="
active
? 'bg-neutral-100 dark-theme:bg-zinc-700 text-neutral'
: 'text-neutral hover:bg-zinc-100 hover:dark-theme:bg-zinc-700/50'
"
role="button"
@click="onClick"
>
<i-lucide:folder class="text-xs text-neutral" />
<span>
<slot></slot>
</span>
</div>
</template>
<script setup lang="ts">
const { active, onClick } = defineProps<{
active?: boolean
onClick: () => void
}>()
</script>

View File

@@ -0,0 +1,13 @@
<template>
<h3
class="m-0 px-3 py-0 pt-5 text-sm font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
>
{{ title }}
</h3>
</template>
<script setup lang="ts">
const { title } = defineProps<{
title: string
}>()
</script>

View File

@@ -0,0 +1,75 @@
<template>
<div class="flex flex-col h-full w-full bg-white dark-theme:bg-zinc-800">
<PanelHeader>
<template #icon>
<slot name="header-icon"></slot>
</template>
<slot name="header-title"></slot>
</PanelHeader>
<nav class="flex-1 px-3 py-4 flex flex-col gap-2">
<template v-for="(item, index) in navItems" :key="index">
<div v-if="'items' in item" class="flex flex-col gap-2">
<NavTitle :title="item.title" />
<NavItem
v-for="subItem in item.items"
:key="subItem.id"
:active="activeItem === subItem.id"
@click="activeItem = subItem.id"
>
{{ subItem.label }}
</NavItem>
</div>
<div v-else class="flex flex-col gap-2">
<NavItem
:active="activeItem === item.id"
@click="activeItem = item.id"
>
{{ item.label }}
</NavItem>
</div>
</template>
</nav>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
import NavItem from '../nav/NavItem.vue'
import NavTitle from '../nav/NavTitle.vue'
import PanelHeader from './PanelHeader.vue'
const { navItems = [], modelValue } = defineProps<{
navItems?: (NavItemData | NavGroupData)[]
modelValue?: string | null
}>()
const emit = defineEmits<{
'update:modelValue': [value: string | null]
}>()
const getFirstItemId = () => {
if (!navItems || navItems.length === 0) {
return null
}
const firstEntry = navItems[0]
if ('items' in firstEntry && firstEntry.items.length > 0) {
return firstEntry.items[0].id
}
if ('id' in firstEntry) {
return firstEntry.id
}
return null
}
const activeItem = computed({
get: () => modelValue ?? getFirstItemId(),
set: (value: string | null) => emit('update:modelValue', value)
})
</script>

View File

@@ -0,0 +1,12 @@
<template>
<header class="flex items-center justify-between h-16 px-6">
<div class="flex items-center gap-2 pl-1">
<slot name="icon">
<i-lucide:puzzle class="text-neutral text-base" />
</slot>
<h2 class="font-bold text-base text-neutral">
<slot></slot>
</h2>
</div>
</header>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<div class="w-full h-full pl-4 pr-6 pb-8 bg-white dark-theme:bg-zinc-800">
<slot></slot>
</div>
</template>

View File

@@ -10,14 +10,16 @@
:aria-labelledby="item.key"
>
<template #header>
<component
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
<div v-if="!item.dialogComponentProps?.headless">
<component
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
</div>
</template>
<component

View File

@@ -1,5 +1,6 @@
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
import {
DEFAULT_DARK_COLOR_PALETTE,
DEFAULT_LIGHT_COLOR_PALETTE
@@ -878,6 +879,17 @@ export function useCoreCommands(): ComfyCommand[] {
navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph
)
}
},
{
id: 'Comfy.Dev.ShowModelSelector',
icon: 'pi pi-box',
label: 'Show Model Selector (Dev)',
versionAdded: '1.26.2',
category: 'view-controls' as const,
function: () => {
const modelSelectorDialog = useModelSelectorDialog()
modelSelectorDialog.show()
}
}
]

View File

@@ -0,0 +1,29 @@
import ModelSelector from '@/components/custom/widget/ModelSelector.vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
const DIALOG_KEY = 'global-model-selector'
export const useModelSelectorDialog = () => {
const dialogService = useDialogService()
const dialogStore = useDialogStore()
function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
}
function show() {
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: ModelSelector,
props: {
onClose: hide
}
})
}
return {
show,
hide
}
}

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "الاتصال بالدعم"
},
"Comfy_Dev_ShowModelSelector": {
"label": "إظهار منتقي النماذج (للمطورين)"
},
"Comfy_DuplicateWorkflow": {
"label": "تكرار سير العمل الحالي"
},

View File

@@ -840,6 +840,7 @@
"Save": "حفظ",
"Save As": "حفظ باسم",
"Show Keybindings Dialog": "عرض مربع حوار اختصارات لوحة المفاتيح",
"Show Model Selector (Dev)": "إظهار منتقي النماذج (للمطورين)",
"Show Settings Dialog": "عرض نافذة الإعدادات",
"Sign Out": "تسجيل خروج",
"Toggle Essential Bottom Panel": "تبديل اللوحة السفلية الأساسية",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "Contact Support"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Show Model Selector (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Duplicate Current Workflow"
},

View File

@@ -980,6 +980,7 @@
"Clear Pending Tasks": "Clear Pending Tasks",
"Clear Workflow": "Clear Workflow",
"Contact Support": "Contact Support",
"Show Model Selector (Dev)": "Show Model Selector (Dev)",
"Duplicate Current Workflow": "Duplicate Current Workflow",
"Export": "Export",
"Export (API)": "Export (API)",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "Contactar soporte"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Mostrar selector de modelo (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Duplicar flujo de trabajo actual"
},

View File

@@ -840,6 +840,7 @@
"Save": "Guardar",
"Save As": "Guardar como",
"Show Keybindings Dialog": "Mostrar diálogo de combinaciones de teclas",
"Show Model Selector (Dev)": "Mostrar selector de modelo (Desarrollo)",
"Show Settings Dialog": "Mostrar diálogo de configuración",
"Sign Out": "Cerrar sesión",
"Toggle Essential Bottom Panel": "Alternar panel inferior esencial",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "Contacter le support"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Afficher le sélecteur de modèle (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Dupliquer le flux de travail actuel"
},

View File

@@ -840,6 +840,7 @@
"Save": "Enregistrer",
"Save As": "Enregistrer sous",
"Show Keybindings Dialog": "Afficher la boîte de dialogue des raccourcis clavier",
"Show Model Selector (Dev)": "Afficher le sélecteur de modèle (Dev)",
"Show Settings Dialog": "Afficher la boîte de dialogue des paramètres",
"Sign Out": "Se déconnecter",
"Toggle Essential Bottom Panel": "Afficher/Masquer le panneau inférieur essentiel",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "サポートに連絡"
},
"Comfy_Dev_ShowModelSelector": {
"label": "モデルセレクターを表示(開発用)"
},
"Comfy_DuplicateWorkflow": {
"label": "現在のワークフローを複製"
},

View File

@@ -840,6 +840,7 @@
"Save": "保存",
"Save As": "名前を付けて保存",
"Show Keybindings Dialog": "キーバインドダイアログを表示",
"Show Model Selector (Dev)": "モデルセレクターを表示(開発用)",
"Show Settings Dialog": "設定ダイアログを表示",
"Sign Out": "サインアウト",
"Toggle Essential Bottom Panel": "エッセンシャル下部パネルの切り替え",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "지원팀에 문의하기"
},
"Comfy_Dev_ShowModelSelector": {
"label": "모델 선택기 표시 (개발자용)"
},
"Comfy_DuplicateWorkflow": {
"label": "현재 워크플로 복제"
},

View File

@@ -839,7 +839,8 @@
"Restart": "재시작",
"Save": "저장",
"Save As": "다른 이름으로 저장",
"Show Keybindings Dialog": "키 바인딩 대화상자 표시",
"Show Keybindings Dialog": "단축키 대화상자 표시",
"Show Model Selector (Dev)": "모델 선택기 표시 (개발자용)",
"Show Settings Dialog": "설정 대화상자 표시",
"Sign Out": "로그아웃",
"Toggle Essential Bottom Panel": "필수 하단 패널 전환",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "Связаться с поддержкой"
},
"Comfy_Dev_ShowModelSelector": {
"label": "Показать выбор модели (Dev)"
},
"Comfy_DuplicateWorkflow": {
"label": "Дублировать текущий рабочий процесс"
},

View File

@@ -840,6 +840,7 @@
"Save": "Сохранить",
"Save As": "Сохранить как",
"Show Keybindings Dialog": "Показать диалог клавиш быстрого доступа",
"Show Model Selector (Dev)": "Показать выбор модели (Dev)",
"Show Settings Dialog": "Показать диалог настроек",
"Sign Out": "Выйти",
"Toggle Essential Bottom Panel": "Показать/скрыть основную нижнюю панель",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "聯絡支援"
},
"Comfy_Dev_ShowModelSelector": {
"label": "顯示模型選擇器(開發)"
},
"Comfy_DuplicateWorkflow": {
"label": "複製目前工作流程"
},

View File

@@ -840,6 +840,7 @@
"Save": "儲存",
"Save As": "另存新檔",
"Show Keybindings Dialog": "顯示快捷鍵對話框",
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
"Show Settings Dialog": "顯示設定對話框",
"Sign Out": "登出",
"Toggle Essential Bottom Panel": "切換基本下方面板",

View File

@@ -107,6 +107,9 @@
"Comfy_ContactSupport": {
"label": "联系支持"
},
"Comfy_Dev_ShowModelSelector": {
"label": "顯示模型選擇器(開發)"
},
"Comfy_DuplicateWorkflow": {
"label": "复制当前工作流"
},

View File

@@ -839,7 +839,8 @@
"Restart": "重启",
"Save": "保存",
"Save As": "另存为",
"Show Keybindings Dialog": "示快捷键对话框",
"Show Keybindings Dialog": "示快捷鍵對話框",
"Show Model Selector (Dev)": "顯示模型選擇器(開發用)",
"Show Settings Dialog": "显示设置对话框",
"Sign Out": "退出登录",
"Toggle Essential Bottom Panel": "切换基础底部面板",

View File

@@ -1,3 +1,6 @@
import { merge } from 'es-toolkit/compat'
import { Component } from 'vue'
import ApiNodesSignInContent from '@/components/dialog/content/ApiNodesSignInContent.vue'
import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationDialogContent.vue'
import ErrorDialogContent from '@/components/dialog/content/ErrorDialogContent.vue'
@@ -20,7 +23,11 @@ import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsCo
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
import { t } from '@/i18n'
import type { ExecutionErrorWsMessage } from '@/schemas/apiSchema'
import { type ShowDialogOptions, useDialogStore } from '@/stores/dialogStore'
import {
type DialogComponentProps,
type ShowDialogOptions,
useDialogStore
} from '@/stores/dialogStore'
export type ConfirmationDialogType =
| 'default'
@@ -424,6 +431,33 @@ export const useDialogService = () => {
}
}
function showLayoutDialog(options: {
key: string
component: Component
props: { onClose: () => void }
dialogComponentProps?: DialogComponentProps
}) {
const layoutDefaultProps: DialogComponentProps = {
headless: true,
unstyled: true,
modal: true,
closable: false,
pt: {
mask: {
class: 'bg-black bg-opacity-40'
}
}
}
return dialogStore.showDialog({
...options,
dialogComponentProps: merge(
layoutDefaultProps,
options.dialogComponentProps || {}
)
})
}
return {
showLoadWorkflowWarning,
showMissingModelsWarning,
@@ -443,6 +477,7 @@ export const useDialogService = () => {
prompt,
confirm,
toggleManagerDialog,
toggleManagerProgressDialog
toggleManagerProgressDialog,
showLayoutDialog
}
}

View File

@@ -28,9 +28,11 @@ interface CustomDialogComponentProps {
pt?: DialogPassThroughOptions
closeOnEscape?: boolean
dismissableMask?: boolean
unstyled?: boolean
headless?: boolean
}
type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
CustomDialogComponentProps
interface DialogInstance {

View File

@@ -0,0 +1,2 @@
export * from './navTypes'
export * from './widgetTypes'

View File

@@ -0,0 +1,9 @@
export interface NavItemData {
id: string
label: string
}
export interface NavGroupData {
title: string
items: NavItemData[]
}

View File

@@ -0,0 +1,3 @@
import { InjectionKey } from 'vue'
export const OnCloseKey: InjectionKey<() => void> = Symbol()

View File

@@ -79,14 +79,14 @@ export default {
colors: {
zinc: {
50: '#fafafa',
100: '#f4f4f5',
100: '#8282821a',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#3f3f46',
800: '#27272a',
700: '#38393b',
800: '#262729',
900: '#18181b',
950: '#09090b'
},