Compare commits

..

5 Commits

Author SHA1 Message Date
dante01yoon
7faf9eafde fix: resolve empty combo options for promoted subgraph widgets
PromotedWidgetView never registers in widgetValueStore, so reading
options from the store always returned {}. Read options directly from
the widget for promoted views instead.

Fixes #9012

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:01:26 +09:00
Christian Byrne
e2cb3560cc test: fix flaky no_workflow.webp screenshot test (#9458)
## Summary

Fix flaky `no_workflow.webp` screenshot test by waiting for async upload
and `/view` response before asserting.

## Changes

- **What**: In `loadWorkflowInMedia.spec.ts`, added `waitForUpload:
true` for `no_workflow.webp` and a `waitForResponse` call for the
`/view` endpoint. This ensures the error toast (from the 500 response)
is consistently visible before the screenshot assertion.

## Review Focus

The fix is scoped to `no_workflow.webp` only (via a `filesWithUpload`
Set) since it's the only test file that triggers an upload + `/view`
call. Other media files embed workflows and don't hit this path.

Fixes #9450

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9458-test-fix-flaky-no_workflow-webp-screenshot-test-31b6d73d365081b88deaee91769baec1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-05 18:27:43 -08:00
Alexander Brown
9ae85068eb feat: Transparent background for the Image and Video Previews (#9455)
## Summary

Less jarring appearance, especially with different aspect ratios or
Alpha channels.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9455-feat-Transparent-background-for-the-Image-and-Video-Previews-31b6d73d3650819eaa82def10e66da21)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-05 18:06:53 -08:00
Comfy Org PR Bot
23bb5f2afa 1.41.13 (#9452)
Patch version increment to 1.41.13

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9452-1-41-13-31b6d73d3650819db118e6455c555bce)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-05 18:05:01 -08:00
Christian Byrne
ef4e4a69d5 fix: enable enforce-consistent-class-order tailwind lint rule (#9428)
## Summary

Enable `better-tailwindcss/enforce-consistent-class-order` lint rule and
auto-fix all 1027 violations across 263 files. Stacked on #9427.

## Changes

- **What**: Sort Tailwind classes into consistent order via `eslint
--fix`
- Enable `enforce-consistent-class-order` as `'error'` in eslint config
- Purely cosmetic reordering — no behavioral or visual changes

## Review Focus

Mechanical auto-fix PR — all changes are class reordering only. This is
the largest diff but lowest risk since it changes no class names, only
their order.

**Stack:** #9417#9427 → **this PR**

Fixes #9300 (partial — 3 of 3 rules)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9428-fix-enable-enforce-consistent-class-order-tailwind-lint-rule-31a6d73d3650811c9065f5178ba3e724)
by [Unito](https://www.unito.io)
2026-03-05 17:24:34 -08:00
323 changed files with 1242 additions and 2858 deletions

View File

@@ -25,11 +25,9 @@ import {
import { Topbar } from './components/Topbar'
import { CanvasHelper } from './helpers/CanvasHelper'
import { PerformanceHelper } from './helpers/PerformanceHelper'
import { QueueHelper } from './helpers/QueueHelper'
import { ClipboardHelper } from './helpers/ClipboardHelper'
import { CommandHelper } from './helpers/CommandHelper'
import { DragDropHelper } from './helpers/DragDropHelper'
import { FeatureFlagHelper } from './helpers/FeatureFlagHelper'
import { KeyboardHelper } from './helpers/KeyboardHelper'
import { NodeOperationsHelper } from './helpers/NodeOperationsHelper'
import { SettingsHelper } from './helpers/SettingsHelper'
@@ -186,11 +184,9 @@ export class ComfyPage {
public readonly contextMenu: ContextMenu
public readonly toast: ToastHelper
public readonly dragDrop: DragDropHelper
public readonly featureFlags: FeatureFlagHelper
public readonly command: CommandHelper
public readonly bottomPanel: BottomPanel
public readonly perf: PerformanceHelper
public readonly queue: QueueHelper
/** Worker index to test user ID */
public readonly userIds: string[] = []
@@ -233,11 +229,9 @@ export class ComfyPage {
this.contextMenu = new ContextMenu(page)
this.toast = new ToastHelper(page)
this.dragDrop = new DragDropHelper(page, this.assetPath.bind(this))
this.featureFlags = new FeatureFlagHelper(page)
this.command = new CommandHelper(page)
this.bottomPanel = new BottomPanel(page)
this.perf = new PerformanceHelper(page)
this.queue = new QueueHelper(page)
}
get visibleToasts() {

View File

@@ -1,47 +0,0 @@
import type { Page } from '@playwright/test'
export class FeatureFlagHelper {
constructor(private readonly page: Page) {}
/**
* Set feature flags via localStorage. Uses the `ff:` prefix
* that devFeatureFlagOverride.ts reads in dev mode.
* Call BEFORE comfyPage.setup() for flags needed at init time,
* or use page.evaluate() for runtime changes.
*/
async setFlags(flags: Record<string, unknown>): Promise<void> {
await this.page.evaluate((flagMap: Record<string, unknown>) => {
for (const [key, value] of Object.entries(flagMap)) {
localStorage.setItem(`ff:${key}`, JSON.stringify(value))
}
}, flags)
}
async setFlag(name: string, value: unknown): Promise<void> {
await this.setFlags({ [name]: value })
}
async clearFlags(): Promise<void> {
await this.page.evaluate(() => {
const keysToRemove: string[] = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key?.startsWith('ff:')) keysToRemove.push(key)
}
keysToRemove.forEach((k) => localStorage.removeItem(k))
})
}
/**
* Mock server feature flags via route interception on /api/features.
*/
async mockServerFeatures(features: Record<string, unknown>): Promise<void> {
await this.page.route('**/api/features', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(features)
})
)
}
}

View File

@@ -1,70 +0,0 @@
import type { Page } from '@playwright/test'
export class QueueHelper {
constructor(private readonly page: Page) {}
/**
* Mock the /api/queue endpoint to return specific queue state.
*/
async mockQueueState(
running: number = 0,
pending: number = 0
): Promise<void> {
await this.page.route('**/api/queue', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
queue_running: Array.from({ length: running }, (_, i) => [
i,
`running-${i}`,
{},
{},
[]
]),
queue_pending: Array.from({ length: pending }, (_, i) => [
i,
`pending-${i}`,
{},
{},
[]
])
})
})
)
}
/**
* Mock the /api/history endpoint with completed/failed job entries.
*/
async mockHistory(
jobs: Array<{ promptId: string; status: 'success' | 'error' }>
): Promise<void> {
const history: Record<string, unknown> = {}
for (const job of jobs) {
history[job.promptId] = {
prompt: [0, job.promptId, {}, {}, []],
outputs: {},
status: {
status_str: job.status === 'success' ? 'success' : 'error',
completed: job.status === 'success'
}
}
}
await this.page.route('**/api/history**', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(history)
})
)
}
/**
* Clear all route mocks set by this helper.
*/
async clearMocks(): Promise<void> {
await this.page.unroute('**/api/queue')
await this.page.unroute('**/api/history**')
}
}

View File

@@ -1,116 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Bottom Panel Logs', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('should open bottom panel via toggle button', async ({ comfyPage }) => {
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).not.toBeVisible()
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
})
test('should show Logs tab when terminal panel opens', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasLogs = await logsTab.isVisible().catch(() => false)
if (hasLogs) {
await expect(logsTab).toBeVisible()
}
})
test('should close bottom panel via toggle button', async ({ comfyPage }) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should switch between shortcuts and terminal panels', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
await expect(
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
).toBeVisible()
await bottomPanel.toggleButton.click()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasTerminalTabs = await logsTab.isVisible().catch(() => false)
if (hasTerminalTabs) {
await expect(logsTab).toBeVisible()
await expect(
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
).not.toBeVisible()
}
})
test('should persist Logs tab content in bottom panel', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasLogs = await logsTab.isVisible().catch(() => false)
if (hasLogs) {
await logsTab.click()
const xtermContainer = bottomPanel.root.locator('.xterm')
const hasXterm = await xtermContainer.isVisible().catch(() => false)
if (hasXterm) {
await expect(xtermContainer).toBeVisible()
}
}
})
test('should render xterm container in terminal panel', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasLogs = await logsTab.isVisible().catch(() => false)
if (hasLogs) {
await logsTab.click()
const xtermScreen = bottomPanel.root.locator('.xterm, .xterm-screen')
const hasXterm = await xtermScreen
.first()
.isVisible()
.catch(() => false)
if (hasXterm) {
await expect(xtermScreen.first()).toBeVisible()
}
}
})
})

View File

@@ -1,88 +0,0 @@
import type { Page } from '@playwright/test'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('Error overlay See Errors flow', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
async function triggerExecutionError(comfyPage: {
canvasOps: { disconnectEdge: () => Promise<void> }
page: Page
command: { executeCommand: (cmd: string) => Promise<void> }
}) {
await comfyPage.canvasOps.disconnectEdge()
await comfyPage.page.keyboard.press('Escape')
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
}
test('Error overlay appears on execution error', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="error-overlay"]')
).toBeVisible()
})
test('Error overlay shows error message', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await expect(overlay).toHaveText(/\S/)
})
test('"See Errors" opens right side panel', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /See Errors/i }).click()
await expect(comfyPage.page.getByTestId('properties-panel')).toBeVisible()
})
test('"See Errors" dismisses the overlay', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /See Errors/i }).click()
await expect(overlay).not.toBeVisible()
})
test('"Dismiss" closes overlay without opening panel', async ({
comfyPage
}) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /Dismiss/i }).click()
await expect(overlay).not.toBeVisible()
await expect(
comfyPage.page.getByTestId('properties-panel')
).not.toBeVisible()
})
test('Close button (X) dismisses overlay', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /close/i }).click()
await expect(overlay).not.toBeVisible()
})
})

View File

@@ -1,148 +0,0 @@
import type { Page } from '@playwright/test'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
async function triggerExecutionError(comfyPage: {
canvasOps: { disconnectEdge: () => Promise<void> }
page: Page
command: { executeCommand: (cmd: string) => Promise<void> }
}) {
await comfyPage.canvasOps.disconnectEdge()
await comfyPage.page.keyboard.press('Escape')
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
}
test.describe('Errors tab in right side panel', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
await comfyPage.setup()
})
test('Errors tab appears after execution error', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
// Dismiss the error overlay
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /Dismiss/i }).click()
await comfyPage.actionbar.propertiesButton.click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
await expect(
propertiesPanel.root.getByRole('tab', { name: 'Errors' })
).toBeVisible()
})
test('Error card shows locate button', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /See Errors/i }).click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
const locateButton = propertiesPanel.root.locator(
'button .icon-\\[lucide--locate\\]'
)
await expect(locateButton.first()).toBeVisible()
})
test('Clicking locate button focuses canvas', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /See Errors/i }).click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
const locateButton = propertiesPanel.root
.locator('button')
.filter({ has: comfyPage.page.locator('.icon-\\[lucide--locate\\]') })
.first()
await locateButton.click()
await expect(comfyPage.canvas).toBeVisible()
})
test('Collapse all button collapses error groups', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /See Errors/i }).click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
const collapseButton = propertiesPanel.root.getByRole('button', {
name: 'Collapse all'
})
// The collapse toggle only appears when there are multiple groups.
// If only one group exists, this test verifies the button is not shown.
const count = await collapseButton.count()
if (count > 0) {
await collapseButton.click()
const expandButton = propertiesPanel.root.getByRole('button', {
name: 'Expand all'
})
await expect(expandButton).toBeVisible()
}
})
test('Search filters errors', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /See Errors/i }).click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
// Search for a term that won't match any error
await propertiesPanel.searchBox.fill('zzz_nonexistent_zzz')
await expect(
propertiesPanel.root.locator('.icon-\\[lucide--locate\\]')
).toHaveCount(0)
// Clear the search to restore results
await propertiesPanel.searchBox.fill('')
await expect(
propertiesPanel.root.locator('.icon-\\[lucide--locate\\]').first()
).toBeVisible()
})
test('Errors tab shows error message text', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const overlay = comfyPage.page.locator('[data-testid="error-overlay"]')
await expect(overlay).toBeVisible()
await overlay.getByRole('button', { name: /See Errors/i }).click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
// Error cards contain <p> elements with error messages
const errorMessage = propertiesPanel.root
.locator('.whitespace-pre-wrap')
.first()
await expect(errorMessage).toBeVisible()
await expect(errorMessage).not.toHaveText('')
})
})

View File

@@ -1,75 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.describe('Floating Canvas Menus', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.setup()
})
test('Floating menu is visible on canvas', async ({ comfyPage }) => {
const toggleLinkButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleLinkVisibilityButton
)
const toggleMinimapButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(toggleLinkButton).toBeVisible()
await expect(toggleMinimapButton).toBeVisible()
})
test('Link visibility toggle button is present', async ({ comfyPage }) => {
const button = comfyPage.page.getByTestId(
TestIds.canvas.toggleLinkVisibilityButton
)
await expect(button).toBeVisible()
await expect(button).toBeEnabled()
})
test('Clicking link toggle changes link render mode', async ({
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.LinkRenderMode', 2)
const button = comfyPage.page.getByTestId(
TestIds.canvas.toggleLinkVisibilityButton
)
await button.click()
await comfyPage.nextFrame()
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
return window.LiteGraph!.HIDDEN_LINK
})
expect(await comfyPage.settings.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
})
test('Zoom controls button shows percentage', async ({ comfyPage }) => {
const zoomButton = comfyPage.page.getByTestId('zoom-controls-button')
await expect(zoomButton).toBeVisible()
await expect(zoomButton).toContainText('%')
})
test('Fit view button is present and clickable', async ({ comfyPage }) => {
const fitViewButton = comfyPage.page
.locator('button')
.filter({ has: comfyPage.page.locator('.icon-\\[lucide--focus\\]') })
await expect(fitViewButton).toBeVisible()
await fitViewButton.click()
await comfyPage.nextFrame()
})
test('Zoom controls popup opens on click', async ({ comfyPage }) => {
const zoomButton = comfyPage.page.getByTestId('zoom-controls-button')
await zoomButton.click()
await comfyPage.nextFrame()
const zoomModal = comfyPage.page.getByText('Zoom To Fit')
await expect(zoomModal).toBeVisible()
})
})

View File

@@ -1,65 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('Focus Mode', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
test('Focus mode hides UI chrome', async ({ comfyPage }) => {
await expect(comfyPage.menu.sideToolbar).toBeVisible()
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
})
test('Focus mode restores UI chrome', async ({ comfyPage }) => {
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
await comfyPage.setFocusMode(false)
await expect(comfyPage.menu.sideToolbar).toBeVisible()
})
test('Toggle focus mode command works', async ({ comfyPage }) => {
await expect(comfyPage.menu.sideToolbar).toBeVisible()
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.menu.sideToolbar).toBeVisible()
})
test('Focus mode hides topbar', async ({ comfyPage }) => {
const topMenu = comfyPage.page.locator('.comfy-menu-button-wrapper')
await expect(topMenu).toBeVisible()
await comfyPage.setFocusMode(true)
await expect(topMenu).not.toBeVisible()
})
test('Canvas remains visible in focus mode', async ({ comfyPage }) => {
await comfyPage.setFocusMode(true)
await expect(comfyPage.canvas).toBeVisible()
})
test('Focus mode can be toggled multiple times', async ({ comfyPage }) => {
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
await comfyPage.setFocusMode(false)
await expect(comfyPage.menu.sideToolbar).toBeVisible()
await comfyPage.setFocusMode(true)
await expect(comfyPage.menu.sideToolbar).not.toBeVisible()
})
})

View File

@@ -1,96 +0,0 @@
import type { Locator } from '@playwright/test'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('Job History Actions', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
async function openMoreOptionsPopover(comfyPage: {
page: { locator(sel: string): Locator }
}) {
const moreButton = comfyPage.page
.locator('.icon-\\[lucide--more-horizontal\\]')
.first()
await moreButton.click()
}
test('More options popover opens', async ({ comfyPage }) => {
await openMoreOptionsPopover(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="docked-job-history-action"]')
).toBeVisible()
})
test('Docked job history action is visible with text', async ({
comfyPage
}) => {
await openMoreOptionsPopover(comfyPage)
const action = comfyPage.page.locator(
'[data-testid="docked-job-history-action"]'
)
await expect(action).toBeVisible()
await expect(action).not.toBeEmpty()
})
test('Show run progress bar action is visible', async ({ comfyPage }) => {
await openMoreOptionsPopover(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="show-run-progress-bar-action"]')
).toBeVisible()
})
test('Clear history action is visible', async ({ comfyPage }) => {
await openMoreOptionsPopover(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="clear-history-action"]')
).toBeVisible()
})
test('Clicking docked job history closes popover', async ({ comfyPage }) => {
await openMoreOptionsPopover(comfyPage)
const action = comfyPage.page.locator(
'[data-testid="docked-job-history-action"]'
)
await expect(action).toBeVisible()
await action.click()
await expect(action).not.toBeVisible()
})
test('Clicking show run progress bar toggles check icon', async ({
comfyPage
}) => {
await openMoreOptionsPopover(comfyPage)
const action = comfyPage.page.locator(
'[data-testid="show-run-progress-bar-action"]'
)
const checkIcon = action.locator('.icon-\\[lucide--check\\]')
const hadCheck = await checkIcon.isVisible()
await action.click()
await openMoreOptionsPopover(comfyPage)
const checkIconAfter = comfyPage.page
.locator('[data-testid="show-run-progress-bar-action"]')
.locator('.icon-\\[lucide--check\\]')
if (hadCheck) {
await expect(checkIconAfter).not.toBeVisible()
} else {
await expect(checkIconAfter).toBeVisible()
}
})
})

View File

@@ -1,87 +0,0 @@
import type { Page } from '@playwright/test'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
import type { WorkspaceStore } from '../types/globals'
test.describe('Linear Mode', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
})
async function enterAppMode(comfyPage: {
page: Page
nextFrame: () => Promise<void>
}) {
await comfyPage.page.evaluate(() => {
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow
if (workflow) workflow.activeMode = 'app'
})
await comfyPage.nextFrame()
}
async function enterGraphMode(comfyPage: {
page: Page
nextFrame: () => Promise<void>
}) {
await comfyPage.page.evaluate(() => {
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow
if (workflow) workflow.activeMode = 'graph'
})
await comfyPage.nextFrame()
}
test('Displays linear controls when app mode active', async ({
comfyPage
}) => {
await enterAppMode(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).toBeVisible({ timeout: 5000 })
})
test('Run button visible in linear mode', async ({ comfyPage }) => {
await enterAppMode(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="linear-run-button"]')
).toBeVisible({ timeout: 5000 })
})
test('Workflow info section visible', async ({ comfyPage }) => {
await enterAppMode(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="linear-workflow-info"]')
).toBeVisible({ timeout: 5000 })
})
test('Returns to graph mode', async ({ comfyPage }) => {
await enterAppMode(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).toBeVisible({ timeout: 5000 })
await enterGraphMode(comfyPage)
await expect(comfyPage.canvas).toBeVisible({ timeout: 5000 })
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).not.toBeVisible()
})
test('Canvas not visible in app mode', async ({ comfyPage }) => {
await enterAppMode(comfyPage)
await expect(
comfyPage.page.locator('[data-testid="linear-widgets"]')
).toBeVisible({ timeout: 5000 })
await expect(comfyPage.canvas).not.toBeVisible()
})
})

View File

@@ -29,11 +29,23 @@ test.describe(
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
]
const filesWithUpload = new Set(['no_workflow.webp'])
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
const waitForUpload = filesWithUpload.has(fileName)
await comfyPage.dragDrop.dragAndDropFile(
`workflowInMedia/${fileName}`,
{ waitForUpload }
)
if (waitForUpload) {
await comfyPage.page.waitForResponse(
(resp) => resp.url().includes('/view') && resp.status() !== 0,
{ timeout: 10000 }
)
}
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
})

View File

@@ -1,71 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.describe('Minimap Status', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.setup()
})
test('Minimap is visible when enabled', async ({ comfyPage }) => {
const minimap = comfyPage.page.locator('.litegraph-minimap')
await expect(minimap).toBeVisible()
})
test('Minimap contains canvas and viewport elements', async ({
comfyPage
}) => {
const minimap = comfyPage.page.locator('.litegraph-minimap')
await expect(minimap.locator('.minimap-canvas')).toBeVisible()
await expect(minimap.locator('.minimap-viewport')).toBeVisible()
})
test('Minimap toggle button exists in canvas menu', async ({ comfyPage }) => {
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(toggleButton).toBeVisible()
})
test('Minimap can be hidden via toggle', async ({ comfyPage }) => {
const minimap = comfyPage.page.locator('.litegraph-minimap')
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(minimap).toBeVisible()
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimap).not.toBeVisible()
})
test('Minimap can be re-shown via toggle', async ({ comfyPage }) => {
const minimap = comfyPage.page.locator('.litegraph-minimap')
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimap).not.toBeVisible()
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimap).toBeVisible()
})
test('Minimap persists across queue operations', async ({ comfyPage }) => {
const minimap = comfyPage.page.locator('.litegraph-minimap')
await expect(minimap).toBeVisible()
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
await comfyPage.nextFrame()
await expect(minimap).toBeVisible()
})
})

View File

@@ -1,99 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('Node Library Essentials Tab', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
test('Node library opens via sidebar', async ({ comfyPage }) => {
const tabButton = comfyPage.page.locator('.node-library-tab-button')
await tabButton.click()
const sidebarContent = comfyPage.page.locator(
'.comfy-vue-side-bar-container'
)
await expect(sidebarContent).toBeVisible()
})
test('Essentials tab is visible in node library', async ({ comfyPage }) => {
const tabButton = comfyPage.page.locator('.node-library-tab-button')
await tabButton.click()
const essentialsTab = comfyPage.page.getByRole('tab', {
name: /essentials/i
})
await expect(essentialsTab).toBeVisible()
})
test('Clicking essentials tab shows essentials panel', async ({
comfyPage
}) => {
const tabButton = comfyPage.page.locator('.node-library-tab-button')
await tabButton.click()
const essentialsTab = comfyPage.page.getByRole('tab', {
name: /essentials/i
})
await essentialsTab.click()
const essentialCards = comfyPage.page.locator('[data-node-name]')
await expect(essentialCards.first()).toBeVisible()
})
test('Essential node cards are displayed', async ({ comfyPage }) => {
const tabButton = comfyPage.page.locator('.node-library-tab-button')
await tabButton.click()
const essentialsTab = comfyPage.page.getByRole('tab', {
name: /essentials/i
})
await essentialsTab.click()
const essentialCards = comfyPage.page.locator('[data-node-name]')
await expect(async () => {
expect(await essentialCards.count()).toBeGreaterThan(0)
}).toPass()
})
test('Essential node cards have node names', async ({ comfyPage }) => {
const tabButton = comfyPage.page.locator('.node-library-tab-button')
await tabButton.click()
const essentialsTab = comfyPage.page.getByRole('tab', {
name: /essentials/i
})
await essentialsTab.click()
const firstCard = comfyPage.page.locator('[data-node-name]').first()
await expect(firstCard).toBeVisible()
const nodeName = await firstCard.getAttribute('data-node-name')
expect(nodeName).toBeTruthy()
expect(nodeName!.length).toBeGreaterThan(0)
})
test('Node library can switch between all and essentials tabs', async ({
comfyPage
}) => {
const tabButton = comfyPage.page.locator('.node-library-tab-button')
await tabButton.click()
const essentialsTab = comfyPage.page.getByRole('tab', {
name: /essentials/i
})
const allNodesTab = comfyPage.page.getByRole('tab', {
name: /all nodes/i
})
await essentialsTab.click()
const essentialCards = comfyPage.page.locator('[data-node-name]')
await expect(essentialCards.first()).toBeVisible()
await allNodesTab.click()
await expect(essentialCards.first()).not.toBeVisible()
})
})

View File

@@ -1,140 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('Node search box V2 extended', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.Action',
'search box'
)
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'search box'
)
await comfyPage.searchBoxV2.reload(comfyPage)
})
test('Double-click on empty canvas opens search', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await expect(searchBoxV2.dialog).toBeVisible()
})
test('Escape closes search box without adding node', async ({
comfyPage
}) => {
const { searchBoxV2 } = comfyPage
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.input.fill('KSampler')
await expect(searchBoxV2.results.first()).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(searchBoxV2.input).not.toBeVisible()
const newCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(newCount).toBe(initialCount)
})
test('Search clears when reopening', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.input.fill('KSampler')
await expect(searchBoxV2.results.first()).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(searchBoxV2.input).not.toBeVisible()
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await expect(searchBoxV2.input).toHaveValue('')
})
test.describe('Category navigation', () => {
test('Category navigation updates results', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.categoryButton('sampling').click()
await expect(searchBoxV2.results.first()).toBeVisible()
const samplingResults = await searchBoxV2.results.allTextContents()
await searchBoxV2.categoryButton('loaders').click()
await expect(searchBoxV2.results.first()).toBeVisible()
const loaderResults = await searchBoxV2.results.allTextContents()
expect(samplingResults).not.toEqual(loaderResults)
})
})
test.describe('Filter workflow', () => {
test('Filter chip removal restores results', async ({ comfyPage }) => {
const { searchBoxV2 } = comfyPage
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
// Get unfiltered count
await expect(searchBoxV2.results.first()).toBeVisible()
const unfilteredCount = await searchBoxV2.results.count()
// Apply Input filter with MODEL type
await searchBoxV2.filterBarButton('Input').click()
await expect(searchBoxV2.filterOptions.first()).toBeVisible()
await searchBoxV2.input.fill('MODEL')
await searchBoxV2.filterOptions
.filter({ hasText: 'MODEL' })
.first()
.click()
await expect(searchBoxV2.results.first()).toBeVisible()
const filteredCount = await searchBoxV2.results.count()
expect(filteredCount).toBeLessThanOrEqual(unfilteredCount)
// Remove filter by pressing Backspace with empty input
await searchBoxV2.input.fill('')
await comfyPage.page.keyboard.press('Backspace')
// Results should restore to unfiltered count
await expect(searchBoxV2.results.first()).toBeVisible()
const restoredCount = await searchBoxV2.results.count()
expect(restoredCount).toBeGreaterThanOrEqual(filteredCount)
})
})
test.describe('Keyboard navigation', () => {
test('ArrowUp on first item keeps first selected', async ({
comfyPage
}) => {
const { searchBoxV2 } = comfyPage
await comfyPage.canvasOps.doubleClick()
await expect(searchBoxV2.input).toBeVisible()
await searchBoxV2.input.fill('KSampler')
const results = searchBoxV2.results
await expect(results.first()).toBeVisible()
// First result should be selected by default
await expect(results.first()).toHaveAttribute('aria-selected', 'true')
// ArrowUp on first item should keep first selected
await comfyPage.page.keyboard.press('ArrowUp')
await expect(results.first()).toHaveAttribute('aria-selected', 'true')
})
})
})

View File

@@ -1,82 +0,0 @@
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('Queue Notification Banners', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
async function triggerExecutionError(comfyPage: {
canvasOps: { disconnectEdge(): Promise<void> }
page: { keyboard: { press(key: string): Promise<void> } }
command: { executeCommand(cmd: string): Promise<void> }
nextFrame(): Promise<void>
}) {
await comfyPage.canvasOps.disconnectEdge()
await comfyPage.nextFrame()
await comfyPage.page.keyboard.press('Escape')
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
}
test('Toast appears when prompt is queued', async ({ comfyPage }) => {
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
})
test('Error toast appears on failed execution', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const errorToast = comfyPage.page.locator(
'.p-toast-message.p-toast-message-error'
)
await expect(errorToast.first()).toBeVisible()
})
test('Error toast contains error description', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const errorToast = comfyPage.page.locator(
'.p-toast-message.p-toast-message-error'
)
await expect(errorToast.first()).toBeVisible()
await expect(errorToast.first()).not.toHaveText('')
})
test('Toast close button dismisses individual toast', async ({
comfyPage
}) => {
await triggerExecutionError(comfyPage)
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
const closeButton = comfyPage.page.locator('.p-toast-close-button').first()
await closeButton.click()
await expect(comfyPage.toast.visibleToasts).toHaveCount(0)
})
test('Multiple toasts can stack', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
await triggerExecutionError(comfyPage)
await expect(comfyPage.toast.visibleToasts).not.toHaveCount(0)
const count = await comfyPage.toast.getVisibleToastCount()
expect(count).toBeGreaterThanOrEqual(2)
})
test('All toasts can be cleared', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
await comfyPage.toast.closeToasts()
expect(await comfyPage.toast.getVisibleToastCount()).toBe(0)
})
})

View File

@@ -1,113 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Right Side Panel Tabs', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Properties panel opens with workflow overview', async ({
comfyPage
}) => {
await comfyPage.actionbar.propertiesButton.click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
await expect(propertiesPanel.panelTitle).toContainText('Workflow Overview')
})
test('Properties panel shows node details on selection', async ({
comfyPage
}) => {
await comfyPage.actionbar.propertiesButton.click()
const { propertiesPanel } = comfyPage.menu
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(propertiesPanel.panelTitle).toContainText('KSampler')
})
test('Node title input is editable', async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
const { propertiesPanel } = comfyPage.menu
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(propertiesPanel.panelTitle).toContainText('KSampler')
// Click on the title to enter edit mode
await propertiesPanel.panelTitle.click()
const titleInput = propertiesPanel.root.getByTestId('node-title-input')
await expect(titleInput).toBeVisible()
await titleInput.fill('My Custom Sampler')
await titleInput.press('Enter')
await expect(propertiesPanel.panelTitle).toContainText('My Custom Sampler')
})
test('Search box filters properties', async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
const { propertiesPanel } = comfyPage.menu
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(propertiesPanel.panelTitle).toContainText('3 items selected')
await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1)
await expect(
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(2)
await propertiesPanel.searchBox.fill('seed')
await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1)
await expect(
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(0)
await propertiesPanel.searchBox.fill('')
await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1)
await expect(
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(2)
})
test('Collapse all / Expand all toggles sections', async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
const { propertiesPanel } = comfyPage.menu
// Select multiple nodes so collapse toggle button appears
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
const collapseButton = propertiesPanel.root.getByRole('button', {
name: 'Collapse all'
})
await expect(collapseButton).toBeVisible()
await collapseButton.click()
const expandButton = propertiesPanel.root.getByRole('button', {
name: 'Expand all'
})
await expect(expandButton).toBeVisible()
await expandButton.click()
// After expanding, the button label switches back to "Collapse all"
await expect(collapseButton).toBeVisible()
})
test('Properties panel can be closed', async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
const { propertiesPanel } = comfyPage.menu
await expect(propertiesPanel.root).toBeVisible()
// Click the properties button again to close
await comfyPage.actionbar.propertiesButton.click()
await expect(propertiesPanel.root).toBeHidden()
})
})

View File

@@ -1,79 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('@canvas Selection Rectangle', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.vueNodes.waitForNodes()
})
test('Ctrl+A selects all nodes', async ({ comfyPage }) => {
const totalCount = await comfyPage.vueNodes.getNodeCount()
expect(totalCount).toBeGreaterThan(0)
await comfyPage.canvas.click({ position: { x: 10, y: 10 } })
await comfyPage.nextFrame()
await comfyPage.page.keyboard.press('Control+a')
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(totalCount)
})
test('Click empty space deselects all', async ({ comfyPage }) => {
await comfyPage.canvas.click({ position: { x: 10, y: 10 } })
await comfyPage.page.keyboard.press('Control+a')
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBeGreaterThan(0)
await comfyPage.vueNodes.clearSelection()
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
})
test('Single click selects one node', async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
})
test('Ctrl+click adds to selection', async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
await comfyPage.page.getByText('Empty Latent Image').click({
modifiers: ['Control']
})
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
})
test('Selected nodes have visual indicator', async ({ comfyPage }) => {
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.nextFrame()
await expect(checkpointNode).toHaveClass(/outline-node-component-outline/)
})
test('Drag-select rectangle selects multiple nodes', async ({
comfyPage
}) => {
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
await comfyPage.page.mouse.move(10, 400)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(800, 600, { steps: 10 })
await comfyPage.page.mouse.up()
await comfyPage.nextFrame()
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBeGreaterThan(1)
})
})

View File

@@ -1,121 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Selection Toolbox - Button Actions', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame()
})
test('bypass button toggles node bypass state', async ({ comfyPage }) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.nextFrame()
const nodeRef = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0]
// Click bypass button to bypass the node
await comfyPage.page.locator('[data-testid="bypass-button"]').click()
await comfyPage.nextFrame()
await expect(nodeRef).toBeBypassed()
// Re-select the node to show toolbox again
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.nextFrame()
// Click bypass button again to un-bypass
await comfyPage.page.locator('[data-testid="bypass-button"]').click()
await comfyPage.nextFrame()
await expect(nodeRef).not.toBeBypassed()
})
test('delete button removes selected node', async ({ comfyPage }) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.nextFrame()
const initialCount = await comfyPage.page.evaluate(
() => window.app!.graph!._nodes.length
)
await comfyPage.page.locator('[data-testid="delete-button"]').click()
await comfyPage.nextFrame()
const newCount = await comfyPage.page.evaluate(
() => window.app!.graph!._nodes.length
)
expect(newCount).toBe(initialCount - 1)
})
test('info button opens properties panel', async ({ comfyPage }) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.nextFrame()
await comfyPage.page.locator('[data-testid="info-button"]').click()
await comfyPage.nextFrame()
await expect(
comfyPage.page.locator('[data-testid="properties-panel"]')
).toBeVisible()
})
test('refresh button is visible when node is selected', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.nextFrame()
await expect(
comfyPage.page.locator('[data-testid="refresh-button"]')
).toBeVisible()
})
test('convert-to-subgraph button visible with multi-select', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.nextFrame()
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await comfyPage.nextFrame()
await expect(
comfyPage.page.locator('[data-testid="convert-to-subgraph-button"]')
).toBeVisible()
})
test('delete button removes multiple selected nodes', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.nextFrame()
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await comfyPage.nextFrame()
const initialCount = await comfyPage.page.evaluate(
() => window.app!.graph!._nodes.length
)
await comfyPage.page.locator('[data-testid="delete-button"]').click()
await comfyPage.nextFrame()
const newCount = await comfyPage.page.evaluate(
() => window.app!.graph!._nodes.length
)
expect(newCount).toBe(initialCount - 2)
})
})

View File

@@ -1,57 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('Settings Sidebar', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
test('Settings button is visible in sidebar', async ({ comfyPage }) => {
await expect(comfyPage.menu.sideToolbar).toBeVisible()
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
await expect(settingsButton).toBeVisible()
})
test('Clicking settings button opens settings dialog', async ({
comfyPage
}) => {
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
await settingsButton.click()
await expect(comfyPage.settingDialog.root).toBeVisible()
})
test('Settings dialog shows categories', async ({ comfyPage }) => {
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
await settingsButton.click()
await expect(comfyPage.settingDialog.root).toBeVisible()
await expect(comfyPage.settingDialog.categories.first()).toBeVisible()
expect(await comfyPage.settingDialog.categories.count()).toBeGreaterThan(0)
})
test('Settings dialog can be closed with Escape', async ({ comfyPage }) => {
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
await settingsButton.click()
await expect(comfyPage.settingDialog.root).toBeVisible()
await comfyPage.page.keyboard.press('Escape')
await expect(comfyPage.settingDialog.root).not.toBeVisible()
})
test('Settings search box is functional', async ({ comfyPage }) => {
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
await settingsButton.click()
await expect(comfyPage.settingDialog.root).toBeVisible()
await comfyPage.settingDialog.searchBox.fill('color')
await expect(comfyPage.settingDialog.searchBox).toHaveValue('color')
})
test('Settings dialog can navigate to About panel', async ({ comfyPage }) => {
const settingsButton = comfyPage.menu.sideToolbar.getByLabel(/settings/i)
await settingsButton.click()
await expect(comfyPage.settingDialog.root).toBeVisible()
await comfyPage.settingDialog.goToAboutPanel()
await expect(comfyPage.page.locator('.about-container')).toBeVisible()
})
})

View File

@@ -1,70 +0,0 @@
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('Toast Notifications', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setup()
})
async function triggerExecutionError(comfyPage: {
canvasOps: { disconnectEdge(): Promise<void> }
page: { keyboard: { press(key: string): Promise<void> } }
command: { executeCommand(cmd: string): Promise<void> }
nextFrame(): Promise<void>
}) {
await comfyPage.canvasOps.disconnectEdge()
await comfyPage.nextFrame()
await comfyPage.page.keyboard.press('Escape')
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
}
test('Error toast appears on execution failure', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
})
test('Toast shows correct error severity class', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
const errorToast = comfyPage.page.locator(
'.p-toast-message.p-toast-message-error'
)
await expect(errorToast.first()).toBeVisible()
})
test('Toast can be dismissed via close button', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
const closeButton = comfyPage.page.locator('.p-toast-close-button').first()
await closeButton.click()
await expect(comfyPage.toast.visibleToasts).toHaveCount(0)
})
test('All toasts cleared via closeToasts helper', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
await expect(comfyPage.toast.visibleToasts.first()).toBeVisible()
await comfyPage.toast.closeToasts()
expect(await comfyPage.toast.getVisibleToastCount()).toBe(0)
})
test('Toast error count is accurate', async ({ comfyPage }) => {
await triggerExecutionError(comfyPage)
await expect(
comfyPage.page.locator('.p-toast-message.p-toast-message-error').first()
).toBeVisible()
const errorCount = await comfyPage.toast.getToastErrorCount()
expect(errorCount).toBeGreaterThanOrEqual(1)
})
})

View File

@@ -1,69 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../fixtures/ComfyPage'
test.describe('Vue Node Header Actions', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.vueNodes.waitForNodes()
})
test('Collapse button is visible on node header', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect(vueNode.collapseButton).toBeVisible()
})
test('Clicking collapse button hides node body', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect(vueNode.body).toBeVisible()
await vueNode.toggleCollapse()
await comfyPage.nextFrame()
await expect(vueNode.body).not.toBeVisible()
})
test('Clicking collapse button again expands node', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await vueNode.toggleCollapse()
await comfyPage.nextFrame()
await expect(vueNode.body).not.toBeVisible()
await vueNode.toggleCollapse()
await comfyPage.nextFrame()
await expect(vueNode.body).toBeVisible()
})
test('Double-click header enters title edit mode', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await vueNode.header.dblclick()
await expect(vueNode.titleInput).toBeVisible()
})
test('Title edit saves on Enter', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await vueNode.setTitle('My Custom Sampler')
expect(await vueNode.getTitle()).toBe('My Custom Sampler')
})
test('Title edit cancels on Escape', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await vueNode.setTitle('Renamed Sampler')
expect(await vueNode.getTitle()).toBe('Renamed Sampler')
await vueNode.header.dblclick()
await vueNode.titleInput.fill('This Should Be Cancelled')
await vueNode.titleInput.press('Escape')
await comfyPage.nextFrame()
expect(await vueNode.getTitle()).toBe('Renamed Sampler')
})
})

View File

@@ -1,84 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.describe('Widget copy button', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.vueNodes.waitForNodes()
})
test('Vue nodes render with widgets', async ({ comfyPage }) => {
const nodeCount = await comfyPage.vueNodes.getNodeCount()
expect(nodeCount).toBeGreaterThan(0)
const firstNode = comfyPage.vueNodes.nodes.first()
await expect(firstNode).toBeVisible()
})
test('Textarea widgets exist on nodes', async ({ comfyPage }) => {
const textareas = comfyPage.page.locator('[data-node-id] textarea')
await expect(textareas.first()).toBeVisible()
expect(await textareas.count()).toBeGreaterThan(0)
})
test('Copy button has correct aria-label', async ({ comfyPage }) => {
const copyButtons = comfyPage.page.locator(
'[data-node-id] button[aria-label]'
)
const count = await copyButtons.count()
if (count > 0) {
const button = copyButtons.filter({
has: comfyPage.page.locator('.icon-\\[lucide--copy\\]')
})
if ((await button.count()) > 0) {
await expect(button.first()).toHaveAttribute('aria-label', /copy/i)
}
}
})
test('Copy icon uses lucide copy class', async ({ comfyPage }) => {
const copyIcons = comfyPage.page.locator(
'[data-node-id] .icon-\\[lucide--copy\\]'
)
const count = await copyIcons.count()
if (count > 0) {
await expect(copyIcons.first()).toBeVisible()
}
})
test('Widget container has group class for hover', async ({ comfyPage }) => {
const textareas = comfyPage.page.locator('[data-node-id] textarea')
const count = await textareas.count()
if (count > 0) {
const container = textareas.first().locator('..')
await expect(container).toHaveClass(/group/)
}
})
test('Copy button exists within textarea widget group container', async ({
comfyPage
}) => {
const groupContainers = comfyPage.page.locator('[data-node-id] div.group')
const count = await groupContainers.count()
if (count > 0) {
const container = groupContainers.first()
await container.hover()
await comfyPage.nextFrame()
const copyButton = container.locator('button').filter({
has: comfyPage.page.locator('.icon-\\[lucide--copy\\]')
})
if ((await copyButton.count()) > 0) {
await expect(copyButton.first()).toHaveClass(/invisible/)
}
}
})
})

View File

@@ -127,7 +127,7 @@ export default defineConfig([
// Off: may conflict with oxfmt formatting
'better-tailwindcss/enforce-consistent-line-wrapping': 'off',
// Off: large batch change, enable and apply with `eslint --fix`
'better-tailwindcss/enforce-consistent-class-order': 'off',
'better-tailwindcss/enforce-consistent-class-order': 'error',
'better-tailwindcss/enforce-canonical-classes': 'error',
'better-tailwindcss/no-deprecated-classes': 'error'
}

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.41.12",
"version": "1.41.13",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -1,6 +1,6 @@
<template>
<div
class="size-full absolute top-0 left-0 z-999 pointer-events-none flex flex-col"
class="pointer-events-none absolute top-0 left-0 z-999 flex size-full flex-col"
>
<slot name="workflow-tabs" />
@@ -17,7 +17,7 @@
<Splitter
:key="splitterRefreshKey"
class="bg-transparent pointer-events-none border-none flex-1 overflow-hidden"
class="pointer-events-none flex-1 overflow-hidden border-none bg-transparent"
:state-key="isSelectMode ? 'builder-splitter' : sidebarStateKey"
state-storage="local"
@resizestart="onResizestart"
@@ -30,10 +30,10 @@
:class="
sidebarLocation === 'left'
? cn(
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
'side-bar-panel pointer-events-auto bg-comfy-menu-bg',
sidebarPanelVisible && 'min-w-78'
)
: 'bg-comfy-menu-bg pointer-events-auto'
: 'pointer-events-auto bg-comfy-menu-bg'
"
:min-size="
sidebarLocation === 'left' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE
@@ -60,7 +60,7 @@
<slot name="topmenu" :sidebar-panel-visible />
<Splitter
class="bg-transparent pointer-events-none border-none splitter-overlay-bottom mx-1 mb-1 flex-1"
class="splitter-overlay-bottom pointer-events-none mx-1 mb-1 flex-1 border-none bg-transparent"
layout="vertical"
:pt:gutter="
cn(
@@ -77,7 +77,7 @@
</SplitterPanel>
<SplitterPanel
v-show="bottomPanelVisible && !focusMode"
class="bottom-panel border border-(--p-panel-border-color) max-w-full overflow-x-auto bg-comfy-menu-bg pointer-events-auto rounded-lg"
class="bottom-panel pointer-events-auto max-w-full overflow-x-auto rounded-lg border border-(--p-panel-border-color) bg-comfy-menu-bg"
>
<slot name="bottom-panel" />
</SplitterPanel>
@@ -92,10 +92,10 @@
:class="
sidebarLocation === 'right'
? cn(
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
'side-bar-panel pointer-events-auto bg-comfy-menu-bg',
sidebarPanelVisible && 'min-w-78'
)
: 'bg-comfy-menu-bg pointer-events-auto'
: 'pointer-events-auto bg-comfy-menu-bg'
"
:min-size="
sidebarLocation === 'right' ? SIDEBAR_MIN_SIZE : BUILDER_MIN_SIZE

View File

@@ -1,7 +1,7 @@
<template>
<div
v-show="workspaceState.focusMode"
class="fixed z-9999 flex flex-row no-drag top-0 right-0"
class="no-drag fixed top-0 right-0 z-9999 flex flex-row"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"

View File

@@ -38,7 +38,7 @@
ref="actionbarContainerRef"
:class="
cn(
'actionbar-container relative pointer-events-auto flex gap-2 h-12 items-center rounded-lg border bg-comfy-menu-bg px-2 shadow-interface',
'actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 rounded-lg border bg-comfy-menu-bg px-2 shadow-interface',
hasAnyError
? 'border-destructive-background-hover'
: 'border-interface-stroke'

View File

@@ -19,12 +19,12 @@
content: { class: isDocked ? 'p-0' : 'p-1' }
}"
>
<div class="relative flex items-center select-none gap-2">
<div class="relative flex items-center gap-2 select-none">
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle cursor-grab w-3 h-max',
'drag-handle h-max w-3 cursor-grab',
isDragging && 'cursor-grabbing'
)
"
@@ -423,18 +423,18 @@ const actionbarClass = computed(() =>
cn(
'w-[200px] border-dashed border-blue-500 opacity-80',
'm-1.5 flex items-center justify-center self-stretch',
'rounded-md before:w-50 before:-ml-50 before:h-full',
'rounded-md before:-ml-50 before:h-full before:w-50',
'pointer-events-auto',
isMouseOverDropZone.value &&
'border-[3px] opacity-100 scale-105 shadow-[0_0_20px] shadow-blue-500'
'scale-105 border-[3px] opacity-100 shadow-[0_0_20px] shadow-blue-500'
)
)
const panelClass = computed(() =>
cn(
'actionbar pointer-events-auto z-1300',
isDragging.value && 'select-none pointer-events-none',
isDragging.value && 'pointer-events-none select-none',
isDocked.value
? 'p-0 static border-none bg-transparent'
? 'static border-none bg-transparent p-0'
: 'fixed shadow-interface'
)
)

View File

@@ -42,7 +42,7 @@ function openTemplates() {
</script>
<template>
<div class="flex flex-col gap-2 pointer-events-auto">
<div class="pointer-events-auto flex flex-col gap-2">
<WorkflowActionsDropdown source="app_mode_toolbar">
<template #button="{ hasUnseenItems }">
<Button
@@ -53,7 +53,7 @@ function openTemplates() {
variant="secondary"
size="unset"
:aria-label="t('sideToolbar.labels.menu')"
class="relative h-10 rounded-lg pl-3 pr-2 gap-1 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
class="relative h-10 gap-1 rounded-lg pr-2 pl-3 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
>
<i class="icon-[lucide--panels-top-left] size-4" />
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
@@ -83,7 +83,7 @@ function openTemplates() {
</Button>
<div
class="flex flex-col w-10 rounded-lg bg-secondary-background overflow-hidden"
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"
>
<Button
v-tooltip.right="{

View File

@@ -12,7 +12,7 @@
size="sm"
:class="
cn('absolute top-2 right-8 transition-opacity', {
'opacity-0 pointer-events-none select-none': !isHovered
'pointer-events-none opacity-0 select-none': !isHovered
})
"
:aria-label="tooltipText"

View File

@@ -1,7 +1,7 @@
<template>
<div
data-testid="subgraph-breadcrumb"
class="subgraph-breadcrumb flex w-auto drop-shadow-(--interface-panel-drop-shadow) items-center -mt-4 pt-4"
class="subgraph-breadcrumb -mt-4 flex w-auto items-center pt-4 drop-shadow-(--interface-panel-drop-shadow)"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
@@ -17,7 +17,7 @@
<WorkflowActionsDropdown source="breadcrumb_subgraph_menu_selected" />
<Button
v-if="isInSubgraph"
class="back-button pointer-events-auto size-8 shrink-0 border border-transparent bg-transparent p-0 ml-1.5 transition-all hover:rounded-lg hover:border-interface-stroke hover:bg-comfy-menu-bg"
class="back-button pointer-events-auto ml-1.5 size-8 shrink-0 border border-transparent bg-transparent p-0 transition-all hover:rounded-lg hover:border-interface-stroke hover:bg-comfy-menu-bg"
text
severity="secondary"
size="small"

View File

@@ -204,7 +204,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
)
</script>
<template>
<div class="flex font-bold p-2 border-border-subtle border-b items-center">
<div class="flex items-center border-b border-border-subtle p-2 font-bold">
{{
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
}}
@@ -217,7 +217,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<div
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
:key="`${nodeId}: ${widgetName}`"
:class="cn(dragClass, 'p-2 my-2 pointer-events-auto')"
:class="cn(dragClass, 'pointer-events-auto my-2 p-2')"
:aria-label="`${widget?.label ?? widgetName} ${node.title}`"
>
<div v-if="widget" class="pointer-events-none" inert>
@@ -228,7 +228,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
hidden-widget-actions
/>
</div>
<div v-else class="text-muted-foreground text-sm p-1 pointer-events-none">
<div v-else class="pointer-events-none p-1 text-sm text-muted-foreground">
{{ widgetName }}
<p class="text-xs italic">
({{ t('linearMode.builder.unknownWidget') }})
@@ -241,14 +241,14 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
:label="t('nodeHelpPage.inputs')"
enable-empty-state
:disabled="!appModeStore.selectedInputs.length"
class="border-border-subtle border-b"
class="border-b border-border-subtle"
:tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
:tooltip-delay="100"
>
<template #label>
<div class="flex gap-3">
{{ t('nodeHelpPage.inputs') }}
<i class="bg-muted-foreground icon-[lucide--circle-alert]" />
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
</div>
</template>
<template #empty>
@@ -271,7 +271,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
rename
} in inputsWithState"
:key="`${nodeId}: ${widgetName}`"
:class="cn(dragClass, 'bg-primary-background/30 p-2 my-2 rounded-lg')"
:class="cn(dragClass, 'my-2 rounded-lg bg-primary-background/30 p-2')"
:title="label ?? widgetName"
:sub-title="subLabel"
:rename
@@ -296,7 +296,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<template #label>
<div class="flex gap-3">
{{ t('nodeHelpPage.outputs') }}
<i class="bg-muted-foreground icon-[lucide--circle-alert]" />
<i class="icon-[lucide--circle-alert] bg-muted-foreground" />
</div>
</template>
<template #empty>
@@ -319,8 +319,8 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
:class="
cn(
dragClass,
'bg-warning-background/40 p-2 my-2 rounded-lg',
index === 0 && 'ring-warning-background ring-2'
'my-2 rounded-lg bg-warning-background/40 p-2',
index === 0 && 'ring-2 ring-warning-background'
)
"
:title
@@ -337,7 +337,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<div
:class="
cn(
'absolute size-full pointer-events-auto',
'pointer-events-auto absolute size-full',
hoveringSelectable ? 'cursor-pointer' : 'cursor-grab'
)
"
@@ -352,7 +352,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
v-for="[key, style] in renderedInputs"
:key
:style="toValue(style)"
class="fixed bg-primary-background/30 rounded-lg"
class="fixed rounded-lg bg-primary-background/30"
/>
</template>
<template v-else>
@@ -362,7 +362,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
:style="toValue(style)"
:class="
cn(
'fixed ring-warning-background ring-5 rounded-2xl',
'fixed rounded-2xl ring-5 ring-warning-background',
!isSelected && 'ring-warning-background/50'
)
"
@@ -370,17 +370,17 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<div class="absolute top-0 right-0 size-8">
<div
v-if="isSelected"
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg cursor-pointer pointer-events-auto"
class="pointer-events-auto absolute -top-1/2 -right-1/2 size-full cursor-pointer rounded-lg bg-warning-background p-2"
@click.stop="
remove(appModeStore.selectedOutputs, (k) => k == key)
"
@pointerdown.stop
>
<i class="icon-[lucide--check] bg-text-foreground size-full" />
<i class="bg-text-foreground icon-[lucide--check] size-full" />
</div>
<div
v-else
class="absolute -top-1/2 -right-1/2 size-full ring-warning-background/50 ring-4 ring-inset bg-component-node-background rounded-lg cursor-pointer pointer-events-auto"
class="pointer-events-auto absolute -top-1/2 -right-1/2 size-full cursor-pointer rounded-lg bg-component-node-background ring-4 ring-warning-background/50 ring-inset"
@click.stop="appModeStore.selectedOutputs.push(key)"
@pointerdown.stop
/>

View File

@@ -4,7 +4,7 @@
<button
:class="
cn(
'absolute left-4 top-[calc(var(--workflow-tabs-height)+16px)] z-1000 inline-flex h-10 cursor-pointer items-center gap-2.5 rounded-lg py-2 pr-2 pl-3 shadow-interface transition-colors border-none',
'absolute top-[calc(var(--workflow-tabs-height)+16px)] left-4 z-1000 inline-flex h-10 cursor-pointer items-center gap-2.5 rounded-lg border-none py-2 pr-2 pl-3 shadow-interface transition-colors',
'bg-secondary-background hover:bg-secondary-background-hover',
'data-[state=open]:bg-secondary-background-hover'
)
@@ -22,10 +22,10 @@
<button
:class="
cn(
'flex w-full items-center gap-3 rounded-md bg-transparent px-3 py-2 text-sm border-none',
'flex w-full items-center gap-3 rounded-md border-none bg-transparent px-3 py-2 text-sm',
hasOutputs
? 'cursor-pointer hover:bg-secondary-background-hover'
: 'opacity-50 pointer-events-none'
: 'pointer-events-none opacity-50'
)
"
:disabled="!hasOutputs"
@@ -36,7 +36,7 @@
</button>
<div class="my-1 border-t border-border-default" />
<button
class="flex w-full cursor-pointer items-center gap-3 rounded-md bg-transparent px-3 py-2 text-sm border-none hover:bg-secondary-background-hover"
class="flex w-full cursor-pointer items-center gap-3 rounded-md border-none bg-transparent px-3 py-2 text-sm hover:bg-secondary-background-hover"
@click="onExitBuilder(close)"
>
<i class="icon-[lucide--square-pen] size-4" />

View File

@@ -13,7 +13,7 @@
stepClasses,
activeStep === step.id
? 'bg-interface-builder-mode-background'
: 'hover:bg-secondary-background bg-transparent'
: 'bg-transparent hover:bg-secondary-background'
)
"
:aria-current="activeStep === step.id ? 'step' : undefined"
@@ -32,7 +32,7 @@
:is-select-active="isSelectStep"
@switch="navigateToStep('builder:outputs')"
>
<button :class="cn(stepClasses, 'opacity-30 bg-transparent')">
<button :class="cn(stepClasses, 'bg-transparent opacity-30')">
<StepBadge
:step="defaultViewStep"
:index="steps.length"
@@ -48,7 +48,7 @@
stepClasses,
activeStep === 'setDefaultView'
? 'bg-interface-builder-mode-background'
: 'hover:bg-secondary-background bg-transparent'
: 'bg-transparent hover:bg-secondary-background'
)
"
@click="navigateToStep('setDefaultView')"

View File

@@ -7,7 +7,7 @@
side="bottom"
:side-offset="8"
:collision-padding="10"
class="z-1001 w-80 rounded-xl border border-border-default bg-base-background shadow-interface will-change-[transform,opacity] data-[state=open]:data-[side=bottom]:animate-slideUpAndFade"
class="data-[state=open]:data-[side=bottom]:animate-slideUpAndFade z-1001 w-80 rounded-xl border border-border-default bg-base-background shadow-interface will-change-[transform,opacity]"
>
<div class="flex h-12 items-center justify-between px-4">
<h3 class="text-sm font-medium text-base-foreground">

View File

@@ -32,13 +32,13 @@ const entries = computed(() => {
})
</script>
<template>
<div class="p-2 my-2 rounded-lg flex items-center-safe gap-2">
<div class="my-2 flex items-center-safe gap-2 rounded-lg p-2">
<div
class="mr-auto flex-[4_1_0%] max-w-max min-w-0 truncate drag-handle inline"
class="drag-handle mr-auto inline max-w-max min-w-0 flex-[4_1_0%] truncate"
v-text="title"
/>
<div
class="flex-[2_1_0%] max-w-max min-w-0 truncate text-muted-foreground text-end drag-handle inline"
class="drag-handle inline max-w-max min-w-0 flex-[2_1_0%] truncate text-end text-muted-foreground"
v-text="subTitle"
/>
<Popover :entries>

View File

@@ -4,7 +4,7 @@
{{ step.title }}
</span>
<span
class="hidden whitespace-nowrap text-xs text-muted-foreground sm:inline"
class="hidden text-xs whitespace-nowrap text-muted-foreground sm:inline"
>
{{ step.subtitle }}
</span>

View File

@@ -2,7 +2,7 @@
<div
:class="
cn(
'flex justify-center items-center shrink-0 outline-hidden border-none p-0 rounded-lg shadow-sm transition-all duration-200 cursor-pointer bg-secondary-background',
'flex shrink-0 cursor-pointer items-center justify-center rounded-lg border-none bg-secondary-background p-0 shadow-sm outline-hidden transition-all duration-200',
backgroundClass
)
"

View File

@@ -14,6 +14,6 @@ const { fullHeight = true } = defineProps<{
}>()
const containerClasses = computed(() =>
cn('flex-1 w-full', fullHeight && 'h-full')
cn('w-full flex-1', fullHeight && 'h-full')
)
</script>

View File

@@ -59,7 +59,7 @@ const containerClasses = computed(() => {
outline: cn(
hasBorder && 'border-2 border-border-subtle',
hasCursor && 'cursor-pointer',
'hover:border-border-subtle/50 transition-colors'
'transition-colors hover:border-border-subtle/50'
)
}

View File

@@ -19,9 +19,9 @@ const baseClasses =
const variantStyles = {
dark: 'bg-zinc-500/40 text-white/90',
light: cn('backdrop-blur-[2px] bg-base-background/50 text-base-foreground'),
light: cn('bg-base-background/50 text-base-foreground backdrop-blur-[2px]'),
gray: cn(
'backdrop-blur-[2px] bg-modal-card-tag-background text-base-foreground'
'bg-modal-card-tag-background text-base-foreground backdrop-blur-[2px]'
)
}

View File

@@ -51,7 +51,7 @@ onBeforeUnmount(() => {
})
</script>
<template>
<div ref="draggableItems" class="pb-2 px-2 space-y-0.5 mt-0.5">
<div ref="draggableItems" class="mt-0.5 space-y-0.5 px-2 pb-2">
<slot
drag-class="draggable-item drag-handle cursor-grab [&.is-draggable]:cursor-grabbing"
/>

View File

@@ -22,7 +22,7 @@ defineProps<{ itemClass: string; contentClass: string; item: MenuItem }>()
<template>
<DropdownMenuSeparator
v-if="item.separator"
class="h-px bg-border-subtle m-1"
class="m-1 h-px bg-border-subtle"
/>
<DropdownMenuSub v-else-if="item.items">
<DropdownMenuSubTrigger
@@ -58,7 +58,7 @@ defineProps<{ itemClass: string; contentClass: string; item: MenuItem }>()
{{ item.label }}
<div
v-if="item.new"
class="ml-auto bg-primary-background rounded-full text-xxs font-bold px-1 flex leading-none items-center"
class="ml-auto flex items-center rounded-full bg-primary-background px-1 text-xxs leading-none font-bold"
v-text="t('contextMenu.new')"
/>
</DropdownMenuItem>

View File

@@ -27,14 +27,14 @@ const { itemClass: itemProp, contentClass: contentProp } = defineProps<{
const itemClass = computed(() =>
cn(
'data-highlighted:bg-secondary-background-hover data-disabled:pointer-events-none data-disabled:text-muted-foreground flex p-2 leading-none rounded-lg gap-1 cursor-pointer m-1',
'm-1 flex cursor-pointer gap-1 rounded-lg p-2 leading-none data-disabled:pointer-events-none data-disabled:text-muted-foreground data-highlighted:bg-secondary-background-hover',
itemProp
)
)
const contentClass = computed(() =>
cn(
'z-1700 rounded-lg p-2 bg-base-background border border-border-subtle min-w-[220px] shadow-sm will-change-[opacity,transform] data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade',
'data-[side=top]:animate-slideDownAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade data-[side=left]:animate-slideRightAndFade z-1700 min-w-[220px] rounded-lg border border-border-subtle bg-base-background p-2 shadow-sm will-change-[opacity,transform]',
contentProp
)
)

View File

@@ -1,9 +1,9 @@
<template>
<div
class="overflow-hidden @container mask-[linear-gradient(to_right,black_70%,transparent)] motion-safe:group-hover:mask-none"
class="@container overflow-hidden mask-[linear-gradient(to_right,black_70%,transparent)] motion-safe:group-hover:mask-none"
>
<span
class="whitespace-nowrap inline-block min-w-full text-center [--_marquee-end:min(calc(-100%+100cqw),0px)] motion-safe:group-hover:animate-[marquee-scroll_3s_linear_infinite_alternate]"
class="inline-block min-w-full text-center whitespace-nowrap [--_marquee-end:min(calc(-100%+100cqw),0px)] motion-safe:group-hover:animate-[marquee-scroll_3s_linear_infinite_alternate]"
>
<slot />
</span>

View File

@@ -1,11 +1,11 @@
<template>
<span class="relative inline-flex items-center justify-center size-[1em]">
<span class="relative inline-flex size-[1em] items-center justify-center">
<i :class="mainIcon" class="text-[1em]" />
<i
:class="
cn(
subIcon,
'absolute leading-none pointer-events-none',
'pointer-events-none absolute leading-none',
positionX === 'left' ? 'left-0' : 'right-0',
positionY === 'top' ? 'top-0' : 'bottom-0'
)

View File

@@ -8,7 +8,7 @@
v-if="!hideButtons"
:aria-label="t('g.decrement')"
data-testid="decrement"
class="h-full aspect-8/7 rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
class="aspect-8/7 h-full rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
variant="muted-textonly"
:disabled="!canDecrement"
tabindex="-1"
@@ -16,7 +16,7 @@
>
<i class="pi pi-minus" />
</Button>
<div class="relative min-w-[4ch] flex-1 py-1.5 my-0.25">
<div class="relative my-0.25 min-w-[4ch] flex-1 py-1.5">
<input
ref="inputField"
v-bind="inputAttrs"
@@ -24,7 +24,7 @@
:disabled
:class="
cn(
'bg-transparent border-0 focus:outline-0 p-1 truncate text-sm absolute inset-0'
'absolute inset-0 truncate border-0 bg-transparent p-1 text-sm focus:outline-0'
)
"
inputmode="decimal"
@@ -53,7 +53,7 @@
v-if="!hideButtons"
:aria-label="t('g.increment')"
data-testid="increment"
class="h-full aspect-8/7 rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
class="aspect-8/7 h-full rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
variant="muted-textonly"
:disabled="!canIncrement"
tabindex="-1"

View File

@@ -2,7 +2,7 @@
<div
:class="
cn(
'relative flex w-full items-center gap-2 bg-comfy-input cursor-text text-comfy-input-foreground',
'relative flex w-full cursor-text items-center gap-2 bg-comfy-input text-comfy-input-foreground',
customClass,
wrapperStyle
)
@@ -16,7 +16,7 @@
unstyled
:class="
cn(
'absolute inset-0 size-full border-none outline-none bg-transparent text-sm',
'absolute inset-0 size-full border-none bg-transparent text-sm outline-none',
isLarge ? 'pl-11' : 'pl-8'
)
"
@@ -26,7 +26,7 @@
v-if="filterIcon"
size="icon"
variant="textonly"
class="filter-button absolute right-0 inset-y-0 m-0 p-0"
class="filter-button absolute inset-y-0 right-0 m-0 p-0"
@click="$emit('showFilter', $event)"
>
<i :class="filterIcon" />
@@ -117,7 +117,7 @@ watchDebounced(
const wrapperStyle = computed(() => {
if (showBorder) {
return cn(
'rounded-sm p-2 border border-solid border-border-default box-border',
'box-border rounded-sm border border-solid border-border-default p-2',
isLarge.value ? 'h-10' : 'h-8'
)
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col gap-2 flex-auto">
<div class="flex flex-auto flex-col gap-2">
<ComboboxRoot :ignore-filter="true" :open="false">
<ComboboxAnchor
:class="
@@ -7,7 +7,7 @@
'relative flex w-full cursor-text items-center',
'rounded-lg bg-comfy-input text-comfy-input-foreground',
showBorder &&
'border border-solid border-border-default box-border',
'box-border border border-solid border-border-default',
sizeClasses,
className
)
@@ -15,7 +15,7 @@
>
<i
v-if="!searchTerm"
:class="cn('absolute left-4 pointer-events-none', icon, iconClass)"
:class="cn('pointer-events-none absolute left-4', icon, iconClass)"
/>
<Button
v-else

View File

@@ -3,7 +3,7 @@
<!-- Hidden single-line measurement element for overflow detection -->
<div
ref="measureRef"
class="invisible absolute inset-x-0 top-0 overflow-hidden whitespace-nowrap pointer-events-none"
class="pointer-events-none invisible absolute inset-x-0 top-0 overflow-hidden whitespace-nowrap"
aria-hidden="true"
>
<slot />
@@ -13,7 +13,7 @@
<slot />
</MarqueeLine>
<div v-else class="flex flex-col w-full">
<div v-else class="flex w-full flex-col">
<MarqueeLine>{{ firstLine }}</MarqueeLine>
<MarqueeLine>{{ secondLine }}</MarqueeLine>
</div>

View File

@@ -3,7 +3,7 @@
v-bind="$attrs"
v-model:expanded-keys="expandedKeys"
v-model:selection-keys="selectionKeys"
class="tree-explorer px-2 py-0 2xl:px-4 bg-transparent"
class="tree-explorer bg-transparent px-2 py-0 2xl:px-4"
:class="props.class"
:value="renderedRoot.children"
selection-mode="single"

View File

@@ -29,7 +29,7 @@
/>
</div>
<div
class="node-actions flex gap-1 touch:opacity-100 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100"
class="node-actions flex gap-1 motion-safe:opacity-0 motion-safe:group-hover/tree-node:opacity-100 touch:opacity-100"
>
<slot name="actions" :node="props.node" />
</div>

View File

@@ -42,7 +42,7 @@
class="z-9999 min-w-32 overflow-hidden rounded-md border border-border-default bg-comfy-menu-bg p-1 shadow-md"
>
<ContextMenuItem
class="flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-highlight focus:bg-highlight"
class="flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none hover:bg-highlight focus:bg-highlight"
@select="handleAddToFavorites"
>
<i

View File

@@ -19,7 +19,7 @@
@dragend="handleDragEnd"
>
<i class="icon-[comfy--node] size-4 shrink-0 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate text-sm text-foreground">
<span class="text-foreground min-w-0 flex-1 truncate text-sm">
<slot name="node" :node="item.value">
{{ item.value.label }}
</slot>
@@ -27,7 +27,7 @@
<button
:class="
cn(
'flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-sm border-none bg-transparent text-muted-foreground hover:text-foreground',
'hover:text-foreground flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-sm border-none bg-transparent text-muted-foreground',
'opacity-0 group-hover/tree-node:opacity-100'
)
"
@@ -64,7 +64,7 @@
<i
:class="cn(item.value.icon, 'size-4 shrink-0 text-muted-foreground')"
/>
<span class="min-w-0 flex-1 truncate text-sm text-foreground">
<span class="text-foreground min-w-0 flex-1 truncate text-sm">
<slot name="folder" :node="item.value">
{{ item.value.label }}
</slot>

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="container"
class="h-full overflow-y-auto [overflow-anchor:none] [scrollbar-gutter:stable] scrollbar-thin scrollbar-track-transparent scrollbar-thumb-(--dialog-surface)"
class="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-(--dialog-surface) h-full overflow-y-auto [overflow-anchor:none] [scrollbar-gutter:stable]"
>
<div :style="topSpacerStyle" />
<div :style="mergedGridStyle">

View File

@@ -55,7 +55,7 @@ function handleOpen(open: boolean) {
variant="secondary"
size="unset"
:aria-label="t('breadcrumbsMenu.workflowActions')"
class="relative h-10 rounded-lg pl-3 pr-2 pointer-events-auto gap-1 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
class="pointer-events-auto relative h-10 gap-1 rounded-lg pr-2 pl-3 data-[state=open]:bg-secondary-background-hover data-[state=open]:shadow-interface"
>
<i
class="size-4"
@@ -79,7 +79,7 @@ function handleOpen(open: boolean) {
:align
:side-offset="5"
:collision-padding="10"
class="z-1000 rounded-lg px-2 py-3 min-w-56 bg-base-background shadow-interface border border-border-subtle"
class="z-1000 min-w-56 rounded-lg border border-border-subtle bg-base-background px-2 py-3 shadow-interface"
>
<WorkflowActionsList :items="menuItems" />
</DropdownMenuContent>

View File

@@ -22,7 +22,7 @@ const {
<component
:is="separatorComponent"
v-if="item.separator"
class="border-b w-full border-border-subtle my-1"
class="my-1 w-full border-b border-border-subtle"
/>
<component
:is="itemComponent"
@@ -30,11 +30,11 @@ const {
:disabled="item.disabled"
:class="
cn(
'flex min-h-6 p-2 items-center gap-2 self-stretch rounded-sm outline-none',
'flex min-h-6 items-center gap-2 self-stretch rounded-sm p-2 outline-none',
!item.disabled && item.command && 'cursor-pointer',
'data-highlighted:bg-secondary-background-hover',
!item.disabled && 'hover:bg-secondary-background-hover',
'data-disabled:opacity-50 data-disabled:cursor-default'
'data-disabled:cursor-default data-disabled:opacity-50'
)
"
@select="() => item.command?.()"
@@ -44,7 +44,7 @@ const {
<span class="flex-1">{{ item.label }}</span>
<span
v-if="item.badge"
class="rounded-full uppercase ml-3 flex items-center gap-1 bg-(--primary-background) px-1.5 py-0.5 text-xxs text-base-foreground"
class="ml-3 flex items-center gap-1 rounded-full bg-(--primary-background) px-1.5 py-0.5 text-xxs text-base-foreground uppercase"
>
{{ item.badge }}
</span>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="flex flex-col wrap-break-word px-4 py-2 text-sm text-muted-foreground border-t border-border-default"
class="flex flex-col border-t border-border-default px-4 py-2 text-sm wrap-break-word text-muted-foreground"
>
<p v-if="promptTextReal">
{{ promptTextReal }}

View File

@@ -1,5 +1,5 @@
<template>
<section class="w-full flex flex-wrap gap-2 justify-end px-2 pb-2">
<section class="flex w-full flex-wrap justify-end gap-2 px-2 pb-2">
<Button :disabled variant="textonly" autofocus @click="$emit('cancel')">
{{ cancelTextX }}
</Button>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="flex items-center gap-2 p-4 font-bold text-sm text-base-foreground font-inter"
class="flex items-center gap-2 p-4 font-inter text-sm font-bold text-base-foreground"
>
<span v-if="title" class="flex-auto">{{ title }}</span>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<section
class="m-2 mt-4 flex flex-col gap-6 whitespace-pre-wrap wrap-break-word"
class="m-2 mt-4 flex flex-col gap-6 wrap-break-word whitespace-pre-wrap"
>
<div>
<span>{{ message }}</span>
@@ -40,12 +40,12 @@
v-if="doNotAskAgain"
keypath="missingModelsDialog.reEnableInSettings"
tag="span"
class="text-sm text-muted-foreground ml-8"
class="ml-8 text-sm text-muted-foreground"
>
<template #link>
<Button
variant="textonly"
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
class="cursor-pointer p-0 text-sm text-muted-foreground underline hover:bg-transparent"
@click="openBlueprintOverwriteSetting"
>
{{ t('missingModelsDialog.reEnableInSettingsLink') }}

View File

@@ -8,7 +8,7 @@
</p>
<div
class="flex max-h-[300px] flex-col overflow-y-auto rounded-lg bg-secondary-background scrollbar-custom"
class="flex scrollbar-custom max-h-[300px] flex-col overflow-y-auto rounded-lg bg-secondary-background"
>
<div
v-for="model in processedModels"
@@ -17,13 +17,13 @@
>
<div class="flex items-center gap-2 overflow-hidden">
<span
class="min-w-0 truncate text-sm text-foreground"
class="text-foreground min-w-0 truncate text-sm"
:title="model.name"
>
{{ model.name }}
</span>
<span
class="inline-flex h-4 shrink-0 items-center rounded-full bg-muted-foreground/20 px-1.5 text-xxxs font-semibold uppercase text-muted-foreground"
class="inline-flex h-4 shrink-0 items-center rounded-full bg-muted-foreground/20 px-1.5 text-xxxs font-semibold text-muted-foreground uppercase"
>
{{ model.badgeLabel }}
</span>
@@ -81,7 +81,7 @@
</div>
</div>
<p class="m-0 text-xs/5 text-muted-foreground whitespace-pre-line">
<p class="m-0 text-xs/5 whitespace-pre-line text-muted-foreground">
{{ $t('missingModelsDialog.footerDescription') }}
</p>
@@ -90,7 +90,7 @@
class="flex gap-3 rounded-lg border border-warning-background bg-warning-background/10 p-3"
>
<i
class="icon-[lucide--triangle-alert] mt-0.5 size-4 shrink-0 text-warning-background"
class="mt-0.5 icon-[lucide--triangle-alert] size-4 shrink-0 text-warning-background"
/>
<div class="flex flex-col gap-1">
<p class="m-0 text-xs/5 font-semibold text-warning-background">

View File

@@ -23,7 +23,7 @@
<!-- Section header with Replace button -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-xs font-semibold uppercase text-primary">
<span class="text-xs font-semibold text-primary uppercase">
{{ $t('nodeReplacement.quickFixAvailable') }}
</span>
<div class="size-2 rounded-full bg-primary" />
@@ -35,7 +35,7 @@
:disabled="selectedTypes.size === 0"
@click="handleReplaceSelected"
>
<i class="icon-[lucide--refresh-cw] mr-1.5 size-4" />
<i class="mr-1.5 icon-[lucide--refresh-cw] size-4" />
{{
$t('nodeReplacement.replaceSelected', {
count: selectedTypes.size
@@ -46,7 +46,7 @@
<!-- Replaceable nodes list -->
<div
class="flex max-h-[200px] flex-col overflow-y-auto rounded-lg bg-secondary-background scrollbar-custom"
class="flex scrollbar-custom max-h-[200px] flex-col overflow-y-auto rounded-lg bg-secondary-background"
>
<!-- Select All row (sticky header) -->
<div
@@ -55,7 +55,7 @@
'sticky top-0 z-10 flex items-center gap-3 border-b border-border-default bg-secondary-background px-3 py-2',
pendingNodes.length > 0
? 'cursor-pointer hover:bg-secondary-background-hover'
: 'opacity-50 pointer-events-none'
: 'pointer-events-none opacity-50'
)
"
tabindex="0"
@@ -77,14 +77,14 @@
>
<i
v-if="isAllSelected"
class="icon-[lucide--check] text-bold text-xs text-base-foreground"
class="text-bold icon-[lucide--check] text-xs text-base-foreground"
/>
<i
v-else-if="isSomeSelected"
class="icon-[lucide--minus] text-bold text-xs text-base-foreground"
class="text-bold icon-[lucide--minus] text-xs text-base-foreground"
/>
</div>
<span class="text-xs font-medium uppercase text-muted-foreground">
<span class="text-xs font-medium text-muted-foreground uppercase">
{{ $t('nodeReplacement.compatibleAlternatives') }}
</span>
</div>
@@ -97,7 +97,7 @@
cn(
'flex items-center gap-3 px-3 py-2',
replacedTypes.has(node.label)
? 'opacity-50 pointer-events-none'
? 'pointer-events-none opacity-50'
: 'cursor-pointer hover:bg-secondary-background-hover'
)
"
@@ -124,24 +124,24 @@
v-if="
replacedTypes.has(node.label) || selectedTypes.has(node.label)
"
class="icon-[lucide--check] text-bold text-xs text-base-foreground"
class="text-bold icon-[lucide--check] text-xs text-base-foreground"
/>
</div>
<div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2">
<span
v-if="replacedTypes.has(node.label)"
class="inline-flex h-4 items-center rounded-full border border-success bg-success/10 px-1.5 text-xxxs font-semibold uppercase text-success"
class="border-success bg-success/10 text-success inline-flex h-4 items-center rounded-full border px-1.5 text-xxxs font-semibold uppercase"
>
{{ $t('nodeReplacement.replaced') }}
</span>
<span
v-else
class="inline-flex h-4 items-center rounded-full border border-primary bg-primary/10 px-1.5 text-xxxs font-semibold uppercase text-primary"
class="inline-flex h-4 items-center rounded-full border border-primary bg-primary/10 px-1.5 text-xxxs font-semibold text-primary uppercase"
>
{{ $t('nodeReplacement.replaceable') }}
</span>
<span class="text-sm text-foreground">
<span class="text-foreground text-sm">
{{ node.label }}
</span>
</div>
@@ -160,7 +160,7 @@
>
<!-- Section header -->
<div class="flex items-center gap-2">
<span class="text-xs font-semibold uppercase text-error">
<span class="text-xs font-semibold text-error uppercase">
{{ $t('nodeReplacement.installationRequired') }}
</span>
<i class="icon-[lucide--info] text-xs text-error" />
@@ -168,7 +168,7 @@
<!-- Non-replaceable nodes list -->
<div
class="flex flex-col overflow-y-auto rounded-lg bg-secondary-background scrollbar-custom"
class="flex scrollbar-custom flex-col overflow-y-auto rounded-lg bg-secondary-background"
>
<div
v-for="node in nonReplaceableNodes"
@@ -179,11 +179,11 @@
<div class="flex flex-col gap-0.5">
<div class="flex items-center gap-2">
<span
class="inline-flex h-4 items-center rounded-full border border-error bg-error/10 px-1.5 text-xxxs font-semibold uppercase text-error"
class="inline-flex h-4 items-center rounded-full border border-error bg-error/10 px-1.5 text-xxxs font-semibold text-error uppercase"
>
{{ $t('nodeReplacement.notReplaceable') }}
</span>
<span class="text-sm text-foreground">
<span class="text-foreground text-sm">
{{ node.label }}
</span>
</div>
@@ -209,9 +209,9 @@
class="flex gap-3 rounded-lg border border-warning-background bg-warning-background/10 p-3"
>
<i
class="icon-[lucide--triangle-alert] mt-0.5 size-4 shrink-0 text-warning-background"
class="mt-0.5 icon-[lucide--triangle-alert] size-4 shrink-0 text-warning-background"
/>
<p class="m-0 text-xs/5 text-neutral-foreground">
<p class="text-neutral-foreground m-0 text-xs/5">
<i18n-t keypath="nodeReplacement.instructionMessage">
<template #red>
<span class="text-error">{{

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex w-full flex-col gap-2 py-2 px-4">
<div class="flex w-full flex-col gap-2 px-4 py-2">
<div class="flex flex-col gap-1 text-sm text-muted-foreground">
<div class="flex items-center gap-1">
<input
@@ -16,12 +16,12 @@
v-if="doNotAskAgain"
keypath="missingModelsDialog.reEnableInSettings"
tag="span"
class="text-sm text-muted-foreground ml-6"
class="ml-6 text-sm text-muted-foreground"
>
<template #link>
<Button
variant="textonly"
class="underline cursor-pointer p-0 text-sm text-muted-foreground hover:bg-transparent"
class="cursor-pointer p-0 text-sm text-muted-foreground underline hover:bg-transparent"
@click="openShowMissingNodesSetting"
>
{{ $t('missingModelsDialog.reEnableInSettingsLink') }}

View File

@@ -3,8 +3,8 @@
class="flex min-w-[460px] flex-col rounded-2xl border border-border-default bg-base-background shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)]"
>
<!-- Header -->
<div class="flex p-8 items-center justify-between">
<h2 class="text-lg font-bold text-base-foreground m-0">
<div class="flex items-center justify-between p-8">
<h2 class="m-0 text-lg font-bold text-base-foreground">
{{
isInsufficientCredits
? $t('credits.topUp.addMoreCreditsToRun')
@@ -12,7 +12,7 @@
}}
</h2>
<button
class="cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
class="focus-visible:ring-secondary-foreground cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:ring-1 focus-visible:outline-none"
@click="() => handleClose()"
>
<i class="icon-[lucide--x] size-6" />
@@ -20,7 +20,7 @@
</div>
<p
v-if="isInsufficientCredits"
class="text-sm text-muted-foreground m-0 px-8"
class="m-0 px-8 text-sm text-muted-foreground"
>
{{ $t('credits.topUp.insufficientWorkflowMessage') }}
</p>
@@ -39,7 +39,7 @@
size="lg"
:class="
cn(
'h-10 text-base font-medium w-full focus-visible:ring-secondary-foreground',
'focus-visible:ring-secondary-foreground h-10 w-full text-base font-medium',
selectedPreset === amount && 'bg-secondary-background-selected'
)
"
@@ -95,7 +95,7 @@
<p
v-if="isBelowMin"
class="text-sm text-red-500 m-0 px-8 pt-4 text-center flex items-center justify-center gap-1"
class="m-0 flex items-center justify-center gap-1 px-8 pt-4 text-center text-sm text-red-500"
>
<i class="icon-[lucide--component] size-4" />
{{
@@ -106,7 +106,7 @@
</p>
<p
v-if="showCeilingWarning"
class="text-sm text-gold-500 m-0 px-8 pt-4 text-center flex items-center justify-center gap-1"
class="m-0 flex items-center justify-center gap-1 px-8 pt-4 text-center text-sm text-gold-500"
>
<i class="icon-[lucide--component] size-4" />
{{
@@ -123,7 +123,7 @@
>
</p>
<div class="p-8 flex flex-col gap-8">
<div class="flex flex-col gap-8 p-8">
<Button
:disabled="!isValidAmount || loading"
:loading="loading"

View File

@@ -1,10 +1,10 @@
<template>
<div
class="flex items-center justify-between p-2 rounded-lg cursor-pointer transition-all duration-200"
class="flex cursor-pointer items-center justify-between rounded-lg p-2 transition-all duration-200"
:class="[
selected
? 'bg-secondary-background border-2 border-border-default'
: 'bg-component-node-disabled hover:bg-secondary-background border-2 border-transparent'
? 'border-2 border-border-default bg-secondary-background'
: 'bg-component-node-disabled border-2 border-transparent hover:bg-secondary-background'
]"
@click="$emit('select')"
>

View File

@@ -67,7 +67,7 @@
v-if="!isApiKeyLogin"
keypath="auth.deleteAccount.contactSupport"
tag="p"
class="text-muted text-sm"
class="text-sm text-muted"
>
<template #email>
<a href="mailto:support@comfy.org" class="underline"

View File

@@ -10,7 +10,7 @@
{{ $t('subscription.cancelDialog.title') }}
</h2>
<button
class="cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
class="focus-visible:ring-secondary-foreground cursor-pointer rounded-sm border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:ring-1 focus-visible:outline-none"
:aria-label="$t('g.close')"
:disabled="isLoading"
@click="onClose"

View File

@@ -4,7 +4,7 @@
enter-from-class="-translate-y-3 opacity-0"
enter-to-class="translate-y-0 opacity-100"
>
<div v-if="isVisible" class="flex justify-end w-full pointer-events-none">
<div v-if="isVisible" class="pointer-events-none flex w-full justify-end">
<div
role="alert"
aria-live="assertive"
@@ -32,12 +32,12 @@
<li
v-for="(message, idx) in groupedErrorMessages"
:key="idx"
class="flex items-baseline gap-2 text-sm/snug text-muted-foreground min-w-0"
class="flex min-w-0 items-baseline gap-2 text-sm/snug text-muted-foreground"
>
<span
class="mt-1.5 size-1 shrink-0 rounded-full bg-muted-foreground"
/>
<span class="wrap-break-word line-clamp-3 whitespace-pre-wrap">{{
<span class="line-clamp-3 wrap-break-word whitespace-pre-wrap">{{
message
}}</span>
</li>

View File

@@ -74,7 +74,7 @@ const pressed = ref(false)
<SliderThumb
:class="
cn(
'block size-4 shrink-0 cursor-grab rounded-full shadow-md ring-1 ring-black/25 top-1/2',
'top-1/2 block size-4 shrink-0 cursor-grab rounded-full shadow-md ring-1 ring-black/25',
'transition-[color,box-shadow,background-color]',
'before:absolute before:-inset-1.5 before:block before:rounded-full before:bg-transparent',
'hover:ring-2 hover:ring-black/40 focus-visible:ring-2 focus-visible:ring-black/40 focus-visible:outline-hidden',

View File

@@ -6,7 +6,7 @@
<template v-if="showUI" #workflow-tabs>
<div
v-if="workflowTabsPosition === 'Topbar'"
class="workflow-tabs-container pointer-events-auto relative w-full h-(--workflow-tabs-height)"
class="workflow-tabs-container pointer-events-auto relative h-(--workflow-tabs-height) w-full"
>
<!-- Native drag area for Electron -->
<div

View File

@@ -2,7 +2,7 @@
<ContextMenu
ref="contextMenu"
:model="menuItems"
class="max-h-[80vh] md:max-h-none overflow-y-auto md:overflow-y-visible"
class="max-h-[80vh] overflow-y-auto md:max-h-none md:overflow-y-visible"
@show="onMenuShow"
@hide="onMenuHide"
>

View File

@@ -23,7 +23,7 @@
:class="
isColorSubmenu
? 'flex flex-col gap-1 p-2'
: 'flex flex-col p-2 min-w-40'
: 'flex min-w-40 flex-col p-2'
"
>
<div
@@ -31,12 +31,12 @@
:key="subOption.label"
:class="
cn(
'hover:bg-secondary-background-hover rounded-sm cursor-pointer',
'cursor-pointer rounded-sm hover:bg-secondary-background-hover',
isColorSubmenu
? 'size-7 flex items-center justify-center'
? 'flex size-7 items-center justify-center'
: 'flex items-center gap-2 px-3 py-1.5 text-sm',
subOption.disabled
? 'cursor-not-allowed pointer-events-none text-node-icon-disabled'
? 'pointer-events-none cursor-not-allowed text-node-icon-disabled'
: 'hover:bg-secondary-background-hover'
)
"

View File

@@ -33,7 +33,7 @@
<span class="menu-label">{{ menuItem.label }}</span>
<i
v-if="menuItem.showExternalIcon"
class="icon-[lucide--external-link] text-primary size-4 ml-auto"
class="ml-auto icon-[lucide--external-link] size-4 text-primary"
/>
<i
v-if="menuItem.key === 'more'"

View File

@@ -31,10 +31,10 @@ function toggle() {
<div
:class="
cn(
'overflow-hidden transition-all duration-300 min-w-0 max-w-full',
'max-w-full min-w-0 overflow-hidden transition-all duration-300',
isExpanded
? 'w-full max-h-100 sm:w-[max(400px,40vw)]'
: 'w-0 max-h-0'
? 'max-h-100 w-full sm:w-[max(400px,40vw)]'
: 'max-h-0 w-0'
)
"
>

View File

@@ -16,7 +16,7 @@
:pt="{
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
class: cn(
'h-10 relative inline-flex cursor-pointer select-none',
'relative inline-flex h-10 cursor-pointer select-none',
'rounded-lg bg-secondary-background text-base-foreground',
'transition-all duration-200 ease-in-out',
'border-[2.5px] border-solid',
@@ -24,7 +24,7 @@
? 'border-node-component-border'
: 'border-transparent',
'focus-within:border-node-component-border',
{ 'opacity-60 cursor-default': props.disabled }
{ 'cursor-default opacity-60': props.disabled }
)
}),
labelContainer: {
@@ -62,7 +62,7 @@
// Option row hover and focus tone
option: ({ context }: MultiSelectPassThroughMethodOptions) => ({
class: cn(
'flex gap-2 items-center h-10 px-2 rounded-lg cursor-pointer',
'flex h-10 cursor-pointer items-center gap-2 rounded-lg px-2',
'hover:bg-secondary-background-hover',
// Add focus/highlight state for keyboard navigation
context?.focused &&
@@ -150,7 +150,7 @@
<template #option="slotProps">
<div
role="button"
class="flex items-center gap-2 cursor-pointer"
class="flex cursor-pointer items-center gap-2"
:style="popoverStyle"
>
<div

View File

@@ -40,7 +40,7 @@
},
overlay: {
class: cn(
'mt-2 p-2 rounded-lg',
'mt-2 rounded-lg p-2',
'bg-base-background text-base-foreground',
'border border-solid border-border-default'
)
@@ -57,7 +57,7 @@
option: ({ context }: SelectPassThroughMethodOptions<SelectOption>) => ({
class: cn(
// Row layout
'flex items-center justify-between gap-3 px-2 py-3 rounded-sm',
'flex items-center justify-between gap-3 rounded-sm px-2 py-3',
'hover:bg-secondary-background-hover',
// Add focus state for keyboard navigation
context.focused && 'bg-secondary-background-hover',

View File

@@ -1,5 +1,5 @@
<template>
<div class="relative show-slider">
<div class="show-slider relative">
<Button
v-tooltip.right="{ value: tooltipText, showDelay: 300 }"
size="icon"
@@ -12,7 +12,7 @@
</Button>
<div
v-show="showSlider"
class="absolute top-0 left-12 rounded-lg bg-interface-menu-surface p-4 shadow-lg w-[150px]"
class="absolute top-0 left-12 w-[150px] rounded-lg bg-interface-menu-surface p-4 shadow-lg"
>
<Slider
v-model="value"

View File

@@ -13,7 +13,7 @@
:class="
cn(
'rounded-full',
isRecording && 'text-red-500 recording-button-blink'
isRecording && 'recording-button-blink text-red-500'
)
"
:aria-label="

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex flex-col gap-3 pb-3">
<h3 class="text-center text-[15px] font-sans text-descrip-text mt-2.5">
<h3 class="text-descrip-text mt-2.5 text-center font-sans text-[15px]">
{{ t('maskEditor.brushSettings') }}
</h3>
@@ -10,19 +10,19 @@
<!-- Brush Shape -->
<div class="flex flex-col gap-3 pb-3">
<span class="text-left text-xs font-sans text-descrip-text">
<span class="text-descrip-text text-left font-sans text-xs">
{{ t('maskEditor.brushShape') }}
</span>
<div
class="flex flex-row gap-2.5 items-center h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
class="flex h-[50px] w-full flex-row items-center gap-2.5 rounded-[10px] bg-secondary-background-hover"
>
<div
class="maskEditor_sidePanelBrushShapeCircle hover:bg-comfy-menu-bg"
:class="
cn(
store.brushSettings.type === BrushShape.Arc
? 'bg-(--p-button-text-primary-color) active'
? 'active bg-(--p-button-text-primary-color)'
: 'bg-transparent'
)
"
@@ -34,7 +34,7 @@
:class="
cn(
store.brushSettings.type === BrushShape.Rect
? 'bg-(--p-button-text-primary-color) active'
? 'active bg-(--p-button-text-primary-color)'
: 'bg-transparent'
)
"
@@ -45,27 +45,27 @@
<!-- Color -->
<div class="flex flex-col gap-3 pb-3">
<span class="text-left text-xs font-sans text-descrip-text">
<span class="text-descrip-text text-left font-sans text-xs">
{{ t('maskEditor.colorSelector') }}
</span>
<input
ref="colorInputRef"
v-model="store.rgbColor"
type="color"
class="h-10 rounded-md cursor-pointer"
class="h-10 cursor-pointer rounded-md"
/>
</div>
<!-- Thickness -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
<span class="text-descrip-text text-left font-sans text-xs">
{{ t('maskEditor.thickness') }}
</span>
<input
v-model.number="brushSize"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
class="border-p-form-field-border-color text-input-text w-16 rounded-md border bg-comfy-menu-bg px-2 py-1 text-center text-sm"
:min="1"
:max="250"
:step="1"
@@ -84,13 +84,13 @@
<!-- Opacity -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
<span class="text-descrip-text text-left font-sans text-xs">
{{ t('maskEditor.opacity') }}
</span>
<input
v-model.number="brushOpacity"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
class="border-p-form-field-border-color text-input-text w-16 rounded-md border bg-comfy-menu-bg px-2 py-1 text-center text-sm"
:min="0"
:max="1"
:step="0.01"
@@ -109,13 +109,13 @@
<!-- Hardness -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
<span class="text-descrip-text text-left font-sans text-xs">
{{ t('maskEditor.hardness') }}
</span>
<input
v-model.number="brushHardness"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
class="border-p-form-field-border-color text-input-text w-16 rounded-md border bg-comfy-menu-bg px-2 py-1 text-center text-sm"
:min="0"
:max="1"
:step="0.01"
@@ -134,13 +134,13 @@
<!-- Step Size -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
<span class="text-descrip-text text-left font-sans text-xs">
{{ t('maskEditor.stepSize') }}
</span>
<input
v-model.number="brushStepSize"
type="number"
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
class="border-p-form-field-border-color text-input-text w-16 rounded-md border bg-comfy-menu-bg px-2 py-1 text-center text-sm"
:min="1"
:max="100"
:step="1"

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex flex-col gap-3 pb-3">
<h3 class="text-center text-[15px] font-sans text-(--descrip-text) mt-2.5">
<h3 class="mt-2.5 text-center font-sans text-[15px] text-(--descrip-text)">
{{ t('maskEditor.colorSelectSettings') }}
</h3>

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex flex-col gap-3 pb-3">
<h3 class="text-center text-[15px] font-sans text-(--descrip-text) mt-2.5">
<h3 class="mt-2.5 text-center font-sans text-[15px] text-(--descrip-text)">
{{ t('maskEditor.layers') }}
</h3>
@@ -13,12 +13,12 @@
@update:model-value="onMaskOpacityChange"
/>
<span class="text-left text-xs font-sans text-(--descrip-text)">{{
<span class="text-left font-sans text-xs text-(--descrip-text)">{{
t('maskEditor.maskBlendingOptions')
}}</span>
<div
class="flex flex-row gap-2.5 items-center min-h-6 relative h-[50px] w-full rounded-[10px] -mt-2 -mb-1.5"
class="relative -mt-2 -mb-1.5 flex h-[50px] min-h-6 w-full flex-row items-center gap-2.5 rounded-[10px]"
>
<select
class="maskEditor_sidePanelDropdown"
@@ -31,11 +31,11 @@
</select>
</div>
<span class="text-left text-xs font-sans text-(--descrip-text)">{{
<span class="text-left font-sans text-xs text-(--descrip-text)">{{
t('maskEditor.maskLayer')
}}</span>
<div
class="flex flex-row gap-2.5 items-center min-h-6 relative h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
class="relative flex h-[50px] min-h-6 w-full flex-row items-center gap-2.5 rounded-[10px] bg-secondary-background-hover"
:style="{
border: store.activeLayer === 'mask' ? '2px solid #007acc' : 'none'
}"
@@ -64,11 +64,11 @@
</button>
</div>
<span class="text-left text-xs font-sans text-(--descrip-text)">{{
<span class="text-left font-sans text-xs text-(--descrip-text)">{{
t('maskEditor.paintLayer')
}}</span>
<div
class="flex flex-row gap-2.5 items-center min-h-6 relative h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
class="relative flex h-[50px] min-h-6 w-full flex-row items-center gap-2.5 rounded-[10px] bg-secondary-background-hover"
:style="{
border: store.activeLayer === 'rgb' ? '2px solid #007acc' : 'none'
}"
@@ -104,11 +104,11 @@
</button>
</div>
<span class="text-left text-xs font-sans text-(--descrip-text)">{{
<span class="text-left font-sans text-xs text-(--descrip-text)">{{
t('maskEditor.baseImageLayer')
}}</span>
<div
class="flex flex-row gap-2.5 items-center min-h-6 relative h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
class="relative flex h-[50px] min-h-6 w-full flex-row items-center gap-2.5 rounded-[10px] bg-secondary-background-hover"
>
<input
type="checkbox"

View File

@@ -13,29 +13,29 @@
>
<canvas
ref="imgCanvasRef"
class="absolute top-0 left-0 size-full z-0"
class="absolute top-0 left-0 z-0 size-full"
@contextmenu.prevent
/>
<canvas
ref="rgbCanvasRef"
class="absolute top-0 left-0 size-full z-10"
class="absolute top-0 left-0 z-10 size-full"
@contextmenu.prevent
/>
<canvas
ref="maskCanvasRef"
class="absolute top-0 left-0 size-full z-30"
class="absolute top-0 left-0 z-30 size-full"
@contextmenu.prevent
/>
<!-- GPU Preview Canvas -->
<canvas
ref="gpuCanvasRef"
class="absolute top-0 left-0 size-full pointer-events-none"
class="pointer-events-none absolute top-0 left-0 size-full"
:class="{
'z-20': store.activeLayer === 'rgb',
'z-40': store.activeLayer === 'mask'
}"
/>
<div ref="canvasBackgroundRef" class="bg-white size-full" />
<div ref="canvasBackgroundRef" class="size-full bg-white" />
</div>
<div class="maskEditor-ui-container flex min-h-0 flex-1 flex-col">

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex flex-col gap-3 pb-3">
<h3 class="text-center text-[15px] font-sans text-(--descrip-text) mt-2.5">
<h3 class="mt-2.5 text-center font-sans text-[15px] text-(--descrip-text)">
{{ t('maskEditor.paintBucketSettings') }}
</h3>

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="pointerZoneRef"
class="w-[calc(100%-4rem-220px)] h-full"
class="h-full w-[calc(100%-4rem-220px)]"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"

View File

@@ -1,11 +1,11 @@
<template>
<div
class="flex flex-col gap-3 pb-3 h-full items-stretch! bg-(--comfy-menu-bg) overflow-y-auto w-55 px-2.5"
class="flex h-full w-55 flex-col items-stretch! gap-3 overflow-y-auto bg-(--comfy-menu-bg) px-2.5 pb-3"
>
<div class="w-full min-h-full">
<div class="min-h-full w-full">
<SettingsPanelContainer />
<div class="w-full h-0.5 bg-(--border-color) mt-6 mb-1.5" />
<div class="mt-6 mb-1.5 h-0.5 w-full bg-(--border-color)" />
<ImageLayerSettingsPanel :tool-manager="toolManager" />
</div>

View File

@@ -1,5 +1,5 @@
<template>
<div class="h-full z-8888 flex flex-col justify-between bg-comfy-menu-bg">
<div class="z-8888 flex h-full flex-col justify-between bg-comfy-menu-bg">
<div class="flex flex-col">
<div
v-for="tool in allTools"
@@ -19,7 +19,7 @@
</div>
<div
class="flex flex-col items-center cursor-pointer rounded-md mb-2 transition-colors duration-200 hover:bg-secondary-background-hover"
class="mb-2 flex cursor-pointer flex-col items-center rounded-md transition-colors duration-200 hover:bg-secondary-background-hover"
:title="t('maskEditor.clickToResetZoom')"
@click="onResetZoom"
>

View File

@@ -1,10 +1,10 @@
<template>
<div class="flex flex-row gap-2.5 items-center min-h-6 relative">
<span class="text-left text-xs font-sans text-(--descrip-text)">{{
<div class="relative flex min-h-6 flex-row items-center gap-2.5">
<span class="text-left font-sans text-xs text-(--descrip-text)">{{
label
}}</span>
<select
class="absolute right-0 h-6 px-1.5 rounded-md border border-border-default transition-colors duration-100 bg-secondary-background focus:outline focus:outline-node-component-border"
class="absolute right-0 h-6 rounded-md border border-border-default bg-secondary-background px-1.5 transition-colors duration-100 focus:outline focus:outline-node-component-border"
:value="modelValue"
@change="onChange"
>

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex flex-col gap-3 pb-3">
<span class="text-left text-xs font-sans text-(--descrip-text)">{{
<span class="text-left font-sans text-xs text-(--descrip-text)">{{
label
}}</span>
<input

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex flex-row gap-2.5 items-center min-h-6 relative">
<span class="text-left text-xs font-sans text-(--descrip-text)">{{
<div class="relative flex min-h-6 flex-row items-center gap-2.5">
<span class="text-left font-sans text-xs text-(--descrip-text)">{{
label
}}</span>
<label class="maskEditor_sidePanelToggleContainer">

View File

@@ -11,7 +11,7 @@
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-current"
class="pointer-events-none h-6.25 w-6.25 fill-current"
>
<path
d="M8.77,12.18c-.25,0-.46-.2-.46-.46s.2-.46.46-.46c1.47,0,2.67-1.2,2.67-2.67,0-1.57-1.34-2.67-3.26-2.67h-3.98l1.43,1.43c.18.18.18.47,0,.64-.18.18-.47.18-.64,0l-2.21-2.21c-.18-.18-.18-.47,0-.64l2.21-2.21c.18-.18.47-.18.64,0,.18.18.18.47,0,.64l-1.43,1.43h3.98c2.45,0,4.17,1.47,4.17,3.58,0,1.97-1.61,3.58-3.58,3.58Z"
@@ -26,7 +26,7 @@
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-(--input-text)"
class="pointer-events-none h-6.25 w-6.25 fill-(--input-text)"
>
<path
class="cls-1"
@@ -35,7 +35,7 @@
</svg>
</button>
<div class="h-5 border-l border-border" />
<div class="border-border h-5 border-l" />
<button
:class="iconButtonClass"
@@ -44,7 +44,7 @@
>
<svg
viewBox="-6 -7 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-(--input-text)"
class="pointer-events-none h-6.25 w-6.25 fill-(--input-text)"
>
<path
d="m2.25-2.625c.3452 0 .625.2798.625.625v5c0 .3452-.2798.625-.625.625h-5c-.3452 0-.625-.2798-.625-.625v-5c0-.3452.2798-.625.625-.625h5zm1.25.625v5c0 .6904-.5596 1.25-1.25 1.25h-5c-.6904 0-1.25-.5596-1.25-1.25v-5c0-.6904.5596-1.25 1.25-1.25h5c.6904 0 1.25.5596 1.25 1.25zm-.1673-2.3757-.4419.4419-1.5246-1.5246 1.5416-1.5417.442.4419-.7871.7872h.9373c1.3807 0 2.5 1.1193 2.5 2.5h-.625c0-1.0355-.8395-1.875-1.875-1.875h-.9375l.7702.7702z"
@@ -59,7 +59,7 @@
>
<svg
viewBox="-9 -7 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-(--input-text)"
class="pointer-events-none h-6.25 w-6.25 fill-(--input-text)"
>
<g transform="scale(-1, 1)">
<path
@@ -76,7 +76,7 @@
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-(--input-text)"
class="pointer-events-none h-6.25 w-6.25 fill-(--input-text)"
>
<path
d="M7.5,1.5c-.28,0-.5.22-.5.5v11c0,.28.22.5.5.5s.5-.22.5-.5v-11c0-.28-.22-.5-.5-.5Z"
@@ -92,7 +92,7 @@
>
<svg
viewBox="0 0 15 15"
class="h-6.25 w-6.25 pointer-events-none fill-(--input-text)"
class="pointer-events-none h-6.25 w-6.25 fill-(--input-text)"
>
<path
d="M2,7.5c0-.28.22-.5.5-.5h11c.28,0,.5.22.5.5s-.22.5-.5.5h-11c-.28,0-.5-.22-.5-.5Z"

View File

@@ -1,6 +1,6 @@
<template>
<div
class="flex w-50 flex-col overflow-hidden rounded-2xl bg-base-background border border-border-default"
class="flex w-50 flex-col overflow-hidden rounded-2xl border border-border-default bg-base-background"
>
<div ref="previewContainerRef" class="overflow-hidden p-3">
<div ref="previewWrapperRef" class="origin-top-left scale-50">
@@ -11,14 +11,14 @@
<!-- Content Section -->
<div class="flex flex-col gap-2 p-3 pt-1">
<!-- Title -->
<h3 class="text-xs font-semibold text-foreground m-0">
<h3 class="text-foreground m-0 text-xs font-semibold">
{{ nodeDef.display_name }}
</h3>
<!-- Category Path -->
<p
v-if="showCategoryPath && nodeDef.category"
class="text-xs text-muted-foreground -mt-1"
class="-mt-1 text-xs text-muted-foreground"
>
{{ nodeDef.category.replaceAll('/', ' > ') }}
</p>
@@ -32,7 +32,7 @@
<!-- Description -->
<p
v-if="nodeDef.description"
class="text-[11px] font-normal leading-normal text-muted-foreground m-0"
class="m-0 text-[11px] leading-normal font-normal text-muted-foreground"
>
{{ nodeDef.description }}
</p>
@@ -49,7 +49,7 @@
class="flex flex-col gap-1"
>
<h4
class="text-xxs font-semibold uppercase tracking-wide text-muted-foreground m-0"
class="m-0 text-xxs font-semibold tracking-wide text-muted-foreground uppercase"
>
{{ $t('nodeHelpPage.inputs') }}
</h4>
@@ -58,7 +58,7 @@
:key="input.name"
class="flex items-center justify-between gap-2 text-xxs"
>
<span class="shrink-0 text-foreground">{{ input.name }}</span>
<span class="text-foreground shrink-0">{{ input.name }}</span>
<span class="min-w-0 truncate text-muted-foreground">{{
input.type
}}</span>
@@ -71,7 +71,7 @@
class="flex flex-col gap-1"
>
<h4
class="text-xxs font-semibold uppercase tracking-wide text-muted-foreground m-0"
class="m-0 text-xxs font-semibold tracking-wide text-muted-foreground uppercase"
>
{{ $t('nodeHelpPage.outputs') }}
</h4>
@@ -80,7 +80,7 @@
:key="output.name"
class="flex items-center justify-between gap-2 text-xxs"
>
<span class="shrink-0 text-foreground">{{ output.name }}</span>
<span class="text-foreground shrink-0">{{ output.name }}</span>
<span class="min-w-0 truncate text-muted-foreground">{{
output.type
}}</span>

View File

@@ -29,7 +29,7 @@
<div
v-show="cursorVisible"
ref="cursorEl"
class="pointer-events-none absolute left-0 top-0 rounded-full border border-black/60 shadow-[0_0_0_1px_rgba(255,255,255,0.8)] will-change-transform"
class="pointer-events-none absolute top-0 left-0 rounded-full border border-black/60 shadow-[0_0_0_1px_rgba(255,255,255,0.8)] will-change-transform"
:style="cursorSizeStyle"
/>
</div>
@@ -109,7 +109,7 @@
class="flex-1"
@update:model-value="(v) => v?.length && (brushSize = v[0])"
/>
<span class="w-8 text-center text-xs text-node-text-muted">{{
<span class="text-node-text-muted w-8 text-center text-xs">{{
brushSize
}}</span>
</div>
@@ -127,7 +127,7 @@
<input
type="color"
:value="brushColorDisplay"
class="h-4 w-8 cursor-pointer appearance-none overflow-hidden rounded-full border-none bg-transparent [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:border-none [&::-webkit-color-swatch]:rounded-full [&::-moz-color-swatch]:border-none [&::-moz-color-swatch]:rounded-full"
class="h-4 w-8 cursor-pointer appearance-none overflow-hidden rounded-full border-none bg-transparent [&::-moz-color-swatch]:rounded-full [&::-moz-color-swatch]:border-none [&::-webkit-color-swatch]:rounded-full [&::-webkit-color-swatch]:border-none [&::-webkit-color-swatch-wrapper]:p-0"
@input="
(e) => (brushColorDisplay = (e.target as HTMLInputElement).value)
"
@@ -135,14 +135,14 @@
<span class="min-w-[4ch] truncate text-xs">{{
brushColorDisplay
}}</span>
<span class="ml-auto flex items-center text-xs text-node-text-muted">
<span class="text-node-text-muted ml-auto flex items-center text-xs">
<input
type="number"
:value="brushOpacityPercent"
min="0"
max="100"
step="1"
class="w-7 appearance-none border-0 bg-transparent text-right text-xs text-node-text-muted outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]"
class="text-node-text-muted w-7 appearance-none border-0 bg-transparent text-right text-xs outline-none [-moz-appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
@click.stop
@change="
(e) => {
@@ -177,7 +177,7 @@
(v) => v?.length && (brushHardnessPercent = v[0])
"
/>
<span class="w-8 text-center text-xs text-node-text-muted"
<span class="text-node-text-muted w-8 text-center text-xs"
>{{ brushHardnessPercent }}%</span
>
</div>
@@ -201,7 +201,7 @@
class="flex-1"
@update:model-value="(v) => v?.length && (canvasWidth = v[0])"
/>
<span class="w-10 text-center text-xs text-node-text-muted">{{
<span class="text-node-text-muted w-10 text-center text-xs">{{
canvasWidth
}}</span>
</div>
@@ -223,7 +223,7 @@
class="flex-1"
@update:model-value="(v) => v?.length && (canvasHeight = v[0])"
/>
<span class="w-10 text-center text-xs text-node-text-muted">{{
<span class="text-node-text-muted w-10 text-center text-xs">{{
canvasHeight
}}</span>
</div>
@@ -240,7 +240,7 @@
<input
type="color"
:value="backgroundColorDisplay"
class="h-4 w-8 cursor-pointer appearance-none overflow-hidden rounded-full border-none bg-transparent [&::-webkit-color-swatch-wrapper]:p-0 [&::-webkit-color-swatch]:border-none [&::-webkit-color-swatch]:rounded-full [&::-moz-color-swatch]:border-none [&::-moz-color-swatch]:rounded-full"
class="h-4 w-8 cursor-pointer appearance-none overflow-hidden rounded-full border-none bg-transparent [&::-moz-color-swatch]:rounded-full [&::-moz-color-swatch]:border-none [&::-webkit-color-swatch]:rounded-full [&::-webkit-color-swatch]:border-none [&::-webkit-color-swatch-wrapper]:p-0"
@input="
(e) =>
(backgroundColorDisplay = (e.target as HTMLInputElement).value)

View File

@@ -67,12 +67,12 @@
class="icon-[lucide--trash-2] size-4 shrink-0 self-center text-destructive-background"
/>
<span
class="flex flex-col items-start wrap-break-word text-left leading-tight"
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 text-text-secondary font-light">
<span class="text-xs font-light text-text-secondary">
{{
t(
'sideToolbar.queueProgressOverlay.clearHistoryMenuAssetsNote'

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="shouldShow" class="flex justify-end">
<div
class="flex items-center whitespace-nowrap text-[0.75rem] leading-[normal] drop-shadow-[1px_1px_8px_rgba(0,0,0,0.4)]"
class="flex items-center text-[0.75rem] leading-[normal] whitespace-nowrap drop-shadow-[1px_1px_8px_rgba(0,0,0,0.4)]"
role="status"
aria-live="polite"
aria-atomic="true"

View File

@@ -45,7 +45,7 @@
</div>
<div class="flex h-full items-center">
<span
class="overflow-hidden text-ellipsis text-center font-inter text-[12px] leading-normal font-normal text-base-foreground"
class="overflow-hidden text-center font-inter text-[12px] leading-normal font-normal text-ellipsis text-base-foreground"
>
{{ bannerText }}
</span>

View File

@@ -20,7 +20,7 @@
@update:selected-sort-mode="$emit('update:selectedSortMode', $event)"
/>
<div class="flex-1 min-h-0 overflow-y-auto">
<div class="min-h-0 flex-1 overflow-y-auto">
<JobAssetsList
:displayed-job-groups="displayedJobGroups"
@cancel-item="onCancelItemEvent"

View File

@@ -4,7 +4,7 @@
:class="['flex', 'justify-end', 'w-full', 'pointer-events-none']"
>
<div
class="pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
class="pointer-events-auto flex max-h-[60vh] w-[350px] min-w-[310px] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
:class="containerClass"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
@@ -15,7 +15,7 @@
v-model:selected-job-tab="selectedJobTab"
v-model:selected-workflow-filter="selectedWorkflowFilter"
v-model:selected-sort-mode="selectedSortMode"
class="flex-1 min-h-0"
class="min-h-0 flex-1"
:header-title="headerTitle"
:queued-count="queuedCount"
:displayed-job-groups="displayedJobGroups"

View File

@@ -1,11 +1,11 @@
<template>
<section
class="w-[360px] rounded-2xl border border-interface-stroke bg-interface-panel-surface text-text-primary shadow-interface font-inter"
class="w-[360px] rounded-2xl border border-interface-stroke bg-interface-panel-surface font-inter text-text-primary shadow-interface"
>
<header
class="flex items-center justify-between border-b border-interface-stroke p-4"
>
<p class="m-0 text-[14px] font-normal leading-none">
<p class="m-0 text-[14px] leading-none font-normal">
{{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogTitle') }}
</p>
<Button

View File

@@ -66,7 +66,7 @@
<div class="relative z-1 flex items-center gap-1">
<div class="relative inline-flex items-center justify-center">
<div
class="absolute left-1/2 top-1/2 size-10 -translate-1/2"
class="absolute top-1/2 left-1/2 size-10 -translate-1/2"
@mouseenter.stop="onIconEnter"
@mouseleave.stop="onIconLeave"
/>

Some files were not shown because too many files have changed in this diff Show More