Component: Button Migration 3: IconTextButton (#7603)

## Summary

Replace all the `IconTextButton`s with `Button`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7603-WIP-Component-Button-Migraion-3-IconTextButton-2cd6d73d365081b7b742fa2172dc2ba8)
by [Unito](https://www.unito.io)
This commit is contained in:
Alexander Brown
2025-12-18 16:09:56 -08:00
committed by GitHub
parent 6244cf1008
commit 2c26fbb550
26 changed files with 338 additions and 754 deletions

View File

@@ -1,213 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import IconTextButton from './IconTextButton.vue'
const meta: Meta<typeof IconTextButton> = {
title: 'Components/Button/IconTextButton',
component: IconTextButton,
tags: ['autodocs'],
argTypes: {
label: {
control: 'text'
},
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'
},
iconPosition: {
control: { type: 'select' },
options: ['left', 'right']
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--package] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Deploy',
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--settings] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Settings',
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--x] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Cancel',
type: 'transparent',
size: 'md'
}
}
export const WithIconRight: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--chevron-right] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Next',
type: 'primary',
size: 'md',
iconPosition: 'right'
}
}
export const Small: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--save] size-3" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Save',
type: 'primary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: {
IconTextButton
},
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<IconTextButton label="Download" type="primary" size="sm" @click="() => {}">
<template #icon>
<i class="icon-[lucide--download] size-3" />
</template>
</IconTextButton>
<IconTextButton label="Download" type="primary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--download] size-4" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Settings" type="secondary" size="sm" @click="() => {}">
<template #icon>
<i class="icon-[lucide--settings] size-3" />
</template>
</IconTextButton>
<IconTextButton label="Settings" type="secondary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--settings] size-4" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Delete" type="transparent" size="sm" @click="() => {}">
<template #icon>
<i class="icon-[lucide--trash-2] size-3" />
</template>
</IconTextButton>
<IconTextButton label="Delete" type="transparent" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--trash-2] size-4" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Next" type="primary" size="md" iconPosition="right" @click="() => {}">
<template #icon>
<i class="icon-[lucide--chevron-right] size-4" />
</template>
</IconTextButton>
<IconTextButton label="Previous" type="secondary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--chevron-left] size-4" />
</template>
</IconTextButton>
<IconTextButton label="Save File" type="primary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--save] size-4" />
</template>
</IconTextButton>
</div>
</div>
`
}),
parameters: {
controls: { disable: true },
actions: { disable: true }
}
}

View File

@@ -1,58 +0,0 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></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,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
defineOptions({
inheritAttrs: false
})
interface IconTextButtonProps extends BaseButtonProps {
iconPosition?: 'left' | 'right'
label: string
onClick?: () => void
}
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
iconPosition = 'left',
label,
onClick
} = defineProps<IconTextButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
const sizeClasses = getButtonSizeClasses(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 IconTextButton from './IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import MoreButton from './MoreButton.vue'
const meta: Meta<typeof MoreButton> = {
@@ -17,30 +17,26 @@ type Story = StoryObj<typeof MoreButton>
export const Basic: Story = {
render: () => ({
components: { MoreButton, IconTextButton },
components: { MoreButton, Button },
template: `
<div style="height: 200px; display: flex; align-items: center; justify-content: center;">
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="transparent"
label="Settings"
<Button
variant="textonly"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--download] size-4" />
</template>
</IconTextButton>
<i class="icon-[lucide--download] size-4" />
<span>Settings</span>
</Button>
<IconTextButton
type="transparent"
label="Profile"
<Button
variant="textonly"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--scroll-text] size-4" />
</template>
</IconTextButton>
<i class="icon-[lucide--scroll-text] size-4" />
<span>Profile</span>
</Button>
</template>
</MoreButton>
</div>

View File

@@ -22,16 +22,17 @@
<template #header-right-area>
<div class="flex gap-2">
<IconTextButton
<Button
v-if="filteredCount !== totalCount"
type="secondary"
:label="$t('templateWorkflows.resetFilters', 'Clear Filters')"
variant="secondary"
size="lg"
@click="resetFilters"
>
<template #icon>
<i class="icon-[lucide--filter-x]" />
</template>
</IconTextButton>
<i class="icon-[lucide--filter-x]" />
<span>{{
$t('templateWorkflows.resetFilters', 'Clear Filters')
}}</span>
</Button>
</div>
</template>
@@ -382,7 +383,6 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
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'

View File

@@ -4,20 +4,17 @@
v-if="isCloud"
class="flex w-full items-center justify-between gap-2 py-2 px-4"
>
<IconTextButton
:label="$t('missingNodes.cloud.learnMore')"
type="transparent"
<Button
variant="textonly"
size="sm"
icon-position="left"
as="a"
href="https://www.comfy.org/cloud"
target="_blank"
rel="noopener noreferrer"
>
<template #icon>
<i class="icon-[lucide--info]"></i>
</template>
</IconTextButton>
<i class="icon-[lucide--info]"></i>
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
</Button>
<Button variant="secondary" size="md" @click="handleGotItClick">{{
$t('missingNodes.cloud.gotIt')
}}</Button>
@@ -50,7 +47,6 @@
import { computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'

View File

@@ -8,20 +8,15 @@
/>
<div class="flex items-center justify-between px-3">
<IconTextButton
class="grow gap-1 p-2 text-center font-inter text-[12px] leading-none hover:opacity-90 justify-center"
type="secondary"
:label="t('sideToolbar.queueProgressOverlay.showAssets')"
:aria-label="t('sideToolbar.queueProgressOverlay.showAssets')"
<Button
class="grow gap-1 justify-center"
variant="secondary"
size="sm"
@click="$emit('showAssets')"
>
<template #icon>
<div
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
aria-hidden="true"
/>
</template>
</IconTextButton>
<i class="icon-[comfy--image-ai-edit] size-4" />
<span>{{ t('sideToolbar.queueProgressOverlay.showAssets') }}</span>
</Button>
<div class="ml-4 inline-flex items-center">
<div
class="inline-flex h-6 items-center text-[12px] leading-none text-text-primary opacity-90"
@@ -78,7 +73,6 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import type {
JobGroup,

View File

@@ -46,19 +46,18 @@
<div
class="flex flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3 font-inter"
>
<IconTextButton
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
:label="t('sideToolbar.queueProgressOverlay.clearHistory')"
<Button
class="w-full justify-start"
variant="textonly"
size="sm"
:aria-label="t('sideToolbar.queueProgressOverlay.clearHistory')"
@click="onClearHistoryFromMenu"
>
<template #icon>
<i
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
/>
</template>
</IconTextButton>
<i class="icon-[lucide--file-x-2] size-4 text-muted" />
<span>{{
t('sideToolbar.queueProgressOverlay.clearHistory')
}}</span>
</Button>
</div>
</Popover>
</div>
@@ -71,7 +70,6 @@ import type { PopoverMethods } from 'primevue/popover'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
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

@@ -20,24 +20,23 @@
<div v-if="entry.kind === 'divider'" class="px-2 py-1">
<div class="h-px bg-interface-stroke" />
</div>
<IconTextButton
<Button
v-else
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-interface-panel-hover-surface"
type="transparent"
:label="entry.label"
class="w-full justify-start bg-transparent"
variant="textonly"
size="sm"
:aria-label="entry.label"
@click="onEntry(entry)"
>
<template #icon>
<i
v-if="entry.icon"
:class="[
entry.icon,
'block size-4 shrink-0 leading-none text-text-secondary'
]"
/>
</template>
</IconTextButton>
<i
v-if="entry.icon"
:class="[
entry.icon,
'block size-4 shrink-0 leading-none text-text-secondary'
]"
/>
<span>{{ entry.label }}</span>
</Button>
</template>
</div>
</Popover>
@@ -47,7 +46,7 @@
import Popover from 'primevue/popover'
import { ref } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import type { MenuEntry } from '@/composables/queue/useJobMenu'
defineProps<{ entries: MenuEntry[] }>()

View File

@@ -58,31 +58,28 @@
{{ t('queue.jobDetails.errorMessage') }}
</div>
<div class="flex items-center justify-between gap-4">
<IconTextButton
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
type="transparent"
:label="copyAriaLabel"
:aria-label="copyAriaLabel"
<Button
class="justify-start px-0"
variant="muted-textonly"
size="sm"
icon-position="right"
@click.stop="copyErrorMessage"
>
<template #icon>
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
</template>
</IconTextButton>
<IconTextButton
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
type="transparent"
:label="t('queue.jobDetails.report')"
<span>{{ copyAriaLabel }}</span>
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
</Button>
<Button
class="justify-start px-0"
variant="muted-textonly"
size="sm"
icon-position="right"
@click.stop="reportJobError"
>
<template #icon>
<i
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
/>
</template>
</IconTextButton>
<span>{{ t('queue.jobDetails.report') }}</span>
<i
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
/>
</Button>
</div>
<div
class="col-span-2 mt-2 rounded bg-interface-panel-hover-surface px-4 py-2 text-[0.75rem] leading-normal text-text-secondary"
@@ -98,7 +95,6 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { isCloud } from '@/platform/distribution/types'

View File

@@ -47,41 +47,34 @@
<div
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
>
<IconTextButton
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
icon-position="right"
:label="t('sideToolbar.queueProgressOverlay.filterAllWorkflows')"
:aria-label="
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
"
<Button
class="w-full justify-between"
variant="textonly"
size="sm"
@click="selectWorkflowFilter('all')"
>
<template #icon>
<i
v-if="selectedWorkflowFilter === 'all'"
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
/>
</template>
</IconTextButton>
<span>{{
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
}}</span>
<i
v-if="selectedWorkflowFilter === 'all'"
class="icon-[lucide--check] size-4"
/>
</Button>
<div class="mx-2 mt-1 h-px" />
<IconTextButton
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
icon-position="right"
:label="t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')"
:aria-label="
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
"
<Button
class="w-full justify-between"
variant="textonly"
@click="selectWorkflowFilter('current')"
>
<template #icon>
<i
v-if="selectedWorkflowFilter === 'current'"
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
/>
</template>
</IconTextButton>
<span>{{
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
}}</span>
<i
v-if="selectedWorkflowFilter === 'current'"
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
/>
</Button>
</div>
</Popover>
<Button
@@ -115,21 +108,18 @@
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
>
<template v-for="(mode, index) in jobSortModes" :key="mode">
<IconTextButton
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
icon-position="right"
:label="sortLabel(mode)"
:aria-label="sortLabel(mode)"
<Button
class="w-full justify-between"
variant="textonly"
size="sm"
@click="selectSortMode(mode)"
>
<template #icon>
<i
v-if="selectedSortMode === mode"
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
/>
</template>
</IconTextButton>
<span>{{ sortLabel(mode) }}</span>
<i
v-if="selectedSortMode === mode"
class="icon-[lucide--check] size-4 text-text-secondary"
/>
</Button>
<div
v-if="index < jobSortModes.length - 1"
class="mx-2 mt-1 h-px"
@@ -146,7 +136,6 @@ import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { jobSortModes, jobTabs } from '@/composables/queue/useJobList'
import type { JobSortMode, JobTab } from '@/composables/queue/useJobList'

View File

@@ -37,15 +37,10 @@
<template #header>
<!-- Job Detail View Header -->
<div v-if="isInFolderView" class="px-2 2xl:px-4">
<IconTextButton
:label="$t('sideToolbar.backToAssets')"
type="secondary"
@click="exitFolderView"
>
<template #icon>
<i class="icon-[lucide--arrow-left] size-4" />
</template>
</IconTextButton>
<Button variant="secondary" size="lg" @click="exitFolderView">
<i class="icon-[lucide--arrow-left] size-4" />
<span>{{ $t('sideToolbar.backToAssets') }}</span>
</Button>
</div>
<!-- Filter Bar -->
@@ -128,7 +123,7 @@
</Button>
</div>
</div>
<div class="flex shrink gap-2 pr-4 items-center-safe">
<div class="flex shrink gap-2 pr-4 items-center-safe justify-end-safe">
<template v-if="isCompact">
<!-- Compact mode: Icon only -->
<Button
@@ -144,27 +139,18 @@
</template>
<template v-else>
<!-- Normal mode: Icon + Text -->
<IconTextButton
<Button
v-if="shouldShowDeleteButton"
:label="$t('mediaAsset.selection.deleteSelected')"
type="secondary"
icon-position="right"
variant="secondary"
@click="handleDeleteSelected"
>
<template #icon>
<i class="icon-[lucide--trash-2] size-4" />
</template>
</IconTextButton>
<IconTextButton
:label="$t('mediaAsset.selection.downloadSelected')"
type="secondary"
icon-position="right"
@click="handleDownloadSelected"
>
<template #icon>
<i class="icon-[lucide--download] size-4" />
</template>
</IconTextButton>
<span>{{ $t('mediaAsset.selection.deleteSelected') }}</span>
<i class="icon-[lucide--trash-2] size-4" />
</Button>
<Button variant="secondary" @click="handleDownloadSelected">
<span>{{ $t('mediaAsset.selection.downloadSelected') }}</span>
<i class="icon-[lucide--download] size-4" />
</Button>
</template>
</div>
</div>
@@ -184,7 +170,6 @@ import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'

View File

@@ -1,10 +1,20 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type {
Meta,
StoryObj,
ComponentPropsAndSlots
} from '@storybook/vue3-vite'
import Button from './Button.vue'
import { FOR_STORIES } from '@/components/ui/button/button.variants'
interface ButtonPropsAndStoryArgs extends ComponentPropsAndSlots<
typeof Button
> {
icon?: 'left' | 'right'
}
const { variants, sizes } = FOR_STORIES
const meta: Meta<typeof Button> = {
const meta: Meta<ButtonPropsAndStoryArgs> = {
title: 'Components/Button/Button',
component: Button,
tags: ['autodocs'],
@@ -22,13 +32,19 @@ const meta: Meta<typeof Button> = {
as: { defaultValue: 'button' },
asChild: { defaultValue: false },
default: {
control: { type: 'text' },
defaultValue: 'Button'
},
icon: {
control: { type: 'select' },
options: [undefined, 'left', 'right']
}
},
args: {
variant: 'secondary',
size: 'md',
default: 'Button'
default: 'Button',
icon: undefined
}
}
@@ -36,10 +52,18 @@ export default meta
type Story = StoryObj<typeof meta>
export const SingleButton: Story = {
args: {
variant: 'primary',
size: 'lg'
}
render: (args) => ({
components: { Button },
setup() {
return { args }
},
template: `
<Button v-bind="args">
<i v-if="args.icon === 'left'" class="icon-[lucide--settings]" />
{{args.default}}
<i v-if="args.icon === 'right'" class="icon-[lucide--settings]" />
</Button>`
})
}
function generateVariants() {

View File

@@ -17,43 +17,34 @@
<template #header-right-area>
<div class="flex gap-2">
<IconTextButton
type="primary"
:label="$t('g.upload')"
@click="() => {}"
>
<template #icon>
<i class="icon-[lucide--upload]" />
</template>
</IconTextButton>
<Button variant="primary" @click="() => {}">
<i class="icon-[lucide--upload]" />
<span>{{ $t('g.upload') }}</span>
</Button>
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="secondary"
:label="$t('g.settings')"
<Button
variant="secondary"
@click="
() => {
close()
}
"
>
<template #icon>
<i class="icon-[lucide--download]" />
</template>
</IconTextButton>
<IconTextButton
type="primary"
:label="$t('g.profile')"
<i class="icon-[lucide--download]" />
<span>{{ $t('g.settings') }}</span>
</Button>
<Button
variant="primary"
@click="
() => {
close()
}
"
>
<template #icon>
<i class="icon-[lucide--scroll]" />
</template>
</IconTextButton>
<i class="icon-[lucide--scroll]" />
<span>{{ $t('g.profile') }}</span>
</Button>
</template>
</MoreButton>
</div>
@@ -134,7 +125,6 @@
<script setup lang="ts">
import { computed, provide, ref } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import CardBottom from '@/components/card/CardBottom.vue'
import CardContainer from '@/components/card/CardContainer.vue'

View File

@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { computed, provide, ref } from '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'
import CardContainer from '@/components/card/CardContainer.vue'
@@ -75,7 +74,6 @@ const createStoryTemplate = (args: StoryArgs) => ({
MultiSelect,
SingleSelect,
Button,
IconTextButton,
MoreButton,
CardContainer,
CardTop,
@@ -202,33 +200,32 @@ const createStoryTemplate = (args: StoryArgs) => ({
<!-- Header Right Area -->
<template v-if="args.hasHeaderRightArea" #header-right-area>
<div class="flex gap-2">
<IconTextButton type="primary" label="Upload Model" @click="() => {}">
<template #icon>
<Button variant="primary" @click="() => {}">
<i class="icon-[lucide--upload] size-3" />
</template>
</IconTextButton>
<span> Upload Model </span>
</Button>
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="secondary"
<Button
variant="secondary"
label="Settings"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--download] size-3" />
</template>
</IconTextButton>
</Button>
<IconTextButton
type="primary"
<Button
variant="primary"
label="Profile"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--scroll] size-3" />
</template>
</IconTextButton>
</Button>
</template>
</MoreButton>
</div>
@@ -327,33 +324,28 @@ const createStoryTemplate = (args: StoryArgs) => ({
<!-- Header Right Area -->
<template v-if="args.hasHeaderRightArea" #header-right-area>
<div class="flex gap-2">
<IconTextButton type="primary" label="Upload Model" @click="() => {}">
<template #icon>
<Button variant="primary" @click="() => {}">
<i class="icon-[lucide--upload] size-3" />
</template>
</IconTextButton>
<span>Upload Model</span>
</Button>
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="secondary"
label="Settings"
<Button
variant="secondary"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--download] size-3" />
</template>
</IconTextButton>
<span>Settings</span>
</Button>
<IconTextButton
type="primary"
label="Profile"
<Button
variant="primary"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--scroll] size-3" />
</template>
</IconTextButton>
<span>Profile</span>
</Button>
</template>
</MoreButton>
</div>

View File

@@ -29,19 +29,18 @@
:placeholder="$t('g.searchPlaceholder')"
class="max-w-96"
/>
<IconTextButton
<Button
v-if="isUploadButtonEnabled"
type="accent"
size="md"
class="!h-10 [&>span]:hidden md:[&>span]:inline"
variant="primary"
:size="breakpoints.md ? 'md' : 'icon'"
data-attr="upload-model-button"
:label="$t('assetBrowser.uploadModel')"
:on-click="showUploadDialog"
@click="showUploadDialog"
>
<template #icon>
<i class="icon-[lucide--folder-input]" />
</template>
</IconTextButton>
<i class="icon-[lucide--folder-input]" />
<span class="hidden md:inline">{{
$t('assetBrowser.uploadModel')
}}</span>
</Button>
</div>
</template>
@@ -64,12 +63,16 @@
</template>
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import {
breakpointsTailwind,
useAsyncState,
useBreakpoints
} from '@vueuse/core'
import { computed, provide, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import SearchBox from '@/components/common/SearchBox.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 AssetFilterBar from '@/platform/assets/components/AssetFilterBar.vue'
@@ -99,6 +102,8 @@ const emit = defineEmits<{
close: []
}>()
const breakpoints = useBreakpoints(breakpointsTailwind)
provide(OnCloseKey, props.onClose ?? (() => {}))
const fetchAssets = async () => {

View File

@@ -43,26 +43,24 @@
>
<MoreButton ref="dropdown-menu-button" size="sm">
<template #default>
<IconTextButton
:label="$t('g.rename')"
type="secondary"
<Button
variant="secondary"
size="md"
class="justify-start"
@click="startAssetRename"
>
<template #icon>
<i class="icon-[lucide--pencil]" />
</template>
</IconTextButton>
<IconTextButton
:label="$t('g.delete')"
type="secondary"
<i class="icon-[lucide--pencil]" />
<span>{{ $t('g.rename') }}</span>
</Button>
<Button
variant="secondary"
size="md"
class="justify-start"
@click="confirmDeletion"
>
<template #icon>
<i class="icon-[lucide--trash-2]" />
</template>
</IconTextButton>
<i class="icon-[lucide--trash-2]" />
<span>{{ $t('g.delete') }}</span>
</Button>
</template>
</MoreButton>
</IconGroup>
@@ -121,10 +119,10 @@ import { computed, ref, toValue, useId, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import IconGroup from '@/components/button/IconGroup.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import EditableText from '@/components/common/EditableText.vue'
import { showConfirmDialog } from '@/components/dialog/confirm/confirmDialog'
import Button from '@/components/ui/button/Button.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'

View File

@@ -70,19 +70,17 @@
<!-- Output count (top-right) -->
<template v-if="showOutputCount" #top-right>
<IconTextButton
<Button
v-tooltip.top.pt:pointer-events-none="
$t('mediaAsset.actions.seeMoreOutputs')
"
type="secondary"
variant="secondary"
size="sm"
:label="String(outputCount)"
@click.stop="handleOutputCountClick"
>
<template #icon>
<i class="icon-[lucide--layers] size-4" />
</template>
</IconTextButton>
<i class="icon-[lucide--layers] size-4" />
<span>{{ outputCount }}</span>
</Button>
</template>
</CardTop>
</template>
@@ -130,7 +128,6 @@ import { useElementHover, whenever } from '@vueuse/core'
import { computed, defineAsyncComponent, provide, ref, toRef } from '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'

View File

@@ -13,18 +13,17 @@
}"
>
<template #item="{ item, props }">
<IconTextButton
type="secondary"
size="full-width"
:label="
typeof item.label === 'function' ? item.label() : (item.label ?? '')
"
<Button
variant="secondary"
size="sm"
class="w-full justify-start"
v-bind="props.action"
>
<template #icon>
<i :class="item.icon" class="size-4" />
</template>
</IconTextButton>
<i :class="item.icon" class="size-4" />
<span>{{
typeof item.label === 'function' ? item.label() : (item.label ?? '')
}}</span>
</Button>
</template>
</ContextMenu>
</template>
@@ -34,9 +33,9 @@ import { onClickOutside } from '@vueuse/core'
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import { t } from '@/i18n'
import Button from '@/components/ui/button/Button.vue'
import { isCloud } from '@/platform/distribution/types'
import { supportsWorkflowMetadata } from '@/platform/workflow/utils/workflowExtractionUtil'
import { detectNodeTypeFromFilename } from '@/utils/loaderNodeUtil'
@@ -60,6 +59,7 @@ const emit = defineEmits<{
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
const actions = useMediaAssetActions()
const { t } = useI18n()
// Close context menu when clicking outside
onClickOutside(

View File

@@ -25,10 +25,9 @@
>
<template #default="{ close }">
<MediaAssetSortMenu
:sort-by="sortBy"
v-model:sort-by="sortBy"
:show-generation-time-sort
:close="close"
@update:sort-by="handleSortChange"
/>
</template>
</AssetSortButton>
@@ -43,30 +42,25 @@ import MediaAssetFilterButton from './MediaAssetFilterButton.vue'
import MediaAssetFilterMenu from './MediaAssetFilterMenu.vue'
import AssetSortButton from './MediaAssetSortButton.vue'
import MediaAssetSortMenu from './MediaAssetSortMenu.vue'
import type { SortBy } from './MediaAssetSortMenu.vue'
const { showGenerationTimeSort = false } = defineProps<{
searchQuery: string
sortBy: 'newest' | 'oldest' | 'longest' | 'fastest'
showGenerationTimeSort?: boolean
mediaTypeFilters: string[]
}>()
const emit = defineEmits<{
'update:searchQuery': [value: string]
'update:sortBy': [value: 'newest' | 'oldest' | 'longest' | 'fastest']
'update:mediaTypeFilters': [value: string[]]
}>()
const sortBy = defineModel<SortBy>('sortBy', { required: true })
const handleSearchChange = (value: string | undefined) => {
emit('update:searchQuery', value ?? '')
}
const handleSortChange = (
value: 'newest' | 'oldest' | 'longest' | 'fastest'
) => {
emit('update:sortBy', value)
}
const handleMediaTypeFiltersChange = (value: string[]) => {
emit('update:mediaTypeFilters', value)
}

View File

@@ -1,74 +1,59 @@
<template>
<div class="flex flex-col">
<IconTextButton
type="transparent"
icon-position="right"
:label="$t('sideToolbar.mediaAssets.sortNewestFirst')"
<Button
variant="textonly"
class="justify-start"
@click="handleSortChange('newest')"
>
<template #icon>
<i v-if="sortBy === 'newest'" class="icon-[lucide--check] size-4" />
</template>
</IconTextButton>
<span>{{ $t('sideToolbar.mediaAssets.sortNewestFirst') }}</span>
<i v-if="sortBy === 'newest'" class="icon-[lucide--check] size-4" />
</Button>
<IconTextButton
type="transparent"
icon-position="right"
:label="$t('sideToolbar.mediaAssets.sortOldestFirst')"
<Button
variant="textonly"
class="justify-start"
@click="handleSortChange('oldest')"
>
<template #icon>
<i v-if="sortBy === 'oldest'" class="icon-[lucide--check] size-4" />
</template>
</IconTextButton>
<span>{{ $t('sideToolbar.mediaAssets.sortOldestFirst') }}</span>
<i v-if="sortBy === 'oldest'" class="icon-[lucide--check] size-4" />
</Button>
<template v-if="showGenerationTimeSort">
<IconTextButton
type="transparent"
icon-position="right"
:label="$t('sideToolbar.mediaAssets.sortLongestFirst')"
<Button
variant="textonly"
class="justify-start"
@click="handleSortChange('longest')"
>
<template #icon>
<i v-if="sortBy === 'longest'" class="icon-[lucide--check] size-4" />
</template>
</IconTextButton>
<span>{{ $t('sideToolbar.mediaAssets.sortLongestFirst') }}</span>
<i v-if="sortBy === 'longest'" class="icon-[lucide--check] size-4" />
</Button>
<IconTextButton
type="transparent"
icon-position="right"
:label="$t('sideToolbar.mediaAssets.sortFastestFirst')"
<Button
variant="textonly"
class="justify-start"
@click="handleSortChange('fastest')"
>
<template #icon>
<i v-if="sortBy === 'fastest'" class="icon-[lucide--check] size-4" />
</template>
</IconTextButton>
<span>{{ $t('sideToolbar.mediaAssets.sortFastestFirst') }}</span>
<i v-if="sortBy === 'fastest'" class="icon-[lucide--check] size-4" />
</Button>
</template>
</div>
</template>
<script setup lang="ts">
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
const {
sortBy,
close,
showGenerationTimeSort = false
} = defineProps<{
sortBy: 'newest' | 'oldest' | 'longest' | 'fastest'
export type SortBy = 'newest' | 'oldest' | 'longest' | 'fastest'
const { close, showGenerationTimeSort = false } = defineProps<{
close: () => void
showGenerationTimeSort?: boolean
}>()
const emit = defineEmits<{
'update:sortBy': [value: 'newest' | 'oldest' | 'longest' | 'fastest']
}>()
const sortBy = defineModel<SortBy>('sortBy', { required: true })
const handleSortChange = (
value: 'newest' | 'oldest' | 'longest' | 'fastest'
) => {
emit('update:sortBy', value)
const handleSortChange = (value: SortBy) => {
sortBy.value = value
close()
}
</script>

View File

@@ -1,18 +1,16 @@
<template>
<div class="flex justify-end gap-2 w-full">
<IconTextButton
<Button
v-if="currentStep === 1"
:label="$t('assetBrowser.uploadModelHowDoIFindThis')"
type="transparent"
size="md"
class="mr-auto underline text-muted-foreground"
variant="muted-textonly"
size="lg"
class="mr-auto underline"
data-attr="upload-model-step1-help-link"
@click="showVideoHelp = true"
>
<template #icon>
<i class="icon-[lucide--circle-question-mark]" />
</template>
</IconTextButton>
<i class="icon-[lucide--circle-question-mark]" />
<span>{{ $t('assetBrowser.uploadModelHowDoIFindThis') }}</span>
</Button>
<Button
v-if="currentStep === 1"
variant="muted-textonly"
@@ -35,38 +33,31 @@
</Button>
<span v-else />
<IconTextButton
<Button
v-if="currentStep === 1"
:label="$t('g.continue')"
type="secondary"
size="md"
variant="secondary"
size="lg"
data-attr="upload-model-step1-continue-button"
:disabled="!canFetchMetadata || isFetchingMetadata"
@click="emit('fetchMetadata')"
>
<template #icon>
<i
v-if="isFetchingMetadata"
class="icon-[lucide--loader-circle] animate-spin"
/>
</template>
</IconTextButton>
<IconTextButton
<i
v-if="isFetchingMetadata"
class="icon-[lucide--loader-circle] animate-spin"
/>
<span>{{ $t('g.continue') }}</span>
</Button>
<Button
v-else-if="currentStep === 2"
:label="$t('assetBrowser.upload')"
type="secondary"
size="md"
variant="secondary"
size="lg"
data-attr="upload-model-step2-confirm-button"
:disabled="!canUploadModel || isUploading"
@click="emit('upload')"
>
<template #icon>
<i
v-if="isUploading"
class="icon-[lucide--loader-circle] animate-spin"
/>
</template>
</IconTextButton>
<i v-if="isUploading" class="icon-[lucide--loader-circle] animate-spin" />
<span>{{ $t('assetBrowser.upload') }}</span>
</Button>
<Button
v-else-if="currentStep === 3 && uploadStatus === 'success'"
variant="secondary"
@@ -86,7 +77,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import VideoHelpDialog from '@/platform/assets/components/VideoHelpDialog.vue'

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { useModelUpload } from '@/platform/assets/composables/useModelUpload'
import { cn } from '@/utils/tailwindUtil'
@@ -40,16 +40,15 @@ const singleFilterOption = computed(() => filterOptions.length === 1)
>
{{ option.name }}
</button>
<IconTextButton
<Button
v-if="isUploadButtonEnabled && singleFilterOption"
:label="$t('g.import')"
class="ml-auto text-base-foreground hover:bg-node-component-widget-input-surface"
type="transparent"
class="ml-auto"
size="md"
variant="textonly"
@click="showUploadDialog"
>
<template #icon>
<i class="icon-[lucide--folder-input]" />
</template>
</IconTextButton>
<i class="icon-[lucide--folder-input]" />
<span>{{ $t('g.import') }}</span>
</Button>
</div>
</template>

View File

@@ -1,69 +0,0 @@
import { cn } from '@comfyorg/tailwind-utils'
import type { HTMLAttributes } from 'vue'
export type ButtonSize = 'full-width' | 'fit-content' | 'sm' | 'md'
type ButtonType = 'primary' | 'secondary' | 'transparent' | 'accent'
type ButtonBorder = boolean
export interface BaseButtonProps {
size?: ButtonSize
type?: ButtonType
border?: ButtonBorder
disabled?: boolean
class?: HTMLAttributes['class']
}
export const getButtonSizeClasses = (size: ButtonSize = 'md') => {
const sizeClasses = {
'fit-content': '',
'full-width': 'w-full',
sm: 'px-2 py-1.5 text-xs',
md: 'px-4 py-2 text-sm'
}
return sizeClasses[size]
}
export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
const baseByType = {
primary: 'bg-base-foreground border-none text-base-background',
secondary: cn(
'bg-secondary-background border-none text-base-foreground hover:bg-secondary-background-hover'
),
transparent: cn(
'bg-transparent border-none text-muted-foreground hover:bg-secondary-background-hover'
),
accent:
'bg-primary-background hover:bg-primary-background-hover border-none text-white font-bold'
} as const
return baseByType[type]
}
export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
const baseByType = {
primary: 'bg-base-background text-base-foreground',
secondary: 'bg-secondary-background text-base-foreground',
transparent: cn(
'bg-transparent text-base-foreground hover:bg-secondary-background-hover'
),
accent:
'bg-primary-background hover:bg-primary-background-hover text-white font-bold'
} as const
const borderByType = {
primary: 'border border-solid border-base-background',
secondary: 'border border-solid border-base-foreground',
transparent: 'border border-solid border-base-foreground',
accent: 'border border-solid border-primary-background'
} as const
return `${baseByType[type]} ${borderByType[type]}`
}
export const getBaseButtonClasses = () => {
return [
'flex items-center justify-center shrink-0',
'outline-hidden rounded-lg cursor-pointer transition-all duration-200',
'disabled:opacity-50 disabled:pointer-events-none'
].join(' ')
}

View File

@@ -1,34 +1,31 @@
<template>
<IconTextButton
v-bind="$attrs"
type="secondary"
:label="computedLabel"
:size="size"
<Button
variant="secondary"
:size
:disabled="isLoading || isInstalling"
@click="installAllPacks"
>
<template #icon>
<i
v-if="hasConflict && !isInstalling && !isLoading"
class="pi pi-exclamation-triangle text-yellow-500"
/>
<DotSpinner
v-else-if="isLoading || isInstalling"
duration="1s"
:size="size === 'sm' ? 12 : 16"
/>
</template>
</IconTextButton>
<i
v-if="hasConflict && !isInstalling && !isLoading"
class="pi pi-exclamation-triangle text-yellow-500"
/>
<DotSpinner
v-else-if="isLoading || isInstalling"
duration="1s"
:size="size === 'sm' ? 12 : 16"
/>
<span>{{ computedLabel }}</span>
</Button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import { t } from '@/i18n'
import Button from '@/components/ui/button/Button.vue'
import type { ButtonVariants } from '@/components/ui/button/button.variants'
import { useDialogService } from '@/services/dialogService'
import type { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
@@ -51,13 +48,14 @@ const {
nodePacks: NodePack[]
isLoading?: boolean
label?: string
size?: ButtonSize
size?: ButtonVariants['size']
hasConflict?: boolean
conflictInfo?: ConflictDetail[]
}>()
const managerStore = useComfyManagerStore()
const { showNodeConflictDialog } = useDialogService()
const { t } = useI18n()
// Check if any of the packs are currently being installed
const isInstalling = computed(() => {

View File

@@ -1,22 +1,23 @@
<template>
<IconTextButton
v-bind="$attrs"
type="transparent"
:label="
nodePacks.length > 1
? $t('manager.uninstallSelected')
: $t('manager.uninstall')
"
:border="true"
:size="size"
class="border-red-500"
<Button
variant="textonly"
:size
class="border border-red-500"
@click="uninstallItems"
/>
>
{{
nodePacks.length > 1
? t('manager.uninstallSelected')
: t('manager.uninstall')
}}
</Button>
</template>
<script setup lang="ts">
import IconTextButton from '@/components/button/IconTextButton.vue'
import type { ButtonSize } from '@/types/buttonTypes'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import type { ButtonVariants } from '@/components/ui/button/button.variants'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
@@ -25,10 +26,11 @@ type NodePack = components['schemas']['Node']
const { nodePacks, size } = defineProps<{
nodePacks: NodePack[]
size?: ButtonSize
size?: ButtonVariants['size']
}>()
const managerStore = useComfyManagerStore()
const { t } = useI18n()
const createPayload = (
uninstallItem: NodePack

View File

@@ -1,27 +1,24 @@
<template>
<IconTextButton
<Button
v-tooltip.top="
hasDisabledUpdatePacks ? $t('manager.disabledNodesWontUpdate') : null
"
v-bind="$attrs"
type="transparent"
:label="$t('manager.updateAll')"
:border="true"
variant="textonly"
class="border"
size="sm"
:disabled="isUpdating"
@click="updateAllPacks"
>
<template v-if="isUpdating" #icon>
<DotSpinner duration="1s" :size="12" />
</template>
</IconTextButton>
<DotSpinner v-if="isUpdating" duration="1s" :size="12" />
<span>{{ $t('manager.updateAll') }}</span>
</Button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.vue'
import Button from '@/components/ui/button/Button.vue'
import type { components } from '@/types/comfyRegistryTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'