fix: resolve merge conflict in knip.config.ts

Merged both sets of ignore entries from sno-deploy-ghpage and main branches.
This commit is contained in:
snomiao
2025-10-20 18:42:19 +00:00
215 changed files with 5786 additions and 1403 deletions

View File

@@ -458,15 +458,15 @@ echo "Workflow triggered. Waiting for PR creation..."
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
```bash
sleep 10
gh run list --workflow=release.yaml --limit=1
gh run list --workflow=release-draft-create.yaml --limit=1
```
4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
- Create a `core/x.yy` branch for the PREVIOUS minor version
- Apply branch protection rules
- Document the feature freeze policy
```bash
# Monitor branch creation (for minor/major releases)
gh run list --workflow=create-release-candidate-branch.yaml --limit=1
gh run list --workflow=release-branch-create.yaml --limit=1
```
4. If workflow didn't trigger due to [skip ci]:
```bash
@@ -477,7 +477,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```
5. If workflow triggered, monitor execution:
```bash
WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
WORKFLOW_RUN_ID=$(gh run list --workflow=release-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
gh run watch ${WORKFLOW_RUN_ID}
```

View File

@@ -246,7 +246,7 @@ For each commit:
3. Merge the PR: `gh pr merge --merge`
4. Monitor release workflow:
```bash
gh run list --workflow=release.yaml --limit=1
gh run list --workflow=release-draft-create.yaml --limit=1
gh run watch
```
5. Track progress:

21
.github/workflows/README.md vendored Normal file
View File

@@ -0,0 +1,21 @@
# GitHub Workflows
## Naming Convention
Workflow files follow a consistent naming pattern: `<prefix>-<descriptive-name>.yaml`
### Category Prefixes
| Prefix | Purpose | Example |
| ---------- | ----------------------------------- | ------------------------------------ |
| `ci-` | Testing, linting, validation | `ci-tests-e2e.yaml` |
| `release-` | Version management, publishing | `release-version-bump.yaml` |
| `pr-` | PR automation (triggered by labels) | `pr-claude-review.yaml` |
| `api-` | External Api type generation | `api-update-registry-api-types.yaml` |
| `i18n-` | Internationalization updates | `i18n-update-core.yaml` |
## Documentation
Each workflow file contains comments explaining its purpose, triggers, and behavior. For specific details about what each workflow does, refer to the comments at the top of each `.yaml` file.
For GitHub Actions documentation, see [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).

View File

@@ -1,4 +1,5 @@
name: Update Electron Types
name: 'Api: Update Electron API Types'
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
workflow_dispatch:
@@ -25,15 +26,6 @@ jobs:
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 }}-
- name: Update electron types
run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest

View File

@@ -1,4 +1,5 @@
name: Update ComfyUI-Manager API Types
name: 'Api: Update Manager API Types'
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger
@@ -30,26 +31,9 @@ jobs:
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 }}-
- 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 }}-
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
with:

View File

@@ -1,4 +1,5 @@
name: Update Comfy Registry API Types
name: 'Api: Update Registry API Types'
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger
@@ -29,26 +30,9 @@ jobs:
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 }}-
- 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 }}-
- name: Checkout comfy-api repository
uses: actions/checkout@v5
with:

View File

@@ -1,4 +1,5 @@
name: Validate JSON
name: "CI: JSON Validation"
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
on:
push:

View File

@@ -1,4 +1,5 @@
name: Lint and Format
name: "CI: Lint Format"
description: "Linting and code formatting validation for pull requests"
on:
pull_request:
@@ -32,21 +33,6 @@ jobs:
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 }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile

View File

@@ -1,4 +1,5 @@
name: Devtools Python Check
name: "CI: Python Validation"
description: "Validates Python code in tools/devtools directory"
on:
pull_request:

View File

@@ -1,8 +1,9 @@
name: PR Playwright Deploy (Forks)
name: "CI: Tests E2E (Deploy for Forks)"
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
on:
workflow_run:
workflows: ["Tests CI"]
workflows: ["CI: Tests E2E"]
types: [requested, completed]
env:

View File

@@ -1,4 +1,5 @@
name: Tests CI
name: "CI: Tests E2E"
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
on:
push:

View File

@@ -1,8 +1,9 @@
name: PR Storybook Deploy (Forks)
name: "CI: Tests Storybook (Deploy for Forks)"
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
on:
workflow_run:
workflows: ['Storybook and Chromatic CI']
workflows: ["CI: Tests Storybook"]
types: [requested, completed]
env:

View File

@@ -1,6 +1,5 @@
name: Storybook and Chromatic CI
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
on:
workflow_dispatch: # Allow manual triggering
@@ -53,19 +52,6 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -118,19 +104,6 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile

View File

@@ -1,4 +1,5 @@
name: Vitest Tests
name: "CI: Tests Unit"
description: "Unit and component testing with Vitest"
on:
push:
@@ -28,19 +29,6 @@ jobs:
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

View File

@@ -1,4 +1,5 @@
name: Update Locales
name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
on:
# Manual dispatch for urgent translation updates
@@ -54,5 +55,5 @@ jobs:
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
git diff --staged --quiet || git commit -m "Update locales"
git push origin HEAD:${{ github.head_ref }}

View File

@@ -1,4 +1,4 @@
name: Update Locales for given custom node repository
name: i18n Update Custom Nodes
on:
workflow_dispatch:

View File

@@ -1,4 +1,4 @@
name: Update Node Definitions Locales
name: i18n Update Nodes
on:
workflow_dispatch:

View File

@@ -1,4 +1,4 @@
name: Auto Backport
name: PR Backport
on:
pull_request_target:
@@ -95,41 +95,61 @@ jobs:
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
- name: Extract version labels
- name: Collect backport targets
if: steps.check-existing.outputs.skip != 'true'
id: versions
id: targets
run: |
# Extract version labels (e.g., "1.24", "1.22")
VERSIONS=""
TARGETS=()
declare -A SEEN=()
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
# For manual triggers, get labels from the PR
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else
# For automatic triggers, extract from PR event
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
LABELS=$(echo "$LABELS" | jq -r '.[].name')
fi
for label in $LABELS; do
# Match version labels like "1.24" (major.minor only)
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
# Validate the branch exists before adding to list
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
VERSIONS="${VERSIONS}${label} "
else
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
fi
fi
done
if [ -z "$VERSIONS" ]; then
echo "::error::No version labels found (e.g., 1.24, 1.22)"
add_target() {
local label="$1"
local target="$2"
if [ -z "$target" ]; then
return
fi
target=$(echo "$target" | xargs)
if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
return
fi
if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
TARGETS+=("$target")
SEEN["$target"]=1
else
echo "::warning::Label '${label}' references missing branch '${target}'"
fi
}
while IFS= read -r label; do
[ -z "$label" ] && continue
if [[ "$label" =~ ^branch:(.+)$ ]]; then
add_target "$label" "${BASH_REMATCH[1]}"
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
add_target "$label" "${BASH_REMATCH[1]}"
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
add_target "$label" "core/${label}"
fi
done <<< "$LABELS"
if [ "${#TARGETS[@]}" -eq 0 ]; then
echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
exit 1
fi
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
echo "Found version labels: ${VERSIONS}"
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
echo "Found backport targets: ${TARGETS[*]}"
- name: Backport commits
if: steps.check-existing.outputs.skip != 'true'
@@ -150,16 +170,17 @@ jobs:
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
for version in ${{ steps.versions.outputs.versions }}; do
echo "::group::Backporting to core/${version}"
for target in ${{ steps.targets.outputs.targets }}; do
TARGET_BRANCH="${target}"
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
TARGET_BRANCH="core/${version}"
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
echo "::group::Backporting to ${TARGET_BRANCH}"
# Fetch target branch (fail if doesn't exist)
if ! git fetch origin "${TARGET_BRANCH}"; then
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
FAILED="${FAILED}${version}:branch-missing "
FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
echo "::endgroup::"
continue
fi
@@ -170,7 +191,7 @@ jobs:
# Try cherry-pick
if git cherry-pick "${MERGE_COMMIT}"; then
git push origin "${BACKPORT_BRANCH}"
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
# Return to main (keep the branch, we need it for PR)
git checkout main
@@ -180,7 +201,7 @@ jobs:
git cherry-pick --abort
echo "::error::Cherry-pick failed due to conflicts"
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
# Clean up the failed branch
git checkout main
@@ -214,13 +235,13 @@ jobs:
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r version branch <<< "${backport}"
IFS=':' read -r target branch <<< "${backport}"
if PR_URL=$(gh pr create \
--base "core/${version}" \
--base "${target}" \
--head "${branch}" \
--title "[backport ${version}] ${PR_TITLE}" \
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
--title "[backport ${target}] ${PR_TITLE}" \
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
--label "backport" 2>&1); then
# Extract PR number from URL
@@ -230,9 +251,9 @@ jobs:
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
fi
else
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
# Still try to comment on the original PR about the failure
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
fi
done
@@ -253,16 +274,16 @@ jobs:
fi
for failure in ${{ steps.backport.outputs.failed }}; do
IFS=':' read -r version reason conflicts <<< "${failure}"
IFS=':' read -r target reason conflicts <<< "${failure}"
if [ "${reason}" = "branch-missing" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
elif [ "${reason}" = "conflicts" ]; then
# Convert comma-separated conflicts back to newlines for display
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
fi
done

View File

@@ -1,4 +1,5 @@
name: Claude PR Review
name: "PR: Claude Review"
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
permissions:
contents: read

View File

@@ -1,5 +1,5 @@
# Setting test expectation screenshots for Playwright
name: Update Playwright Expectations
name: "PR: Update Playwright Expectations"
on:
pull_request:

View File

@@ -44,6 +44,7 @@ jobs:
contents: read
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ENABLE_MINIFY: 'true'
steps:
- name: Validate inputs
env:

View File

@@ -1,4 +1,4 @@
name: Create Release Branch
name: Release Branch Create
on:
pull_request:

View File

@@ -1,4 +1,4 @@
name: Create Release Draft
name: Release Draft Create
on:
pull_request:
@@ -28,16 +28,6 @@ jobs:
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
@@ -55,6 +45,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile
@@ -126,7 +117,7 @@ jobs:
publish_types:
needs: build
uses: ./.github/workflows/publish-frontend-types.yaml
uses: ./.github/workflows/release-npm-types.yaml
with:
version: ${{ needs.build.outputs.version }}
ref: ${{ github.event.pull_request.merge_commit_sha }}

View File

@@ -1,4 +1,4 @@
name: Publish Frontend Types
name: Release NPM Types
on:
workflow_dispatch:

View File

@@ -1,4 +1,4 @@
name: Create Dev PyPI Package
name: Release PyPI Dev
on:
workflow_dispatch:
@@ -25,17 +25,6 @@ jobs:
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
@@ -44,6 +33,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
run: |
pnpm install --frozen-lockfile

View File

@@ -1,4 +1,5 @@
name: Version Bump
name: "Release: Version Bump"
description: "Manual workflow to increment package version with semantic versioning support"
on:
workflow_dispatch:
@@ -14,6 +15,11 @@ on:
required: false
default: ''
type: string
branch:
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
required: true
default: 'main'
type: string
jobs:
bump-version:
@@ -25,6 +31,24 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
- name: Validate branch exists
run: |
BRANCH="${{ github.event.inputs.branch }}"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
echo "Available core branches:"
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
echo ""
echo "Main branch:"
echo " - main"
exit 1
fi
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -58,7 +82,9 @@ jobs:
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ github.event.inputs.branch }}`
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: main
base: ${{ github.event.inputs.branch }}
labels: |
Release

52
.github/workflows/size-data.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: size data
on:
push:
branches:
- main
pull_request:
branches:
- main
permissions:
contents: read
jobs:
collect:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Build project
run: pnpm build
- name: Collect size data
run: node scripts/size-collect.js
- name: Save PR number & base branch
if: ${{ github.event_name == 'pull_request' }}
run: |
echo ${{ github.event.number }} > ./temp/size/number.txt
echo ${{ github.base_ref }} > ./temp/size/base.txt
- name: Upload size data
uses: actions/upload-artifact@v4
with:
name: size-data
path: temp/size

104
.github/workflows/size-report.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
name: size report
on:
workflow_run:
workflows: ['size data']
types:
- completed
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to report on'
required: true
type: number
run_id:
description: 'Size data workflow run ID'
required: true
type: string
permissions:
contents: read
pull-requests: write
issues: write
jobs:
size-report:
runs-on: ubuntu-latest
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
(
(github.event_name == 'workflow_run' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success') ||
github.event_name == 'workflow_dispatch'
)
steps:
- uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Download size data
uses: dawidd6/action-download-artifact@v11
with:
name: size-data
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
path: temp/size
- name: Set PR number
id: pr-number
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
fi
- name: Set base branch
id: pr-base
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "content=main" >> $GITHUB_OUTPUT
else
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
fi
- name: Download previous size data
uses: dawidd6/action-download-artifact@v11
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: size-data.yml
event: push
name: size-data
path: temp/size-prev
if_no_artifact_found: warn
- name: Generate size report
run: node scripts/size-report.js > size-report.md
- name: Read size report
id: size-report
uses: juliangruber/read-file-action@v1
with:
path: ./size-report.md
- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}
body: |
${{ steps.size-report.outputs.content }}
<!-- COMFYUI_FRONTEND_SIZE -->
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'

View File

@@ -14,6 +14,11 @@ on:
required: false
default: ''
type: string
branch:
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
required: true
default: 'main'
type: string
jobs:
bump-version-desktop-ui:
@@ -26,8 +31,25 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
persist-credentials: false
- name: Validate branch exists
run: |
BRANCH="${{ github.event.inputs.branch }}"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
echo "Available core branches:"
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
echo ""
echo "Main branch:"
echo " - main"
exit 1
fi
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -64,8 +86,10 @@ jobs:
title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ github.event.inputs.branch }}`
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: main
base: ${{ github.event.inputs.branch }}
labels: |
Release

View File

@@ -1,23 +1,23 @@
<template>
<div
class="task-div max-w-48 min-h-52 grid relative"
class="task-div relative grid min-h-52 max-w-48"
:class="{ 'opacity-75': isLoading }"
>
<Card
class="max-w-48 relative h-full overflow-hidden"
class="relative h-full max-w-48 overflow-hidden"
:class="{ 'opacity-65': runner.state !== 'error' }"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
<i
v-if="runner.state === 'error'"
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
class="pi pi-exclamation-triangle absolute top-0 -right-14 m-2 text-red-500 opacity-15"
style="font-size: 10rem"
/>
<img
v-if="task.headerImg"
:src="task.headerImg"
class="object-contain w-full h-full opacity-25 pt-4 px-4"
class="h-full w-full object-contain px-4 pt-4 opacity-25"
/>
</template>
<template #title>
@@ -27,7 +27,7 @@
{{ description }}
</template>
<template #footer>
<div class="flex gap-4 mt-1">
<div class="mt-1 flex gap-4">
<Button
:icon="task.button?.icon"
:label="task.button?.text"
@@ -73,7 +73,7 @@ defineEmits<{
// Bindings
const description = computed(() =>
runner.value.state === 'error'
? props.task.errorDescription ?? props.task.shortDescription
? (props.task.errorDescription ?? props.task.shortDescription)
: props.task.shortDescription
)

View File

@@ -1,67 +1,163 @@
import arCommands from '@frontend-locales/ar/commands.json' with { type: 'json' }
import ar from '@frontend-locales/ar/main.json' with { type: 'json' }
import arNodes from '@frontend-locales/ar/nodeDefs.json' with { type: 'json' }
import arSettings from '@frontend-locales/ar/settings.json' with { type: 'json' }
// Import only English locale eagerly as the default/fallback
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
// but these are properly configured in tsconfig.json and resolved by Vite at build time.
// eslint-disable-next-line import-x/no-unresolved
import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import en from '@frontend-locales/en/main.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
import esCommands from '@frontend-locales/es/commands.json' with { type: 'json' }
import es from '@frontend-locales/es/main.json' with { type: 'json' }
import esNodes from '@frontend-locales/es/nodeDefs.json' with { type: 'json' }
import esSettings from '@frontend-locales/es/settings.json' with { type: 'json' }
import frCommands from '@frontend-locales/fr/commands.json' with { type: 'json' }
import fr from '@frontend-locales/fr/main.json' with { type: 'json' }
import frNodes from '@frontend-locales/fr/nodeDefs.json' with { type: 'json' }
import frSettings from '@frontend-locales/fr/settings.json' with { type: 'json' }
import jaCommands from '@frontend-locales/ja/commands.json' with { type: 'json' }
import ja from '@frontend-locales/ja/main.json' with { type: 'json' }
import jaNodes from '@frontend-locales/ja/nodeDefs.json' with { type: 'json' }
import jaSettings from '@frontend-locales/ja/settings.json' with { type: 'json' }
import koCommands from '@frontend-locales/ko/commands.json' with { type: 'json' }
import ko from '@frontend-locales/ko/main.json' with { type: 'json' }
import koNodes from '@frontend-locales/ko/nodeDefs.json' with { type: 'json' }
import koSettings from '@frontend-locales/ko/settings.json' with { type: 'json' }
import ruCommands from '@frontend-locales/ru/commands.json' with { type: 'json' }
import ru from '@frontend-locales/ru/main.json' with { type: 'json' }
import ruNodes from '@frontend-locales/ru/nodeDefs.json' with { type: 'json' }
import ruSettings from '@frontend-locales/ru/settings.json' with { type: 'json' }
import trCommands from '@frontend-locales/tr/commands.json' with { type: 'json' }
import tr from '@frontend-locales/tr/main.json' with { type: 'json' }
import trNodes from '@frontend-locales/tr/nodeDefs.json' with { type: 'json' }
import trSettings from '@frontend-locales/tr/settings.json' with { type: 'json' }
import zhTWCommands from '@frontend-locales/zh-TW/commands.json' with { type: 'json' }
import zhTW from '@frontend-locales/zh-TW/main.json' with { type: 'json' }
import zhTWNodes from '@frontend-locales/zh-TW/nodeDefs.json' with { type: 'json' }
import zhTWSettings from '@frontend-locales/zh-TW/settings.json' with { type: 'json' }
import zhCommands from '@frontend-locales/zh/commands.json' with { type: 'json' }
import zh from '@frontend-locales/zh/main.json' with { type: 'json' }
import zhNodes from '@frontend-locales/zh/nodeDefs.json' with { type: 'json' }
import zhSettings from '@frontend-locales/zh/settings.json' with { type: 'json' }
import { createI18n } from 'vue-i18n'
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
function buildLocale<
M extends Record<string, unknown>,
N extends Record<string, unknown>,
C extends Record<string, unknown>,
S extends Record<string, unknown>
>(main: M, nodes: N, commands: C, settings: S) {
return {
...main,
nodeDefs: nodes,
commands: commands,
settings: settings
}
} as M & { nodeDefs: N; commands: C; settings: S }
}
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings),
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings),
ar: buildLocale(ar, arNodes, arCommands, arSettings),
tr: buildLocale(tr, trNodes, trCommands, trSettings)
// Locale loader map - dynamically import locales only when needed
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
/* eslint-disable import-x/no-unresolved */
const localeLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/main.json'),
es: () => import('@frontend-locales/es/main.json'),
fr: () => import('@frontend-locales/fr/main.json'),
ja: () => import('@frontend-locales/ja/main.json'),
ko: () => import('@frontend-locales/ko/main.json'),
ru: () => import('@frontend-locales/ru/main.json'),
tr: () => import('@frontend-locales/tr/main.json'),
zh: () => import('@frontend-locales/zh/main.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
}
const nodeDefsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/nodeDefs.json'),
es: () => import('@frontend-locales/es/nodeDefs.json'),
fr: () => import('@frontend-locales/fr/nodeDefs.json'),
ja: () => import('@frontend-locales/ja/nodeDefs.json'),
ko: () => import('@frontend-locales/ko/nodeDefs.json'),
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
}
const commandsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/commands.json'),
es: () => import('@frontend-locales/es/commands.json'),
fr: () => import('@frontend-locales/fr/commands.json'),
ja: () => import('@frontend-locales/ja/commands.json'),
ko: () => import('@frontend-locales/ko/commands.json'),
ru: () => import('@frontend-locales/ru/commands.json'),
tr: () => import('@frontend-locales/tr/commands.json'),
zh: () => import('@frontend-locales/zh/commands.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
}
const settingsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/settings.json'),
es: () => import('@frontend-locales/es/settings.json'),
fr: () => import('@frontend-locales/fr/settings.json'),
ja: () => import('@frontend-locales/ja/settings.json'),
ko: () => import('@frontend-locales/ko/settings.json'),
ru: () => import('@frontend-locales/ru/settings.json'),
tr: () => import('@frontend-locales/tr/settings.json'),
zh: () => import('@frontend-locales/zh/settings.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
}
// Track which locales have been loaded
const loadedLocales = new Set<string>(['en'])
// Track locales currently being loaded to prevent race conditions
const loadingLocales = new Map<string, Promise<void>>()
/**
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
*/
export async function loadLocale(locale: string): Promise<void> {
if (loadedLocales.has(locale)) {
return
}
// If already loading, return the existing promise to prevent duplicate loads
const existingLoad = loadingLocales.get(locale)
if (existingLoad) {
return existingLoad
}
const loader = localeLoaders[locale]
const nodeDefsLoader = nodeDefsLoaders[locale]
const commandsLoader = commandsLoaders[locale]
const settingsLoader = settingsLoaders[locale]
if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) {
console.warn(`Locale "${locale}" is not supported`)
return
}
// Create and track the loading promise
const loadPromise = (async () => {
try {
const [main, nodes, commands, settings] = await Promise.all([
loader(),
nodeDefsLoader(),
commandsLoader(),
settingsLoader()
])
const messages = buildLocale(
main.default,
nodes.default,
commands.default,
settings.default
)
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
loadedLocales.add(locale)
} catch (error) {
console.error(`Failed to load locale "${locale}":`, error)
throw error
} finally {
// Clean up the loading promise once complete
loadingLocales.delete(locale)
}
})()
loadingLocales.set(locale, loadPromise)
return loadPromise
}
// Only include English in the initial bundle
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings)
}
// Type for locale messages - inferred from the English locale structure
type LocaleMessages = typeof messages.en
export const i18n = createI18n({
// Must set `false`, as Vue I18n Legacy API is for Vue 2
legacy: false,

View File

@@ -46,6 +46,10 @@ class ComfyMenu {
.nth(0)
}
get buttons() {
return this.sideToolbar.locator('.side-bar-button')
}
get nodeLibraryTab() {
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab

View File

@@ -7,7 +7,7 @@ export class Topbar {
constructor(public readonly page: Page) {
this.menuLocator = page.locator('.comfy-command-menu')
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
this.menuTrigger = page.locator('.comfy-menu-button-wrapper')
}
async getTabNames(): Promise<string[]> {
@@ -105,7 +105,7 @@ export class Topbar {
* Close the topbar menu by clicking outside
*/
async closeTopbarMenu() {
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
await this.page.locator('body').click({ position: { x: 300, y: 10 } })
await expect(this.menuLocator).not.toBeVisible()
}

View File

@@ -116,9 +116,10 @@ test.describe('Actionbar', () => {
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
await comfyPage.page.dragAndDrop(
'.actionbar .drag-handle',
'.comfyui-menu',
'.actionbar-container',
{
targetPosition: { x: 0, y: 0 }
targetPosition: { x: 50, y: 20 },
force: true
}
)
expect(await comfyPage.actionbar.isDocked()).toBe(true)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -233,6 +233,7 @@ test.describe('Group Node', () => {
}
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
await comfyPage.menu.nodeLibraryTab.open()
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
.getFolder(GROUP_NODE_CATEGORY)
.count()
@@ -253,8 +254,6 @@ test.describe('Group Node', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.loadWorkflow(WORKFLOW_NAME)
await comfyPage.menu.nodeLibraryTab.open()
groupNode = await comfyPage.getFirstNodeRef()
if (!groupNode)
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)

View File

@@ -792,10 +792,19 @@ test.describe('Viewport settings', () => {
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame()
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
// Save workflow as a new file, then zoom out before screen shot
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
await comfyPage.nextFrame()
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
await changeTab(tabA)
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
await changeTab(tabB)
await comfyMouse.move(comfyPage.emptySpace)
for (let i = 0; i < 4; i++) {
await comfyMouse.wheel(0, 60)
@@ -807,9 +816,6 @@ test.describe('Viewport settings', () => {
// Ensure that the screenshots are different due to zoom level
expect(screenshotB).not.toBe(screenshotA)
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
// Go back to Workflow A
await changeTab(tabA)
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(

View File

@@ -8,9 +8,7 @@ test.describe('Menu', () => {
})
test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
(el) => el.children.length
)
const initialChildrenCount = await comfyPage.menu.buttons.count()
await comfyPage.page.evaluate(async () => {
window['app'].extensionManager.registerSidebarTab({
@@ -26,9 +24,7 @@ test.describe('Menu', () => {
})
await comfyPage.nextFrame()
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
(el) => el.children.length
)
const newChildrenCount = await comfyPage.menu.buttons.count()
expect(newChildrenCount).toBe(initialChildrenCount + 1)
})

View File

@@ -0,0 +1,28 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Record Audio Node', () => {
test('should add a record audio node and take a screenshot', async ({
comfyPage
}) => {
// Open the search box by double clicking on the canvas
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
// Search for and add the RecordAudio node
await comfyPage.searchBox.fillAndSelectFirstNode('RecordAudio')
await comfyPage.nextFrame()
// Verify the RecordAudio node was added
const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio')
expect(recordAudioNodes.length).toBe(1)
// Take a screenshot of the canvas with the RecordAudio node
await expect(comfyPage.canvas).toHaveScreenshot('record_audio_node.png')
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -12,21 +12,26 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling bypass on a selected node with hotkey', async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
test.fixme(
'should allow toggling bypass on a selected node with hotkey',
async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.mouse.click(400, 300)
await comfyPage.page.waitForTimeout(128)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
})
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
}
)
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -1,6 +1,7 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import pluginJs from '@eslint/js'
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
import { importX } from 'eslint-plugin-import-x'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook'
@@ -23,10 +24,17 @@ const commonGlobals = {
} as const
const settings = {
'import/resolver': {
typescript: true,
node: true
},
'import-x/resolver-next': [
createTypeScriptImportResolver({
alwaysTryTypes: true,
project: [
'./tsconfig.json',
'./apps/*/tsconfig.json',
'./packages/*/tsconfig.json'
],
noWarnOnMultipleProjects: true
})
],
tailwindcss: {
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
functions: ['cn', 'clsx', 'tw']
@@ -52,6 +60,7 @@ export default defineConfig([
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
'packages/registry-types/src/comfyRegistryTypes.ts',
'public/auth-sw.js',
'src/extensions/core/*',
'src/scripts/*',
'src/types/generatedManagerTypes.ts',
@@ -67,11 +76,8 @@ export default defineConfig([
...commonParserOptions,
projectService: {
allowDefaultProject: [
'vite.config.mts',
'vite.electron.config.mts',
'vite.types.config.mts',
'playwright.config.ts',
'playwright.i18n.config.ts'
'vite.types.config.mts'
]
}
}
@@ -249,5 +255,17 @@ export default defineConfig([
rules: {
'no-console': 'off'
}
},
{
files: ['scripts/**/*.js'],
languageOptions: {
globals: {
...globals.node
}
},
rules: {
'@typescript-eslint/no-floating-promises': 'off',
'no-console': 'off'
}
}
])

6
global.d.ts vendored
View File

@@ -4,6 +4,12 @@ declare const __SENTRY_DSN__: string
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
declare const __USE_PROD_CONFIG__: boolean
declare const __MIXPANEL_TOKEN__: string
type BuildFeatureFlags = {
REQUIRE_SUBSCRIPTION: boolean
}
declare const __BUILD_FLAGS__: BuildFeatureFlags
interface Navigator {
/**

View File

@@ -12,6 +12,10 @@ const config: KnipConfig = {
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
},
'apps/desktop-ui': {
entry: ['src/main.ts', 'src/i18n.ts'],
project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}']
},
'packages/tailwind-utils': {
project: ['src/**/*.{js,ts}']
},
@@ -54,7 +58,9 @@ const config: KnipConfig = {
'src/renderer/extensions/vueNodes/composables/slotLinkDragContext.ts',
'src/types/spatialIndex.ts',
'src/lib/litegraph/src/litegraph.ts',
'src/utils/vintageClipboard.ts'
'src/utils/vintageClipboard.ts',
// Service worker - registered at runtime via navigator.serviceWorker.register()
'public/auth-sw.js'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.29.2",
"version": "1.30.1",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -13,6 +13,8 @@
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
"build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build",
"build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build",
"size:collect": "node scripts/size-collect.js",
"size:report": "node scripts/size-report.js",
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
"dev:desktop": "nx dev @comfyorg/desktop-ui",
"dev:electron": "nx serve --config vite.electron.config.mts",
@@ -88,9 +90,13 @@
"jsdom": "catalog:",
"knip": "catalog:",
"lint-staged": "catalog:",
"markdown-table": "catalog:",
"mixpanel-browser": "catalog:",
"nx": "catalog:",
"picocolors": "catalog:",
"postcss-html": "catalog:",
"prettier": "catalog:",
"pretty-bytes": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"storybook": "catalog:",
"stylelint": "catalog:",

View File

@@ -63,6 +63,7 @@
--color-sand-200: #d6cfc2;
--color-sand-300: #888682;
--color-pure-black: #000000;
--color-pure-white: #ffffff;
--color-slate-100: #9c9eab;
@@ -144,6 +145,9 @@
--content-hover-bg: #adadad;
--content-hover-fg: #000;
--button-surface: var(--color-pure-white);
--button-surface-contrast: var(--color-pure-black);
/* Code styling colors for help menu*/
--code-text-color: rgb(0 122 255 / 1);
--code-bg-color: rgb(96 165 250 / 0.2);
@@ -155,6 +159,7 @@
--backdrop: var(--color-white);
--button-hover-surface: var(--color-gray-200);
--button-active-surface: var(--color-gray-400);
--button-icon: var(--color-gray-600);
--dialog-surface: var(--color-neutral-200);
--interface-menu-component-surface-hovered: var(--color-gray-200);
--interface-menu-component-surface-selected: var(--color-gray-400);
@@ -201,8 +206,11 @@
.dark-theme {
--accent-primary: var(--color-pure-white);
--backdrop: var(--color-neutral-900);
--button-surface: var(--color-charcoal-600);
--button-surface-contrast: var(--color-pure-white);
--button-hover-surface: var(--color-charcoal-600);
--button-active-surface: var(--color-charcoal-600);
--button-icon: var(--color-gray-800);
--dialog-surface: var(--color-neutral-700);
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
--interface-menu-component-surface-selected: var(--color-charcoal-300);
@@ -244,8 +252,11 @@
@theme inline {
--color-backdrop: var(--backdrop);
--color-button-hover-surface: var(--button-hover-surface);
--color-button-active-surface: var(--button-active-surface);
--color-button-hover-surface: var(--button-hover-surface);
--color-button-icon: var(--button-icon);
--color-button-surface: var(--button-surface);
--color-button-surface-contrast: var(--button-surface-contrast);
--color-dialog-surface: var(--dialog-surface);
--color-interface-menu-component-surface-hovered: var(--interface-menu-component-surface-hovered);
--color-interface-menu-component-surface-selected: var(--interface-menu-component-surface-selected);

View File

@@ -1,14 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 16 16" fill="none">
<g clip-path="url(#clip0_704_2695)">
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="#9C9EAB" stroke-width="1.3"/>
<path d="M6.5 5.5L10 2" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
<path d="M8 8L12.5 3.5" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="square"/>
<path d="M10.5 9.5L14 6" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="currentColor" stroke-width="1.3"/>
<path d="M6.5 5.5L10 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M8 8L12.5 3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="square"/>
<path d="M10.5 9.5L14 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_704_2695">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 938 B

After

Width:  |  Height:  |  Size: 964 B

218
pnpm-lock.yaml generated
View File

@@ -183,9 +183,18 @@ catalogs:
lint-staged:
specifier: ^15.2.7
version: 15.2.7
markdown-table:
specifier: ^3.0.4
version: 3.0.4
mixpanel-browser:
specifier: ^2.71.0
version: 2.71.0
nx:
specifier: 21.4.1
version: 21.4.1
picocolors:
specifier: ^1.1.1
version: 1.1.1
pinia:
specifier: ^2.1.7
version: 2.2.2
@@ -193,8 +202,11 @@ catalogs:
specifier: ^1.8.0
version: 1.8.0
prettier:
specifier: ^3.3.2
version: 3.3.2
specifier: ^3.6.2
version: 3.6.2
pretty-bytes:
specifier: ^7.1.0
version: 7.1.0
primeicons:
specifier: ^7.0.0
version: 7.0.0
@@ -254,7 +266,7 @@ catalogs:
version: 3.5.13
vue-component-type-helpers:
specifier: ^3.0.7
version: 3.1.0
version: 3.1.1
vue-eslint-parser:
specifier: ^10.2.0
version: 10.2.0
@@ -476,7 +488,7 @@ importers:
version: 21.4.1(@babel/traverse@7.28.3)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
'@nx/storybook':
specifier: 'catalog:'
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
'@nx/vite':
specifier: 'catalog:'
version: 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vitest@3.2.4)
@@ -488,19 +500,19 @@ importers:
version: 1.52.0
'@storybook/addon-docs':
specifier: 'catalog:'
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
'@storybook/vue3':
specifier: 'catalog:'
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
'@storybook/vue3-vite':
specifier: 'catalog:'
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.1.12(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
'@trivago/prettier-plugin-sort-imports':
specifier: 'catalog:'
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)
'@types/eslint-plugin-tailwindcss':
specifier: 'catalog:'
version: 3.17.0
@@ -548,10 +560,10 @@ importers:
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
eslint-plugin-prettier:
specifier: 'catalog:'
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2)
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
eslint-plugin-storybook:
specifier: 'catalog:'
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
eslint-plugin-tailwindcss:
specifier: 'catalog:'
version: 4.0.0-beta.0(tailwindcss@4.1.12)
@@ -585,21 +597,33 @@ importers:
lint-staged:
specifier: 'catalog:'
version: 15.2.7
markdown-table:
specifier: 'catalog:'
version: 3.0.4
mixpanel-browser:
specifier: 'catalog:'
version: 2.71.0
nx:
specifier: 'catalog:'
version: 21.4.1
picocolors:
specifier: 'catalog:'
version: 1.1.1
postcss-html:
specifier: 'catalog:'
version: 1.8.0
prettier:
specifier: 'catalog:'
version: 3.3.2
version: 3.6.2
pretty-bytes:
specifier: 'catalog:'
version: 7.1.0
rollup-plugin-visualizer:
specifier: 'catalog:'
version: 6.0.4(rollup@4.22.4)
storybook:
specifier: 'catalog:'
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
stylelint:
specifier: 'catalog:'
version: 16.24.0(typescript@5.9.2)
@@ -647,7 +671,7 @@ importers:
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
vue-component-type-helpers:
specifier: 'catalog:'
version: 3.1.0
version: 3.1.1
vue-eslint-parser:
specifier: 'catalog:'
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
@@ -2183,6 +2207,21 @@ packages:
'@microsoft/tsdoc@0.15.1':
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
'@mixpanel/rrdom@2.0.0-alpha.18.2':
resolution: {integrity: sha512-vX/tbnS14ZzzatC7vOyvAm9tOLU8tof0BuppBlphzEx1YHTSw8DQiAmyAc0AmXidchLV0W+cUHV/WsehPLh2hQ==}
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
resolution: {integrity: sha512-2kSnjZZ3QZ9zOz/isOt8s54mXUUDgXk/u0eEi/rE0xBWDeuA0NHrBcqiMc+w4F/yWWUpo5F5zcuPeYpc6ufAsw==}
'@mixpanel/rrweb-types@2.0.0-alpha.18.2':
resolution: {integrity: sha512-ucIYe1mfJ2UksvXW+d3bOySTB2/0yUSqQJlUydvbBz6OO2Bhq3nJHyLXV9ExkgUMZm1ZyDcvvmNUd1+5tAXlpA==}
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2':
resolution: {integrity: sha512-OomKIB6GTx5xvCLJ7iic2khT/t/tnCJUex13aEqsbSqIT/UzUUsqf+LTrgUK5ex+f6odmkCNjre2y5jvpNqn+g==}
'@mixpanel/rrweb@2.0.0-alpha.18.2':
resolution: {integrity: sha512-J3dVTEu6Z4p8di7y9KKvUooNuBjX97DdG6XGWoPEPi07A9512h9M8MEtvlY3mK0PGfuC0Mz5Pv/Ws6gjGYfKQg==}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -3002,6 +3041,9 @@ packages:
'@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
'@types/css-font-loading-module@0.0.7':
resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -3500,6 +3542,9 @@ packages:
'@webgpu/types@0.1.51':
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
'@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
'@xterm/addon-fit@0.10.0':
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
peerDependencies:
@@ -3782,6 +3827,10 @@ packages:
balanced-match@2.0.0:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -4892,9 +4941,6 @@ packages:
get-tsconfig@4.10.1:
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
get-tsconfig@4.7.5:
resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==}
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -5972,6 +6018,9 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mixpanel-browser@2.71.0:
resolution: {integrity: sha512-jKmDXe68/oQFgk/9ns9Z36bA0CJ31PH8Y77XTLLGfJvhsUPbvu+7Se9e281NejZF6+OMqx7cE+zFxToozYyNrA==}
mkdirp@3.0.1:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
@@ -5999,11 +6048,6 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.1.5:
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
engines: {node: ^18 || >=20}
@@ -6351,10 +6395,6 @@ packages:
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
postcss@8.5.1:
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
@@ -6367,11 +6407,15 @@ packages:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
prettier@3.3.2:
resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==}
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
pretty-bytes@7.1.0:
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
engines: {node: '>=20'}
pretty-format@27.5.1:
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -7473,9 +7517,6 @@ packages:
vue-component-type-helpers@2.2.12:
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
vue-component-type-helpers@3.1.0:
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
@@ -9492,6 +9533,29 @@ snapshots:
'@microsoft/tsdoc@0.15.1': {}
'@mixpanel/rrdom@2.0.0-alpha.18.2':
dependencies:
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
dependencies:
postcss: 8.5.6
'@mixpanel/rrweb-types@2.0.0-alpha.18.2': {}
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2': {}
'@mixpanel/rrweb@2.0.0-alpha.18.2':
dependencies:
'@mixpanel/rrdom': 2.0.0-alpha.18.2
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
'@mixpanel/rrweb-types': 2.0.0-alpha.18.2
'@mixpanel/rrweb-utils': 2.0.0-alpha.18.2
'@types/css-font-loading-module': 0.0.7
'@xstate/fsm': 1.6.5
base64-arraybuffer: 1.0.2
mitt: 3.0.1
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.4.5
@@ -9668,7 +9732,7 @@ snapshots:
- typescript
- verdaccio
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
dependencies:
'@nx/cypress': 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
'@nx/devkit': 21.4.1(nx@21.4.1)
@@ -9676,7 +9740,7 @@ snapshots:
'@nx/js': 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)
'@phenomnomnominal/tsquery': 5.0.1(typescript@5.9.2)
semver: 7.7.2
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
tslib: 2.8.1
transitivePeerDependencies:
- '@babel/traverse'
@@ -10027,29 +10091,29 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
dependencies:
'@mdx-js/react': 3.1.0(@types/react@19.1.9)(react@19.1.1)
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
'@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
ts-dedent: 2.2.0
transitivePeerDependencies:
- '@types/react'
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
dependencies:
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
ts-dedent: 2.2.0
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
dependencies:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
unplugin: 1.16.1
'@storybook/global@5.0.0': {}
@@ -10059,19 +10123,19 @@ snapshots:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
dependencies:
react: 19.1.1
react-dom: 19.1.1(react@19.1.1)
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
dependencies:
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
find-package-json: 1.2.0
magic-string: 0.30.19
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
typescript: 5.9.2
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
vue-component-meta: 2.2.12(typescript@5.9.2)
@@ -10079,10 +10143,10 @@ snapshots:
transitivePeerDependencies:
- vue
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
dependencies:
'@storybook/global': 5.0.0
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.1
@@ -10347,7 +10411,7 @@ snapshots:
'@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
'@tiptap/pm': 2.10.4
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)':
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)':
dependencies:
'@babel/generator': 7.28.3
'@babel/parser': 7.28.4
@@ -10355,7 +10419,7 @@ snapshots:
'@babel/types': 7.28.4
javascript-natural-sort: 0.7.1
lodash: 4.17.21
prettier: 3.3.2
prettier: 3.6.2
optionalDependencies:
'@vue/compiler-sfc': 3.5.13
transitivePeerDependencies:
@@ -10380,6 +10444,8 @@ snapshots:
dependencies:
'@types/deep-eql': 4.0.2
'@types/css-font-loading-module@0.0.7': {}
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -10973,6 +11039,8 @@ snapshots:
'@webgpu/types@0.1.51': {}
'@xstate/fsm@1.6.5': {}
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
dependencies:
'@xterm/xterm': 5.5.0
@@ -11289,6 +11357,8 @@ snapshots:
balanced-match@2.0.0: {}
base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {}
better-opn@3.0.2:
@@ -12133,20 +12203,20 @@ snapshots:
- supports-color
optional: true
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2):
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
dependencies:
eslint: 9.35.0(jiti@2.4.2)
prettier: 3.3.2
prettier: 3.6.2
prettier-linter-helpers: 1.0.0
synckit: 0.11.11
optionalDependencies:
eslint-config-prettier: 10.1.8(eslint@9.35.0(jiti@2.4.2))
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
dependencies:
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
eslint: 9.35.0(jiti@2.4.2)
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
transitivePeerDependencies:
- supports-color
- typescript
@@ -12585,10 +12655,6 @@ snapshots:
dependencies:
resolve-pkg-maps: 1.0.0
get-tsconfig@4.7.5:
dependencies:
resolve-pkg-maps: 1.0.0
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
@@ -13861,6 +13927,10 @@ snapshots:
mitt@3.0.1: {}
mixpanel-browser@2.71.0:
dependencies:
'@mixpanel/rrweb': 2.0.0-alpha.18.2
mkdirp@3.0.1: {}
mlly@1.8.0:
@@ -13883,8 +13953,6 @@ snapshots:
nanoid@3.3.11: {}
nanoid@3.3.8: {}
nanoid@5.1.5: {}
napi-postinstall@0.3.3: {}
@@ -14264,14 +14332,14 @@ snapshots:
dependencies:
htmlparser2: 8.0.2
js-tokens: 9.0.1
postcss: 8.5.1
postcss-safe-parser: 6.0.0(postcss@8.5.1)
postcss: 8.5.6
postcss-safe-parser: 6.0.0(postcss@8.5.6)
postcss-resolve-nested-selector@0.1.6: {}
postcss-safe-parser@6.0.0(postcss@8.5.1):
postcss-safe-parser@6.0.0(postcss@8.5.6):
dependencies:
postcss: 8.5.1
postcss: 8.5.6
postcss-safe-parser@7.0.1(postcss@8.5.6):
dependencies:
@@ -14289,12 +14357,6 @@ snapshots:
postcss-value-parser@4.2.0: {}
postcss@8.5.1:
dependencies:
nanoid: 3.3.8
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
@@ -14307,7 +14369,9 @@ snapshots:
dependencies:
fast-diff: 1.3.0
prettier@3.3.2: {}
prettier@3.6.2: {}
pretty-bytes@7.1.0: {}
pretty-format@27.5.1:
dependencies:
@@ -14984,7 +15048,7 @@ snapshots:
internal-slot: 1.1.0
optional: true
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
dependencies:
'@storybook/global': 5.0.0
'@testing-library/jest-dom': 6.6.4
@@ -14999,7 +15063,7 @@ snapshots:
semver: 7.7.2
ws: 8.18.3
optionalDependencies:
prettier: 3.3.2
prettier: 3.6.2
transitivePeerDependencies:
- '@testing-library/dom'
- bufferutil
@@ -15321,7 +15385,7 @@ snapshots:
tsx@4.19.4:
dependencies:
esbuild: 0.25.5
get-tsconfig: 4.7.5
get-tsconfig: 4.10.1
optionalDependencies:
fsevents: 2.3.3
@@ -15673,7 +15737,7 @@ snapshots:
vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2):
dependencies:
esbuild: 0.21.5
postcss: 8.5.1
postcss: 8.5.6
rollup: 4.22.4
optionalDependencies:
'@types/node': 20.14.10
@@ -15738,8 +15802,6 @@ snapshots:
vue-component-type-helpers@2.2.12: {}
vue-component-type-helpers@3.1.0: {}
vue-component-type-helpers@3.1.1: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):

View File

@@ -62,10 +62,13 @@ catalog:
jsdom: ^26.1.0
knip: ^5.62.0
lint-staged: ^15.2.7
markdown-table: ^3.0.4
nx: 21.4.1
picocolors: ^1.1.1
pinia: ^2.1.7
postcss-html: ^1.8.0
prettier: ^3.3.2
prettier: ^3.6.2
pretty-bytes: ^7.1.0
primeicons: ^7.0.0
primevue: ^4.2.5
rollup-plugin-visualizer: ^6.0.4
@@ -95,12 +98,10 @@ catalog:
zod: ^3.23.8
zod-to-json-schema: ^3.24.1
zod-validation-error: ^3.3.0
mixpanel-browser: ^2.71.0
cleanupUnusedCatalogs: true
overrides:
'@types/eslint': '-'
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs
@@ -115,3 +116,6 @@ onlyBuiltDependencies:
- esbuild
- nx
- oxc-resolver
overrides:
'@types/eslint': '-'

Binary file not shown.

147
public/auth-sw.js Normal file
View File

@@ -0,0 +1,147 @@
/**
* @fileoverview Authentication Service Worker
* Intercepts /api/view requests and adds Firebase authentication headers.
* Required for browser-native requests (img, video, audio) that cannot send custom headers.
*/
/**
* @typedef {Object} AuthHeader
* @property {string} Authorization - Bearer token for authentication
*/
/**
* @typedef {Object} CachedAuth
* @property {AuthHeader|null} header
* @property {number} expiresAt - Timestamp when cache expires
*/
const CACHE_TTL_MS = 50 * 60 * 1000 // 50 minutes (Firebase tokens expire in 1 hour)
/** @type {CachedAuth|null} */
let authCache = null
/** @type {Promise<AuthHeader|null>|null} */
let authRequestInFlight = null
self.addEventListener('message', (event) => {
if (event.data.type === 'INVALIDATE_AUTH_HEADER') {
authCache = null
authRequestInFlight = null
}
})
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
if (
!url.pathname.startsWith('/api/view') &&
!url.pathname.startsWith('/api/viewvideo')
) {
return
}
event.respondWith(
(async () => {
try {
const authHeader = await getAuthHeader()
if (!authHeader) {
return fetch(event.request)
}
const headers = new Headers(event.request.headers)
for (const [key, value] of Object.entries(authHeader)) {
headers.set(key, value)
}
return fetch(
new Request(event.request.url, {
method: event.request.method,
headers: headers,
mode: 'same-origin',
credentials: event.request.credentials,
cache: 'no-store',
redirect: event.request.redirect,
referrer: event.request.referrer,
integrity: event.request.integrity
})
)
} catch (error) {
console.error('[Auth SW] Request failed:', error)
return fetch(event.request)
}
})()
)
})
/**
* Gets auth header from cache or requests from main thread
* @returns {Promise<AuthHeader|null>}
*/
async function getAuthHeader() {
// Return cached value if valid
if (authCache && authCache.expiresAt > Date.now()) {
return authCache.header
}
// Clear expired cache
if (authCache) {
authCache = null
}
// Deduplicate concurrent requests
if (authRequestInFlight) {
return authRequestInFlight
}
authRequestInFlight = requestAuthHeaderFromMainThread()
const header = await authRequestInFlight
authRequestInFlight = null
// Cache the result
if (header) {
authCache = {
header,
expiresAt: Date.now() + CACHE_TTL_MS
}
}
return header
}
/**
* Requests auth header from main thread via MessageChannel
* @returns {Promise<AuthHeader|null>}
*/
async function requestAuthHeaderFromMainThread() {
const clients = await self.clients.matchAll()
if (clients.length === 0) {
return null
}
const messageChannel = new MessageChannel()
return new Promise((resolve) => {
let timeoutId
messageChannel.port1.onmessage = (event) => {
clearTimeout(timeoutId)
resolve(event.data.authHeader)
}
timeoutId = setTimeout(() => {
console.error(
'[Auth SW] Timeout waiting for auth header from main thread'
)
resolve(null)
}, 1000)
clients[0].postMessage({ type: 'REQUEST_AUTH_HEADER' }, [
messageChannel.port2
])
})
}
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})

View File

@@ -0,0 +1,130 @@
// @ts-check
/**
* Bundle categorization configuration
*
* This file defines how bundles are categorized in size reports.
* Categories help identify which parts of the application are growing.
*/
/**
* @typedef {Object} BundleCategory
* @property {string} name - Display name of the category
* @property {string} description - Description of what this category includes
* @property {RegExp[]} patterns - Regex patterns to match bundle files
* @property {number} order - Sort order for display (lower = first)
*/
/** @type {BundleCategory[]} */
export const BUNDLE_CATEGORIES = [
{
name: 'App Entry Points',
description: 'Main entry bundles and manifests',
patterns: [/^index-.*\.js$/i, /^manifest-.*\.js$/i],
order: 1
},
{
name: 'Graph Workspace',
description: 'Graph editor runtime, canvas, workflow orchestration',
patterns: [
/Graph(View|State)?-.*\.js$/i,
/(Canvas|Workflow|History|NodeGraph|Compositor)-.*\.js$/i
],
order: 2
},
{
name: 'Views & Navigation',
description: 'Top-level views, pages, and routed surfaces',
patterns: [/.*(View|Page|Layout|Screen|Route)-.*\.js$/i],
order: 3
},
{
name: 'Panels & Settings',
description: 'Configuration panels, inspectors, and settings screens',
patterns: [/.*(Panel|Settings|Config|Preferences|Manager)-.*\.js$/i],
order: 4
},
{
name: 'User & Accounts',
description: 'Authentication, profile, and account management bundles',
patterns: [
/.*((User(Panel|Select|Auth|Account|Profile|Settings|Preferences|Manager|List|Menu|Modal))|Account|Auth|Profile|Login|Signup|Password).*-.+\.js$/i
],
order: 5
},
{
name: 'Editors & Dialogs',
description: 'Modals, dialogs, drawers, and in-app editors',
patterns: [/.*(Modal|Dialog|Drawer|Editor)-.*\.js$/i],
order: 6
},
{
name: 'UI Components',
description: 'Reusable component library chunks',
patterns: [
/.*(Button|Avatar|Badge|Dropdown|Tabs|Table|List|Card|Form|Input|Toggle|Menu|Toolbar|Sidebar)-.*\.js$/i,
/.*\.vue_vue_type_script_setup_true_lang-.*\.js$/i
],
order: 7
},
{
name: 'Data & Services',
description: 'Stores, services, APIs, and repositories',
patterns: [/.*(Service|Store|Api|Repository)-.*\.js$/i],
order: 8
},
{
name: 'Utilities & Hooks',
description: 'Helpers, composables, and utility bundles',
patterns: [
/.*(Util|Utils|Helper|Composable|Hook)-.*\.js$/i,
/use[A-Z].*\.js$/
],
order: 9
},
{
name: 'Vendor & Third-Party',
description: 'External libraries and shared vendor chunks',
patterns: [
/^(chunk|vendor|prime|three|lodash|chart|firebase|yjs|axios|uuid)-.*\.js$/i
],
order: 10
},
{
name: 'Other',
description: 'Bundles that do not match a named category',
patterns: [/.*/],
order: 99
}
]
/**
* Categorize a bundle file based on its name
*
* @param {string} fileName - The bundle file name (e.g., "assets/GraphView-BnV6iF9h.js")
* @returns {string} - The category name
*/
export function categorizeBundle(fileName) {
// Extract just the file name without path
const baseName = fileName.split('/').pop() || fileName
// Find the first matching category
for (const category of BUNDLE_CATEGORIES) {
for (const pattern of category.patterns) {
if (pattern.test(baseName)) {
return category.name
}
}
}
return 'Other'
}
/**
* Get category metadata by name
*
* @param {string} categoryName - The category name
* @returns {BundleCategory | undefined} - The category metadata
*/
export function getCategoryMetadata(categoryName) {
return BUNDLE_CATEGORIES.find((cat) => cat.name === categoryName)
}

90
scripts/size-collect.js Normal file
View File

@@ -0,0 +1,90 @@
// @ts-check
import { existsSync } from 'node:fs'
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { brotliCompressSync, gzipSync } from 'node:zlib'
import pico from 'picocolors'
import prettyBytes from 'pretty-bytes'
import { categorizeBundle } from './bundle-categories.js'
const distDir = path.resolve('dist')
const sizeDir = path.resolve('temp/size')
/**
* @typedef {Object} SizeResult
* @property {string} file
* @property {string} category
* @property {number} size
* @property {number} gzip
* @property {number} brotli
*/
run()
/**
* Main function to collect bundle size data
*/
async function run() {
if (!existsSync(distDir)) {
console.error(pico.red('Error: dist directory does not exist'))
console.error(pico.yellow('Please run "pnpm build" first'))
process.exit(1)
}
console.log(pico.blue('\nCollecting bundle size data...\n'))
// Collect main bundle files from dist/assets
const assetsDir = path.join(distDir, 'assets')
const bundles = []
if (existsSync(assetsDir)) {
const files = await readdir(assetsDir)
const jsFiles = files.filter(
(file) => file.endsWith('.js') && !file.includes('legacy')
)
for (const file of jsFiles) {
const filePath = path.join(assetsDir, file)
const content = await readFile(filePath, 'utf-8')
const size = Buffer.byteLength(content)
const gzip = gzipSync(content).length
const brotli = brotliCompressSync(content).length
const fileName = `assets/${file}`
const category = categorizeBundle(fileName)
bundles.push({
file: fileName,
category,
size,
gzip,
brotli
})
console.log(
`${pico.green(file)} ${pico.dim(`[${category}]`)} - ` +
`Size: ${prettyBytes(size)} / ` +
`Gzip: ${prettyBytes(gzip)} / ` +
`Brotli: ${prettyBytes(brotli)}`
)
}
}
// Create temp/size directory
await mkdir(sizeDir, { recursive: true })
// Write individual bundle files
for (const bundle of bundles) {
const fileName = bundle.file.replace(/[/\\]/g, '_').replace('.js', '.json')
await writeFile(
path.join(sizeDir, fileName),
JSON.stringify(bundle, null, 2),
'utf-8'
)
}
console.log(
pico.green(`\n✓ Collected size data for ${bundles.length} bundles\n`)
)
console.log(pico.blue(`Data saved to: ${sizeDir}\n`))
}

560
scripts/size-report.js Normal file
View File

@@ -0,0 +1,560 @@
// @ts-check
import { markdownTable } from 'markdown-table'
import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises'
import path from 'node:path'
import prettyBytes from 'pretty-bytes'
import { getCategoryMetadata } from './bundle-categories.js'
/**
* @typedef {Object} SizeMetrics
* @property {number} size
* @property {number} gzip
* @property {number} brotli
*/
/**
* @typedef {Object} SizeResult
* @property {number} size
* @property {number} gzip
* @property {number} brotli
*/
/**
* @typedef {SizeResult & { file: string, category?: string }} BundleResult
*/
/**
* @typedef {'added' | 'removed' | 'increased' | 'decreased' | 'unchanged'} BundleStatus
*/
/**
* @typedef {Object} BundleDiff
* @property {string} fileName
* @property {BundleResult | undefined} curr
* @property {BundleResult | undefined} prev
* @property {SizeMetrics} diff
* @property {BundleStatus} status
*/
/**
* @typedef {Object} CountSummary
* @property {number} added
* @property {number} removed
* @property {number} increased
* @property {number} decreased
* @property {number} unchanged
*/
/**
* @typedef {Object} CategoryReport
* @property {string} name
* @property {string | undefined} description
* @property {number} order
* @property {{ current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }} metrics
* @property {CountSummary} counts
* @property {BundleDiff[]} bundles
*/
/**
* @typedef {Object} BundleReport
* @property {CategoryReport[]} categories
* @property {{ currentBundles: number, baselineBundles: number, metrics: { current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }, counts: CountSummary }} overall
* @property {boolean} hasBaseline
*/
const currDir = path.resolve('temp/size')
const prevDir = path.resolve('temp/size-prev')
run()
/**
* Main entry for generating the size report
*/
async function run() {
if (!existsSync(currDir)) {
console.error('Error: temp/size directory does not exist')
console.error('Please run "pnpm size:collect" first')
process.exit(1)
}
const report = await buildBundleReport()
const output = renderReport(report)
process.stdout.write(output)
}
/**
* Build bundle comparison data from current and baseline artifacts
* @returns {Promise<BundleReport>}
*/
async function buildBundleReport() {
/**
* @param {string[]} files
* @returns {string[]}
*/
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
const currFiles = filterFiles(await readdir(currDir))
const baselineFiles = existsSync(prevDir)
? filterFiles(await readdir(prevDir))
: []
const fileList = new Set([...currFiles, ...baselineFiles])
/** @type {Map<string, CategoryReport>} */
const categories = new Map()
const overall = {
currentBundles: 0,
baselineBundles: 0,
metrics: {
current: createMetrics(),
baseline: createMetrics(),
diff: createMetrics()
},
counts: createCounts()
}
for (const file of fileList) {
const currPath = path.resolve(currDir, file)
const prevPath = path.resolve(prevDir, file)
const curr = await importJSON(currPath)
const prev = await importJSON(prevPath)
const fileName = curr?.file || prev?.file
if (!fileName) continue
const categoryName = curr?.category || prev?.category || 'Other'
const category = ensureCategoryEntry(categories, categoryName)
const currMetrics = toMetrics(curr)
const baselineMetrics = toMetrics(prev)
const diffMetrics = subtractMetrics(currMetrics, baselineMetrics)
const status = getStatus(curr, prev, diffMetrics.size)
if (curr) {
overall.currentBundles++
}
if (prev) {
overall.baselineBundles++
}
addMetrics(overall.metrics.current, currMetrics)
addMetrics(overall.metrics.baseline, baselineMetrics)
addMetrics(overall.metrics.diff, diffMetrics)
incrementStatus(overall.counts, status)
addMetrics(category.metrics.current, currMetrics)
addMetrics(category.metrics.baseline, baselineMetrics)
addMetrics(category.metrics.diff, diffMetrics)
incrementStatus(category.counts, status)
category.bundles.push({
fileName,
curr,
prev,
diff: diffMetrics,
status
})
}
const sortedCategories = Array.from(categories.values()).sort(
(a, b) => a.order - b.order
)
return {
categories: sortedCategories,
overall,
hasBaseline: baselineFiles.length > 0
}
}
/**
* Render the complete report in markdown
* @param {BundleReport} report
* @returns {string}
*/
function renderReport(report) {
const parts = ['## Bundle Size Report\n']
parts.push(renderSummary(report))
if (report.categories.length > 0) {
const glance = renderCategoryGlance(report)
if (glance) {
parts.push('\n' + glance)
}
parts.push('\n' + renderCategoryDetails(report))
}
return (
parts
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trimEnd() + '\n'
)
}
/**
* Render overall summary bullets
* @param {BundleReport} report
* @returns {string}
*/
function renderSummary(report) {
const { overall, hasBaseline } = report
const lines = ['**Summary**']
const rawLineParts = [
`- Raw size: ${prettyBytes(overall.metrics.current.size)}`
]
if (hasBaseline) {
rawLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.size)}`)
rawLineParts.push(`${formatDiffIndicator(overall.metrics.diff.size)}`)
}
lines.push(rawLineParts.join(' '))
const gzipLineParts = [`- Gzip: ${prettyBytes(overall.metrics.current.gzip)}`]
if (hasBaseline) {
gzipLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.gzip)}`)
gzipLineParts.push(`${formatDiffIndicator(overall.metrics.diff.gzip)}`)
}
lines.push(gzipLineParts.join(' '))
const brotliLineParts = [
`- Brotli: ${prettyBytes(overall.metrics.current.brotli)}`
]
if (hasBaseline) {
brotliLineParts.push(
`baseline ${prettyBytes(overall.metrics.baseline.brotli)}`
)
brotliLineParts.push(
`${formatDiffIndicator(overall.metrics.diff.brotli)}`
)
}
lines.push(brotliLineParts.join(' '))
const bundleStats = [`${overall.currentBundles} current`]
if (hasBaseline) {
bundleStats.push(`${overall.baselineBundles} baseline`)
}
const statusParts = []
if (overall.counts.added) statusParts.push(`${overall.counts.added} added`)
if (overall.counts.removed)
statusParts.push(`${overall.counts.removed} removed`)
if (overall.counts.increased)
statusParts.push(`${overall.counts.increased} grew`)
if (overall.counts.decreased)
statusParts.push(`${overall.counts.decreased} shrank`)
let bundlesLine = `- Bundles: ${bundleStats.join(' • ')}`
if (statusParts.length > 0) {
bundlesLine += `${statusParts.join(' / ')}`
}
lines.push(bundlesLine)
if (!hasBaseline) {
lines.push(
'_Baseline artifact not found; showing current bundle sizes only._'
)
}
return lines.join('\n')
}
/**
* Render a compact category glance line
* @param {BundleReport} report
* @returns {string}
*/
function renderCategoryGlance(report) {
const { categories, hasBaseline } = report
const relevant = categories.filter(
(category) =>
category.metrics.current.size > 0 ||
(hasBaseline && category.metrics.baseline.size > 0)
)
if (relevant.length === 0) return ''
const sorted = relevant.slice().sort((a, b) => {
if (hasBaseline) {
return (
Math.abs(b.metrics.diff.size) - Math.abs(a.metrics.diff.size) ||
b.metrics.current.size - a.metrics.current.size
)
}
return b.metrics.current.size - a.metrics.current.size
})
const limit = 6
const trimmed = sorted.slice(0, limit)
const parts = trimmed.map((category) => {
const currentStr = prettyBytes(category.metrics.current.size)
if (hasBaseline) {
return `${category.name} ${formatDiffIndicator(category.metrics.diff.size)} (${currentStr})`
}
return `${category.name} ${currentStr}`
})
if (sorted.length > limit) {
parts.push(`+ ${sorted.length - limit} more`)
}
return `**Category Glance**\n${parts.join(' · ')}`
}
/**
* Render per-category detail tables wrapped in collapsible sections
* @param {BundleReport} report
* @returns {string}
*/
function renderCategoryDetails(report) {
const lines = ['<details>', '<summary>Per-category breakdown</summary>', '']
for (const category of report.categories) {
lines.push(renderCategoryBlock(category, report.hasBaseline))
}
lines.push('</details>')
return lines.join('\n')
}
/**
* Render a single category block with its table
* @param {CategoryReport} category
* @param {boolean} hasBaseline
* @returns {string}
*/
function renderCategoryBlock(category, hasBaseline) {
const lines = ['<details>']
const currentStr = prettyBytes(category.metrics.current.size)
const summaryParts = [`<summary>${category.name}${currentStr}`]
if (hasBaseline) {
summaryParts.push(
` (baseline ${prettyBytes(category.metrics.baseline.size)}) • ${formatDiffIndicator(category.metrics.diff.size)}`
)
}
summaryParts.push('</summary>')
lines.push(summaryParts.join(''))
if (category.description) {
lines.push(`_${category.description}_`)
}
if (category.bundles.length === 0) {
lines.push('No bundles matched this category.\n')
lines.push('</details>\n')
return lines.join('\n')
}
const headers = hasBaseline
? ['File', 'Before', 'After', 'Δ Raw', 'Δ Gzip', 'Δ Brotli']
: ['File', 'Size', 'Gzip', 'Brotli']
const rows = category.bundles
.slice()
.sort((a, b) => {
const diffMagnitude = Math.abs(b.diff.size) - Math.abs(a.diff.size)
if (diffMagnitude !== 0) return diffMagnitude
return a.fileName.localeCompare(b.fileName)
})
.map((bundle) => {
if (hasBaseline) {
return [
formatFileLabel(bundle),
formatSize(bundle.prev?.size),
formatSize(bundle.curr?.size),
formatDiffIndicator(bundle.diff.size),
formatDiffIndicator(bundle.diff.gzip),
formatDiffIndicator(bundle.diff.brotli)
]
}
return [
formatFileLabel(bundle),
formatSize(bundle.curr?.size),
formatSize(bundle.curr?.gzip),
formatSize(bundle.curr?.brotli)
]
})
lines.push(markdownTable([headers, ...rows]))
const statusParts = []
if (category.counts.added) statusParts.push(`${category.counts.added} added`)
if (category.counts.removed)
statusParts.push(`${category.counts.removed} removed`)
if (category.counts.increased)
statusParts.push(`${category.counts.increased} grew`)
if (category.counts.decreased)
statusParts.push(`${category.counts.decreased} shrank`)
if (statusParts.length > 0) {
lines.push(`\n_Status:_ ${statusParts.join(' / ')}`)
}
lines.push('</details>\n')
return lines.join('\n')
}
/**
* Ensure a category entry exists in the map
* @param {Map<string, CategoryReport>} categories
* @param {string} categoryName
* @returns {CategoryReport}
*/
function ensureCategoryEntry(categories, categoryName) {
if (!categories.has(categoryName)) {
const meta = getCategoryMetadata(categoryName)
categories.set(categoryName, {
name: categoryName,
description: meta?.description,
order: meta?.order ?? 99,
metrics: {
current: createMetrics(),
baseline: createMetrics(),
diff: createMetrics()
},
counts: createCounts(),
bundles: []
})
}
// @ts-expect-error - ensured by check above
return categories.get(categoryName)
}
/**
* Convert bundle result to metrics
* @param {BundleResult | undefined} bundle
* @returns {SizeMetrics}
*/
function toMetrics(bundle) {
if (!bundle) return createMetrics()
return {
size: bundle.size,
gzip: bundle.gzip,
brotli: bundle.brotli
}
}
/**
* Create an empty metrics object
* @returns {SizeMetrics}
*/
function createMetrics() {
return { size: 0, gzip: 0, brotli: 0 }
}
/**
* Add source metrics into target metrics
* @param {SizeMetrics} target
* @param {SizeMetrics} source
*/
function addMetrics(target, source) {
target.size += source.size
target.gzip += source.gzip
target.brotli += source.brotli
}
/**
* Subtract baseline metrics from current metrics
* @param {SizeMetrics} current
* @param {SizeMetrics} baseline
* @returns {SizeMetrics}
*/
function subtractMetrics(current, baseline) {
return {
size: current.size - baseline.size,
gzip: current.gzip - baseline.gzip,
brotli: current.brotli - baseline.brotli
}
}
/**
* Create an empty counts object
* @returns {CountSummary}
*/
function createCounts() {
return { added: 0, removed: 0, increased: 0, decreased: 0, unchanged: 0 }
}
/**
* Increment status counters
* @param {CountSummary} counts
* @param {BundleStatus} status
*/
function incrementStatus(counts, status) {
counts[status] += 1
}
/**
* Determine bundle status for reporting
* @param {BundleResult | undefined} curr
* @param {BundleResult | undefined} prev
* @param {number} sizeDiff
* @returns {BundleStatus}
*/
function getStatus(curr, prev, sizeDiff) {
if (curr && prev) {
if (sizeDiff > 0) return 'increased'
if (sizeDiff < 0) return 'decreased'
return 'unchanged'
}
if (curr && !prev) return 'added'
if (!curr && prev) return 'removed'
return 'unchanged'
}
/**
* Format file label with status hints
* @param {BundleDiff} bundle
* @returns {string}
*/
function formatFileLabel(bundle) {
if (bundle.status === 'added') {
return `**${bundle.fileName}** _(new)_`
}
if (bundle.status === 'removed') {
return `~~${bundle.fileName}~~ _(removed)_`
}
return bundle.fileName
}
/**
* Format size for table output
* @param {number | undefined} value
* @returns {string}
*/
function formatSize(value) {
if (value === undefined) return '—'
return prettyBytes(value)
}
/**
* Format a diff with an indicator emoji
* @param {number} diff
* @returns {string}
*/
function formatDiffIndicator(diff) {
if (diff > 0) {
return `:red_circle: +${prettyBytes(diff)}`
}
if (diff < 0) {
return `:green_circle: -${prettyBytes(Math.abs(diff))}`
}
return ':white_circle: 0 B'
}
/**
* Import JSON data if it exists
* @template T
* @param {string} filePath
* @returns {Promise<T | undefined>}
*/
async function importJSON(filePath) {
if (!existsSync(filePath)) return undefined
return (await import(filePath, { with: { type: 'json' } })).default
}

View File

@@ -53,7 +53,7 @@
"comfy_base": {
"fg-color": "#222",
"bg-color": "#DDD",
"comfy-menu-bg": "#F5F5F5",
"comfy-menu-bg": "#FFFFFF",
"comfy-menu-hover-bg": "#ccc",
"comfy-menu-secondary-bg": "#EEE",
"comfy-input-bg": "#C9C9C9",

View File

@@ -1,48 +1,88 @@
<template>
<Splitter
:key="sidebarStateKey"
class="splitter-overlay-root splitter-overlay"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
:state-key="sidebarStateKey"
state-storage="local"
>
<SplitterPanel
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'left'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel" />
</SplitterPanel>
<div class="splitter-overlay-root pointer-events-none flex flex-col">
<slot name="workflow-tabs" />
<div
class="pointer-events-none flex flex-1 overflow-hidden"
:class="{
'flex-row': sidebarLocation === 'left',
'flex-row-reverse': sidebarLocation === 'right'
}"
>
<div class="side-toolbar-container">
<slot name="side-toolbar" />
</div>
<SplitterPanel :size="100">
<Splitter
class="splitter-overlay max-w-full"
layout="vertical"
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
state-key="bottom-panel-splitter"
key="main-splitter-stable"
class="splitter-overlay flex-1 overflow-hidden"
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
:state-key="sidebarStateKey || 'main-splitter'"
state-storage="local"
>
<SplitterPanel class="graph-canvas-panel relative">
<slot name="graph-canvas-panel" />
<SplitterPanel
v-if="sidebarLocation === 'left'"
class="side-bar-panel pointer-events-auto"
:min-size="10"
:size="20"
:style="{
display:
sidebarPanelVisible && sidebarLocation === 'left'
? 'flex'
: 'none'
}"
>
<slot
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
name="side-bar-panel"
/>
</SplitterPanel>
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
<slot name="bottom-panel" />
<SplitterPanel :size="80" class="flex flex-col">
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
<Splitter
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
layout="vertical"
:pt:gutter="
'rounded-tl-lg rounded-tr-lg ' +
(bottomPanelVisible ? '' : 'hidden')
"
state-key="bottom-panel-splitter"
state-storage="local"
>
<SplitterPanel class="graph-canvas-panel relative">
<slot name="graph-canvas-panel" />
</SplitterPanel>
<SplitterPanel
v-show="bottomPanelVisible"
class="bottom-panel pointer-events-auto rounded-lg"
>
<slot name="bottom-panel" />
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
v-if="sidebarLocation === 'right'"
class="side-bar-panel pointer-events-auto"
:min-size="10"
:size="20"
:style="{
display:
sidebarPanelVisible && sidebarLocation === 'right'
? 'flex'
: 'none'
}"
>
<slot
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
name="side-bar-panel"
/>
</SplitterPanel>
</Splitter>
</SplitterPanel>
<SplitterPanel
v-show="sidebarPanelVisible"
v-if="sidebarLocation === 'right'"
class="side-bar-panel"
:min-size="10"
:size="20"
>
<slot name="side-bar-panel" />
</SplitterPanel>
</Splitter>
</div>
</div>
</template>
<script setup lang="ts">
@@ -74,7 +114,11 @@ const activeSidebarTabId = computed(
)
const sidebarStateKey = computed(() => {
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
if (unifiedWidth.value) {
return 'unified-sidebar'
}
// When no tab is active, use a default key to maintain state
return activeSidebarTabId.value ?? 'default-sidebar'
})
</script>
@@ -93,12 +137,17 @@ const sidebarStateKey = computed(() => {
.side-bar-panel {
background-color: var(--bg-color);
pointer-events: auto;
}
.bottom-panel {
background-color: var(--bg-color);
pointer-events: auto;
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
max-width: 100%;
overflow-x: auto;
}
.splitter-overlay-bottom :deep(.p-splitter-gutter) {
transform: translateY(5px);
}
.splitter-overlay {

View File

@@ -1,8 +1,7 @@
<template>
<div
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger no-drag"
:style="positionCSS"
class="comfy-menu-hamburger no-drag top-0 right-0"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
@@ -15,14 +14,13 @@
@click="exitFocusMode"
@contextmenu="showNativeSystemMenu"
/>
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
<div class="window-actions-spacer" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import type { CSSProperties } from 'vue'
import { computed, watchEffect } from 'vue'
import { watchEffect } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
@@ -45,15 +43,6 @@ watchEffect(() => {
app.ui.menuContainer.style.display = 'block'
}
})
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
const positionCSS = computed<CSSProperties>(() =>
// 'Bottom' menuSetting shows the hamburger button in the bottom right corner
// 'Disabled', 'Top' menuSetting shows the hamburger button in the top right corner
menuSetting.value === 'Bottom'
? { bottom: '0px', right: '0px' }
: { top: '0px', right: '0px' }
)
</script>
<style scoped>

View File

@@ -0,0 +1,51 @@
<template>
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-2">
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div
class="actionbar-container pointer-events-auto mx-2 flex h-12 items-center rounded-lg px-2 shadow-md"
>
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<LoginButton v-if="!isLoggedIn" />
<CurrentUserButton v-else class="shrink-0" />
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
const workspaceStore = useWorkspaceStore()
const { isLoggedIn } = useCurrentUser()
// Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>()
onMounted(() => {
if (legacyCommandsContainerRef.value) {
app.menu.element.style.width = 'fit-content'
legacyCommandsContainerRef.value.appendChild(app.menu.element)
}
})
</script>
<style scoped>
.actionbar-container {
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
}
</style>

View File

@@ -1,50 +1,69 @@
<template>
<Panel
class="actionbar w-fit"
:style="style"
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
>
<div ref="panelRef" class="actionbar-content flex items-center select-none">
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle cursor-grab w-3 h-max mr-2',
isDragging && 'cursor-grabbing'
)
"
/>
<ComfyQueueButton />
<div class="flex h-full items-center">
<div
v-if="isDragging && !isDocked"
class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
:class="{
'drop-zone-active': isMouseOverDropZone
}"
@mouseenter="onMouseEnterDropZone"
@mouseleave="onMouseLeaveDropZone"
>
{{ t('actionbar.dockToTop') }}
</div>
</Panel>
<Panel
class="actionbar"
:style="style"
:class="{
fixed: !isDocked,
'is-dragging': isDragging,
'is-docked static mr-2 border-none bg-transparent p-0': isDocked
}"
>
<div
ref="panelRef"
class="actionbar-content flex items-center select-none"
>
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle cursor-grab w-3 h-max mr-2',
isDragging && 'cursor-grabbing'
)
"
/>
<ComfyRunButton />
</div>
</Panel>
</div>
</template>
<script lang="ts" setup>
import {
useDraggable,
useElementBounding,
useEventBus,
useEventListener,
useLocalStorage,
watchDebounced
} from '@vueuse/core'
import { clamp } from 'es-toolkit/compat'
import Panel from 'primevue/panel'
import type { Ref } from 'vue'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { cn } from '@/utils/tailwindUtil'
import ComfyQueueButton from './ComfyQueueButton.vue'
import ComfyRunButton from './ComfyRunButton'
const settingsStore = useSettingStore()
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
const visible = computed(() => position.value !== 'Disabled')
const topMenuRef = inject<Ref<HTMLDivElement | null>>('topMenuRef')
const tabContainer = document.querySelector('.workflow-tabs-container')
const panelRef = ref<HTMLElement | null>(null)
const dragHandleRef = ref<HTMLElement | null>(null)
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
@@ -63,11 +82,9 @@ const {
containerElement: document.body,
onMove: (event) => {
// Prevent dragging the menu over the top of the tabs
if (position.value === 'Top') {
const minY = topMenuRef?.value?.getBoundingClientRect().top ?? 40
if (event.y < minY) {
event.y = minY
}
const minY = tabContainer?.getBoundingClientRect().bottom ?? 40
if (event.y < minY) {
event.y = minY
}
}
})
@@ -202,39 +219,38 @@ const adjustMenuPosition = () => {
useEventListener(window, 'resize', adjustMenuPosition)
const topMenuBounds = useElementBounding(topMenuRef)
const overlapThreshold = 20 // pixels
const isOverlappingWithTopMenu = computed(() => {
if (!panelRef.value) {
return false
// Drop zone state
const isMouseOverDropZone = ref(false)
// Mouse event handlers for self-contained drop zone
const onMouseEnterDropZone = () => {
if (isDragging.value) {
isMouseOverDropZone.value = true
}
const { height } = panelRef.value.getBoundingClientRect()
const actionbarBottom = y.value + height
const topMenuBottom = topMenuBounds.bottom.value
}
const overlapPixels =
Math.min(actionbarBottom, topMenuBottom) -
Math.max(y.value, topMenuBounds.top.value)
return overlapPixels > overlapThreshold
})
const onMouseLeaveDropZone = () => {
if (isDragging.value) {
isMouseOverDropZone.value = false
}
}
watch(isDragging, (newIsDragging) => {
if (!newIsDragging) {
// Stop dragging
isDocked.value = isOverlappingWithTopMenu.value
// Handle drag state changes
watch(isDragging, (dragging) => {
if (dragging) {
// Starting to drag - undock if docked
if (isDocked.value) {
isDocked.value = false
}
} else {
// Start dragging
isDocked.value = false
// Stopped dragging - dock if mouse is over drop zone
if (isMouseOverDropZone.value) {
isDocked.value = true
}
// Reset drop zone state
isMouseOverDropZone.value = false
}
})
const eventBus = useEventBus<string>('topMenu')
watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
eventBus.emit('updateHighlight', {
isDragging: dragging,
isOverlapping: overlapping
})
})
</script>
<style scoped>
@@ -242,17 +258,27 @@ watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
.actionbar {
pointer-events: all;
position: fixed;
z-index: 1000;
}
.actionbar.is-docked {
position: static;
@apply bg-transparent border-none p-0;
.actionbar-drop-zone {
width: 265px;
border: 2px dashed var(--p-primary-color);
opacity: 0.8;
}
.actionbar-drop-zone.drop-zone-active {
background: var(--p-highlight-background-focus);
border-color: var(--p-primary-color);
border-width: 3px;
box-shadow: 0 0 20px var(--p-primary-color);
opacity: 1;
transform: scale(1.05);
}
.actionbar.is-dragging {
user-select: none;
pointer-events: none;
}
:deep(.p-panel-content) {

View File

@@ -0,0 +1,19 @@
<template>
<component
:is="currentButton"
:key="isActiveSubscription ? 'queue' : 'subscribe'"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueButton.vue'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
const { isActiveSubscription } = useSubscription()
const currentButton = computed(() =>
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
)
</script>

View File

@@ -8,7 +8,7 @@
showDelay: 600
}"
class="comfyui-queue-button"
:label="activeQueueModeMenuItem.label"
:label="String(activeQueueModeMenuItem?.label ?? '')"
severity="primary"
size="small"
:model="queueModeMenuItems"
@@ -33,7 +33,7 @@
value: item.tooltip,
showDelay: 600
}"
:label="String(item.label)"
:label="String(item.label ?? '')"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"
@@ -82,10 +82,13 @@
import { storeToRefs } from 'pinia'
import Button from 'primevue/button'
import ButtonGroup from 'primevue/buttongroup'
import type { MenuItem } from 'primevue/menuitem'
import SplitButton from 'primevue/splitbutton'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
import {
useQueuePendingTaskCountStore,
@@ -93,43 +96,52 @@ import {
} from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import BatchCountEdit from './BatchCountEdit.vue'
import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => ({
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
instant: {
key: 'instant',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
queueMode.value = 'instant'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
queueMode.value = 'change'
const queueModeMenuItemLookup = computed(() => {
const items: Record<string, MenuItem> = {
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
queueMode.value = 'change'
}
}
}
}))
if (!isCloud) {
items.instant = {
key: 'instant',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
queueMode.value = 'instant'
}
}
}
return items
})
const activeQueueModeMenuItem = computed(
() => queueModeMenuItemLookup.value[queueMode.value]
)
const activeQueueModeMenuItem = computed(() => {
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
return (
queueModeMenuItemLookup.value[queueMode.value] ||
queueModeMenuItemLookup.value.disabled
)
})
const queueModeMenuItems = computed(() =>
Object.values(queueModeMenuItemLookup.value)
)
@@ -141,10 +153,15 @@ const hasPendingTasks = computed(
const commandStore = useCommandStore()
const queuePrompt = async (e: Event) => {
const commandId =
'shiftKey' in e && e.shiftKey
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
const isShiftPressed = 'shiftKey' in e && e.shiftKey
const commandId = isShiftPressed
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
if (isCloud) {
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
}
await commandStore.execute(commandId)
}
</script>

View File

@@ -0,0 +1,7 @@
import { defineAsyncComponent } from 'vue'
import { isCloud } from '@/platform/distribution/types'
export default isCloud && __BUILD_FLAGS__.REQUIRE_SUBSCRIPTION
? defineAsyncComponent(() => import('./CloudRunButtonWrapper.vue'))
: defineAsyncComponent(() => import('./ComfyQueueButton.vue'))

View File

@@ -3,17 +3,42 @@
<Tabs
:key="$i18n.locale"
v-model:value="bottomPanelStore.activeBottomPanelTabId"
style="--p-tabs-tablist-background: var(--comfy-menu-bg)"
>
<TabList pt:tab-list="border-none">
<TabList
pt:tab-list="border-none h-full flex items-center py-2 border-b-1 border-solid"
class="bg-transparent"
>
<div class="flex w-full justify-between">
<div class="tabs-container">
<Tab
v-for="tab in bottomPanelStore.bottomPanelTabs"
:key="tab.id"
:value="tab.id"
class="border-none p-3"
class="m-1 mx-2 border-none"
:class="{
'tab-list-single-item':
bottomPanelStore.bottomPanelTabs.length === 1
}"
:pt:root="
(x: TabPassThroughMethodOptions) => ({
class: {
'p-3 rounded-lg': true,
'pointer-events-none':
bottomPanelStore.bottomPanelTabs.length === 1
},
style: {
color: 'var(--fg-color)',
backgroundColor:
!x.context.active ||
bottomPanelStore.bottomPanelTabs.length === 1
? ''
: 'var(--bg-color)'
}
})
"
>
<span class="font-bold">
<span class="font-normal">
{{ getTabDisplayTitle(tab) }}
</span>
</Tab>
@@ -56,6 +81,7 @@
<script setup lang="ts">
import Button from 'primevue/button'
import Tab from 'primevue/tab'
import type { TabPassThroughMethodOptions } from 'primevue/tab'
import TabList from 'primevue/tablist'
import Tabs from 'primevue/tabs'
import { computed } from 'vue'
@@ -95,3 +121,9 @@ const closeBottomPanel = () => {
bottomPanelStore.activePanel = null
}
</script>
<style scoped>
:deep(.p-tablist-active-bar) {
display: none;
}
</style>

View File

@@ -64,7 +64,6 @@ const terminalCreated = (
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="h-full w-full bg-black">
<div class="h-full w-full bg-transparent">
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>
@@ -94,7 +94,6 @@ const terminalCreated = (
}
:deep(.p-terminal) .xterm-screen {
background-color: black;
overflow-y: hidden;
}
</style>

View File

@@ -1,12 +1,13 @@
<template>
<div
class="subgraph-breadcrumb w-auto"
class="subgraph-breadcrumb w-auto drop-shadow-md"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
}"
:style="{
'--p-breadcrumb-gap': `${ITEM_GAP}px`,
'--p-breadcrumb-gap': `0px`,
'--p-breadcrumb-item-margin': `${ITEM_GAP / 2}px`,
'--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,
'--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,
'--p-breadcrumb-icon-width': `${ICON_WIDTH}px`
@@ -14,8 +15,9 @@
>
<Breadcrumb
ref="breadcrumbRef"
class="bg-transparent p-0"
class="w-fit rounded-lg p-0"
:model="items"
:pt="{ item: { class: 'pointer-events-auto' } }"
aria-label="Graph navigation"
>
<template #item="{ item }">
@@ -174,30 +176,65 @@ onUpdated(() => {
@apply overflow-hidden;
}
:deep(.p-breadcrumb) {
width: 100%;
background-color: transparent;
}
:deep(.p-breadcrumb-item) {
@apply flex items-center rounded-lg overflow-hidden;
@apply flex items-center overflow-hidden;
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem);
/* Collapse middle items first */
flex-shrink: 10000;
}
:deep(.p-breadcrumb-separator) {
display: flex;
padding: 0 var(--p-breadcrumb-item-margin);
}
:deep(.p-breadcrumb-item-link) {
padding: 0
calc(var(--p-breadcrumb-item-margin) + var(--p-breadcrumb-item-padding));
}
:deep(.p-breadcrumb-separator),
:deep(.p-breadcrumb-item) {
@apply h-12;
border-top: 1px solid var(--p-panel-border-color);
border-bottom: 1px solid var(--p-panel-border-color);
background-color: var(--comfy-menu-bg);
}
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-icon-visible)) {
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem + 20px);
}
:deep(.p-breadcrumb-item:first-child) {
@apply rounded-l-lg;
/* Then collapse the root workflow */
flex-shrink: 5000;
border-left: 1px solid var(--p-panel-border-color);
.p-breadcrumb-item-link {
padding-left: var(--p-breadcrumb-item-padding);
}
}
:deep(.p-breadcrumb-item:last-child) {
@apply rounded-r-lg;
/* Then collapse the active item */
flex-shrink: 1;
border-right: 1px solid var(--p-panel-border-color);
}
:deep(.p-breadcrumb-item:hover),
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-menu-visible)) {
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
:deep(.p-breadcrumb-item-link:hover),
:deep(.p-breadcrumb-item-link-menu-visible) {
background-color: color-mix(
in srgb,
var(--fg-color) 10%,
var(--comfy-menu-bg)
) !important;
color: var(--fg-color);
}
</style>
@@ -214,7 +251,7 @@ onUpdated(() => {
.p-breadcrumb-item:nth-last-child(3),
.p-breadcrumb-separator:nth-last-child(2),
.p-breadcrumb-item:nth-last-child(1) {
@apply block;
@apply flex;
}
}
</style>

View File

@@ -6,7 +6,7 @@
showDelay: 512
}"
href="#"
class="p-breadcrumb-item-link cursor-pointer"
class="p-breadcrumb-item-link h-12 cursor-pointer px-2"
:class="{
'flex items-center gap-1': isActive,
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
@@ -15,7 +15,7 @@
}"
@click="handleClick"
>
<span class="p-breadcrumb-item-label">{{ item.label }}</span>
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
</a>
@@ -26,7 +26,7 @@
:popup="true"
:pt="{
root: {
style: 'background-color: var(--comfy-menu-secondary-bg)'
style: 'background-color: var(--comfy-menu-bg)'
},
itemLink: {
class: 'py-2'
@@ -38,7 +38,7 @@
ref="itemInputRef"
v-model="itemLabel"
class="fixed z-10000 px-2 py-2 text-[.8rem]"
@blur="inputBlur(true)"
@blur="inputBlur(false)"
@click.stop
@keydown.enter="inputBlur(true)"
@keydown.esc="inputBlur(false)"
@@ -240,7 +240,6 @@ const inputBlur = async (doRename: boolean) => {
.p-breadcrumb-item-link {
@apply overflow-hidden;
padding: var(--p-breadcrumb-item-padding);
}
.p-breadcrumb-item-label {

View File

@@ -67,10 +67,10 @@
/>
<Button
v-if="!isApiKeyLogin"
class="w-32"
class="w-fit"
variant="text"
severity="danger"
:label="$t('auth.deleteAccount.deleteAccount')"
icon="pi pi-trash"
@click="handleDeleteAccount"
/>
</div>

View File

@@ -2,28 +2,46 @@
<!-- 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">
<template v-if="showUI && workflowTabsPosition === 'Topbar'" #workflow-tabs>
<div
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
>
<!-- Native drag area for Electron -->
<div
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
/>
<div class="flex">
<WorkflowTabs />
<TopbarBadges />
</div>
</div>
</template>
<template v-if="showUI" #side-toolbar>
<SideToolbar />
</template>
<template v-if="!workspaceStore.focusMode" #bottom-panel>
<template v-if="showUI" #side-bar-panel>
<div
class="sidebar-content-container h-full w-full overflow-x-hidden overflow-y-auto"
>
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
</div>
</template>
<template v-if="showUI" #topmenu>
<TopMenuSection />
</template>
<template v-if="showUI" #bottom-panel>
<BottomPanel />
</template>
<template #graph-canvas-panel>
<div class="pointer-events-auto absolute top-0 left-0 w-auto max-w-full">
<SecondRowWorkflowTabs
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
/>
</div>
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
<MiniMap
v-if="comfyAppReady && minimapEnabled"
v-if="comfyAppReady && minimapEnabled && showUI"
class="pointer-events-auto"
/>
</template>
</LiteGraphCanvasSplitterOverlay>
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
<canvas
id="graph-canvas"
ref="canvasRef"
@@ -81,7 +99,9 @@ import {
} from 'vue'
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
import TopMenuSection from '@/components/TopMenuSection.vue'
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
@@ -90,7 +110,8 @@ import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
@@ -129,6 +150,7 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isNativeWindow } from '@/utils/envUtil'
const emit = defineEmits<{
ready: []
@@ -160,6 +182,12 @@ const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
const selectionToolboxEnabled = computed(() =>
settingStore.get('Comfy.Canvas.SelectionToolbox')
)
const activeSidebarTab = computed(() => {
return workspaceStore.sidebarTab.activeSidebarTab
})
const showUI = computed(
() => !workspaceStore.focusMode && betaMenuEnabled.value
)
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))

View File

@@ -10,7 +10,7 @@
></div>
<ButtonGroup
class="absolute right-2 bottom-2 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>

View File

@@ -75,7 +75,7 @@ let promptInput = findPromptInput()
const previousPromptInput = ref<string | null>(null)
const getPreviousResponseId = (index: number) =>
index > 0 ? parsedHistory.value[index - 1]?.response_id ?? '' : ''
index > 0 ? (parsedHistory.value[index - 1]?.response_id ?? '') : ''
const storePromptInput = () => {
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')

View File

@@ -0,0 +1,32 @@
<template>
<svg
:class="iconClass"
:width="size"
:height="size"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.8193 0.600586C15.1248 0.600586 15.3296 0.70893 15.459 0.881836C15.5914 1.05888 15.6471 1.33774 15.5527 1.66895L14.8037 4.30176C14.7063 4.64386 14.4729 4.97024 14.1641 5.21191C13.8544 5.45415 13.496 5.58984 13.1699 5.58984H13.1689L9.5791 5.59668H7.90625C7.52654 5.59668 7.19496 5.84986 7.09082 6.21289L5.69434 11.0889C5.63007 11.3133 5.66134 11.5534 5.77734 11.7529L5.83203 11.8359C5.99177 12.0491 6.24252 12.1758 6.50977 12.1758H6.51074L8.88281 12.1709H11.4971C11.7643 12.171 11.9541 12.254 12.084 12.3906L12.1357 12.4521C12.2685 12.6295 12.3249 12.9089 12.2305 13.2402L11.4805 15.8721C11.383 16.2144 11.1498 16.5415 10.8408 16.7832C10.5314 17.0252 10.1736 17.161 9.84766 17.1611H9.84668L6.25684 17.168H3.64258C3.33762 17.1679 3.13349 17.0588 3.00391 16.8857C2.87135 16.7087 2.81482 16.43 2.90918 16.0986L3.39551 14.3887C3.46841 14.1327 3.41794 13.8576 3.25879 13.6445V13.6436C3.09901 13.4303 2.84745 13.3037 2.58008 13.3037H1.18066C0.875088 13.3037 0.670398 13.1953 0.541016 13.0225C0.408483 12.8451 0.351891 12.5655 0.446289 12.2344L2.11914 6.38965L2.30371 5.74707V5.74609C2.40139 5.40341 2.63456 5.07671 2.94336 4.83496C3.25302 4.59258 3.61143 4.45705 3.9375 4.45703H5.6123C5.94484 4.45703 6.24083 4.26316 6.37891 3.9707L6.42773 3.83984L6.98145 1.89551C7.07894 1.55317 7.31212 1.22614 7.62109 0.984375C7.93074 0.742127 8.2892 0.606445 8.61523 0.606445H8.61621L12.1982 0.600586H14.8193Z"
:stroke="color"
stroke-width="1"
/>
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
size?: number | string
color?: string
class?: string
}
const {
size = 16,
color = 'currentColor',
class: className
} = defineProps<Props>()
const iconClass = computed(() => className || '')
</script>

View File

@@ -106,7 +106,7 @@ const getLabel = (val: string | null | undefined) => {
if (val == null) return label ?? ''
if (!options) return label ?? ''
const found = options.find((o) => o.value === val)
return found ? found.name : label ?? ''
return found ? found.name : (label ?? '')
}
// Extract complex style logic from template

View File

@@ -1,30 +1,28 @@
<template>
<div
class="comfyui-logo-wrapper mr-2 flex cursor-pointer items-center justify-center rounded-md p-1"
class="comfy-menu-button-wrapper flex shrink-0 cursor-pointer flex-col items-center justify-center rounded-t-md p-2 transition-colors"
:class="{
'comfyui-logo-menu-visible': menuRef?.visible
}"
:style="{
minWidth: isLargeSidebar ? '4rem' : 'auto'
'comfy-menu-button-active': menuRef?.visible
}"
@click="menuRef?.toggle($event)"
>
<img
src="/assets/images/comfy-logo-mono.svg"
<ComfyLogoTransparent
alt="ComfyUI Logo"
class="comfyui-logo h-7"
@contextmenu="showNativeSystemMenu"
class="comfyui-logo h-[18px] w-[18px]"
/>
<i class="pi pi-angle-down ml-1 text-[10px]" />
<span
v-if="!isSmall"
class="side-bar-button-label mt-1 text-center text-[10px]"
>{{ t('sideToolbar.labels.menu') }}</span
>
</div>
<TieredMenu
ref="menuRef"
:model="translatedItems"
:popup="true"
class="comfy-command-menu"
:class="{
'comfy-command-menu-top': isTopMenu
}"
@show="onMenuShow"
>
<template #item="{ item, props }">
@@ -48,7 +46,7 @@
v-else-if="
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
"
class="p-menubar-item-icon"
class="p-menubar-item-icon text-sm"
:class="item.icon"
/>
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
@@ -67,8 +65,6 @@
</a>
</template>
</TieredMenu>
<SubgraphBreadcrumb />
</template>
<script setup lang="ts">
@@ -78,38 +74,34 @@ import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
import { computed, nextTick, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
import ComfyLogoTransparent from '@/components/icons/ComfyLogoTransparent.vue'
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useColorPaletteService } from '@/services/colorPaletteService'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
import { showNativeSystemMenu } from '@/utils/envUtil'
import { normalizeI18nKey } from '@/utils/formatUtil'
import { whileMouseDown } from '@/utils/mouseDownUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const { t } = useI18n()
const commandStore = useCommandStore()
const menuItemStore = useMenuItemStore()
const colorPaletteStore = useColorPaletteStore()
const colorPaletteService = useColorPaletteService()
const menuItemsStore = useMenuItemStore()
const commandStore = useCommandStore()
const dialogStore = useDialogStore()
const settingStore = useSettingStore()
const { t } = useI18n()
const managerState = useManagerState()
const { isSmall = false } = defineProps<{
isSmall?: boolean
}>()
const menuRef = ref<
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
>(null)
const isLargeSidebar = computed(
() => settingStore.get('Comfy.Sidebar.Size') !== 'small'
)
const isTopMenu = computed(() => settingStore.get('Comfy.UseNewMenu') === 'Top')
const translateMenuItem = (item: MenuItem): MenuItem => {
const label = typeof item.label === 'function' ? item.label() : item.label
@@ -185,7 +177,7 @@ const extraMenuItems = computed(() => [
])
const translatedItems = computed(() => {
const items = menuItemsStore.menuItems.map(translateMenuItem)
const items = menuItemStore.menuItems.map(translateMenuItem)
let helpIndex = items.findIndex((item) => item.key === 'Help')
let helpItem: MenuItem | undefined
@@ -272,16 +264,24 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
return (
item.parentPath &&
(item.parentPath === 'theme' ||
menuItemsStore.menuItemHasActiveStateChildren[item.parentPath])
menuItemStore.menuItemHasActiveStateChildren[item.parentPath])
)
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
.comfy-menu-button-wrapper {
width: var(--sidebar-width);
height: var(--sidebar-item-height);
}
:deep(.p-menubar-submenu.dropdown-direction-up) {
@apply top-auto bottom-full flex-col-reverse;
.comfy-menu-button-wrapper:hover {
background: var(--p-button-text-secondary-hover-background);
}
.comfy-menu-button-active,
.comfy-menu-button-active:hover {
background-color: var(--content-hover-bg);
}
.keybinding-tag {
@@ -289,11 +289,6 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
border-color: var(--p-content-border-color);
border-style: solid;
}
.comfyui-logo-menu-visible,
.comfyui-logo-wrapper:hover {
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
}
</style>
<style>
@@ -309,22 +304,8 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
transparent
);
}
.comfy-command-menu ul {
background-color: var(--comfy-menu-secondary-bg) !important;
}
.comfy-command-menu-top .p-tieredmenu-submenu {
left: calc(100% + 15px) !important;
top: -4px !important;
}
@media (max-height: 700px) {
.comfy-command-menu .p-tieredmenu-submenu {
@apply absolute max-h-[90vh] overflow-y-auto;
}
/* Help (last) submenu upward offset in compact mode */
.p-tieredmenu-root-list
> .p-tieredmenu-item:last-of-type
.p-tieredmenu-submenu {
top: -188px !important;
}
background-color: var(--comfy-menu-bg) !important;
}
</style>

View File

@@ -1,48 +1,69 @@
<template>
<teleport :to="teleportTarget">
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
<SidebarIcon
v-for="tab in tabs"
:key="tab.id"
:icon="tab.icon"
:icon-badge="tab.iconBadge"
:tooltip="tab.tooltip"
:tooltip-suffix="getTabTooltipSuffix(tab)"
:label="tab.label || tab.title"
:is-small="isSmall"
:selected="tab.id === selectedTab?.id"
:class="tab.id + '-tab-button'"
@click="onTabClick(tab)"
/>
<SidebarTemplatesButton />
<div class="side-tool-bar-end">
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
<SidebarHelpCenterIcon />
<SidebarBottomPanelToggleButton />
<SidebarShortcutsToggleButton />
</div>
</nav>
</teleport>
<div
v-if="selectedTab"
class="sidebar-content-container h-full overflow-x-hidden overflow-y-auto"
<nav
ref="sideToolbarRef"
class="side-tool-bar-container flex h-full flex-col items-center bg-transparent [.floating-sidebar]:-mr-2"
:class="{
'small-sidebar': isSmall,
'connected-sidebar': isConnected,
'floating-sidebar': !isConnected,
'overflowing-sidebar': isOverflowing
}"
>
<ExtensionSlot :extension="selectedTab" />
</div>
<div
ref="contentMeasureRef"
:class="
isOverflowing
? 'side-tool-bar-container overflow-y-auto'
: 'flex flex-col h-full'
"
>
<div ref="topToolbarRef" :class="groupClasses">
<ComfyMenuButton :is-small="isSmall" />
<SidebarIcon
v-for="tab in tabs"
:key="tab.id"
:icon="tab.icon"
:icon-badge="tab.iconBadge"
:tooltip="tab.tooltip"
:tooltip-suffix="getTabTooltipSuffix(tab)"
:label="tab.label || tab.title"
:is-small="isSmall"
:selected="tab.id === selectedTab?.id"
:class="tab.id + '-tab-button'"
@click="onTabClick(tab)"
/>
<SidebarTemplatesButton />
</div>
<div ref="bottomToolbarRef" class="mt-auto" :class="groupClasses">
<SidebarLogoutIcon
v-if="userStore.isMultiUserServer"
:is-small="isSmall"
/>
<SidebarHelpCenterIcon :is-small="isSmall" />
<SidebarBottomPanelToggleButton :is-small="isSmall" />
<SidebarShortcutsToggleButton :is-small="isSmall" />
</div>
</div>
</nav>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useResizeObserver } from '@vueuse/core'
import { debounce } from 'es-toolkit/compat'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
import { cn } from '@/utils/tailwindUtil'
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
import SidebarIcon from './SidebarIcon.vue'
@@ -53,16 +74,25 @@ const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore()
const userStore = useUserStore()
const commandStore = useCommandStore()
const teleportTarget = computed(() =>
settingStore.get('Comfy.Sidebar.Location') === 'left'
? '.comfyui-body-left'
: '.comfyui-body-right'
)
const canvasStore = useCanvasStore()
const sideToolbarRef = ref<HTMLElement>()
const contentMeasureRef = ref<HTMLElement>()
const topToolbarRef = ref<HTMLElement>()
const bottomToolbarRef = ref<HTMLElement>()
const isSmall = computed(
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
)
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))
const isConnected = computed(
() =>
selectedTab.value ||
isOverflowing.value ||
sidebarStyle.value === 'connected'
)
const tabs = computed(() => workspaceStore.getSidebarTabs())
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
@@ -79,6 +109,68 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
)
return keybinding ? ` (${keybinding.combo.toString()})` : ''
}
const isOverflowing = ref(false)
const groupClasses = computed(() =>
cn(
'sidebar-item-group pointer-events-auto flex flex-col items-center overflow-hidden flex-shrink-0' +
(isConnected.value ? '' : ' rounded-lg shadow-md')
)
)
const ENTER_OVERFLOW_MARGIN = 20
const EXIT_OVERFLOW_MARGIN = 50
const checkOverflow = debounce(() => {
if (!sideToolbarRef.value || !topToolbarRef.value || !bottomToolbarRef.value)
return
const containerHeight = sideToolbarRef.value.clientHeight
const topHeight = topToolbarRef.value.scrollHeight
const bottomHeight = bottomToolbarRef.value.scrollHeight
const contentHeight = topHeight + bottomHeight
if (isOverflowing.value) {
isOverflowing.value = containerHeight < contentHeight + EXIT_OVERFLOW_MARGIN
} else {
isOverflowing.value =
containerHeight < contentHeight + ENTER_OVERFLOW_MARGIN
}
}, 16)
onMounted(() => {
if (!sideToolbarRef.value) return
const overflowObserver = useResizeObserver(
sideToolbarRef.value,
checkOverflow
)
checkOverflow()
onBeforeUnmount(() => {
overflowObserver.stop()
})
watch(
[isSmall, sidebarLocation],
async () => {
if (canvasStore.canvas) {
if (sidebarLocation.value === 'left') {
await nextTick()
canvasStore.canvas.fpsInfoLocation = [
sideToolbarRef.value?.getBoundingClientRect()?.right,
null
]
} else {
canvasStore.canvas.fpsInfoLocation = null
}
canvasStore.canvas.setDirty(false, true)
}
},
{ immediate: true }
)
})
</script>
<style>
@@ -88,36 +180,64 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
* but need to reference sidebar dimensions for proper positioning.
*/
:root {
--sidebar-width: 4rem;
--sidebar-padding: 8px;
--sidebar-icon-size: 1rem;
--sidebar-default-floating-width: 56px;
--sidebar-default-connected-width: calc(
var(--sidebar-default-floating-width) + var(--sidebar-padding) * 2
);
--sidebar-default-item-height: 56px;
--sidebar-small-floating-width: 48px;
--sidebar-small-connected-width: calc(
var(--sidebar-small-floating-width) + var(--sidebar-padding) * 2
);
--sidebar-small-item-height: 48px;
--sidebar-width: var(--sidebar-default-floating-width);
--sidebar-item-height: var(--sidebar-default-item-height);
}
:root:has(.side-tool-bar-container.small-sidebar) {
--sidebar-width: 2.5rem;
--sidebar-width: var(--sidebar-small-floating-width);
--sidebar-item-height: var(--sidebar-small-item-height);
}
:root:has(.side-tool-bar-container.connected-sidebar) {
--sidebar-width: var(--sidebar-default-connected-width);
}
:root:has(.side-tool-bar-container.small-sidebar.connected-sidebar) {
--sidebar-width: var(--sidebar-small-connected-width);
}
</style>
<style scoped>
.side-tool-bar-container {
display: flex;
flex-direction: column;
align-items: center;
@reference "tailwindcss";
width: var(--sidebar-width);
height: 100%;
background-color: var(--comfy-menu-secondary-bg);
color: var(--fg-color);
box-shadow: var(--bar-shadow);
.floating-sidebar {
padding: var(--sidebar-padding);
}
.side-tool-bar-container.small-sidebar {
--sidebar-width: 2.5rem;
--sidebar-icon-size: 1rem;
.floating-sidebar .sidebar-item-group {
border-color: var(--p-panel-border-color);
}
.side-tool-bar-end {
align-self: flex-end;
margin-top: auto;
.connected-sidebar {
padding: var(--sidebar-padding) 0;
background-color: var(--comfy-menu-bg);
}
.sidebar-item-group {
background-color: var(--comfy-menu-bg);
border: 1px solid transparent;
}
.overflowing-sidebar :deep(.comfy-menu-button-wrapper) {
position: sticky;
top: 0;
z-index: 1;
background-color: var(--comfy-menu-bg);
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<SidebarIcon
label="sideToolbar.labels.console"
:tooltip="$t('menu.toggleBottomPanel')"
:selected="bottomPanelStore.activePanel == 'terminal'"
@click="bottomPanelStore.toggleBottomPanel"

View File

@@ -3,8 +3,10 @@
<SidebarIcon
icon="pi pi-question-circle"
class="comfy-help-center-btn"
label="menu.help"
:tooltip="$t('sideToolbar.helpCenter')"
:icon-badge="shouldShowRedDot ? '' : ''"
:is-small="isSmall"
@click="toggleHelpCenter"
/>
@@ -16,7 +18,7 @@
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
'small-sidebar': isSmall
}"
>
<HelpCenterMenuContent @close="closeHelpCenter" />
@@ -29,7 +31,7 @@
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
'small-sidebar': isSmall
}"
/>
</Teleport>
@@ -40,7 +42,7 @@
:class="{
'sidebar-left': sidebarLocation === 'left',
'sidebar-right': sidebarLocation === 'right',
'small-sidebar': sidebarSize === 'small'
'small-sidebar': isSmall
}"
@whats-new-dismissed="handleWhatsNewDismissed"
/>
@@ -59,7 +61,7 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { computed, onMounted } from 'vue'
import { computed, onMounted, toRefs } from 'vue'
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -87,6 +89,11 @@ const { showNodeConflictDialog } = useDialogService()
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
useConflictAcknowledgment()
const props = defineProps<{
isSmall: boolean
}>()
const { isSmall } = toRefs(props)
// Use either release red dot or conflict red dot
const shouldShowRedDot = computed((): boolean => {
const releaseRedDot = showReleaseRedDot.value
@@ -97,8 +104,6 @@ const sidebarLocation = computed(() =>
settingStore.get('Comfy.Sidebar.Location')
)
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
const toggleHelpCenter = () => {
helpCenterStore.toggle()
}

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