Merge remote-tracking branch 'origin/main' into bl-selective-snapshot-update

This commit is contained in:
Benjamin Lu
2025-10-07 19:29:01 -07:00
126 changed files with 1084 additions and 543 deletions

View File

@@ -89,7 +89,7 @@ jobs:
run: sleep 10
- name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
uses: actions/cache/restore@v4
with:
fail-on-cache-miss: true
path: |
@@ -155,7 +155,7 @@ jobs:
run: sleep 10
- name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
uses: actions/cache/restore@v4
with:
fail-on-cache-miss: true
path: |

View File

@@ -14,6 +14,9 @@ jobs:
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Frontend
uses: ./.github/actions/setup-frontend

View File

@@ -3,114 +3,165 @@ name: Update Playwright Expectations
on:
pull_request:
types: [ labeled ]
types: [labeled]
issue_comment:
types: [created]
jobs:
test:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
if: >
( github.event_name == 'pull_request' && github.event.label.name == 'New Browser Test Expectations' ) ||
( github.event.issue.pull_request &&
github.event_name == 'issue_comment' &&
(
github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'
) &&
startsWith(github.event.comment.body, '/update-playwright') )
steps:
- name: Checkout workflow repo
uses: actions/checkout@v5
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Locate failed screenshot manifest artifact
id: locate-manifest
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo
const headSha = context.payload.pull_request.head.sha
- name: Initial Checkout
uses: actions/checkout@v5
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'tests-ci.yaml',
head_sha: headSha,
event: 'pull_request',
per_page: 1,
})
const run = data.workflow_runs?.[0]
- name: Pull Request Checkout
if: github.event.issue.pull_request && github.event_name == 'issue_comment'
run: gh pr checkout ${{ github.event.issue.number }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
let has = 'false'
let runId = ''
if (run) {
runId = String(run.id)
const { data: { artifacts = [] } } = await github.rest.actions.listWorkflowRunArtifacts({
- name: Setup Frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Locate failed screenshot manifest artifact
id: locate-manifest
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo
let headSha = ''
if (context.eventName === 'pull_request') {
headSha = context.payload.pull_request.head.sha
} else if (context.eventName === 'issue_comment') {
const prNumber = context.payload.issue.number
const pr = await github.rest.pulls.get({ owner, repo, pull_number: prNumber })
headSha = pr.data.head.sha
}
if (!headSha) {
core.setOutput('run_id', '')
core.setOutput('has_manifest', 'false')
return
}
const { data } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
run_id: run.id,
per_page: 100,
workflow_id: 'tests-ci.yaml',
head_sha: headSha,
event: 'pull_request',
per_page: 1,
})
if (artifacts.some(a => a.name === 'failed-screenshot-tests' && !a.expired)) has = 'true'
}
core.setOutput('run_id', runId)
core.setOutput('has_manifest', has)
const run = data.workflow_runs?.[0]
- name: Download failed screenshot manifest
if: steps.locate-manifest.outputs.has_manifest == 'true'
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.locate-manifest.outputs.run_id }}
name: failed-screenshot-tests
path: ComfyUI_frontend/ci-rerun
let has = 'false'
let runId = ''
if (run) {
runId = String(run.id)
const { data: { artifacts = [] } } = await github.rest.actions.listWorkflowRunArtifacts({
owner,
repo,
run_id: run.id,
per_page: 100,
})
if (artifacts.some(a => a.name === 'failed-screenshot-tests' && !a.expired)) has = 'true'
}
core.setOutput('run_id', runId)
core.setOutput('has_manifest', has)
- name: Re-run failed screenshot tests and update snapshots
id: playwright-tests
shell: bash
working-directory: ComfyUI_frontend
run: |
set -euo pipefail
if [ ! -d ci-rerun ]; then
echo "No manifest found; running full suite as fallback"
pnpm exec playwright test --update-snapshots
exit 0
fi
shopt -s nullglob
files=(ci-rerun/*.txt)
if [ ${#files[@]} -eq 0 ]; then
echo "Manifest is empty; running full suite as fallback"
pnpm exec playwright test --update-snapshots
exit 0
fi
for f in "${files[@]}"; do
project="$(basename "$f" .txt)"
mapfile -t lines < "$f"
# Filter out blank lines
filtered=( )
for l in "${lines[@]}"; do
[ -n "$l" ] && filtered+=("$l")
done
if [ ${#filtered[@]} -eq 0 ]; then
continue
- name: Download failed screenshot manifest
if: steps.locate-manifest.outputs.has_manifest == 'true'
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.locate-manifest.outputs.run_id }}
name: failed-screenshot-tests
path: ComfyUI_frontend/ci-rerun
- name: Re-run failed screenshot tests and update snapshots
id: playwright-tests
shell: bash
working-directory: ComfyUI_frontend
continue-on-error: true
run: |
set -euo pipefail
if [ ! -d ci-rerun ]; then
echo "No manifest found; running full suite as fallback"
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright test --update-snapshots \
--reporter=line --reporter=html
exit 0
fi
shopt -s nullglob
files=(ci-rerun/*.txt)
if [ ${#files[@]} -eq 0 ]; then
echo "Manifest is empty; running full suite as fallback"
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright test --update-snapshots \
--reporter=line --reporter=html
exit 0
fi
for f in "${files[@]}"; do
project="$(basename "$f" .txt)"
mapfile -t lines < "$f"
filtered=( )
for l in "${lines[@]}"; do
[ -n "$l" ] && filtered+=("$l")
done
if [ ${#filtered[@]} -eq 0 ]; then
continue
fi
echo "Re-running ${#filtered[@]} tests for project $project"
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright test --project="$project" --update-snapshots \
--reporter=line --reporter=html \
"${filtered[@]}"
done
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Debugging info
working-directory: ComfyUI_frontend
run: |
echo "Branch: ${{ github.head_ref }}"
git status
- name: Commit updated expectations
working-directory: ComfyUI_frontend
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
if [ "${{ github.event_name }}" = "issue_comment" ]; then
true
else
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
fi
git add browser_tests
if git diff --cached --quiet; then
echo "No expectation updates detected; skipping commit."
else
git commit -m "[automated] Update test expectations"
if [ "${{ github.event_name }}" = "issue_comment" ]; then
git push
else
git push origin HEAD:${{ github.head_ref }}
fi
fi
echo "Re-running ${#filtered[@]} tests for project $project"
pnpm exec playwright test --project="$project" --update-snapshots "${filtered[@]}"
done
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Debugging info
working-directory: ComfyUI_frontend
run: |
echo "Branch: ${{ github.head_ref }}"
git status
- name: Commit updated expectations
working-directory: ComfyUI_frontend
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
git add browser_tests
if git diff --cached --quiet; then
echo "No expectation updates detected; skipping commit."
else
git commit -m "[automated] Update test expectations"
git push origin HEAD:${{ github.head_ref }}
fi

View File

@@ -211,18 +211,17 @@ This Storybook setup includes:
## Icon Usage in Storybook
In this project, the `<i-lucide:... />` syntax from unplugin-icons is not supported in Storybook.
In this project, only the `<i class="icon-[lucide--folder]" />` syntax from unplugin-icons is supported in Storybook.
**Example:**
```vue
<script setup lang="ts">
import { Trophy, Settings } from 'lucide-vue-next'
</script>
<template>
<Trophy :size="16" class="text-neutral" />
<Settings :size="16" class="text-neutral" />
<i class="icon-[lucide--trophy] text-neutral size-4" />
<i class="icon-[lucide--settings] text-neutral size-4" />
</template>
```

View File

@@ -7,15 +7,15 @@
}
],
"rules": {
"import-notation": "url",
"import-notation": "string",
"font-family-no-missing-generic-family-keyword": true,
"declaration-block-no-redundant-longhand-properties": true,
"declaration-property-value-no-unknown": [
true,
{
"ignoreProperties": {
"speak": ["none"],
"app-region": ["drag", "no-drag"]
"app-region": ["drag", "no-drag"],
"/^(width|height)$/": ["/^v-bind/"]
}
}
],
@@ -35,7 +35,7 @@
"selector-max-type": 2,
"declaration-block-no-duplicate-properties": true,
"block-no-empty": true,
"no-descending-specificity": true,
"no-descending-specificity": null,
"no-duplicate-at-import-rules": true,
"at-rule-no-unknown": [
true,
@@ -57,7 +57,8 @@
true,
{
"ignoreFunctions": [
"theme"
"theme",
"v-bind"
]
}
]

View File

@@ -255,7 +255,7 @@ pnpm format
The project supports three types of icons, all with automatic imports (no manual imports needed):
1. **PrimeIcons** - Built-in PrimeVue icons using CSS classes: `<i class="pi pi-plus" />`
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i-lucide:settings />`, `<i-mdi:folder />`
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i class="icon-[lucide--settings]" />`, `<i class="icon-[mdi--folder]" />`
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.

View File

@@ -53,7 +53,7 @@
:value="$t('install.gpuPicker.recommended')"
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
/>
<i-lucide:badge-check class="text-neutral-300 text-lg" />
<i class="icon-[lucide--badge-check] text-neutral-300 text-lg" />
</div>
</div>

View File

@@ -286,6 +286,12 @@ const onFocus = async () => {
.p-accordionheader {
@apply rounded-t-xl rounded-b-none;
}
.p-accordionheader-toggle-icon {
&::before {
content: '\e902';
}
}
}
.p-accordioncontent {
@@ -302,13 +308,5 @@ const onFocus = async () => {
content: '\e933';
}
}
.p-accordionpanel-active {
.p-accordionheader-toggle-icon {
&::before {
content: '\e902';
}
}
}
}
</style>

View File

@@ -65,12 +65,12 @@ onUnmounted(() => electron.Validation.dispose())
.download-bg::before {
@apply m-0 absolute text-muted;
font-family: 'primeicons';
font-family: 'primeicons', sans-serif;
top: -2rem;
right: 2rem;
speak: none;
font-style: normal;
font-weight: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;

View File

@@ -186,12 +186,12 @@ onUnmounted(() => electron.Validation.dispose())
.backspan::before {
@apply m-0 absolute text-muted;
font-family: 'primeicons';
font-family: 'primeicons', sans-serif;
top: -2rem;
right: -2rem;
speak: none;
font-style: normal;
font-weight: normal;
font-weight: 400;
font-variant: normal;
text-transform: none;
line-height: 1;

View File

@@ -18,16 +18,16 @@
style="
background: radial-gradient(
ellipse 800px 600px at center,
rgba(23, 23, 23, 0.95) 0%,
rgba(23, 23, 23, 0.93) 10%,
rgba(23, 23, 23, 0.9) 20%,
rgba(23, 23, 23, 0.85) 30%,
rgba(23, 23, 23, 0.75) 40%,
rgba(23, 23, 23, 0.6) 50%,
rgba(23, 23, 23, 0.4) 60%,
rgba(23, 23, 23, 0.2) 70%,
rgba(23, 23, 23, 0.1) 80%,
rgba(23, 23, 23, 0.05) 90%,
rgb(23 23 23 / 0.95) 0%,
rgb(23 23 23 / 0.93) 10%,
rgb(23 23 23 / 0.9) 20%,
rgb(23 23 23 / 0.85) 30%,
rgb(23 23 23 / 0.75) 40%,
rgb(23 23 23 / 0.6) 50%,
rgb(23 23 23 / 0.4) 60%,
rgb(23 23 23 / 0.2) 70%,
rgb(23 23 23 / 0.1) 80%,
rgb(23 23 23 / 0.05) 90%,
transparent 100%
);
"

View File

@@ -24,9 +24,7 @@ export class VueNodeHelpers {
* Get locator for selected Vue node components (using visual selection indicators)
*/
get selectedNodes(): Locator {
return this.page.locator(
'[data-node-id].outline-black, [data-node-id].outline-white'
)
return this.page.locator('[data-node-id].outline-node-component-outline')
}
/**

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -8,6 +8,7 @@ const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)
await comfyPage.vueNodes.waitForNodes()
})
@@ -15,6 +16,7 @@ test.describe('Vue Node Groups', () => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-create-group.png'
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -50,17 +50,17 @@ test.describe('Vue Node Collapse', () => {
// Check initial expanded state icon
let iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down')
expect(iconClass).not.toContain('-rotate-90')
// Collapse and check icon
await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-right')
expect(iconClass).toContain('-rotate-90')
// Expand and check icon
await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down')
expect(iconClass).not.toContain('-rotate-90')
})
test('should preserve title when collapsing/expanding', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -44,9 +44,9 @@ const config: KnipConfig = {
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
css: (text: string) =>
[
...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g)
].join('\n')
[...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g)]
.map((match) => match[0].replace(/url\(['"]?([^'"()]+)['"]?\)/, '$1'))
.join('\n')
},
vite: {
config: ['vite?(.*).config.mts']

View File

@@ -9,35 +9,6 @@
@config '../../tailwind.config.ts';
:root {
--fg-color: #000;
--bg-color: #fff;
--comfy-menu-bg: #353535;
--comfy-menu-secondary-bg: #292929;
--comfy-topbar-height: 2.5rem;
--comfy-input-bg: #222;
--input-text: #ddd;
--descrip-text: #999;
--drag-text: #ccc;
--error-text: #ff4444;
--border-color: #4e4e4e;
--tr-even-bg-color: #222;
--tr-odd-bg-color: #353535;
--primary-bg: #236692;
--primary-fg: #ffffff;
--primary-hover-bg: #3485bb;
--primary-hover-fg: #ffffff;
--content-bg: #e0e0e0;
--content-fg: #000;
--content-hover-bg: #adadad;
--content-hover-fg: #000;
/* Code styling colors for help menu*/
--code-text-color: rgba(0, 122, 255, 1);
--code-bg-color: rgba(96, 165, 250, 0.2);
--code-block-bg-color: rgba(60, 60, 60, 0.12);
}
@media (prefers-color-scheme: dark) {
:root {
--fg-color: #fff;
@@ -128,12 +99,121 @@
--color-dark-elevation-2: rgba(from white r g b / 0.03);
}
:root {
--fg-color: #000;
--bg-color: #fff;
--comfy-menu-bg: #353535;
--comfy-menu-secondary-bg: #292929;
--comfy-topbar-height: 2.5rem;
--comfy-input-bg: #222;
--input-text: #ddd;
--descrip-text: #999;
--drag-text: #ccc;
--error-text: #ff4444;
--border-color: #4e4e4e;
--tr-even-bg-color: #222;
--tr-odd-bg-color: #353535;
--primary-bg: #236692;
--primary-fg: #ffffff;
--primary-hover-bg: #3485bb;
--primary-hover-fg: #ffffff;
--content-bg: #e0e0e0;
--content-fg: #000;
--content-hover-bg: #adadad;
--content-hover-fg: #000;
/* Code styling colors for help menu*/
--code-text-color: rgb(0 122 255 / 1);
--code-bg-color: rgb(96 165 250 / 0.2);
--code-block-bg-color: rgb(60 60 60 / 0.12);
/* --- */
--backdrop: var(--color-white);
--dialog-surface: var(--color-neutral-200);
--node-component-border: var(--color-gray-400);
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-stone-200);
--node-component-header-surface: var(--color-white);
--node-component-outline: var(--color-black);
--node-component-ring: rgb(from var(--color-gray-500) r g b / 50%);
--node-component-slot-dot-outline-opacity-mult: 1;
--node-component-slot-dot-outline-opacity: 5%;
--node-component-slot-dot-outline: var(--color-black);
--node-component-slot-text: var(--color-stone-200);
--node-component-surface-highlight: var(--color-stone-100);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-white);
--node-component-tooltip: var(--color-charcoal-700);
--node-component-tooltip-border: var(--color-sand-100);
--node-component-tooltip-surface: var(--color-white);
--node-component-widget-input: var(--fg-color);
--node-component-widget-input-surface: rgb(
from var(--color-zinc-500) r g b / 10%
);
--node-component-widget-skeleton-surface: var(--color-zinc-300);
--node-stroke: var(--color-stone-100);
}
.dark-theme {
--backdrop: var(--color-neutral-900);
--dialog-surface: var(--color-neutral-700);
--node-component-border: var(--color-stone-200);
--node-component-header-icon: var(--color-slate-300);
--node-component-header-surface: var(--color-charcoal-800);
--node-component-outline: var(--color-white);
--node-component-ring: rgb(var(--color-gray-500) / 20%);
--node-component-slot-dot-outline-opacity: 10%;
--node-component-slot-dot-outline: var(--color-white);
--node-component-slot-text: var(--color-slate-200);
--node-component-surface-highlight: var(--color-slate-100);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-charcoal-800);
--node-component-tooltip: var(--color-white);
--node-component-tooltip-border: var(--color-slate-300);
--node-component-tooltip-surface: var(--color-charcoal-800);
--node-component-widget-skeleton-surface: var(--color-zinc-800);
--node-stroke: var(--color-slate-100);
}
@theme inline {
--color-node-component-surface: var(--color-charcoal-600);
--color-node-component-surface-highlight: var(--color-slate-100);
--color-node-component-surface-hovered: var(--color-charcoal-400);
--color-node-component-surface-selected: var(--color-charcoal-200);
--color-node-stroke: var(--color-stone-100);
--color-backdrop: var(--backdrop);
--color-dialog-surface: var(--dialog-surface);
--color-node-component-border: var(--node-component-border);
--color-node-component-executing: var(--node-component-executing);
--color-node-component-header: var(--node-component-header);
--color-node-component-header-icon: var(--node-component-header-icon);
--color-node-component-header-surface: var(--node-component-header-surface);
--color-node-component-outline: var(--node-component-outline);
--color-node-component-ring: var(--node-component-ring);
--color-node-component-slot-dot-outline: rgb(
from var(--node-component-slot-dot-outline) r g b /
calc(
var(--node-component-slot-dot-outline-opacity) *
var(--node-component-slot-dot-outline-opacity-mult)
)
);
--color-node-component-slot-text: var(--node-component-slot-text);
--color-node-component-surface-highlight: var(
--node-component-surface-highlight
);
--color-node-component-surface-hovered: var(--node-component-surface-hovered);
--color-node-component-surface-selected: var(--component-surface-selected);
--color-node-component-surface: var(--node-component-surface);
--color-node-component-tooltip: var(--node-component-tooltip);
--color-node-component-tooltip-border: var(--node-component-tooltip-border);
--color-node-component-tooltip-surface: var(--node-component-tooltip-surface);
--color-node-component-widget-input: var(--node-component-widget-input);
--color-node-component-widget-input-surface: var(
--node-component-widget-input-surface
);
--color-node-component-widget-skeleton-surface: var(
--node-component-widget-skeleton-surface
);
--color-node-stroke: var(--node-stroke);
}
@custom-variant dark-theme {
@@ -418,7 +498,7 @@ body {
/* Strong and emphasis */
.comfy-markdown-content strong {
font-weight: bold;
font-weight: 700;
}
.comfy-markdown-content em {
@@ -429,7 +509,7 @@ body {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 100; /* Sit on top */
padding: 30px 30px 10px 30px;
padding: 30px 30px 10px;
background-color: var(--comfy-menu-bg); /* Modal background */
color: var(--error-text);
box-shadow: 0 0 20px #888888;
@@ -477,8 +557,8 @@ body {
background-color: var(--comfy-menu-bg);
font-family: sans-serif;
padding: 10px;
border-radius: 0 8px 8px 8px;
box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4);
border-radius: 0 8px 8px;
box-shadow: 3px 3px 8px rgb(0 0 0 / 0.4);
}
.comfy-menu-header {
@@ -496,7 +576,7 @@ body {
}
.comfy-menu .comfy-menu-actions button {
background-color: rgba(0, 0, 0, 0);
background-color: rgb(0 0 0 / 0);
padding: 0;
border: none;
cursor: pointer;
@@ -611,7 +691,7 @@ span.drag-handle::after {
min-width: 160px;
margin: 0;
padding: 3px;
font-weight: normal;
font-weight: 400;
}
.comfy-list-items button {
@@ -728,7 +808,7 @@ dialog {
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
background: rgb(0 0 0 / 0.5);
}
.comfy-dialog.comfyui-dialog.comfy-modal {
@@ -934,9 +1014,6 @@ audio.comfy-audio.empty-audio-widget {
.lg-node {
/* Disable text selection on all nodes */
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.lg-node .lg-slot,
@@ -963,7 +1040,6 @@ audio.comfy-audio.empty-audio-widget {
filter: none;
backdrop-filter: none;
text-shadow: none;
-webkit-mask-image: none;
mask-image: none;
clip-path: none;
background-image: none;

View File

@@ -26,9 +26,9 @@ ComfyUI supports three types of icons that can be used throughout the interface.
```vue
<template>
<!-- Primary icon set: Lucide -->
<i-lucide:download />
<i-lucide:settings />
<i-lucide:workflow class="text-2xl" />
<i class="icon-[lucide--download]" />
<i class="icon-[lucide--settings]" />
<i class="icon-[lucide--workflow]" class="text-2xl" />
<!-- Other popular icon sets -->
<i-mdi:folder-open />
@@ -41,7 +41,7 @@ ComfyUI supports three types of icons that can be used throughout the interface.
<!-- Carbon Icons -->
<!-- With styling -->
<i-lucide:save class="w-6 h-6 text-blue-500" />
<i class="icon-[lucide--save]" class="w-6 h-6 text-blue-500" />
</template>
```
@@ -77,7 +77,7 @@ ComfyUI supports three types of icons that can be used throughout the interface.
<!-- Iconify/Custom in button (template) -->
<Button>
<template #icon>
<i-lucide:save />
<i class="icon-[lucide--save]" />
</template>
Save File
</Button>
@@ -88,8 +88,8 @@ ComfyUI supports three types of icons that can be used throughout the interface.
```vue
<template>
<i-lucide:eye v-if="isVisible" />
<i-lucide:eye-off v-else />
<i class="icon-[lucide--eye]" v-if="isVisible" />
<i class="icon-[lucide--eye-off]" v-else />
<!-- Or with ternary -->
<component :is="isLocked ? 'i-lucide:lock' : 'i-lucide:lock-open'" />
@@ -100,7 +100,7 @@ ComfyUI supports three types of icons that can be used throughout the interface.
```vue
<template>
<i-lucide:info
<i class="icon-[lucide--info]"
v-tooltip="'Click for more information'"
class="cursor-pointer"
/>
@@ -174,20 +174,20 @@ No imports needed - icons are auto-discovered!
```vue
<template>
<!-- Size with Tailwind classes -->
<i-lucide:plus class="w-4 h-4" />
<i class="icon-[lucide--plus]" class="w-4 h-4" />
<!-- 16px -->
<i-lucide:plus class="w-6 h-6" />
<i class="icon-[lucide--plus]" class="w-6 h-6" />
<!-- 24px (default) -->
<i-lucide:plus class="w-8 h-8" />
<i class="icon-[lucide--plus]" class="w-8 h-8" />
<!-- 32px -->
<!-- Or text size -->
<i-lucide:plus class="text-sm" />
<i-lucide:plus class="text-2xl" />
<i class="icon-[lucide--plus]" class="text-sm" />
<i class="icon-[lucide--plus]" class="text-2xl" />
<!-- Colors -->
<i-lucide:check class="text-green-500" />
<i-lucide:x class="text-red-500" />
<i class="icon-[lucide--check]" class="text-green-500" />
<i class="icon-[lucide--x]" class="text-red-500" />
</template>
```
@@ -219,7 +219,7 @@ Always use `currentColor` in SVGs for automatic theme adaptation:
<!-- After -->
<Button>
<template #icon>
<i-lucide:download />
<i class="icon-[lucide--download]" />
</template>
</Button>
</template>

View File

@@ -4,22 +4,13 @@ import { addDynamicIconSelectors } from '@iconify/tailwind'
import { iconCollection } from './src/iconCollection'
export default {
content: [],
safelist: [
'icon-[lucide--folder]',
'icon-[lucide--package]',
'icon-[lucide--image]',
'icon-[lucide--video]',
'icon-[lucide--box]',
'icon-[lucide--audio-waveform]',
'icon-[lucide--message-circle]'
],
plugins: [
addDynamicIconSelectors({
iconSets: {
comfy: iconCollection,
lucide
},
scale: 1.2,
prefix: 'icon'
})
]

View File

@@ -16,10 +16,16 @@
@click="queuePrompt"
>
<template #icon>
<i-lucide:list-start v-if="workspaceStore.shiftDown" />
<i-lucide:play v-else-if="queueMode === 'disabled'" />
<i-lucide:fast-forward v-else-if="queueMode === 'instant'" />
<i-lucide:step-forward v-else-if="queueMode === 'change'" />
<i v-if="workspaceStore.shiftDown" class="icon-[lucide--list-start]" />
<i v-else-if="queueMode === 'disabled'" class="icon-[lucide--play]" />
<i
v-else-if="queueMode === 'instant'"
class="icon-[lucide--fast-forward]"
/>
<i
v-else-if="queueMode === 'change'"
class="icon-[lucide--step-forward]"
/>
</template>
<template #item="{ item }">
<Button

View File

@@ -1,7 +1,7 @@
<template>
<div class="relative inline-flex items-center">
<IconButton @click="toggle">
<i-lucide:more-vertical class="text-sm" />
<i class="icon-[lucide--more-vertical] text-sm" />
</IconButton>
<Popover

View File

@@ -29,7 +29,7 @@
@click="resetFilters"
>
<template #icon>
<i-lucide:filter-x />
<i class="icon-[lucide--filter-x]" />
</template>
</IconTextButton>
</div>
@@ -49,7 +49,7 @@
:show-clear-button="true"
>
<template #icon>
<i-lucide:cpu />
<i class="icon-[lucide--cpu]" />
</template>
</MultiSelect>
@@ -63,7 +63,7 @@
:show-clear-button="true"
>
<template #icon>
<i-lucide:target />
<i class="icon-[lucide--target]" />
</template>
</MultiSelect>
@@ -77,7 +77,7 @@
:show-clear-button="true"
>
<template #icon>
<i-lucide:file-text />
<i class="icon-[lucide--file-text]" />
</template>
</MultiSelect>
@@ -90,7 +90,7 @@
class="min-w-[270px]"
>
<template #icon>
<i-lucide:arrow-up-down />
<i class="icon-[lucide--arrow-up-down]" />
</template>
</SingleSelect>
</div>
@@ -111,7 +111,7 @@
v-if="!isLoading && filteredTemplates.length === 0"
class="flex flex-col items-center justify-center h-64 text-neutral-500"
>
<i-lucide:search class="w-12 h-12 mb-4 opacity-50" />
<i class="icon-[lucide--search] w-12 h-12 mb-4 opacity-50" />
<p class="text-lg mb-2">
{{ $t('templateWorkflows.noResults', 'No templates found') }}
</p>
@@ -128,7 +128,7 @@
<!-- Title -->
<span
v-if="isLoading"
class="inline-block h-8 w-48 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse"
class="inline-block h-8 w-48 bg-dialog-surface rounded animate-pulse"
></span>
<!-- Template Cards Grid -->
@@ -148,7 +148,7 @@
<CardTop ratio="landscape">
<template #default>
<div
class="w-full h-full bg-neutral-200 dark-theme:bg-neutral-700 animate-pulse"
class="w-full h-full bg-dialog-surface animate-pulse"
></div>
</template>
</CardTop>
@@ -157,10 +157,10 @@
<CardBottom>
<div class="px-4 py-3">
<div
class="h-6 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse mb-2"
class="h-6 bg-dialog-surface rounded animate-pulse mb-2"
></div>
<div
class="h-4 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse"
class="h-4 bg-dialog-surface rounded animate-pulse"
></div>
</div>
</CardBottom>
@@ -323,7 +323,7 @@
<CardTop ratio="square">
<template #default>
<div
class="w-full h-full bg-neutral-200 dark-theme:bg-neutral-700 animate-pulse"
class="w-full h-full bg-dialog-surface animate-pulse"
></div>
</template>
</CardTop>
@@ -332,10 +332,10 @@
<CardBottom>
<div class="px-4 py-3">
<div
class="h-6 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse mb-2"
class="h-6 bg-dialog-surface rounded animate-pulse mb-2"
></div>
<div
class="h-4 bg-neutral-200 dark-theme:bg-neutral-700 rounded animate-pulse"
class="h-4 bg-dialog-surface rounded animate-pulse"
></div>
</div>
</CardBottom>

View File

@@ -25,7 +25,7 @@
@click="() => commandStore.execute('Comfy.Canvas.Unlock')"
>
<template #icon>
<i-lucide:mouse-pointer-2 />
<i class="icon-[lucide--mouse-pointer-2]" />
</template>
</Button>
@@ -39,7 +39,7 @@
@click="() => commandStore.execute('Comfy.Canvas.Lock')"
>
<template #icon>
<i-lucide:hand />
<i class="icon-[lucide--hand]" />
</template>
</Button>
@@ -56,7 +56,7 @@
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
<i-lucide:focus />
<i class="icon-[lucide--focus]" />
</template>
</Button>
@@ -73,7 +73,7 @@
>
<span class="inline-flex text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i-lucide:chevron-down />
<i class="icon-[lucide--chevron-down]" />
</span>
</Button>
@@ -90,7 +90,7 @@
@click="() => commandStore.execute('Workspace.ToggleFocusMode')"
>
<template #icon>
<i-lucide:lightbulb />
<i class="icon-[lucide--lightbulb]" />
</template>
</Button>
@@ -111,7 +111,7 @@
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i-lucide:route-off />
<i class="icon-[lucide--route-off]" />
</template>
</Button>
</ButtonGroup>

View File

@@ -136,7 +136,7 @@ useEventListener(window, 'click', hideTooltip)
pointer-events: none;
background: var(--comfy-input-bg);
border-radius: 5px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
box-shadow: 0 0 5px rgb(0 0 0 / 0.4);
color: var(--input-text);
font-family: sans-serif;
left: 0;

View File

@@ -47,7 +47,7 @@ const canvasStore = useCanvasStore()
const previousCanvasDraggable = ref(true)
const onEdit = (newValue: string) => {
if (titleEditorStore.titleEditorTarget && newValue.trim() !== '') {
if (titleEditorStore.titleEditorTarget && newValue?.trim()) {
const trimmedTitle = newValue.trim()
titleEditorStore.titleEditorTarget.title = trimmedTitle

View File

@@ -229,10 +229,10 @@ watch(
</script>
<style>
.zoomInputContainer:focus-within {
border: 1px solid rgb(204, 204, 204);
border: 1px solid rgb(204 204 204);
}
.dark-theme .zoomInputContainer:focus-within {
border: 1px solid rgb(204, 204, 204);
border: 1px solid rgb(204 204 204);
}
</style>

View File

@@ -11,7 +11,7 @@
@click="toggleBypass"
>
<template #icon>
<i-lucide:ban class="w-4 h-4" />
<i class="icon-[lucide--ban] w-4 h-4" />
</template>
</Button>
</template>

View File

@@ -11,7 +11,7 @@
@click="() => commandStore.execute('Comfy.Graph.UnpackSubgraph')"
>
<template #icon>
<i-lucide:expand class="w-4 h-4" />
<i class="icon-[lucide--expand] w-4 h-4" />
</template>
</Button>
<Button
@@ -26,7 +26,7 @@
@click="() => commandStore.execute('Comfy.Graph.ConvertToSubgraph')"
>
<template #icon>
<i-lucide:shrink />
<i class="icon-[lucide--shrink]" />
</template>
</Button>
</template>

View File

@@ -10,7 +10,7 @@
@mouseleave="() => handleMouseLeave()"
@click="handleClick"
>
<i-lucide:play class="fill-path-white w-4 h-4" />
<i class="icon-[lucide--play] fill-path-white w-4 h-4" />
</Button>
</template>

View File

@@ -9,7 +9,7 @@
severity="secondary"
@click="frameNodes"
>
<i-lucide:frame class="w-4 h-4" />
<i class="icon-[lucide--frame] w-4 h-4" />
</Button>
</template>

View File

@@ -9,7 +9,7 @@
severity="secondary"
@click="toggleHelp"
>
<i-lucide:info class="w-4 h-4" />
<i class="icon-[lucide--info] w-4 h-4" />
</Button>
</template>

View File

@@ -14,10 +14,10 @@
<span v-if="option.shortcut" class="text-xs opacity-60">
{{ option.shortcut }}
</span>
<i-lucide:chevron-right
<i
v-if="option.hasSubmenu"
:size="14"
class="opacity-60"
class="icon-[lucide--chevron-right] opacity-60"
/>
<Badge
v-if="option.badge"

View File

@@ -11,7 +11,7 @@
severity="secondary"
@click="handleClick"
>
<i-lucide:more-vertical class="w-4 h-4" />
<i class="icon-[lucide--more-vertical] w-4 h-4" />
</Button>
</template>

View File

@@ -7,7 +7,7 @@
data-testid="refresh-button"
@click="refreshSelected"
>
<i-lucide:refresh-cw class="w-4 h-4" />
<i class="icon-[lucide--refresh-cw] w-4 h-4" />
</Button>
</template>

View File

@@ -10,7 +10,7 @@
@click="() => commandStore.execute('Comfy.PublishSubgraph')"
>
<template #icon>
<i-lucide:book-open />
<i class="icon-[lucide--book-open]" />
</template>
</Button>
</template>

View File

@@ -32,9 +32,9 @@
:style="{ backgroundColor: subOption.color }"
/>
<template v-else-if="!subOption.color">
<i-lucide:check
<i
v-if="isShapeSelected(subOption)"
class="w-4 h-4 flex-shrink-0"
class="icon-[lucide--check] w-4 h-4 flex-shrink-0"
/>
<div v-else class="w-4 flex-shrink-0" />
<span>{{ subOption.label }}</span>

View File

@@ -526,7 +526,7 @@ onMounted(async () => {
overflow-y: auto;
background: var(--p-content-background);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
box-shadow: 0 8px 32px rgb(0 0 0 / 0.15);
border: 1px solid var(--p-content-border-color);
backdrop-filter: blur(8px);
position: relative;
@@ -611,7 +611,7 @@ onMounted(async () => {
font-size: 0.8rem;
font-weight: 600;
color: var(--p-text-muted-color);
margin: 0 0 0.5rem 0;
margin: 0 0 0.5rem;
padding: 0 1rem;
text-transform: uppercase;
letter-spacing: 0.5px;
@@ -669,7 +669,7 @@ onMounted(async () => {
background: var(--p-content-background);
border-radius: 12px;
border: 1px solid var(--p-content-border-color);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
box-shadow: 0 8px 32px rgb(0 0 0 / 0.15);
overflow: hidden;
transition: opacity 0.15s ease-out;
}

View File

@@ -75,7 +75,7 @@
<!-- Chevron size identical to current -->
<template #dropdownicon>
<i-lucide:chevron-down class="text-lg text-neutral-400" />
<i class="icon-[lucide--chevron-down] text-lg text-neutral-400" />
</template>
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
@@ -89,9 +89,9 @@
: 'bg-neutral-100 dark-theme:bg-zinc-700'
"
>
<i-lucide:check
<i
v-if="slotProps.selected"
class="text-xs text-bold text-white"
class="icon-[lucide--check] text-xs text-bold text-white"
/>
</div>
<Button

View File

@@ -8,7 +8,7 @@ const meta: Meta<typeof SearchBox> = {
component: SearchBox,
tags: ['autodocs'],
argTypes: {
placeHolder: {
placeholder: {
control: 'text'
},
showBorder: {
@@ -22,7 +22,7 @@ const meta: Meta<typeof SearchBox> = {
}
},
args: {
placeHolder: 'Search...',
placeholder: 'Search...',
showBorder: false,
size: 'md'
}

View File

@@ -1,14 +1,14 @@
<template>
<div :class="wrapperStyle" @click="focusInput">
<i-lucide:search :class="iconColorStyle" />
<i class="icon-[lucide--search]" :class="iconColorStyle" />
<InputText
ref="input"
v-model="searchQuery"
:aria-label="
placeHolder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
placeholder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
"
:placeholder="
placeHolder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
placeholder || t('templateWidgets.sort.searchPlaceholder', 'Search...')
"
type="text"
unstyled
@@ -19,17 +19,19 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { computed, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { t } from '@/i18n'
import { cn } from '@/utils/tailwindUtil'
const {
placeHolder,
autofocus = false,
placeholder,
showBorder = false,
size = 'md'
} = defineProps<{
placeHolder?: string
autofocus?: boolean
placeholder?: string
showBorder?: boolean
size?: 'md' | 'lg'
}>()
@@ -43,6 +45,8 @@ const focusInput = () => {
}
}
onMounted(() => autofocus && focusInput())
const wrapperStyle = computed(() => {
const baseClasses = [
'relative flex w-full items-center gap-2',

View File

@@ -38,7 +38,7 @@
<!-- Trigger caret -->
<template #dropdownicon>
<i-lucide:chevron-down class="text-base text-neutral-500" />
<i class="icon-[lucide--chevron-down] text-base text-neutral-500" />
</template>
<!-- Option row -->
@@ -48,9 +48,9 @@
:style="optionStyle"
>
<span class="truncate">{{ option.name }}</span>
<i-lucide:check
<i
v-if="selected"
class="text-neutral-600 dark-theme:text-white"
class="icon-[lucide--check] text-neutral-600 dark-theme:text-white"
/>
</div>
</template>

View File

@@ -202,7 +202,6 @@ const truncateDefaultValue = (value: any, charLimit: number = 32): string => {
._sb_node_preview {
background-color: var(--comfy-menu-bg);
font-family: 'Open Sans', sans-serif;
font-size: small;
color: var(--descrip-text);
border: 1px solid var(--descrip-text);
min-width: 300px;
@@ -265,7 +264,7 @@ const truncateDefaultValue = (value: any, charLimit: number = 32): string => {
._long_field {
background: var(--bg-color);
border: 2px solid var(--border-color);
margin: 5px 5px 0 5px;
margin: 5px 5px 0;
border-radius: 10px;
line-height: 1.7;
text-wrap: nowrap;
@@ -278,7 +277,7 @@ const truncateDefaultValue = (value: any, charLimit: number = 32): string => {
._sb_preview_badge {
text-align: center;
background: var(--comfy-input-bg);
font-weight: bold;
font-weight: 700;
color: var(--error-text);
}
</style>

View File

@@ -89,7 +89,7 @@ const props = defineProps<{
:deep(.highlight) {
background-color: var(--p-primary-color);
color: var(--p-primary-contrast-color);
font-weight: bold;
font-weight: 700;
border-radius: 0.25rem;
padding: 0 0.125rem;
margin: -0.125rem 0.125rem;

View File

@@ -89,7 +89,7 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
.side-bar-button-selected .side-bar-button-icon {
font-size: var(--sidebar-icon-size) !important;
font-weight: bold;
font-weight: 700;
}
</style>

View File

@@ -5,7 +5,7 @@
@click="toggleShortcutsPanel"
>
<template #icon>
<i-lucide:keyboard />
<i class="icon-[lucide--keyboard]" />
</template>
</SidebarIcon>
</template>

View File

@@ -72,7 +72,7 @@ const modelDef = props.modelDef
object-fit: contain;
}
.model_preview_title {
font-weight: bold;
font-weight: 700;
text-align: center;
font-size: 14px;
}
@@ -89,6 +89,6 @@ const modelDef = props.modelDef
font-size: 10px;
}
.model_preview_prefix {
font-weight: bold;
font-weight: 700;
}
</style>

View File

@@ -32,7 +32,7 @@
@click.stop="editBlueprint"
>
<template #icon>
<i-lucide:square-pen />
<i class="icon-[lucide--square-pen]" />
</template>
</Button>
</template>

View File

@@ -66,7 +66,7 @@
outlined
@click="handleOutputLengthClick"
>
<span style="font-weight: bold">{{ flatOutputs.length }}</span>
<span style="font-weight: 700">{{ flatOutputs.length }}</span>
</Button>
</div>
</div>

View File

@@ -13,7 +13,7 @@
<div class="relative">
<span
v-if="shouldShowStatusIndicator"
class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-secondary-bg) w-4"
class="group-hover:hidden absolute font-bold text-2xl top-1/2 left-1/2 -translate-1/2 z-10 bg-(--comfy-menu-bg) w-4"
></span
>
<Button

View File

@@ -223,8 +223,8 @@ defineExpose({
@apply shadow-2xl;
}
.workflow-popover-fade.p-popover:after,
.workflow-popover-fade.p-popover:before {
.workflow-popover-fade.p-popover::after,
.workflow-popover-fade.p-popover::before {
--p-popover-border-color: var(--comfy-menu-secondary-bg);
left: 50%;
transform: translateX(calc(-50% + var(--shift)));

View File

@@ -3,7 +3,7 @@
<template #leftPanel>
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
<template #header-icon>
<i-lucide:puzzle class="text-neutral" />
<i class="icon-[lucide--puzzle] text-neutral" />
</template>
<template #header-title>
<span class="text-neutral text-base">{{ t('g.title') }}</span>
@@ -19,7 +19,7 @@
<div class="flex gap-2">
<IconTextButton type="primary" label="Upload Model" @click="() => {}">
<template #icon>
<i-lucide:upload />
<i class="icon-[lucide--upload]" />
</template>
</IconTextButton>
<MoreButton>
@@ -34,7 +34,7 @@
"
>
<template #icon>
<i-lucide:download />
<i class="icon-[lucide--download]" />
</template>
</IconTextButton>
<IconTextButton
@@ -47,7 +47,7 @@
"
>
<template #icon>
<i-lucide:scroll />
<i class="icon-[lucide--scroll]" />
</template>
</IconTextButton>
</template>
@@ -79,7 +79,7 @@
class="w-[135px]"
>
<template #icon>
<i-lucide:filter />
<i class="icon-[lucide--filter]" />
</template>
</SingleSelect>
</div>
@@ -99,7 +99,7 @@
class="!bg-white !text-neutral-900"
@click="() => {}"
>
<i-lucide:info />
<i class="icon-[lucide--info]" />
</IconButton>
</template>
<template #bottom-right>
@@ -107,7 +107,7 @@
<SquareChip label="1.2 MB" />
<SquareChip label="LoRA">
<template #icon>
<i-lucide:folder />
<i class="icon-[lucide--folder]" />
</template>
</SquareChip>
</template>

View File

@@ -5,7 +5,7 @@
:class="rightPanelButtonClasses"
@click="toggleRightPanel"
>
<i-lucide:panel-right class="text-sm" />
<i class="icon-[lucide--panel-right] text-sm" />
</IconButton>
<IconButton :class="closeButtonClasses" @click="closeDialog">
<i class="pi pi-times text-sm"></i>
@@ -29,8 +29,11 @@
<header v-if="$slots.header" :class="headerClasses">
<div class="flex-1 flex gap-2 shrink-0">
<IconButton v-if="!notMobile" @click="toggleLeftPanel">
<i-lucide:panel-left v-if="!showLeftPanel" class="text-sm" />
<i-lucide:panel-left-close v-else class="text-sm" />
<i
v-if="!showLeftPanel"
class="icon-[lucide--panel-left] text-sm"
/>
<i v-else class="icon-[lucide--panel-left-close] text-sm" />
</IconButton>
<slot name="header"></slot>
</div>
@@ -40,7 +43,7 @@
v-if="isRightPanelOpen && hasRightPanel"
@click="toggleRightPanel"
>
<i-lucide:panel-right-close class="text-sm" />
<i class="icon-[lucide--panel-right-close] text-sm" />
</IconButton>
</div>
</header>

View File

@@ -10,7 +10,7 @@
@click="onClick"
>
<NavIcon v-if="icon" :icon="icon" />
<i-lucide:folder v-else class="text-xs text-neutral" />
<i v-else class="icon-[lucide--folder] text-xs text-neutral" />
<span class="flex items-center">
<slot></slot>
</span>

View File

@@ -2,7 +2,7 @@
<header class="flex items-center justify-between h-16 px-6">
<div class="flex items-center gap-2 pl-1">
<slot name="icon">
<i-lucide:puzzle class="text-neutral text-base" />
<i class="icon-[lucide--puzzle] text-neutral text-base" />
</slot>
<h2 class="font-bold text-base text-neutral">
<slot></slot>

View File

@@ -116,7 +116,7 @@ function useVueNodeLifecycleIndividual() {
slotSyncManager.attemptStart(canvas as LGraphCanvas)
}
},
{ immediate: true }
{ immediate: true, flush: 'sync' }
)
// Handle case where Vue nodes are enabled but graph starts empty

View File

@@ -169,6 +169,74 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
: `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run`
}
// ---- constants ----
const SORA_SIZES = {
BASIC: new Set(['720x1280', '1280x720']),
PRO: new Set(['1024x1792', '1792x1024'])
}
const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])
// ---- sora-2 pricing helpers ----
function validateSora2Selection(
modelRaw: string,
duration: number,
sizeRaw: string
): string | undefined {
const model = modelRaw?.toLowerCase() ?? ''
const size = sizeRaw?.toLowerCase() ?? ''
if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'
if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
if (!ALL_SIZES.has(size))
return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
if (model.includes('sora-2-pro')) return undefined
if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))
return 'sora-2 supports only 720x1280 or 1280x720'
if (!model.includes('sora-2')) return 'Unsupported model'
return undefined
}
function perSecForSora2(modelRaw: string, sizeRaw: string): number {
const model = modelRaw?.toLowerCase() ?? ''
const size = sizeRaw?.toLowerCase() ?? ''
if (model.includes('sora-2-pro')) {
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3
}
if (model.includes('sora-2')) return 0.1
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1
}
function formatRunPrice(perSec: number, duration: number) {
return `$${(perSec * duration).toFixed(2)}/Run`
}
// ---- pricing calculator ----
const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
const getWidgetValue = (name: string) =>
String(node.widgets?.find((w) => w.name === name)?.value ?? '')
const model = getWidgetValue('model')
const size = getWidgetValue('size')
const duration = Number(
node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))
?.value
)
if (!model || !size || !duration) return 'Set model, duration & size'
const validationError = validateSora2Selection(model, duration, size)
if (validationError) return validationError
const perSec = perSecForSora2(model, size)
return formatRunPrice(perSec, duration)
}
/**
* Static pricing data for API nodes, now supporting both strings and functions
*/
@@ -195,6 +263,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
FluxProKontextMaxNode: {
displayPrice: '$0.08/Run'
},
OpenAIVideoSora2: {
displayPrice: sora2PricingCalculator
},
IdeogramV1: {
displayPrice: (node: LGraphNode): string => {
const numImagesWidget = node.widgets?.find(
@@ -1658,6 +1729,7 @@ export const useNodePricing = () => {
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
OpenAIVideoSora2: ['model', 'size', 'duration'],
OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'],

View File

@@ -246,7 +246,7 @@ onBeforeUnmount(() => {
/>
<div
v-if="filteredActive.length"
class="pt-1 pb-4 border-b-1 border-sand-100 dark-theme:border-charcoal-600"
class="pt-1 pb-4 border-b-1 border-node-component-border"
>
<div class="flex py-0 px-4 justify-between">
<div class="text-slate-100 text-[9px] font-semibold uppercase">
@@ -302,7 +302,7 @@ onBeforeUnmount(() => {
</div>
<div
v-if="recommendedWidgets.length"
class="justify-center flex py-4 border-t-1 border-sand-100 dark-theme:border-charcoal-600"
class="justify-center flex py-4 border-t-1 border-node-component-border"
>
<Button
size="small"

View File

@@ -16,7 +16,7 @@ defineEmits<{
function classes() {
return cn(
'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1',
'bg-pure-white dark-theme:bg-charcoal-800',
'bg-node-component-surface',
props.isDraggable
? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing'
: ''

View File

@@ -10,7 +10,7 @@ export function showSubgraphNodeDialog() {
position: 'topright',
pt: {
root: {
class: 'bg-pure-white dark-theme:bg-charcoal-800 mt-22'
class: 'bg-node-component-surface mt-22'
},
header: {
class: 'h-8 text-xs ml-3'

View File

@@ -30,14 +30,14 @@
}
.comfy-group-manage h2 {
margin: 0;
font-weight: normal;
font-weight: 400;
}
.comfy-group-manage main {
display: flex;
overflow: hidden;
}
.comfy-group-manage .drag-handle {
font-weight: bold;
font-weight: 700;
}
.comfy-group-manage-list {
border-right: 1px solid var(--comfy-menu-bg);
@@ -49,8 +49,7 @@
}
.comfy-group-manage-list-items {
max-height: calc(100% - 40px);
overflow-y: scroll;
overflow-x: hidden;
overflow: hidden scroll;
}
.comfy-group-manage-list li {
display: flex;

View File

@@ -1168,6 +1168,7 @@ class MaskEditorDialog extends ComfyDialog {
if (ComfyApp.clipspace?.imgs && paintedIndex !== undefined) {
// Create and set new image
const newImage = new Image()
newImage.crossOrigin = 'anonymous'
newImage.src = mkFileUrl({ ref: filepath, preview: true })
ComfyApp.clipspace.imgs[paintedIndex] = newImage
@@ -1209,6 +1210,7 @@ class MaskEditorDialog extends ComfyDialog {
if (!ComfyApp.clipspace?.imgs || indexToSaveTo === undefined) return
// Create and set new image
const newImage = new Image()
newImage.crossOrigin = 'anonymous'
newImage.src = mkFileUrl({ ref: filepath, preview: true })
ComfyApp.clipspace.imgs[indexToSaveTo] = newImage
@@ -4162,6 +4164,7 @@ class UIManager {
this.image = await new Promise<HTMLImageElement>((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => resolve(img)
img.onerror = reject
img.src = rgb_url.toString()
@@ -4173,6 +4176,7 @@ class UIManager {
this.paint_image = await new Promise<HTMLImageElement>(
(resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => resolve(img)
img.onerror = reject
img.src = paintURL.toString()
@@ -4308,6 +4312,7 @@ class UIManager {
private loadImage(imagePath: URL): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image() as HTMLImageElement
image.crossOrigin = 'anonymous'
image.onload = function () {
resolve(image)
}

View File

@@ -1944,7 +1944,7 @@
"connectionError": "Please check your connection and try again",
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Search assets...",
"searchAssetsPlaceholder": "Type to search...",
"allModels": "All Models",
"allCategory": "All {category}",
"unknown": "Unknown",

View File

@@ -23,6 +23,7 @@
<template #header>
<SearchBox
v-model="searchQuery"
:autofocus="true"
size="lg"
:placeholder="$t('assetBrowser.searchAssetsPlaceholder')"
class="max-w-96"

View File

@@ -66,15 +66,15 @@
"
>
<span v-if="asset.stats.stars" class="flex items-center gap-1">
<i-lucide:star class="size-3" />
<i class="icon-[lucide--star] size-3" />
{{ asset.stats.stars }}
</span>
<span v-if="asset.stats.downloadCount" class="flex items-center gap-1">
<i-lucide:download class="size-3" />
<i class="icon-[lucide--download] size-3" />
{{ asset.stats.downloadCount }}
</span>
<span v-if="asset.stats.formattedDate" class="flex items-center gap-1">
<i-lucide:clock class="size-3" />
<i class="icon-[lucide--clock] size-3" />
{{ asset.stats.formattedDate }}
</span>
</div>

View File

@@ -32,7 +32,7 @@
@update:model-value="handleFilterChange"
>
<template #icon>
<i-lucide:arrow-up-down class="size-3" />
<i class="icon-[lucide--arrow-up-down] size-3" />
</template>
</SingleSelect>
</div>

View File

@@ -27,7 +27,7 @@
)
"
>
<i-lucide:search class="size-10 mb-4" />
<i class="icon-[lucide--search] size-10 mb-4" />
<h3 class="text-lg font-medium mb-2">
{{ $t('assetBrowser.noAssetsFound') }}
</h3>
@@ -39,7 +39,8 @@
v-if="loading"
class="col-span-full flex items-center justify-center py-16"
>
<i-lucide:loader
<i
class="icon-[lucide--loader]"
:class="
cn('size-6 animate-spin', 'text-stone-300 dark-theme:text-stone-200')
"

View File

@@ -31,7 +31,7 @@
</Message>
<Message v-if="commandLineArgs" severity="secondary" pt:text="w-full">
<template #icon>
<i-lucide:terminal class="text-xl font-bold" />
<i class="icon-[lucide--terminal] text-xl font-bold" />
</template>
<div class="flex items-center justify-between">
<p>{{ commandLineArgs }}</p>

View File

@@ -72,14 +72,10 @@ export const useReleaseStore = defineStore('release', () => {
) === 0
)
const hasMediumOrHighAttention = computed(() =>
recentReleases.value
.slice(0, -1)
.some(
(release) =>
release.attention === 'medium' || release.attention === 'high'
)
)
const hasMediumOrHighAttention = computed(() => {
const attention = recentRelease.value?.attention
return attention === 'medium' || attention === 'high'
})
// Show toast if needed
const shouldShowToast = computed(() => {

View File

@@ -172,7 +172,7 @@ onMounted(async () => {
width: 448px;
padding: 16px 16px 8px;
background: #353535;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
box-shadow: 0 4px 4px rgb(0 0 0 / 0.25);
border-radius: 12px;
outline: 1px solid #4e4e4e;
outline-offset: -1px;
@@ -193,7 +193,7 @@ onMounted(async () => {
width: 42px;
height: 42px;
padding: 10px;
background: rgba(0, 122, 255, 0.2);
background: rgb(0 122 255 / 0.2);
border-radius: 8px;
display: flex;
justify-content: center;

View File

@@ -218,7 +218,7 @@ defineExpose({
width: 400px;
outline: 1px solid #4e4e4e;
outline-offset: -1px;
box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.3);
box-shadow: 0 8px 32px rgb(0 0 0 / 0.3);
position: relative;
}
@@ -293,12 +293,6 @@ defineExpose({
transform: translate(-50%, -50%) rotate(-45deg);
}
/* Content Section */
.popup-content {
display: flex;
flex-direction: column;
}
.content-text {
color: white;
font-size: 14px;

View File

@@ -21,7 +21,7 @@ import type { useTransformState } from '@/renderer/core/layout/transform/useTran
* const state = inject(TransformStateKey)!
* const screen = state.canvasToScreen({ x: 100, y: 50 })
*/
interface TransformState
export interface TransformState
extends Pick<
ReturnType<typeof useTransformState>,
'screenToCanvas' | 'canvasToScreen' | 'camera' | 'isNodeInViewport'

View File

@@ -28,7 +28,7 @@
@click.stop="toggleOptionsPanel"
>
<template #icon>
<i-lucide:settings-2 />
<i class="icon-[lucide--settings-2]" />
</template>
</Button>
<Button
@@ -40,12 +40,12 @@
@click.stop="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
>
<template #icon>
<i-lucide:x />
<i class="icon-[lucide--x]" />
</template>
</Button>
<hr
class="absolute top-5 bg-[#E1DED5] dark-theme:bg-[#262729] h-[1px] border-0"
class="absolute top-5 bg-node-component-border h-px border-0"
:style="{
width: containerStyles.width
}"

View File

@@ -13,7 +13,7 @@
(value) => $emit('updateOption', 'Comfy.Minimap.NodeColors', value)
"
/>
<i-lucide:palette />
<i class="icon-[lucide--palette]" />
<label for="node-colors">{{ $t('minimap.nodeColors') }}</label>
</div>
@@ -27,7 +27,7 @@
(value) => $emit('updateOption', 'Comfy.Minimap.ShowLinks', value)
"
/>
<i-lucide:route />
<i class="icon-[lucide--route]" />
<label for="show-links">{{ $t('minimap.showLinks') }}</label>
</div>
@@ -41,7 +41,7 @@
(value) => $emit('updateOption', 'Comfy.Minimap.ShowGroups', value)
"
/>
<i-lucide:frame />
<i class="icon-[lucide--frame]" />
<label for="show-groups">{{ $t('minimap.showGroups') }}</label>
</div>
@@ -56,7 +56,7 @@
$emit('updateOption', 'Comfy.Minimap.RenderBypassState', value)
"
/>
<i-lucide:circle-slash-2 />
<i class="icon-[lucide--circle-slash-2]" />
<label for="render-bypass">{{ $t('minimap.renderBypassState') }}</label>
</div>
@@ -71,7 +71,7 @@
$emit('updateOption', 'Comfy.Minimap.RenderErrorState', value)
"
/>
<i-lucide:message-circle-warning />
<i class="icon-[lucide--message-circle-warning]" />
<label for="render-error">{{ $t('minimap.renderErrorState') }}</label>
</div>
</div>

View File

@@ -19,7 +19,7 @@
v-if="videoError"
class="w-full h-[352px] flex flex-col items-center justify-center text-white text-center bg-gray-800/50"
>
<i-lucide:video-off class="w-12 h-12 mb-2 text-gray-400" />
<i class="icon-[lucide--video-off] w-12 h-12 mb-2 text-gray-400" />
<p class="text-sm text-gray-300">{{ $t('g.videoFailedToLoad') }}</p>
<p class="text-xs text-gray-400 mt-1">
{{ getVideoFilename(currentVideoUrl) }}
@@ -54,7 +54,7 @@
:aria-label="$t('g.downloadVideo')"
@click="handleDownload"
>
<i-lucide:download class="w-4 h-4" />
<i class="icon-[lucide--download] w-4 h-4" />
</button>
<!-- Close Button -->
@@ -64,7 +64,7 @@
:aria-label="$t('g.removeVideo')"
@click="handleRemove"
>
<i-lucide:x class="w-4 h-4" />
<i class="icon-[lucide--x] w-4 h-4" />
</button>
</div>

View File

@@ -19,7 +19,7 @@
v-if="imageError"
class="w-full h-[352px] flex flex-col items-center justify-center text-white text-center bg-gray-800/50"
>
<i-lucide:image-off class="w-12 h-12 mb-2 text-gray-400" />
<i class="icon-[lucide--image-off] w-12 h-12 mb-2 text-gray-400" />
<p class="text-sm text-gray-300">{{ $t('g.imageFailedToLoad') }}</p>
<p class="text-xs text-gray-400 mt-1">
{{ getImageFilename(currentImageUrl) }}
@@ -53,7 +53,7 @@
:aria-label="$t('g.editOrMaskImage')"
@click="handleEditMask"
>
<i-lucide:venetian-mask class="w-4 h-4" />
<i class="icon-[lucide--venetian-mask] w-4 h-4" />
</button>
<!-- Download Button -->
@@ -63,7 +63,7 @@
:aria-label="$t('g.downloadImage')"
@click="handleDownload"
>
<i-lucide:download class="w-4 h-4" />
<i class="icon-[lucide--download] w-4 h-4" />
</button>
<!-- Close Button -->
@@ -73,7 +73,7 @@
:aria-label="$t('g.removeImage')"
@click="handleRemove"
>
<i-lucide:x class="w-4 h-4" />
<i class="icon-[lucide--x] w-4 h-4" />
</button>
</div>

View File

@@ -27,9 +27,7 @@
<script setup lang="ts">
import {
type ComponentPublicInstance,
type Ref,
computed,
inject,
onErrorCaptured,
ref,
watchEffect
@@ -73,24 +71,21 @@ const hasSlotError = computed(() => {
const errorClassesDot = computed(() => {
return hasSlotError.value
? 'ring-2 ring-error dark-theme:ring-error ring-offset-0 rounded-full'
? 'ring-2 ring-error ring-offset-0 rounded-full'
: ''
})
const labelClasses = computed(() =>
hasSlotError.value
? 'text-error dark-theme:text-error font-medium'
: 'dark-theme:text-slate-200 text-stone-200'
? 'text-error font-medium'
: 'text-node-component-slot-text'
)
const renderError = ref<string | null>(null)
const { toastErrorHandler } = useErrorHandling()
const tooltipContainer =
inject<Ref<HTMLElement | undefined>>('tooltipContainer')
const { getInputSlotTooltip, createTooltipConfig } = useNodeTooltips(
props.nodeType || '',
tooltipContainer
props.nodeType || ''
)
const tooltipConfig = computed(() => {

View File

@@ -8,12 +8,12 @@
:data-node-id="nodeData.id"
:class="
cn(
'bg-white dark-theme:bg-charcoal-800',
'bg-node-component-surface',
'lg-node absolute rounded-2xl touch-none',
'border-1 border-solid border-gray-400 dark-theme:border-stone-200',
'border-1 border-solid border-node-component-border',
// hover (only when node should handle events)
shouldHandleNodePointerEvents &&
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
'hover:ring-7 ring-node-component-ring',
'outline-transparent -outline-offset-2 outline-2',
borderClass,
outlineClass,
@@ -113,12 +113,19 @@
</div>
</div>
</template>
<!-- Resize handle -->
<div
v-if="!isCollapsed"
class="absolute bottom-0 right-0 w-3 h-3 cursor-se-resize opacity-0 hover:opacity-20 hover:bg-white transition-opacity duration-200"
@pointerdown.stop="startResize"
/>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, inject, onErrorCaptured, onMounted, provide, ref } from 'vue'
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
@@ -145,6 +152,7 @@ import {
} from '@/utils/graphTraversalUtil'
import { cn } from '@/utils/tailwindUtil'
import { useNodeResize } from '../composables/useNodeResize'
import NodeContent from './NodeContent.vue'
import NodeHeader from './NodeHeader.vue'
import NodeSlots from './NodeSlots.vue'
@@ -173,6 +181,11 @@ const { selectedNodeIds } = storeToRefs(useCanvasStore())
// Inject transform state for coordinate conversion
const transformState = inject(TransformStateKey)
if (!transformState) {
throw new Error(
'TransformState must be provided for node resize functionality'
)
}
// Computed selection state - only this node re-evaluates when its selection changes
const isSelected = computed(() => {
@@ -264,6 +277,19 @@ onMounted(() => {
}
})
const { startResize } = useNodeResize(
(newSize, element) => {
// Apply size directly to DOM element - ResizeObserver will pick this up
if (isCollapsed.value) return
element.style.width = `${newSize.width}px`
element.style.height = `${newSize.height}px`
},
{
transformState
}
)
// Track collapsed state
const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
@@ -274,8 +300,7 @@ const hasCustomContent = computed(() => {
})
// Computed classes and conditions for better reusability
const separatorClasses =
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
const separatorClasses = 'bg-node-component-border h-px mx-0 w-full lod-toggle'
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
@@ -287,17 +312,17 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
const borderClass = computed(() => {
return (
(hasAnyError.value && 'border-error dark-theme:border-error') ||
(executing.value && 'border-blue-500')
(hasAnyError.value && 'border-error') ||
(executing.value && 'border-node-executing')
)
})
const outlineClass = computed(() => {
return (
return cn(
isSelected.value &&
((hasAnyError.value && 'outline-error dark-theme:outline-error') ||
(executing.value && 'outline-blue-500 dark-theme:outline-blue-500') ||
'outline-black dark-theme:outline-white')
((hasAnyError.value && 'outline-error ') ||
(executing.value && 'outline-node-executing') ||
'outline-node-component-outline')
)
})
@@ -371,5 +396,4 @@ const nodeMedia = computed(() => {
})
const nodeContainerRef = ref()
provide('tooltipContainer', nodeContainerRef)
</script>

View File

@@ -1,13 +1,11 @@
<template>
<div class="scale-75">
<div
class="bg-white dark-theme:bg-charcoal-800 lg-node absolute rounded-2xl border border-solid border-sand-100 dark-theme:border-charcoal-600 outline-transparent -outline-offset-2 outline-2 pointer-events-none"
class="bg-node-component-surface lg-node absolute rounded-2xl border border-solid border-node-component-border outline-transparent -outline-offset-2 outline-2 pointer-events-none"
>
<NodeHeader :node-data="nodeData" :readonly="readonly" />
<div
class="bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full mb-4"
/>
<div class="bg-node-component-border h-px mx-0 w-full mb-4" />
<div class="flex flex-col gap-4 pb-4">
<NodeSlots

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