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 run: sleep 10
- name: Restore cached setup - name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 uses: actions/cache/restore@v4
with: with:
fail-on-cache-miss: true fail-on-cache-miss: true
path: | path: |
@@ -155,7 +155,7 @@ jobs:
run: sleep 10 run: sleep 10
- name: Restore cached setup - name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 uses: actions/cache/restore@v4
with: with:
fail-on-cache-miss: true fail-on-cache-miss: true
path: | 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-')) 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 runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup Frontend - name: Setup Frontend
uses: ./.github/actions/setup-frontend uses: ./.github/actions/setup-frontend

View File

@@ -3,114 +3,165 @@ name: Update Playwright Expectations
on: on:
pull_request: pull_request:
types: [ labeled ] types: [labeled]
issue_comment:
types: [created]
jobs: jobs:
test: test:
runs-on: ubuntu-latest 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: steps:
- name: Checkout workflow repo - name: Initial Checkout
uses: actions/checkout@v5 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
const { data } = await github.rest.actions.listWorkflowRuns({ - name: Pull Request Checkout
owner, if: github.event.issue.pull_request && github.event_name == 'issue_comment'
repo, run: gh pr checkout ${{ github.event.issue.number }}
workflow_id: 'tests-ci.yaml', env:
head_sha: headSha, GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
event: 'pull_request',
per_page: 1,
})
const run = data.workflow_runs?.[0]
let has = 'false' - name: Setup Frontend
let runId = '' uses: ./.github/actions/setup-frontend
if (run) {
runId = String(run.id) - name: Setup Playwright
const { data: { artifacts = [] } } = await github.rest.actions.listWorkflowRunArtifacts({ 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, owner,
repo, repo,
run_id: run.id, workflow_id: 'tests-ci.yaml',
per_page: 100, head_sha: headSha,
event: 'pull_request',
per_page: 1,
}) })
if (artifacts.some(a => a.name === 'failed-screenshot-tests' && !a.expired)) has = 'true' const run = data.workflow_runs?.[0]
}
core.setOutput('run_id', runId)
core.setOutput('has_manifest', has)
- name: Download failed screenshot manifest let has = 'false'
if: steps.locate-manifest.outputs.has_manifest == 'true' let runId = ''
uses: actions/download-artifact@v4 if (run) {
with: runId = String(run.id)
run-id: ${{ steps.locate-manifest.outputs.run_id }} const { data: { artifacts = [] } } = await github.rest.actions.listWorkflowRunArtifacts({
name: failed-screenshot-tests owner,
path: ComfyUI_frontend/ci-rerun 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 - name: Download failed screenshot manifest
id: playwright-tests if: steps.locate-manifest.outputs.has_manifest == 'true'
shell: bash uses: actions/download-artifact@v4
working-directory: ComfyUI_frontend with:
run: | run-id: ${{ steps.locate-manifest.outputs.run_id }}
set -euo pipefail name: failed-screenshot-tests
if [ ! -d ci-rerun ]; then path: ComfyUI_frontend/ci-rerun
echo "No manifest found; running full suite as fallback"
pnpm exec playwright test --update-snapshots - name: Re-run failed screenshot tests and update snapshots
exit 0 id: playwright-tests
fi shell: bash
shopt -s nullglob working-directory: ComfyUI_frontend
files=(ci-rerun/*.txt) continue-on-error: true
if [ ${#files[@]} -eq 0 ]; then run: |
echo "Manifest is empty; running full suite as fallback" set -euo pipefail
pnpm exec playwright test --update-snapshots if [ ! -d ci-rerun ]; then
exit 0 echo "No manifest found; running full suite as fallback"
fi PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
for f in "${files[@]}"; do pnpm exec playwright test --update-snapshots \
project="$(basename "$f" .txt)" --reporter=line --reporter=html
mapfile -t lines < "$f" exit 0
# Filter out blank lines fi
filtered=( ) shopt -s nullglob
for l in "${lines[@]}"; do files=(ci-rerun/*.txt)
[ -n "$l" ] && filtered+=("$l") if [ ${#files[@]} -eq 0 ]; then
done echo "Manifest is empty; running full suite as fallback"
if [ ${#filtered[@]} -eq 0 ]; then PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
continue 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 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 ## 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:** **Example:**
```vue ```vue
<script setup lang="ts"> <script setup lang="ts">
import { Trophy, Settings } from 'lucide-vue-next'
</script> </script>
<template> <template>
<Trophy :size="16" class="text-neutral" /> <i class="icon-[lucide--trophy] text-neutral size-4" />
<Settings :size="16" class="text-neutral" /> <i class="icon-[lucide--settings] text-neutral size-4" />
</template> </template>
``` ```

View File

@@ -7,15 +7,15 @@
} }
], ],
"rules": { "rules": {
"import-notation": "url", "import-notation": "string",
"font-family-no-missing-generic-family-keyword": true, "font-family-no-missing-generic-family-keyword": true,
"declaration-block-no-redundant-longhand-properties": true,
"declaration-property-value-no-unknown": [ "declaration-property-value-no-unknown": [
true, true,
{ {
"ignoreProperties": { "ignoreProperties": {
"speak": ["none"], "speak": ["none"],
"app-region": ["drag", "no-drag"] "app-region": ["drag", "no-drag"],
"/^(width|height)$/": ["/^v-bind/"]
} }
} }
], ],
@@ -35,7 +35,7 @@
"selector-max-type": 2, "selector-max-type": 2,
"declaration-block-no-duplicate-properties": true, "declaration-block-no-duplicate-properties": true,
"block-no-empty": true, "block-no-empty": true,
"no-descending-specificity": true, "no-descending-specificity": null,
"no-duplicate-at-import-rules": true, "no-duplicate-at-import-rules": true,
"at-rule-no-unknown": [ "at-rule-no-unknown": [
true, true,
@@ -57,7 +57,8 @@
true, true,
{ {
"ignoreFunctions": [ "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): 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" />` 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 />` 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. 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')" :value="$t('install.gpuPicker.recommended')"
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]" 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>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

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.describe('Vue Node Groups', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
@@ -15,6 +16,7 @@ test.describe('Vue Node Groups', () => {
await comfyPage.page.getByText('Load Checkpoint').click() await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] }) await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY) await comfyPage.page.keyboard.press(CREATE_GROUP_HOTKEY)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-create-group.png' '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 // Check initial expanded state icon
let iconClass = await vueNode.getCollapseIconClass() let iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down') expect(iconClass).not.toContain('-rotate-90')
// Collapse and check icon // Collapse and check icon
await vueNode.toggleCollapse() await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass() iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-right') expect(iconClass).toContain('-rotate-90')
// Expand and check icon // Expand and check icon
await vueNode.toggleCollapse() await vueNode.toggleCollapse()
iconClass = await vueNode.getCollapseIconClass() iconClass = await vueNode.getCollapseIconClass()
expect(iconClass).toContain('pi-chevron-down') expect(iconClass).not.toContain('-rotate-90')
}) })
test('should preserve title when collapsing/expanding', async ({ 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: { compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199 // https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
css: (text: string) => css: (text: string) =>
[ [...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g)]
...text.replaceAll('plugin', 'import').matchAll(/(?<=@)import[^;]+/g) .map((match) => match[0].replace(/url\(['"]?([^'"()]+)['"]?\)/, '$1'))
].join('\n') .join('\n')
}, },
vite: { vite: {
config: ['vite?(.*).config.mts'] config: ['vite?(.*).config.mts']

View File

@@ -9,35 +9,6 @@
@config '../../tailwind.config.ts'; @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) { @media (prefers-color-scheme: dark) {
:root { :root {
--fg-color: #fff; --fg-color: #fff;
@@ -128,12 +99,121 @@
--color-dark-elevation-2: rgba(from white r g b / 0.03); --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 { @theme inline {
--color-node-component-surface: var(--color-charcoal-600); --color-backdrop: var(--backdrop);
--color-node-component-surface-highlight: var(--color-slate-100); --color-dialog-surface: var(--dialog-surface);
--color-node-component-surface-hovered: var(--color-charcoal-400); --color-node-component-border: var(--node-component-border);
--color-node-component-surface-selected: var(--color-charcoal-200); --color-node-component-executing: var(--node-component-executing);
--color-node-stroke: var(--color-stone-100); --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 { @custom-variant dark-theme {
@@ -418,7 +498,7 @@ body {
/* Strong and emphasis */ /* Strong and emphasis */
.comfy-markdown-content strong { .comfy-markdown-content strong {
font-weight: bold; font-weight: 700;
} }
.comfy-markdown-content em { .comfy-markdown-content em {
@@ -429,7 +509,7 @@ body {
display: none; /* Hidden by default */ display: none; /* Hidden by default */
position: fixed; /* Stay in place */ position: fixed; /* Stay in place */
z-index: 100; /* Sit on top */ z-index: 100; /* Sit on top */
padding: 30px 30px 10px 30px; padding: 30px 30px 10px;
background-color: var(--comfy-menu-bg); /* Modal background */ background-color: var(--comfy-menu-bg); /* Modal background */
color: var(--error-text); color: var(--error-text);
box-shadow: 0 0 20px #888888; box-shadow: 0 0 20px #888888;
@@ -477,8 +557,8 @@ body {
background-color: var(--comfy-menu-bg); background-color: var(--comfy-menu-bg);
font-family: sans-serif; font-family: sans-serif;
padding: 10px; padding: 10px;
border-radius: 0 8px 8px 8px; border-radius: 0 8px 8px;
box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4); box-shadow: 3px 3px 8px rgb(0 0 0 / 0.4);
} }
.comfy-menu-header { .comfy-menu-header {
@@ -496,7 +576,7 @@ body {
} }
.comfy-menu .comfy-menu-actions button { .comfy-menu .comfy-menu-actions button {
background-color: rgba(0, 0, 0, 0); background-color: rgb(0 0 0 / 0);
padding: 0; padding: 0;
border: none; border: none;
cursor: pointer; cursor: pointer;
@@ -611,7 +691,7 @@ span.drag-handle::after {
min-width: 160px; min-width: 160px;
margin: 0; margin: 0;
padding: 3px; padding: 3px;
font-weight: normal; font-weight: 400;
} }
.comfy-list-items button { .comfy-list-items button {
@@ -728,7 +808,7 @@ dialog {
} }
dialog::backdrop { dialog::backdrop {
background: rgba(0, 0, 0, 0.5); background: rgb(0 0 0 / 0.5);
} }
.comfy-dialog.comfyui-dialog.comfy-modal { .comfy-dialog.comfyui-dialog.comfy-modal {
@@ -934,9 +1014,6 @@ audio.comfy-audio.empty-audio-widget {
.lg-node { .lg-node {
/* Disable text selection on all nodes */ /* Disable text selection on all nodes */
user-select: none; user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
} }
.lg-node .lg-slot, .lg-node .lg-slot,
@@ -963,7 +1040,6 @@ audio.comfy-audio.empty-audio-widget {
filter: none; filter: none;
backdrop-filter: none; backdrop-filter: none;
text-shadow: none; text-shadow: none;
-webkit-mask-image: none;
mask-image: none; mask-image: none;
clip-path: none; clip-path: none;
background-image: none; background-image: none;

View File

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

View File

@@ -4,22 +4,13 @@ import { addDynamicIconSelectors } from '@iconify/tailwind'
import { iconCollection } from './src/iconCollection' import { iconCollection } from './src/iconCollection'
export default { 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: [ plugins: [
addDynamicIconSelectors({ addDynamicIconSelectors({
iconSets: { iconSets: {
comfy: iconCollection, comfy: iconCollection,
lucide lucide
}, },
scale: 1.2,
prefix: 'icon' prefix: 'icon'
}) })
] ]

View File

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

View File

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

View File

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

View File

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

View File

@@ -136,7 +136,7 @@ useEventListener(window, 'click', hideTooltip)
pointer-events: none; pointer-events: none;
background: var(--comfy-input-bg); background: var(--comfy-input-bg);
border-radius: 5px; 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); color: var(--input-text);
font-family: sans-serif; font-family: sans-serif;
left: 0; left: 0;

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
@mouseleave="() => handleMouseLeave()" @mouseleave="() => handleMouseLeave()"
@click="handleClick" @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> </Button>
</template> </template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,7 +89,7 @@ const props = defineProps<{
:deep(.highlight) { :deep(.highlight) {
background-color: var(--p-primary-color); background-color: var(--p-primary-color);
color: var(--p-primary-contrast-color); color: var(--p-primary-contrast-color);
font-weight: bold; font-weight: 700;
border-radius: 0.25rem; border-radius: 0.25rem;
padding: 0 0.125rem; padding: 0 0.125rem;
margin: -0.125rem 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 { .side-bar-button-selected .side-bar-button-icon {
font-size: var(--sidebar-icon-size) !important; font-size: var(--sidebar-icon-size) !important;
font-weight: bold; font-weight: 700;
} }
</style> </style>

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
<div class="relative"> <div class="relative">
<span <span
v-if="shouldShowStatusIndicator" 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 ></span
> >
<Button <Button

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
@click="onClick" @click="onClick"
> >
<NavIcon v-if="icon" :icon="icon" /> <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"> <span class="flex items-center">
<slot></slot> <slot></slot>
</span> </span>

View File

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

View File

@@ -116,7 +116,7 @@ function useVueNodeLifecycleIndividual() {
slotSyncManager.attemptStart(canvas as LGraphCanvas) slotSyncManager.attemptStart(canvas as LGraphCanvas)
} }
}, },
{ immediate: true } { immediate: true, flush: 'sync' }
) )
// Handle case where Vue nodes are enabled but graph starts empty // 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` : `$${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 * Static pricing data for API nodes, now supporting both strings and functions
*/ */
@@ -195,6 +263,9 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
FluxProKontextMaxNode: { FluxProKontextMaxNode: {
displayPrice: '$0.08/Run' displayPrice: '$0.08/Run'
}, },
OpenAIVideoSora2: {
displayPrice: sora2PricingCalculator
},
IdeogramV1: { IdeogramV1: {
displayPrice: (node: LGraphNode): string => { displayPrice: (node: LGraphNode): string => {
const numImagesWidget = node.widgets?.find( const numImagesWidget = node.widgets?.find(
@@ -1658,6 +1729,7 @@ export const useNodePricing = () => {
MinimaxHailuoVideoNode: ['resolution', 'duration'], MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'], OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'], OpenAIDalle2: ['size', 'n'],
OpenAIVideoSora2: ['model', 'size', 'duration'],
OpenAIGPTImage1: ['quality', 'n'], OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images', 'turbo'], IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'], IdeogramV2: ['num_images', 'turbo'],

View File

@@ -246,7 +246,7 @@ onBeforeUnmount(() => {
/> />
<div <div
v-if="filteredActive.length" 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="flex py-0 px-4 justify-between">
<div class="text-slate-100 text-[9px] font-semibold uppercase"> <div class="text-slate-100 text-[9px] font-semibold uppercase">
@@ -302,7 +302,7 @@ onBeforeUnmount(() => {
</div> </div>
<div <div
v-if="recommendedWidgets.length" 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 <Button
size="small" size="small"

View File

@@ -16,7 +16,7 @@ defineEmits<{
function classes() { function classes() {
return cn( return cn(
'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1', '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 props.isDraggable
? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing' ? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing'
: '' : ''

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,15 +66,15 @@
" "
> >
<span v-if="asset.stats.stars" class="flex items-center gap-1"> <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 }} {{ asset.stats.stars }}
</span> </span>
<span v-if="asset.stats.downloadCount" class="flex items-center gap-1"> <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 }} {{ asset.stats.downloadCount }}
</span> </span>
<span v-if="asset.stats.formattedDate" class="flex items-center gap-1"> <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 }} {{ asset.stats.formattedDate }}
</span> </span>
</div> </div>

View File

@@ -32,7 +32,7 @@
@update:model-value="handleFilterChange" @update:model-value="handleFilterChange"
> >
<template #icon> <template #icon>
<i-lucide:arrow-up-down class="size-3" /> <i class="icon-[lucide--arrow-up-down] size-3" />
</template> </template>
</SingleSelect> </SingleSelect>
</div> </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"> <h3 class="text-lg font-medium mb-2">
{{ $t('assetBrowser.noAssetsFound') }} {{ $t('assetBrowser.noAssetsFound') }}
</h3> </h3>
@@ -39,7 +39,8 @@
v-if="loading" v-if="loading"
class="col-span-full flex items-center justify-center py-16" class="col-span-full flex items-center justify-center py-16"
> >
<i-lucide:loader <i
class="icon-[lucide--loader]"
:class=" :class="
cn('size-6 animate-spin', 'text-stone-300 dark-theme:text-stone-200') cn('size-6 animate-spin', 'text-stone-300 dark-theme:text-stone-200')
" "

View File

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

View File

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

View File

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

View File

@@ -218,7 +218,7 @@ defineExpose({
width: 400px; width: 400px;
outline: 1px solid #4e4e4e; outline: 1px solid #4e4e4e;
outline-offset: -1px; 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; position: relative;
} }
@@ -293,12 +293,6 @@ defineExpose({
transform: translate(-50%, -50%) rotate(-45deg); transform: translate(-50%, -50%) rotate(-45deg);
} }
/* Content Section */
.popup-content {
display: flex;
flex-direction: column;
}
.content-text { .content-text {
color: white; color: white;
font-size: 14px; font-size: 14px;

View File

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

View File

@@ -28,7 +28,7 @@
@click.stop="toggleOptionsPanel" @click.stop="toggleOptionsPanel"
> >
<template #icon> <template #icon>
<i-lucide:settings-2 /> <i class="icon-[lucide--settings-2]" />
</template> </template>
</Button> </Button>
<Button <Button
@@ -40,12 +40,12 @@
@click.stop="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')" @click.stop="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
> >
<template #icon> <template #icon>
<i-lucide:x /> <i class="icon-[lucide--x]" />
</template> </template>
</Button> </Button>
<hr <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="{ :style="{
width: containerStyles.width width: containerStyles.width
}" }"

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,12 +8,12 @@
:data-node-id="nodeData.id" :data-node-id="nodeData.id"
:class=" :class="
cn( cn(
'bg-white dark-theme:bg-charcoal-800', 'bg-node-component-surface',
'lg-node absolute rounded-2xl touch-none', '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) // hover (only when node should handle events)
shouldHandleNodePointerEvents && 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', 'outline-transparent -outline-offset-2 outline-2',
borderClass, borderClass,
outlineClass, outlineClass,
@@ -113,12 +113,19 @@
</div> </div>
</div> </div>
</template> </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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { storeToRefs } from 'pinia' 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 type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu' import { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
@@ -145,6 +152,7 @@ import {
} from '@/utils/graphTraversalUtil' } from '@/utils/graphTraversalUtil'
import { cn } from '@/utils/tailwindUtil' import { cn } from '@/utils/tailwindUtil'
import { useNodeResize } from '../composables/useNodeResize'
import NodeContent from './NodeContent.vue' import NodeContent from './NodeContent.vue'
import NodeHeader from './NodeHeader.vue' import NodeHeader from './NodeHeader.vue'
import NodeSlots from './NodeSlots.vue' import NodeSlots from './NodeSlots.vue'
@@ -173,6 +181,11 @@ const { selectedNodeIds } = storeToRefs(useCanvasStore())
// Inject transform state for coordinate conversion // Inject transform state for coordinate conversion
const transformState = inject(TransformStateKey) 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 // Computed selection state - only this node re-evaluates when its selection changes
const isSelected = computed(() => { 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 // Track collapsed state
const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false) const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
@@ -274,8 +300,7 @@ const hasCustomContent = computed(() => {
}) })
// Computed classes and conditions for better reusability // Computed classes and conditions for better reusability
const separatorClasses = const separatorClasses = 'bg-node-component-border h-px mx-0 w-full lod-toggle'
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300' const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState( const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
@@ -287,17 +312,17 @@ const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
const borderClass = computed(() => { const borderClass = computed(() => {
return ( return (
(hasAnyError.value && 'border-error dark-theme:border-error') || (hasAnyError.value && 'border-error') ||
(executing.value && 'border-blue-500') (executing.value && 'border-node-executing')
) )
}) })
const outlineClass = computed(() => { const outlineClass = computed(() => {
return ( return cn(
isSelected.value && isSelected.value &&
((hasAnyError.value && 'outline-error dark-theme:outline-error') || ((hasAnyError.value && 'outline-error ') ||
(executing.value && 'outline-blue-500 dark-theme:outline-blue-500') || (executing.value && 'outline-node-executing') ||
'outline-black dark-theme:outline-white') 'outline-node-component-outline')
) )
}) })
@@ -371,5 +396,4 @@ const nodeMedia = computed(() => {
}) })
const nodeContainerRef = ref() const nodeContainerRef = ref()
provide('tooltipContainer', nodeContainerRef)
</script> </script>

View File

@@ -1,13 +1,11 @@
<template> <template>
<div class="scale-75"> <div class="scale-75">
<div <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" /> <NodeHeader :node-data="nodeData" :readonly="readonly" />
<div <div class="bg-node-component-border h-px mx-0 w-full mb-4" />
class="bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full mb-4"
/>
<div class="flex flex-col gap-4 pb-4"> <div class="flex flex-col gap-4 pb-4">
<NodeSlots <NodeSlots

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