Compare commits

..

1 Commits

Author SHA1 Message Date
snomiao
8eac858bd6 [bugfix] Fix Chinese locale detection to properly handle traditional Chinese
- Update getDefaultLocale() to distinguish between zh-CN (simplified) and zh-TW/zh-HK/zh-Hant (traditional)
- Fixes issue where browsers set to traditional Chinese were incorrectly defaulting to simplified Chinese translations
- Preserves existing behavior for all other languages

Fixes #5151

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 04:48:14 +00:00
148 changed files with 20228 additions and 15144 deletions

View File

@@ -128,25 +128,7 @@ echo "Last stable release: $LAST_STABLE"
### Step 4: Analyze Dependency Updates
1. **Use pnpm's built-in dependency analysis:**
```bash
# Get outdated dependencies with pnpm
pnpm outdated --format table > outdated-deps-${NEW_VERSION}.txt
# Check for license compliance
pnpm licenses ls --json > licenses-${NEW_VERSION}.json
# Analyze why specific dependencies exist
echo "Dependency analysis:" > dep-analysis-${NEW_VERSION}.md
MAJOR_DEPS=("vue" "vite" "@vitejs/plugin-vue" "typescript" "pinia")
for dep in "${MAJOR_DEPS[@]}"; do
echo -e "\n## $dep\n\`\`\`" >> dep-analysis-${NEW_VERSION}.md
pnpm why "$dep" >> dep-analysis-${NEW_VERSION}.md || echo "Not found" >> dep-analysis-${NEW_VERSION}.md
echo "\`\`\`" >> dep-analysis-${NEW_VERSION}.md
done
```
2. **Check for significant dependency updates:**
1. **Check significant dependency updates:**
```bash
# Extract all dependency changes for major version bumps
OTHER_DEP_CHANGES=""
@@ -218,48 +200,22 @@ echo "Last stable release: $LAST_STABLE"
PR data: [contents of prs-${NEW_VERSION}.json]
```
3. **Generate GTM notification using this EXACT Slack-compatible format:**
3. **Generate GTM notification:**
```bash
# Only create file if GTM-worthy features exist:
if [ "$GTM_FEATURES_FOUND" = "true" ]; then
cat > gtm-summary-${NEW_VERSION}.md << 'EOF'
*GTM Summary: ComfyUI Frontend v${NEW_VERSION}*
_Disclaimer: the below is AI-generated_
1. *[Feature Title]* (#[PR_NUMBER])
* *Author:* @[username]
* *Demo:* [Media Link or "No demo available"]
* *Why users should care:* [One compelling sentence]
* *Key Features:*
* [Feature detail 1]
* [Feature detail 2]
2. *[Feature Title]* (#[PR_NUMBER])
* *Author:* @[username]
* *Demo:* [Media Link]
* *Why users should care:* [One compelling sentence]
* *Key Features:*
* [Feature detail 1]
* [Feature detail 2]
EOF
# Save to gtm-summary-${NEW_VERSION}.md based on analysis
# If GTM-worthy features exist, include them with testing instructions
# If not, note that this is a maintenance/bug fix release
# Check if notification is needed
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
echo "✅ No GTM notification needed for this release"
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
else
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
echo "📤 Share this file in #gtm channel to notify the team"
else
echo "✅ No GTM notification needed for this release"
echo "📄 No gtm-summary file created - no marketing-worthy features"
fi
```
**CRITICAL Formatting Requirements:**
- Use single asterisk (*) for emphasis, NOT double (**)
- Use underscore (_) for italics
- Use 4 spaces for indentation (not tabs)
- Convert author names to @username format (e.g., "John Smith" → "@john")
- No section headers (#), no code language specifications
- Always include "Disclaimer: the below is AI-generated"
- Keep content minimal - no testing instructions, additional sections, etc.
### Step 6: Version Preview
**Version Preview:**
@@ -272,42 +228,37 @@ echo "Last stable release: $LAST_STABLE"
### Step 7: Security and Dependency Audit
1. Run pnpm security audit:
1. Run security audit:
```bash
pnpm audit --audit-level moderate
pnpm licenses ls --summary
npm audit --audit-level moderate
```
2. Check for known vulnerabilities in dependencies
3. Run comprehensive dependency health check:
```bash
pnpm doctor
```
4. Scan for hardcoded secrets or credentials:
3. Scan for hardcoded secrets or credentials:
```bash
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
```
5. Verify no sensitive data in recent commits
6. **SECURITY REVIEW**: Address any critical findings before proceeding?
4. Verify no sensitive data in recent commits
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
### Step 8: Pre-Release Testing
1. Run complete test suite:
```bash
pnpm test:unit
pnpm test:component
npm run test:unit
npm run test:component
```
2. Run type checking:
```bash
pnpm typecheck
npm run typecheck
```
3. Run linting (may have issues with missing packages):
```bash
pnpm lint || echo "Lint issues - verify if critical"
npm run lint || echo "Lint issues - verify if critical"
```
4. Test build process:
```bash
pnpm build
pnpm build:types
npm run build
npm run build:types
```
5. **QUALITY GATE**: All tests and builds passing?
@@ -537,7 +488,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```bash
# Check npm availability
for i in {1..10}; do
if pnpm view @comfyorg/comfyui-frontend-types@${NEW_VERSION} version >/dev/null 2>&1; then
if npm view @comfyorg/comfyui-frontend-types@${NEW_VERSION} version >/dev/null 2>&1; then
echo "✅ npm package available"
break
fi

View File

@@ -80,7 +80,7 @@ For each commit:
- **CONFIRMATION REQUIRED**: Conflicts resolved correctly?
3. After successful cherry-pick:
- Show the changes: `git show HEAD`
- Run validation: `pnpm typecheck && pnpm lint`
- Run validation: `npm run typecheck && npm run lint`
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
### Step 6: Create PR to Core Branch
@@ -197,7 +197,7 @@ For each commit:
5. Track progress:
- GitHub release draft/publication
- PyPI upload
- pnpm types publication
- npm types publication
### Step 12: Post-Release Verification
@@ -211,7 +211,7 @@ For each commit:
```
3. Verify npm package:
```bash
pnpm view @comfyorg/comfyui-frontend-types@1.23.5
npm view @comfyorg/comfyui-frontend-types@1.23.5
```
4. Generate release summary with:
- Version released

View File

@@ -5,7 +5,7 @@ Follow these steps systematically to verify our changes:
1. **Server Setup**
- Check if the dev server is running on port 5173 using browser navigation or port checking
- If not running, start it with `pnpm dev` from the root directory
- If not running, start it with `npm run dev` from the root directory
- If the server fails to start, provide detailed troubleshooting steps by reading package.json and README.md for accurate instructions
- Wait for the server to be fully ready before proceeding

6
.gitattributes vendored
View File

@@ -2,13 +2,9 @@
* text=auto
# Force TS to LF to make the unixy scripts not break on Windows
*.cjs text eol=lf
*.js text eol=lf
*.json text eol=lf
*.mjs text eol=lf
*.mts text eol=lf
*.ts text eol=lf
*.vue text eol=lf
*.js text eol=lf
# Generated files
src/types/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -3,15 +3,14 @@ name: 'Chromatic'
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
on:
workflow_dispatch: # Allow manual triggering
push:
branches: [main]
pull_request:
branches: [main]
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
# Only run for PRs from version-bump-* branches or manual triggers
if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-')
permissions:
pull-requests: write
issues: write
@@ -21,16 +20,11 @@ jobs:
with:
fetch-depth: 0 # Required for Chromatic baseline
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
cache: 'npm'
- name: Get current time
id: current-time
@@ -38,7 +32,6 @@ jobs:
- name: Comment PR - Build Started
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -56,21 +49,8 @@ jobs:
---
*This comment will be updated when the build completes*
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Build Storybook and run Chromatic
id: chromatic
@@ -88,7 +68,6 @@ jobs:
- name: Comment PR - Build Complete
if: github.event_name == 'pull_request' && always()
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}

View File

@@ -53,20 +53,14 @@ jobs:
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies for analysis tools
run: |
pnpm install -g typescript @vue/compiler-sfc
npm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review
uses: anthropics/claude-code-action@main

View File

@@ -16,26 +16,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
dist
tsconfig.tsbuildinfo
key: dev-release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
dev-release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
@@ -46,9 +29,9 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
pnpm build
pnpm zipdist
npm ci
npm run build
npm run zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:

View File

@@ -42,14 +42,9 @@ jobs:
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- uses: actions/setup-python@v4
with:
python-version: '3.10'
@@ -68,8 +63,8 @@ jobs:
working-directory: ComfyUI/custom_nodes/${{ inputs.repository }}
- name: Build & Install ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
npm ci
npm run build
rm -rf ../ComfyUI/web/*
mv dist/* ../ComfyUI/web/
working-directory: ComfyUI_frontend
@@ -84,18 +79,18 @@ jobs:
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Capture base i18n
run: npx tsx scripts/diff-i18n capture
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n
run: npm run collect-i18n
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -13,22 +13,22 @@ jobs:
update-locales:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-node-defs.ts
run: npm run collect-i18n -- scripts/collect-i18n-node-defs.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -1,45 +1,37 @@
name: Update Locales
on:
# Manual dispatch for urgent translation updates
workflow_dispatch:
# Only trigger on PRs to main/master - additional branch filtering in job condition
pull_request:
branches: [ main ]
types: [opened, synchronize, reopened]
branches: [ main, master, dev* ]
paths-ignore:
- '.github/**'
- '.husky/**'
- '.vscode/**'
- 'browser_tests/**'
- 'tests-ui/**'
jobs:
update-locales:
# Branch detection: Only run for manual dispatch or version-bump-* branches from main repo
if: github.event_name == 'workflow_dispatch' || (github.event.pull_request.head.repo.full_name == github.repository && startsWith(github.head_ref, 'version-bump-'))
# Don't run on fork PRs
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
ComfyUI_frontend/.cache
ComfyUI_frontend/.cache
key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
i18n-tools-cache-${{ runner.os }}-
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: pnpm dev:electron &
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: pnpm collect-i18n -- scripts/collect-i18n-general.ts
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: pnpm locale
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend

View File

@@ -19,40 +19,20 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
.eslintcache
tsconfig.tsbuildinfo
.prettierCache
.knip-cache
key: lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js,mts}', '*.config.*', '.eslintrc.*', '.prettierrc.*', 'tsconfig.json') }}
restore-keys: |
lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
lint-format-cache-${{ runner.os }}-
ci-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Run ESLint with auto-fix
run: pnpm lint:fix
run: npm run lint:fix
- name: Run Prettier with auto-format
run: pnpm format
run: npm run format
- name: Check for changes
id: verify-changed-files
@@ -74,13 +54,12 @@ jobs:
- name: Final validation
run: |
pnpm lint
pnpm format:check
pnpm knip
npm run lint
npm run format:check
npm run knip
- name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
@@ -93,7 +72,6 @@ jobs:
- name: Comment on PR about manual fix needed
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
@@ -101,5 +79,5 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\npnpm prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\npnpm lint:fix\npnpm format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\nnpm run prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\nnpm run lint:fix\nnpm run format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
})

View File

@@ -19,25 +19,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
tsconfig.tsbuildinfo
key: release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
@@ -57,9 +41,9 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
pnpm build
pnpm zipdist
npm ci
npm run build
npm run zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
with:
@@ -129,31 +113,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
registry-url: https://registry.npmjs.org
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
tsconfig.tsbuildinfo
dist
key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
types-tools-cache-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
- run: pnpm build:types
- run: npm ci
- run: npm run build:types
- name: Publish package
run: pnpm publish --access public
run: npm publish --access public
working-directory: dist
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend

View File

@@ -37,16 +37,9 @@ jobs:
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
cache-dependency-path: 'ComfyUI_frontend/pnpm-lock.yaml'
- name: Get current time
id: current-time
@@ -54,7 +47,6 @@ jobs:
- name: Comment PR - Tests Started
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -66,28 +58,16 @@ jobs:
---
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>[${{ steps.current-time.outputs.time }} UTC] Preparing browser tests across multiple browsers...</bold>
---
*This comment will be updated when tests complete*
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
ComfyUI_frontend/.cache
ComfyUI_frontend/tsconfig.tsbuildinfo
key: playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ hashFiles('ComfyUI_frontend/src/**/*.{ts,vue,js}', 'ComfyUI_frontend/*.config.*') }}
restore-keys: |
playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
playwright-setup-cache-${{ runner.os }}-
playwright-tools-cache-${{ runner.os }}-
- name: Build ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
npm ci
npm run build
working-directory: ComfyUI_frontend
- name: Generate cache key
@@ -134,15 +114,9 @@ jobs:
ComfyUI_frontend
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- uses: actions/setup-python@v4
with:
python-version: '3.10'
cache: 'pip'
- name: Get current time
id: current-time
@@ -160,7 +134,6 @@ jobs:
- name: Comment PR - Browser Test Started
if: github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -168,7 +141,7 @@ jobs:
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<img alt='comfy-loading-gif' src="https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<img alt='claude-loading-gif' src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
<bold>${{ matrix.browser }}</bold>: Running tests...
- name: Install requirements
@@ -185,21 +158,12 @@ jobs:
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-${{ matrix.browser }}
restore-keys: |
playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}-
playwright-browsers-${{ runner.os }}-
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Install Wrangler
run: pnpm install -g wrangler
run: npm install -g wrangler
- name: Run Playwright tests (${{ matrix.browser }})
id: playwright
@@ -216,7 +180,6 @@ jobs:
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
id: cloudflare-deploy
if: always()
continue-on-error: true
run: |
# Retry logic for wrangler deploy (3 attempts)
RETRY_COUNT=0
@@ -275,7 +238,6 @@ jobs:
- name: Comment PR - Browser Test Complete
if: always() && github.event_name == 'pull_request'
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -361,7 +323,6 @@ jobs:
fi
- name: Comment PR - Tests Complete
continue-on-error: true
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}

View File

@@ -14,33 +14,19 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: electron-types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
electron-types-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Update electron types
run: pnpm install @comfyorg/comfyui-electron-types@latest
run: npm install @comfyorg/comfyui-electron-types@latest
- name: Get new version
id: get-version
run: |
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./pnpm-lock.yaml')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)")
NEW_VERSION=$(node -e "console.log(JSON.parse(require('fs').readFileSync('./package-lock.json')).packages['node_modules/@comfyorg/comfyui-electron-types'].version)")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request

View File

@@ -19,36 +19,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-manager-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-manager-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache ComfyUI-Manager repository
uses: actions/cache@v4
with:
path: ComfyUI-Manager
key: comfyui-manager-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfyui-manager-repo-${{ runner.os }}-
run: npm ci
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v4
@@ -86,7 +64,7 @@ jobs:
- name: Lint generated types
run: |
echo "Linting generated ComfyUI-Manager API types..."
pnpm lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
npm run lint:fix:no-cache -- ./src/types/generatedManagerTypes.ts
- name: Check for changes
id: check-changes

View File

@@ -18,36 +18,14 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-registry-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-registry-tools-cache-${{ runner.os }}-
cache: 'npm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache comfy-api repository
uses: actions/cache@v4
with:
path: comfy-api
key: comfy-api-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfy-api-repo-${{ runner.os }}-
run: npm ci
- name: Checkout comfy-api repository
uses: actions/checkout@v4
@@ -86,7 +64,7 @@ jobs:
- name: Lint generated types
run: |
echo "Linting generated Comfy Registry API types..."
pnpm lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
npm run lint:fix:no-cache -- ./src/types/comfyRegistryTypes.ts
- name: Check for changes
id: check-changes

View File

@@ -26,21 +26,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
cache: 'npm'
- name: Bump version
id: bump-version
run: |
pnpm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
npm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT

View File

@@ -13,34 +13,15 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
coverage
.vitest-cache
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
restore-keys: |
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
vitest-cache-${{ runner.os }}-
test-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Run Vitest tests
run: |
pnpm test:component
pnpm test:unit
npm run test:component
npm run test:unit

2
.gitignore vendored
View File

@@ -10,6 +10,7 @@ lerna-debug.log*
# Package manager lockfiles (allow users to use different package managers)
bun.lock
bun.lockb
pnpm-lock.yaml
yarn.lock
# Cache files
@@ -22,6 +23,7 @@ dist-ssr
*.local
# Claude configuration
.claude/*.local.json
.claude/settings.json
# Editor directories and files
.vscode/*

View File

@@ -2,9 +2,9 @@
## Quick Commands
- `pnpm storybook`: Start Storybook development server
- `pnpm build-storybook`: Build static Storybook
- `pnpm test:component`: Run component tests (includes Storybook components)
- `npm run storybook`: Start Storybook development server
- `npm run build-storybook`: Build static Storybook
- `npm run test:component`: Run component tests (includes Storybook components)
## Development Workflow for Storybook
@@ -19,8 +19,8 @@
- Ensure proper theming and styling
3. **Code Quality**:
- Run `pnpm typecheck` to verify TypeScript
- Run `pnpm lint` to check for linting issues
- Run `npm run typecheck` to verify TypeScript
- Run `npm run lint` to check for linting issues
- Follow existing story patterns and conventions
## Story Creation Guidelines
@@ -138,13 +138,13 @@ The Storybook preview is configured with:
```bash
# Check TypeScript issues
pnpm typecheck
npm run typecheck
# Lint Storybook files
pnpm lint .storybook/
npm run lint .storybook/
# Build to check for production issues
pnpm build-storybook
npm run build-storybook
```
## File Organization

View File

@@ -40,10 +40,10 @@ Storybook is a frontend workshop for building UI components and pages in isolati
```bash
# Start Storybook development server
pnpm storybook
npm run storybook
# Build static Storybook for deployment
pnpm build-storybook
npm run build-storybook
```
### Creating Stories

View File

@@ -8,19 +8,19 @@
- Config: `vite.config.mts`, `vitest.config.ts`, `playwright.config.ts`, `eslint.config.js`, `.prettierrc`.
## Build, Test, and Development Commands
- `pnpm dev`: Start Vite dev server.
- `pnpm dev:electron`: Dev server with Electron API mocks.
- `pnpm build`: Type-check then production build to `dist/`.
- `pnpm preview`: Preview the production build locally.
- `pnpm test:unit`: Run Vitest unit tests (`tests-ui/`).
- `pnpm test:component`: Run component tests (`src/components/`).
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`).
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint). `pnpm format` / `format:check`: Prettier.
- `pnpm typecheck`: Vue TSC type checking.
- `npm run dev`: Start Vite dev server.
- `npm run dev:electron`: Dev server with Electron API mocks.
- `npm run build`: Type-check then production build to `dist/`.
- `npm run preview`: Preview the production build locally.
- `npm run test:unit`: Run Vitest unit tests (`tests-ui/`).
- `npm run test:component`: Run component tests (`src/components/`).
- `npm run test:browser`: Run Playwright E2E tests (`browser_tests/`).
- `npm run lint` / `npm run lint:fix`: Lint (ESLint). `npm run format` / `format:check`: Prettier.
- `npm run typecheck`: Vue TSC type checking.
## Coding Style & Naming Conventions
- Language: TypeScript, Vue SFCs (`.vue`). Indent 2 spaces; single quotes; no semicolons; width 80 (see `.prettierrc`).
- Imports: sorted/grouped by plugin; run `pnpm format` before committing.
- Imports: sorted/grouped by plugin; run `npm run format` before committing.
- ESLint: Vue + TS rules; no floating promises; unused imports disallowed; i18n raw text restrictions in templates.
- Naming: Vue components in PascalCase (e.g., `MenuHamburger.vue`); composables `useXyz.ts`; Pinia stores `*Store.ts`.
@@ -33,7 +33,7 @@
## Commit & Pull Request Guidelines
- Commits: Prefer Conventional Commits (e.g., `feat(ui): add sidebar`), `refactor(litegraph): …`. Use `[skip ci]` for locale-only updates when appropriate.
- PRs: Include clear description, linked issues (`Fixes #123`), and screenshots/GIFs for UI changes. Add/adjust tests and i18n strings when applicable.
- Quality gates: `pnpm lint`, `pnpm typecheck`, and relevant tests must pass. Keep PRs focused and small.
- Quality gates: `npm run lint`, `npm run typecheck`, and relevant tests must pass. Keep PRs focused and small.
## Security & Configuration Tips
- Secrets: Use `.env` (see `.env_example`); do not commit secrets.

View File

@@ -2,13 +2,13 @@
## Quick Commands
- `pnpm`: See all available commands
- `pnpm typecheck`: Type checking
- `pnpm lint`: Linting
- `pnpm format`: Prettier formatting
- `pnpm test:component`: Run component tests with browser environment
- `pnpm test:unit`: Run all unit tests
- `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file
- `npm run`: See all available commands
- `npm run typecheck`: Type checking
- `npm run lint`: Linting
- `npm run format`: Prettier formatting
- `npm run test:component`: Run component tests with browser environment
- `npm run test:unit`: Run all unit tests
- `npm run test:unit -- tests-ui/tests/example.test.ts`: Run single test file
## Development Workflow

View File

@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- Node.js (v16 or later; v24 strongly recommended) and pnpm
- Node.js (v16 or later; v20/v22 strongly recommended) and npm
- Git for version control
- A running ComfyUI backend instance
@@ -39,7 +39,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
2. Install dependencies:
```bash
pnpm install
npm install
```
3. Configure environment (optional):
@@ -57,13 +57,13 @@ python main.py --port 8188
### Git pre-commit hooks
Run `pnpm prepare` to install Git pre-commit hooks. Currently, the pre-commit hook is used to auto-format code on commit.
Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit hook is used to auto-format code on commit.
### Dev Server
- Start local ComfyUI backend at `localhost:8188`
- Run `pnpm dev` to start the dev server
- Run `pnpm dev:electron` to start the dev server with electron API mocked
- Run `npm run dev` to start the dev server
- Run `npm run dev:electron` to start the dev server with electron API mocked
#### Access dev server on touch devices
@@ -155,7 +155,7 @@ For ComfyUI_frontend development, you can ask coding assistants to use Playwrigh
##### Setup for Claude Code
After installing dependencies with `pnpm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
After installing dependencies with `npm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
@@ -210,14 +210,14 @@ Here's how Claude Code can use the Playwright MCP server to inspect the interfac
### Unit Tests
- `pnpm i` to install all dependencies
- `pnpm test:unit` to execute all unit tests
- `npm i` to install all dependencies
- `npm run test:unit` to execute all unit tests
### Component Tests
Component tests verify Vue components in `src/components/`.
- `pnpm test:component` to execute all component tests
- `npm run test:component` to execute all component tests
### Playwright Tests
@@ -228,12 +228,12 @@ Playwright tests verify the whole app. See [browser_tests/README.md](browser_tes
Before submitting a PR, ensure all tests pass:
```bash
pnpm test:unit
pnpm test:component
pnpm test:browser
pnpm typecheck
pnpm lint
pnpm format
npm run test:unit
npm run test:component
npm run test:browser
npm run typecheck
npm run lint
npm run format
```
## Code Style Guidelines
@@ -265,7 +265,7 @@ The project supports three types of icons, all with automatic imports (no manual
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i-lucide:settings />`, `<i-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 `src/assets/icons/custom/` and processed by `build/customIconCollection.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 `src/assets/icons/custom/`.
For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md).

View File

@@ -124,7 +124,6 @@ export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
public readonly canvas: Locator
public readonly selectionToolbox: Locator
public readonly widgetTextBox: Locator
// Buttons
@@ -159,7 +158,6 @@ export class ComfyPage {
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
this.selectionToolbox = page.locator('.selection-toolbox')
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })

View File

@@ -65,7 +65,6 @@ export class Topbar {
}
async openTopbarMenu() {
await this.page.waitForTimeout(1000)
await this.page.locator('.comfyui-logo-wrapper').click()
const menu = this.page.locator('.comfy-command-menu')
await menu.waitFor({ state: 'visible' })

View File

@@ -1,57 +0,0 @@
import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function verifyCustomIconSvg(iconElement: Locator) {
const svgVariable = await iconElement.evaluate((element) => {
const styles = getComputedStyle(element)
return styles.getPropertyValue('--svg')
})
expect(svgVariable).toBeTruthy()
const dataUrlMatch = svgVariable.match(
/url\("data:image\/svg\+xml,([^"]+)"\)/
)
expect(dataUrlMatch).toBeTruthy()
const encodedSvg = dataUrlMatch![1]
const decodedSvg = decodeURIComponent(encodedSvg)
// Check for SVG header to confirm it's a valid SVG
expect(decodedSvg).toContain("<svg xmlns='http://www.w3.org/2000/svg'")
}
test.describe('Custom Icons', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => {
// Find the icon in the sidebar
const icon = comfyPage.page.locator(
'.icon-\\[comfy--ai-model\\].side-bar-button-icon'
)
await expect(icon).toBeVisible()
// Verify the custom SVG content
await verifyCustomIconSvg(icon)
})
test('Browse Templates menu item uses custom template icon', async ({
comfyPage
}) => {
// Open the topbar menu
await comfyPage.menu.topbar.openTopbarMenu()
const menuItem = comfyPage.menu.topbar.getMenuItem('Browse Templates')
// Find the icon as a previous sibling of the menu item label
const templateIcon = menuItem
.locator('..')
.locator('.icon-\\[comfy--template\\]')
await expect(templateIcon).toBeVisible()
// Verify the custom SVG content
await verifyCustomIconSvg(templateIcon)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -7,11 +7,13 @@ test.describe('Graph Canvas Menu', () => {
// Set link render mode to spline to make sure it's not affected by other tests'
// side effects.
await comfyPage.setSetting('Comfy.LinkRenderMode', 2)
// Enable canvas menu for all tests
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
test('Can toggle link visibility', async ({ comfyPage }) => {
// Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup.
// so no cleanup is needed.
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
@@ -34,45 +36,4 @@ test.describe('Graph Canvas Menu', () => {
hiddenLinkRenderMode
)
})
test('Focus mode button is clickable and has correct test id', async ({
comfyPage
}) => {
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
await expect(focusButton).toBeVisible()
await expect(focusButton).toBeEnabled()
// Test that the button can be clicked without error
await focusButton.click()
await comfyPage.nextFrame()
})
test('Zoom controls popup opens and closes', async ({ comfyPage }) => {
// Find the zoom button by its percentage text content
const zoomButton = comfyPage.page.locator('button').filter({
hasText: '%'
})
await expect(zoomButton).toBeVisible()
// Click to open zoom controls
await zoomButton.click()
await comfyPage.nextFrame()
// Zoom controls modal should be visible
const zoomModal = comfyPage.page
.locator('div')
.filter({
hasText: 'Zoom To Fit'
})
.first()
await expect(zoomModal).toBeVisible()
// Click backdrop to close
const backdrop = comfyPage.page.locator('.fixed.inset-0').first()
await backdrop.click()
await comfyPage.nextFrame()
// Modal should be hidden
await expect(zoomModal).not.toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -780,18 +780,9 @@ test.describe('Viewport settings', () => {
// Screenshot the canvas element
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await toggleButton.click()
// close zoom menu
await zoomControlsButton.click()
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -35,44 +35,34 @@ test.describe('Minimap', () => {
})
test('Validate minimap toggle button state', async ({ comfyPage }) => {
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(toggleButton).toBeVisible()
await expect(toggleButton).toHaveClass(/minimap-active/)
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
await expect(minimapContainer).toBeVisible()
})
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapContainer).toBeVisible()
await expect(toggleButton).toHaveClass(/minimap-active/)
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).not.toBeVisible()
await expect(toggleButton).toContainText('Show Minimap')
await expect(toggleButton).not.toHaveClass(/minimap-active/)
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).toBeVisible()
await expect(toggleButton).toContainText('Hide Minimap')
await expect(toggleButton).toHaveClass(/minimap-active/)
})
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {

View File

@@ -41,12 +41,15 @@ test.describe('Node Help', () => {
// Select the node with panning to ensure toolbox is visible
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Wait for selection toolbox to appear
await expect(comfyPage.selectionToolbox).toBeVisible()
// Wait for selection overlay container and toolbox to appear
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible()
// Click the help button in the selection toolbox
const helpButton = comfyPage.selectionToolbox.locator(
'button:has(.pi-question-circle)'
const helpButton = comfyPage.page.locator(
'.selection-toolbox button:has(.pi-question-circle)'
)
await expect(helpButton).toBeVisible()
await helpButton.click()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -14,17 +14,20 @@ test.describe('Selection Toolbox', () => {
test('shows selection toolbox', async ({ comfyPage }) => {
// By default, selection toolbox should be enabled
await expect(comfyPage.selectionToolbox).not.toBeVisible()
expect(
await comfyPage.page.locator('.selection-overlay-container').isVisible()
).toBe(false)
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection toolbox should be visible with multiple nodes selected
await expect(comfyPage.selectionToolbox).toBeVisible()
// Border is now drawn on canvas, check via screenshot
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-multiple-nodes-border.png'
)
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).toBeVisible()
})
test('shows at correct position when node is pasted', async ({
@@ -36,16 +39,18 @@ test.describe('Selection Toolbox', () => {
await comfyPage.page.mouse.move(100, 100)
await comfyPage.ctrlV()
const toolboxContainer = comfyPage.selectionToolbox
await expect(toolboxContainer).toBeVisible()
const overlayContainer = comfyPage.page.locator(
'.selection-overlay-container'
)
await expect(overlayContainer).toBeVisible()
// Verify toolbox is positioned (canvas-based positioning has different coordinates)
const boundingBox = await toolboxContainer.boundingBox()
// Verify the absolute position
const boundingBox = await overlayContainer.boundingBox()
expect(boundingBox).not.toBeNull()
// Canvas-based positioning can vary, just verify toolbox appears in reasonable bounds
expect(boundingBox!.x).toBeGreaterThan(-200) // Not too far off-screen left
expect(boundingBox!.x).toBeLessThan(1000) // Not too far off-screen right
expect(boundingBox!.y).toBeGreaterThan(-100) // Not too far off-screen top
// 10px offset for the pasted node
expect(Math.round(boundingBox!.x)).toBeCloseTo(90, -1) // Allow ~10px tolerance
// 30px offset of node title height
expect(Math.round(boundingBox!.y)).toBeCloseTo(60, -1)
})
test('hide when select and drag happen at the same time', async ({
@@ -60,35 +65,38 @@ test.describe('Selection Toolbox', () => {
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200)
await comfyPage.nextFrame()
await expect(comfyPage.selectionToolbox).not.toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container')
).not.toBeVisible()
})
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await comfyPage.selectNodes(['KSampler'])
// Selection toolbox should be visible but without border
await expect(comfyPage.selectionToolbox).toBeVisible()
// Border is now drawn on canvas, check via screenshot
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-single-node-no-border.png'
)
// Selection overlay should be visible but without border
await expect(
comfyPage.page.locator('.selection-overlay-container')
).toBeVisible()
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).not.toBeVisible()
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
// Selection border should show with multiple selections (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-multiple-selections-border.png'
)
// Selection overlay should show border with multiple selections
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).toBeVisible()
// Deselect to single node
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
// Border should be hidden again (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot(
'selection-toolbox-single-selection-no-border.png'
)
// Border should be hidden again
await expect(
comfyPage.page.locator('.selection-overlay-container.show-border')
).not.toBeVisible()
})
test('displays bypass button in toolbox when nodes are selected', async ({

View File

@@ -193,7 +193,6 @@ test.describe('Workflows sidebar', () => {
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.confirmDialog.click('overwrite')
await comfyPage.page.waitForTimeout(200)
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
])

View File

@@ -256,7 +256,6 @@ test.describe('Animated image widget', () => {
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await comfyPage.page.waitForTimeout(200)
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 170 KiB

View File

@@ -1,100 +0,0 @@
import { existsSync, readFileSync, readdirSync } from 'fs'
import { join } from 'path'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const fileName = fileURLToPath(import.meta.url)
const dirName = dirname(fileName)
const customIconsPath = join(dirName, '..', 'src', 'assets', 'icons', 'custom')
// Iconify collection structure
interface IconifyIcon {
body: string
width?: number
height?: number
}
interface IconifyCollection {
prefix: string
icons: Record<string, IconifyIcon>
width?: number
height?: number
}
// Create an Iconify collection for custom icons
export const iconCollection: IconifyCollection = {
prefix: 'comfy',
icons: {},
width: 16,
height: 16
}
/**
* Validates that an SVG file contains valid SVG content
*/
function validateSvgContent(content: string, filename: string): void {
if (!content.trim()) {
throw new Error(`Empty SVG file: ${filename}`)
}
if (!content.includes('<svg')) {
throw new Error(`Invalid SVG file (missing <svg> tag): ${filename}`)
}
// Basic XML structure validation
const openTags = (content.match(/<svg[^>]*>/g) || []).length
const closeTags = (content.match(/<\/svg>/g) || []).length
if (openTags !== closeTags) {
throw new Error(`Malformed SVG file (mismatched svg tags): ${filename}`)
}
}
/**
* Loads custom SVG icons from the icons directory
*/
function loadCustomIcons(): void {
if (!existsSync(customIconsPath)) {
console.warn(`Custom icons directory not found: ${customIconsPath}`)
return
}
try {
const files = readdirSync(customIconsPath)
const svgFiles = files.filter((file) => file.endsWith('.svg'))
if (svgFiles.length === 0) {
console.warn('No SVG files found in custom icons directory')
return
}
svgFiles.forEach((file) => {
const name = file.replace('.svg', '')
const filePath = join(customIconsPath, file)
try {
const content = readFileSync(filePath, 'utf-8')
validateSvgContent(content, file)
iconCollection.icons[name] = {
body: content
}
} catch (error) {
console.error(
`Failed to load custom icon ${file}:`,
error instanceof Error ? error.message : error
)
// Continue loading other icons instead of failing the entire build
}
})
} catch (error) {
console.error(
'Failed to read custom icons directory:',
error instanceof Error ? error.message : error
)
// Don't throw here - allow build to continue without custom icons
}
}
// Load icons when this module is imported
loadCustomIcons()

View File

@@ -1,50 +0,0 @@
# 2. Restructure ComfyUI_frontend as a monorepo
Date: 2025-08-25
## Status
Proposed
<!-- [Proposed | Accepted | Rejected | Deprecated | Superseded by [ADR-NNNN](NNNN-title.md)] -->
## Context
[Most of the context is in here](https://github.com/Comfy-Org/ComfyUI_frontend/issues/4661)
TL;DR: As we're merging more subprojects like litegraph, devtools, and soon a fork of PrimeVue,
a monorepo structure will help a lot with code sharing and organization.
For more information on Monorepos, check out [monorepo.tools](https://monorepo.tools/)
## Decision
- Swap out NPM for PNPM
- Add a workspace for the PrimeVue fork
- Move the frontend code into its own app workspace
- Longer term: Extract and reorganize common infrastructure to take advantage of the new monorepo tooling
### Tools proposed
[PNPM](https://pnpm.io/) and [PNPM workspaces](https://pnpm.io/workspaces)
For monorepo management, I'd probably go with [Nx](https://nx.dev/), but I could be conviced otherwise.
There's a [whole list here](https://monorepo.tools/#tools-review) if you're interested.
## Consequences
### Positive
- Adding new projects with shared dependencies becomes really easy
- Makes the process of forking and customizing projects more structured, if not strictly easier
- It *could* speed up the build and development process (not guaranteed)
- It would let us cleanly organize and release packages like `comfyui-frontend-types`
### Negative
- Monorepos take some getting used to
- Reviews and code contribution management has to account for the different projects' situations and constraints
<!-- ## Notes
Optional section for additional information, references, or clarifications. -->

View File

@@ -11,7 +11,6 @@ An Architecture Decision Record captures an important architectural decision mad
| ADR | Title | Status | Date |
|-----|-------|--------|------|
| [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 |
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Proposed | 2025-08-25 |
## Creating a New ADR

View File

@@ -105,7 +105,7 @@ The alternative would have been breaking all existing extensions or staying with
Build the frontend for full functionality:
```bash
pnpm build
npm run build
```
For faster iteration during development, use watch mode:

19437
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.26.7",
"version": "1.26.6",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -10,7 +10,7 @@
"scripts": {
"dev": "vite",
"dev:electron": "vite --config vite.electron.config.mts",
"build": "pnpm typecheck && vite build",
"build": "npm run typecheck && vite build",
"build:types": "vite build --config vite.types.config.mts && node scripts/prepare-types.js",
"zipdist": "node scripts/zipdist.js",
"typecheck": "vue-tsc --noEmit",
@@ -21,7 +21,6 @@
"test:browser": "npx playwright test",
"test:unit": "vitest run tests-ui/tests",
"test:component": "vitest run src/components/",
"preinstall": "npx only-allow pnpm",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "vite preview",
"lint": "eslint src --cache",
@@ -40,13 +39,11 @@
"@eslint/js": "^9.8.0",
"@executeautomation/playwright-mcp-server": "^1.0.5",
"@iconify/json": "^2.2.245",
"@iconify/tailwind": "^1.2.0",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",
"@storybook/addon-docs": "^9.1.1",
"@storybook/vue3": "^9.1.1",
"@storybook/vue3-vite": "^9.1.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
@@ -58,7 +55,6 @@
"@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.19",
"chalk": "^5.3.0",
"commander": "^14.0.0",
"eslint": "^9.12.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
@@ -70,22 +66,17 @@
"happy-dom": "^15.11.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0",
"ink": "^4.2.0",
"knip": "^5.62.0",
"lint-staged": "^15.2.7",
"lucide-vue-next": "^0.540.0",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"react": "^18.3.1",
"react-reconciler": "^0.29.2",
"storybook": "^9.1.1",
"tailwindcss": "^3.4.17",
"tailwindcss": "^3.4.4",
"tsx": "^4.15.6",
"typescript": "^5.4.5",
"typescript-eslint": "^8.0.0",
"unplugin-icons": "^0.22.0",
"unplugin-vue-components": "^0.28.0",
"uuid": "^11.1.0",
"vite": "^5.4.19",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-html": "^3.2.2",
@@ -93,20 +84,15 @@
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
"zod-to-json-schema": "^3.24.1",
"lucide-vue-next": "^0.540.0"
},
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
"@comfyorg/comfyui-electron-types": "^0.4.43",
"@primeuix/forms": "0.0.2",
"@primeuix/styled": "0.3.2",
"@primeuix/utils": "^0.3.2",
"@primevue/core": "^4.2.5",
"@primevue/forms": "^4.2.5",
"@primevue/icons": "4.2.5",
"@primevue/themes": "^4.2.5",
"@sentry/core": "^10.5.0",
"@sentry/vue": "^8.48.0",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
@@ -126,10 +112,8 @@
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"fast-glob": "^3.3.3",
"firebase": "^11.6.0",
"fuse.js": "^7.0.0",
"glob": "^11.0.3",
"jsondiffpatch": "^0.6.0",
"loglevel": "^1.9.2",
"marked": "^15.0.11",

View File

@@ -90,8 +90,8 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'pnpm dev',
// url: 'http://127.0.0.1:5173',
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
})

12519
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
packages:
- apps/**
- packages/**
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs
- vue-demi
onlyBuiltDependencies:
- '@playwright/browser-chromium'
- '@playwright/browser-firefox'
- '@playwright/browser-webkit'
- esbuild
- oxc-resolver

View File

@@ -247,29 +247,9 @@ Icons are automatically imported using `unplugin-icons` - no manual imports need
### Configuration
The icon system has two layers:
1. **Build-time Processing** (`build/customIconCollection.ts`):
- Scans `src/assets/icons/custom/` for SVG files
- Validates SVG content and structure
- Creates Iconify collection for Tailwind CSS
- Provides error handling for malformed files
2. **Vite Runtime** (`vite.config.mts`):
- Enables direct SVG import as Vue components
- Supports dynamic icon loading
The icon system is configured in `vite.config.mts`:
```typescript
// Build script creates Iconify collection
export const iconCollection: IconifyCollection = {
prefix: 'comfy',
icons: {
'workflow': { body: '<svg>...</svg>' },
'node': { body: '<svg>...</svg>' }
}
}
// Vite configuration for component-based usage
Icons({
compiler: 'vue3',
customCollections: {
@@ -291,9 +271,8 @@ Icons are fully typed. If TypeScript doesn't recognize a new custom icon:
### Icon Not Showing
1. **Check filename**: Must be kebab-case without special characters
2. **Restart dev server**: Required after adding new icons
3. **Verify SVG**: Ensure it's valid SVG syntax (build script validates automatically)
3. **Verify SVG**: Ensure it's valid SVG syntax
4. **Check console**: Look for Vue component resolution errors
5. **Build script errors**: Check console during build - malformed SVGs are logged but don't break builds
### Icon Wrong Color
- Replace hardcoded colors with `currentColor`

View File

@@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><g stroke="currentColor" stroke-linecap="round" stroke-width="1.3" clip-path="url(#a)"><path d="m4.998 13.909.557-2.225a1.112 1.112 0 0 0-1.08-1.382H2.32c-.51 0-.955.347-1.079.842L.684 13.37a1.112 1.112 0 0 0 1.079 1.382h2.156c.51 0 .956-.347 1.08-.842ZM6.11 7.234l.557-2.224a1.112 1.112 0 0 0-1.08-1.383H3.433c-.51 0-.956.348-1.08.843l-.556 2.225a1.112 1.112 0 0 0 1.08 1.382h2.156c.51 0 .955-.347 1.079-.843ZM11.673 13.909l.556-2.225a1.112 1.112 0 0 0-1.08-1.382H8.994c-.51 0-.955.347-1.079.842l-.556 2.225a1.112 1.112 0 0 0 1.08 1.382h2.156c.51 0 .955-.347 1.079-.842ZM13.141 5.816l-.784 1.83a.334.334 0 0 1-.614 0l-.785-1.83a.333.333 0 0 0-.175-.176l-1.831-.784a.334.334 0 0 1 0-.614l1.831-.785a.333.333 0 0 0 .175-.175l.785-1.831a.334.334 0 0 1 .614 0l.784 1.831a.334.334 0 0 0 .176.175l1.83.785c.27.116.27.498 0 .614l-1.83.784a.334.334 0 0 0-.176.176Z"/></g><defs><clipPath id="a"><path fill="currentColor" d="M0 0h16v16H0z"/></clipPath></defs></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.91396 12.7428L5.41396 10.7428C5.57175 10.1116 5.09439 9.50024 4.44382 9.50024H2.50538C2.04651 9.50024 1.64652 9.81253 1.53523 10.2577L1.03523 12.2577C0.877446 12.8888 1.3548 13.5002 2.00538 13.5002H3.94382C4.40269 13.5002 4.80267 13.1879 4.91396 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M5.91396 6.74277L6.41396 4.74277C6.57175 4.11163 6.09439 3.50024 5.44382 3.50024H3.50538C3.04651 3.50024 2.64652 3.81253 2.53523 4.2577L2.03523 6.2577C1.87745 6.88885 2.3548 7.50024 3.00538 7.50024H4.94382C5.40269 7.50024 5.80267 7.18794 5.91396 6.74277Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M10.914 12.7428L11.414 10.7428C11.5718 10.1116 11.0944 9.50024 10.4438 9.50024H8.50538C8.04651 9.50024 7.64652 9.81253 7.53523 10.2577L7.03523 12.2577C6.87745 12.8888 7.3548 13.5002 8.00538 13.5002H9.94382C10.4027 13.5002 10.8027 13.1879 10.914 12.7428Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M12.2342 5.46739L11.5287 7.11354C11.4248 7.35597 11.0811 7.35597 10.9772 7.11354L10.2717 5.46739C10.2414 5.39659 10.185 5.34017 10.1141 5.30983L8.468 4.60433C8.22557 4.50044 8.22557 4.15675 8.468 4.05285L10.1141 3.34736C10.185 3.31701 10.2414 3.26059 10.2717 3.18979L10.9772 1.54364C11.0811 1.30121 11.4248 1.30121 11.5287 1.54364L12.2342 3.18979C12.2645 3.26059 12.3209 3.31701 12.3918 3.34736L14.0379 4.05285C14.2803 4.15675 14.2803 4.50044 14.0379 4.60433L12.3918 5.30983C12.3209 5.34017 12.2645 5.39659 12.2342 5.46739Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path stroke="currentColor" stroke-linecap="round" stroke-width="1.3" d="m11.2667 10.45-.0842.3156c-.143.5363-.6286.9094-1.18362.9094H4.6945c-.80486 0-1.39102-.7629-1.18364-1.5406l1.30667-4.90002c.143-.53625.62865-.90937 1.18364-.90937h5.46393c.7243 0 1.2518.68663 1.0652 1.38652l-.0301.11275M15.35 8.00001h-4.9m-6.73748 0H.65002"/></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6667 10L10.598 10.2577C10.4812 10.6954 10.0848 11 9.63172 11H5.30161C4.64458 11 4.16608 10.3772 4.33538 9.74234L5.40204 5.74234C5.51878 5.30458 5.91523 5 6.36828 5H10.8286C11.4199 5 11.8505 5.56051 11.6982 6.13185L11.6736 6.22389M14 8H10M4.5 8H2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -1 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><g stroke="currentColor" stroke-linecap="round" stroke-width="1.3" clip-path="url(#a)"><path d="m14.6685 5.7416.6425-2.57c.2028-.811-.4106-1.5967-1.2466-1.5967H2.5782a1.285 1.285 0 0 0-1.2467.9733l-.6425 2.57c-.2027.8111.4107 1.5968 1.2467 1.5968h11.4861a1.285 1.285 0 0 0 1.2467-.9734Zm0 7.7102.6425-2.5701c.2028-.811-.4106-1.5967-1.2466-1.5967h-5.061a1.285 1.285 0 0 0-1.2467.9734l-.6425 2.5701c-.2028.811.4106 1.5966 1.2466 1.5966h5.061a1.285 1.285 0 0 0 1.2467-.9733Zm-10.2802 0 .6425-2.5701c.2027-.811-.4107-1.5967-1.2467-1.5967H2.5782a1.285 1.285 0 0 0-1.2467.9734L.689 12.8285c-.2027.811.4107 1.5966 1.2467 1.5966h1.206a1.285 1.285 0 0 0 1.2466-.9733Z"/></g><defs><clipPath id="a"><path fill="currentColor" d="M0 0h16v16H0z"/></clipPath></defs></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.1894 6.24254L13.6894 4.24254C13.8471 3.61139 13.3698 3 12.7192 3H3.78077C3.3219 3 2.92192 3.3123 2.81062 3.75746L2.31062 5.75746C2.15284 6.38861 2.63019 7 3.28077 7H12.2192C12.6781 7 13.0781 6.6877 13.1894 6.24254Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M13.1894 12.2425L13.6894 10.2425C13.8471 9.61139 13.3698 9 12.7192 9H8.78077C8.3219 9 7.92192 9.3123 7.81062 9.75746L7.31062 11.7575C7.15284 12.3886 7.6302 13 8.28077 13H12.2192C12.6781 13 13.0781 12.6877 13.1894 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M5.18936 12.2425L5.68936 10.2425C5.84714 9.61139 5.36978 9 4.71921 9H3.78077C3.3219 9 2.92192 9.3123 2.81062 9.75746L2.31062 11.7575C2.15284 12.3886 2.6302 13 3.28077 13H4.21921C4.67808 13 5.07806 12.6877 5.18936 12.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 830 B

After

Width:  |  Height:  |  Size: 970 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 16"><path stroke="currentColor" stroke-linecap="round" stroke-width="1.3" d="M9.18613 3.09999H6.81377M9.18613 12.9H7.55288c-3.08678 0-5.35171-2.99581-4.60305-6.08843l.3054-1.26158M14.7486 2.1721l-.5931 2.45c-.132.54533-.6065.92789-1.1508.92789h-2.2993c-.77173 0-1.33797-.74895-1.1508-1.5221l.5931-2.45c.132-.54533.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.74896 1.1508 1.52211Zm-8.3033 0-.59309 2.45c-.13201.54533-.60646.92789-1.15076.92789H2.4021c-.7717 0-1.33793-.74895-1.15077-1.5221l.59309-2.45c.13201-.54533.60647-.9279 1.15077-.9279h2.29935c.77169 0 1.33792.74896 1.15076 1.52211Zm8.3033 9.8-.5931 2.45c-.132.5453-.6065.9279-1.1508.9279h-2.2993c-.77173 0-1.33797-.749-1.1508-1.5221l.5931-2.45c.132-.5453.6065-.9279 1.1508-.9279h2.2993c.7717 0 1.3379.7489 1.1508 1.5221Z"/></svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.99999 4H6.99999M8.99999 12H7.6231C5.02081 12 3.11138 9.55445 3.74252 7.02986L3.99999 6M13.6894 3.24254L13.1894 5.24254C13.0781 5.6877 12.6781 6 12.2192 6H10.2808C9.63019 6 9.15284 5.38861 9.31062 4.75746L9.81062 2.75746C9.92192 2.3123 10.3219 2 10.7808 2H12.7192C13.3698 2 13.8471 2.61139 13.6894 3.24254ZM6.68936 3.24254L6.18936 5.24254C6.07806 5.6877 5.67808 6 5.21921 6H3.28077C2.63019 6 2.15284 5.38861 2.31062 4.75746L2.81062 2.75746C2.92191 2.3123 3.3219 2 3.78077 2H5.71921C6.36978 2 6.84714 2.61139 6.68936 3.24254ZM13.6894 11.2425L13.1894 13.2425C13.0781 13.6877 12.6781 14 12.2192 14H10.2808C9.63019 14 9.15284 13.3886 9.31062 12.7575L9.81062 10.7575C9.92192 10.3123 10.3219 10 10.7808 10H12.7192C13.3698 10 13.8471 10.6114 13.6894 11.2425Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 857 B

After

Width:  |  Height:  |  Size: 910 B

View File

@@ -41,6 +41,7 @@
>
<template #header>
<CurrentUserMessage v-if="tabValue === 'Comfy'" />
<FirstTimeUIMessage v-if="tabValue === 'Comfy'" />
<ColorPaletteMessage v-if="tabValue === 'Appearance'" />
</template>
<SettingsPanel :setting-groups="sortedGroups(category)" />
@@ -75,6 +76,7 @@ import { flattenTree } from '@/utils/treeUtil'
import ColorPaletteMessage from './setting/ColorPaletteMessage.vue'
import CurrentUserMessage from './setting/CurrentUserMessage.vue'
import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue'
import PanelTemplate from './setting/PanelTemplate.vue'
import SettingsPanel from './setting/SettingsPanel.vue'
@@ -118,7 +120,7 @@ const sortedGroups = (category: SettingTreeNode): ISettingGroup[] => {
}
const handleSearch = (query: string) => {
handleSearchBase(query.trim())
handleSearchBase(query)
activeCategory.value = query ? null : defaultCategory.value
}

View File

@@ -0,0 +1,26 @@
<template>
<Message
v-if="show"
class="first-time-ui-message"
severity="info"
:closable="true"
@close="handleClose"
>
{{ $t('g.firstTimeUIMessage') }}
</Message>
</template>
<script setup lang="ts">
import Message from 'primevue/message'
import { computed } from 'vue'
import { useSettingStore } from '@/stores/settingStore'
const settingStore = useSettingStore()
const show = computed(() => !settingStore.exists('Comfy.UseNewMenu'))
const handleClose = async () => {
// Explicitly write the current value to the store.
const currentValue = settingStore.get('Comfy.UseNewMenu')
await settingStore.set('Comfy.UseNewMenu', currentValue)
}
</script>

View File

@@ -2,11 +2,13 @@
<!-- Load splitter overlay only after comfyApp is ready. -->
<!-- If load immediately, the top-level splitter stateKey won't be correctly
synced with the stateStorage (localStorage). -->
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
<LiteGraphCanvasSplitterOverlay
v-if="comfyAppReady && betaMenuEnabled && !workspaceStore.focusMode"
>
<template #side-bar-panel>
<SideToolbar />
</template>
<template v-if="!workspaceStore.focusMode" #bottom-panel>
<template #bottom-panel>
<BottomPanel />
</template>
<template #graph-canvas-panel>
@@ -32,20 +34,22 @@
/>
<NodeTooltip v-if="tooltipEnabled" />
<NodeSearchboxPopover ref="nodeSearchboxPopoverRef" />
<NodeSearchboxPopover />
<!-- Initialize components after comfyApp is ready. useAbsolutePosition requires
canvasStore.canvas to be initialized. -->
<template v-if="comfyAppReady">
<TitleEditor />
<SelectionToolbox v-if="selectionToolboxEnabled" />
<SelectionOverlay v-if="selectionToolboxEnabled">
<SelectionToolbox />
</SelectionOverlay>
<DomWidgets />
</template>
</template>
<script setup lang="ts">
import { useEventListener, whenever } from '@vueuse/core'
import { computed, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
import { computed, onMounted, ref, watch, watchEffect } from 'vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
@@ -53,6 +57,7 @@ import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import MiniMap from '@/components/graph/MiniMap.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
@@ -86,16 +91,12 @@ import { useSettingStore } from '@/stores/settingStore'
import { useToastStore } from '@/stores/toastStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
const emit = defineEmits<{
ready: []
}>()
const canvasRef = ref<HTMLCanvasElement | null>(null)
const nodeSearchboxPopoverRef = shallowRef<InstanceType<
typeof NodeSearchboxPopover
> | null>(null)
const settingStore = useSettingStore()
const nodeDefStore = useNodeDefStore()
const workspaceStore = useWorkspaceStore()
@@ -319,7 +320,6 @@ onMounted(async () => {
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
workspaceStore.spinner = false
useSearchBoxStore().setPopoverRef(nodeSearchboxPopoverRef.value)
window.app = comfyApp
window.graph = comfyApp.graph

View File

@@ -1,278 +1,125 @@
<template>
<div>
<ZoomControlsModal :visible="isModalVisible" />
<!-- Backdrop -->
<div
v-if="hasActivePopup"
class="fixed inset-0 z-[1200]"
@click="hideModal"
></div>
<ButtonGroup
class="p-buttongroup-vertical p-1 absolute bottom-4 right-2 md:right-4"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
<ButtonGroup
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
@wheel="canvasInteractions.handleWheel"
>
<Button
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
severity="secondary"
icon="pi pi-plus"
:aria-label="$t('graphCanvasMenu.zoomIn')"
@mousedown="repeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
/>
<Button
v-tooltip.left="t('graphCanvasMenu.zoomOut')"
severity="secondary"
icon="pi pi-minus"
:aria-label="$t('graphCanvasMenu.zoomOut')"
@mousedown="repeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
/>
<Button
v-tooltip.left="t('graphCanvasMenu.fitView')"
severity="secondary"
icon="pi pi-expand"
:aria-label="$t('graphCanvasMenu.fitView')"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
/>
<Button
v-tooltip.left="
t(
'graphCanvasMenu.' +
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
) + ' (Space)'
"
severity="secondary"
:aria-label="
t(
'graphCanvasMenu.' +
(canvasStore.canvas?.read_only ? 'panMode' : 'selectMode')
)
"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLock')"
>
<Button
v-tooltip.top="selectTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
severity="secondary"
:aria-label="selectTooltip"
:pressed="isCanvasReadOnly"
icon="i-material-symbols:pan-tool-outline"
:class="selectButtonClass"
@click="() => commandStore.execute('Comfy.Canvas.Unlock')"
>
<template #icon>
<i-lucide:mouse-pointer-2 />
</template>
</Button>
<Button
v-tooltip.top="handTooltip"
severity="secondary"
:aria-label="handTooltip"
:pressed="isCanvasUnlocked"
:class="handButtonClass"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.Lock')"
>
<template #icon>
<i-lucide:hand />
</template>
</Button>
<!-- vertical line with bg E1DED5 -->
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
v-tooltip.top="fitViewTooltip"
severity="secondary"
icon="pi pi-expand"
:aria-label="fitViewTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
class="hover:dark-theme:!bg-[#444444] hover:!bg-[#E7E6E6]"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
<i-lucide:focus />
</template>
</Button>
<Button
ref="zoomButton"
v-tooltip.top="t('zoomControls.label')"
severity="secondary"
:label="t('zoomControls.label')"
:class="zoomButtonClass"
:aria-label="t('zoomControls.label')"
data-testid="zoom-controls-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="toggleModal"
>
<span class="inline-flex text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i-lucide:chevron-down />
</span>
</Button>
<div class="w-px my-1 bg-[#E1DED5] dark-theme:bg-[#2E3037] mx-2" />
<Button
ref="focusButton"
v-tooltip.top="focusModeTooltip"
severity="secondary"
:aria-label="focusModeTooltip"
data-testid="focus-mode-button"
:style="stringifiedMinimapStyles.buttonStyles"
:class="focusButtonClass"
@click="() => commandStore.execute('Workspace.ToggleFocusMode')"
>
<template #icon>
<i-lucide:lightbulb />
</template>
</Button>
<Button
v-tooltip.top="{
value: linkVisibilityTooltip,
pt: {
root: {
style: 'z-index: 2; transform: translateY(-20px);'
}
}
}"
severity="secondary"
:class="linkVisibleClass"
:aria-label="linkVisibilityAriaLabel"
data-testid="toggle-link-visibility-button"
:style="stringifiedMinimapStyles.buttonStyles"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i-lucide:route-off />
</template>
</Button>
</ButtonGroup>
</div>
<template #icon>
<i-material-symbols:pan-tool-outline
v-if="canvasStore.canvas?.read_only"
/>
<i-simple-line-icons:cursor v-else />
</template>
</Button>
<Button
v-tooltip.left="t('graphCanvasMenu.toggleLinkVisibility')"
severity="secondary"
:icon="linkHidden ? 'pi pi-eye-slash' : 'pi pi-eye'"
:aria-label="$t('graphCanvasMenu.toggleLinkVisibility')"
data-testid="toggle-link-visibility-button"
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
/>
<Button
v-tooltip.left="minimapTooltip"
severity="secondary"
:icon="'pi pi-map'"
:aria-label="$t('graphCanvasMenu.toggleMinimap')"
:class="{ 'minimap-active': minimapVisible }"
data-testid="toggle-minimap-button"
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
/>
</ButtonGroup>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ButtonGroup from 'primevue/buttongroup'
import { computed, onBeforeUnmount, onMounted } from 'vue'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useZoomControls } from '@/composables/useZoomControls'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import ZoomControlsModal from './modals/ZoomControlsModal.vue'
const { t } = useI18n()
const commandStore = useCommandStore()
const { formatKeySequence } = useCommandStore()
const canvasStore = useCanvasStore()
const keybindingStore = useKeybindingStore()
const settingStore = useSettingStore()
const canvasInteractions = useCanvasInteractions()
const workspaceStore = useWorkspaceStore()
const minimap = useMinimap()
const { isModalVisible, toggleModal, hideModal, hasActivePopup } =
useZoomControls()
const stringifiedMinimapStyles = computed(() => {
const buttonGroupKeys = ['backgroundColor', 'borderRadius', '']
const buttonKeys = ['backgroundColor', 'borderRadius']
const additionalButtonStyles = {
border: 'none',
width: '35px',
height: '35px',
'margin-right': '2px',
'margin-left': '2px'
}
const containerStyles = minimap.containerStyles.value
const buttonStyles = {
...Object.fromEntries(
Object.entries(containerStyles).filter(([key]) =>
buttonKeys.includes(key)
)
),
...additionalButtonStyles
}
const buttonGroupStyles = Object.entries(containerStyles)
.filter(([key]) => buttonGroupKeys.includes(key))
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
return { buttonStyles, buttonGroupStyles }
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
const minimapTooltip = computed(() => {
const baseText = t('graphCanvasMenu.toggleMinimap')
const keybinding = keybindingStore.getKeybindingByCommandId(
'Comfy.Canvas.ToggleMinimap'
)
return keybinding ? `${baseText} (${keybinding.combo.toString()})` : baseText
})
// Computed properties for reactive states
const isCanvasReadOnly = computed(() => canvasStore.canvas?.read_only ?? false)
const isCanvasUnlocked = computed(() => !isCanvasReadOnly.value)
const linkHidden = computed(
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
)
// Computed properties for command text
const unlockCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Comfy.Canvas.Unlock')
).toUpperCase()
)
const lockCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.Lock')).toUpperCase()
)
const fitViewCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Comfy.Canvas.FitView')
).toUpperCase()
)
const focusCommandText = computed(() =>
formatKeySequence(
commandStore.getCommand('Workspace.ToggleFocusMode')
).toUpperCase()
)
// Computed properties for button classes and states
const selectButtonClass = computed(() =>
isCanvasUnlocked.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: ''
)
const handButtonClass = computed(() =>
isCanvasReadOnly.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: ''
)
const zoomButtonClass = computed(() => [
'!w-16',
isModalVisible.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: '',
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]'
])
const focusButtonClass = computed(() => ({
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]': true,
'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]':
workspaceStore.focusMode
}))
// Computed properties for tooltip and aria-label texts
const selectTooltip = computed(
() => `${t('graphCanvasMenu.select')} (${unlockCommandText.value})`
)
const handTooltip = computed(
() => `${t('graphCanvasMenu.hand')} (${lockCommandText.value})`
)
const fitViewTooltip = computed(
() => `${t('graphCanvasMenu.fitView')} (${fitViewCommandText.value})`
)
const focusModeTooltip = computed(
() => `${t('graphCanvasMenu.focusMode')} (${focusCommandText.value})`
)
const linkVisibilityTooltip = computed(() =>
linkHidden.value
? t('graphCanvasMenu.showLinks')
: t('graphCanvasMenu.hideLinks')
)
const linkVisibilityAriaLabel = computed(() =>
linkHidden.value
? t('graphCanvasMenu.showLinks')
: t('graphCanvasMenu.hideLinks')
)
const linkVisibleClass = computed(() => [
linkHidden.value
? 'dark-theme:[&:not(:active)]:!bg-[#262729] [&:not(:active)]:!bg-[#E7E6E6]'
: '',
'hover:dark-theme:!bg-[#262729] hover:!bg-[#E7E6E6]'
])
onMounted(() => {
canvasStore.initScaleSync()
})
onBeforeUnmount(() => {
canvasStore.cleanupScaleSync()
})
let interval: number | null = null
const repeat = async (command: string) => {
if (interval) return
const cmd = () => commandStore.execute(command)
await cmd()
interval = window.setInterval(cmd, 100)
}
const stopRepeat = () => {
if (interval) {
clearInterval(interval)
interval = null
}
}
</script>
<style scoped>
.p-buttongroup-vertical {
display: flex;
flex-direction: row;
z-index: 1200;
flex-direction: column;
border-radius: var(--p-button-border-radius);
overflow: hidden;
border: 1px solid var(--p-panel-border-color);
@@ -282,4 +129,15 @@ onBeforeUnmount(() => {
margin: 0;
border-radius: 0;
}
.p-button.minimap-active {
background-color: var(--p-button-primary-background);
border-color: var(--p-button-primary-border-color);
color: var(--p-button-primary-color);
}
.p-button.minimap-active:hover {
background-color: var(--p-button-primary-hover-background);
border-color: var(--p-button-primary-hover-border-color);
}
</style>

View File

@@ -2,7 +2,7 @@
<div
v-if="visible && initialized"
ref="minimapRef"
class="minimap-main-container flex absolute bottom-[66px] right-2 md:right-11 z-[1000]"
class="minimap-main-container flex absolute bottom-[20px] right-[90px] z-[1000]"
>
<MiniMapPanel
v-if="showOptionsPanel"
@@ -31,25 +31,6 @@
<i-lucide:settings-2 />
</template>
</Button>
<Button
class="absolute z-10 right-0"
size="small"
text
severity="secondary"
data-testid="close-minmap-button"
@click.stop="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
>
<template #icon>
<i-lucide:x />
</template>
</Button>
<hr
class="absolute top-5 bg-[#E1DED5] dark-theme:bg-[#262729] h-[1px] border-0"
:style="{
width: containerStyles.width
}"
/>
<canvas
ref="canvasRef"
@@ -77,12 +58,9 @@ import Button from 'primevue/button'
import { onMounted, onUnmounted, ref } from 'vue'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import MiniMapPanel from './MiniMapPanel.vue'
const commandStore = useCommandStore()
const minimapRef = ref<HTMLDivElement>()
const {

View File

@@ -0,0 +1,106 @@
<!-- This component is used to bound the selected items on the canvas. -->
<template>
<div
v-show="visible"
class="selection-overlay-container pointer-events-none z-40"
:class="{
'show-border': showBorder
}"
:style="style"
>
<slot />
</div>
</template>
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { provide, readonly, ref, watch } from 'vue'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'
import { createBounds } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/stores/graphStore'
import { SelectionOverlayInjectionKey } from '@/types/selectionOverlayTypes'
const canvasStore = useCanvasStore()
const { style, updatePosition } = useAbsolutePosition()
const { getSelectableItems } = useSelectedLiteGraphItems()
const visible = ref(false)
const showBorder = ref(false)
// Increment counter to notify child components of position/visibility change
// This does not include viewport changes.
const overlayUpdateCount = ref(0)
provide(SelectionOverlayInjectionKey, {
visible: readonly(visible),
updateCount: readonly(overlayUpdateCount)
})
const positionSelectionOverlay = () => {
const selectableItems = getSelectableItems()
showBorder.value = selectableItems.size > 1
if (!selectableItems.size) {
visible.value = false
return
}
visible.value = true
const bounds = createBounds(selectableItems)
if (bounds) {
updatePosition({
pos: [bounds[0], bounds[1]],
size: [bounds[2], bounds[3]]
})
}
}
whenever(
() => canvasStore.getCanvas().state.selectionChanged,
() => {
requestAnimationFrame(() => {
positionSelectionOverlay()
overlayUpdateCount.value++
canvasStore.getCanvas().state.selectionChanged = false
})
},
{ immediate: true }
)
canvasStore.getCanvas().ds.onChanged = positionSelectionOverlay
watch(
() => canvasStore.canvas?.state?.draggingItems,
(draggingItems) => {
// Litegraph draggingItems state can end early before the bounding boxes of
// the selected items are updated. Delay to make sure we put the overlay in
// the correct position.
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/2656
if (draggingItems === false) {
requestAnimationFrame(() => {
visible.value = true
positionSelectionOverlay()
overlayUpdateCount.value++
})
} else {
// Selection change update to visible state is delayed by a frame. Here
// we also delay a frame so that the order of events is correct when
// the initial selection and dragging happens at the same time.
requestAnimationFrame(() => {
visible.value = false
overlayUpdateCount.value++
})
}
}
)
</script>
<style scoped>
.selection-overlay-container > * {
pointer-events: auto;
}
.show-border {
@apply border-dashed rounded-md border-2 border-[var(--border-color)];
}
</style>

View File

@@ -1,42 +1,34 @@
<template>
<div
ref="toolboxRef"
style="transform: translate(var(--tb-x), var(--tb-y))"
class="fixed left-0 top-0 z-40"
<Panel
class="selection-toolbox absolute left-1/2 rounded-lg"
:class="{ 'animate-slide-up': shouldAnimate }"
:pt="{
header: 'hidden',
content: 'p-0 flex flex-row'
}"
@wheel="canvasInteractions.handleWheel"
>
<Transition name="slide-up">
<Panel
v-if="visible"
class="rounded-lg selection-toolbox"
:pt="{
header: 'hidden',
content: 'p-0 flex flex-row'
}"
@wheel="canvasInteractions.handleWheel"
>
<ExecuteButton />
<ColorPickerButton />
<BypassButton />
<PinButton />
<Load3DViewerButton />
<MaskEditorButton />
<ConvertToSubgraphButton />
<DeleteButton />
<RefreshSelectionButton />
<ExtensionCommandButton
v-for="command in extensionToolboxCommands"
:key="command.id"
:command="command"
/>
<HelpButton />
</Panel>
</Transition>
</div>
<ExecuteButton />
<ColorPickerButton />
<BypassButton />
<PinButton />
<Load3DViewerButton />
<MaskEditorButton />
<ConvertToSubgraphButton />
<DeleteButton />
<RefreshSelectionButton />
<ExtensionCommandButton
v-for="command in extensionToolboxCommands"
:key="command.id"
:command="command"
/>
<HelpButton />
</Panel>
</template>
<script setup lang="ts">
import Panel from 'primevue/panel'
import { computed, ref } from 'vue'
import { computed, inject } from 'vue'
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
@@ -49,19 +41,23 @@ import Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewer
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
import { useRetriggerableAnimation } from '@/composables/element/useRetriggerableAnimation'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useExtensionService } from '@/services/extensionService'
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { SelectionOverlayInjectionKey } from '@/types/selectionOverlayTypes'
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const extensionService = useExtensionService()
const canvasInteractions = useCanvasInteractions()
const toolboxRef = ref<HTMLElement | undefined>()
const { visible } = useSelectionToolboxPosition(toolboxRef)
const selectionOverlayState = inject(SelectionOverlayInjectionKey)
const { shouldAnimate } = useRetriggerableAnimation(
selectionOverlayState?.updateCount,
{ animateOnMount: true }
)
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
const commandIds = new Set<string>(
@@ -83,29 +79,21 @@ const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
<style scoped>
.selection-toolbox {
transform: translateX(-50%) translateY(-120%);
will-change: transform, opacity;
}
/* Slide up animation using CSS animation */
@keyframes slideUp {
0% {
from {
transform: translateX(-50%) translateY(-100%);
opacity: 0;
}
50% {
transform: translateX(-50%) translateY(-125%);
opacity: 0.5;
}
100% {
to {
transform: translateX(-50%) translateY(-120%);
opacity: 1;
}
}
.slide-up-enter-active {
animation: slideUp 125ms ease-out;
}
.slide-up-leave-active {
animation: slideUp 25ms ease-out reverse;
.animate-slide-up {
animation: slideUp 0.3s ease-out;
}
</style>

View File

@@ -1,237 +0,0 @@
<template>
<div
v-if="visible"
class="w-[250px] absolute flex justify-center right-2 md:right-11 z-[1300] bottom-[66px] !bg-inherit !border-0"
>
<div
class="bg-white dark-theme:bg-[#2b2b2b] border border-gray-200 dark-theme:border-gray-700 rounded-lg shadow-lg p-4 w-4/5"
:style="filteredMinimapStyles"
@click.stop
>
<div>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@mousedown="startRepeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<template #default>
<span class="text-sm font-medium block">{{
$t('graphCanvasMenu.zoomIn')
}}</span>
<span class="text-sm text-gray-500 block">{{
zoomInCommandText
}}</span>
</template>
</Button>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@mousedown="startRepeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<template #default>
<span class="text-sm font-medium block">{{
$t('graphCanvasMenu.zoomOut')
}}</span>
<span class="text-sm text-gray-500 block">{{
zoomOutCommandText
}}</span>
</template>
</Button>
<Button
severity="secondary"
text
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@click="executeCommand('Comfy.Canvas.FitView')"
>
<template #default>
<span class="text-sm font-medium block">{{
$t('zoomControls.zoomToFit')
}}</span>
<span class="text-sm text-gray-500 block">{{
zoomToFitCommandText
}}</span>
</template>
</Button>
<hr class="border-[#E1DED5] mb-1 dark-theme:border-[#2E3037]" />
<Button
severity="secondary"
text
data-testid="toggle-minimap-button"
:pt="{
root: {
class:
'flex items-center justify-between cursor-pointer p-2 rounded w-full text-left hover:!bg-transparent focus:!bg-transparent active:!bg-transparent'
},
label: {
class: 'flex flex-col items-start w-full'
}
}"
@click="executeCommand('Comfy.Canvas.ToggleMinimap')"
>
<template #default>
<span class="text-sm font-medium block">{{
minimapToggleText
}}</span>
<span class="text-sm text-gray-500 block">{{
showMinimapCommandText
}}</span>
</template>
</Button>
<hr class="border-[#E1DED5] mt-1 dark-theme:border-[#2E3037]" />
<div
ref="zoomInputContainer"
class="flex items-center px-2 bg-[#E7E6E6] focus-within:bg-[#F3F3F3] dark-theme:bg-[#8282821A] rounded p-2 zoomInputContainer"
>
<InputNumber
ref="zoomInput"
:default-value="canvasStore.appScalePercentage"
:min="1"
:max="1000"
:show-buttons="false"
:use-grouping="false"
:unstyled="true"
input-class="flex-1 bg-transparent border-none outline-none text-sm shadow-none my-0 "
fluid
@input="applyZoom"
@keyup.enter="applyZoom"
/>
<span class="text-sm text-gray-500 -ml-4">%</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, InputNumber, InputNumberInputEvent } from 'primevue'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import { useCanvasStore } from '@/stores/graphStore'
import { useSettingStore } from '@/stores/settingStore'
const { t } = useI18n()
const minimap = useMinimap()
const settingStore = useSettingStore()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const { formatKeySequence } = useCommandStore()
interface Props {
visible: boolean
}
const props = defineProps<Props>()
const interval = ref<number | null>(null)
// Computed properties for reactive states
const minimapToggleText = computed(() =>
settingStore.get('Comfy.Minimap.Visible')
? t('zoomControls.hideMinimap')
: t('zoomControls.showMinimap')
)
const applyZoom = (val: InputNumberInputEvent) => {
const inputValue = val.value as number
if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {
return
}
canvasStore.setAppZoomFromPercentage(inputValue)
}
const executeCommand = (command: string) => {
void commandStore.execute(command)
}
const startRepeat = (command: string) => {
if (interval.value) return
const cmd = () => commandStore.execute(command)
void cmd()
interval.value = window.setInterval(cmd, 100)
}
const stopRepeat = () => {
if (interval.value) {
clearInterval(interval.value)
interval.value = null
}
}
const filteredMinimapStyles = computed(() => {
return {
...minimap.containerStyles.value,
height: undefined,
width: undefined
}
})
const zoomInCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomIn'))
)
const zoomOutCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomOut'))
)
const zoomToFitCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))
)
const showMinimapCommandText = computed(() =>
formatKeySequence(commandStore.getCommand('Comfy.Canvas.ToggleMinimap'))
)
const zoomInput = ref<InstanceType<typeof InputNumber> | null>(null)
const zoomInputContainer = ref<HTMLDivElement | null>(null)
watch(
() => props.visible,
async (newVal) => {
if (newVal) {
await nextTick()
const input = zoomInputContainer.value?.querySelector(
'input'
) as HTMLInputElement
input?.focus()
}
}
)
</script>
<style>
.zoomInputContainer:focus-within {
border: 1px solid rgb(204, 204, 204);
}
.dark-theme .zoomInputContainer:focus-within {
border: 1px solid rgb(204, 204, 204);
}
</style>

View File

@@ -136,6 +136,8 @@ const closePopup = async () => {
hide()
}
// Learn more handled by anchor href
// const handleCTA = async () => {
// window.open('https://docs.comfy.org/installation/update_comfyui', '_blank')
// await closePopup()
@@ -159,10 +161,8 @@ defineExpose({
<style scoped>
/* Popup container - positioning handled by parent */
.whats-new-popup-container {
--whats-new-popup-bottom: 1rem;
position: absolute;
bottom: var(--whats-new-popup-bottom);
bottom: 1rem;
z-index: 1000;
pointer-events: auto;
}
@@ -171,8 +171,8 @@ defineExpose({
.help-center-arrow {
position: absolute;
bottom: calc(
var(--sidebar-width) * 2 + var(--sidebar-width) / 2
); /* Position to center of help center icon (2 icons below + half icon height for center) */
var(--sidebar-width, 4rem) + 0.25rem
); /* Position toward center of help center icon */
transform: none;
z-index: 999;
pointer-events: none;
@@ -185,10 +185,7 @@ defineExpose({
.whats-new-popup-container.sidebar-left.small-sidebar .help-center-arrow {
left: -14px; /* Overlap with popup outline */
bottom: calc(
var(--sidebar-width) * 2 + var(--sidebar-icon-size) / 2 -
var(--whats-new-popup-bottom)
); /* Position to center of help center icon (2 icons below + half icon height for center - whats new popup bottom position ) */
bottom: calc(2.5rem + 0.25rem); /* Adjust for small sidebar */
}
/* Sidebar positioning classes applied by parent */

View File

@@ -13,7 +13,7 @@
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { computed } from 'vue'
import { computed, defineModel } from 'vue'
const { placeHolder, hasBorder = false } = defineProps<{
placeHolder?: string

View File

@@ -61,10 +61,9 @@ let listenerController: AbortController | null = null
let disconnectOnReset = false
const settingStore = useSettingStore()
const searchBoxStore = useSearchBoxStore()
const litegraphService = useLitegraphService()
const { visible, newSearchBoxEnabled } = storeToRefs(searchBoxStore)
const { visible } = storeToRefs(useSearchBoxStore())
const dismissable = ref(true)
const getNewNodeLocation = (): Point => {
return triggerEvent
@@ -108,9 +107,12 @@ const addNode = (nodeDef: ComfyNodeDefImpl) => {
window.requestAnimationFrame(closeDialog)
}
const showSearchBox = (e: CanvasPointerEvent | null) => {
const newSearchBoxEnabled = computed(
() => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'
)
const showSearchBox = (e: CanvasPointerEvent) => {
if (newSearchBoxEnabled.value) {
if (e?.pointerType === 'touch') {
if (e.pointerType === 'touch') {
setTimeout(() => {
showNewSearchBox(e)
}, 128)
@@ -126,7 +128,7 @@ const getFirstLink = () =>
canvasStore.getCanvas().linkConnector.renderLinks.at(0)
const nodeDefStore = useNodeDefStore()
const showNewSearchBox = (e: CanvasPointerEvent | null) => {
const showNewSearchBox = (e: CanvasPointerEvent) => {
const firstLink = getFirstLink()
if (firstLink) {
const filter =
@@ -302,7 +304,6 @@ watch(visible, () => {
})
useEventListener(document, 'litegraph:canvas', canvasEventHandler)
defineExpose({ showSearchBox })
</script>
<style>

View File

@@ -76,22 +76,6 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
}
</script>
<style>
/* Global CSS variables for sidebar
* These variables need to be global (not scoped) because they are used by
* teleported components like WhatsNewPopup that render outside the sidebar
* but need to reference sidebar dimensions for proper positioning.
*/
:root {
--sidebar-width: 4rem;
--sidebar-icon-size: 1rem;
}
:root:has(.side-tool-bar-container.small-sidebar) {
--sidebar-width: 2.5rem;
}
</style>
<style scoped>
.side-tool-bar-container {
display: flex;
@@ -104,6 +88,9 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
background-color: var(--comfy-menu-secondary-bg);
color: var(--fg-color);
box-shadow: var(--bar-shadow);
--sidebar-width: 4rem;
--sidebar-icon-size: 1rem;
}
.side-tool-bar-container.small-sidebar {

View File

@@ -109,7 +109,7 @@ const computedTooltip = computed(() => t(tooltip) + tooltipSuffix)
}
.side-bar-button-label {
@apply text-[10px] text-center;
@apply text-[10px] text-center whitespace-nowrap;
line-height: 1;
}

View File

@@ -1,6 +1,11 @@
<template>
<SidebarIcon
:tooltip="tooltipText"
:tooltip="
$t('shortcuts.keyboardShortcuts') +
' (' +
formatKeySequence(command.keybinding!.combo.getKeySequences()) +
')'
"
:selected="isShortcutsPanelVisible"
@click="toggleShortcutsPanel"
>
@@ -12,28 +17,28 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCommandStore } from '@/stores/commandStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import SidebarIcon from './SidebarIcon.vue'
const { t } = useI18n()
const bottomPanelStore = useBottomPanelStore()
const commandStore = useCommandStore()
const command = commandStore.getCommand('Workspace.ToggleBottomPanel.Shortcuts')
const { formatKeySequence } = commandStore
const command = useCommandStore().getCommand(
'Workspace.ToggleBottomPanel.Shortcuts'
)
const isShortcutsPanelVisible = computed(
() => bottomPanelStore.activePanel === 'shortcuts'
)
const tooltipText = computed(
() => `${t('shortcuts.keyboardShortcuts')} (${formatKeySequence(command)})`
)
const toggleShortcutsPanel = () => {
bottomPanelStore.togglePanel('shortcuts')
}
const formatKeySequence = (sequences: string[]): string => {
return sequences
.map((seq) => seq.replace(/Control/g, 'Ctrl').replace(/Shift/g, 'Shift'))
.join(' + ')
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<SidebarIcon
icon="icon-[comfy--template]"
:icon="TemplateIcon"
:tooltip="$t('sideToolbar.templates')"
:label="$t('sideToolbar.labels.templates')"
:is-small="isSmall"
@@ -10,13 +10,18 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, defineAsyncComponent, markRaw } from 'vue'
import { useCommandStore } from '@/stores/commandStore'
import { useSettingStore } from '@/stores/settingStore'
import SidebarIcon from './SidebarIcon.vue'
// Import the custom template icon
const TemplateIcon = markRaw(
defineAsyncComponent(() => import('virtual:icons/comfy/template'))
)
const settingStore = useSettingStore()
const commandStore = useCommandStore()

View File

@@ -94,16 +94,18 @@ describe('HoverDissolveThumbnail', () => {
// Check base image
const baseImageClass = lazyImages[0].props('imageClass')
const baseClassList = Array.isArray(baseImageClass)
? baseImageClass
: baseImageClass.split(' ')
expect(baseClassList).toContain('size-full')
const baseClassString = Array.isArray(baseImageClass)
? baseImageClass.join(' ')
: baseImageClass
expect(baseClassString).toContain('absolute')
expect(baseClassString).toContain('inset-0')
// Check overlay image
const overlayImageClass = lazyImages[1].props('imageClass')
const overlayClassList = Array.isArray(overlayImageClass)
? overlayImageClass
: overlayImageClass.split(' ')
expect(overlayClassList).toContain('size-full')
const overlayClassString = Array.isArray(overlayImageClass)
? overlayImageClass.join(' ')
: overlayImageClass
expect(overlayClassString).toContain('absolute')
expect(overlayClassString).toContain('inset-0')
})
})

View File

@@ -1,20 +1,12 @@
<template>
<BaseThumbnail :is-hovered="isHovered">
<div class="relative w-full h-full">
<div class="absolute inset-0">
<LazyImage
:src="baseImageSrc"
:alt="alt"
:image-class="baseImageClass"
/>
</div>
<div class="absolute inset-0 z-10">
<LazyImage
:src="overlayImageSrc"
:alt="alt"
:image-class="overlayImageClass"
/>
</div>
<LazyImage :src="baseImageSrc" :alt="alt" :image-class="baseImageClass" />
<LazyImage
:src="overlayImageSrc"
:alt="alt"
:image-class="overlayImageClass"
/>
</div>
</BaseThumbnail>
</template>
@@ -40,15 +32,14 @@ const isVideoType =
false
const baseImageClass = computed(() => {
const sizeClasses = isVideoType
? 'size-full object-cover'
: 'size-full object-contain'
return sizeClasses
return `absolute inset-0 ${isVideoType ? 'w-full h-full object-cover' : 'max-w-full max-h-64 object-contain'}`
})
const overlayImageClass = computed(() => {
const baseClasses = 'size-full transition-opacity duration-300'
const sizeClasses = isVideoType ? 'object-cover' : 'object-contain'
const baseClasses = 'absolute inset-0 transition-opacity duration-300'
const sizeClasses = isVideoType
? 'w-full h-full object-cover'
: 'max-w-full max-h-64 object-contain'
const opacityClasses = isHovered ? 'opacity-100' : 'opacity-0'
return `${baseClasses} ${sizeClasses} ${opacityClasses}`
})

View File

@@ -187,7 +187,7 @@ const extraMenuItems = computed<MenuItem[]>(() => [
{
key: 'browse-templates',
label: t('menuLabels.Browse Templates'),
icon: 'icon-[comfy--template]',
icon: 'pi pi-folder-open',
command: () => commandStore.execute('Comfy.BrowseTemplates')
},
{

View File

@@ -1,47 +0,0 @@
<template>
<div>
<Button
v-tooltip="{ value: $t('g.moreWorkflows'), showDelay: 300 }"
class="rounded-none"
icon="pi pi-ellipsis-h"
text
severity="secondary"
:aria-label="$t('g.moreWorkflows')"
@click="menu?.toggle($event)"
/>
<Menu
ref="menu"
:model="menuItems"
:popup="true"
class="max-h-[40vh] overflow-auto"
/>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Menu from 'primevue/menu'
import { computed, ref } from 'vue'
import { useWorkflowService } from '@/services/workflowService'
import type { ComfyWorkflow } from '@/stores/workflowStore'
const props = defineProps<{
workflows: ComfyWorkflow[]
activeWorkflow: ComfyWorkflow | null
}>()
const menu = ref<InstanceType<typeof Menu> | null>(null)
const workflowService = useWorkflowService()
const menuItems = computed(() =>
props.workflows.map((workflow: ComfyWorkflow) => ({
label: workflow.filename,
icon:
props.activeWorkflow?.key === workflow.key ? 'pi pi-check' : undefined,
command: () => {
void workflowService.openWorkflow(workflow)
}
}))
)
</script>

View File

@@ -16,7 +16,7 @@
ref="scrollPanelRef"
class="overflow-hidden no-drag"
:pt:content="{
class: 'p-0 w-full flex',
class: 'p-0 w-full',
onwheel: handleWheel
}"
pt:bar-x="h-1"
@@ -48,11 +48,6 @@
:disabled="!rightArrowEnabled"
@mousedown="whileMouseDown($event, () => scroll(1))"
/>
<WorkflowOverflowMenu
v-if="showOverflowArrows"
:workflows="workflowStore.openWorkflows"
:active-workflow="workflowStore.activeWorkflow"
/>
<Button
v-tooltip="{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }"
class="new-blank-workflow-button flex-shrink-0 no-drag rounded-none"
@@ -90,8 +85,6 @@ import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
import WorkflowOverflowMenu from './WorkflowOverflowMenu.vue'
interface WorkflowOption {
value: string
workflow: ComfyWorkflow

View File

@@ -1,113 +0,0 @@
import { ref, watch } from 'vue'
import type { Ref } from 'vue'
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { createBounds } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/stores/graphStore'
/**
* Manages the position of the selection toolbox independently.
* Uses CSS custom properties for performant transform updates.
*/
export function useSelectionToolboxPosition(
toolboxRef: Ref<HTMLElement | undefined>
) {
const canvasStore = useCanvasStore()
const lgCanvas = canvasStore.getCanvas()
const { getSelectableItems } = useSelectedLiteGraphItems()
// World position of selection center
const worldPosition = ref({ x: 0, y: 0 })
const visible = ref(false)
/**
* Update position based on selection
*/
const updateSelectionBounds = () => {
const selectableItems = getSelectableItems()
if (!selectableItems.size) {
visible.value = false
return
}
visible.value = true
const bounds = createBounds(selectableItems)
if (!bounds) {
return
}
const [xBase, y, width] = bounds
worldPosition.value = {
x: xBase + width / 2,
y: y
}
updateTransform()
}
const updateTransform = () => {
if (!visible.value) return
const { scale, offset } = lgCanvas.ds
const canvasRect = lgCanvas.canvas.getBoundingClientRect()
const screenX =
(worldPosition.value.x + offset[0]) * scale + canvasRect.left
const screenY = (worldPosition.value.y + offset[1]) * scale + canvasRect.top
// Update CSS custom properties directly for best performance
if (toolboxRef.value) {
toolboxRef.value.style.setProperty('--tb-x', `${screenX}px`)
toolboxRef.value.style.setProperty('--tb-y', `${screenY}px`)
}
}
// Sync with canvas transform
const { startSync, stopSync } = useCanvasTransformSync(updateTransform, {
autoStart: false
})
// Watch for selection changes
watch(
() => canvasStore.getCanvas().state.selectionChanged,
(changed) => {
if (changed) {
updateSelectionBounds()
canvasStore.getCanvas().state.selectionChanged = false
// Start transform sync if we have selection
if (visible.value) {
startSync()
} else {
stopSync()
}
}
},
{ immediate: true }
)
// Watch for dragging state
watch(
() => canvasStore.canvas?.state?.draggingItems,
(dragging) => {
if (dragging) {
// Hide during node dragging
visible.value = false
} else {
// Update after dragging ends
requestAnimationFrame(() => {
updateSelectionBounds()
})
}
}
)
return {
visible
}
}

View File

@@ -0,0 +1,80 @@
import { onMounted, ref, watch } from 'vue'
import type { Ref, WatchSource } from 'vue'
/**
* A composable that manages retriggerable CSS animations.
* Provides a boolean ref that can be toggled to restart CSS animations.
*
* @param trigger - Optional reactive source that triggers the animation when it changes
* @param options - Configuration options
* @returns An object containing the animation state ref
*
* @example
* ```vue
* <template>
* <div :class="{ 'animate-slide-up': shouldAnimate }">
* Content
* </div>
* </template>
*
* <script setup>
* const { shouldAnimate } = useRetriggerableAnimation(someReactiveTrigger)
* </script>
* ```
*/
export function useRetriggerableAnimation<T = any>(
trigger?: WatchSource<T> | Ref<T>,
options: {
animateOnMount?: boolean
animationDelay?: number
} = {}
) {
const { animateOnMount = true, animationDelay = 0 } = options
const shouldAnimate = ref(false)
/**
* Retriggers the animation by removing and re-adding the animation class
*/
const retriggerAnimation = () => {
// Remove animation class
shouldAnimate.value = false
// Force browser reflow to ensure the class removal is processed
void document.body.offsetHeight
// Re-add animation class in the next frame
requestAnimationFrame(() => {
if (animationDelay > 0) {
setTimeout(() => {
shouldAnimate.value = true
}, animationDelay)
} else {
shouldAnimate.value = true
}
})
}
// Trigger animation on mount if requested
if (animateOnMount) {
onMounted(() => {
if (animationDelay > 0) {
setTimeout(() => {
shouldAnimate.value = true
}, animationDelay)
} else {
shouldAnimate.value = true
}
})
}
// Watch for trigger changes to retrigger animation
if (trigger) {
watch(trigger, () => {
retriggerAnimation()
})
}
return {
shouldAnimate,
retriggerAnimation
}
}

View File

@@ -1332,9 +1332,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
return 'Token-based'
}
},
GeminiImageNode: {
displayPrice: '$0.03 per 1K tokens'
},
// OpenAI nodes
OpenAIChatNode: {
displayPrice: (node: LGraphNode): string => {
@@ -1386,38 +1383,6 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
},
ViduStartEndToVideoNode: {
displayPrice: '$0.4/Run'
},
ByteDanceImageNode: {
displayPrice: (node: LGraphNode): string => {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model'
) as IComboWidget
if (!modelWidget) return 'Token-based'
const model = String(modelWidget.value)
if (model.includes('seedream-3-0-t2i')) {
return '$0.03/Run'
}
return 'Token-based'
}
},
ByteDanceImageEditNode: {
displayPrice: (node: LGraphNode): string => {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model'
) as IComboWidget
if (!modelWidget) return 'Token-based'
const model = String(modelWidget.value)
if (model.includes('seededit-3-0-i2i')) {
return '$0.03/Run'
}
return 'Token-based'
}
}
}
@@ -1505,10 +1470,7 @@ export const useNodePricing = () => {
// Google/Gemini nodes
GeminiNode: ['model'],
// OpenAI nodes
OpenAIChatNode: ['model'],
// ByteDance
ByteDanceImageNode: ['model'],
ByteDanceImageEditNode: ['model']
OpenAIChatNode: ['model']
}
return widgetMap[nodeType] || []
}

View File

@@ -1,14 +1,18 @@
import { markRaw } from 'vue'
import { defineAsyncComponent, markRaw } from 'vue'
import ModelLibrarySidebarTab from '@/components/sidebar/tabs/ModelLibrarySidebarTab.vue'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { isElectron } from '@/utils/envUtil'
const AiModelIcon = markRaw(
defineAsyncComponent(() => import('virtual:icons/comfy/ai-model'))
)
export const useModelLibrarySidebarTab = (): SidebarTabExtension => {
return {
id: 'model-library',
icon: 'icon-[comfy--ai-model]',
icon: AiModelIcon,
title: 'sideToolbar.modelLibrary',
tooltip: 'sideToolbar.modelLibrary',
label: 'sideToolbar.labels.models',

View File

@@ -1,12 +1,16 @@
import { markRaw } from 'vue'
import { defineAsyncComponent, markRaw } from 'vue'
import NodeLibrarySidebarTab from '@/components/sidebar/tabs/NodeLibrarySidebarTab.vue'
import type { SidebarTabExtension } from '@/types/extensionTypes'
const NodeIcon = markRaw(
defineAsyncComponent(() => import('virtual:icons/comfy/node'))
)
export const useNodeLibrarySidebarTab = (): SidebarTabExtension => {
return {
id: 'node-library',
icon: 'icon-[comfy--node]',
icon: NodeIcon,
title: 'sideToolbar.nodeLibrary',
tooltip: 'sideToolbar.nodeLibrary',
label: 'sideToolbar.labels.nodes',

View File

@@ -1,16 +1,20 @@
import { markRaw } from 'vue'
import { defineAsyncComponent, markRaw } from 'vue'
import WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
const WorkflowIcon = markRaw(
defineAsyncComponent(() => import('virtual:icons/comfy/workflow'))
)
export const useWorkflowsSidebarTab = (): SidebarTabExtension => {
const settingStore = useSettingStore()
const workflowStore = useWorkflowStore()
return {
id: 'workflows',
icon: 'icon-[comfy--workflow]',
icon: WorkflowIcon,
iconBadge: () => {
if (
settingStore.get('Comfy.Workflow.WorkflowTabsPosition') !== 'Sidebar'

View File

@@ -298,26 +298,8 @@ export function useCoreCommands(): ComfyCommand[] {
id: 'Comfy.Canvas.ToggleLock',
icon: 'pi pi-lock',
label: 'Canvas Toggle Lock',
category: 'view-controls' as const,
function: () => {
app.canvas.state.readOnly = !app.canvas.state.readOnly
}
},
{
id: 'Comfy.Canvas.Lock',
icon: 'pi pi-lock',
label: 'Lock Canvas',
category: 'view-controls' as const,
function: () => {
app.canvas.state.readOnly = true
}
},
{
id: 'Comfy.Canvas.Unlock',
icon: 'pi pi-lock-open',
label: 'Unlock Canvas',
function: () => {
app.canvas.state.readOnly = false
app.canvas['read_only'] = !app.canvas['read_only']
}
},
{
@@ -446,9 +428,6 @@ export function useCoreCommands(): ComfyCommand[] {
)
group.resizeTo(canvas.selectedItems, padding)
canvas.graph?.add(group)
group.recomputeInsideNodes()
useTitleEditorStore().titleEditorTarget = group
}
},

View File

@@ -1,27 +0,0 @@
import { computed, ref } from 'vue'
export function useZoomControls() {
const isModalVisible = ref(false)
const showModal = () => {
isModalVisible.value = true
}
const hideModal = () => {
isModalVisible.value = false
}
const toggleModal = () => {
isModalVisible.value = !isModalVisible.value
}
const hasActivePopup = computed(() => isModalVisible.value)
return {
isModalVisible,
showModal,
hideModal,
toggleModal,
hasActivePopup
}
}

View File

@@ -191,18 +191,6 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
},
{
combo: {
key: 'v'
},
commandId: 'Comfy.Canvas.Unlock'
},
{
combo: {
key: 'h'
},
commandId: 'Comfy.Canvas.Lock'
},
{
combo: {
key: 'Escape'

View File

@@ -13,7 +13,6 @@ export const CORE_MENU_COMMANDS = [
],
[['Edit'], ['Comfy.Undo', 'Comfy.Redo']],
[['Edit'], ['Comfy.OpenClipspace']],
[['Edit'], ['Comfy.RefreshNodeDefinitions']],
[
['Help'],
[

View File

@@ -791,11 +791,11 @@ export const CORE_SETTINGS: SettingParams[] = [
type: 'combo',
options: [
{ value: 'standard', text: 'Standard (New)' },
{ value: 'legacy', text: 'Drag Navigation' }
{ value: 'legacy', text: 'Left-Click Pan (Legacy)' }
],
versionAdded: '1.25.0',
defaultsByInstallVersion: {
'1.25.0': 'legacy'
'1.25.0': 'standard'
}
},
{

View File

@@ -42,8 +42,6 @@ app.registerExtension({
this.graph.add(group)
// @ts-expect-error fixme ts strict error
this.graph.change()
group.recomputeInsideNodes()
}
})
}

View File

@@ -14,7 +14,6 @@ import './previewAny'
import './rerouteNode'
import './saveImageExtraOutput'
import './saveMesh'
import './selectionBorder'
import './simpleTouchSupport'
import './slotDefaults'
import './uploadAudio'

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