Component: Button Migration 2: IconButton (#7598)

## Summary

Still a work in progress. Buttons with just icons are already in the
stories for button.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7598-WIP-Component-Button-Migration-2-IconButton-2cc6d73d365081c09143c63464ac60b7)
by [Unito](https://www.unito.io)
This commit is contained in:
Alexander Brown
2025-12-17 18:11:43 -08:00
committed by GitHub
parent 1d014c0dbe
commit fba580dc7d
35 changed files with 334 additions and 632 deletions

View File

@@ -1,4 +1,5 @@
import clsx, { type ClassArray } from 'clsx'
import { clsx } from 'clsx'
import type { ClassArray } from 'clsx'
import { twMerge } from 'tailwind-merge'
export type { ClassValue } from 'clsx'

135
pnpm-lock.yaml generated
View File

@@ -265,14 +265,14 @@ catalogs:
specifier: ^8.49.0
version: 8.49.0
unplugin-icons:
specifier: ^0.22.0
version: 0.22.0
specifier: ^22.5.0
version: 22.5.0
unplugin-typegpu:
specifier: 0.8.0
version: 0.8.0
unplugin-vue-components:
specifier: ^0.28.0
version: 0.28.0
specifier: ^30.0.0
version: 30.0.0
vite:
specifier: ^7.3.0
version: 7.3.0
@@ -692,13 +692,13 @@ importers:
version: 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
unplugin-icons:
specifier: 'catalog:'
version: 0.22.0(@vue/compiler-sfc@3.5.25)
version: 22.5.0(@vue/compiler-sfc@3.5.25)
unplugin-typegpu:
specifier: 'catalog:'
version: 0.8.0(typegpu@0.8.2)
unplugin-vue-components:
specifier: 'catalog:'
version: 0.28.0(@babel/parser@7.28.5)(rollup@4.53.5)(vue@3.5.13(typescript@5.9.3))
version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.13(typescript@5.9.3))
uuid:
specifier: ^11.1.0
version: 11.1.0
@@ -780,10 +780,10 @@ importers:
version: 16.6.1
unplugin-icons:
specifier: 'catalog:'
version: 0.22.0(@vue/compiler-sfc@3.5.25)
version: 22.5.0(@vue/compiler-sfc@3.5.25)
unplugin-vue-components:
specifier: 'catalog:'
version: 0.28.0(@babel/parser@7.28.5)(rollup@4.53.5)(vue@3.5.13(typescript@5.9.3))
version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.13(typescript@5.9.3))
vite:
specifier: 'catalog:'
version: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
@@ -907,18 +907,9 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
'@antfu/install-pkg@0.5.0':
resolution: {integrity: sha512-dKnk2xlAyC7rvTkpkHmu+Qy/2Zc3Vm/l8PtNyIOGDBtXPY3kThfU4ORNEp3V7SXw5XSOb+tOJaUYpfquPzL/Tg==}
'@antfu/install-pkg@1.1.0':
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
'@antfu/utils@0.7.10':
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
'@antfu/utils@8.1.1':
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
'@asamuzakjp/css-color@3.2.0':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
@@ -2147,8 +2138,8 @@ packages:
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
'@iconify/utils@2.3.0':
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
'@iconify/utils@3.1.0':
resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
'@internationalized/date@3.9.0':
resolution: {integrity: sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==}
@@ -4430,6 +4421,10 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
@@ -5170,8 +5165,8 @@ packages:
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
engines: {node: '>=12.0.0'}
exsolve@1.0.7:
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
exsolve@1.0.8:
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
extend-shallow@2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
@@ -6129,10 +6124,6 @@ packages:
resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
engines: {node: '>=20.0.0'}
local-pkg@0.5.1:
resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
engines: {node: '>=14'}
local-pkg@1.1.2:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'}
@@ -6696,11 +6687,8 @@ packages:
resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==}
engines: {node: '>=18'}
package-manager-detector@0.2.11:
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
@@ -7061,6 +7049,10 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
recast@0.23.11:
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
engines: {node: '>= 4'}
@@ -7567,8 +7559,9 @@ packages:
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
tinyexec@1.0.1:
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
tinyexec@1.0.2:
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
engines: {node: '>=18'}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
@@ -7772,8 +7765,8 @@ packages:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
unplugin-icons@0.22.0:
resolution: {integrity: sha512-CP+iZq5U7doOifer5bcM0jQ9t3Is7EGybIYt3myVxceI8Zuk8EZEpe1NPtJvh7iqMs1VdbK0L41t9+um9VuuLw==}
unplugin-icons@22.5.0:
resolution: {integrity: sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ==}
peerDependencies:
'@svgr/core': '>=7.0.0'
'@svgx/core': ^1.0.1
@@ -7804,12 +7797,12 @@ packages:
resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
engines: {node: '>=20.19.0'}
unplugin-vue-components@0.28.0:
resolution: {integrity: sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==}
unplugin-vue-components@30.0.0:
resolution: {integrity: sha512-4qVE/lwCgmdPTp6h0qsRN2u642tt4boBQtcpn4wQcWZAsr8TQwq+SPT3NDu/6kBFxzo/sSEK4ioXhOOBrXc3iw==}
engines: {node: '>=14'}
peerDependencies:
'@babel/parser': ^7.15.8
'@nuxt/kit': ^3.2.2
'@nuxt/kit': ^3.2.2 || ^4.0.0
vue: 2 || 3
peerDependenciesMeta:
'@babel/parser':
@@ -8411,19 +8404,10 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@antfu/install-pkg@0.5.0':
dependencies:
package-manager-detector: 0.2.11
tinyexec: 0.3.2
'@antfu/install-pkg@1.1.0':
dependencies:
package-manager-detector: 1.3.0
tinyexec: 1.0.1
'@antfu/utils@0.7.10': {}
'@antfu/utils@8.1.1': {}
package-manager-detector: 1.6.0
tinyexec: 1.0.2
'@asamuzakjp/css-color@3.2.0':
dependencies:
@@ -9797,18 +9781,11 @@ snapshots:
'@iconify/types@2.0.0': {}
'@iconify/utils@2.3.0':
'@iconify/utils@3.1.0':
dependencies:
'@antfu/install-pkg': 1.1.0
'@antfu/utils': 8.1.1
'@iconify/types': 2.0.0
debug: 4.4.3
globals: 15.15.0
kolorist: 1.8.0
local-pkg: 1.1.2
mlly: 1.8.0
transitivePeerDependencies:
- supports-color
'@internationalized/date@3.9.0':
dependencies:
@@ -12442,6 +12419,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
chownr@3.0.0: {}
clean-css@5.3.3:
@@ -13274,7 +13255,7 @@ snapshots:
expect-type@1.2.2: {}
exsolve@1.0.7: {}
exsolve@1.0.8: {}
extend-shallow@2.0.1:
dependencies:
@@ -14313,11 +14294,6 @@ snapshots:
rfdc: 1.4.1
wrap-ansi: 9.0.2
local-pkg@0.5.1:
dependencies:
mlly: 1.8.0
pkg-types: 1.3.1
local-pkg@1.1.2:
dependencies:
mlly: 1.8.0
@@ -15188,11 +15164,7 @@ snapshots:
registry-url: 6.0.1
semver: 7.7.3
package-manager-detector@0.2.11:
dependencies:
quansync: 0.2.11
package-manager-detector@1.3.0: {}
package-manager-detector@1.6.0: {}
pako@1.0.11: {}
@@ -15282,7 +15254,7 @@ snapshots:
pkg-types@2.3.0:
dependencies:
confbox: 0.2.2
exsolve: 1.0.7
exsolve: 1.0.8
pathe: 2.0.3
playwright-core@1.52.0: {}
@@ -15617,6 +15589,8 @@ snapshots:
dependencies:
picomatch: 2.3.1
readdirp@4.1.2: {}
recast@0.23.11:
dependencies:
ast-types: 0.16.1
@@ -16283,7 +16257,7 @@ snapshots:
tinyexec@0.3.2: {}
tinyexec@1.0.1: {}
tinyexec@1.0.2: {}
tinyglobby@0.2.15:
dependencies:
@@ -16498,14 +16472,12 @@ snapshots:
universalify@2.0.1: {}
unplugin-icons@0.22.0(@vue/compiler-sfc@3.5.25):
unplugin-icons@22.5.0(@vue/compiler-sfc@3.5.25):
dependencies:
'@antfu/install-pkg': 0.5.0
'@antfu/utils': 0.7.10
'@iconify/utils': 2.3.0
'@antfu/install-pkg': 1.1.0
'@iconify/utils': 3.1.0
debug: 4.4.3
kolorist: 1.8.0
local-pkg: 0.5.1
local-pkg: 1.1.2
unplugin: 2.3.11
optionalDependencies:
'@vue/compiler-sfc': 3.5.25
@@ -16530,23 +16502,20 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.3
unplugin-vue-components@0.28.0(@babel/parser@7.28.5)(rollup@4.53.5)(vue@3.5.13(typescript@5.9.3)):
unplugin-vue-components@30.0.0(@babel/parser@7.28.5)(vue@3.5.13(typescript@5.9.3)):
dependencies:
'@antfu/utils': 0.7.10
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
chokidar: 3.6.0
chokidar: 4.0.3
debug: 4.4.3
fast-glob: 3.3.3
local-pkg: 0.5.1
local-pkg: 1.1.2
magic-string: 0.30.21
minimatch: 9.0.5
mlly: 1.8.0
tinyglobby: 0.2.15
unplugin: 2.3.11
unplugin-utils: 0.3.1
vue: 3.5.13(typescript@5.9.3)
optionalDependencies:
'@babel/parser': 7.28.5
transitivePeerDependencies:
- rollup
- supports-color
unplugin@1.0.1:

View File

@@ -1,7 +1,3 @@
packages:
- apps/**
- packages/**
catalog:
'@alloc/quick-lru': ^5.2.0
'@comfyorg/comfyui-electron-types': 0.5.5
@@ -89,9 +85,9 @@ catalog:
typegpu: ^0.8.2
typescript: ^5.9.3
typescript-eslint: ^8.49.0
unplugin-icons: ^0.22.0
unplugin-icons: ^22.5.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^0.28.0
unplugin-vue-components: ^30.0.0
vite: ^7.3.0
vite-plugin-dts: ^4.5.4
vite-plugin-html: ^3.2.2
@@ -108,15 +104,12 @@ catalog:
zod: ^3.23.8
zod-to-json-schema: ^3.24.1
zod-validation-error: ^3.3.0
cleanupUnusedCatalogs: true
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs
- unrs-resolver
- vue-demi
onlyBuiltDependencies:
- '@playwright/browser-chromium'
- '@playwright/browser-firefox'
@@ -126,6 +119,8 @@ onlyBuiltDependencies:
- esbuild
- nx
- oxc-resolver
overrides:
'@types/eslint': '-'
packages:
- apps/**
- packages/**

View File

@@ -15,20 +15,19 @@
v-if="managerState.shouldShowManagerButtons.value && isDesktop"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<IconButton
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
type="transparent"
size="sm"
class="text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
variant="secondary"
size="icon"
:aria-label="t('menu.customNodesManager')"
@click="openCustomNodeManager"
>
<i class="icon-[lucide--puzzle] size-4" />
</IconButton>
</Button>
</div>
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
class="actionbar-container pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
@@ -37,11 +36,10 @@
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<IconButton
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
type="transparent"
size="sm"
class="relative mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
type="destructive"
size="icon"
:aria-pressed="isQueueOverlayExpanded"
:aria-label="
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
@@ -55,20 +53,19 @@
>
{{ queuedCount }}
</span>
</IconButton>
</Button>
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" />
<IconButton
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="transparent"
size="sm"
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</IconButton>
</Button>
</div>
</div>
<QueueProgressOverlay
@@ -86,11 +83,11 @@ import { useI18n } from 'vue-i18n'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import IconButton from '@/components/button/IconButton.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'

View File

@@ -18,12 +18,12 @@
content: { class: isDocked ? 'p-0' : 'p-1' }
}"
>
<div ref="panelRef" class="flex items-center select-none">
<div ref="panelRef" class="flex items-center select-none gap-2">
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle cursor-grab w-3 h-max mr-2',
'drag-handle cursor-grab w-3 h-max',
isDragging && 'cursor-grabbing'
)
"
@@ -31,17 +31,16 @@
<Suspense @resolve="comfyRunButtonResolved">
<ComfyRunButton />
</Suspense>
<IconButton
<Button
v-tooltip.bottom="cancelJobTooltipConfig"
type="transparent"
size="sm"
class="ml-2 bg-destructive-background text-base-foreground transition-colors duration-200 ease-in-out hover:bg-destructive-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive-background"
variant="destructive"
size="icon"
:disabled="isExecutionIdle"
:aria-label="t('menu.interrupt')"
@click="cancelCurrentJob"
>
<i class="icon-[lucide--x] size-4" />
</IconButton>
</Button>
</div>
</Panel>
</div>
@@ -58,10 +57,10 @@ import { clamp } from 'es-toolkit/compat'
import { storeToRefs } from 'pinia'
import Panel from 'primevue/panel'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
@@ -72,6 +71,7 @@ import ComfyRunButton from './ComfyRunButton'
const settingsStore = useSettingStore()
const commandStore = useCommandStore()
const { t } = useI18n()
const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
@@ -301,7 +301,7 @@ const panelClass = computed(() =>
'actionbar pointer-events-auto z-1300',
isDragging.value && 'select-none pointer-events-none',
isDocked.value
? 'p-0 static mr-2 border-none bg-transparent'
? 'p-0 static border-none bg-transparent'
: 'fixed shadow-interface'
)
)

View File

@@ -1,152 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import IconButton from './IconButton.vue'
const meta: Meta<typeof IconButton> = {
title: 'Components/Button/IconButton',
component: IconButton,
tags: ['autodocs'],
argTypes: {
size: {
control: { type: 'select' },
options: ['sm', 'md']
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--trophy] size-4" />
</IconButton>
`
}),
args: {
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--settings] size-4" />
</IconButton>
`
}),
args: {
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--x] size-4" />
</IconButton>
`
}),
args: {
type: 'transparent',
size: 'md'
}
}
export const Small: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--bell] size-3" />
</IconButton>
`
}),
args: {
type: 'secondary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: { IconButton },
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<IconButton type="primary" size="sm" @click="() => {}">
<i class="icon-[lucide--trophy] size-3" />
</IconButton>
<IconButton type="primary" size="md" @click="() => {}">
<i class="icon-[lucide--trophy] size-4" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="secondary" size="sm" @click="() => {}">
<i class="icon-[lucide--settings] size-3" />
</IconButton>
<IconButton type="secondary" size="md" @click="() => {}">
<i class="icon-[lucide--settings] size-4" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="transparent" size="sm" @click="() => {}">
<i class="icon-[lucide--x] size-3" />
</IconButton>
<IconButton type="transparent" size="md" @click="() => {}">
<i class="icon-[lucide--x] size-4" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="primary" size="md" @click="() => {}">
<i class="icon-[lucide--bell] size-4" />
</IconButton>
<IconButton type="secondary" size="md" @click="() => {}">
<i class="icon-[lucide--heart] size-4" />
</IconButton>
<IconButton type="transparent" size="md" @click="() => {}">
<i class="icon-[lucide--download] size-4" />
</IconButton>
</div>
</div>
`
}),
parameters: {
controls: { disable: true },
actions: { disable: true }
}
}

View File

@@ -1,52 +0,0 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot></slot>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonTypeClasses,
getIconButtonSizeClasses
} from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
interface IconButtonProps extends BaseButtonProps {
onClick?: (event: MouseEvent) => void
}
defineOptions({
inheritAttrs: false
})
const {
size = 'md',
type = 'secondary',
border = false,
disabled = false,
class: className,
onClick
} = defineProps<IconButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} p-0`
const sizeClasses = getIconButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return cn(baseClasses, sizeClasses, typeClasses, className)
})
</script>

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import IconButton from './IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import IconGroup from './IconGroup.vue'
const meta: Meta<typeof IconGroup> = {
@@ -16,18 +16,18 @@ type Story = StoryObj<typeof IconGroup>
export const Basic: Story = {
render: () => ({
components: { IconGroup, IconButton },
components: { IconGroup, Button },
template: `
<IconGroup>
<IconButton @click="console.log('Hello World!!')">
<Button size="icon" @click="console.log('Hello World!!')">
<i class="icon-[lucide--heart] size-4" />
</IconButton>
<IconButton @click="console.log('Hello World!!')">
</Button>
<Button size="icon" @click="console.log('Hello World!!')">
<i class="icon-[lucide--download] size-4" />
</IconButton>
<IconButton @click="console.log('Hello World!!')">
</Button>
<Button size="icon" @click="console.log('Hello World!!')">
<i class="icon-[lucide--external-link] size-4" />
</IconButton>
</Button>
</IconGroup>
`
})

View File

@@ -1,9 +1,17 @@
<template>
<div class="relative inline-flex items-center">
<IconButton :size="size" :type="type" @click="popover?.toggle">
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
<i v-else class="icon-[lucide--more-vertical] text-sm" />
</IconButton>
<Button size="icon" variant="secondary" @click="popover?.toggle">
<i
:class="
cn(
!isVertical
? 'icon-[lucide--ellipsis]'
: 'icon-[lucide--more-vertical]',
'text-sm'
)
"
/>
</Button>
<Popover
ref="popover"
@@ -49,20 +57,14 @@
import Popover from 'primevue/popover'
import { ref } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
import IconButton from './IconButton.vue'
interface MoreButtonProps extends BaseButtonProps {
interface MoreButtonProps {
isVertical?: boolean
}
const {
size = 'md',
type = 'secondary',
isVertical = false
} = defineProps<MoreButtonProps>()
const { isVertical = false } = defineProps<MoreButtonProps>()
defineEmits<{
menuOpened: []

View File

@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import IconButton from '../button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import SquareChip from '../chip/SquareChip.vue'
import CardBottom from './CardBottom.vue'
import CardContainer from './CardContainer.vue'
@@ -173,7 +173,7 @@ const createCardTemplate = (args: CardStoryArgs) => ({
CardBottom,
CardTitle,
CardDescription,
IconButton,
Button,
SquareChip
},
setup() {
@@ -222,19 +222,19 @@ const createCardTemplate = (args: CardStoryArgs) => ({
</template>
<template v-if="args.showTopRight" #top-right>
<IconButton
<Button
class="!bg-white/90 !text-neutral-900"
@click="() => console.log('Info clicked')"
>
<i class="icon-[lucide--info] size-4" />
</IconButton>
<IconButton
</Button>
<Button
class="!bg-white/90"
:class="favorited ? '!text-red-500' : '!text-neutral-900'"
@click="toggleFavorite"
>
<i class="icon-[lucide--heart] size-4" :class="favorited ? 'fill-current' : ''" />
</IconButton>
</Button>
</template>
<template v-if="args.showBottomLeft" #bottom-left>

View File

@@ -17,22 +17,25 @@
class="absolute inset-0 size-full pl-11 border-none outline-none bg-transparent text-sm"
:aria-label="placeholder"
/>
<IconButton
<Button
v-if="filterIcon"
class="p-inputicon filter-button absolute right-0 inset-y-0 h-full m-0 p-0"
:icon="filterIcon"
severity="contrast"
size="icon"
variant="textonly"
class="filter-button absolute right-0 inset-y-0 m-0 p-0"
@click="$emit('showFilter', $event)"
/>
>
<i :class="filterIcon" />
</Button>
<InputIcon v-if="!modelValue" :class="icon" />
<Button
v-if="modelValue"
class="p-inputicon clear-button"
icon="pi pi-times"
text
severity="contrast"
class="clear-button absolute left-0"
variant="textonly"
size="icon"
@click="modelValue = ''"
/>
>
<i class="icon-[lucide--x] size-4" />
</Button>
</div>
<div v-if="filters?.length" class="search-filters flex flex-wrap gap-2 pt-2">
<SearchFilterChip
@@ -49,12 +52,12 @@
<script setup lang="ts" generic="TFilter extends SearchFilter">
import { cn } from '@comfyorg/tailwind-utils'
import { watchDebounced } from '@vueuse/core'
import Button from 'primevue/button'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import { computed, ref } from 'vue'
import IconButton from '../button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import type { SearchFilter } from './SearchFilterChip.vue'
import SearchFilterChip from './SearchFilterChip.vue'
@@ -125,8 +128,4 @@ const wrapperStyle = computed(() => {
:deep(.p-inputtext) {
--p-form-field-padding-x: 0.625rem;
}
.p-button.p-inputicon {
@apply p-0 w-auto border-none;
}
</style>

View File

@@ -301,16 +301,16 @@
v-if="template.tutorialUrl"
class="flex flex-col-reverse justify-center"
>
<IconButton
<Button
v-if="hoveredTemplate === template.name"
v-tooltip.bottom="$t('g.seeTutorial')"
v-bind="$attrs"
type="primary"
size="sm"
variant="inverted"
size="icon"
@click.stop="openTutorial(template)"
>
<i class="icon-[lucide--info] size-4" />
</IconButton>
</Button>
</div>
</div>
</div>
@@ -382,7 +382,6 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import CardBottom from '@/components/card/CardBottom.vue'
import CardContainer from '@/components/card/CardContainer.vue'
@@ -395,6 +394,7 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
import Button from '@/components/ui/button/Button.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'

View File

@@ -1,8 +1,8 @@
<template>
<IconButton
type="secondary"
size="fit-content"
class="group w-full justify-between gap-3 rounded-lg p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
<Button
variant="secondary"
size="lg"
class="group w-full justify-between gap-3 p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-label="props.ariaLabel"
@click="emit('click', $event)"
>
@@ -81,11 +81,11 @@
>
<i class="icon-[lucide--chevron-down] block size-4 leading-none" />
</span>
</IconButton>
</Button>
</template>
<script setup lang="ts">
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import type {
CompletionSummary,
CompletionSummaryMode

View File

@@ -42,19 +42,18 @@
t('sideToolbar.queueProgressOverlay.running')
}}</span>
</span>
<IconButton
<Button
v-if="runningCount > 0"
v-tooltip.top="cancelJobTooltip"
type="secondary"
size="sm"
class="size-6 bg-destructive-background hover:bg-destructive-background-hover"
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
@click="$emit('interruptAll')"
>
<i
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
/>
</IconButton>
</Button>
</div>
<div class="flex items-center gap-2">
@@ -64,26 +63,25 @@
t('sideToolbar.queueProgressOverlay.queuedSuffix')
}}</span>
</span>
<IconButton
<Button
v-if="queuedCount > 0"
v-tooltip.top="clearQueueTooltip"
type="secondary"
size="sm"
class="size-6 bg-secondary-background hover:bg-destructive-background"
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
@click="$emit('clearQueued')"
>
<i
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
/>
</IconButton>
</Button>
</div>
</div>
<Button
class="min-w-30 flex-1 px-2 py-0"
variant="secondary"
size="sm"
size="md"
@click="$emit('viewAllJobs')"
>
{{ t('sideToolbar.queueProgressOverlay.viewAllJobs') }}
@@ -96,7 +94,6 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'

View File

@@ -31,18 +31,16 @@
t('sideToolbar.queueProgressOverlay.queuedSuffix')
}}</span>
</div>
<IconButton
<Button
v-if="queuedCount > 0"
class="group ml-2 size-6 bg-secondary-background hover:bg-destructive-background"
type="secondary"
size="sm"
class="ml-2"
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
@click="$emit('clearQueued')"
>
<i
class="pointer-events-none icon-[lucide--list-x] block size-4 leading-none text-text-primary transition-colors group-hover:text-base-background"
/>
</IconButton>
<i class="icon-[lucide--list-x] size-4" />
</Button>
</div>
</div>
@@ -80,8 +78,8 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import type {
JobGroup,
JobListItem,

View File

@@ -18,18 +18,17 @@
</span>
</div>
<div v-if="!isCloud" class="flex items-center gap-1">
<IconButton
<Button
v-tooltip.top="moreTooltipConfig"
type="transparent"
size="sm"
class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100"
variant="textonly"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
@click="onMoreClick"
>
<i
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
/>
</IconButton>
</Button>
<Popover
ref="morePopoverRef"
:dismissable="true"
@@ -72,8 +71,8 @@ import type { PopoverMethods } from 'primevue/popover'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { isCloud } from '@/platform/distribution/types'

View File

@@ -8,15 +8,14 @@
<p class="m-0 text-[14px] font-normal leading-none">
{{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogTitle') }}
</p>
<IconButton
type="transparent"
size="sm"
class="size-6 bg-transparent text-text-secondary hover:bg-secondary-background hover:opacity-100"
<Button
size="icon"
variant="muted-textonly"
:aria-label="t('g.close')"
@click="onCancel"
>
<i class="icon-[lucide--x] block size-4 leading-none" />
</IconButton>
</Button>
</header>
<div class="flex flex-col gap-4 px-4 py-4 text-[14px] text-text-secondary">
@@ -51,7 +50,6 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useDialogStore } from '@/stores/dialogStore'

View File

@@ -20,18 +20,15 @@
class="flex min-w-0 items-center text-[0.75rem] leading-normal font-normal text-text-secondary"
>
<span class="block min-w-0 truncate">{{ row.value }}</span>
<IconButton
<Button
v-if="row.canCopy"
type="transparent"
size="sm"
class="ml-2 size-6 bg-transparent hover:opacity-90"
size="icon"
variant="muted-textonly"
:aria-label="copyAriaLabel"
@click.stop="copyJobId"
>
<i
class="icon-[lucide--copy] block size-4 leading-none text-text-secondary"
/>
</IconButton>
<i class="icon-[lucide--copy] size-4" />
</Button>
</div>
</template>
</div>
@@ -101,10 +98,9 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useDialogService } from '@/services/dialogService'
@@ -128,7 +124,7 @@ const workflowStore = useWorkflowStore()
const queueStore = useQueueStore()
const executionStore = useExecutionStore()
const dialog = useDialogService()
const { locale } = useI18n()
const { locale, t } = useI18n()
const workflowValue = computed(() => {
const wid = props.workflowId

View File

@@ -15,23 +15,20 @@
</div>
</div>
<div class="ml-2 flex shrink-0 items-center gap-2">
<IconButton
<Button
v-if="showWorkflowFilter"
v-tooltip.top="filterTooltipConfig"
type="secondary"
size="sm"
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
@click="onFilterClick"
>
<i
class="icon-[lucide--list-filter] block size-4 leading-none text-text-primary"
/>
<i class="icon-[lucide--list-filter] size-4" />
<span
v-if="selectedWorkflowFilter !== 'all'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</IconButton>
</Button>
<Popover
v-if="showWorkflowFilter"
ref="filterPopoverRef"
@@ -87,22 +84,19 @@
</IconTextButton>
</div>
</Popover>
<IconButton
<Button
v-tooltip.top="sortTooltipConfig"
type="secondary"
size="sm"
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
@click="onSortClick"
>
<i
class="icon-[lucide--arrow-up-down] block size-4 leading-none text-text-primary"
/>
<i class="icon-[lucide--arrow-up-down] size-4" />
<span
v-if="selectedSortMode !== 'mostRecent'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</IconButton>
</Button>
<Popover
ref="sortPopoverRef"
:dismissable="true"
@@ -152,7 +146,6 @@ import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { jobSortModes, jobTabs } from '@/composables/queue/useJobList'

View File

@@ -128,51 +128,47 @@
key="actions"
class="inline-flex items-center gap-2 pr-1"
>
<IconButton
<Button
v-if="props.state === 'failed' && computedShowClear"
v-tooltip.top="deleteTooltipConfig"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
variant="destructive"
size="icon"
:aria-label="t('g.delete')"
@click.stop="onDeleteClick"
>
<i class="icon-[lucide--trash-2] size-4" />
</IconButton>
<IconButton
</Button>
<Button
v-else-if="
props.state !== 'completed' &&
props.state !== 'running' &&
computedShowClear
"
v-tooltip.top="cancelTooltipConfig"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
variant="destructive"
size="icon"
:aria-label="t('g.cancel')"
@click.stop="onCancelClick"
>
<i class="icon-[lucide--x] size-4" />
</IconButton>
</Button>
<Button
v-else-if="props.state === 'completed'"
class="transform bg-modal-card-button-surface px-2 py-0 transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
variant="textonly"
size="sm"
@click.stop="emit('view')"
>{{ t('menuLabels.View') }}</Button
>
<IconButton
<Button
v-if="props.showMenu !== undefined ? props.showMenu : true"
v-tooltip.top="moreTooltipConfig"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-modal-card-button-surface text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
variant="textonly"
size="icon-sm"
:aria-label="t('g.more')"
@click.stop="emit('menu', $event)"
>
<i class="icon-[lucide--more-horizontal] size-4" />
</IconButton>
</Button>
</div>
<div
v-else-if="props.state !== 'running'"
@@ -183,17 +179,16 @@
</div>
</Transition>
<!-- Running job cancel button - always visible -->
<IconButton
<Button
v-if="props.state === 'running' && computedShowClear"
v-tooltip.top="cancelTooltipConfig"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
variant="destructive"
size="icon"
:aria-label="t('g.cancel')"
@click.stop="onCancelClick"
>
<i class="icon-[lucide--x] size-4" />
</IconButton>
</Button>
</div>
</div>
</div>
@@ -203,7 +198,6 @@
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
import Button from '@/components/ui/button/Button.vue'

View File

@@ -3,10 +3,10 @@ import { storeToRefs } from 'pinia'
import { computed, ref, toValue, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import EditableText from '@/components/common/EditableText.vue'
import Tab from '@/components/tab/Tab.vue'
import TabList from '@/components/tab/TabList.vue'
import Button from '@/components/ui/button/Button.vue'
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
@@ -140,16 +140,11 @@ function handleTitleCancel() {
</h3>
<div class="flex gap-2">
<IconButton
<Button
v-if="isSubgraphNode"
type="transparent"
size="sm"
:class="
cn(
'bg-secondary-background hover:bg-secondary-background-hover text-base-foreground',
isEditingSubgraph && 'bg-secondary-background-selected'
)
"
variant="secondary"
size="icon"
:class="cn(isEditingSubgraph && 'bg-secondary-background-selected')"
@click="
rightSidePanelStore.openPanel(
isEditingSubgraph ? 'parameters' : 'subgraph'
@@ -157,17 +152,16 @@ function handleTitleCancel() {
"
>
<i class="icon-[lucide--settings-2]" />
</IconButton>
<IconButton
type="transparent"
size="sm"
class="bg-secondary-background hover:bg-secondary-background-hover text-base-foreground"
</Button>
<Button
variant="secondary"
size="icon"
:aria-pressed="rightSidePanelStore.isOpen"
:aria-label="t('rightSidePanel.togglePanel')"
@click="closePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</IconButton>
</Button>
</div>
</div>
<nav v-if="hasSelection" class="px-4 pb-2 pt-1">

View File

@@ -128,18 +128,19 @@
</Button>
</div>
</div>
<div class="flex gap-2 pr-4">
<div class="flex shrink gap-2 pr-4 items-center-safe">
<template v-if="isCompact">
<!-- Compact mode: Icon only -->
<IconButton
<Button
v-if="shouldShowDeleteButton"
size="icon"
@click="handleDeleteSelected"
>
<i class="icon-[lucide--trash-2] size-4" />
</IconButton>
<IconButton @click="handleDownloadSelected">
</Button>
<Button size="icon" @click="handleDownloadSelected">
<i class="icon-[lucide--download] size-4" />
</IconButton>
</Button>
</template>
<template v-else>
<!-- Normal mode: Icon + Text -->
@@ -183,7 +184,6 @@ import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'

View File

@@ -47,7 +47,13 @@ function generateVariants() {
for (const variant of variants) {
for (const size of sizes) {
variantButtons.push(
`<Button variant="${variant}" size="${size}">${size === 'icon' ? `<i class="icon-[lucide--settings]" />` : variant}</Button>`
`<Button
variant="${variant}"
size="${size}">${
size.startsWith('icon')
? `<i class="icon-[lucide--settings]" />`
: variant
}</Button>`
)
}
}
@@ -59,7 +65,7 @@ export const AllVariants: Story = {
render: () => ({
components: { Button },
template: `
<div class="grid grid-cols-4 gap-4 place-items-center-safe">
<div class="grid grid-cols-5 gap-4 place-items-center-safe">
${generateVariants().join('\n')}
</div>

View File

@@ -22,7 +22,8 @@ export const buttonVariants = cva({
sm: 'h-6 rounded-sm px-2 py-1 text-xs',
md: 'h-8 rounded-lg p-2 text-xs',
lg: 'h-10 rounded-lg px-4 py-2 text-sm',
icon: 'size-9'
icon: 'size-8',
'icon-sm': 'size-5 p-0'
}
},
@@ -42,7 +43,7 @@ const variants = [
'textonly',
'muted-textonly'
] as const satisfies Array<ButtonVariants['variant']>
const sizes = ['sm', 'md', 'lg', 'icon'] as const satisfies Array<
const sizes = ['sm', 'md', 'lg', 'icon', 'icon-sm'] as const satisfies Array<
ButtonVariants['size']
>

View File

@@ -99,12 +99,13 @@
<div class="h-full w-full bg-blue-500"></div>
</template>
<template #top-right>
<IconButton
<Button
size="icon"
class="!bg-white !text-neutral-900"
@click="() => {}"
>
<i class="icon-[lucide--info]" />
</IconButton>
</Button>
</template>
<template #bottom-right>
<SquareChip label="png" />
@@ -133,7 +134,6 @@
<script setup lang="ts">
import { computed, provide, ref } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import CardBottom from '@/components/card/CardBottom.vue'
@@ -143,6 +143,7 @@ import SquareChip from '@/components/chip/SquareChip.vue'
import SearchBox from '@/components/common/SearchBox.vue'
import MultiSelect from '@/components/input/MultiSelect.vue'
import SingleSelect from '@/components/input/SingleSelect.vue'
import Button from '@/components/ui/button/Button.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
import RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'

View File

@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { computed, provide, ref } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import CardBottom from '@/components/card/CardBottom.vue'
@@ -74,7 +74,7 @@ const createStoryTemplate = (args: StoryArgs) => ({
SearchBox,
MultiSelect,
SingleSelect,
IconButton,
Button,
IconTextButton,
MoreButton,
CardContainer,
@@ -277,9 +277,9 @@ const createStoryTemplate = (args: StoryArgs) => ({
<div class="w-full h-full bg-blue-500"></div>
</template>
<template #top-right>
<IconButton class="!bg-white !text-neutral-900" @click="() => {}">
<Button size="icon" class="!bg-white !text-neutral-900" @click="() => {}">
<i class="icon-[lucide--info] size-4" />
</IconButton>
</Button>
</template>
<template #bottom-right>
<SquareChip label="png" />
@@ -399,9 +399,9 @@ const createStoryTemplate = (args: StoryArgs) => ({
<div class="w-full h-full bg-blue-500"></div>
</template>
<template #top-right>
<IconButton class="!bg-white !text-neutral-900" @click="() => {}">
<Button size="icon" class="!bg-white !text-neutral-900" @click="() => {}">
<i class="icon-[lucide--info] size-4" />
</IconButton>
</Button>
</template>
<template #bottom-right>
<SquareChip label="png" />

View File

@@ -1,15 +1,23 @@
<template>
<div :class="layoutClasses">
<IconButton
<div class="base-widget-layout rounded-2xl overflow-hidden relative">
<Button
v-show="!isRightPanelOpen && hasRightPanel"
:class="rightPanelButtonClasses"
size="icon"
:class="
cn('absolute top-4 right-18 z-10', 'transition-opacity duration-200', {
'opacity-0 pointer-events-none': isRightPanelOpen || !hasRightPanel
})
"
@click="toggleRightPanel"
>
<i class="icon-[lucide--panel-right] text-sm" />
</IconButton>
<IconButton :class="closeButtonClasses" @click="closeDialog">
</Button>
<Button
class="absolute top-4 right-6 z-10 transition-opacity duration-200"
@click="closeDialog"
>
<i class="pi pi-times text-sm"></i>
</IconButton>
</Button>
<div class="flex h-full w-full">
<Transition name="slide-panel">
<nav
@@ -24,27 +32,42 @@
</nav>
</Transition>
<div :class="mainContainerClasses">
<div class="flex-1 flex bg-base-background">
<div class="flex h-full w-full flex-col">
<header v-if="$slots.header" :class="headerClasses">
<header
v-if="$slots.header"
class="w-full h-18 px-6 flex items-center justify-between gap-2"
>
<div class="flex flex-1 shrink-0 gap-2">
<IconButton v-if="!notMobile" @click="toggleLeftPanel">
<Button v-if="!notMobile" size="icon" @click="toggleLeftPanel">
<i
v-if="!showLeftPanel"
class="icon-[lucide--panel-left] text-sm"
:class="
cn(
showLeftPanel
? 'icon-[lucide--panel-left]'
: 'icon-[lucide--panel-left-close]'
)
"
/>
<i v-else class="icon-[lucide--panel-left-close] text-sm" />
</IconButton>
</Button>
<slot name="header"></slot>
</div>
<slot name="header-right-area"></slot>
<div :class="rightAreaClasses">
<IconButton
<div
:class="
cn(
'flex justify-end gap-2 w-0',
hasRightPanel && !isRightPanelOpen ? 'min-w-22' : 'min-w-10'
)
"
>
<Button
v-if="isRightPanelOpen && hasRightPanel"
size="icon"
@click="toggleRightPanel"
>
<i class="icon-[lucide--panel-right-close] text-sm" />
</IconButton>
<i class="icon-[lucide--panel-right-close]" />
</Button>
</div>
</header>
@@ -57,14 +80,14 @@
>
{{ contentTitle }}
</h2>
<div :class="contentContainerClasses">
<div class="min-h-0 px-6 pt-0 pb-10 overflow-y-auto">
<slot name="content"></slot>
</div>
</main>
</div>
<aside
v-if="hasRightPanel && isRightPanelOpen"
:class="rightPanelClasses"
class="w-1/4 min-w-40 max-w-80"
>
<slot name="rightPanel"></slot>
</aside>
@@ -77,7 +100,7 @@
import { useBreakpoints } from '@vueuse/core'
import { computed, inject, ref, useSlots, watch } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { OnCloseKey } from '@/types/widgetTypes'
import { cn } from '@/utils/tailwindUtil'
@@ -128,46 +151,6 @@ const toggleLeftPanel = () => {
const toggleRightPanel = () => {
isRightPanelOpen.value = !isRightPanelOpen.value
}
// Computed classes for better readability
const layoutClasses = cn(
'base-widget-layout',
'rounded-2xl overflow-hidden relative'
)
const rightPanelButtonClasses = computed(() => {
return cn('absolute top-4 right-18 z-10', 'transition-opacity duration-200', {
'opacity-0 pointer-events-none':
isRightPanelOpen.value || !hasRightPanel.value
})
})
const closeButtonClasses = cn(
'absolute top-4 right-6 z-10',
'transition-opacity duration-200'
)
const mainContainerClasses = cn('flex-1 flex bg-base-background')
const headerClasses = cn(
'w-full h-18 px-6',
'flex items-center justify-between gap-2'
)
const rightAreaClasses = computed(() => {
return cn(
'flex justify-end gap-2 w-0',
hasRightPanel.value && !isRightPanelOpen.value ? 'min-w-22' : 'min-w-10'
)
})
const contentContainerClasses = computed(() => {
return cn('min-h-0 px-6 pt-0 pb-10', 'overflow-y-auto')
})
const rightPanelClasses = computed(() => {
return cn('w-1/4 min-w-40 max-w-80')
})
</script>
<style scoped>
.base-widget-layout {

View File

@@ -41,9 +41,6 @@
)
"
>
<IconButton v-if="false" size="sm">
<i class="icon-[lucide--file-text]" />
</IconButton>
<MoreButton ref="dropdown-menu-button" size="sm">
<template #default>
<IconTextButton
@@ -123,7 +120,6 @@ import { useImage } from '@vueuse/core'
import { computed, ref, toValue, useId, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconGroup from '@/components/button/IconGroup.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import MoreButton from '@/components/button/MoreButton.vue'

View File

@@ -59,12 +59,12 @@
<!-- Media actions - show on hover or when playing -->
<IconGroup v-else-if="showActionsOverlay">
<IconButton size="sm" @click.stop="handleZoomClick">
<Button size="icon" @click.stop="handleZoomClick">
<i class="icon-[lucide--zoom-in] size-4" />
</IconButton>
<IconButton size="sm" @click.stop="handleContextMenu">
</Button>
<Button size="icon" @click.stop="handleContextMenu">
<i class="icon-[lucide--ellipsis] size-4" />
</IconButton>
</Button>
</IconGroup>
</template>
@@ -129,13 +129,13 @@
import { useElementHover, whenever } from '@vueuse/core'
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import IconGroup from '@/components/button/IconGroup.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import CardBottom from '@/components/card/CardBottom.vue'
import CardContainer from '@/components/card/CardContainer.vue'
import CardTop from '@/components/card/CardTop.vue'
import SquareChip from '@/components/chip/SquareChip.vue'
import Button from '@/components/ui/button/Button.vue'
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
import { cn } from '@/utils/tailwindUtil'

View File

@@ -1,8 +1,8 @@
<template>
<div class="relative inline-flex items-center">
<IconButton :size :type @click="toggle">
<i class="icon-[lucide--list-filter] text-sm" />
</IconButton>
<Button variant="secondary" size="icon" @click="toggle">
<i class="icon-[lucide--list-filter]" />
</Button>
<Popover
ref="popover"
@@ -27,17 +27,11 @@
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
interface AssetFilterButtonProps extends BaseButtonProps {}
const popover = ref<InstanceType<typeof Popover>>()
const { size = 'md', type = 'secondary' } =
defineProps<AssetFilterButtonProps>()
defineEmits<{
menuOpened: []
menuClosed: []
@@ -57,9 +51,7 @@ const pt = computed(() => ({
},
content: {
class: cn(
'mt-1 rounded-lg',
'bg-base-background text-base-foreground border border-border-default',
'shadow-lg'
'mt-1 rounded-lg bg-base-background text-base-foreground border border-border-default shadow-lg'
)
}
}))

View File

@@ -1,8 +1,8 @@
<template>
<div class="relative inline-flex items-center">
<IconButton :size="size" :type="type" @click="toggle">
<i class="icon-[lucide--arrow-up-down] text-sm" />
</IconButton>
<Button variant="secondary" size="icon" @click="toggle">
<i class="icon-[lucide--arrow-up-down]" />
</Button>
<Popover
ref="popover"
@@ -27,16 +27,11 @@
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
interface AssetSortButtonProps extends BaseButtonProps {}
const popover = ref<InstanceType<typeof Popover>>()
const { size = 'md', type = 'secondary' } = defineProps<AssetSortButtonProps>()
defineEmits<{
menuOpened: []
menuClosed: []

View File

@@ -14,13 +14,15 @@
:style="{ width: '90vw', maxWidth: '800px' }"
>
<div class="relative">
<IconButton
<Button
variant="textonly"
size="icon"
class="absolute top-4 right-6 z-10"
:aria-label="$t('g.close')"
@click="isVisible = false"
>
<i class="pi pi-times text-sm" />
</IconButton>
</Button>
<video
autoplay
muted
@@ -40,7 +42,7 @@ import { useEventListener } from '@vueuse/core'
import Dialog from 'primevue/dialog'
import { onWatcherCleanup, watch } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
const isVisible = defineModel<boolean>({ required: true })

View File

@@ -19,9 +19,10 @@
<!-- Collapse/Expand Button -->
<div class="relative grow-1 flex items-center gap-2.5 min-w-0 flex-1">
<div class="flex shrink-0 items-center px-0.5">
<IconButton
size="fit-content"
type="transparent"
<Button
size="icon-sm"
variant="textonly"
class="hover:bg-transparent"
data-testid="node-collapse-button"
@click.stop="handleCollapse"
@dblclick.stop
@@ -33,20 +34,22 @@
collapsed && '-rotate-90'
)
"
class="relative top-px text-xs leading-none text-node-component-header-icon"
class="text-node-component-header-icon"
/>
</IconButton>
</Button>
</div>
<div v-if="isSubgraphNode" class="icon-[comfy--workflow] size-4" />
<div
v-if="isApiNode"
:class="
flags.subscriptionTiersEnabled
? 'icon-[lucide--component]'
: 'icon-[lucide--dollar-sign]'
cn(
'size-4',
flags.subscriptionTiersEnabled
? 'icon-[lucide--component]'
: 'icon-[lucide--dollar-sign]'
)
"
class="size-4"
/>
<!-- Node Title -->
@@ -79,22 +82,19 @@
class="size-5"
data-testid="node-pin-indicator"
/>
<IconButton
<Button
v-if="isSubgraphNode"
v-tooltip.top="enterSubgraphTooltipConfig"
type="transparent"
variant="textonly"
size="sm"
data-testid="subgraph-enter-button"
class="ml-2 text-node-component-header h-5"
class="text-node-component-header h-5 px-0.5"
@click.stop="handleEnterSubgraph"
@dblclick.stop
>
<div
class="min-w-max rounded-sm bg-node-component-surface px-1 py-0.5 text-xs flex items-center gap-1"
>
{{ $t('g.edit') }}
<i class="icon-[lucide--scaling] size-5" />
</div>
</IconButton>
<span>{{ $t('g.edit') }}</span>
<i class="icon-[lucide--scaling] size-5" />
</Button>
</div>
</div>
</div>
@@ -103,8 +103,8 @@
<script setup lang="ts">
import { computed, onErrorCaptured, ref, toValue, watch } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import EditableText from '@/components/common/EditableText.vue'
import Button from '@/components/ui/button/Button.vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useFeatureFlags } from '@/composables/useFeatureFlags'

View File

@@ -60,16 +60,6 @@ export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
return `${baseByType[type]} ${borderByType[type]}`
}
export const getIconButtonSizeClasses = (size: ButtonSize = 'md') => {
const sizeClasses = {
'fit-content': 'w-auto h-auto',
'full-width': 'w-full h-auto',
sm: 'size-8 text-xs !rounded-md',
md: 'size-10 text-sm'
}
return sizeClasses[size]
}
export const getBaseButtonClasses = () => {
return [
'flex items-center justify-center shrink-0',

View File

@@ -6,13 +6,20 @@
<ContentDivider :width="0.3" />
<Button
v-if="isSmallScreen"
:icon="isSideNavOpen ? 'pi pi-chevron-left' : 'pi pi-chevron-right'"
severity="secondary"
filled
class="absolute top-1/2 z-10 -translate-y-1/2"
:class="isSideNavOpen ? 'left-[12rem]' : 'left-2'"
variant="secondary"
size="icon"
:class="
cn(
'absolute top-1/2 z-10 -translate-y-1/2',
isSideNavOpen ? 'left-[12rem]' : 'left-2'
)
"
@click="toggleSideNav"
/>
>
<i
:class="isSideNavOpen ? 'pi pi-chevron-left' : 'pi pi-chevron-right'"
/>
</Button>
<div class="relative flex flex-1 overflow-hidden">
<ManagerNavSidebar
v-if="isSideNavOpen"
@@ -46,13 +53,14 @@
{{ $t('manager.conflicts.warningBanner.button') }}
</p>
</div>
<IconButton
<Button
class="absolute top-0 right-0"
type="transparent"
variant="textonly"
size="icon"
@click="dismissWarningBanner"
>
<i class="pi pi-times text-xs text-base-foreground"></i>
</IconButton>
</Button>
</div>
<RegistrySearchBar
v-model:search-query="searchQuery"
@@ -125,7 +133,6 @@
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { merge } from 'es-toolkit/compat'
import Button from 'primevue/button'
import {
computed,
onBeforeUnmount,
@@ -137,14 +144,15 @@ import {
} from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import ContentDivider from '@/components/common/ContentDivider.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Button from '@/components/ui/button/Button.vue'
import { useResponsiveCollapse } from '@/composables/element/useResponsiveCollapse'
import { useExternalLink } from '@/composables/useExternalLink'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes'
import { cn } from '@/utils/tailwindUtil'
import ManagerNavSidebar from '@/workbench/extensions/manager/components/manager/ManagerNavSidebar.vue'
import InfoPanel from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanel.vue'
import InfoPanelMultiItem from '@/workbench/extensions/manager/components/manager/infoPanel/InfoPanelMultiItem.vue'