Compare commits

..

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
ec282d4eab Complete Storybook issue tracking system with documentation
Co-authored-by: snomiao <7323030+snomiao@users.noreply.github.com>
2025-08-20 10:29:21 +00:00
copilot-swe-agent[bot]
ae7f6b8143 Add Storybook issue template and tracking documentation
Co-authored-by: snomiao <7323030+snomiao@users.noreply.github.com>
2025-08-20 10:28:05 +00:00
copilot-swe-agent[bot]
90c09db919 Initial plan 2025-08-20 10:22:03 +00:00
207 changed files with 21229 additions and 23839 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

@@ -1,158 +0,0 @@
# Setup Repository
Bootstrap the ComfyUI Frontend monorepo with all necessary dependencies and verification checks.
## Overview
This command will:
1. Install pnpm package manager (if not present)
2. Install all project dependencies
3. Verify the project builds successfully
4. Run unit tests to ensure functionality
5. Start development server to verify frontend boots correctly
## Prerequisites Check
First, let's verify the environment:
```bash
# Check Node.js version (should be >= 24)
node --version
# Check if we're in a git repository
git status
```
## Step 1: Install pnpm
```bash
# Check if pnpm is already installed
pnpm --version 2>/dev/null || {
echo "Installing pnpm..."
npm install -g pnpm
}
# Verify pnpm installation
pnpm --version
```
## Step 2: Install Dependencies
```bash
# Install all dependencies using pnpm
echo "Installing project dependencies..."
pnpm install
# Verify node_modules exists and has packages
ls -la node_modules | head -5
```
## Step 3: Verify Build
```bash
# Run TypeScript type checking
echo "Running TypeScript checks..."
pnpm typecheck
# Build the project
echo "Building project..."
pnpm build
# Verify dist folder was created
ls -la dist/
```
## Step 4: Run Unit Tests
```bash
# Run unit tests
echo "Running unit tests..."
pnpm test:unit
# If tests fail, show the output and stop
if [ $? -ne 0 ]; then
echo "❌ Unit tests failed. Please fix failing tests before continuing."
exit 1
fi
echo "✅ Unit tests passed successfully"
```
## Step 5: Verify Development Server
```bash
# Start development server in background
echo "Starting development server..."
pnpm dev &
SERVER_PID=$!
# Wait for server to start (check for port 5173 or similar)
echo "Waiting for server to start..."
sleep 10
# Check if server is running
if curl -s http://localhost:5173 > /dev/null 2>&1; then
echo "✅ Development server started successfully at http://localhost:5173"
# Kill the background server
kill $SERVER_PID
wait $SERVER_PID 2>/dev/null
else
echo "❌ Development server failed to start or is not accessible"
kill $SERVER_PID 2>/dev/null
wait $SERVER_PID 2>/dev/null
exit 1
fi
```
## Step 6: Final Verification
```bash
# Run linting to ensure code quality
echo "Running linter..."
pnpm lint
# Show project status
echo ""
echo "🎉 Repository setup complete!"
echo ""
echo "Available commands:"
echo " pnpm dev - Start development server"
echo " pnpm build - Build for production"
echo " pnpm test:unit - Run unit tests"
echo " pnpm test:component - Run component tests"
echo " pnpm typecheck - Run TypeScript checks"
echo " pnpm lint - Run ESLint"
echo " pnpm format - Format code with Prettier"
echo ""
echo "Next steps:"
echo "1. Run 'pnpm dev' to start developing"
echo "2. Open http://localhost:5173 in your browser"
echo "3. Check README.md for additional setup instructions"
```
## Troubleshooting
If any step fails:
1. **pnpm installation fails**: Try using `curl -fsSL https://get.pnpm.io/install.sh | sh -`
2. **Dependencies fail to install**: Try clearing cache with `pnpm store prune` and retry
3. **Build fails**: Check for TypeScript errors and fix them first
4. **Tests fail**: Review test output and fix failing tests
5. **Dev server fails**: Check if port 5173 is already in use
## Manual Verification Steps
After running the setup, manually verify:
1. **Dependencies installed**: `ls node_modules | wc -l` should show many packages
2. **Build artifacts**: `ls dist/` should show built files
3. **Server accessible**: Open http://localhost:5173 in browser
4. **Hot reload works**: Edit a file and see changes reflect
## Environment Requirements
- Node.js >= 24
- Git repository
- Internet connection for package downloads
- Available ports (typically 5173 for dev server)

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

@@ -1,5 +1,6 @@
name: Bug Report
description: 'Report something that is not working correctly'
title: '[Bug]: '
labels: ['Potential Bug']
type: Bug

View File

@@ -1,6 +1,7 @@
name: Feature Request
description: Report a problem or limitation you're experiencing
labels: []
title: '[Feature]: '
labels: ['enhancement']
type: Feature
body:

View File

@@ -0,0 +1,96 @@
name: Storybook Improvement
description: 'Report Storybook-related improvements, new stories, or configuration issues'
title: '[Storybook]: '
labels: ['area:storybook', 'enhancement']
type: Storybook
body:
- type: checkboxes
attributes:
label: Prerequisites
options:
- label: I have checked the existing Storybook documentation in `.storybook/`
required: true
- label: I have searched existing issues to make sure this isn't a duplicate
required: true
- label: I have checked if this relates to any existing Storybook PRs
- type: dropdown
id: category
attributes:
label: Category
description: What type of Storybook improvement is this?
options:
- Component Stories - Add new stories for existing components
- Story Enhancement - Improve existing stories
- Configuration - Storybook configuration improvements
- Visual Testing - Chromatic/visual regression testing
- Documentation - Storybook documentation improvements
- Build/Performance - Build optimizations or performance improvements
- Theme/Styling - Theme support or styling improvements
- Tools/Addons - Storybook addons or tool integration
- Other
validations:
required: true
- type: textarea
id: description
attributes:
label: What improvement are you suggesting?
description: Describe the Storybook improvement you'd like to see
placeholder: |
Example: "Add comprehensive stories for the NodeWidget component covering all widget types (text, number, combo, etc.) to improve component development and testing."
validations:
required: true
- type: textarea
id: context
attributes:
label: Use Case & Context
description: Why is this improvement needed? What problem does it solve?
placeholder: |
- Current state of the component/story
- What's missing or could be improved
- How this would benefit development workflow
validations:
required: true
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this improvement?
options:
- Low - Nice to have enhancement
- Medium - Would improve development workflow
- High - Important for component quality/testing
- Critical - Needed for proper Storybook functionality
validations:
required: true
- type: textarea
id: components
attributes:
label: Components Affected
description: Which components or areas would be affected by this improvement?
placeholder: |
- NodeWidget
- Settings components
- All button components
- etc.
- type: textarea
id: implementation
attributes:
label: Implementation Ideas (Optional)
description: Any ideas on how this could be implemented?
placeholder: |
- Specific stories to create
- Configuration changes needed
- Dependencies or tools required
- type: textarea
id: examples
attributes:
label: Examples or References
description: Any examples from other projects, screenshots, or links that help illustrate the improvement

View File

@@ -3,48 +3,54 @@ 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
steps:
- name: Checkout code
uses: actions/checkout@v4
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
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Cache tool outputs
uses: actions/cache@v4
- name: Comment PR - Build Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
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 }}-
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
🔄 **Building Storybook and running visual tests...**
⏳ Build started at: ${{ steps.current-time.outputs.time }} UTC
---
*This comment will be updated when the build completes*
- name: Install dependencies
run: pnpm install --frozen-lockfile
run: npm ci
- name: Build Storybook and run Chromatic
id: chromatic
@@ -55,3 +61,36 @@ jobs:
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Build Complete
if: github.event_name == 'pull_request' && always()
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{ steps.chromatic.outcome == 'success' && '✅' || '❌' }} **${{ steps.chromatic.outcome == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 📊 Build Summary
- **Components**: ${{ steps.chromatic.outputs.componentCount || '0' }}
- **Stories**: ${{ steps.chromatic.outputs.testCount || '0' }}
- **Visual changes**: ${{ steps.chromatic.outputs.changeCount || '0' }}
- **Errors**: ${{ steps.chromatic.outputs.errorCount || '0' }}
### 🔗 Links
${{ steps.chromatic.outputs.buildUrl && format('- [📸 View Chromatic Build]({0})', steps.chromatic.outputs.buildUrl) || '' }}
${{ steps.chromatic.outputs.storybookUrl && format('- [📖 Preview Storybook]({0})', steps.chromatic.outputs.storybookUrl) || '' }}
---
${{ steps.chromatic.outcome == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}

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,52 +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 }}-
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ 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
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

@@ -1,336 +0,0 @@
name: PR Playwright Deploy and Comment
on:
workflow_run:
workflows: ["Tests CI"]
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-reports:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed'
permissions:
actions: read
strategy:
fail-fast: false
matrix:
browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome]
steps:
- name: Get PR info
id: pr-info
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return { number: null, sanitized_branch: null };
}
const pr = pullRequests[0];
console.log(`✅ Found PR #${pr.number} for branch: ${context.payload.workflow_run.head_branch}`);
console.log(`PR number is: ${pr.number}`);
const branchName = context.payload.workflow_run.head_branch;
const sanitizedBranch = branchName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/^-|-$/g, '');
return {
number: pr.number,
sanitized_branch: sanitizedBranch
};
- name: Set project name
if: fromJSON(steps.pr-info.outputs.result).number != null
id: project-name
run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
else
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
fi
echo "branch=${{ fromJSON(steps.pr-info.outputs.result).sanitized_branch }}" >> $GITHUB_OUTPUT
- name: Download playwright report
if: fromJSON(steps.pr-info.outputs.result).number != null
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: playwright-report-${{ matrix.browser }}
path: playwright-report
- name: Install Wrangler
if: fromJSON(steps.pr-info.outputs.result).number != null
run: npm install -g wrangler
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
if: fromJSON(steps.pr-info.outputs.result).number != null
id: cloudflare-deploy
continue-on-error: true
run: |
# Retry logic for wrangler deploy (3 attempts)
RETRY_COUNT=0
MAX_RETRIES=3
SUCCESS=false
DEPLOYMENT_URL=""
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
# Capture wrangler output to extract deployment URL
OUTPUT=$(npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }} 2>&1)
EXIT_CODE=$?
echo "$OUTPUT"
if [ $EXIT_CODE -eq 0 ]; then
SUCCESS=true
echo "Deployment successful on attempt $RETRY_COUNT"
# Extract the deployment URL from wrangler output
# Look for the URL in various formats that wrangler might output
DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oE 'https://[a-z0-9.-]+\.pages\.dev(/[^[:space:]]*)?$' | head -1)
if [ -z "$DEPLOYMENT_URL" ]; then
# Try another pattern if the first one fails
DEPLOYMENT_URL=$(echo "$OUTPUT" | grep -oE 'https://[^[:space:]]+\.pages\.dev' | head -1)
fi
if [ -n "$DEPLOYMENT_URL" ]; then
echo "Deployment URL: $DEPLOYMENT_URL"
echo "url=$DEPLOYMENT_URL" >> $GITHUB_OUTPUT
else
echo "Warning: Could not extract deployment URL from wrangler output"
# Construct expected URL as fallback
FALLBACK_URL="https://${{ steps.project-name.outputs.name }}-${{ steps.project-name.outputs.branch }}.pages.dev"
echo "Using fallback URL: $FALLBACK_URL"
echo "url=$FALLBACK_URL" >> $GITHUB_OUTPUT
fi
else
echo "Deployment failed on attempt $RETRY_COUNT"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ $SUCCESS = false ]; then
echo "All deployment attempts failed"
exit 1
fi
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
- name: Save deployment info
if: fromJSON(steps.pr-info.outputs.result).number != null && always()
run: |
# Read test exit code from the artifact
TEST_EXIT_CODE="1"
if [ -f "playwright-report/test-exit-code.txt" ]; then
TEST_EXIT_CODE=$(cat playwright-report/test-exit-code.txt)
fi
# Use deployment URL if available, otherwise use a fallback
URL="${{ steps.cloudflare-deploy.outputs.url }}"
if [ -z "$URL" ] || [ "${{ steps.cloudflare-deploy.outcome }}" != "success" ]; then
URL="https://${{ steps.project-name.outputs.name }}-${{ steps.project-name.outputs.branch }}.pages.dev"
fi
echo "${{ matrix.browser }}|$TEST_EXIT_CODE|$URL" > deployment-info.txt
echo "Saved deployment info: ${{ matrix.browser }}|$TEST_EXIT_CODE|$URL"
- name: Upload deployment info
if: fromJSON(steps.pr-info.outputs.result).number != null && always()
uses: actions/upload-artifact@v4
with:
name: deployment-info-${{ matrix.browser }}
path: deployment-info.txt
retention-days: 1
comment-tests-starting:
runs-on: ubuntu-latest
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'requested'
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
const prNumber = pullRequests[0].number;
console.log(`✅ Found PR #${prNumber} for branch: ${context.payload.workflow_run.head_branch}`);
return prNumber;
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body for start
if: steps.pr.outputs.result != 'null'
id: comment-body-start
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
echo "<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-right: 4px;' /> **Tests are starting...** " >> comment.md
echo "" >> comment.md
echo "⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 🚀 Running Tests" >> comment.md
echo "- 🧪 **chromium**: Running tests..." >> comment.md
echo "- 🧪 **chromium-0.5x**: Running tests..." >> comment.md
echo "- 🧪 **chromium-2x**: Running tests..." >> comment.md
echo "- 🧪 **mobile-chrome**: Running tests..." >> comment.md
echo "" >> comment.md
echo "---" >> comment.md
echo "⏱️ Please wait while tests are running across all browsers..." >> comment.md
- name: Comment PR - Tests Started
if: steps.pr.outputs.result != 'null'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
comment-tests-completed:
runs-on: ubuntu-latest
needs: deploy-reports
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed' && always()
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
const prNumber = pullRequests[0].number;
console.log(`✅ Found PR #${prNumber} for branch: ${context.payload.workflow_run.head_branch}`);
return prNumber;
- name: Download all deployment info
if: steps.pr.outputs.result != 'null'
uses: actions/download-artifact@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
pattern: deployment-info-*
merge-multiple: true
path: deployment-info
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body for completion
if: steps.pr.outputs.result != 'null'
id: comment-body-completed
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
# Check if all tests passed
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
# Validate URLs before using them in comments
sanitized_url=$(echo "$url" | grep -E '^https://[a-z0-9.-]+\.pages\.dev(/.*)?$' || echo "INVALID_URL")
if [ "$sanitized_url" = "INVALID_URL" ]; then
echo "Invalid deployment URL detected: $url"
url="#" # Use safe fallback
fi
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Complete
if: steps.pr.outputs.result != 'null'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md

View File

@@ -1,126 +0,0 @@
name: PR Storybook Comment
on:
workflow_run:
workflows: ['Chromatic']
types: [requested, completed]
jobs:
comment-storybook:
runs-on: ubuntu-latest
if: >-
github.repository == 'Comfy-Org/ComfyUI_frontend'
&& github.event.workflow_run.event == 'pull_request'
&& startsWith(github.event.workflow_run.head_branch, 'version-bump-')
permissions:
pull-requests: write
actions: read
steps:
- name: Get PR number
id: pr
uses: actions/github-script@v7
with:
script: |
const { data: pullRequests } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
});
if (pullRequests.length === 0) {
console.log('No open PR found for this branch');
return null;
}
return pullRequests[0].number;
- name: Log when no PR found
if: steps.pr.outputs.result == 'null'
run: |
echo "⚠️ No open PR found for branch: ${{ github.event.workflow_run.head_branch }}"
echo "Workflow run ID: ${{ github.event.workflow_run.id }}"
echo "Repository: ${{ github.event.workflow_run.repository.full_name }}"
echo "Event: ${{ github.event.workflow_run.event }}"
- name: Get workflow run details
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
id: workflow-run
uses: actions/github-script@v7
with:
script: |
const run = await github.rest.actions.getWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
return {
conclusion: run.data.conclusion,
html_url: run.data.html_url
};
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Storybook Started
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
<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-right: 4px;' /> **Build is starting...**
⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC
### 🚀 Building Storybook
- 📦 Installing dependencies...
- 🔧 Building Storybook components...
- 🎨 Running Chromatic visual tests...
---
⏱️ Please wait while the Storybook build is in progress...
- name: Comment PR - Storybook Complete
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
with:
issue-number: ${{ steps.pr.outputs.result }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '✅'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && '⏭️'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && '🚫'
|| '❌'
}} **${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && 'Build completed successfully!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && 'Build skipped.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && 'Build cancelled.'
|| 'Build failed!'
}}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 🔗 Links
- [📊 View Workflow Run](${{ fromJSON(steps.workflow-run.outputs.result).html_url }})
---
${{
fromJSON(steps.workflow-run.outputs.result).conclusion == 'success' && '🎉 Your Storybook is ready for review!'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'skipped' && ' Chromatic was skipped for this PR.'
|| fromJSON(steps.workflow-run.outputs.result).conclusion == 'cancelled' && ' The Chromatic run was cancelled.'
|| '⚠️ Please check the workflow logs for error 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,14 +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
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/pnpm-lock.yaml') }}
restore-keys: |
playwright-browsers-${{ 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

View File

@@ -7,11 +7,15 @@ on:
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
sanitized-branch: ${{ steps.branch-info.outputs.sanitized }}
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
@@ -33,40 +37,51 @@ 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
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Cache tool outputs
uses: actions/cache@v4
- name: Comment PR - Tests Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
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 }}-
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- PLAYWRIGHT_TEST_STATUS -->
---
<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: Build ComfyUI_frontend
run: |
pnpm install --frozen-lockfile
pnpm build
npm ci
npm run build
working-directory: ComfyUI_frontend
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Generate sanitized branch name
id: branch-info
run: |
# Get branch name and sanitize it for Cloudflare branch names
BRANCH_NAME="${{ github.head_ref || github.ref_name }}"
SANITIZED_BRANCH=$(echo "$BRANCH_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
echo "sanitized=${SANITIZED_BRANCH}" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
@@ -79,6 +94,8 @@ jobs:
needs: setup
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
contents: read
strategy:
fail-fast: false
@@ -97,16 +114,35 @@ 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
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Set project name
id: project-name
run: |
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
else
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
fi
echo "branch=${{ needs.setup.outputs.sanitized-branch }}" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<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
run: |
@@ -122,15 +158,6 @@ 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
@@ -139,20 +166,162 @@ jobs:
id: playwright
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
working-directory: ComfyUI_frontend
continue-on-error: true
- name: Save test exit code
if: always()
run: |
echo "${{ steps.playwright.outcome == 'success' && '0' || '1' }}" > test-exit-code.txt
echo "Test outcome: ${{ steps.playwright.outcome }}"
- uses: actions/upload-artifact@v4
if: always() # note: use always() to allow results to be upload/report even tests failed.
with:
name: playwright-report-${{ matrix.browser }}
path: |
ComfyUI_frontend/playwright-report/
test-exit-code.txt
path: ComfyUI_frontend/playwright-report/
retention-days: 30
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
id: cloudflare-deploy
if: always()
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}
- name: Save deployment info for summary
if: always()
run: |
mkdir -p deployment-info
# Use step conclusion to determine test result
if [ "${{ steps.playwright.conclusion }}" = "success" ]; then
EXIT_CODE="0"
else
EXIT_CODE="1"
fi
DEPLOYMENT_URL="${{ steps.cloudflare-deploy.outputs.deployment-url || steps.cloudflare-deploy.outputs.url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }}"
echo "${{ matrix.browser }}|${EXIT_CODE}|${DEPLOYMENT_URL}" > deployment-info/${{ matrix.browser }}.txt
- name: Upload deployment info
if: always()
uses: actions/upload-artifact@v4
with:
name: deployment-info-${{ matrix.browser }}
path: deployment-info/
retention-days: 1
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Comment PR - Browser Test Complete
if: always() && github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
${{ steps.playwright.conclusion == 'success' && '✅' || '❌' }} **${{ matrix.browser }}**: ${{ steps.playwright.conclusion == 'success' && 'Tests passed!' || 'Tests failed!' }} [View Report](${{ steps.cloudflare-deploy.outputs.deployment-url || format('https://{0}.{1}.pages.dev', steps.project-name.outputs.branch, steps.project-name.outputs.name) }})
comment-summary:
needs: playwright-tests
runs-on: ubuntu-latest
if: always() && github.event_name == 'pull_request'
permissions:
pull-requests: write
steps:
- name: Download all deployment info
uses: actions/download-artifact@v4
with:
pattern: deployment-info-*
merge-multiple: true
path: deployment-info
- name: Get completion time
id: completion-time
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
- name: Generate comment body
id: comment-body
run: |
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
echo "## 🎭 Playwright Test Results" >> comment.md
echo "" >> comment.md
# Check if all tests passed
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "true" ]; then
echo "✅ **All tests passed across all browsers!**" >> comment.md
else
echo "❌ **Some tests failed!**" >> comment.md
fi
echo "" >> comment.md
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
echo "" >> comment.md
echo "### 📊 Test Reports by Browser" >> comment.md
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
browser=$(basename "$file" .txt)
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
url=$(echo "$info" | cut -d'|' -f3)
if [ "$exit_code" = "0" ]; then
status="✅"
else
status="❌"
fi
echo "- $status **$browser**: [View Report]($url)" >> comment.md
fi
done
echo "" >> comment.md
echo "---" >> comment.md
if [ "$ALL_PASSED" = "true" ]; then
echo "🎉 Your tests are passing across all browsers!" >> comment.md
else
echo "⚠️ Please check the test reports for details on failures." >> comment.md
fi
- name: Comment PR - Tests Complete
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body-path: comment.md
- name: Check test results and fail if needed
run: |
# Check if all tests passed and fail the job if not
ALL_PASSED=true
for file in deployment-info/*.txt; do
if [ -f "$file" ]; then
info=$(cat "$file")
exit_code=$(echo "$info" | cut -d'|' -f2)
if [ "$exit_code" != "0" ]; then
ALL_PASSED=false
break
fi
fi
done
if [ "$ALL_PASSED" = "false" ]; then
echo "❌ Tests failed in one or more browsers. Failing the CI job."
exit 1
else
echo "✅ All tests passed across all browsers!"
fi

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

12
.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,7 +23,7 @@ dist-ssr
*.local
# Claude configuration
.claude/*.local.json
CLAUDE.local.md
.claude/settings.json
# Editor directories and files
.vscode/*
@@ -77,12 +78,3 @@ vite.config.mts.timestamp-*.mjs
*storybook.log
storybook-static
.nx/cache
.nx/workspace-data
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
vite.config.*.timestamp*
vitest.config.*.timestamp*

1
.nvmrc
View File

@@ -1 +0,0 @@
24

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
@@ -93,44 +93,6 @@ export const WithVariant: Story = {
## Development Tips
## ComfyUI Storybook Guidelines
### Scope When to Create Stories
- **PrimeVue components**:
No need to create stories. Just refer to the official PrimeVue documentation.
- **Custom shared components (design system components)**:
Always create stories. These components are built in collaboration with designers, and Storybook serves as both documentation and a communication tool.
- **Container components (logic-heavy)**:
Do not create stories. Only the underlying pure UI components should be included in Storybook.
### Maintenance Philosophy
- Stories are lightweight and generally stable.
Once created, they rarely need updates unless:
- The design changes
- New props (e.g. size, color variants) are introduced
- For existing usage patterns, simply copy real code examples into Storybook to create stories.
### File Placement
- Keep `*.stories.ts` files at the **same level as the component** (similar to test files).
- This makes it easier to check usage examples without navigating to another directory.
### Developer/Designer Workflow
- **UI vs Container**: Separate pure UI components from container components.
Only UI components should live in Storybook.
- **Communication Tool**: Storybook is not just about code quality—it enables designers and developers to see:
- Which props exist
- What cases are covered
- How variants (e.g. size, colors) look in isolation
- **Example**:
`PackActionButton.vue` wraps a PrimeVue button with additional logic.
→ Only create a story for the base UI button, not for the wrapper.
### Suggested Workflow
1. Use PrimeVue docs for standard components
2. Use Storybook for **shared/custom components** that define our design system
3. Keep story files alongside components
4. When in doubt, focus on components reused across the app or those that need to be showcased to designers
### Best Practices
1. **Keep Stories Simple**: Each story should demonstrate one specific use case
@@ -173,7 +135,6 @@ export const WithLongText: Story = {
- **`main.ts`**: Core Storybook configuration and Vite integration
- **`preview.ts`**: Global decorators, parameters, and Vue app setup
- **`manager.ts`**: Storybook UI customization (if needed)
- **`preview-head.html`**: Injects custom HTML into the `<head>` of every Storybook iframe (used for global styles, fonts, or fixes for iframe-specific issues)
## Chromatic Visual Testing
@@ -209,22 +170,4 @@ This Storybook setup includes:
- PrimeVue component library integration
- Proper alias resolution for `@/` imports
## Icon Usage in Storybook
In this project, the `<i-lucide:... />` syntax from unplugin-icons is not supported in Storybook.
**Example:**
```vue
<script setup lang="ts">
import { Trophy, Settings } from 'lucide-vue-next'
</script>
<template>
<Trophy :size="16" class="text-neutral" />
<Settings :size="16" class="text-neutral" />
</template>
```
This approach ensures icons render correctly in Storybook and remain consistent with the rest of the app.
For component-specific examples, see the NodePreview stories in `src/components/node/`.

View File

@@ -4,26 +4,17 @@
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Light theme default - with explicit color to override media queries */
body:not(.dark-theme) {
background-color: #fff !important;
color: #000 !important;
}
/* Override browser dark mode preference for light theme */
@media (prefers-color-scheme: dark) {
body:not(.dark-theme) {
color: #000 !important;
--fg-color: #000 !important;
--bg-color: #fff !important;
}
/* Light theme default */
body {
background-color: #ffffff;
color: #1a1a1a;
}
/* Dark theme styles */
body.dark-theme,
.dark-theme body {
background-color: #202020;
color: #fff;
background-color: #0a0a0a;
color: #e5e5e5;
}
/* Ensure Storybook canvas follows theme */
@@ -33,33 +24,11 @@
.dark-theme .sb-show-main,
.dark-theme .docs-story {
background-color: #202020 !important;
background-color: #0a0a0a !important;
}
/* CSS Variables for theme consistency */
body:not(.dark-theme) {
--fg-color: #000;
--bg-color: #fff;
--content-bg: #e0e0e0;
--content-fg: #000;
--content-hover-bg: #adadad;
--content-hover-fg: #000;
}
body.dark-theme {
--fg-color: #fff;
--bg-color: #202020;
--content-bg: #4e4e4e;
--content-fg: #fff;
--content-hover-bg: #222;
--content-hover-fg: #fff;
}
/* Override Storybook's problematic & selector styles */
/* Reset only the specific properties that Storybook injects */
#storybook-root li+li,
#storybook-docs li+li {
margin: inherit;
padding: inherit;
/* Fix for Storybook controls panel in dark mode */
.dark-theme .docblock-argstable-body {
color: #e5e5e5;
}
</style>

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

@@ -1,52 +1,22 @@
# ComfyUI Frontend Project Guidelines
## Repository Setup
For first-time setup, use the Claude command:
```
/setup_repo
```
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006)
## Quick Commands
- `pnpm`: See all available commands
- `pnpm dev`: Start development server (port 5173, via nx)
- `pnpm typecheck`: Type checking
- `pnpm build`: Build for production (via nx)
- `pnpm lint`: Linting (via nx)
- `pnpm format`: Prettier formatting
- `pnpm test:component`: Run component tests with browser environment
- `pnpm test:unit`: Run all unit tests
- `pnpm test:browser`: Run E2E tests via Playwright
- `pnpm test:unit -- tests-ui/tests/example.test.ts`: Run single test file
- `pnpm storybook`: Start Storybook development server (port 6006)
- `pnpm knip`: Detect unused code and dependencies
## Monorepo Architecture
The project now uses **Nx** for build orchestration and task management:
- **Task Orchestration**: Commands like `dev`, `build`, `lint`, and `test:browser` run via Nx
- **Caching**: Nx provides intelligent caching for faster rebuilds
- **Configuration**: Managed through `nx.json` with plugins for ESLint, Storybook, Vite, and Playwright
- **Dependencies**: Nx handles dependency graph analysis and parallel execution
Key Nx features:
- Build target caching and incremental builds
- Parallel task execution across the monorepo
- Plugin-based architecture for different tools
- `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
1. **First-time setup**: Run `/setup_repo` Claude command
2. Make code changes
3. Run tests (see subdirectory CLAUDE.md files)
4. Run typecheck, lint, format
5. Check README updates
6. Consider docs.comfy.org updates
1. Make code changes
2. Run tests (see subdirectory CLAUDE.md files)
3. Run typecheck, lint, format
4. Check README updates
5. Consider docs.comfy.org updates
## Git Conventions
@@ -82,44 +52,6 @@ When referencing Comfy-Org repos:
2. Use GitHub API for branches/PRs/metadata
3. Curl GitHub website if needed
## Settings and Feature Flags Quick Reference
### Settings Usage
```typescript
const settingStore = useSettingStore()
const value = settingStore.get('Comfy.SomeSetting') // Get setting
await settingStore.set('Comfy.SomeSetting', newValue) // Update setting
```
### Dynamic Defaults
```typescript
{
id: 'Comfy.Example.Setting',
defaultValue: () => window.innerWidth < 1024 ? 'small' : 'large' // Runtime context
}
```
### Version-Based Defaults
```typescript
{
id: 'Comfy.Example.Feature',
defaultValue: 'legacy',
defaultsByInstallVersion: { '1.25.0': 'enhanced' } // Gradual rollout
}
```
### Feature Flags
```typescript
if (api.serverSupportsFeature('feature_name')) { // Check capability
// Use enhanced feature
}
const value = api.getServerFeature('config_name', defaultValue) // Get config
```
**Documentation:**
- Settings system: `docs/SETTINGS.md`
- Feature flags system: `docs/FEATURE_FLAGS.md`
## Common Pitfalls
- NEVER use `any` type - use proper TypeScript types

View File

@@ -14,4 +14,4 @@
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
# Mask Editor extension
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs
/src/extensions/core/maskeditor.ts @trsommer @Comfy-Org/comfy_frontend_devs

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 (v18 or later to build; v24 for vite dev server) 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).

62
STORYBOOK_ISSUE_SYSTEM.md Normal file
View File

@@ -0,0 +1,62 @@
# Storybook Issue Tracking System
This directory contains resources for tracking and organizing all Storybook-related work in the ComfyUI Frontend repository.
## 📋 Components
### 1. Issue Template (`.github/ISSUE_TEMPLATE/storybook-improvement.yaml`)
A structured GitHub issue template specifically for Storybook-related improvements and requests.
**Features:**
- Categorizes improvements (Component Stories, Configuration, Visual Testing, etc.)
- Priority levels (Low → Critical)
- Component impact tracking
- Implementation ideas and examples
**Usage:** When creating new issues related to Storybook, use this template to ensure consistent formatting and complete information.
### 2. Tracking Documentation (`STORYBOOK_TRACKING_ISSUE.md`)
Comprehensive documentation listing all 27+ Storybook-related PRs, organized by category.
**Contains:**
- Current status overview
- PRs organized by category (Setup, Stories, Themes, Config, etc.)
- Upcoming priorities roadmap
- Contribution guidelines
- Resource links
## 🔧 How to Use
### For New Storybook Issues
1. Go to [GitHub Issues → New Issue](https://github.com/Comfy-Org/ComfyUI_frontend/issues/new/choose)
2. Select "Storybook Improvement" template
3. Fill out the structured form
4. Add `area:storybook` label if not automatically applied
### For Tracking Progress
1. Reference the tracking documentation in `STORYBOOK_TRACKING_ISSUE.md`
2. Create a GitHub issue using this content as the body
3. Use labels: `area:storybook`, `tracking`
4. Pin the issue for easy access
### For Contributors
1. Check existing tracking issue for current priorities
2. Follow guidelines in `.storybook/README.md` and `.storybook/CLAUDE.md`
3. Reference the tracking issue number in related PRs
4. Update tracking documentation when completing work
## 📚 Related Resources
- **Storybook Documentation**: `.storybook/README.md`
- **Developer Guidelines**: `.storybook/CLAUDE.md`
- **Component Examples**: `src/components/*/\*.stories.ts`
- **Visual Testing**: Chromatic integration in CI/CD workflows
## 🎯 Purpose
This system helps:
- **Organize** all Storybook-related work in one place
- **Track** progress across multiple PRs and initiatives
- **Prioritize** improvements based on impact and urgency
- **Facilitate** collaboration between contributors
- **Maintain** comprehensive documentation of Storybook evolution

121
STORYBOOK_TRACKING_ISSUE.md Normal file
View File

@@ -0,0 +1,121 @@
# 📚 Storybook Development Tracking
This issue serves as a central hub for tracking all Storybook-related PRs and improvements in the ComfyUI Frontend repository.
## 🎯 Overview
Storybook is a crucial part of our component development workflow, enabling:
- Component isolation and development
- Visual documentation and testing
- Automated visual regression testing with Chromatic
- Design system development and maintenance
## 📈 Current Status
**Storybook Setup**: ✅ Complete
**Component Coverage**: 🔄 In Progress
**Visual Testing**: ✅ Integrated with Chromatic
**Documentation**: ✅ Comprehensive guides available
## 📋 Storybook PRs by Category
### 🏗️ Initial Setup & Infrastructure
- **#4861** - [feat] Add Storybook setup and NodePreview story *(merged)*
- Complete Storybook v8 setup with Vue 3 + Vite
- Chromatic integration for visual testing
- Comprehensive documentation and guidelines
### 📖 Component Stories & Documentation
- **#4999** - [feat] 100+ more Stories for Common Components *(open)*
- 76 story variants across 11 components
- Covers STATIC → SIMPLE_PROPS → INTERACTIVE → COMPLEX tiers
- **#5034** - [feat] Add Storybook configuration and settings panel stories *(open)*
- Settings panel components with all input types
- Responsive design and accessibility features
- **#5098** - [feat] Add comprehensive Storybook stories for custom UI components *(open)*
- 12 custom UI components with interactive testing
- Button, input, and layout component stories
- **#5122** - [docs] Add Storybook documentation *(open)*
- Enhanced `.storybook/README.md` with comprehensive guidelines
### 🎨 Theme & Visual Improvements
- **#5088** - [feat] Add dark theme support for Storybook *(merged)*
- Dark theme toggle with persistence
- Smooth transitions and proper styling
### 🔧 Configuration & Build Optimizations
- **#5117** - [ci] Enhance CI/CD caching across all workflows *(open)*
- Improved caching for Storybook builds
- **#5118** - [ci] Add retry logic to wrangler page deploy step *(open)*
- Stability improvements for Storybook deployment
### 🚀 Features & Enhancements
- **#5119** - [feat] Add enhanced filter UI components *(open)*
- SearchBox integration and improved MultiSelect
- **#5096** - [fix] Resolve breadcrumb and workflow tabs layout conflict *(open)*
- Layout improvements affecting Storybook stories
- **#5113** - [fix] Reposition TaskItem info *(open)*
- Component fixes that impact Storybook examples
### 🔨 Technical Improvements & Fixes
- **#5106** - Fix/widget ordering consistency *(open)*
- Node widget improvements affecting stories
- **#5109** - Fix CopyToClipboard Issue *(open)*
- Component fixes relevant to Storybook examples
- **#5092** - Add support for high-resolution wheel events *(open)*
- Input handling improvements
- **#5115** - Fix: Shift+Click+Drag from outputs with Subgraph outputs *(open)*
- Node interaction improvements
- **#5114** - Remove duplicate semantic labeling from issue templates *(open)*
- Issue template improvements
- **#5102** - [fix] Invoke onRemove callback in LGraphNode.removeWidget method *(merged)*
- Widget system improvements
- **#5099** - Remove PR checks workflows *(merged)*
- CI/CD cleanup
- **#5103** - Update to latest version of workflow icon *(merged)*
- Icon updates affecting stories
- **#5107** - [ci] Add caching support to format and knip commands *(merged)*
- Build optimization improvements
- **#5108** - [refactor] Remove obsolete Kontext Edit Button *(merged)*
- Component cleanup
- **#5110** - [chore] Ignore ./claude/settings.json *(merged)*
- Development environment improvements
- **#5112** - [docs] Update browser tests README *(merged)*
- Testing documentation improvements
- **#4908** - Modal Component & Custom UI Components *(merged)*
- Foundation UI components used in stories
## 🎯 Upcoming Priorities
### High Priority
- [ ] Complete component story coverage for all major UI components
- [ ] Implement comprehensive visual regression testing
- [ ] Improve Storybook build performance and caching
### Medium Priority
- [ ] Add interactive component documentation
- [ ] Enhance theme switching and customization
- [ ] Improve mobile responsiveness of stories
### Low Priority
- [ ] Add more sophisticated mock data patterns
- [ ] Implement component testing automation
- [ ] Explore advanced Storybook addons
## 🔄 How to Contribute
1. **Creating New Stories**: Follow guidelines in `.storybook/README.md` and `.storybook/CLAUDE.md`
2. **Improving Existing Stories**: Use the Storybook Improvement issue template
3. **Documentation**: Update relevant documentation when adding features
4. **Testing**: Ensure all stories build and render correctly
## 📚 Resources
- **Storybook Documentation**: `.storybook/README.md`
- **Developer Guidelines**: `.storybook/CLAUDE.md`
- **Component Examples**: `src/components/*/\*.stories.ts`
- **Visual Testing**: Chromatic integration in CI/CD
---
*This issue is automatically maintained. Please reference this issue number when working on Storybook-related improvements.*

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,293 +0,0 @@
# Settings System
## Overview
ComfyUI frontend uses a comprehensive settings system for user preferences with support for dynamic defaults, version-based rollouts, and environment-aware configuration.
### Settings Architecture
- Settings are defined as `SettingParams` in `src/constants/coreSettings.ts`
- Registered at app startup, loaded/saved via `useSettingStore` (Pinia)
- Persisted per user via backend `/settings` endpoint
- If a value hasn't been set by the user, the store returns the computed default
```typescript
// From src/stores/settingStore.ts:105-122
function getDefaultValue<K extends keyof Settings>(
key: K
): Settings[K] | undefined {
const param = getSettingById(key)
if (param === undefined) return
const versionedDefault = getVersionedDefaultValue(key, param)
if (versionedDefault) {
return versionedDefault
}
return typeof param.defaultValue === 'function'
? param.defaultValue()
: param.defaultValue
}
```
### Settings Registration Process
Settings are registered after server values are loaded:
```typescript
// From src/components/graph/GraphCanvas.vue:311-315
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
})
await newUserService().initializeIfNewUser(settingStore)
```
## Dynamic and Environment-Based Defaults
### Computed Defaults
You can compute defaults dynamically using function defaults that access runtime context:
```typescript
// From src/constants/coreSettings.ts:94-101
{
id: 'Comfy.Sidebar.Size',
// Default to small if the window is less than 1536px(2xl) wide
defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')
}
```
```typescript
// From src/constants/coreSettings.ts:306
{
id: 'Comfy.Locale',
defaultValue: () => navigator.language.split('-')[0] || 'en'
}
```
### Version-Based Defaults
You can vary defaults by installed frontend version using `defaultsByInstallVersion`:
```typescript
// From src/stores/settingStore.ts:129-150
function getVersionedDefaultValue<K extends keyof Settings, TValue = Settings[K]>(
key: K,
param: SettingParams<TValue> | undefined
): TValue | null {
const defaultsByInstallVersion = param?.defaultsByInstallVersion
if (defaultsByInstallVersion && key !== 'Comfy.InstalledVersion') {
const installedVersion = get('Comfy.InstalledVersion')
if (installedVersion) {
const sortedVersions = Object.keys(defaultsByInstallVersion).sort(
(a, b) => compareVersions(b, a)
)
for (const version of sortedVersions) {
if (!isSemVer(version)) continue
if (compareVersions(installedVersion, version) >= 0) {
const versionedDefault = defaultsByInstallVersion[version]
return typeof versionedDefault === 'function'
? versionedDefault()
: versionedDefault
}
}
}
}
return null
}
```
Example versioned defaults from codebase:
```typescript
// From src/constants/coreSettings.ts:38-40
{
id: 'Comfy.Graph.LinkReleaseAction',
defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,
defaultsByInstallVersion: {
'1.24.1': LinkReleaseTriggerAction.SEARCH_BOX
}
}
// Another versioned default example
{
id: 'Comfy.Graph.LinkReleaseAction.Shift',
defaultValue: LinkReleaseTriggerAction.SEARCH_BOX,
defaultsByInstallVersion: {
'1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU
}
}
```
### Real Examples from Codebase
Here are actual settings showing different patterns:
```typescript
// Number setting with validation
{
id: 'LiteGraph.Node.TooltipDelay',
name: 'Tooltip Delay',
type: 'number',
attrs: {
min: 100,
max: 3000,
step: 50
},
defaultValue: 500,
versionAdded: '1.9.0'
}
// Hidden system setting for tracking
{
id: 'Comfy.InstalledVersion',
name: 'The frontend version that was running when the user first installed ComfyUI',
type: 'hidden',
defaultValue: null,
versionAdded: '1.24.0'
}
// Slider with complex tooltip
{
id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',
name: 'Low quality rendering zoom threshold',
tooltip: 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in.',
type: 'slider',
attrs: {
min: 0.1,
max: 1.0,
step: 0.05
},
defaultValue: 0.5
}
```
### New User Version Capture
The initial installed version is captured for new users to ensure versioned defaults remain stable:
```typescript
// From src/services/newUserService.ts:49-53
await settingStore.set(
'Comfy.InstalledVersion',
__COMFYUI_FRONTEND_VERSION__
)
```
## Practical Patterns for Environment-Based Defaults
### Dynamic Default Patterns
```typescript
// Device-based default
{
id: 'Comfy.Example.MobileDefault',
type: 'boolean',
defaultValue: () => /Mobile/i.test(navigator.userAgent)
}
// Environment-based default
{
id: 'Comfy.Example.DevMode',
type: 'boolean',
defaultValue: () => import.meta.env.DEV
}
// Window size based
{
id: 'Comfy.Example.CompactUI',
type: 'boolean',
defaultValue: () => window.innerWidth < 1024
}
```
### Version-Based Rollout Pattern
```typescript
{
id: 'Comfy.Example.NewFeature',
type: 'combo',
options: ['legacy', 'enhanced'],
defaultValue: 'legacy',
defaultsByInstallVersion: {
'1.25.0': 'enhanced'
}
}
```
## Settings Persistence and Access
### API Interaction
Values are stored per user via the backend. The store writes through API and falls back to defaults when not set:
```typescript
// From src/stores/settingStore.ts:73-75
onChange(settingsById.value[key], newValue, oldValue)
settingValues.value[key] = newValue
await api.storeSetting(key, newValue)
```
### Usage in Components
```typescript
const settingStore = useSettingStore()
// Get setting value (returns computed default if not set by user)
const value = settingStore.get('Comfy.SomeSetting')
// Update setting value
await settingStore.set('Comfy.SomeSetting', newValue)
```
## Advanced Settings Features
### Migration and Backward Compatibility
Settings support migration from deprecated values:
```typescript
// From src/stores/settingStore.ts:68-69, 172-175
const newValue = tryMigrateDeprecatedValue(
settingsById.value[key],
clonedValue
)
// Migration happens during addSetting for existing values:
if (settingValues.value[setting.id] !== undefined) {
settingValues.value[setting.id] = tryMigrateDeprecatedValue(
setting,
settingValues.value[setting.id]
)
}
```
### onChange Callbacks
Settings can define onChange callbacks that receive the setting definition, new value, and old value:
```typescript
// From src/stores/settingStore.ts:73, 177
onChange(settingsById.value[key], newValue, oldValue) // During set()
onChange(setting, get(setting.id), undefined) // During addSetting()
```
### Settings UI and Categories
Settings are automatically grouped for UI based on their `category` or derived from `id`:
```typescript
{
id: 'Comfy.Sidebar.Size',
category: ['Appearance', 'Sidebar', 'Size'],
// UI will group this under Appearance > Sidebar > Size
}
```
## Related Documentation
- Feature flag system: `docs/FEATURE_FLAGS.md`
- Settings schema for backend: `src/schemas/apiSchema.ts` (zSettings)
- Server configuration (separate from user settings): `src/constants/serverConfig.ts`
## Summary
- **Settings**: User preferences with dynamic/versioned defaults, persisted per user
- **Environment Defaults**: Use function defaults to read runtime context (window, navigator, env)
- **Version Rollouts**: Use `defaultsByInstallVersion` for gradual feature releases
- **API Interaction**: Settings persist to `/settings` endpoint via `storeSetting()`

View File

@@ -1,82 +0,0 @@
# Settings and Feature Flags Sequence Diagram
This diagram shows the flow of settings initialization, default resolution, persistence, and feature flags exchange.
This diagram accurately reflects the actual implementation in the ComfyUI frontend codebase.
```mermaid
sequenceDiagram
participant User as User
participant Vue as Vue Component
participant Store as SettingStore (Pinia)
participant API as ComfyApi (WebSocket/REST)
participant Backend as Backend
participant NewUserSvc as NewUserService
Note over Vue,Store: App startup (GraphCanvas.vue)
Vue->>Store: loadSettingValues()
Store->>API: getSettings()
API->>Backend: GET /settings
Backend-->>API: settings map (per-user)
API-->>Store: settings map
Store-->>Vue: loaded
Vue->>Store: register CORE_SETTINGS (addSetting for each)
loop For each setting registration
Store->>Store: tryMigrateDeprecatedValue(existing value)
Store->>Store: onChange(setting, currentValue, undefined)
end
Note over Vue,NewUserSvc: New user detection
Vue->>NewUserSvc: initializeIfNewUser(settingStore)
NewUserSvc->>NewUserSvc: checkIsNewUser(settingStore)
alt New user detected
NewUserSvc->>Store: set("Comfy.InstalledVersion", __COMFYUI_FRONTEND_VERSION__)
Store->>Store: tryMigrateDeprecatedValue(newValue)
Store->>Store: onChange(setting, newValue, oldValue)
Store->>API: storeSetting(key, newValue)
API->>Backend: POST /settings/{id}
else Existing user
Note over NewUserSvc: Skip setting installed version
end
Note over Vue,Store: Component reads a setting
Vue->>Store: get(key)
Store->>Store: exists(key)?
alt User value exists
Store-->>Vue: return stored user value
else Not set by user
Store->>Store: getVersionedDefaultValue(key)
alt Versioned default matched (defaultsByInstallVersion)
Store-->>Vue: return versioned default
else No version match
Store->>Store: evaluate defaultValue (function or constant)
Note over Store: defaultValue can use window size,<br/>locale, env, etc.
Store-->>Vue: return computed default
end
end
Note over User,Store: User updates a setting
User->>Vue: changes setting in UI
Vue->>Store: set(key, newValue)
Store->>Store: tryMigrateDeprecatedValue(newValue)
Store->>Store: check if newValue === oldValue (early return if same)
Store->>Store: onChange(setting, newValue, oldValue)
Store->>Store: update settingValues[key]
Store->>API: storeSetting(key, newValue)
API->>Backend: POST /settings/{id}
Backend-->>API: 200 OK
API-->>Store: ack
Note over API,Backend: Feature Flags WebSocket Exchange
API->>Backend: WS connect
API->>Backend: send { type: "feature_flags", data: clientFeatureFlags.json }
Backend-->>API: WS send { type: "feature_flags", data: server flags }
API->>API: store serverFeatureFlags = data
Note over Vue,API: Feature flag consumption in UI/logic
Vue->>API: serverSupportsFeature(name)
API-->>Vue: boolean (true only if flag === true)
Vue->>API: getServerFeature(name, default)
API-->>Vue: value or default
```

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:

View File

@@ -17,10 +17,9 @@ export default [
'src/scripts/*',
'src/extensions/core/*',
'src/types/vue-shim.d.ts',
// Generated files that don't need linting
'src/types/comfyRegistryTypes.ts',
'src/types/generatedManagerTypes.ts',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*'
'src/types/generatedManagerTypes.ts'
]
},
{

View File

@@ -74,7 +74,7 @@ const config: KnipConfig = {
// Workspace configuration for monorepo-like structure
workspaces: {
'.': {
entry: ['src/main.ts', 'playwright.i18n.config.ts']
entry: ['src/main.ts']
}
}
}

40
nx.json
View File

@@ -1,40 +0,0 @@
{
"$schema": "./node_modules/nx/schemas/nx-schema.json",
"plugins": [
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
}
},
{
"plugin": "@nx/storybook/plugin",
"options": {
"serveStorybookTargetName": "storybook",
"buildStorybookTargetName": "build-storybook",
"testStorybookTargetName": "test-storybook",
"staticStorybookTargetName": "static-storybook"
}
},
{
"plugin": "@nx/vite/plugin",
"options": {
"buildTargetName": "build",
"testTargetName": "test",
"serveTargetName": "serve",
"devTargetName": "dev",
"previewTargetName": "preview",
"serveStaticTargetName": "serve-static",
"typecheckTargetName": "typecheck",
"buildDepsTargetName": "build-deps",
"watchDepsTargetName": "watch-deps"
}
},
{
"plugin": "@nx/playwright/plugin",
"options": {
"targetName": "e2e"
}
}
]
}

19426
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,29 +1,28 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.26.7",
"version": "1.26.5",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
"description": "Official front-end implementation of ComfyUI",
"license": "GPL-3.0-only",
"scripts": {
"dev": "nx serve",
"dev:electron": "nx serve --config vite.electron.config.mts",
"build": "pnpm typecheck && nx build",
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
"dev": "vite",
"dev:electron": "vite --config vite.electron.config.mts",
"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",
"format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache",
"format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
"test:browser": "npx nx e2e",
"test:unit": "nx run test tests-ui/tests",
"test:component": "nx run test src/components/",
"preinstall": "npx only-allow pnpm",
"test:browser": "npx playwright test",
"test:unit": "vitest run tests-ui/tests",
"test:component": "vitest run src/components/",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
"preview": "vite preview",
"lint": "eslint src --cache",
"lint:fix": "eslint src --cache --fix",
"lint:no-cache": "eslint src",
@@ -31,27 +30,20 @@
"knip": "knip --cache",
"knip:no-cache": "knip",
"locale": "lobe-i18n locale",
"collect-i18n": "npx playwright test --config=playwright.i18n.config.ts",
"collect-i18n": "playwright test --config=playwright.i18n.config.ts",
"json-schema": "tsx scripts/generate-json-schema.ts",
"storybook": "nx storybook -p 6006",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"devDependencies": {
"@eslint/js": "^9.8.0",
"@executeautomation/playwright-mcp-server": "^1.0.6",
"@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.25.1",
"@nx/eslint": "21.4.1",
"@nx/playwright": "21.4.1",
"@nx/storybook": "21.4.1",
"@nx/vite": "21.4.1",
"@nx/web": "21.4.1",
"@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",
@@ -60,11 +52,9 @@
"@types/semver": "^7.7.0",
"@types/three": "^0.169.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vitest/ui": "^3.0.0",
"@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",
@@ -76,16 +66,10 @@
"happy-dom": "^15.11.0",
"husky": "^9.0.11",
"identity-obj-proxy": "^3.0.0",
"ink": "^6.2.2",
"jiti": "2.4.2",
"knip": "^5.62.0",
"lint-staged": "^15.2.7",
"lucide-vue-next": "^0.540.0",
"nx": "21.4.1",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"react": "^19.1.1",
"react-reconciler": "^0.32.0",
"storybook": "^9.1.1",
"tailwindcss": "^3.4.4",
"tsx": "^4.15.6",
@@ -93,12 +77,11 @@
"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",
"vite-plugin-vue-devtools": "^7.7.6",
"vitest": "^3.2.4",
"vitest": "^2.0.0",
"vue-tsc": "^2.1.10",
"zip-dir": "^2.0.0",
"zod-to-json-schema": "^3.24.1"
@@ -107,14 +90,8 @@
"@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",
@@ -134,10 +111,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

@@ -1,31 +1,39 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './browser_tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
reporter: 'html',
// /* // Toggle for [LOCAL] testing.
/* Retry on CI only - increased for better flaky test handling */
retries: process.env.CI ? 3 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry'
},
/*/ // [LOCAL]
// VERY HELPFUL: Skip screenshot tests locally
// grep: process.env.CI ? undefined : /^(?!.*screenshot).*$/,
timeout: 30_000, // Longer timeout for breakpoints
retries: 0, // No retries while debugging. Increase if writing new tests. that may be flaky.
workers: 4, // Single worker for easier debugging. Increase to match CPU cores if you want to run a lot of tests in parallel.
use: {
trace: 'on', // Always capture traces (CI uses 'on-first-retry')
video: 'on' // Always record video (CI uses 'retain-on-failure')
},
//*/
/* Path to global setup file. Exported function runs once before all the tests */
globalSetup: './browser_tests/globalSetup.ts',
/* Path to global teardown file. Exported function runs once after all the tests */
globalTeardown: './browser_tests/globalTeardown.ts',
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
@@ -82,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,
// },
})

View File

@@ -1,6 +1,6 @@
import { defineConfig } from '@playwright/test'
import { PlaywrightTestConfig } from '@playwright/test'
export default defineConfig({
const config: PlaywrightTestConfig = {
testDir: './scripts',
use: {
baseURL: 'http://localhost:5173',
@@ -9,4 +9,6 @@ export default defineConfig({
reporter: 'list',
timeout: 60000,
testMatch: /collect-i18n-.*\.ts/
})
}
export default config

14239
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
packages:
- apps/**
- packages/**
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs
- vue-demi
onlyBuiltDependencies:
- '@playwright/browser-chromium'
- '@playwright/browser-firefox'
- '@playwright/browser-webkit'
- esbuild
- nx
- 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

@@ -1,145 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { Bell, Download, Heart, Settings, Trophy, X } from 'lucide-vue-next'
import IconButton from './IconButton.vue'
const meta: Meta<typeof IconButton> = {
title: 'Components/Button/IconButton',
component: IconButton,
tags: ['autodocs'],
argTypes: {
size: {
control: { type: 'select' },
options: ['sm', 'md']
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
render: (args) => ({
components: { IconButton, Trophy },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<Trophy :size="16" />
</IconButton>
`
}),
args: {
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
render: (args) => ({
components: { IconButton, Settings },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<Settings :size="16" />
</IconButton>
`
}),
args: {
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
render: (args) => ({
components: { IconButton, X },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<X :size="16" />
</IconButton>
`
}),
args: {
type: 'transparent',
size: 'md'
}
}
export const Small: Story = {
render: (args) => ({
components: { IconButton, Bell },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<Bell :size="12" />
</IconButton>
`
}),
args: {
type: 'secondary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: { IconButton, Trophy, Settings, X, Bell, Heart, Download },
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<IconButton type="primary" size="sm" @click="() => {}">
<Trophy :size="12" />
</IconButton>
<IconButton type="primary" size="md" @click="() => {}">
<Trophy :size="16" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="secondary" size="sm" @click="() => {}">
<Settings :size="12" />
</IconButton>
<IconButton type="secondary" size="md" @click="() => {}">
<Settings :size="16" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="transparent" size="sm" @click="() => {}">
<X :size="12" />
</IconButton>
<IconButton type="transparent" size="md" @click="() => {}">
<X :size="16" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="primary" size="md" @click="() => {}">
<Bell :size="16" />
</IconButton>
<IconButton type="secondary" size="md" @click="() => {}">
<Heart :size="16" />
</IconButton>
<IconButton type="transparent" size="md" @click="() => {}">
<Download :size="16" />
</IconButton>
</div>
</div>
`
}),
parameters: {
controls: { disable: true },
actions: { disable: true }
}
}

View File

@@ -1,35 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { Download, ExternalLink, Heart } from 'lucide-vue-next'
import IconButton from './IconButton.vue'
import IconGroup from './IconGroup.vue'
const meta: Meta<typeof IconGroup> = {
title: 'Components/Button/IconGroup',
component: IconGroup,
parameters: {
layout: 'centered'
}
}
export default meta
type Story = StoryObj<typeof IconGroup>
export const Basic: Story = {
render: () => ({
components: { IconGroup, IconButton, Download, ExternalLink, Heart },
template: `
<IconGroup>
<IconButton @click="console.log('Hello World!!')">
<Heart :size="16" />
</IconButton>
<IconButton @click="console.log('Hello World!!')">
<Download :size="16" />
</IconButton>
<IconButton @click="console.log('Hello World!!')">
<ExternalLink :size="16" />
</IconButton>
</IconGroup>
`
})
}

View File

@@ -1,221 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import {
ChevronLeft,
ChevronRight,
Download,
Package,
Save,
Settings,
Trash2,
X
} from 'lucide-vue-next'
import IconTextButton from './IconTextButton.vue'
const meta: Meta<typeof IconTextButton> = {
title: 'Components/Button/IconTextButton',
component: IconTextButton,
tags: ['autodocs'],
argTypes: {
label: {
control: 'text'
},
size: {
control: { type: 'select' },
options: ['sm', 'md']
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
iconPosition: {
control: { type: 'select' },
options: ['left', 'right']
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
render: (args) => ({
components: { IconTextButton, Package },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<Package :size="16" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Deploy',
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
render: (args) => ({
components: { IconTextButton, Settings },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<Settings :size="16" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Settings',
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
render: (args) => ({
components: { IconTextButton, X },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<X :size="16" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Cancel',
type: 'transparent',
size: 'md'
}
}
export const WithIconRight: Story = {
render: (args) => ({
components: { IconTextButton, ChevronRight },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<ChevronRight :size="16" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Next',
type: 'primary',
size: 'md',
iconPosition: 'right'
}
}
export const Small: Story = {
render: (args) => ({
components: { IconTextButton, Save },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<Save :size="12" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Save',
type: 'primary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: {
IconTextButton,
Download,
Settings,
Trash2,
ChevronRight,
ChevronLeft,
Save
},
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<IconTextButton label="Download" type="primary" size="sm" @click="() => {}">
<template #icon>
<Download :size="12" />
</template>
</IconTextButton>
<IconTextButton label="Download" type="primary" size="md" @click="() => {}">
<template #icon>
<Download :size="16" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Settings" type="secondary" size="sm" @click="() => {}">
<template #icon>
<Settings :size="12" />
</template>
</IconTextButton>
<IconTextButton label="Settings" type="secondary" size="md" @click="() => {}">
<template #icon>
<Settings :size="16" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Delete" type="transparent" size="sm" @click="() => {}">
<template #icon>
<Trash2 :size="12" />
</template>
</IconTextButton>
<IconTextButton label="Delete" type="transparent" size="md" @click="() => {}">
<template #icon>
<Trash2 :size="16" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Next" type="primary" size="md" iconPosition="right" @click="() => {}">
<template #icon>
<ChevronRight :size="16" />
</template>
</IconTextButton>
<IconTextButton label="Previous" type="secondary" size="md" @click="() => {}">
<template #icon>
<ChevronLeft :size="16" />
</template>
</IconTextButton>
<IconTextButton label="Save File" type="primary" size="md" @click="() => {}">
<template #icon>
<Save :size="16" />
</template>
</IconTextButton>
</div>
</div>
`
}),
parameters: {
controls: { disable: true },
actions: { disable: true }
}
}

View File

@@ -1,50 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { Download, ScrollText } from 'lucide-vue-next'
import IconTextButton from './IconTextButton.vue'
import MoreButton from './MoreButton.vue'
const meta: Meta<typeof MoreButton> = {
title: 'Components/Button/MoreButton',
component: MoreButton,
parameters: {
layout: 'centered'
},
argTypes: {}
}
export default meta
type Story = StoryObj<typeof MoreButton>
export const Basic: Story = {
render: () => ({
components: { MoreButton, IconTextButton, Download, ScrollText },
template: `
<div style="height: 200px; display: flex; align-items: center; justify-content: center;">
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="transparent"
label="Settings"
@click="() => { close() }"
>
<template #icon>
<Download :size="16" />
</template>
</IconTextButton>
<IconTextButton
type="transparent"
label="Profile"
@click="() => { close() }"
>
<template #icon>
<ScrollText :size="16" />
</template>
</IconTextButton>
</template>
</MoreButton>
</div>
`
})
}

View File

@@ -1,83 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import TextButton from './TextButton.vue'
const meta: Meta<typeof TextButton> = {
title: 'Components/Button/TextButton',
component: TextButton,
tags: ['autodocs'],
argTypes: {
label: {
control: 'text',
defaultValue: 'Click me'
},
size: {
control: { type: 'select' },
options: ['sm', 'md'],
defaultValue: 'md'
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent'],
defaultValue: 'primary'
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {
label: 'Primary Button',
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
args: {
label: 'Secondary Button',
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
args: {
label: 'Transparent Button',
type: 'transparent',
size: 'md'
}
}
export const Small: Story = {
args: {
label: 'Small Button',
type: 'primary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: { TextButton },
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<TextButton label="Primary Small" type="primary" size="sm" @click="() => {}" />
<TextButton label="Primary Medium" type="primary" size="md" @click="() => {}" />
</div>
<div class="flex gap-2 items-center">
<TextButton label="Secondary Small" type="secondary" size="sm" @click="() => {}" />
<TextButton label="Secondary Medium" type="secondary" size="md" @click="() => {}" />
</div>
<div class="flex gap-2 items-center">
<TextButton label="Transparent Small" type="transparent" size="sm" @click="() => {}" />
<TextButton label="Transparent Medium" type="transparent" size="md" @click="() => {}" />
</div>
</div>
`
})
}

View File

@@ -1,665 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import {
Download,
Folder,
Heart,
Info,
MoreVertical,
Star,
Upload
} from 'lucide-vue-next'
import { ref } from 'vue'
import IconButton from '../button/IconButton.vue'
import SquareChip from '../chip/SquareChip.vue'
import CardBottom from './CardBottom.vue'
import CardContainer from './CardContainer.vue'
import CardDescription from './CardDescription.vue'
import CardTitle from './CardTitle.vue'
import CardTop from './CardTop.vue'
interface CardStoryArgs {
// CardContainer props
containerRatio: 'square' | 'portrait' | 'tallPortrait'
maxWidth: number
minWidth: number
// CardTop props
topRatio: 'square' | 'landscape'
// Content props
showTopLeft: boolean
showTopRight: boolean
showBottomLeft: boolean
showBottomRight: boolean
showTitle: boolean
showDescription: boolean
title: string
description: string
// Visual props
backgroundColor: string
showImage: boolean
imageUrl: string
// Tag props
tags: string[]
showFileSize: boolean
fileSize: string
showFileType: boolean
fileType: string
}
const meta: Meta<CardStoryArgs> = {
title: 'Components/Card/Card',
argTypes: {
containerRatio: {
control: 'select',
options: ['square', 'portrait', 'tallPortrait'],
description: 'Card container aspect ratio'
},
maxWidth: {
control: { type: 'range', min: 200, max: 600, step: 10 },
description: 'Maximum width in pixels'
},
minWidth: {
control: { type: 'range', min: 150, max: 400, step: 10 },
description: 'Minimum width in pixels'
},
topRatio: {
control: 'select',
options: ['square', 'landscape'],
description: 'Top section aspect ratio'
},
showTopLeft: {
control: 'boolean',
description: 'Show top-left slot content'
},
showTopRight: {
control: 'boolean',
description: 'Show top-right slot content'
},
showBottomLeft: {
control: 'boolean',
description: 'Show bottom-left slot content'
},
showBottomRight: {
control: 'boolean',
description: 'Show bottom-right slot content'
},
showTitle: {
control: 'boolean',
description: 'Show card title'
},
showDescription: {
control: 'boolean',
description: 'Show card description'
},
title: {
control: 'text',
description: 'Card title text'
},
description: {
control: 'text',
description: 'Card description text'
},
backgroundColor: {
control: 'color',
description: 'Background color for card top'
},
showImage: {
control: 'boolean',
description: 'Show image instead of color background'
},
imageUrl: {
control: 'text',
description: 'Image URL for card top'
},
tags: {
control: 'object',
description: 'Tags to display (array of strings)'
},
showFileSize: {
control: 'boolean',
description: 'Show file size tag'
},
fileSize: {
control: 'text',
description: 'File size text'
},
showFileType: {
control: 'boolean',
description: 'Show file type tag'
},
fileType: {
control: 'text',
description: 'File type text'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
const createCardTemplate = (args: CardStoryArgs) => ({
components: {
CardContainer,
CardTop,
CardBottom,
CardTitle,
CardDescription,
IconButton,
SquareChip,
Info,
Folder,
Heart,
Download,
Star,
Upload,
MoreVertical
},
setup() {
const favorited = ref(false)
const toggleFavorite = () => {
favorited.value = !favorited.value
}
return {
args,
favorited,
toggleFavorite
}
},
template: `
<div class="p-4 min-h-screen bg-zinc-50 dark-theme:bg-zinc-900">
<CardContainer
:ratio="args.containerRatio"
:max-width="args.maxWidth"
:min-width="args.minWidth"
>
<template #top>
<CardTop :ratio="args.topRatio">
<template #default>
<div
v-if="!args.showImage"
class="w-full h-full"
:style="{ backgroundColor: args.backgroundColor }"
></div>
<img
v-else
:src="args.imageUrl || 'https://via.placeholder.com/400'"
class="w-full h-full object-cover"
alt="Card image"
/>
</template>
<template v-if="args.showTopLeft" #top-left>
<SquareChip label="Featured" />
</template>
<template v-if="args.showTopRight" #top-right>
<IconButton
class="!bg-white/90 !text-neutral-900"
@click="() => console.log('Info clicked')"
>
<Info :size="16" />
</IconButton>
<IconButton
class="!bg-white/90"
:class="favorited ? '!text-red-500' : '!text-neutral-900'"
@click="toggleFavorite"
>
<Heart :size="16" :fill="favorited ? 'currentColor' : 'none'" />
</IconButton>
</template>
<template v-if="args.showBottomLeft" #bottom-left>
<SquareChip label="New" />
</template>
<template v-if="args.showBottomRight" #bottom-right>
<SquareChip v-if="args.showFileType" :label="args.fileType" />
<SquareChip v-if="args.showFileSize" :label="args.fileSize" />
<SquareChip v-for="tag in args.tags" :key="tag" :label="tag">
<template v-if="tag === 'LoRA'" #icon>
<Folder :size="12" />
</template>
</SquareChip>
</template>
</CardTop>
</template>
<template #bottom>
<CardBottom class="p-3">
<CardTitle v-if="args.showTitle">{{ args.title }}</CardTitle>
<CardDescription v-if="args.showDescription">{{ args.description }}</CardDescription>
</CardBottom>
</template>
</CardContainer>
</div>
`
})
export const Default: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'portrait',
maxWidth: 300,
minWidth: 200,
topRatio: 'square',
showTopLeft: false,
showTopRight: true,
showBottomLeft: false,
showBottomRight: true,
showTitle: true,
showDescription: true,
title: 'Model Name',
description:
'This is a detailed description of the model that can span multiple lines',
backgroundColor: '#3b82f6',
showImage: false,
imageUrl: '',
tags: ['LoRA', 'SDXL'],
showFileSize: true,
fileSize: '1.2 MB',
showFileType: true,
fileType: 'safetensors'
}
}
export const SquareCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'square',
maxWidth: 400,
minWidth: 250,
topRatio: 'landscape',
showTopLeft: false,
showTopRight: true,
showBottomLeft: false,
showBottomRight: true,
showTitle: true,
showDescription: true,
title: 'Workflow Bundle',
description:
'Complete workflow for image generation with all necessary nodes',
backgroundColor: '#10b981',
showImage: false,
imageUrl: '',
tags: ['Workflow'],
showFileSize: true,
fileSize: '245 KB',
showFileType: true,
fileType: 'json'
}
}
export const TallPortraitCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'tallPortrait',
maxWidth: 280,
minWidth: 180,
topRatio: 'square',
showTopLeft: true,
showTopRight: true,
showBottomLeft: false,
showBottomRight: true,
showTitle: true,
showDescription: true,
title: 'Premium Model',
description:
'High-quality photorealistic model trained on professional photography',
backgroundColor: '#8b5cf6',
showImage: false,
imageUrl: '',
tags: ['SD 1.5', 'Checkpoint'],
showFileSize: true,
fileSize: '2.1 GB',
showFileType: true,
fileType: 'ckpt'
}
}
export const ImageCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'portrait',
maxWidth: 350,
minWidth: 220,
topRatio: 'square',
showTopLeft: false,
showTopRight: true,
showBottomLeft: false,
showBottomRight: true,
showTitle: true,
showDescription: true,
title: 'Generated Image',
description: 'Created with DreamShaper XL',
backgroundColor: '#3b82f6',
showImage: true,
imageUrl: 'https://picsum.photos/400/400',
tags: ['Output'],
showFileSize: true,
fileSize: '856 KB',
showFileType: true,
fileType: 'png'
}
}
export const MinimalCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'square',
maxWidth: 300,
minWidth: 200,
topRatio: 'landscape',
showTopLeft: false,
showTopRight: false,
showBottomLeft: false,
showBottomRight: false,
showTitle: true,
showDescription: false,
title: 'Simple Card',
description: '',
backgroundColor: '#64748b',
showImage: false,
imageUrl: '',
tags: [],
showFileSize: false,
fileSize: '',
showFileType: false,
fileType: ''
}
}
export const FullFeaturedCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'tallPortrait',
maxWidth: 320,
minWidth: 240,
topRatio: 'square',
showTopLeft: true,
showTopRight: true,
showBottomLeft: true,
showBottomRight: true,
showTitle: true,
showDescription: true,
title: 'Ultimate Model Pack',
description:
'Complete collection with checkpoints, LoRAs, embeddings, and VAE models for professional use',
backgroundColor: '#ef4444',
showImage: false,
imageUrl: '',
tags: ['Bundle', 'Premium', 'SDXL'],
showFileSize: true,
fileSize: '5.4 GB',
showFileType: true,
fileType: 'pack'
}
}
export const GridOfCards: Story = {
render: () => ({
components: {
CardContainer,
CardTop,
CardBottom,
CardTitle,
CardDescription,
IconButton,
SquareChip,
Info,
Folder,
Heart,
Download
},
setup() {
const cards = ref([
{
id: 1,
title: 'Realistic Vision',
description: 'Photorealistic model for portraits',
color: 'from-blue-400 to-blue-600',
ratio: 'portrait' as const,
tags: ['SD 1.5'],
size: '2.1 GB'
},
{
id: 2,
title: 'DreamShaper XL',
description: 'Artistic style model with enhanced details',
color: 'from-purple-400 to-pink-600',
ratio: 'portrait' as const,
tags: ['SDXL'],
size: '6.5 GB'
},
{
id: 3,
title: 'Anime LoRA',
description: 'Character style LoRA',
color: 'from-green-400 to-teal-600',
ratio: 'portrait' as const,
tags: ['LoRA'],
size: '144 MB'
},
{
id: 4,
title: 'VAE Model',
description: 'Enhanced color VAE',
color: 'from-orange-400 to-red-600',
ratio: 'portrait' as const,
tags: ['VAE'],
size: '335 MB'
},
{
id: 5,
title: 'Workflow Bundle',
description: 'Complete workflow setup',
color: 'from-indigo-400 to-blue-600',
ratio: 'portrait' as const,
tags: ['Workflow'],
size: '45 KB'
},
{
id: 6,
title: 'Embedding Pack',
description: 'Negative embeddings collection',
color: 'from-yellow-400 to-orange-600',
ratio: 'portrait' as const,
tags: ['Embedding'],
size: '2.3 MB'
}
])
return { cards }
},
template: `
<div class="p-4 min-h-screen bg-zinc-50 dark-theme:bg-zinc-900">
<h3 class="text-lg font-semibold mb-4 text-neutral-900 dark-theme:text-neutral-100">Model Gallery</h3>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
<CardContainer
v-for="card in cards"
:key="card.id"
:ratio="card.ratio"
:max-width="300"
:min-width="180"
>
<template #top>
<CardTop ratio="square">
<template #default>
<div
class="w-full h-full bg-gray-600"
:class="card.color"
></div>
</template>
<template #top-right>
<IconButton
class="!bg-white/90 !text-neutral-900"
@click="() => console.log('Info:', card.title)"
>
<Info :size="16" />
</IconButton>
</template>
<template #bottom-right>
<SquareChip
v-for="tag in card.tags"
:key="tag"
:label="tag"
>
<template v-if="tag === 'LoRA'" #icon>
<Folder :size="12" />
</template>
</SquareChip>
<SquareChip :label="card.size" />
</template>
</CardTop>
</template>
<template #bottom>
<CardBottom class="p-3">
<CardTitle>{{ card.title }}</CardTitle>
<CardDescription>{{ card.description }}</CardDescription>
</CardBottom>
</template>
</CardContainer>
</div>
</div>
`
})
}
export const ResponsiveGrid: Story = {
render: () => ({
components: {
CardContainer,
CardTop,
CardBottom,
CardTitle,
CardDescription,
SquareChip
},
setup() {
const generateCards = (
count: number,
ratio: 'square' | 'portrait' | 'tallPortrait'
) => {
return Array.from({ length: count }, (_, i) => ({
id: i + 1,
title: `Model ${i + 1}`,
description: `Description for model ${i + 1}`,
ratio,
color: `hsl(${(i * 60) % 360}, 70%, 60%)`
}))
}
const squareCards = ref(generateCards(4, 'square'))
const portraitCards = ref(generateCards(6, 'portrait'))
const tallCards = ref(generateCards(5, 'tallPortrait'))
return {
squareCards,
portraitCards,
tallCards
}
},
template: `
<div class="p-4 space-y-8 min-h-screen bg-zinc-50 dark-theme:bg-zinc-900">
<div>
<h3 class="text-lg font-semibold mb-4 text-neutral-900 dark-theme:text-neutral-100">Square Cards (1:1)</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<CardContainer
v-for="card in squareCards"
:key="card.id"
:ratio="card.ratio"
:max-width="400"
:min-width="200"
>
<template #top>
<CardTop ratio="landscape">
<div
class="w-full h-full"
:style="{ backgroundColor: card.color }"
></div>
</CardTop>
</template>
<template #bottom>
<CardBottom class="p-3">
<CardTitle>{{ card.title }}</CardTitle>
<CardDescription>{{ card.description }}</CardDescription>
</CardBottom>
</template>
</CardContainer>
</div>
</div>
<div>
<h3 class="text-lg font-semibold mb-4 text-neutral-900 dark-theme:text-neutral-100">Portrait Cards (2:3)</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
<CardContainer
v-for="card in portraitCards"
:key="card.id"
:ratio="card.ratio"
:max-width="280"
:min-width="160"
>
<template #top>
<CardTop ratio="square">
<div
class="w-full h-full"
:style="{ backgroundColor: card.color }"
></div>
</CardTop>
</template>
<template #bottom>
<CardBottom class="p-2">
<CardTitle>{{ card.title }}</CardTitle>
</CardBottom>
</template>
</CardContainer>
</div>
</div>
<div>
<h3 class="text-lg font-semibold mb-4 text-neutral-900 dark-theme:text-neutral-100">Tall Portrait Cards (2:4)</h3>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<CardContainer
v-for="card in tallCards"
:key="card.id"
:ratio="card.ratio"
:max-width="260"
:min-width="150"
>
<template #top>
<CardTop ratio="square">
<template #default>
<div
class="w-full h-full"
:style="{ backgroundColor: card.color }"
></div>
</template>
<template #bottom-right>
<SquareChip :label="'#' + card.id" />
</template>
</CardTop>
</template>
<template #bottom>
<CardBottom class="p-3">
<CardTitle>{{ card.title }}</CardTitle>
<CardDescription>{{ card.description }}</CardDescription>
</CardBottom>
</template>
</CardContainer>
</div>
</div>
</div>
`
}),
parameters: {
controls: { disable: true },
actions: { disable: true }
}
}

View File

@@ -13,8 +13,8 @@ const {
maxWidth,
minWidth
} = defineProps<{
maxWidth?: number
minWidth?: number
maxWidth: number
minWidth: number
ratio?: 'square' | 'portrait' | 'tallPortrait'
}>()
@@ -31,12 +31,8 @@ const containerClasses = computed(() => {
return `${baseClasses} ${ratioClasses[ratio]}`
})
const containerStyle = computed(() =>
maxWidth || minWidth
? {
maxWidth: `${maxWidth}px`,
minWidth: `${minWidth}px`
}
: {}
)
const containerStyle = computed(() => ({
maxWidth: `${maxWidth}px`,
minWidth: `${minWidth}px`
}))
</script>

View File

@@ -1,36 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import SquareChip from './SquareChip.vue'
const meta: Meta<typeof SquareChip> = {
title: 'Components/SquareChip',
component: SquareChip,
tags: ['autodocs'],
argTypes: {
label: {
control: 'text',
defaultValue: 'Tag'
}
}
}
export default meta
type Story = StoryObj<typeof meta>
export const TagList: Story = {
render: () => ({
components: { SquareChip },
template: `
<div class="flex flex-wrap gap-2">
<SquareChip label="JavaScript" />
<SquareChip label="TypeScript" />
<SquareChip label="Vue.js" />
<SquareChip label="React" />
<SquareChip label="Node.js" />
<SquareChip label="Python" />
<SquareChip label="Docker" />
<SquareChip label="Kubernetes" />
</div>
`
})
}

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

@@ -57,23 +57,14 @@
class="w-8 h-8 mt-4"
style="--pc-spinner-color: #000"
/>
<div v-else class="mt-4 flex flex-col gap-2">
<Button
class="w-32"
severity="secondary"
:label="$t('auth.signOut.signOut')"
icon="pi pi-sign-out"
@click="handleSignOut"
/>
<Button
v-if="!isApiKeyLogin"
class="w-32"
severity="danger"
:label="$t('auth.deleteAccount.deleteAccount')"
icon="pi pi-trash"
@click="handleDeleteAccount"
/>
</div>
<Button
v-else
class="mt-4 w-32"
severity="secondary"
:label="$t('auth.signOut.signOut')"
icon="pi pi-sign-out"
@click="handleSignOut"
/>
</div>
<!-- Login Section -->
@@ -109,7 +100,6 @@ const dialogService = useDialogService()
const {
loading,
isLoggedIn,
isApiKeyLogin,
isEmailProvider,
userDisplayName,
userEmail,
@@ -117,7 +107,6 @@ const {
providerName,
providerIcon,
handleSignOut,
handleSignIn,
handleDeleteAccount
handleSignIn
} = useCurrentUser()
</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>

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