mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-16 16:58:33 +00:00
Compare commits
5 Commits
playwright
...
fix/subgra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7faf9eafde | ||
|
|
e2cb3560cc | ||
|
|
9ae85068eb | ||
|
|
23bb5f2afa | ||
|
|
ef4e4a69d5 |
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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**')
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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('')
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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/)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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="{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]'
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">{{
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
"
|
||||
|
||||
@@ -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'"
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
:class="
|
||||
cn(
|
||||
'rounded-full',
|
||||
isRecording && 'text-red-500 recording-button-blink'
|
||||
isRecording && 'recording-button-blink text-red-500'
|
||||
)
|
||||
"
|
||||
:aria-label="
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user