Compare commits

...

11 Commits

Author SHA1 Message Date
GitHub Action
a9e53b997c [automated] Apply ESLint and Oxfmt fixes 2026-03-27 13:50:45 +00:00
dante01yoon
3aef8da214 fix(test): drop fragile z-index assertion from tooltip e2e test
getComputedStyle().zIndex returns "auto" in CI because Tailwind CSS
z-[1700] is not resolved to a numeric value in the CI build environment.
2026-03-27 22:47:42 +09:00
dante01yoon
5b2f4b303a test: add e2e regression tests for BaseTooltip migration 2026-03-27 22:47:42 +09:00
dante01yoon
7bf2120cc5 fix: add stroke to tooltip arrow for visibility against same-color backgrounds
Arrow fill and action bar background are identical (rgb(23,23,24)),
making the arrow invisible. Add stroke-node-component-tooltip-border
to match the tooltip border and make the arrow visually distinct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:47:42 +09:00
dante01yoon
8cd39fc24f fix: use z-[1700] bracket syntax for tooltip z-index
z-1700 without brackets was not generating a CSS rule (evaluated as
z-index: auto). Use Tailwind arbitrary value syntax z-[1700] to
ensure the class is generated correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:47:42 +09:00
dante01yoon
1065c3da23 fix: increase tooltip z-index to z-1700 for overlay visibility
Tooltips were hidden behind LiteGraphCanvasSplitterOverlay (z-999)
because TooltipPortal renders to <body> with only z-50. Align with
Popover content z-index (z-1700).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:47:42 +09:00
dante01yoon
30e4b443d1 feat: align BaseTooltip with Figma design, add Storybook, fix as-child nesting
- Update tooltip variants: shadow-interface, leading-none, export FOR_STORIES
- Add keybind and showIcon props to BaseTooltip per Figma design spec
- Add comprehensive Storybook stories for all tooltip variants
- Fix Popover + BaseTooltip as-child nesting conflict in
  JobHistoryActionsMenu and JobFilterActions by moving BaseTooltip
  outside the Popover #button slot

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:47:42 +09:00
Dante
77105602c0 refactor: migrate buildTooltipConfig consumers to BaseTooltip (#10378)
PR 2 in the tooltip migration series. Migrates all `buildTooltipConfig`
consumers from PrimeVue `v-tooltip` directive to the new Reka UI
`BaseTooltip` component.

- Replace `v-tooltip` + `buildTooltipConfig` with `<BaseTooltip>`
wrapper in 8 component files
- Remove computed wrappers that only existed for tooltip config
- Delete `src/composables/useTooltipConfig.ts` (no remaining consumers)
- Update 7 test files: replace tooltip directive stubs with
`BaseTooltip` component stubs

1. **PR 1** (base): Add `BaseTooltip` component using Reka UI
2. **PR 2** (this): Migrate `buildTooltipConfig` consumers (small
variant)
3. PR 3: Migrate remaining PrimeVue `v-tooltip` usages (Style 1)
4. PR 4: Remove PrimeVue tooltip directive registration

- [x] `pnpm typecheck` passes
- [x] `pnpm lint:fix` passes
- [x] `pnpm format` passes
- [x] `pnpm test:unit` passes (529 files, 7022 tests)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10378-refactor-migrate-buildTooltipConfig-consumers-to-BaseTooltip-32a6d73d3650812980c4d8b259fb502c)
by [Unito](https://www.unito.io)
2026-03-27 22:47:42 +09:00
dante01yoon
5e8146267a refactor: align tooltip variants with Figma design system
Replace variant prop (default/small/large) with size prop (small/large)
to match Figma component properties. Remove default variant that was
replicating PrimeVue Aura styling. Extract shared styles into cva base.
2026-03-27 22:47:42 +09:00
dante01yoon
f8e2a81666 refactor: remove Storybook stories from foundation PR 2026-03-27 22:47:42 +09:00
dante01yoon
d429a10ea6 feat: add BaseTooltip component using Reka UI
Add foundational tooltip component with three variants (default, small,
large), four positioning sides, and configurable delay/disabled state.
Wrap App.vue in TooltipProvider for app-wide tooltip support. Include
Storybook stories for all variants, sides, disabled, and long text.
2026-03-27 22:47:42 +09:00
22 changed files with 759 additions and 421 deletions

View File

@@ -0,0 +1,47 @@
import type { Locator, Page } from '@playwright/test'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
function tooltipLocator(page: Page): Locator {
return page.locator('[role="tooltip"]')
}
async function hoverAway(page: Page): Promise<void> {
await page.mouse.move(0, 0)
}
test.describe('BaseTooltip regression', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
test('Queue history button shows tooltip on hover', async ({ comfyPage }) => {
const queueButton = comfyPage.page.getByTestId('queue-overlay-toggle')
await queueButton.hover()
const tooltip = tooltipLocator(comfyPage.page)
await expect(tooltip).toBeVisible()
await hoverAway(comfyPage.page)
await expect(tooltip).not.toBeVisible()
})
test('Toggle properties panel button shows tooltip on hover', async ({
comfyPage
}) => {
const panelButton = comfyPage.page
.getByLabel(/Toggle properties panel/i)
.first()
await panelButton.hover()
const tooltip = tooltipLocator(comfyPage.page)
await expect(tooltip).toBeVisible()
await hoverAway(comfyPage.page)
await expect(tooltip).not.toBeVisible()
})
})

View File

@@ -1,12 +1,15 @@
<template>
<router-view />
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
<TooltipProvider :delay-duration="300" disable-hoverable-content>
<router-view />
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
</TooltipProvider>
</template>
<script setup lang="ts">
import { captureException } from '@sentry/vue'
import BlockUI from 'primevue/blockui'
import { TooltipProvider } from 'reka-ui'
import { computed, onMounted, watch } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'

View File

@@ -119,6 +119,7 @@ function createWrapper({
global: {
plugins: [pinia, i18n],
stubs: {
BaseTooltip: { template: '<slot />' },
SubgraphBreadcrumb: true,
QueueProgressOverlay: true,
QueueInlineProgressSummary: true,
@@ -131,9 +132,6 @@ function createWrapper({
template: '<div />'
},
...stubs
},
directives: {
tooltip: () => {}
}
}
})

View File

@@ -16,22 +16,23 @@
v-if="managerState.shouldShowManagerButtons.value"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
variant="secondary"
:aria-label="t('menu.manageExtensions')"
class="relative"
@click="openCustomNodeManager"
>
<i class="icon-[comfy--extensions-blocks] size-4" />
<span class="not-md:hidden">
{{ t('menu.manageExtensions') }}
</span>
<span
v-if="shouldShowRedDot"
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
/>
</Button>
<BaseTooltip :text="t('menu.manageExtensions')" side="bottom">
<Button
variant="secondary"
:aria-label="t('menu.manageExtensions')"
class="relative"
@click="openCustomNodeManager"
>
<i class="icon-[comfy--extensions-blocks] size-4" />
<span class="not-md:hidden">
{{ t('menu.manageExtensions') }}
</span>
<span
v-if="shouldShowRedDot"
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
/>
</Button>
</BaseTooltip>
</div>
<div ref="actionbarContainerRef" :class="actionbarContainerClass">
@@ -53,35 +54,43 @@
class="shrink-0"
/>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<Button
<BaseTooltip
v-if="isCloud && flags.workflowSharingEnabled"
v-tooltip.bottom="shareTooltipConfig"
variant="secondary"
:aria-label="t('actionbar.shareTooltip')"
@click="() => openShareDialog().catch(toastErrorHandler)"
@pointerenter="prefetchShareDialog"
:text="t('actionbar.shareTooltip')"
side="bottom"
>
<i class="icon-[comfy--send] size-4" />
<span class="not-md:hidden">
{{ t('actionbar.share') }}
</span>
</Button>
<div v-if="!isRightSidePanelOpen" class="relative">
<Button
v-tooltip.bottom="rightSidePanelTooltipConfig"
:class="
cn(
showErrorIndicatorOnPanelButton &&
'outline-1 outline-destructive-background'
)
"
variant="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
:aria-label="t('actionbar.shareTooltip')"
@click="() => openShareDialog().catch(toastErrorHandler)"
@pointerenter="prefetchShareDialog"
>
<i class="icon-[lucide--panel-right] size-4" />
<i class="icon-[comfy--send] size-4" />
<span class="not-md:hidden">
{{ t('actionbar.share') }}
</span>
</Button>
</BaseTooltip>
<div v-if="!isRightSidePanelOpen" class="relative">
<BaseTooltip
:text="t('rightSidePanel.togglePanel')"
side="bottom"
>
<Button
:class="
cn(
showErrorIndicatorOnPanelButton &&
'outline-1 outline-destructive-background'
)
"
variant="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
</BaseTooltip>
<StatusBadge
v-if="showErrorIndicatorOnPanelButton"
variant="dot"
@@ -146,7 +155,7 @@ import Button from '@/components/ui/button/Button.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
@@ -251,12 +260,6 @@ const inlineProgressSummaryTarget = computed(() => {
const shouldHideInlineProgressSummary = computed(
() => isQueueProgressOverlayEnabled.value && isQueueOverlayExpanded.value
)
const customNodesManagerTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.manageExtensions'))
)
const shareTooltipConfig = computed(() =>
buildTooltipConfig(t('actionbar.shareTooltip'))
)
const shouldShowRedDot = computed((): boolean => {
return shouldShowConflictRedDot.value
@@ -278,9 +281,6 @@ const showErrorIndicatorOnPanelButton = computed(
// Right side panel toggle
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)
const rightSidePanelTooltipConfig = computed(() =>
buildTooltipConfig(t('rightSidePanel.togglePanel'))
)
// Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>()

View File

@@ -36,6 +36,7 @@ const renderActionbar = (showRunProgressBar: boolean) => {
global: {
plugins: [pinia, i18n],
stubs: {
BaseTooltip: { template: '<slot />' },
ContextMenu: {
name: 'ContextMenu',
template: '<div />'
@@ -50,9 +51,6 @@ const renderActionbar = (showRunProgressBar: boolean) => {
template: '<button type="button">Run</button>'
},
QueueInlineProgress: true
},
directives: {
tooltip: () => {}
}
}
})

View File

@@ -32,47 +32,49 @@
<Suspense @resolve="comfyRunButtonResolved">
<ComfyRunButton />
</Suspense>
<Button
v-tooltip.bottom="cancelJobTooltipConfig"
variant="destructive"
size="icon"
:disabled="isExecutionIdle"
:aria-label="t('menu.interrupt')"
@click="cancelCurrentJob"
>
<i class="icon-[lucide--x] size-4" />
</Button>
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
variant="secondary"
size="md"
:aria-pressed="
isQueuePanelV2Enabled
? activeSidebarTabId === 'job-history'
: queueOverlayExpanded
"
class="relative px-3"
data-testid="queue-overlay-toggle"
@click="toggleQueueOverlay"
@contextmenu.stop.prevent="showQueueContextMenu"
>
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
</span>
<StatusBadge
v-if="activeJobsCount > 0"
data-testid="active-jobs-indicator"
variant="dot"
class="pointer-events-none absolute -top-0.5 -right-0.5 animate-pulse"
/>
<span class="sr-only">
{{
<BaseTooltip :text="t('menu.interrupt')" side="bottom">
<Button
variant="destructive"
size="icon"
:disabled="isExecutionIdle"
:aria-label="t('menu.interrupt')"
@click="cancelCurrentJob"
>
<i class="icon-[lucide--x] size-4" />
</Button>
</BaseTooltip>
<BaseTooltip :text="queueHistoryTooltipText" side="bottom">
<Button
variant="secondary"
size="md"
:aria-pressed="
isQueuePanelV2Enabled
? t('sideToolbar.queueProgressOverlay.viewJobHistory')
: t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
}}
</span>
</Button>
? activeSidebarTabId === 'job-history'
: queueOverlayExpanded
"
class="relative px-3"
data-testid="queue-overlay-toggle"
@click="toggleQueueOverlay"
@contextmenu.stop.prevent="showQueueContextMenu"
>
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
</span>
<StatusBadge
v-if="activeJobsCount > 0"
data-testid="active-jobs-indicator"
variant="dot"
class="pointer-events-none absolute -top-0.5 -right-0.5 animate-pulse"
/>
<span class="sr-only">
{{
isQueuePanelV2Enabled
? t('sideToolbar.queueProgressOverlay.viewJobHistory')
: t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
}}
</span>
</Button>
</BaseTooltip>
<ContextMenu ref="queueContextMenu" :model="queueContextMenuItems" />
</div>
</Panel>
@@ -108,7 +110,7 @@ import StatusBadge from '@/components/common/StatusBadge.vue'
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
@@ -363,16 +365,11 @@ watch(isDragging, (dragging) => {
}
})
const cancelJobTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.interrupt'))
)
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(
t(
isQueuePanelV2Enabled.value
? 'sideToolbar.queueProgressOverlay.viewJobHistory'
: 'sideToolbar.queueProgressOverlay.expandCollapsedQueue'
)
const queueHistoryTooltipText = computed(() =>
t(
isQueuePanelV2Enabled.value
? 'sideToolbar.queueProgressOverlay.viewJobHistory'
: 'sideToolbar.queueProgressOverlay.expandCollapsedQueue'
)
)
const activeJobsLabel = computed(() => {

View File

@@ -52,11 +52,17 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
useSidebarTabStore: () => mockSidebarTabStore
}))
const BaseTooltipStub = {
template: '<slot />'
}
const mountMenu = () =>
mount(JobHistoryActionsMenu, {
global: {
plugins: [i18n],
directives: { tooltip: () => {} }
stubs: {
BaseTooltip: BaseTooltipStub
}
}
})

View File

@@ -1,91 +1,94 @@
<template>
<div class="flex items-center gap-1">
<Popover :show-arrow="false">
<template #button>
<Button
v-tooltip.top="moreTooltipConfig"
variant="textonly"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
>
<i
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
/>
</Button>
</template>
<template #default="{ close }">
<div class="flex min-w-56 flex-col items-stretch font-inter">
<BaseTooltip :text="t('g.more')" side="top">
<div class="flex items-center gap-1">
<Popover :show-arrow="false">
<template #button>
<Button
data-testid="docked-job-history-action"
class="w-full justify-between text-sm font-light"
variant="textonly"
size="md"
@click="onToggleDockedJobHistory(close)"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
>
<span class="flex items-center gap-2">
<i
class="icon-[lucide--panel-left-close] size-4 text-text-secondary"
/>
<span>{{
t('sideToolbar.queueProgressOverlay.dockedJobHistory')
}}</span>
</span>
<i
v-if="isQueuePanelV2Enabled"
class="icon-[lucide--check] size-4"
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
/>
</Button>
<Button
data-testid="show-run-progress-bar-action"
class="w-full justify-between text-sm font-light"
variant="textonly"
size="md"
@click="onToggleRunProgressBar"
>
<span class="flex items-center gap-2">
<i class="icon-[lucide--hourglass] size-4 text-text-secondary" />
<span>{{
t('sideToolbar.queueProgressOverlay.showRunProgressBar')
}}</span>
</span>
<i
v-if="isRunProgressBarEnabled"
class="icon-[lucide--check] size-4"
/>
</Button>
<!-- TODO: Bug in assets sidebar panel derives assets from history, so despite this not deleting the assets, it still effectively shows to the user as deleted -->
<template v-if="showClearHistoryAction">
<div class="my-1 border-t border-interface-stroke" />
</template>
<template #default="{ close }">
<div class="flex min-w-56 flex-col items-stretch font-inter">
<Button
data-testid="clear-history-action"
class="h-auto min-h-8 w-full items-start justify-start whitespace-normal"
data-testid="docked-job-history-action"
class="w-full justify-between text-sm font-light"
variant="textonly"
size="md"
@click="onClearHistoryFromMenu(close)"
@click="onToggleDockedJobHistory(close)"
>
<i
class="icon-[lucide--trash-2] size-4 shrink-0 self-center text-destructive-background"
/>
<span
class="flex flex-col items-start text-left leading-tight wrap-break-word"
>
<span class="text-sm font-light">
{{ t('sideToolbar.queueProgressOverlay.clearHistory') }}
</span>
<span class="text-xs font-light text-text-secondary">
{{
t(
'sideToolbar.queueProgressOverlay.clearHistoryMenuAssetsNote'
)
}}
</span>
<span class="flex items-center gap-2">
<i
class="icon-[lucide--panel-left-close] size-4 text-text-secondary"
/>
<span>{{
t('sideToolbar.queueProgressOverlay.dockedJobHistory')
}}</span>
</span>
<i
v-if="isQueuePanelV2Enabled"
class="icon-[lucide--check] size-4"
/>
</Button>
</template>
</div>
</template>
</Popover>
</div>
<Button
data-testid="show-run-progress-bar-action"
class="w-full justify-between text-sm font-light"
variant="textonly"
size="md"
@click="onToggleRunProgressBar"
>
<span class="flex items-center gap-2">
<i
class="icon-[lucide--hourglass] size-4 text-text-secondary"
/>
<span>{{
t('sideToolbar.queueProgressOverlay.showRunProgressBar')
}}</span>
</span>
<i
v-if="isRunProgressBarEnabled"
class="icon-[lucide--check] size-4"
/>
</Button>
<!-- TODO: Bug in assets sidebar panel derives assets from history, so despite this not deleting the assets, it still effectively shows to the user as deleted -->
<template v-if="showClearHistoryAction">
<div class="my-1 border-t border-interface-stroke" />
<Button
data-testid="clear-history-action"
class="h-auto min-h-8 w-full items-start justify-start whitespace-normal"
variant="textonly"
size="md"
@click="onClearHistoryFromMenu(close)"
>
<i
class="icon-[lucide--trash-2] size-4 shrink-0 self-center text-destructive-background"
/>
<span
class="flex flex-col items-start text-left leading-tight wrap-break-word"
>
<span class="text-sm font-light">
{{ t('sideToolbar.queueProgressOverlay.clearHistory') }}
</span>
<span class="text-xs font-light text-text-secondary">
{{
t(
'sideToolbar.queueProgressOverlay.clearHistoryMenuAssetsNote'
)
}}
</span>
</span>
</Button>
</template>
</div>
</template>
</Popover>
</div>
</BaseTooltip>
</template>
<script setup lang="ts">
@@ -95,7 +98,7 @@ import { useI18n } from 'vue-i18n'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
@@ -108,7 +111,6 @@ const { t } = useI18n()
const settingStore = useSettingStore()
const sidebarTabStore = useSidebarTabStore()
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
useQueueFeatureFlags()
const showClearHistoryAction = computed(() => !isCloud)

View File

@@ -1,9 +1,8 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import QueueOverlayActive from './QueueOverlayActive.vue'
import * as tooltipConfig from '@/composables/useTooltipConfig'
const i18n = createI18n({
legacy: false,
@@ -27,11 +26,6 @@ const i18n = createI18n({
}
})
const tooltipDirectiveStub = {
mounted: vi.fn(),
updated: vi.fn()
}
const SELECTORS = {
interruptAllButton: 'button[aria-label="Interrupt all running jobs"]',
clearQueuedButton: 'button[aria-label="Clear queued"]',
@@ -43,6 +37,10 @@ const COPY = {
viewAllJobs: 'View all jobs'
}
const BaseTooltipStub = {
template: '<slot />'
}
const mountComponent = (props: Record<string, unknown> = {}) =>
mount(QueueOverlayActive, {
props: {
@@ -58,8 +56,8 @@ const mountComponent = (props: Record<string, unknown> = {}) =>
},
global: {
plugins: [i18n],
directives: {
tooltip: tooltipDirectiveStub
stubs: {
BaseTooltip: BaseTooltipStub
}
}
})
@@ -113,13 +111,4 @@ describe('QueueOverlayActive', () => {
expect(wrapper.find(SELECTORS.interruptAllButton).exists()).toBe(false)
expect(wrapper.find(SELECTORS.clearQueuedButton).exists()).toBe(false)
})
it('builds tooltip configs with translated strings', () => {
const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig')
mountComponent()
expect(spy).toHaveBeenCalledWith('Cancel job')
expect(spy).toHaveBeenCalledWith('Clear queue')
})
})

View File

@@ -42,18 +42,22 @@
t('sideToolbar.queueProgressOverlay.running')
}}</span>
</span>
<Button
<BaseTooltip
v-if="runningCount > 0"
v-tooltip.top="cancelJobTooltip"
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
@click="$emit('interruptAll')"
:text="t('sideToolbar.queueProgressOverlay.cancelJobTooltip')"
side="top"
>
<i
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
/>
</Button>
<Button
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
@click="$emit('interruptAll')"
>
<i
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
/>
</Button>
</BaseTooltip>
</div>
<div class="flex items-center gap-2">
@@ -63,18 +67,22 @@
t('sideToolbar.queueProgressOverlay.queuedSuffix')
}}</span>
</span>
<Button
<BaseTooltip
v-if="queuedCount > 0"
v-tooltip.top="clearQueueTooltip"
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
@click="$emit('clearQueued')"
:text="t('sideToolbar.queueProgressOverlay.clearQueueTooltip')"
side="top"
>
<i
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
/>
</Button>
<Button
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
@click="$emit('clearQueued')"
>
<i
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
/>
</Button>
</BaseTooltip>
</div>
</div>
@@ -91,11 +99,10 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
defineProps<{
totalProgressStyle: Record<string, string>
@@ -115,10 +122,4 @@ defineEmits<{
}>()
const { t } = useI18n()
const cancelJobTooltip = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.cancelJobTooltip'))
)
const clearQueueTooltip = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearQueueTooltip'))
)
</script>

View File

@@ -48,11 +48,9 @@ vi.mock('@/stores/workspace/sidebarTabStore', () => ({
}))
import QueueOverlayHeader from './QueueOverlayHeader.vue'
import * as tooltipConfig from '@/composables/useTooltipConfig'
const tooltipDirectiveStub = {
mounted: vi.fn(),
updated: vi.fn()
const BaseTooltipStub = {
template: '<slot />'
}
const mountHeader = (props = {}) =>
@@ -64,7 +62,9 @@ const mountHeader = (props = {}) =>
},
global: {
plugins: [i18n],
directives: { tooltip: tooltipDirectiveStub }
stubs: {
BaseTooltip: BaseTooltipStub
}
}
})
@@ -105,14 +105,11 @@ describe('QueueOverlayHeader', () => {
})
it('emits clear history from the menu', async () => {
const spy = vi.spyOn(tooltipConfig, 'buildTooltipConfig')
const wrapper = mountHeader()
expect(wrapper.find('button[aria-label="More options"]').exists()).toBe(
true
)
expect(spy).toHaveBeenCalledWith('More')
const clearHistoryButton = wrapper.get(
'[data-testid="clear-history-action"]'

View File

@@ -11,28 +11,31 @@
<span :class="{ 'opacity-50': queuedCount === 0 }">{{
t('sideToolbar.queueProgressOverlay.clearQueueTooltip')
}}</span>
<Button
v-tooltip.top="clearAllJobsTooltip"
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
:disabled="queuedCount === 0"
@click="$emit('clearQueued')"
<BaseTooltip
:text="t('sideToolbar.queueProgressOverlay.clearAllJobsTooltip')"
side="top"
>
<i class="icon-[lucide--list-x] size-4" />
</Button>
<Button
variant="destructive"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
:disabled="queuedCount === 0"
@click="$emit('clearQueued')"
>
<i class="icon-[lucide--list-x] size-4" />
</Button>
</BaseTooltip>
</div>
<JobHistoryActionsMenu @clear-history="$emit('clearHistory')" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import JobHistoryActionsMenu from '@/components/queue/JobHistoryActionsMenu.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
defineProps<{
headerTitle: string
@@ -45,7 +48,4 @@ defineEmits<{
}>()
const { t } = useI18n()
const clearAllJobsTooltip = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearAllJobsTooltip'))
)
</script>

View File

@@ -11,103 +11,119 @@
class="flex shrink-0 items-center gap-2"
:class="{ 'ml-2': !showSearch }"
>
<Popover :show-arrow="false">
<template #button>
<Button
v-tooltip.top="filterTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
>
<i class="icon-[lucide--list-filter] size-4" />
<span
v-if="selectedWorkflowFilter !== 'all'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</Button>
</template>
<template #default="{ close }">
<div class="flex min-w-48 flex-col items-stretch">
<Button
class="w-full justify-between"
variant="textonly"
size="md"
@click="onSelectWorkflowFilter('all', close)"
>
<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" />
<Button
class="w-full justify-between"
variant="textonly"
size="md"
@click="onSelectWorkflowFilter('current', close)"
>
<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>
</template>
</Popover>
<Popover :show-arrow="false">
<template #button>
<Button
v-tooltip.top="sortTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
>
<i class="icon-[lucide--arrow-up-down] size-4" />
<span
v-if="selectedSortMode !== 'mostRecent'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</Button>
</template>
<template #default="{ close }">
<div class="flex min-w-48 flex-col items-stretch">
<template v-for="(mode, index) in jobSortModes" :key="mode">
<BaseTooltip
:text="t('sideToolbar.queueProgressOverlay.filterBy')"
side="top"
>
<div>
<Popover :show-arrow="false">
<template #button>
<Button
class="w-full justify-between"
variant="textonly"
size="md"
@click="onSelectSortMode(mode, close)"
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
>
<span>{{ sortLabel(mode) }}</span>
<i
v-if="selectedSortMode === mode"
class="icon-[lucide--check] size-4 text-text-secondary"
<i class="icon-[lucide--list-filter] size-4" />
<span
v-if="selectedWorkflowFilter !== 'all'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</Button>
<div
v-if="index < jobSortModes.length - 1"
class="mx-2 mt-1 h-px"
/>
</template>
</div>
</template>
</Popover>
<Button
v-if="showAssetsAction"
v-tooltip.top="showAssetsTooltipConfig"
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.showAssetsPanel')"
@click="emit('showAssets')"
<template #default="{ close }">
<div class="flex min-w-48 flex-col items-stretch">
<Button
class="w-full justify-between"
variant="textonly"
size="md"
@click="onSelectWorkflowFilter('all', close)"
>
<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" />
<Button
class="w-full justify-between"
variant="textonly"
size="md"
@click="onSelectWorkflowFilter('current', close)"
>
<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>
</template>
</Popover>
</div>
</BaseTooltip>
<BaseTooltip
:text="t('sideToolbar.queueProgressOverlay.sortBy')"
side="top"
>
<i class="icon-[comfy--image-ai-edit] size-4" />
</Button>
<div>
<Popover :show-arrow="false">
<template #button>
<Button
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
>
<i class="icon-[lucide--arrow-up-down] size-4" />
<span
v-if="selectedSortMode !== 'mostRecent'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</Button>
</template>
<template #default="{ close }">
<div class="flex min-w-48 flex-col items-stretch">
<template v-for="(mode, index) in jobSortModes" :key="mode">
<Button
class="w-full justify-between"
variant="textonly"
size="md"
@click="onSelectSortMode(mode, close)"
>
<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"
/>
</template>
</div>
</template>
</Popover>
</div>
</BaseTooltip>
<BaseTooltip
v-if="showAssetsAction"
:text="t('sideToolbar.queueProgressOverlay.showAssets')"
side="top"
>
<Button
variant="secondary"
size="icon"
:aria-label="t('sideToolbar.queueProgressOverlay.showAssetsPanel')"
@click="emit('showAssets')"
>
<i class="icon-[comfy--image-ai-edit] size-4" />
</Button>
</BaseTooltip>
</div>
</div>
</template>
@@ -121,7 +137,7 @@ import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { jobSortModes } from '@/composables/queue/useJobList'
import type { JobSortMode } from '@/composables/queue/useJobList'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
const {
hideShowAssetsAction = false,
@@ -148,15 +164,6 @@ const emit = defineEmits<{
const { t } = useI18n()
const filterTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.filterBy'))
)
const sortTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.sortBy'))
)
const showAssetsTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.showAssets'))
)
const showAssetsAction = computed(() => !hideShowAssetsAction)
const searchPlaceholderText = computed(
() => searchPlaceholder ?? t('sideToolbar.queueProgressOverlay.searchJobs')

View File

@@ -54,6 +54,10 @@ const i18n = createI18n({
}
})
const BaseTooltipStub = {
template: '<slot />'
}
describe('JobFiltersBar', () => {
it('emits showAssets when the assets icon button is clicked', async () => {
const wrapper = mount(JobFiltersBar, {
@@ -65,7 +69,9 @@ describe('JobFiltersBar', () => {
},
global: {
plugins: [i18n],
directives: { tooltip: () => undefined }
stubs: {
BaseTooltip: BaseTooltipStub
}
}
})
@@ -88,7 +94,9 @@ describe('JobFiltersBar', () => {
},
global: {
plugins: [i18n],
directives: { tooltip: () => undefined }
stubs: {
BaseTooltip: BaseTooltipStub
}
}
})

View File

@@ -124,30 +124,38 @@
key="actions"
class="inline-flex items-center gap-2 pr-1"
>
<Button
<BaseTooltip
v-if="state === 'failed' && computedShowClear"
v-tooltip.top="deleteTooltipConfig"
variant="destructive"
size="icon"
:aria-label="t('g.delete')"
@click.stop="onDeleteClick"
:text="t('g.delete')"
side="top"
>
<i class="icon-[lucide--trash-2] size-4" />
</Button>
<Button
<Button
variant="destructive"
size="icon"
:aria-label="t('g.delete')"
@click.stop="onDeleteClick"
>
<i class="icon-[lucide--trash-2] size-4" />
</Button>
</BaseTooltip>
<BaseTooltip
v-else-if="
state !== 'completed' &&
state !== 'running' &&
computedShowClear
"
v-tooltip.top="cancelTooltipConfig"
variant="destructive"
size="icon"
:aria-label="t('g.cancel')"
@click.stop="onCancelClick"
:text="t('g.cancel')"
side="top"
>
<i class="icon-[lucide--x] size-4" />
</Button>
<Button
variant="destructive"
size="icon"
:aria-label="t('g.cancel')"
@click.stop="onCancelClick"
>
<i class="icon-[lucide--x] size-4" />
</Button>
</BaseTooltip>
<Button
v-else-if="state === 'completed'"
variant="textonly"
@@ -155,32 +163,40 @@
@click.stop="emit('view')"
>{{ t('menuLabels.View') }}</Button
>
<Button
<BaseTooltip
v-if="showMenu !== undefined ? showMenu : true"
v-tooltip.top="moreTooltipConfig"
variant="textonly"
size="icon-sm"
:aria-label="t('g.more')"
@click.stop="emit('menu', $event)"
:text="t('g.more')"
side="top"
>
<i class="icon-[lucide--more-horizontal] size-4" />
</Button>
<Button
variant="textonly"
size="icon-sm"
:aria-label="t('g.more')"
@click.stop="emit('menu', $event)"
>
<i class="icon-[lucide--more-horizontal] size-4" />
</Button>
</BaseTooltip>
</div>
<div v-else-if="state !== 'running'" key="secondary" class="pr-2">
<slot name="secondary">{{ rightText }}</slot>
</div>
</Transition>
<!-- Running job cancel button - always visible -->
<Button
<BaseTooltip
v-if="state === 'running' && computedShowClear"
v-tooltip.top="cancelTooltipConfig"
variant="destructive"
size="icon"
:aria-label="t('g.cancel')"
@click.stop="onCancelClick"
:text="t('g.cancel')"
side="top"
>
<i class="icon-[lucide--x] size-4" />
</Button>
<Button
variant="destructive"
size="icon"
:aria-label="t('g.cancel')"
@click.stop="onCancelClick"
>
<i class="icon-[lucide--x] size-4" />
</Button>
</BaseTooltip>
</div>
</div>
</div>
@@ -195,7 +211,7 @@ import { getHoverPopoverPosition } from '@/components/queue/job/getHoverPopoverP
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
import Button from '@/components/ui/button/Button.vue'
import { useProgressBarBackground } from '@/composables/useProgressBarBackground'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
import type { JobState } from '@/types/queue'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
@@ -247,10 +263,6 @@ const {
progressPercentStyle
} = useProgressBarBackground()
const cancelTooltipConfig = computed(() => buildTooltipConfig(t('g.cancel')))
const deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete')))
const moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))
const rowRef = ref<HTMLDivElement | null>(null)
const showDetails = computed(() => activeDetailsId === jobId)

View File

@@ -0,0 +1,189 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { TooltipProvider } from 'reka-ui'
import BaseTooltip from './BaseTooltip.vue'
import { FOR_STORIES } from './tooltip.variants'
const { sizes, sides } = FOR_STORIES
const meta: Meta<typeof BaseTooltip> = {
title: 'Components/Tooltip/BaseTooltip',
component: BaseTooltip,
tags: ['autodocs'],
decorators: [
(story) => ({
components: { TooltipProvider, story },
template:
'<TooltipProvider :delay-duration="0"><div class="flex items-center justify-center p-20"><story /></div></TooltipProvider>'
})
],
argTypes: {
size: {
control: { type: 'select' },
options: sizes
},
side: {
control: { type: 'select' },
options: sides
},
text: { control: 'text' },
keybind: { control: 'text' },
showIcon: { control: 'boolean' },
disabled: { control: 'boolean' }
},
args: {
size: 'small',
side: 'top',
text: 'Tooltip text',
disabled: false
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Small: Story = {
render: (args) => ({
components: { BaseTooltip },
setup: () => ({ args }),
template: `
<BaseTooltip v-bind="args">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
Hover me
</button>
</BaseTooltip>`
})
}
export const Large: Story = {
args: {
size: 'large',
text: 'This is a longer tooltip that can wrap to multiple lines for detailed descriptions of node functionality.'
},
render: (args) => ({
components: { BaseTooltip },
setup: () => ({ args }),
template: `
<BaseTooltip v-bind="args">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
Hover me
</button>
</BaseTooltip>`
})
}
export const WithKeybind: Story = {
args: {
text: 'Undo',
keybind: 'Ctrl+Z'
},
render: (args) => ({
components: { BaseTooltip },
setup: () => ({ args }),
template: `
<BaseTooltip v-bind="args">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
Hover me
</button>
</BaseTooltip>`
})
}
export const WithIcon: Story = {
args: {
text: 'More options',
showIcon: true,
size: 'small'
},
render: (args) => ({
components: { BaseTooltip },
setup: () => ({ args }),
template: `
<BaseTooltip v-bind="args">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
Hover me
</button>
</BaseTooltip>`
})
}
export const WithKeybindAndIcon: Story = {
args: {
text: 'Save',
keybind: 'Ctrl+S',
showIcon: true,
size: 'small'
},
render: (args) => ({
components: { BaseTooltip },
setup: () => ({ args }),
template: `
<BaseTooltip v-bind="args">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
Hover me
</button>
</BaseTooltip>`
})
}
export const Disabled: Story = {
args: {
text: 'This tooltip is disabled',
disabled: true
},
render: (args) => ({
components: { BaseTooltip },
setup: () => ({ args }),
template: `
<BaseTooltip v-bind="args">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">
Hover me (disabled tooltip)
</button>
</BaseTooltip>`
})
}
export const AllSides: Story = {
render: () => ({
components: { BaseTooltip },
template: `
<div class="grid grid-cols-2 gap-12">
${sides
.map(
(side) => `
<BaseTooltip text="${side} tooltip" side="${side}" size="small">
<button class="w-full rounded-lg bg-secondary-background px-4 py-2 text-sm">
${side}
</button>
</BaseTooltip>`
)
.join('\n')}
</div>`
})
}
export const AllVariants: Story = {
render: () => ({
components: { BaseTooltip },
template: `
<div class="flex flex-col gap-12">
<div class="flex flex-wrap items-center gap-8">
<BaseTooltip text="Small tooltip" size="small" side="bottom">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">Small</button>
</BaseTooltip>
<BaseTooltip text="This is a large tooltip with longer text that wraps across multiple lines." size="large" side="bottom">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">Large</button>
</BaseTooltip>
<BaseTooltip text="Undo" keybind="Ctrl+Z" size="small" side="bottom">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">With Keybind</button>
</BaseTooltip>
<BaseTooltip text="More options" :show-icon="true" size="small" side="bottom">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">With Icon</button>
</BaseTooltip>
<BaseTooltip text="Save" keybind="Ctrl+S" :show-icon="true" size="small" side="bottom">
<button class="rounded-lg bg-secondary-background px-4 py-2 text-sm">All Features</button>
</BaseTooltip>
</div>
</div>`
})
}

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import {
TooltipArrow,
TooltipContent,
TooltipPortal,
TooltipRoot,
TooltipTrigger
} from 'reka-ui'
import type { TooltipVariants } from '@/components/ui/tooltip/tooltip.variants'
import { tooltipVariants } from '@/components/ui/tooltip/tooltip.variants'
import { cn } from '@/utils/tailwindUtil'
const {
text = '',
side = 'top',
sideOffset = 4,
size = 'small',
keybind,
showIcon = false,
delayDuration,
disabled = false,
class: className
} = defineProps<{
text?: string
side?: 'top' | 'bottom' | 'left' | 'right'
sideOffset?: number
size?: NonNullable<TooltipVariants['size']>
keybind?: string
showIcon?: boolean
delayDuration?: number
disabled?: boolean
class?: HTMLAttributes['class']
}>()
</script>
<template>
<TooltipRoot :delay-duration="delayDuration" :disabled="disabled || !text">
<TooltipTrigger as-child>
<slot />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent
:side="side"
:side-offset="sideOffset"
:class="cn(tooltipVariants({ size }), className)"
>
<span
v-if="keybind || (showIcon && size === 'small')"
class="inline-flex items-center gap-2"
>
<span>{{ text }}</span>
<i
v-if="showIcon && size === 'small'"
class="icon-[lucide--chevron-right] size-4 shrink-0"
/>
<span
v-if="keybind"
class="shrink-0 rounded-sm bg-interface-menu-keybind-surface-default px-1 text-xs leading-none"
>
{{ keybind }}
</span>
</span>
<template v-else>{{ text }}</template>
<TooltipArrow
:width="8"
:height="5"
class="fill-node-component-tooltip-surface stroke-node-component-tooltip-border"
/>
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
</template>

View File

@@ -0,0 +1,24 @@
import type { VariantProps } from 'cva'
import { cva } from 'cva'
export const tooltipVariants = cva({
base: 'z-[1700] select-none border border-node-component-tooltip-border bg-node-component-tooltip-surface px-4 py-2 text-node-component-tooltip shadow-interface',
variants: {
size: {
small: 'rounded-lg text-xs leading-none',
large: 'max-w-75 rounded-sm text-sm/tight font-normal'
}
},
defaultVariants: {
size: 'small'
}
})
export type TooltipVariants = VariantProps<typeof tooltipVariants>
const sizes = ['small', 'large'] as const satisfies Array<
TooltipVariants['size']
>
const sides = ['top', 'bottom', 'left', 'right'] as const
export const FOR_STORIES = { sizes, sides } as const

View File

@@ -1,18 +0,0 @@
/**
* Build a tooltip configuration object compatible with v-tooltip.
* Consumers pass the translated text value.
*/
export const buildTooltipConfig = (value: string) => ({
value,
showDelay: 300,
hideDelay: 0,
pt: {
text: {
class:
'border-node-component-tooltip-border bg-node-component-tooltip-surface text-node-component-tooltip border rounded-md px-2 py-1 text-xs leading-none shadow-none'
},
arrow: {
class: 'border-t-node-component-tooltip-border'
}
}
})

View File

@@ -46,12 +46,11 @@
onThumbnailError($event.name, $event.previewUrl)
"
/>
<span
v-tooltip="buildTooltipConfig(item.name)"
class="truncate text-xs text-base-foreground"
>
{{ item.name }}
</span>
<BaseTooltip :text="item.name">
<span class="truncate text-xs text-base-foreground">
{{ item.name }}
</span>
</BaseTooltip>
<span
v-if="item.in_library"
class="ml-auto shrink-0 text-xs text-muted-foreground"
@@ -77,7 +76,7 @@ import ShareAssetThumbnail from '@/platform/workflow/sharing/components/ShareAss
import { useAssetSections } from '@/platform/workflow/sharing/composables/useAssetSections'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import BaseTooltip from '@/components/ui/tooltip/BaseTooltip.vue'
const { items } = defineProps<{
items: AssetInfo[]

View File

@@ -53,7 +53,10 @@ describe(ShareAssetWarningBox, () => {
...props
},
global: {
plugins: [i18n]
plugins: [i18n],
stubs: {
BaseTooltip: { template: '<slot />' }
}
}
})
}

View File

@@ -187,6 +187,7 @@ describe('ShareWorkflowDialogContent', () => {
global: {
plugins: [i18n],
stubs: {
BaseTooltip: { template: '<slot />' },
ComfyHubPublishIntroPanel: {
template:
'<section data-testid="publish-intro"><button data-testid="publish-intro-cta" @click="$props.onCreateProfile()">Start publishing</button></section>',