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 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' import MoreButton from './MoreButton.vue'
const meta: Meta<typeof MoreButton> = { const meta: Meta<typeof MoreButton> = {
@@ -17,30 +17,26 @@ type Story = StoryObj<typeof MoreButton>
export const Basic: Story = { export const Basic: Story = {
render: () => ({ render: () => ({
components: { MoreButton, IconTextButton }, components: { MoreButton, Button },
template: ` template: `
<div style="height: 200px; display: flex; align-items: center; justify-content: center;"> <div style="height: 200px; display: flex; align-items: center; justify-content: center;">
<MoreButton> <MoreButton>
<template #default="{ close }"> <template #default="{ close }">
<IconTextButton <Button
type="transparent" variant="textonly"
label="Settings"
@click="() => { close() }" @click="() => { close() }"
> >
<template #icon> <i class="icon-[lucide--download] size-4" />
<i class="icon-[lucide--download] size-4" /> <span>Settings</span>
</template> </Button>
</IconTextButton>
<IconTextButton <Button
type="transparent" variant="textonly"
label="Profile"
@click="() => { close() }" @click="() => { close() }"
> >
<template #icon> <i class="icon-[lucide--scroll-text] size-4" />
<i class="icon-[lucide--scroll-text] size-4" /> <span>Profile</span>
</template> </Button>
</IconTextButton>
</template> </template>
</MoreButton> </MoreButton>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,15 +37,10 @@
<template #header> <template #header>
<!-- Job Detail View Header --> <!-- Job Detail View Header -->
<div v-if="isInFolderView" class="px-2 2xl:px-4"> <div v-if="isInFolderView" class="px-2 2xl:px-4">
<IconTextButton <Button variant="secondary" size="lg" @click="exitFolderView">
:label="$t('sideToolbar.backToAssets')" <i class="icon-[lucide--arrow-left] size-4" />
type="secondary" <span>{{ $t('sideToolbar.backToAssets') }}</span>
@click="exitFolderView" </Button>
>
<template #icon>
<i class="icon-[lucide--arrow-left] size-4" />
</template>
</IconTextButton>
</div> </div>
<!-- Filter Bar --> <!-- Filter Bar -->
@@ -128,7 +123,7 @@
</Button> </Button>
</div> </div>
</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"> <template v-if="isCompact">
<!-- Compact mode: Icon only --> <!-- Compact mode: Icon only -->
<Button <Button
@@ -144,27 +139,18 @@
</template> </template>
<template v-else> <template v-else>
<!-- Normal mode: Icon + Text --> <!-- Normal mode: Icon + Text -->
<IconTextButton <Button
v-if="shouldShowDeleteButton" v-if="shouldShowDeleteButton"
:label="$t('mediaAsset.selection.deleteSelected')" variant="secondary"
type="secondary"
icon-position="right"
@click="handleDeleteSelected" @click="handleDeleteSelected"
> >
<template #icon> <span>{{ $t('mediaAsset.selection.deleteSelected') }}</span>
<i class="icon-[lucide--trash-2] size-4" /> <i class="icon-[lucide--trash-2] size-4" />
</template> </Button>
</IconTextButton> <Button variant="secondary" @click="handleDownloadSelected">
<IconTextButton <span>{{ $t('mediaAsset.selection.downloadSelected') }}</span>
:label="$t('mediaAsset.selection.downloadSelected')" <i class="icon-[lucide--download] size-4" />
type="secondary" </Button>
icon-position="right"
@click="handleDownloadSelected"
>
<template #icon>
<i class="icon-[lucide--download] size-4" />
</template>
</IconTextButton>
</template> </template>
</div> </div>
</div> </div>
@@ -184,7 +170,6 @@ import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VirtualGrid from '@/components/common/VirtualGrid.vue' import VirtualGrid from '@/components/common/VirtualGrid.vue'
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.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 Button from './Button.vue'
import { FOR_STORIES } from '@/components/ui/button/button.variants' import { FOR_STORIES } from '@/components/ui/button/button.variants'
interface ButtonPropsAndStoryArgs extends ComponentPropsAndSlots<
typeof Button
> {
icon?: 'left' | 'right'
}
const { variants, sizes } = FOR_STORIES const { variants, sizes } = FOR_STORIES
const meta: Meta<typeof Button> = { const meta: Meta<ButtonPropsAndStoryArgs> = {
title: 'Components/Button/Button', title: 'Components/Button/Button',
component: Button, component: Button,
tags: ['autodocs'], tags: ['autodocs'],
@@ -22,13 +32,19 @@ const meta: Meta<typeof Button> = {
as: { defaultValue: 'button' }, as: { defaultValue: 'button' },
asChild: { defaultValue: false }, asChild: { defaultValue: false },
default: { default: {
control: { type: 'text' },
defaultValue: 'Button' defaultValue: 'Button'
},
icon: {
control: { type: 'select' },
options: [undefined, 'left', 'right']
} }
}, },
args: { args: {
variant: 'secondary', variant: 'secondary',
size: 'md', size: 'md',
default: 'Button' default: 'Button',
icon: undefined
} }
} }
@@ -36,10 +52,18 @@ export default meta
type Story = StoryObj<typeof meta> type Story = StoryObj<typeof meta>
export const SingleButton: Story = { export const SingleButton: Story = {
args: { render: (args) => ({
variant: 'primary', components: { Button },
size: 'lg' 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() { function generateVariants() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' 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 { useModelUpload } from '@/platform/assets/composables/useModelUpload'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
@@ -40,16 +40,15 @@ const singleFilterOption = computed(() => filterOptions.length === 1)
> >
{{ option.name }} {{ option.name }}
</button> </button>
<IconTextButton <Button
v-if="isUploadButtonEnabled && singleFilterOption" v-if="isUploadButtonEnabled && singleFilterOption"
:label="$t('g.import')" class="ml-auto"
class="ml-auto text-base-foreground hover:bg-node-component-widget-input-surface" size="md"
type="transparent" variant="textonly"
@click="showUploadDialog" @click="showUploadDialog"
> >
<template #icon> <i class="icon-[lucide--folder-input]" />
<i class="icon-[lucide--folder-input]" /> <span>{{ $t('g.import') }}</span>
</template> </Button>
</IconTextButton>
</div> </div>
</template> </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> <template>
<IconTextButton <Button
v-bind="$attrs" variant="secondary"
type="secondary" :size
:label="computedLabel"
:size="size"
:disabled="isLoading || isInstalling" :disabled="isLoading || isInstalling"
@click="installAllPacks" @click="installAllPacks"
> >
<template #icon> <i
<i v-if="hasConflict && !isInstalling && !isLoading"
v-if="hasConflict && !isInstalling && !isLoading" class="pi pi-exclamation-triangle text-yellow-500"
class="pi pi-exclamation-triangle text-yellow-500" />
/> <DotSpinner
<DotSpinner v-else-if="isLoading || isInstalling"
v-else-if="isLoading || isInstalling" duration="1s"
duration="1s" :size="size === 'sm' ? 12 : 16"
:size="size === 'sm' ? 12 : 16" />
/> <span>{{ computedLabel }}</span>
</template> </Button>
</IconTextButton>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import DotSpinner from '@/components/common/DotSpinner.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 { useDialogService } from '@/services/dialogService'
import type { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes' import type { components } from '@/types/comfyRegistryTypes'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection' import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore' import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
@@ -51,13 +48,14 @@ const {
nodePacks: NodePack[] nodePacks: NodePack[]
isLoading?: boolean isLoading?: boolean
label?: string label?: string
size?: ButtonSize size?: ButtonVariants['size']
hasConflict?: boolean hasConflict?: boolean
conflictInfo?: ConflictDetail[] conflictInfo?: ConflictDetail[]
}>() }>()
const managerStore = useComfyManagerStore() const managerStore = useComfyManagerStore()
const { showNodeConflictDialog } = useDialogService() const { showNodeConflictDialog } = useDialogService()
const { t } = useI18n()
// Check if any of the packs are currently being installed // Check if any of the packs are currently being installed
const isInstalling = computed(() => { const isInstalling = computed(() => {

View File

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

View File

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