Compare commits

..

2 Commits

Author SHA1 Message Date
bymyself
49e5207c47 fix: add h-full to LivePreview img to prevent clipping in overflow-hidden wrapper
Amp-Thread-ID: https://ampcode.com/threads/T-019cbd6e-8879-7461-9e85-1775c81626fe
2026-03-05 16:16:17 -08:00
bymyself
ce16416cd8 feat: support transparent background for preview components
Remove solid backgrounds (bg-node-component-surface, bg-muted-background)
from ImagePreview, VideoPreview, and LivePreview so images/videos with
alpha channels blend naturally with the node surface.

Add opt-in Comfy.Preview.CheckerboardBackground setting that renders a
checkerboard pattern behind previews for inspecting transparency.

- Add bg-checkerboard @utility with repeating-conic-gradient
- Add --checkerboard-color-1/2 tokens for light and dark themes
- Register setting in Zod schema and core settings

Amp-Thread-ID: https://ampcode.com/threads/T-019cbd5a-243d-751c-904e-1026454275f6
2026-03-05 16:16:17 -08:00
617 changed files with 4113 additions and 16885 deletions

View File

@@ -1,3 +0,0 @@
{
"posthog-js@*": { "licenses": "Apache-2.0" }
}

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
cache: 'pnpm'
- name: Update electron types

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies

View File

@@ -27,7 +27,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
cache: 'pnpm'
- name: Install dependencies

View File

@@ -26,7 +26,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
@@ -79,22 +79,3 @@ jobs:
exit 1
fi
echo '✅ No Mixpanel references found'
- name: Scan dist for PostHog telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for PostHog references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e '(?i)posthog\.init' \
-e '(?i)posthog\.capture' \
-e 'PostHogTelemetryProvider' \
-e 'ph\.comfy\.org' \
-e 'posthog-js' \
dist; then
echo '❌ ERROR: PostHog references found in dist assets!'
echo 'PostHog must be properly tree-shaken from OSS builds.'
exit 1
fi
echo '✅ No PostHog references found'

View File

@@ -27,7 +27,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
@@ -82,7 +82,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
@@ -100,7 +100,6 @@ jobs:
--production \
--summary \
--excludePackages '@comfyorg/comfyui-frontend;@comfyorg/design-system;@comfyorg/registry-types;@comfyorg/shared-frontend-utils;@comfyorg/tailwind-utils;@comfyorg/comfyui-electron-types' \
--clarificationsFile .github/license-clarifications.json \
--onlyAllow 'MIT;MIT*;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;BlueOak-1.0.0;Python-2.0;CC0-1.0;Unlicense;(MIT OR Apache-2.0);(MIT OR GPL-3.0);(Apache-2.0 OR MIT);(MPL-2.0 OR Apache-2.0);CC-BY-4.0;CC-BY-3.0;GPL-3.0-only'; then
echo ''
echo '✅ All production dependency licenses are approved!'

View File

@@ -13,8 +13,6 @@ on:
branches:
- 'cloud/*'
- 'main'
pull_request:
types: [labeled]
workflow_dispatch:
permissions: {}
@@ -25,31 +23,16 @@ concurrency:
jobs:
dispatch:
# Fork guard: prevent forks from dispatching to the cloud repo.
# For pull_request events, only dispatch when the 'preview' label is added.
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
(github.event_name != 'pull_request' ||
github.event.label.name == 'preview')
# Fork guard: prevent forks from dispatching to the cloud repo
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
steps:
- name: Build client payload
id: payload
env:
EVENT_NAME: ${{ github.event_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
if [ "${EVENT_NAME}" = "pull_request" ]; then
REF="${PR_HEAD_SHA}"
BRANCH="${PR_HEAD_REF}"
else
REF="${GITHUB_SHA}"
BRANCH="${GITHUB_REF_NAME}"
fi
payload="$(jq -nc \
--arg ref "${REF}" \
--arg branch "${BRANCH}" \
--arg ref "${GITHUB_SHA}" \
--arg branch "${GITHUB_REF_NAME}" \
'{ref: $ref, branch: $branch}')"
echo "json=${payload}" >> "${GITHUB_OUTPUT}"

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '20'
cache: 'pnpm'
- name: Install dependencies for analysis tools

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 22
- name: Download PR metadata
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '24.x'
- name: Read desktop-ui version
id: get_version

View File

@@ -91,7 +91,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '24.x'
cache: 'pnpm'
registry-url: https://registry.npmjs.org

View File

@@ -82,7 +82,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: 'frontend/.nvmrc'
node-version: lts/*
- name: Install dependencies
working-directory: frontend

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
- name: Check version bump type
id: check_version

View File

@@ -26,7 +26,7 @@ jobs:
version: 10
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
- name: Get current version

View File

@@ -82,7 +82,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
registry-url: https://registry.npmjs.org

View File

@@ -22,7 +22,7 @@ jobs:
version: 10
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: 'lts/*'
cache: 'pnpm'
- name: Get current version

View File

@@ -149,7 +149,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: lts/*
- name: Bump version
id: bump-version

View File

@@ -58,7 +58,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '24.x'
cache: 'pnpm'
- name: Bump desktop-ui version

View File

@@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
node-version: '20'
cache: 'pnpm'
- name: Install dependencies for analysis tools

View File

@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- Node.js (see `.nvmrc`, currently v24) and pnpm
- Node.js (v24) and pnpm
- Git for version control
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)

View File

@@ -1,7 +1,10 @@
<template>
<div ref="rootEl" class="relative size-full overflow-hidden bg-neutral-900">
<div class="p-terminal size-full rounded-none p-2">
<div ref="terminalEl" class="terminal-host h-full" />
<div
ref="rootEl"
class="relative overflow-hidden h-full w-full bg-neutral-900"
>
<div class="p-terminal rounded-none h-full w-full p-2">
<div ref="terminalEl" class="h-full terminal-host" />
</div>
<Button
v-tooltip.left="{
@@ -13,7 +16,7 @@
size="small"
:class="
cn('absolute top-2 right-8 transition-opacity', {
'pointer-events-none opacity-0 select-none': !isHovered
'opacity-0 pointer-events-none select-none': !isHovered
})
"
:aria-label="tooltipText"

View File

@@ -1,12 +1,12 @@
<template>
<div class="mx-auto flex w-full max-w-3xl flex-col gap-8 select-none">
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
<!-- Installation Path Section -->
<div class="flex grow flex-col gap-6 text-neutral-300">
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
<div class="grow flex flex-col gap-6 text-neutral-300">
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
{{ $t('install.locationPicker.title') }}
</h2>
<p class="px-12 text-center text-neutral-400">
<p class="text-center text-neutral-400 px-12">
{{ $t('install.locationPicker.subtitle') }}
</p>
@@ -15,7 +15,7 @@
<InputText
v-model="installPath"
:placeholder="$t('install.locationPicker.pathPlaceholder')"
class="flex-1 border-neutral-700 bg-neutral-800/50 text-neutral-200 placeholder:text-neutral-500"
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
:class="{ 'p-invalid': pathError }"
@update:model-value="validatePath"
@focus="onFocus"
@@ -23,7 +23,7 @@
<Button
icon="pi pi-folder-open"
severity="secondary"
class="border-0 bg-neutral-700 hover:bg-neutral-600"
class="bg-neutral-700 hover:bg-neutral-600 border-0"
@click="browsePath"
/>
</div>
@@ -33,7 +33,7 @@
<Message
v-if="pathError"
severity="error"
class="w-full whitespace-pre-line"
class="whitespace-pre-line w-full"
>
{{ pathError }}
</Message>

View File

@@ -26,7 +26,7 @@
<img
v-if="task.headerImg"
:src="task.headerImg"
class="size-full object-contain px-4 pt-4 opacity-25"
class="h-full w-full object-contain px-4 pt-4 opacity-25"
/>
</template>
<template #title>
@@ -52,7 +52,7 @@
<i
v-if="!isLoading && runner.state === 'OK'"
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 z-10 col-span-full row-span-full text-[4rem] text-green-500 opacity-100 transition-opacity [text-shadow:0.25rem_0_0.5rem_black] group-hover/task-card:opacity-20"
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 col-span-full row-span-full z-10 text-[4rem] text-green-500 opacity-100 transition-opacity group-hover/task-card:opacity-20 [text-shadow:0.25rem_0_0.5rem_black]"
/>
</div>
</template>

View File

@@ -4,7 +4,7 @@
<template v-if="filter.tasks.length === 0">
<!-- Empty filter -->
<Divider />
<p class="w-full text-center text-neutral-400">
<p class="text-neutral-400 w-full text-center">
{{ $t('maintenance.allOk') }}
</p>
</template>
@@ -25,7 +25,7 @@
<!-- Display: Cards -->
<template v-else>
<div class="pad-y my-4 flex flex-wrap justify-evenly gap-8">
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
<TaskCard
v-for="task in filter.tasks"
:key="task.id"
@@ -45,8 +45,7 @@ import { useConfirm, useToast } from 'primevue'
import ConfirmPopup from 'primevue/confirmpopup'
import Divider from 'primevue/divider'
import { useI18n } from 'vue-i18n'
import { t } from '@/i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type {
MaintenanceFilter,
@@ -56,7 +55,6 @@ import type {
import TaskCard from './TaskCard.vue'
import TaskListItem from './TaskListItem.vue'
const { t } = useI18n()
const toast = useToast()
const confirm = useConfirm()
const taskStore = useMaintenanceTaskStore()
@@ -82,7 +80,8 @@ const executeTask = async (task: MaintenanceTask) => {
toast.add({
severity: 'error',
summary: t('maintenance.error.toastTitle'),
detail: message ?? t('maintenance.error.defaultDescription')
detail: message ?? t('maintenance.error.defaultDescription'),
life: 10_000
})
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="flex size-full flex-col justify-between rounded-lg p-6">
<h1 class="m-0 font-inter text-xl font-semibold italic">
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
<h1 class="font-inter font-semibold text-xl m-0 italic">
{{ $t(`desktopDialogs.${id}.title`, title) }}
</h1>
<p class="whitespace-pre-wrap">

View File

@@ -1,7 +1,7 @@
<template>
<BaseViewTemplate dark>
<div
class="grid h-screen w-screen items-center justify-around overflow-y-auto"
class="h-screen w-screen grid items-center justify-around overflow-y-auto"
>
<div class="relative m-8 text-center">
<!-- Header -->
@@ -13,7 +13,7 @@
<span>{{ $t('desktopUpdate.description') }}</span>
</div>
<ProgressSpinner class="m-8 size-48" />
<ProgressSpinner class="m-8 w-48 h-48" />
<!-- Console button -->
<Button

View File

@@ -1,10 +1,10 @@
<template>
<BaseViewTemplate dark>
<!-- Fixed height container with flexbox layout for proper content management -->
<div class="flex size-full flex-col">
<div class="w-full h-full flex flex-col">
<Stepper
v-model:value="currentStep"
class="flex h-full flex-col"
class="flex flex-col h-full"
@update:value="handleStepChange"
>
<!-- Main content area that grows to fill available space -->
@@ -37,7 +37,7 @@
<!-- Install footer with navigation -->
<InstallFooter
class="mx-auto my-6 w-full max-w-2xl"
class="w-full max-w-2xl my-6 mx-auto"
:current-step
:can-proceed
:disable-location-step="noGpu"

View File

@@ -1,21 +1,21 @@
<template>
<BaseViewTemplate dark>
<div
class="dark-theme grid h-screen min-h-full w-screen min-w-full justify-around overflow-y-auto bg-neutral-900 font-sans text-neutral-300"
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme overflow-y-auto"
>
<div class="relative m-8 w-screen max-w-(--breakpoint-sm)">
<div class="max-w-(--breakpoint-sm) w-screen m-8 relative">
<!-- Header -->
<h1 class="backspan pi-wrench text-4xl font-bold">
{{ t('maintenance.title') }}
</h1>
<!-- Toolbar -->
<div class="flex w-full flex-wrap items-center gap-4">
<div class="w-full flex flex-wrap gap-4 items-center">
<span class="grow">
{{ t('maintenance.status') }}:
<StatusTag :refreshing="isRefreshing" :error="anyErrors" />
</span>
<div class="flex items-center gap-4">
<div class="flex gap-4 items-center">
<SelectButton
v-model="displayAsList"
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
@@ -56,10 +56,10 @@
:value="t('icon.exclamation-triangle')"
/>
<span>
<strong class="mb-1 block">
<strong class="block mb-1">
{{ t('maintenance.unsafeMigration.title') }}
</strong>
<span class="mb-1 block">
<span class="block mb-1">
{{ unsafeReasonText }}
</span>
<span class="block text-sm text-neutral-400">
@@ -71,13 +71,13 @@
<!-- Tasks -->
<TaskListPanel
class="border-x-0 border-y border-solid border-neutral-700"
class="border-neutral-700 border-solid border-x-0 border-y"
:filter
:display-as-list
/>
<!-- Actions -->
<div class="flex flex-row justify-between gap-4">
<div class="flex justify-between gap-4 flex-row">
<Button
:label="t('maintenance.consoleLogs')"
icon="pi pi-desktop"
@@ -189,7 +189,8 @@ const completeValidation = async () => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('maintenance.error.cannotContinue')
detail: t('maintenance.error.cannotContinue'),
life: 5_000
})
}
}

View File

@@ -1,8 +1,8 @@
<template>
<BaseViewTemplate dark hide-language-selector>
<div class="flex h-full flex-col items-center justify-center p-8 2xl:p-16">
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
<div
class="flex w-full max-w-[600px] flex-col gap-6 rounded-lg bg-neutral-800 p-6 shadow-lg"
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
>
<h2 class="text-3xl font-semibold text-neutral-100">
{{ $t('install.helpImprove') }}
@@ -15,7 +15,7 @@
<a
href="https://comfy.org/privacy"
target="_blank"
class="text-blue-400 underline hover:text-blue-300"
class="text-blue-400 hover:text-blue-300 underline"
>
{{ $t('install.privacyPolicy') }} </a
>.
@@ -33,7 +33,7 @@
}}
</span>
</div>
<div class="flex justify-end pt-6">
<div class="flex pt-6 justify-end">
<Button
:label="$t('g.ok')"
icon="pi pi-check"
@@ -72,7 +72,8 @@ const updateConsent = async () => {
toast.add({
severity: 'error',
summary: t('install.settings.errorUpdatingConsent'),
detail: t('install.settings.errorUpdatingConsentDetail')
detail: t('install.settings.errorUpdatingConsentDetail'),
life: 3000
})
} finally {
isUpdating.value = false

View File

@@ -9,7 +9,7 @@
/>
<div class="no-drag sad-text flex items-center">
<div class="flex min-w-110 flex-col gap-8 p-8">
<div class="flex flex-col gap-8 p-8 min-w-110">
<!-- Header -->
<h1 class="text-4xl font-bold text-red-500">
{{ $t('notSupported.title') }}
@@ -20,7 +20,7 @@
<p class="text-xl">
{{ $t('notSupported.message') }}
</p>
<ul class="list-inside list-disc space-y-1 text-neutral-800">
<ul class="list-disc list-inside space-y-1 text-neutral-800">
<li>{{ $t('notSupported.supportedDevices.macos') }}</li>
<li>{{ $t('notSupported.supportedDevices.windows') }}</li>
</ul>

View File

@@ -2,14 +2,14 @@
<BaseViewTemplate dark>
<div class="relative min-h-screen">
<!-- Terminal Background Layer (always visible during loading) -->
<div v-if="!isError" class="fixed inset-0 z-0 overflow-hidden">
<div class="size-full">
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
<div class="h-full w-full">
<BaseTerminal @created="terminalCreated" />
</div>
</div>
<!-- Semi-transparent overlay -->
<div v-if="!isError" class="fixed inset-0 z-5 bg-neutral-900/80"></div>
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
<!-- Smooth radial gradient overlay -->
<div
@@ -45,9 +45,9 @@
<!-- Error Section (positioned at bottom) -->
<div
v-if="isError"
class="absolute inset-x-0 bottom-20 flex flex-col items-center gap-4"
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
>
<div class="flex justify-center gap-4">
<div class="flex gap-4 justify-center">
<Button
icon="pi pi-flag"
:label="$t('serverStart.reportIssue')"
@@ -71,10 +71,10 @@
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
<div
v-if="terminalVisible && isError"
class="absolute inset-x-4 bottom-4 z-10 mx-auto max-w-4xl"
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
>
<div
class="h-[300px] rounded-lg border border-neutral-700 bg-neutral-900/95 p-4"
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
>
<BaseTerminal @created="terminalCreated" />
</div>

View File

@@ -27,8 +27,7 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
### Node.js & Playwright Prerequisites
Ensure you have the Node.js version from `.nvmrc` installed (currently v24).
Then, set up the Chromium test driver:
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
```bash
pnpm exec playwright install chromium --with-deps

View File

@@ -36,7 +36,14 @@
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"cnr_id": "comfy-core",
"ver": "0.3.65"
"ver": "0.3.65",
"models": [
{
"name": "v1-5-pruned-emaonly-fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
"directory": "checkpoints"
}
]
},
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
},

View File

@@ -206,7 +206,9 @@ export class ComfyPage {
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.runButton = page.getByTestId(TestIds.topbar.queueButton)
this.runButton = page
.getByTestId(TestIds.topbar.queueButton)
.getByRole('button', { name: 'Run' })
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
@@ -430,10 +432,7 @@ export const comfyPageFixture = base.extend<{
'Comfy.VueNodes.AutoScaleLayout': false,
// Disable toast warning about version compatibility, as they may or
// may not appear - depending on upstream ComfyUI dependencies
'Comfy.VersionCompatibility.DisableWarnings': true,
// Browser tests should opt into missing-model warnings explicitly so
// workflows do not render differently based on models present on disk.
'Comfy.Workflow.ShowMissingModelsWarning': false
'Comfy.VersionCompatibility.DisableWarnings': true
})
} catch (e) {
console.error(e)

View File

@@ -172,19 +172,6 @@ export class VueNodeHelpers {
async enterSubgraph(nodeId?: string): Promise<void> {
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
// The footer tab button extends below the node body (visible area),
// but its bounding box center overlaps the node body div.
// Click at the bottom 25% of the button which is the genuinely visible
// and unobstructed area outside the node body boundary.
const box = await editButton.boundingBox()
if (!box) {
throw new Error(
'subgraph-enter-button has no bounding box: element may be hidden or not in DOM'
)
}
await editButton.click({
position: { x: box.width / 2, y: box.height * 0.75 }
})
await editButton.click()
}
}

View File

@@ -33,7 +33,6 @@ export const TestIds = {
},
topbar: {
queueButton: 'queue-button',
queueModeMenuTrigger: 'queue-mode-menu-trigger',
saveButton: 'save-workflow-button'
},
nodeLibrary: {

View File

@@ -29,10 +29,8 @@ class ComfyQueueButton {
public readonly dropdownButton: Locator
constructor(public readonly actionbar: ComfyActionbar) {
this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton)
this.primaryButton = this.root
this.dropdownButton = actionbar.root.getByTestId(
TestIds.topbar.queueModeMenuTrigger
)
this.primaryButton = this.root.locator('.p-splitbutton-button')
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
}
public async toggleOptions() {

View File

@@ -89,17 +89,6 @@ test.describe('Execution error', () => {
})
test.describe('Missing models warning', () => {
test('Should be disabled by default in browser tests', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('missing/missing_models')
const dialogTitle = comfyPage.page.getByText(
'This workflow is missing models'
)
await expect(dialogTitle).not.toBeVisible()
})
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -819,13 +819,16 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
const activeWorkflowName =
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
const workflowPathA = `${workflowA}.json`
const workflowPathB = `${workflowB}.json`
expect(openWorkflows).toEqual(
expect.arrayContaining([workflowA, workflowB])
expect.arrayContaining([workflowPathA, workflowPathB])
)
expect(openWorkflows.indexOf(workflowA)).toBeLessThan(
openWorkflows.indexOf(workflowB)
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
openWorkflows.indexOf(workflowPathB)
)
expect(activeWorkflowName).toEqual(workflowB)
expect(activeWorkflowName).toEqual(workflowPathB)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -29,27 +29,12 @@ 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
}) => {
const shouldUpload = filesWithUpload.has(fileName)
const uploadRequestPromise = shouldUpload
? comfyPage.page.waitForRequest((req) =>
req.url().includes('/upload/')
)
: null
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
if (uploadRequestPromise) {
const request = await uploadRequestPromise
expect(request.url()).toContain('/upload/')
} else {
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
}
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -13,9 +13,9 @@ test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
})
test('loads from inserted workflow', async ({ comfyPage }) => {
const workflowName = 'single_connected_reroute_node'
const workflowName = 'single_connected_reroute_node.json'
await comfyPage.workflow.setupWorkflowsDirectory({
[`${workflowName}.json`]: `links/${workflowName}.json`
[workflowName]: 'links/single_connected_reroute_node.json'
})
await comfyPage.setup()
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -21,12 +21,14 @@ test.describe('Workflows sidebar', () => {
test('Can create new blank workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow',
'*Unsaved Workflow (2)'
'*Unsaved Workflow.json',
'*Unsaved Workflow (2).json'
])
})
@@ -39,37 +41,37 @@ test.describe('Workflows sidebar', () => {
const tab = comfyPage.menu.workflowsTab
await tab.open()
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
expect.arrayContaining(['workflow1', 'workflow2'])
expect.arrayContaining(['workflow1.json', 'workflow2.json'])
)
})
test('Can duplicate workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1')
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
expect.arrayContaining(['workflow1'])
expect.arrayContaining(['workflow1.json'])
)
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1',
'*workflow1 (Copy)'
'workflow1.json',
'*workflow1 (Copy).json'
])
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1',
'*workflow1 (Copy)',
'*workflow1 (Copy) (2)'
'workflow1.json',
'*workflow1 (Copy).json',
'*workflow1 (Copy) (2).json'
])
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1',
'*workflow1 (Copy)',
'*workflow1 (Copy) (2)',
'*workflow1 (Copy) (3)'
'workflow1.json',
'*workflow1 (Copy).json',
'*workflow1 (Copy) (2).json',
'*workflow1 (Copy) (3).json'
])
})
@@ -83,12 +85,12 @@ test.describe('Workflows sidebar', () => {
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
await tab.insertWorkflow(tab.getPersistedItem('workflow1'))
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
await expect
.poll(() => comfyPage.nodeOps.getNodeCount())
.toEqual(originalNodeCount + 1)
await tab.getPersistedItem('workflow1').click()
await tab.getPersistedItem('workflow1.json').click()
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
})
@@ -111,22 +113,22 @@ test.describe('Workflows sidebar', () => {
const openedWorkflow = tab.getOpenedItem('foo/bar')
await tab.renameWorkflow(openedWorkflow, 'foo/baz')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow',
'foo/baz'
'*Unsaved Workflow.json',
'foo/baz.json'
])
})
test('Can save workflow as', async ({ comfyPage }) => {
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', 'workflow3'])
.toEqual(['*Unsaved Workflow.json', 'workflow3.json'])
await comfyPage.menu.topbar.saveWorkflowAs('workflow4')
await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', 'workflow3', 'workflow4'])
.toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
})
test('Exported workflow does not contain localized slot names', async ({
@@ -182,15 +184,15 @@ test.describe('Workflows sidebar', () => {
})
test('Can save workflow as with same name', async ({ comfyPage }) => {
await comfyPage.menu.topbar.saveWorkflow('workflow5')
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5'
'workflow5.json'
])
await comfyPage.menu.topbar.saveWorkflowAs('workflow5')
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.confirmDialog.click('overwrite')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5'
'workflow5.json'
])
})
@@ -210,25 +212,25 @@ test.describe('Workflows sidebar', () => {
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
const topbar = comfyPage.menu.topbar
await topbar.saveWorkflow('workflow1')
await topbar.saveWorkflowAs('workflow2')
await topbar.saveWorkflow('workflow1.json')
await topbar.saveWorkflowAs('workflow2.json')
await comfyPage.nextFrame()
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow1', 'workflow2'])
.toEqual(['workflow1.json', 'workflow2.json'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow2')
.toEqual('workflow2.json')
await topbar.saveWorkflowAs('workflow1')
await topbar.saveWorkflowAs('workflow1.json')
await comfyPage.confirmDialog.click('overwrite')
// The old workflow1 should be deleted and the new one should be saved.
// The old workflow1.json should be deleted and the new one should be saved.
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow2', 'workflow1'])
.toEqual(['workflow2.json', 'workflow1.json'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow1')
.toEqual('workflow1.json')
})
test('Does not report warning when switching between opened workflows', async ({
@@ -264,15 +266,17 @@ test.describe('Workflows sidebar', () => {
)
await closeButton.click()
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow'
'*Unsaved Workflow.json'
])
})
test('Can close saved workflow with command', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1')
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
})
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
@@ -280,7 +284,7 @@ test.describe('Workflows sidebar', () => {
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18'
const filename = 'workflow18.json'
await topbar.saveWorkflow(filename)
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
@@ -291,14 +295,14 @@ test.describe('Workflows sidebar', () => {
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow'
'*Unsaved Workflow.json'
])
})
test('Can delete workflows', async ({ comfyPage }) => {
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18'
const filename = 'workflow18.json'
await topbar.saveWorkflow(filename)
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
@@ -310,7 +314,7 @@ test.describe('Workflows sidebar', () => {
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow'
'*Unsaved Workflow.json'
])
})
@@ -322,11 +326,16 @@ test.describe('Workflows sidebar', () => {
const { workflowsTab } = comfyPage.menu
await workflowsTab.open()
await workflowsTab.getPersistedItem('workflow1').click({ button: 'right' })
await workflowsTab
.getPersistedItem('workflow1.json')
.click({ button: 'right' })
await comfyPage.contextMenu.clickMenuItem('Duplicate')
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', '*workflow1 (Copy)'])
await comfyPage.nextFrame()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'*workflow1 (Copy).json'
])
})
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
@@ -338,7 +347,7 @@ test.describe('Workflows sidebar', () => {
// Wait for workflow to appear in Browse section after sync
const workflowItem =
comfyPage.menu.workflowsTab.getPersistedItem('workflow1')
comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json')
await expect(workflowItem).toBeVisible({ timeout: 3000 })
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
@@ -355,7 +364,7 @@ test.describe('Workflows sidebar', () => {
}
await comfyPage.page.dragAndDrop(
'.comfyui-workflows-browse .node-label:has-text("workflow1")',
'.comfyui-workflows-browse .node-label:has-text("workflow1.json")',
'#graph-canvas',
{ targetPosition }
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -22,10 +22,8 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
const checkpointNode = comfyPage.page
.locator('[data-node-id]')
.filter({ hasText: 'Load Checkpoint' })
.getByTestId('node-inner-wrapper')
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -43,14 +41,8 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
const checkpointNode = comfyPage.page
.locator('[data-node-id]')
.filter({ hasText: 'Load Checkpoint' })
.getByTestId('node-inner-wrapper')
const ksamplerNode = comfyPage.page
.locator('[data-node-id]')
.filter({ hasText: 'KSampler' })
.getByTestId('node-inner-wrapper')
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

@@ -3,7 +3,7 @@ import {
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const ERROR_CLASS = /ring-destructive-background/
const ERROR_CLASS = /border-node-stroke-error/
test.describe('Vue Node Error', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -18,10 +18,9 @@ test.describe('Vue Node Error', () => {
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
// Expect error state on missing unknown node
const unknownNode = comfyPage.page
.locator('[data-node-id]')
.filter({ hasText: 'UNKNOWN NODE' })
.getByTestId('node-inner-wrapper')
const unknownNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'UNKNOWN NODE'
})
await expect(unknownNode).toHaveClass(ERROR_CLASS)
})
@@ -32,10 +31,7 @@ test.describe('Vue Node Error', () => {
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
await comfyPage.runButton.click()
const raiseErrorNode = comfyPage.page
.locator('[data-node-id]')
.filter({ hasText: 'Raise Error' })
.getByTestId('node-inner-wrapper')
const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error')
await expect(raiseErrorNode).toHaveClass(ERROR_CLASS)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

@@ -127,8 +127,10 @@ 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': 'error',
'better-tailwindcss/enforce-canonical-classes': 'error',
'better-tailwindcss/enforce-consistent-class-order': 'off',
// Off: large batch change (v3→v4 renames like rounded→rounded-sm),
// enable and apply with `eslint --fix` in a follow-up PR
'better-tailwindcss/enforce-canonical-classes': 'off',
'better-tailwindcss/no-deprecated-classes': 'error'
}
},

2
global.d.ts vendored
View File

@@ -33,8 +33,6 @@ interface Window {
gtm_container_id?: string
ga_measurement_id?: string
mixpanel_token?: string
posthog_project_token?: string
posthog_api_host?: string
require_whitelist?: boolean
subscription_required?: boolean
max_upload_size?: number

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.42.2",
"version": "1.41.12",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",
@@ -60,7 +60,6 @@
"@comfyorg/registry-types": "workspace:*",
"@comfyorg/shared-frontend-utils": "workspace:*",
"@comfyorg/tailwind-utils": "workspace:*",
"@formkit/auto-animate": "catalog:",
"@iconify/json": "catalog:",
"@primeuix/forms": "catalog:",
"@primeuix/styled": "catalog:",
@@ -101,7 +100,6 @@
"loglevel": "^1.9.2",
"marked": "^15.0.11",
"pinia": "catalog:",
"posthog-js": "catalog:",
"primeicons": "catalog:",
"primevue": "catalog:",
"reka-ui": "catalog:",
@@ -195,9 +193,6 @@
"zip-dir": "^2.0.0",
"zod-to-json-schema": "catalog:"
},
"engines": {
"node": "24.x"
},
"pnpm": {
"overrides": {
"vite": "catalog:"

View File

@@ -290,6 +290,8 @@
--border-subtle: var(--color-smoke-400);
--muted-background: var(--color-smoke-700);
--accent-background: var(--color-smoke-800);
--checkerboard-color-1: var(--color-white);
--checkerboard-color-2: #cccccc;
/* Component/Node tokens from design system light */
--component-node-background: var(--color-white);
@@ -437,6 +439,8 @@
--border-subtle: var(--color-charcoal-300);
--muted-background: var(--color-charcoal-100);
--accent-background: var(--color-charcoal-100);
--checkerboard-color-1: #3a3a3a;
--checkerboard-color-2: #2a2a2a;
/* Component/Node tokens from design dark system */
--component-node-background: var(--color-charcoal-600);
@@ -667,6 +671,14 @@
}
/* =================== End Custom Scrollbar (cross-browser) =================== */
@utility bg-checkerboard {
background-image: repeating-conic-gradient(
var(--checkerboard-color-1) 0% 25%,
var(--checkerboard-color-2) 0% 50%
);
background-size: 16px 16px;
}
/* Everthing below here to be cleaned up over time. */
body {

293
pnpm-lock.yaml generated
View File

@@ -15,9 +15,6 @@ catalogs:
'@eslint/js':
specifier: ^9.39.1
version: 9.39.1
'@formkit/auto-animate':
specifier: ^0.9.0
version: 0.9.0
'@iconify-json/lucide':
specifier: ^1.1.178
version: 1.2.79
@@ -264,9 +261,6 @@ catalogs:
postcss-html:
specifier: ^1.8.0
version: 1.8.0
posthog-js:
specifier: ^1.358.1
version: 1.358.1
pretty-bytes:
specifier: ^7.1.0
version: 7.1.0
@@ -395,9 +389,6 @@ importers:
'@comfyorg/tailwind-utils':
specifier: workspace:*
version: link:packages/tailwind-utils
'@formkit/auto-animate':
specifier: 'catalog:'
version: 0.9.0
'@iconify/json':
specifier: 'catalog:'
version: 2.2.380
@@ -518,9 +509,6 @@ importers:
pinia:
specifier: 'catalog:'
version: 3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))
posthog-js:
specifier: 'catalog:'
version: 1.358.1
primeicons:
specifier: 'catalog:'
version: 7.0.0
@@ -776,7 +764,7 @@ importers:
version: 8.0.5(vite@8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))(vue@3.5.13(typescript@5.9.3))
vitest:
specifier: 'catalog:'
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
version: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vue-component-type-helpers:
specifier: 'catalog:'
version: 3.2.5
@@ -2191,9 +2179,6 @@ packages:
'@floating-ui/vue@1.1.9':
resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
'@formkit/auto-animate@0.9.0':
resolution: {integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==}
'@grpc/grpc-js@1.9.15':
resolution: {integrity: sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==}
engines: {node: ^8.13.0 || >=10.10.0}
@@ -2451,21 +2436,25 @@ packages:
resolution: {integrity: sha512-D+tPXB0tkSuDPsuXvyQIsF3f3PBWfAwIe9FkBWtVoDVYqE+jbz+tVGsjQMNWGafLE4sC8ZQdjhsxyT8I53Anbw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@nx/nx-linux-arm64-musl@22.5.2':
resolution: {integrity: sha512-UbO527qqa8KLBi13uXto5SmxcZv1Smer7sPexJonshDlmrJsyvx5m8nm6tcSv04W5yQEL90vPlTux8dNvEDWrw==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@nx/nx-linux-x64-gnu@22.5.2':
resolution: {integrity: sha512-wR6596Vr/Z+blUAmjLHG2TCQMs4O1oi9JXK1J/PoPeO9UqdHwStCJBAd61zDFSUYJe0x+dkeRQu96fE5BW8Kcg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@nx/nx-linux-x64-musl@22.5.2':
resolution: {integrity: sha512-MBXOw4AH4FWl4orwVykj/e75awTNDePogrl3pXNX9NcQLdj6JzS4e2jaALQeRBQLxQzeFvFQV/W4PBzoPV6/NA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@nx/nx-win32-arm64-msvc@22.5.2':
resolution: {integrity: sha512-SaWSZkRH5uV8vP2lj6RRv+kw2IzaIDXkutReOXpooshIWZl9KjrQELNTCZTYyhLDsMlcyhSvLFlTiA4NkZ8udw==}
@@ -2513,78 +2502,6 @@ packages:
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
'@opentelemetry/api-logs@0.208.0':
resolution: {integrity: sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==}
engines: {node: '>=8.0.0'}
'@opentelemetry/api@1.9.0':
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
'@opentelemetry/core@2.2.0':
resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/core@2.6.0':
resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/exporter-logs-otlp-http@0.208.0':
resolution: {integrity: sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-exporter-base@0.208.0':
resolution: {integrity: sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/otlp-transformer@0.208.0':
resolution: {integrity: sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': ^1.3.0
'@opentelemetry/resources@2.2.0':
resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/resources@2.6.0':
resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/sdk-logs@0.208.0':
resolution: {integrity: sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.4.0 <1.10.0'
'@opentelemetry/sdk-metrics@2.2.0':
resolution: {integrity: sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.9.0 <1.10.0'
'@opentelemetry/sdk-trace-base@2.2.0':
resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==}
engines: {node: ^18.19.0 || >=20.6.0}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/semantic-conventions@1.40.0':
resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==}
engines: {node: '>=14'}
'@oxc-project/runtime@0.112.0':
resolution: {integrity: sha512-4vYtWXMnXM6EaweCxbJ6bISAhkNHeN33SihvuX3wrpqaSJA4ZEoW35i9mSvE74+GDf1yTeVE+aEHA+WBpjDk/g==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2631,41 +2548,49 @@ packages:
resolution: {integrity: sha512-SVjjjtMW66Mza76PBGJLqB0KKyFTBnxmtDXLJPbL6ZPGSctcXVmujz7/WAc0rb9m2oV0cHQTtVjnq6orQnI/jg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-arm64-musl@11.15.0':
resolution: {integrity: sha512-JDv2/AycPF2qgzEiDeMJCcSzKNDm3KxNg0KKWipoKEMDFqfM7LxNwwSVyAOGmrYlE4l3dg290hOMsr9xG7jv9g==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-linux-ppc64-gnu@11.15.0':
resolution: {integrity: sha512-zbu9FhvBLW4KJxo7ElFvZWbSt4vP685Qc/Gyk/Ns3g2gR9qh2qWXouH8PWySy+Ko/qJ42+HJCLg+ZNcxikERfg==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-riscv64-gnu@11.15.0':
resolution: {integrity: sha512-Kfleehe6B09C2qCnyIU01xLFqFXCHI4ylzkicfX/89j+gNHh9xyNdpEvit88Kq6i5tTGdavVnM6DQfOE2qNtlg==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-riscv64-musl@11.15.0':
resolution: {integrity: sha512-J7LPiEt27Tpm8P+qURDwNc8q45+n+mWgyys4/V6r5A8v5gDentHRGUx3iVk5NxdKhgoGulrzQocPTZVosq25Eg==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-linux-s390x-gnu@11.15.0':
resolution: {integrity: sha512-+8/d2tAScPjVJNyqa7GPGnqleTB/XW9dZJQ2D/oIM3wpH3TG+DaFEXBbk4QFJ9K9AUGBhvQvWU2mQyhK/yYn3Q==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-gnu@11.15.0':
resolution: {integrity: sha512-xtvSzH7Nr5MCZI2FKImmOdTl9kzuQ51RPyLh451tvD2qnkg3BaqI9Ox78bTk57YJhlXPuxWSOL5aZhKAc9J6qg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-musl@11.15.0':
resolution: {integrity: sha512-14YL1zuXj06+/tqsuUZuzL0T425WA/I4nSVN1kBXeC5WHxem6lQ+2HGvG+crjeJEqHgZUT62YIgj88W+8E7eyg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-openharmony-arm64@11.15.0':
resolution: {integrity: sha512-/7Qli+1Wk93coxnrQaU8ySlICYN8HsgyIrzqjgIkQEpI//9eUeaeIHZptNl2fMvBGeXa7k2QgLbRNaBRgpnvMw==}
@@ -2739,48 +2664,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-arm64-musl@0.34.0':
resolution: {integrity: sha512-H+F8+71gHQoGTFPPJ6z4dD0Fzfzi0UP8Zx94h5kUmIFThLvMq5K1Y/bUUubiXwwHfwb5C3MPjUpYijiy0rj51Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-ppc64-gnu@0.34.0':
resolution: {integrity: sha512-dIGnzTNhCXqQD5pzBwduLg8pClm+t8R53qaE9i5h8iua1iaFAJyLffh4847CNZSlASb7gn1Ofuv7KoG/EpoGZg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-gnu@0.34.0':
resolution: {integrity: sha512-FGQ2GTTooilDte/ogwWwkHuuL3lGtcE3uKM2EcC7kOXNWdUfMY6Jx3JCodNVVbFoybv4A+HuCj8WJji2uu1Ceg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-riscv64-musl@0.34.0':
resolution: {integrity: sha512-2dGbGneJ7ptOIVKMwEIHdCkdZEomh74X3ggo4hCzEXL/rl9HwfsZDR15MkqfQqAs6nVXMvtGIOMxjDYa5lwKaA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxfmt/binding-linux-s390x-gnu@0.34.0':
resolution: {integrity: sha512-cCtGgmrTrxq3OeSG0UAO+w6yLZTMeOF4XM9SAkNrRUxYhRQELSDQ/iNPCLyHhYNi38uHJQbS5RQweLUDpI4ajA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-gnu@0.34.0':
resolution: {integrity: sha512-7AvMzmeX+k7GdgitXp99GQoIV/QZIpAS7rwxQvC/T541yWC45nwvk4mpnU8N+V6dE5SPEObnqfhCjO80s7qIsg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxfmt/binding-linux-x64-musl@0.34.0':
resolution: {integrity: sha512-uNiglhcmivJo1oDMh3hoN/Z0WsbEXOpRXZdQ3W/IkOpyV8WF308jFjSC1ZxajdcNRXWej0zgge9QXba58Owt+g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxfmt/binding-openharmony-arm64@0.34.0':
resolution: {integrity: sha512-5eFsTjCyji25j6zznzlMc+wQAZJoL9oWy576xhqd2efv+N4g1swIzuSDcb1dz4gpcVC6veWe9pAwD7HnrGjLwg==}
@@ -2883,48 +2816,56 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-arm64-musl@1.49.0':
resolution: {integrity: sha512-xeqkMOARgGBlEg9BQuPDf6ZW711X6BT5qjDyeM5XNowCJeTSdmMhpePJjTEiVbbr3t21sIlK8RE6X5bc04nWyQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-ppc64-gnu@1.49.0':
resolution: {integrity: sha512-uvcqRO6PnlJGbL7TeePhTK5+7/JXbxGbN+C6FVmfICDeeRomgQqrfVjf0lUrVpUU8ii8TSkIbNdft3M+oNlOsQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-gnu@1.49.0':
resolution: {integrity: sha512-Dw1HkdXAwHNH+ZDserHP2RzXQmhHtpsYYI0hf8fuGAVCIVwvS6w1+InLxpPMY25P8ASRNiFN3hADtoh6lI+4lg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-riscv64-musl@1.49.0':
resolution: {integrity: sha512-EPlMYaA05tJ9km/0dI9K57iuMq3Tw+nHst7TNIegAJZrBPtsOtYaMFZEaWj02HA8FI5QvSnRHMt+CI+RIhXJBQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@oxlint/binding-linux-s390x-gnu@1.49.0':
resolution: {integrity: sha512-yZiQL9qEwse34aMbnMb5VqiAWfDY+fLFuoJbHOuzB1OaJZbN1MRF9Nk+W89PIpGr5DNPDipwjZb8+Q7wOywoUQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-gnu@1.49.0':
resolution: {integrity: sha512-CcCDwMMXSchNkhdgvhVn3DLZ4EnBXAD8o8+gRzahg+IdSt/72y19xBgShJgadIRF0TsRcV/MhDUMwL5N/W54aQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxlint/binding-linux-x64-musl@1.49.0':
resolution: {integrity: sha512-u3HfKV8BV6t6UCCbN0RRiyqcymhrnpunVmLFI8sEa5S/EBu+p/0bJ3D7LZ2KT6PsBbrB71SWq4DeFrskOVgIZg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxlint/binding-openharmony-arm64@1.49.0':
resolution: {integrity: sha512-dRDpH9fw+oeUMpM4br0taYCFpW6jQtOuEIec89rOgDA1YhqwmeRcx0XYeCv7U48p57qJ1XZHeMGM9LdItIjfzA==}
@@ -2988,12 +2929,6 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@posthog/core@1.23.2':
resolution: {integrity: sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ==}
'@posthog/types@1.358.1':
resolution: {integrity: sha512-SFfhm+NHYqsk+SAxx5FlSg9FuvqsEPZidfTjPP5TYYM24fif//L+pAzxVGqaxJcnyZojIfF66NRZ3NfM5Jd+eg==}
'@primeuix/forms@0.0.2':
resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==}
engines: {node: '>=12.11.0'}
@@ -3093,24 +3028,28 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.3':
resolution: {integrity: sha512-Z03/wrqau9Bicfgb3Dbs6SYTHliELk2PM2LpG2nFd+cGupTMF5kanLEcj2vuuJLLhptNyS61rtk7SOZ+lPsTUA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.3':
resolution: {integrity: sha512-iSXXZsQp08CSilff/DCTFZHSVEpEwdicV3W8idHyrByrcsRDVh9sGC3sev6d8BygSGj3vt8GvUKBPCoyMA4tgQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.3':
resolution: {integrity: sha512-qaj+MFudtdCv9xZo9znFvkgoajLdc+vwf0Kz5N44g+LU5XMe+IsACgn3UG7uTRlCCvhMAGXm1XlpEA5bZBrOcw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.3':
resolution: {integrity: sha512-U662UnMETyjT65gFmG9ma+XziENrs7BBnENi/27swZPYagubfHRirXHG2oMl+pEax2WvO7Kb9gHZmMakpYqBHQ==}
@@ -3188,56 +3127,67 @@ packages:
resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.53.5':
resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.53.5':
resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.53.5':
resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.53.5':
resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.53.5':
resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.53.5':
resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.53.5':
resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.53.5':
resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.53.5':
resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.53.5':
resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.53.5':
resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==}
@@ -3513,24 +3463,28 @@ packages:
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.0':
resolution: {integrity: sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==}
engines: {node: '>= 20'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.0':
resolution: {integrity: sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.0':
resolution: {integrity: sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==}
engines: {node: '>= 20'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.0':
resolution: {integrity: sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==}
@@ -4006,41 +3960,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -4907,9 +4869,6 @@ packages:
core-js-compat@3.48.0:
resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==}
core-js@3.48.0:
resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==}
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -5593,9 +5552,6 @@ packages:
picomatch:
optional: true
fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
@@ -6416,24 +6372,28 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
@@ -7188,12 +7148,6 @@ packages:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
posthog-js@1.358.1:
resolution: {integrity: sha512-teipwLZtfErKDrURiUlLMnmpjgjGlni15JxyJ7oRaSlT3sX4E/mgvNatHIbWnp+7z1zYm3Jz5BYwGqwgyesRnw==}
preact@10.28.4:
resolution: {integrity: sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ==}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -7355,9 +7309,6 @@ packages:
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
query-selector-shadow-dom@1.0.1:
resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -8479,9 +8430,6 @@ packages:
web-vitals@4.2.4:
resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==}
web-vitals@5.1.0:
resolution: {integrity: sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
@@ -10172,8 +10120,6 @@ snapshots:
- '@vue/composition-api'
- vue
'@formkit/auto-animate@0.9.0': {}
'@grpc/grpc-js@1.9.15':
dependencies:
'@grpc/proto-loader': 0.7.13
@@ -10678,7 +10624,7 @@ snapshots:
tsconfig-paths: 4.2.0
tslib: 2.8.1
vite: 8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- '@babel/traverse'
- '@swc-node/register'
@@ -10698,7 +10644,7 @@ snapshots:
tslib: 2.8.1
optionalDependencies:
vite: 8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- '@babel/traverse'
- '@swc-node/register'
@@ -10727,82 +10673,6 @@ snapshots:
'@one-ini/wasm@0.1.1': {}
'@opentelemetry/api-logs@0.208.0':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api@1.9.0': {}
'@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.40.0
'@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.40.0
'@opentelemetry/exporter-logs-otlp-http@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-exporter-base@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/otlp-transformer@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-metrics': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0)
protobufjs: 7.5.0
'@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.40.0
'@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.40.0
'@opentelemetry/sdk-logs@0.208.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-metrics@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.40.0
'@opentelemetry/semantic-conventions@1.40.0': {}
'@oxc-project/runtime@0.112.0': {}
'@oxc-project/types@0.112.0': {}
@@ -11034,12 +10904,6 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@posthog/core@1.23.2':
dependencies:
cross-spawn: 7.0.6
'@posthog/types@1.358.1': {}
'@primeuix/forms@0.0.2':
dependencies:
'@primeuix/utils': 0.3.2
@@ -12119,7 +11983,7 @@ snapshots:
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
transitivePeerDependencies:
- supports-color
@@ -12182,7 +12046,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
vitest: 4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
'@vitest/utils@3.2.4':
dependencies:
@@ -13105,8 +12969,6 @@ snapshots:
dependencies:
browserslist: 4.28.1
core-js@3.48.0: {}
core-util-is@1.0.3: {}
cosmiconfig@7.1.0:
@@ -13929,8 +13791,6 @@ snapshots:
optionalDependencies:
picomatch: 4.0.3
fflate@0.4.8: {}
fflate@0.8.2: {}
figures@3.2.0:
@@ -15856,24 +15716,6 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-js@1.358.1:
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.0)
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
'@posthog/core': 1.23.2
'@posthog/types': 1.358.1
core-js: 3.48.0
dompurify: 3.3.1
fflate: 0.4.8
preact: 10.28.4
query-selector-shadow-dom: 1.0.1
web-vitals: 5.1.0
preact@10.28.4: {}
prelude-ls@1.2.1: {}
prettier@3.7.4:
@@ -16115,8 +15957,6 @@ snapshots:
quansync@0.2.11: {}
query-selector-shadow-dom@1.0.1: {}
queue-microtask@1.2.3: {}
raf-schd@4.0.3: {}
@@ -17422,7 +17262,7 @@ snapshots:
tsx: 4.19.4
yaml: 2.8.2
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
vitest@4.0.16(@types/node@24.10.4)(@vitest/ui@4.0.16)(esbuild@0.27.3)(happy-dom@20.0.11)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.16
'@vitest/mocker': 4.0.16(vite@8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))
@@ -17445,7 +17285,6 @@ snapshots:
vite: 8.0.0-beta.13(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/node': 24.10.4
'@vitest/ui': 4.0.16(vitest@4.0.16)
happy-dom: 20.0.11
@@ -17569,8 +17408,6 @@ snapshots:
web-vitals@4.2.4: {}
web-vitals@5.1.0: {}
webidl-conversions@3.0.1: {}
webidl-conversions@8.0.0: {}

View File

@@ -6,7 +6,6 @@ catalog:
'@alloc/quick-lru': ^5.2.0
'@comfyorg/comfyui-electron-types': 0.6.2
'@eslint/js': ^9.39.1
'@formkit/auto-animate': ^0.9.0
'@iconify-json/lucide': ^1.1.178
'@iconify/json': ^2.2.380
'@iconify/tailwind4': ^1.2.0
@@ -89,7 +88,6 @@ catalog:
picocolors: ^1.1.1
pinia: ^3.0.4
postcss-html: ^1.8.0
posthog-js: ^1.358.1
pretty-bytes: ^7.1.0
primeicons: ^7.0.0
primevue: ^4.2.5

View File

@@ -1,13 +1,13 @@
<template>
<router-view />
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
<div
v-if="isLoading"
class="pointer-events-none fixed inset-0 z-1200 flex items-center justify-center"
class="absolute inset-0 flex items-center justify-center"
>
<LogoComfyWaveLoader size="xl" color="yellow" />
<Loader size="lg" class="text-white" />
</div>
<GlobalDialog />
<BlockUI full-screen :blocked="isLoading" />
</template>
<script setup lang="ts">
@@ -15,7 +15,7 @@ import { captureException } from '@sentry/vue'
import BlockUI from 'primevue/blockui'
import { computed, onMounted } from 'vue'
import LogoComfyWaveLoader from '@/components/loader/LogoComfyWaveLoader.vue'
import Loader from '@/components/common/Loader.vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { useWorkspaceStore } from '@/stores/workspaceStore'

View File

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

View File

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

View File

@@ -166,22 +166,13 @@ describe('TopMenuSection', () => {
})
describe('authentication state', () => {
function createLegacyTabBarWrapper() {
const pinia = createTestingPinia({ createSpy: vi.fn })
const settingStore = useSettingStore(pinia)
vi.mocked(settingStore.get).mockImplementation((key) =>
key === 'Comfy.UI.TabBarLayout' ? 'Legacy' : undefined
)
return createWrapper({ pinia })
}
describe('when user is logged in', () => {
beforeEach(() => {
mockData.isLoggedIn = true
})
it('should display CurrentUserButton and not display LoginButton', () => {
const wrapper = createLegacyTabBarWrapper()
const wrapper = createWrapper()
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(true)
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
})
@@ -195,7 +186,7 @@ describe('TopMenuSection', () => {
describe('on desktop platform', () => {
it('should display LoginButton and not display CurrentUserButton', () => {
mockData.isDesktop = true
const wrapper = createLegacyTabBarWrapper()
const wrapper = createWrapper()
expect(wrapper.findComponent(LoginButton).exists()).toBe(true)
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
})
@@ -203,7 +194,7 @@ describe('TopMenuSection', () => {
describe('on web platform', () => {
it('should not display CurrentUserButton and not display LoginButton', () => {
const wrapper = createLegacyTabBarWrapper()
const wrapper = createWrapper()
expect(wrapper.findComponent(CurrentUserButton).exists()).toBe(false)
expect(wrapper.findComponent(LoginButton).exists()).toBe(false)
})

View File

@@ -34,18 +34,26 @@
</Button>
</div>
<div ref="actionbarContainerRef" :class="actionbarContainerClass">
<div
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',
hasAnyError
? 'border-destructive-background-hover'
: 'border-interface-stroke'
)
"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar
:top-menu-container="actionbarContainerRef"
:queue-overlay-expanded="isQueueOverlayExpanded"
:has-any-error="hasAnyError"
@update:progress-target="updateProgressTarget"
/>
<CurrentUserButton
@@ -53,19 +61,6 @@
class="shrink-0"
/>
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
<Button
v-if="isCloud && flags.workflowSharingEnabled"
v-tooltip.bottom="shareTooltipConfig"
variant="secondary"
:aria-label="t('actionbar.shareTooltip')"
@click="() => openShareDialog().catch(toastErrorHandler)"
@pointerenter="prefetchShareDialog"
>
<i class="icon-[lucide--share-2] size-4" />
<span class="not-md:hidden">
{{ t('actionbar.share') }}
</span>
</Button>
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
@@ -93,7 +88,7 @@
:to="inlineProgressSummaryTarget"
>
<div
class="pointer-events-none absolute inset-x-0 top-full mt-1 flex justify-end pr-1"
class="pointer-events-none absolute left-0 right-0 top-full mt-1 flex justify-end pr-1"
>
<QueueInlineProgressSummary
:hidden="shouldHideInlineProgressSummary"
@@ -114,7 +109,7 @@
</template>
<script setup lang="ts">
import { useLocalStorage, useMutationObserver } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -136,16 +131,10 @@ import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
import { useQueueUIStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isCloud, isDesktop } from '@/platform/distribution/types'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import {
openShareDialog,
prefetchShareDialog
} from '@/platform/workflow/sharing/composables/lazyShareDialog'
import { isDesktop } from '@/platform/distribution/types'
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
@@ -155,12 +144,10 @@ const settingStore = useSettingStore()
const workspaceStore = useWorkspaceStore()
const rightSidePanelStore = useRightSidePanelStore()
const managerState = useManagerState()
const { flags } = useFeatureFlags()
const { isLoggedIn } = useCurrentUser()
const { t } = useI18n()
const { toastErrorHandler } = useErrorHandling()
const executionErrorStore = useExecutionErrorStore()
const actionBarButtonStore = useActionBarButtonStore()
const queueUIStore = useQueueUIStore()
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
const { shouldShowRedDot: shouldShowConflictRedDot } =
@@ -175,45 +162,8 @@ const isActionbarEnabled = computed(
const isActionbarFloating = computed(
() => isActionbarEnabled.value && !isActionbarDocked.value
)
/**
* Whether the actionbar container has any visible docked buttons
* (excluding ComfyActionbar, which uses position:fixed when floating
* and does not contribute to the container's visual layout).
*/
const hasDockedButtons = computed(() => {
if (actionBarButtonStore.buttons.length > 0) return true
if (hasLegacyContent.value) return true
if (isLoggedIn.value && !isIntegratedTabBar.value) return true
if (isDesktop && !isIntegratedTabBar.value) return true
if (isCloud && flags.workflowSharingEnabled) return true
if (!isRightSidePanelOpen.value) return true
return false
})
const isActionbarContainerEmpty = computed(
() => isActionbarFloating.value && !hasDockedButtons.value
)
const actionbarContainerClass = computed(() => {
const base =
'actionbar-container pointer-events-auto relative flex h-12 items-center gap-2 rounded-lg border bg-comfy-menu-bg shadow-interface'
if (isActionbarContainerEmpty.value) {
return cn(
base,
'-ml-2 w-0 min-w-0 border-transparent shadow-none',
'has-[.border-dashed]:ml-0 has-[.border-dashed]:w-auto has-[.border-dashed]:min-w-auto',
'has-[.border-dashed]:border-interface-stroke has-[.border-dashed]:pl-2 has-[.border-dashed]:shadow-interface'
)
}
const borderClass =
!isActionbarFloating.value && hasAnyError.value
? 'border-destructive-background-hover'
: 'border-interface-stroke'
return cn(base, 'px-2', borderClass)
})
const isIntegratedTabBar = computed(
() => settingStore.get('Comfy.UI.TabBarLayout') !== 'Legacy'
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
)
const { isQueuePanelV2Enabled, isRunProgressBarEnabled } =
useQueueFeatureFlags()
@@ -245,9 +195,6 @@ const shouldHideInlineProgressSummary = computed(
const customNodesManagerTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.manageExtensions'))
)
const shareTooltipConfig = computed(() =>
buildTooltipConfig(t('actionbar.shareTooltip'))
)
const shouldShowRedDot = computed((): boolean => {
return shouldShowConflictRedDot.value
@@ -263,25 +210,6 @@ const rightSidePanelTooltipConfig = computed(() =>
// Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>()
const hasLegacyContent = ref(false)
function checkLegacyContent() {
const el = legacyCommandsContainerRef.value
if (!el) {
hasLegacyContent.value = false
return
}
// Mirror the CSS: [&:not(:has(*>*:not(:empty)))]:hidden
hasLegacyContent.value =
el.querySelector(':scope > * > *:not(:empty)') !== null
}
useMutationObserver(legacyCommandsContainerRef, checkLegacyContent, {
childList: true,
subtree: true,
characterData: true
})
onMounted(() => {
if (legacyCommandsContainerRef.value) {
app.menu.element.style.width = 'fit-content'

View File

@@ -1,98 +0,0 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import { useQueueSettingsStore } from '@/stores/queueStore'
import BatchCountEdit from './BatchCountEdit.vue'
const maxBatchCount = 16
vi.mock('@/platform/settings/settingStore', () => ({
useSettingStore: () => ({
get: (settingId: string) =>
settingId === 'Comfy.QueueButton.BatchCountLimit' ? maxBatchCount : 1
})
}))
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: {
increment: 'Increment',
decrement: 'Decrement'
},
menu: {
batchCount: 'Batch Count'
}
}
}
})
function createWrapper(initialBatchCount = 1) {
const pinia = createTestingPinia({
createSpy: vi.fn,
stubActions: false,
initialState: {
queueSettingsStore: {
batchCount: initialBatchCount
}
}
})
const wrapper = mount(BatchCountEdit, {
global: {
plugins: [pinia, i18n],
directives: {
tooltip: () => {}
}
}
})
const queueSettingsStore = useQueueSettingsStore()
return { wrapper, queueSettingsStore }
}
describe('BatchCountEdit', () => {
it('doubles the current batch count when increment is clicked', async () => {
const { wrapper, queueSettingsStore } = createWrapper(3)
await wrapper.get('button[aria-label="Increment"]').trigger('click')
expect(queueSettingsStore.batchCount).toBe(6)
})
it('halves the current batch count when decrement is clicked', async () => {
const { wrapper, queueSettingsStore } = createWrapper(9)
await wrapper.get('button[aria-label="Decrement"]').trigger('click')
expect(queueSettingsStore.batchCount).toBe(4)
})
it('clamps typed values to queue limits on blur', async () => {
const { wrapper, queueSettingsStore } = createWrapper(2)
const input = wrapper.get('input')
await input.setValue('999')
await input.trigger('blur')
await nextTick()
expect(queueSettingsStore.batchCount).toBe(maxBatchCount)
expect((input.element as HTMLInputElement).value).toBe(
String(maxBatchCount)
)
await input.setValue('0')
await input.trigger('blur')
await nextTick()
expect(queueSettingsStore.batchCount).toBe(1)
expect((input.element as HTMLInputElement).value).toBe('1')
})
})

View File

@@ -1,129 +1,71 @@
<template>
<div
v-tooltip.bottom="{
value: t('menu.batchCount'),
value: $t('menu.batchCount'),
showDelay: 600
}"
class="batch-count h-full"
:aria-label="t('menu.batchCount')"
class="batch-count"
:aria-label="$t('menu.batchCount')"
>
<div
class="flex h-full w-14 overflow-hidden rounded-l-lg bg-secondary-background"
>
<input
ref="batchCountInputRef"
v-model="batchCountInput"
type="text"
inputmode="numeric"
:aria-label="t('menu.batchCount')"
:class="inputClass"
@focus="onInputFocus"
@input="onInput"
@blur="onInputBlur"
@keydown.enter.prevent="onInputEnter"
/>
<div class="flex h-full w-6 flex-col">
<Button
variant="secondary"
size="unset"
:aria-label="t('g.increment')"
:class="cn(stepButtonClass, incrementButtonClass)"
:disabled="isIncrementDisabled"
@click="incrementBatchCount"
>
<TinyChevronIcon rotate-up />
</Button>
<Button
variant="secondary"
size="unset"
:aria-label="t('g.decrement')"
:class="cn(stepButtonClass, decrementButtonClass)"
:disabled="isDecrementDisabled"
@click="decrementBatchCount"
>
<TinyChevronIcon />
</Button>
</div>
</div>
<InputNumber
v-model="batchCount"
class="w-14"
:min="minQueueCount"
:max="maxQueueCount"
fluid
show-buttons
:pt="{
incrementButton: {
class: 'w-6',
onmousedown: () => {
handleClick(true)
}
},
decrementButton: {
class: 'w-6',
onmousedown: () => {
handleClick(false)
}
}
}"
/>
</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import InputNumber from 'primevue/inputnumber'
import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { cn } from '@/utils/tailwindUtil'
import TinyChevronIcon from './TinyChevronIcon.vue'
const { t } = useI18n()
const queueSettingsStore = useQueueSettingsStore()
const { batchCount } = storeToRefs(queueSettingsStore)
const minQueueCount = 1
const settingStore = useSettingStore()
const minQueueCount = 1
const maxQueueCount = computed(() =>
settingStore.get('Comfy.QueueButton.BatchCountLimit')
)
const batchCountInputRef = ref<HTMLInputElement | null>(null)
const batchCountInput = ref(String(batchCount.value))
const isEditing = ref(false)
const isIncrementDisabled = computed(
() => batchCount.value >= maxQueueCount.value
)
const isDecrementDisabled = computed(() => batchCount.value <= minQueueCount)
const inputClass =
'h-full min-w-0 flex-1 border-none bg-secondary-background pl-1 pr-0 text-center text-sm font-normal tabular-nums text-base-foreground outline-none'
const stepButtonClass =
'h-1/2 w-full rounded-none border-none p-0 text-muted-foreground hover:bg-secondary-background-hover disabled:cursor-not-allowed disabled:opacity-50'
const incrementButtonClass = 'rounded-tr-none border-b border-border-subtle'
const decrementButtonClass = 'rounded-br-none'
watch(batchCount, (nextBatchCount) => {
if (!isEditing.value) {
batchCountInput.value = String(nextBatchCount)
const handleClick = (increment: boolean) => {
let newCount: number
if (increment) {
const originalCount = batchCount.value - 1
newCount = Math.min(originalCount * 2, maxQueueCount.value)
} else {
const originalCount = batchCount.value + 1
newCount = Math.floor(originalCount / 2)
}
})
const clampBatchCount = (nextBatchCount: number): number =>
Math.min(Math.max(nextBatchCount, minQueueCount), maxQueueCount.value)
const setBatchCount = (nextBatchCount: number) => {
batchCount.value = clampBatchCount(nextBatchCount)
batchCountInput.value = String(batchCount.value)
}
const incrementBatchCount = () => {
setBatchCount(batchCount.value * 2)
}
const decrementBatchCount = () => {
setBatchCount(Math.floor(batchCount.value / 2))
}
const onInputFocus = () => {
isEditing.value = true
}
const onInput = (event: Event) => {
const input = event.target as HTMLInputElement
batchCountInput.value = input.value.replace(/[^0-9]/g, '')
}
const onInputBlur = () => {
isEditing.value = false
const parsedInput = Number.parseInt(batchCountInput.value, 10)
setBatchCount(Number.isNaN(parsedInput) ? minQueueCount : parsedInput)
}
const onInputEnter = () => {
batchCountInputRef.value?.blur()
batchCount.value = newCount
}
</script>
<style scoped>
:deep(.p-inputtext) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>

View File

@@ -19,12 +19,12 @@
content: { class: isDocked ? 'p-0' : 'p-1' }
}"
>
<div class="relative flex items-center gap-2 select-none">
<div class="relative flex items-center select-none gap-2">
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle h-max w-3 cursor-grab',
'drag-handle cursor-grab w-3 h-max',
isDragging && 'cursor-grabbing'
)
"
@@ -119,14 +119,9 @@ import { cn } from '@/utils/tailwindUtil'
import ComfyRunButton from './ComfyRunButton'
const {
topMenuContainer,
queueOverlayExpanded = false,
hasAnyError = false
} = defineProps<{
const { topMenuContainer, queueOverlayExpanded = false } = defineProps<{
topMenuContainer?: HTMLElement | null
queueOverlayExpanded?: boolean
hasAnyError?: boolean
}>()
const emit = defineEmits<{
@@ -428,24 +423,19 @@ 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:-ml-50 before:h-full before:w-50',
'rounded-md before:w-50 before:-ml-50 before:h-full',
'pointer-events-auto',
isMouseOverDropZone.value &&
'scale-105 border-[3px] opacity-100 shadow-[0_0_20px] shadow-blue-500'
'border-[3px] opacity-100 scale-105 shadow-[0_0_20px] shadow-blue-500'
)
)
const panelClass = computed(() =>
cn(
'actionbar pointer-events-auto z-1300',
isDragging.value && 'pointer-events-none select-none',
isDragging.value && 'select-none pointer-events-none',
isDocked.value
? 'static border-none bg-transparent p-0'
: [
'fixed shadow-interface',
hasAnyError
? 'border-destructive-background-hover'
: 'border-interface-stroke'
]
? 'p-0 static border-none bg-transparent'
: 'fixed shadow-interface'
)
)
</script>

View File

@@ -1,7 +1,7 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { defineComponent, nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import type {
@@ -41,9 +41,28 @@ vi.mock('@/stores/workspaceStore', () => ({
})
}))
const BatchCountEditStub = {
template: '<div data-testid="batch-count-edit" />'
}
const SplitButtonStub = defineComponent({
name: 'SplitButton',
props: {
label: {
type: String,
default: ''
},
severity: {
type: String,
default: 'primary'
}
},
template: `
<button
data-testid="split-button"
:data-label="label"
:data-severity="severity"
>
<slot name="icon" />
</button>
`
})
const i18n = createI18n({
legacy: false,
@@ -88,26 +107,14 @@ function createWrapper() {
tooltip: () => {}
},
stubs: {
BatchCountEdit: BatchCountEditStub,
DropdownMenuRoot: { template: '<div><slot /></div>' },
DropdownMenuTrigger: { template: '<div><slot /></div>' },
DropdownMenuPortal: { template: '<div><slot /></div>' },
DropdownMenuContent: { template: '<div><slot /></div>' },
DropdownMenuItem: { template: '<div><slot /></div>' }
SplitButton: SplitButtonStub,
BatchCountEdit: true
}
}
})
}
describe('ComfyQueueButton', () => {
it('renders the batch count control before the run button', () => {
const wrapper = createWrapper()
const controls = wrapper.get('.queue-button-group').element.children
expect(controls[0]?.getAttribute('data-testid')).toBe('batch-count-edit')
expect(controls[1]?.getAttribute('data-testid')).toBe('queue-button')
})
it('keeps the run instant presentation while idle even with active jobs', async () => {
const wrapper = createWrapper()
const queueSettingsStore = useQueueSettingsStore()
@@ -117,10 +124,10 @@ describe('ComfyQueueButton', () => {
queueStore.runningTasks = [createTask('run-1', 'in_progress')]
await nextTick()
const queueButton = wrapper.get('[data-testid="queue-button"]')
const splitButton = wrapper.get('[data-testid="queue-button"]')
expect(queueButton.text()).toContain('Run (Instant)')
expect(queueButton.attributes('data-variant')).toBe('primary')
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
expect(splitButton.attributes('data-severity')).toBe('primary')
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
})
@@ -131,10 +138,10 @@ describe('ComfyQueueButton', () => {
queueSettingsStore.mode = 'instant-running'
await nextTick()
const queueButton = wrapper.get('[data-testid="queue-button"]')
const splitButton = wrapper.get('[data-testid="queue-button"]')
expect(queueButton.text()).toContain('Stop Run (Instant)')
expect(queueButton.attributes('data-variant')).toBe('destructive')
expect(splitButton.attributes('data-label')).toBe('Stop Run (Instant)')
expect(splitButton.attributes('data-severity')).toBe('danger')
expect(wrapper.find('.icon-\\[lucide--square\\]').exists()).toBe(true)
})
@@ -152,17 +159,19 @@ describe('ComfyQueueButton', () => {
await nextTick()
expect(queueSettingsStore.mode).toBe('instant-idle')
const queueButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
expect(queueButtonWhileStopping.text()).toContain('Run (Instant)')
expect(queueButtonWhileStopping.attributes('data-variant')).toBe('primary')
const splitButtonWhileStopping = wrapper.get('[data-testid="queue-button"]')
expect(splitButtonWhileStopping.attributes('data-label')).toBe(
'Run (Instant)'
)
expect(splitButtonWhileStopping.attributes('data-severity')).toBe('primary')
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
expect(commandStore.execute).not.toHaveBeenCalled()
const queueButton = wrapper.get('[data-testid="queue-button"]')
const splitButton = wrapper.get('[data-testid="queue-button"]')
expect(queueSettingsStore.mode).toBe('instant-idle')
expect(queueButton.text()).toContain('Run (Instant)')
expect(queueButton.attributes('data-variant')).toBe('primary')
expect(splitButton.attributes('data-label')).toBe('Run (Instant)')
expect(splitButton.attributes('data-severity')).toBe('primary')
expect(wrapper.find('.icon-\\[lucide--fast-forward\\]').exists()).toBe(true)
})

View File

@@ -1,83 +1,48 @@
<template>
<ButtonGroup
class="queue-button-group h-8 rounded-lg bg-secondary-background"
>
<BatchCountEdit />
<Button
<div class="queue-button-group flex">
<SplitButton
v-tooltip.bottom="{
value: queueButtonTooltip,
showDelay: 600
}"
:variant="queueButtonVariant"
size="unset"
:class="queueActionButtonClass"
class="comfyui-queue-button"
:label="queueButtonLabel"
:severity="queueButtonSeverity"
size="small"
:model="queueModeMenuItems"
data-testid="queue-button"
:data-variant="queueButtonVariant"
@click="queuePrompt"
>
<i :class="cn(iconClass, 'size-4')" />
{{ queueButtonLabel }}
</Button>
<DropdownMenuRoot>
<DropdownMenuTrigger as-child>
<template #icon>
<i :class="iconClass" />
</template>
<template #item="{ item }">
<Button
variant="secondary"
size="unset"
:class="queueMenuTriggerClass"
:aria-label="t('menu.run')"
data-testid="queue-mode-menu-trigger"
v-tooltip="{
value: item.tooltip,
showDelay: 600
}"
:variant="item.key === selectedQueueMode ? 'primary' : 'secondary'"
size="sm"
class="w-full justify-start"
>
<TinyChevronIcon />
<i v-if="item.icon" :class="item.icon" />
{{ String(item.label ?? '') }}
</Button>
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
:side-offset="4"
class="z-1000 min-w-44 rounded-lg border border-border-subtle bg-base-background p-1 shadow-interface"
>
<DropdownMenuItem
v-for="item in queueModeMenuItems"
:key="item.key"
as-child
@select.prevent="item.command"
>
<Button
v-tooltip="{
value: item.tooltip,
showDelay: 600
}"
:variant="
item.key === selectedQueueMode ? 'primary' : 'secondary'
"
size="sm"
:class="queueMenuItemButtonClass"
>
{{ item.label }}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</ButtonGroup>
</template>
</SplitButton>
<BatchCountEdit />
</div>
</template>
<script setup lang="ts">
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRoot,
DropdownMenuTrigger
} from 'reka-ui'
import { storeToRefs } from 'pinia'
import type { MenuItem } from 'primevue/menuitem'
import SplitButton from 'primevue/splitbutton'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import BatchCountEdit from '@/components/actionbar/BatchCountEdit.vue'
import TinyChevronIcon from '@/components/actionbar/TinyChevronIcon.vue'
import Button from '@/components/ui/button/Button.vue'
import ButtonGroup from '@/components/ui/button-group/ButtonGroup.vue'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'
@@ -89,9 +54,10 @@ import {
useQueueSettingsStore
} from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { cn } from '@/utils/tailwindUtil'
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
@@ -103,60 +69,50 @@ const hasMissingNodes = computed(() =>
const { t } = useI18n()
type QueueModeMenuKey = 'disabled' | 'change' | 'instant-idle'
interface QueueModeMenuItem {
key: QueueModeMenuKey
label: string
tooltip: string
command: () => void
}
const selectedQueueMode = computed<QueueModeMenuKey>(() =>
isInstantMode(queueMode.value) ? 'instant-idle' : queueMode.value
)
const queueModeMenuItemLookup = computed<Record<string, QueueModeMenuItem>>(
() => {
const items: Record<string, QueueModeMenuItem> = {
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_on_change_selected'
})
queueMode.value = 'change'
}
const queueModeMenuItemLookup = computed(() => {
const items: Record<string, MenuItem> = {
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_on_change_selected'
})
queueMode.value = 'change'
}
}
if (!isCloud) {
items['instant-idle'] = {
key: 'instant-idle',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_instant_selected'
})
queueMode.value = 'instant-idle'
}
}
}
return items
}
)
if (!isCloud) {
items['instant-idle'] = {
key: 'instant-idle',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
useTelemetry()?.trackUiButtonClicked({
button_id: 'queue_mode_option_run_instant_selected'
})
queueMode.value = 'instant-idle'
}
}
}
return items
})
const activeQueueModeMenuItem = computed(() => {
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
return (
queueModeMenuItemLookup.value[selectedQueueMode.value] ||
queueModeMenuItemLookup.value.disabled
@@ -176,13 +132,9 @@ const queueButtonLabel = computed(() =>
: String(activeQueueModeMenuItem.value?.label ?? '')
)
const queueButtonVariant = computed<'destructive' | 'primary'>(() =>
isStopInstantAction.value ? 'destructive' : 'primary'
const queueButtonSeverity = computed(() =>
isStopInstantAction.value ? 'danger' : 'primary'
)
const queueActionButtonClass = 'h-full rounded-lg gap-1.5 px-4 font-light'
const queueMenuTriggerClass =
'h-full w-6 rounded-l-none rounded-r-lg border-l border-border-subtle p-0 text-muted-foreground data-[state=open]:bg-secondary-background-hover'
const queueMenuItemButtonClass = 'w-full justify-start font-normal'
const iconClass = computed(() => {
if (isStopInstantAction.value) {
@@ -249,3 +201,10 @@ const queuePrompt = async (e: Event) => {
})
}
</script>
<style scoped>
.comfyui-queue-button :deep(.p-splitbutton-dropdown) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
</style>

View File

@@ -1,26 +0,0 @@
<template>
<svg
class="h-[5px] min-h-[5px] w-[8px] min-w-[8px]"
:class="{ 'rotate-180': rotateUp }"
xmlns="http://www.w3.org/2000/svg"
width="8"
height="5"
viewBox="0 0 8 5"
fill="none"
aria-hidden="true"
>
<path
d="M0.650391 0.649902L3.65039 3.6499L6.65039 0.649902"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
<script setup lang="ts">
const { rotateUp = false } = defineProps<{
rotateUp?: boolean
}>()
</script>

View File

@@ -3,15 +3,9 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import Button from '@/components/ui/button/Button.vue'
import { useAppMode } from '@/composables/useAppMode'
import { isCloud } from '@/platform/distribution/types'
import {
openShareDialog,
prefetchShareDialog
} from '@/platform/workflow/sharing/composables/lazyShareDialog'
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
import { useAppModeStore } from '@/stores/appModeStore'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -24,8 +18,6 @@ const workspaceStore = useWorkspaceStore()
const { enableAppBuilder } = useAppMode()
const appModeStore = useAppModeStore()
const { enterBuilder } = appModeStore
const { toastErrorHandler } = useErrorHandling()
const { flags } = useFeatureFlags()
const { hasNodes } = storeToRefs(appModeStore)
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
@@ -43,77 +35,97 @@ function openAssets() {
function showApps() {
void commandStore.execute('Workspace.ToggleSidebarTab.apps')
}
function openTemplates() {
useWorkflowTemplateSelectorDialog().show('sidebar')
}
</script>
<template>
<div class="pointer-events-auto flex flex-row items-start gap-2">
<div class="pointer-events-auto flex flex-col gap-2">
<Button
v-if="enableAppBuilder"
v-tooltip.right="{
value: t('linearMode.appModeToolbar.appBuilder'),
...tooltipOptions
}"
variant="secondary"
size="unset"
:disabled="!hasNodes"
:aria-label="t('linearMode.appModeToolbar.appBuilder')"
class="size-10 rounded-lg"
@click="enterBuilder"
>
<i class="icon-[lucide--hammer] size-4" />
</Button>
<Button
v-if="isCloud && flags.workflowSharingEnabled"
v-tooltip.right="{
value: t('actionbar.shareTooltip'),
...tooltipOptions
}"
variant="secondary"
size="unset"
:aria-label="t('actionbar.shareTooltip')"
class="size-10 rounded-lg"
@click="() => openShareDialog().catch(toastErrorHandler)"
@pointerenter="prefetchShareDialog"
>
<i class="icon-[lucide--send] size-4" />
</Button>
<div
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"
>
<div class="flex flex-col gap-2 pointer-events-auto">
<WorkflowActionsDropdown source="app_mode_toolbar">
<template #button="{ hasUnseenItems }">
<Button
v-tooltip.right="{
value: t('sideToolbar.mediaAssets.title'),
value: t('sideToolbar.labels.menu'),
...tooltipOptions
}"
variant="textonly"
variant="secondary"
size="unset"
:aria-label="t('sideToolbar.mediaAssets.title')"
:class="
cn('size-10', isAssetsActive && 'bg-secondary-background-hover')
"
@click="openAssets"
>
<i class="icon-[comfy--image-ai-edit] size-4" />
</Button>
<Button
v-tooltip.right="{
value: t('linearMode.appModeToolbar.apps'),
...tooltipOptions
}"
variant="textonly"
size="unset"
:aria-label="t('linearMode.appModeToolbar.apps')"
:class="
cn('size-10', isAppsActive && 'bg-secondary-background-hover')
"
@click="showApps"
: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"
>
<i class="icon-[lucide--panels-top-left] size-4" />
<i class="icon-[lucide--chevron-down] size-4 text-muted-foreground" />
<span
v-if="hasUnseenItems"
aria-hidden="true"
class="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-primary-background"
/>
</Button>
</div>
</template>
</WorkflowActionsDropdown>
<Button
v-if="enableAppBuilder"
v-tooltip.right="{
value: t('linearMode.appModeToolbar.appBuilder'),
...tooltipOptions
}"
variant="secondary"
size="unset"
:disabled="!hasNodes"
:aria-label="t('linearMode.appModeToolbar.appBuilder')"
class="size-10 rounded-lg"
@click="enterBuilder"
>
<i class="icon-[lucide--hammer] size-4" />
</Button>
<div
class="flex flex-col w-10 rounded-lg bg-secondary-background overflow-hidden"
>
<Button
v-tooltip.right="{
value: t('sideToolbar.mediaAssets.title'),
...tooltipOptions
}"
variant="textonly"
size="unset"
:aria-label="t('sideToolbar.mediaAssets.title')"
:class="
cn('size-10', isAssetsActive && 'bg-secondary-background-hover')
"
@click="openAssets"
>
<i class="icon-[comfy--image-ai-edit] size-4" />
</Button>
<Button
v-tooltip.right="{
value: t('linearMode.appModeToolbar.apps'),
...tooltipOptions
}"
variant="textonly"
size="unset"
:aria-label="t('linearMode.appModeToolbar.apps')"
:class="cn('size-10', isAppsActive && 'bg-secondary-background-hover')"
@click="showApps"
>
<i class="icon-[lucide--panels-top-left] size-4" />
</Button>
<Button
v-tooltip.right="{
value: t('sideToolbar.templates'),
...tooltipOptions
}"
variant="textonly"
size="unset"
:aria-label="t('sideToolbar.templates')"
class="size-10"
@click="openTemplates"
>
<i class="icon-[comfy--template] size-4" />
</Button>
</div>
<WorkflowActionsDropdown source="app_mode_toolbar" />
</div>
</template>

View File

@@ -1,6 +1,9 @@
<template>
<div ref="rootEl" class="relative size-full overflow-hidden bg-neutral-900">
<div class="p-terminal size-full rounded-none p-2">
<div
ref="rootEl"
class="relative h-full w-full overflow-hidden bg-neutral-900"
>
<div class="p-terminal h-full w-full rounded-none p-2">
<div ref="terminalEl" class="terminal-host h-full" />
</div>
<Button
@@ -12,7 +15,7 @@
size="sm"
:class="
cn('absolute top-2 right-8 transition-opacity', {
'pointer-events-none opacity-0 select-none': !isHovered
'opacity-0 pointer-events-none select-none': !isHovered
})
"
:aria-label="tooltipText"

View File

@@ -1,5 +1,5 @@
<template>
<div class="size-full bg-transparent">
<div class="h-full w-full bg-transparent">
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>

View File

@@ -1,7 +1,7 @@
<template>
<div
data-testid="subgraph-breadcrumb"
class="subgraph-breadcrumb -mt-3 flex w-auto items-center pt-4 pl-1 drop-shadow-(--interface-panel-drop-shadow)"
class="subgraph-breadcrumb flex w-auto drop-shadow-(--interface-panel-drop-shadow) items-center -mt-4 pt-4"
: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 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"
class="back-button pointer-events-auto h-8 w-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"
text
severity="secondary"
size="small"

View File

@@ -42,7 +42,7 @@
v-if="isEditing"
ref="itemInputRef"
v-model="itemLabel"
class="fixed z-10000 p-2 text-[.8rem]"
class="fixed z-10000 px-2 py-2 text-[.8rem]"
@blur="inputBlur(false)"
@click.stop
@keydown.enter="inputBlur(true)"

View File

@@ -10,11 +10,9 @@ import PropertiesAccordionItem from '@/components/rightSidePanel/layout/Properti
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import {
LGraphEventMode,
TitleMode
} from '@/lib/litegraph/src/types/globalEnums'
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -24,9 +22,9 @@ import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteracti
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import { app } from '@/scripts/app'
import { DOMWidgetImpl } from '@/scripts/domWidget'
import { promptRenameWidget } from '@/utils/widgetUtil'
import { useDialogService } from '@/services/dialogService'
import { useAppMode } from '@/composables/useAppMode'
import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore'
import { useAppModeStore } from '@/stores/appModeStore'
import { resolveNode } from '@/utils/litegraphUtil'
import { cn } from '@/utils/tailwindUtil'
import { HideLayoutFieldKey } from '@/types/widgetTypes'
@@ -72,12 +70,15 @@ const inputsWithState = computed(() =>
}
}
const input = node.inputs.find((i) => i.widget?.name === widget.name)
const rename = input && (() => renameWidget(widget, input))
return {
nodeId,
widgetName,
label: widget.label,
subLabel: node.title,
rename: () => promptRenameWidget(widget, node, t)
rename
}
})
)
@@ -88,6 +89,20 @@ const outputsWithState = computed<[NodeId, string][]>(() =>
])
)
async function renameWidget(widget: IBaseWidget, input: INodeInputSlot) {
const newLabel = await useDialogService().prompt({
title: t('g.rename'),
message: t('g.enterNewNamePrompt'),
defaultValue: widget.label,
placeholder: widget.name
})
if (newLabel === null) return
widget.label = newLabel || undefined
input.label = newLabel || undefined
widget.callback?.(widget.value)
useCanvasStore().canvas?.setDirty(true)
}
function getHovered(
e: MouseEvent
): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] {
@@ -144,12 +159,7 @@ function handleDown(e: MouseEvent) {
}
function handleClick(e: MouseEvent) {
const [node, widget] = getHovered(e) ?? []
if (
node?.mode !== LGraphEventMode.ALWAYS ||
!nodeTypeValidForApp(node.type) ||
node.has_errors
)
return canvasInteractions.forwardEventToCanvas(e)
if (!node) return canvasInteractions.forwardEventToCanvas(e)
if (!widget) {
if (!isSelectOutputsMode.value) return
@@ -160,7 +170,7 @@ function handleClick(e: MouseEvent) {
else appModeStore.selectedOutputs.splice(index, 1)
return
}
if (!isSelectInputsMode.value || widget.options.canvasOnly) return
if (!isSelectInputsMode.value) return
const index = appModeStore.selectedInputs.findIndex(
([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName
@@ -182,12 +192,7 @@ function nodeToDisplayTuple(
const renderedOutputs = computed(() => {
void appModeStore.selectedOutputs.length
return canvas
.graph!.nodes.filter(
(n) =>
n.constructor.nodeData?.output_node &&
n.mode === LGraphEventMode.ALWAYS &&
!n.has_errors
)
.graph!.nodes.filter((n) => n.constructor.nodeData?.output_node)
.map(nodeToDisplayTuple)
})
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
@@ -199,152 +204,131 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
)
</script>
<template>
<div class="flex h-full flex-col">
<div class="flex items-center border-b border-border-subtle p-2 font-bold">
{{
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
}}
</div>
<div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
<DraggableList
v-if="isArrangeMode"
v-slot="{ dragClass }"
v-model="appModeStore.selectedInputs"
class="overflow-x-clip"
>
<div
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
:key="`${nodeId}: ${widgetName}`"
: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>
<WidgetItem
:widget="widget"
:node="node"
show-node-name
hidden-widget-actions
/>
</div>
<div
v-else
class="pointer-events-none p-1 text-sm text-muted-foreground"
>
{{ widgetName }}
<p class="text-xs italic">
({{ t('linearMode.builder.unknownWidget') }})
</p>
</div>
</div>
</DraggableList>
<PropertiesAccordionItem
v-if="isSelectInputsMode"
:label="t('nodeHelpPage.inputs')"
enable-empty-state
:disabled="!appModeStore.selectedInputs.length"
: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="icon-[lucide--info] bg-muted-foreground" />
</div>
</template>
<template #empty>
<div
class="p-4 text-muted-foreground"
v-text="t('linearMode.builder.promptAddInputs')"
/>
</template>
<DraggableList
v-slot="{ dragClass }"
v-model="appModeStore.selectedInputs"
>
<IoItem
v-for="{
nodeId,
widgetName,
label,
subLabel,
rename
} in inputsWithState"
:key="`${nodeId}: ${widgetName}`"
:class="
cn(dragClass, 'my-2 rounded-lg bg-primary-background/30 p-2')
"
:title="label ?? widgetName"
:sub-title="subLabel"
:rename
:remove="
() =>
remove(
appModeStore.selectedInputs,
([id, name]) => nodeId == id && widgetName === name
)
"
/>
</DraggableList>
</PropertiesAccordionItem>
<div
v-if="isSelectInputsMode && !appModeStore.selectedInputs.length"
class="m-4 flex flex-1 items-center justify-center rounded-lg border-2 border-dashed border-primary-background bg-primary-background/20 text-center text-sm text-primary-background"
>
{{ t('linearMode.builder.inputPlaceholder') }}
</div>
<PropertiesAccordionItem
v-if="isSelectOutputsMode"
:label="t('nodeHelpPage.outputs')"
enable-empty-state
:disabled="!appModeStore.selectedOutputs.length"
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
:tooltip-delay="100"
>
<template #label>
<div class="flex gap-3">
{{ t('nodeHelpPage.outputs') }}
<i class="icon-[lucide--info] bg-muted-foreground" />
</div>
</template>
<template #empty>
<div
class="p-4 text-muted-foreground"
v-text="t('linearMode.builder.promptAddOutputs')"
/>
</template>
<DraggableList
v-slot="{ dragClass }"
v-model="appModeStore.selectedOutputs"
>
<IoItem
v-for="([key, title], index) in outputsWithState"
:key
:class="
cn(
dragClass,
'my-2 rounded-lg bg-warning-background/40 p-2',
index === 0 && 'ring-2 ring-warning-background'
)
"
:title
:sub-title="String(key)"
:remove="
() => remove(appModeStore.selectedOutputs, (k) => k == key)
"
/>
</DraggableList>
</PropertiesAccordionItem>
<div
v-if="isSelectOutputsMode && !appModeStore.selectedOutputs.length"
class="m-4 flex flex-1 flex-col items-center justify-center gap-1 rounded-lg border-2 border-dashed border-warning-background bg-warning-background/20 text-center text-sm text-warning-background"
>
{{ t('linearMode.builder.outputPlaceholder') }}
<span class="font-bold">
{{ t('linearMode.builder.outputRequiredPlaceholder') }}
</span>
</div>
</div>
<div class="flex font-bold p-2 border-border-subtle border-b items-center">
{{
isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
}}
</div>
<DraggableList
v-if="isArrangeMode"
v-slot="{ dragClass }"
v-model="appModeStore.selectedInputs"
>
<div
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
:key="`${nodeId}: ${widgetName}`"
:class="cn(dragClass, 'p-2 my-2 pointer-events-auto')"
:aria-label="`${widget?.label ?? widgetName} ${node.title}`"
>
<div v-if="widget" class="pointer-events-none" inert>
<WidgetItem
:widget="widget"
:node="node"
show-node-name
hidden-widget-actions
/>
</div>
<div v-else class="text-muted-foreground text-sm p-1 pointer-events-none">
{{ widgetName }}
<p class="text-xs italic">
({{ t('linearMode.builder.unknownWidget') }})
</p>
</div>
</div>
</DraggableList>
<PropertiesAccordionItem
v-if="isSelectInputsMode"
:label="t('nodeHelpPage.inputs')"
enable-empty-state
:disabled="!appModeStore.selectedInputs.length"
class="border-border-subtle border-b"
: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]" />
</div>
</template>
<template #empty>
<div
class="w-full p-4 pt-2 text-muted-foreground"
v-text="t('linearMode.builder.promptAddInputs')"
/>
</template>
<div
class="w-full p-4 pt-2 text-muted-foreground"
v-text="t('linearMode.builder.promptAddInputs')"
/>
<DraggableList v-slot="{ dragClass }" v-model="appModeStore.selectedInputs">
<IoItem
v-for="{
nodeId,
widgetName,
label,
subLabel,
rename
} in inputsWithState"
:key="`${nodeId}: ${widgetName}`"
:class="cn(dragClass, 'bg-primary-background/30 p-2 my-2 rounded-lg')"
:title="label ?? widgetName"
:sub-title="subLabel"
:rename
:remove="
() =>
remove(
appModeStore.selectedInputs,
([id, name]) => nodeId == id && widgetName === name
)
"
/>
</DraggableList>
</PropertiesAccordionItem>
<PropertiesAccordionItem
v-if="isSelectOutputsMode"
:label="t('nodeHelpPage.outputs')"
enable-empty-state
:disabled="!appModeStore.selectedOutputs.length"
:tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
:tooltip-delay="100"
>
<template #label>
<div class="flex gap-3">
{{ t('nodeHelpPage.outputs') }}
<i class="bg-muted-foreground icon-[lucide--circle-alert]" />
</div>
</template>
<template #empty>
<div
class="w-full p-4 pt-2 text-muted-foreground"
v-text="t('linearMode.builder.promptAddOutputs')"
/>
</template>
<div
class="w-full p-4 pt-2 text-muted-foreground"
v-text="t('linearMode.builder.promptAddOutputs')"
/>
<DraggableList
v-slot="{ dragClass }"
v-model="appModeStore.selectedOutputs"
>
<IoItem
v-for="([key, title], index) in outputsWithState"
:key
:class="
cn(
dragClass,
'bg-warning-background/40 p-2 my-2 rounded-lg',
index === 0 && 'ring-warning-background ring-2'
)
"
:title
:sub-title="String(key)"
:remove="() => remove(appModeStore.selectedOutputs, (k) => k == key)"
/>
</DraggableList>
</PropertiesAccordionItem>
<Teleport
v-if="isSelectMode && !settingStore.get('Comfy.VueNodes.Enabled')"
@@ -353,7 +337,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<div
:class="
cn(
'pointer-events-auto absolute size-full',
'absolute w-full h-full pointer-events-auto',
hoveringSelectable ? 'cursor-pointer' : 'cursor-grab'
)
"
@@ -368,7 +352,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
v-for="[key, style] in renderedInputs"
:key
:style="toValue(style)"
class="fixed rounded-lg bg-primary-background/30"
class="fixed bg-primary-background/30 rounded-lg"
/>
</template>
<template v-else>
@@ -378,7 +362,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
:style="toValue(style)"
:class="
cn(
'fixed rounded-2xl ring-5 ring-warning-background',
'fixed ring-warning-background ring-5 rounded-2xl',
!isSelected && 'ring-warning-background/50'
)
"
@@ -386,17 +370,17 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
<div class="absolute top-0 right-0 size-8">
<div
v-if="isSelected"
class="pointer-events-auto absolute -top-1/2 -right-1/2 size-full cursor-pointer rounded-lg bg-warning-background p-2"
class="absolute -top-1/2 -right-1/2 size-full p-2 bg-warning-background rounded-lg cursor-pointer pointer-events-auto"
@click.stop="
remove(appModeStore.selectedOutputs, (k) => k == key)
"
@pointerdown.stop
>
<i class="bg-text-foreground icon-[lucide--check] size-full" />
<i class="icon-[lucide--check] bg-text-foreground size-full" />
</div>
<div
v-else
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"
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"
@click.stop="appModeStore.selectedOutputs.push(key)"
@pointerdown.stop
/>

View File

@@ -38,8 +38,8 @@
<Button variant="muted-textonly" size="lg" @click="$emit('viewApp')">
{{ $t('builderToolbar.viewApp') }}
</Button>
<Button variant="secondary" size="lg" @click="$emit('exitToWorkflow')">
{{ $t('builderToolbar.exitToWorkflow') }}
<Button variant="secondary" size="lg" @click="$emit('close')">
{{ $t('g.close') }}
</Button>
</template>
</template>
@@ -58,6 +58,5 @@ defineProps<{
defineEmits<{
viewApp: []
close: []
exitToWorkflow: []
}>()
</script>

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