Compare commits

..

1 Commits

Author SHA1 Message Date
Benjamin Lu
799ab5e5aa Refactor slotkey from bespoke string key 2025-10-10 12:58:25 -07:00
358 changed files with 3651 additions and 10999 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-draft-create.yaml --limit=1
gh run list --workflow=release.yaml --limit=1
```
4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
4. **For Minor/Major Version Releases**: The create-release-candidate-branch 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=release-branch-create.yaml --limit=1
gh run list --workflow=create-release-candidate-branch.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-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
WORKFLOW_RUN_ID=$(gh run list --workflow=release.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-draft-create.yaml --limit=1
gh run list --workflow=release.yaml --limit=1
gh run watch
```
5. Track progress:

View File

@@ -1,21 +0,0 @@
# 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,4 @@
name: PR Backport
name: Auto Backport
on:
pull_request_target:
@@ -95,61 +95,41 @@ jobs:
echo "skip=true" >> $GITHUB_OUTPUT
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
- name: Collect backport targets
- name: Extract version labels
if: steps.check-existing.outputs.skip != 'true'
id: targets
id: versions
run: |
TARGETS=()
declare -A SEEN=()
# Extract version labels (e.g., "1.24", "1.22")
VERSIONS=""
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
add_target() {
local label="$1"
local target="$2"
if [ -z "$target" ]; then
return
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
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')"
if [ -z "$VERSIONS" ]; then
echo "::error::No version labels found (e.g., 1.24, 1.22)"
exit 1
fi
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
echo "Found backport targets: ${TARGETS[*]}"
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
echo "Found version labels: ${VERSIONS}"
- name: Backport commits
if: steps.check-existing.outputs.skip != 'true'
@@ -170,17 +150,16 @@ jobs:
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
fi
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}"
for version in ${{ steps.versions.outputs.versions }}; do
echo "::group::Backporting to core/${version}"
echo "::group::Backporting to ${TARGET_BRANCH}"
TARGET_BRANCH="core/${version}"
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
# 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}${TARGET_BRANCH}:branch-missing "
FAILED="${FAILED}${version}:branch-missing "
echo "::endgroup::"
continue
fi
@@ -191,7 +170,7 @@ jobs:
# Try cherry-pick
if git cherry-pick "${MERGE_COMMIT}"; then
git push origin "${BACKPORT_BRANCH}"
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
# Return to main (keep the branch, we need it for PR)
git checkout main
@@ -201,7 +180,7 @@ jobs:
git cherry-pick --abort
echo "::error::Cherry-pick failed due to conflicts"
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
# Clean up the failed branch
git checkout main
@@ -235,13 +214,13 @@ jobs:
fi
for backport in ${{ steps.backport.outputs.success }}; do
IFS=':' read -r target branch <<< "${backport}"
IFS=':' read -r version branch <<< "${backport}"
if PR_URL=$(gh pr create \
--base "${target}" \
--base "core/${version}" \
--head "${branch}" \
--title "[backport ${target}] ${PR_TITLE}" \
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
--title "[backport ${version}] ${PR_TITLE}" \
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
--label "backport" 2>&1); then
# Extract PR number from URL
@@ -251,9 +230,9 @@ jobs:
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
fi
else
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
echo "::error::Failed to create PR for ${version}: ${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 \`${target}\`. Please create the PR manually from branch \`${branch}\`"
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}\`"
fi
done
@@ -274,16 +253,16 @@ jobs:
fi
for failure in ${{ steps.backport.outputs.failed }}; do
IFS=':' read -r target reason conflicts <<< "${failure}"
IFS=':' read -r version reason conflicts <<< "${failure}"
if [ "${reason}" = "branch-missing" ]; then
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` 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 \`${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>"
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>"
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
fi
done

View File

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

View File

@@ -1,4 +1,4 @@
name: Release PyPI Dev
name: Create Dev PyPI Package
on:
workflow_dispatch:
@@ -25,6 +25,17 @@ 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
@@ -33,7 +44,6 @@ 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,4 @@
name: Release Branch Create
name: Create Release Branch
on:
pull_request:

View File

@@ -1,4 +1,4 @@
name: Release Draft Create
name: Create Release Draft
on:
pull_request:
@@ -28,6 +28,16 @@ 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
@@ -45,7 +55,6 @@ 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
@@ -117,7 +126,7 @@ jobs:
publish_types:
needs: build
uses: ./.github/workflows/release-npm-types.yaml
uses: ./.github/workflows/publish-frontend-types.yaml
with:
version: ${{ needs.build.outputs.version }}
ref: ${{ github.event.pull_request.merge_commit_sha }}

View File

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

View File

@@ -1,5 +1,4 @@
name: "CI: Lint Format"
description: "Linting and code formatting validation for pull requests"
name: Lint and Format
on:
pull_request:
@@ -33,6 +32,21 @@ 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,9 +1,8 @@
name: "CI: Tests E2E (Deploy for Forks)"
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
name: PR Playwright Deploy (Forks)
on:
workflow_run:
workflows: ["CI: Tests E2E"]
workflows: ["Tests CI"]
types: [requested, completed]
env:

View File

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

View File

@@ -44,7 +44,6 @@ jobs:
contents: read
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ENABLE_MINIFY: 'true'
steps:
- name: Validate inputs
env:
@@ -161,6 +160,20 @@ jobs:
echo "publish_dir=$PUBLISH_DIR" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
- name: Pack (preview only)
shell: bash
working-directory: ${{ steps.pkg.outputs.publish_dir }}
run: |
set -euo pipefail
npm pack --json | tee pack-result.json
- name: Upload package tarball artifact
uses: actions/upload-artifact@v4
with:
name: desktop-ui-npm-tarball-${{ inputs.version }}
path: ${{ steps.pkg.outputs.publish_dir }}/*.tgz
if-no-files-found: error
- name: Check if version already on npm
id: check_npm
env:

View File

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

View File

@@ -1,52 +0,0 @@
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

View File

@@ -1,104 +0,0 @@
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

@@ -1,5 +1,6 @@
name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
name: Storybook and Chromatic CI
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
on:
workflow_dispatch: # Allow manual triggering
@@ -50,6 +51,19 @@ 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
@@ -102,6 +116,19 @@ 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,5 +1,4 @@
name: "CI: Tests E2E"
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
name: Tests CI
on:
push:

View File

@@ -1,5 +1,4 @@
name: 'Api: Update Registry API Types'
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
name: Update Comfy Registry API Types
on:
# Manual trigger
@@ -30,9 +29,26 @@ 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,5 +1,4 @@
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'
name: Update ComfyUI-Manager API Types
on:
# Manual trigger
@@ -31,9 +30,26 @@ 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,5 +1,4 @@
name: 'Api: Update Electron API Types'
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
name: Update Electron Types
on:
workflow_dispatch:
@@ -26,6 +25,15 @@ 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,4 @@
name: i18n Update Custom Nodes
name: Update Locales for given custom node repository
on:
workflow_dispatch:

View File

@@ -1,5 +1,4 @@
name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
name: Update Locales
on:
# Manual dispatch for urgent translation updates
@@ -19,14 +18,14 @@ jobs:
uses: actions/checkout@v5
# Setup playwright environment
- name: Setup ComfyUI Frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup ComfyUI Server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup ComfyUI Frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
@@ -55,5 +54,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"
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
git push origin HEAD:${{ github.head_ref }}

View File

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

View File

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

View File

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

View File

@@ -14,11 +14,6 @@ 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:
@@ -31,25 +26,8 @@ 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:
@@ -86,10 +64,8 @@ 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: ${{ github.event.inputs.branch }}
base: main
labels: |
Release

View File

@@ -1,5 +1,4 @@
name: "Release: Version Bump"
description: "Manual workflow to increment package version with semantic versioning support"
name: Version Bump
on:
workflow_dispatch:
@@ -15,11 +14,6 @@ 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:
@@ -31,24 +25,6 @@ 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
@@ -59,6 +35,7 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Bump version
id: bump-version
@@ -81,9 +58,7 @@ 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: ${{ github.event.inputs.branch }}
base: main
labels: |
Release

View File

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

2
.gitignore vendored
View File

@@ -78,7 +78,7 @@ templates_repo/
vite.config.mts.timestamp-*.mjs
# Linux core dumps
/core
./core
*storybook.log
storybook-static

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Run Knip with cache via package script
pnpm knip 1>&2
pnpm knip

View File

@@ -7,5 +7,12 @@
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"]
"overrides": [
{
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
"options": {
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
}
]
}

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>ComfyUI</title>
<title>ComfyUI Desktop</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
</head>
<body>

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/desktop-ui",
"version": "0.0.3",
"version": "0.0.1",
"type": "module",
"nx": {
"tags": [

View File

@@ -1,17 +1,17 @@
<template>
<div
class="mx-auto grid h-[40rem] w-full max-w-3xl grid-rows-[1fr_auto_auto_1fr] select-none"
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
>
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
{{ $t('install.gpuPicker.title') }}
</h2>
<!-- GPU Selection buttons - takes up remaining space and centers content -->
<div class="flex flex-1 items-center justify-center gap-8">
<div class="flex-1 flex gap-8 justify-center items-center">
<!-- Apple Metal / NVIDIA -->
<HardwareOption
v-if="platform === 'darwin'"
:image-path="'./assets/images/apple-mps-logo.png'"
:image-path="'/assets/images/apple-mps-logo.png'"
placeholder-text="Apple Metal"
subtitle="Apple Metal"
:value="'mps'"
@@ -21,7 +21,7 @@
/>
<HardwareOption
v-else
:image-path="'./assets/images/nvidia-logo-square.jpg'"
:image-path="'/assets/images/nvidia-logo-square.jpg'"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:value="'nvidia'"
@@ -47,17 +47,17 @@
/>
</div>
<div class="h-16 px-24 pt-12">
<div class="pt-12 px-24 h-16">
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
<Tag
:value="$t('install.gpuPicker.recommended')"
class="rounded-full bg-neutral-300 px-2 py-[1px] text-sm font-bold text-neutral-900"
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
/>
<i class="icon-[lucide--badge-check] text-lg text-neutral-300" />
<i class="icon-[lucide--badge-check] text-neutral-300 text-lg" />
</div>
</div>
<div class="px-24 text-neutral-300">
<div class="text-neutral-300 px-24">
<p v-show="descriptionText" class="leading-relaxed">
{{ descriptionText }}
</p>

View File

@@ -1,23 +1,23 @@
<template>
<div
class="task-div relative grid min-h-52 max-w-48"
class="task-div max-w-48 min-h-52 grid relative"
:class="{ 'opacity-75': isLoading }"
>
<Card
class="relative h-full max-w-48 overflow-hidden"
class="max-w-48 relative h-full 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 absolute top-0 -right-14 m-2 text-red-500 opacity-15"
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
style="font-size: 10rem"
/>
<img
v-if="task.headerImg"
:src="task.headerImg"
class="h-full w-full object-contain px-4 pt-4 opacity-25"
class="object-contain w-full h-full opacity-25 pt-4 px-4"
/>
</template>
<template #title>
@@ -27,7 +27,7 @@
{{ description }}
</template>
<template #footer>
<div class="mt-1 flex gap-4">
<div class="flex gap-4 mt-1">
<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,163 +1,67 @@
// 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 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 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 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) {
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
return {
...main,
nodeDefs: nodes,
commands: commands,
settings: settings
} as M & { nodeDefs: N; commands: C; settings: S }
}
// 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)
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)
}
// 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

@@ -66,6 +66,17 @@
@click="troubleshoot"
/>
</div>
<div class="text-center">
<button
v-if="!terminalVisible"
class="text-sm text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-2 mx-auto"
@click="terminalVisible = true"
>
<i class="pi pi-search"></i>
{{ $t('serverStart.showTerminal') }}
</button>
</div>
</div>
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->

View File

@@ -1,5 +1,6 @@
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { test as base, expect } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
@@ -46,10 +47,6 @@ class ComfyMenu {
.nth(0)
}
get buttons() {
return this.sideToolbar.locator('.side-bar-button')
}
get nodeLibraryTab() {
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
return this._nodeLibraryTab
@@ -133,8 +130,7 @@ export class ComfyPage {
// Buttons
public readonly resetViewButton: Locator
public readonly queueButton: Locator // Run button in Legacy UI
public readonly runButton: Locator // Run button (renamed "Queue" -> "Run")
public readonly queueButton: Locator
// Inputs
public readonly workflowUploadInput: Locator
@@ -169,9 +165,6 @@ export class ComfyPage {
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.runButton = page
.getByTestId('queue-button')
.getByRole('button', { name: 'Run' })
this.workflowUploadInput = page.locator('#comfy-file-input')
this.visibleToasts = page.locator('.p-toast-message:visible')
@@ -1093,6 +1086,12 @@ export class ComfyPage {
const targetPosition = await targetSlot.getPosition()
// Debug: Log the positions we're trying to use
console.log('Drag positions:', {
source: sourcePosition,
target: targetPosition
})
await this.dragAndDrop(sourcePosition, targetPosition)
await this.nextFrame()
}

View File

@@ -3,8 +3,6 @@
*/
import type { Locator, Page } from '@playwright/test'
import { VueNodeFixture } from './utils/vueNodeFixtures'
export class VueNodeHelpers {
constructor(private page: Page) {}
@@ -108,24 +106,6 @@ export class VueNodeHelpers {
await this.page.keyboard.press('Backspace')
}
/**
* Return a DOM-focused VueNodeFixture for the first node matching the title.
* Resolves the node id up front so subsequent interactions survive title changes.
*/
async getFixtureByTitle(title: string): Promise<VueNodeFixture> {
const node = this.getNodeByTitle(title).first()
await node.waitFor({ state: 'visible' })
const nodeId = await node.evaluate((el) => el.getAttribute('data-node-id'))
if (!nodeId) {
throw new Error(
`Vue node titled "${title}" is missing its data-node-id attribute`
)
}
return new VueNodeFixture(this.getNodeLocator(nodeId))
}
/**
* Wait for Vue nodes to be rendered
*/

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('.comfy-menu-button-wrapper')
this.menuTrigger = page.locator('.comfyui-logo-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: 300, y: 10 } })
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
await expect(this.menuLocator).not.toBeVisible()
}

View File

@@ -1,66 +1,131 @@
import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
/** DOM-centric helper for a single Vue-rendered node on the canvas. */
import type { NodeReference } from './litegraphUtils'
/**
* VueNodeFixture provides Vue-specific testing utilities for interacting with
* Vue node components. It bridges the gap between litegraph node references
* and Vue UI components.
*/
export class VueNodeFixture {
constructor(private readonly locator: Locator) {}
constructor(
private readonly nodeRef: NodeReference,
private readonly page: Page
) {}
get header(): Locator {
return this.locator.locator('[data-testid^="node-header-"]')
/**
* Get the node's header element using data-testid
*/
async getHeader(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-header-${nodeId}"]`)
}
get title(): Locator {
return this.locator.locator('[data-testid="node-title"]')
}
get titleInput(): Locator {
return this.locator.locator('[data-testid="node-title-input"]')
}
get body(): Locator {
return this.locator.locator('[data-testid^="node-body-"]')
}
get collapseButton(): Locator {
return this.locator.locator('[data-testid="node-collapse-button"]')
}
get collapseIcon(): Locator {
return this.collapseButton.locator('i')
}
get root(): Locator {
return this.locator
/**
* Get the node's title element
*/
async getTitleElement(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-title"]')
}
/**
* Get the current title text
*/
async getTitle(): Promise<string> {
return (await this.title.textContent()) ?? ''
const titleElement = await this.getTitleElement()
return (await titleElement.textContent()) || ''
}
async setTitle(value: string): Promise<void> {
await this.header.dblclick()
const input = this.titleInput
await expect(input).toBeVisible()
await input.fill(value)
/**
* Set a new title by double-clicking and entering text
*/
async setTitle(newTitle: string): Promise<void> {
const titleElement = await this.getTitleElement()
await titleElement.dblclick()
const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill(newTitle)
await input.press('Enter')
}
/**
* Cancel title editing
*/
async cancelTitleEdit(): Promise<void> {
await this.header.dblclick()
const input = this.titleInput
await expect(input).toBeVisible()
const titleElement = await this.getTitleElement()
await titleElement.dblclick()
const input = (await this.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.press('Escape')
}
/**
* Check if the title is currently being edited
*/
async isEditingTitle(): Promise<boolean> {
const header = await this.getHeader()
const input = header.locator('[data-testid="node-title-input"]')
return await input.isVisible()
}
/**
* Get the collapse/expand button
*/
async getCollapseButton(): Promise<Locator> {
const header = await this.getHeader()
return header.locator('[data-testid="node-collapse-button"]')
}
/**
* Toggle the node's collapsed state
*/
async toggleCollapse(): Promise<void> {
await this.collapseButton.click()
const button = await this.getCollapseButton()
await button.click()
}
/**
* Get the collapse icon element
*/
async getCollapseIcon(): Promise<Locator> {
const button = await this.getCollapseButton()
return button.locator('i')
}
/**
* Get the collapse icon's CSS classes
*/
async getCollapseIconClass(): Promise<string> {
return (await this.collapseIcon.getAttribute('class')) ?? ''
const icon = await this.getCollapseIcon()
return (await icon.getAttribute('class')) || ''
}
boundingBox(): ReturnType<Locator['boundingBox']> {
return this.locator.boundingBox()
/**
* Check if the collapse button is visible
*/
async isCollapseButtonVisible(): Promise<boolean> {
const button = await this.getCollapseButton()
return await button.isVisible()
}
/**
* Get the node's body/content element
*/
async getBody(): Promise<Locator> {
const nodeId = this.nodeRef.id
return this.page.locator(`[data-testid="node-body-${nodeId}"]`)
}
/**
* Check if the node body is visible (not collapsed)
*/
async isBodyVisible(): Promise<boolean> {
const body = await this.getBody()
return await body.isVisible()
}
}

View File

@@ -13,7 +13,7 @@ export class ComfyActionbar {
async isDocked() {
const className = await this.root.getAttribute('class')
return className?.includes('static') ?? false
return className?.includes('is-docked') ?? false
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -39,15 +39,15 @@ test.describe('Graph Canvas Menu', () => {
)
})
test('Toggle minimap button is clickable and has correct test id', async ({
test('Focus mode button is clickable and has correct test id', async ({
comfyPage
}) => {
const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapButton).toBeVisible()
await expect(minimapButton).toBeEnabled()
const focusButton = comfyPage.page.getByTestId('focus-mode-button')
await expect(focusButton).toBeVisible()
await expect(focusButton).toBeEnabled()
// Test that the button can be clicked without error
await minimapButton.click()
await focusButton.click()
await comfyPage.nextFrame()
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -233,7 +233,6 @@ 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()
@@ -254,6 +253,8 @@ 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

@@ -3,10 +3,10 @@ import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import {
type ComfyPage,
comfyPageFixture as test,
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
@@ -786,25 +786,24 @@ test.describe('Viewport settings', () => {
// Screenshot the canvas element
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await toggleButton.click()
// close zoom menu
await zoomControlsButton.click()
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame()
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)
@@ -816,6 +815,9 @@ 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,7 +8,9 @@ test.describe('Menu', () => {
})
test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.buttons.count()
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
(el) => el.children.length
)
await comfyPage.page.evaluate(async () => {
window['app'].extensionManager.registerSidebarTab({
@@ -24,7 +26,9 @@ test.describe('Menu', () => {
})
await comfyPage.nextFrame()
const newChildrenCount = await comfyPage.menu.buttons.count()
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
(el) => el.children.length
)
expect(newChildrenCount).toBe(initialChildrenCount + 1)
})

View File

@@ -35,6 +35,12 @@ test.describe('Minimap', () => {
})
test('Validate minimap toggle button state', async ({ comfyPage }) => {
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(toggleButton).toBeVisible()
@@ -45,6 +51,13 @@ test.describe('Minimap', () => {
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
// Open zoom controls dropdown first
const zoomControlsButton = comfyPage.page.getByTestId(
'zoom-controls-button'
)
await zoomControlsButton.click()
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
await expect(minimapContainer).toBeVisible()
@@ -54,10 +67,22 @@ test.describe('Minimap', () => {
await expect(minimapContainer).not.toBeVisible()
// Open zoom controls dropdown again
await zoomControlsButton.click()
await comfyPage.nextFrame()
await expect(toggleButton).toContainText('Show Minimap')
await toggleButton.click()
await comfyPage.nextFrame()
await expect(minimapContainer).toBeVisible()
// Open zoom controls dropdown again to verify button text
await zoomControlsButton.click()
await comfyPage.nextFrame()
await expect(toggleButton).toContainText('Hide Minimap')
})
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {

View File

@@ -1,28 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,7 +1,6 @@
import type { Locator, Page } from '@playwright/test'
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
import {
comfyExpect as expect,
comfyPageFixture as test
@@ -60,14 +59,18 @@ async function getInputLinkDetails(
)
}
// Test helpers to reduce repetition across cases
function slotLocator(
page: Page,
nodeId: NodeId,
slotIndex: number,
isInput: boolean
) {
const key = getSlotKey(String(nodeId), slotIndex, isInput)
return page.locator(`[data-slot-key="${key}"]`)
const type = isInput ? 'input' : 'output'
const id = String(nodeId)
return page.locator(
`[data-node-id="${id}"][data-slot-type="${type}"][data-slot-index="${slotIndex}"]`
)
}
async function expectVisibleAll(...locators: Locator[]) {
@@ -788,45 +791,6 @@ test.describe('Vue Node Link Interaction', () => {
})
})
test('should batch disconnect all links with ctrl+alt+click on slot', async ({
comfyPage
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
expect(clipNode && samplerNode).toBeTruthy()
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 1 },
() => comfyPage.nextFrame()
)
await connectSlots(
comfyPage.page,
{ nodeId: clipNode.id, index: 0 },
{ nodeId: samplerNode.id, index: 2 },
() => comfyPage.nextFrame()
)
const clipOutput = await clipNode.getOutput(0)
expect(await clipOutput.getLinkCount()).toBe(2)
const clipOutputSlot = slotLocator(comfyPage.page, clipNode.id, 0, false)
await clipOutputSlot.dispatchEvent('pointerdown', {
button: 0,
buttons: 1,
ctrlKey: true,
altKey: true,
shiftKey: false,
bubbles: true,
cancelable: true
})
await comfyPage.nextFrame()
expect(await clipOutput.getLinkCount()).toBe(0)
})
test.describe('Release actions (Shift-drop)', () => {
test('Context menu opens and endpoint is pinned on Shift-drop', async ({
comfyPage,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -2,46 +2,70 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures'
test.describe('Vue Nodes Renaming', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.vueNodes.waitForNodes()
})
test('should display node title', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect(vueNode.header).toContainText('KSampler')
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
expect(nodes.length).toBeGreaterThanOrEqual(1)
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
const title = await vueNode.getTitle()
expect(title).toBe('KSampler')
// Verify title is visible in the header
const header = await vueNode.getHeader()
await expect(header).toContainText('KSampler')
})
test('should allow title renaming by double clicking on the node header', async ({
comfyPage
}) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
// Test renaming with Enter
await vueNode.setTitle('My Custom Sampler')
await expect(await vueNode.getTitle()).toBe('My Custom Sampler')
await expect(vueNode.header).toContainText('My Custom Sampler')
const newTitle = await vueNode.getTitle()
expect(newTitle).toBe('My Custom Sampler')
// Verify the title is displayed
const header = await vueNode.getHeader()
await expect(header).toContainText('My Custom Sampler')
// Test cancel with Escape
await vueNode.title.dblclick()
const titleElement = await vueNode.getTitleElement()
await titleElement.dblclick()
await comfyPage.nextFrame()
await vueNode.titleInput.fill('This Should Be Cancelled')
await vueNode.titleInput.press('Escape')
// Type a different value but cancel
const input = (await vueNode.getHeader()).locator(
'[data-testid="node-title-input"]'
)
await input.fill('This Should Be Cancelled')
await input.press('Escape')
await comfyPage.nextFrame()
// Title should remain as the previously saved value
await expect(await vueNode.getTitle()).toBe('My Custom Sampler')
const titleAfterCancel = await vueNode.getTitle()
expect(titleAfterCancel).toBe('My Custom Sampler')
})
test('Double click node body does not trigger edit', async ({
comfyPage
}) => {
const loadCheckpointNode = comfyPage.vueNodes
.getNodeByTitle('Load Checkpoint')
.first()
const loadCheckpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBbox = await loadCheckpointNode.boundingBox()
if (!nodeBbox) throw new Error('Node not found')
await loadCheckpointNode.dblclick()

View File

@@ -50,23 +50,15 @@ test.describe('Vue Node Selection', () => {
})
}
test('should select all nodes with ctrl+a', async ({ comfyPage }) => {
const initialCount = await comfyPage.vueNodes.getNodeCount()
expect(initialCount).toBeGreaterThan(0)
await comfyPage.canvas.press('Control+a')
const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount()
expect(selectedCount).toBe(initialCount)
})
test('should select pinned node without dragging', async ({ comfyPage }) => {
const PIN_HOTKEY = 'p'
const PIN_INDICATOR = '[data-testid="node-pin-indicator"]'
// Select a node by clicking its title
const checkpointNodeHeader = comfyPage.page.getByText('Load Checkpoint')
await checkpointNodeHeader.click()
// Pin it using the hotkey (as a user would)
await comfyPage.page.keyboard.press(PIN_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')

View File

@@ -12,26 +12,18 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.vueNodes.waitForNodes()
})
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)
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)
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'
)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
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

View File

@@ -2,6 +2,7 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures'
test.describe('Vue Node Collapse', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -9,50 +10,43 @@ test.describe('Vue Node Collapse', () => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setup()
await comfyPage.vueNodes.waitForNodes()
})
test('should allow collapsing node with collapse icon', async ({
comfyPage
}) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect(vueNode.root).toBeVisible()
// Get the KSampler node from the default workflow
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
// Initially should not be collapsed
const body = vueNode.body
expect(await node.isCollapsed()).toBe(false)
const body = await vueNode.getBody()
await expect(body).toBeVisible()
const expandedBoundingBox = await vueNode.boundingBox()
if (!expandedBoundingBox)
throw new Error('Failed to get node bounding box before collapse')
// Collapse the node
await vueNode.toggleCollapse()
await comfyPage.nextFrame()
expect(await node.isCollapsed()).toBe(true)
// Verify node content is hidden
const collapsedSize = await node.getSize()
await expect(body).not.toBeVisible()
const collapsedBoundingBox = await vueNode.boundingBox()
if (!collapsedBoundingBox)
throw new Error('Failed to get node bounding box after collapse')
expect(collapsedBoundingBox.height).toBeLessThan(expandedBoundingBox.height)
// Expand again
await vueNode.toggleCollapse()
await comfyPage.nextFrame()
expect(await node.isCollapsed()).toBe(false)
await expect(body).toBeVisible()
// Size should be restored
const expandedBoundingBoxAfter = await vueNode.boundingBox()
if (!expandedBoundingBoxAfter)
throw new Error('Failed to get node bounding box after expand')
expect(expandedBoundingBoxAfter.height).toBeGreaterThanOrEqual(
collapsedBoundingBox.height
)
const expandedSize = await node.getSize()
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
})
test('should show collapse/expand icon state', async ({ comfyPage }) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect(vueNode.root).toBeVisible()
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
// Check initial expanded state icon
let iconClass = await vueNode.getCollapseIconClass()
@@ -72,8 +66,9 @@ test.describe('Vue Node Collapse', () => {
test('should preserve title when collapsing/expanding', async ({
comfyPage
}) => {
const vueNode = await comfyPage.vueNodes.getFixtureByTitle('KSampler')
await expect(vueNode.root).toBeVisible()
const nodes = await comfyPage.getNodeRefsByType('KSampler')
const node = nodes[0]
const vueNode = new VueNodeFixture(node, comfyPage.page)
// Set custom title
await vueNode.setTitle('Test Sampler')
@@ -88,6 +83,7 @@ test.describe('Vue Node Collapse', () => {
expect(await vueNode.getTitle()).toBe('Test Sampler')
// Verify title is still displayed
await expect(vueNode.header).toContainText('Test Sampler')
const header = await vueNode.getHeader()
await expect(header).toContainText('Test Sampler')
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -3,7 +3,7 @@ import {
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const ERROR_CLASS = /border-node-stroke-error/
const ERROR_CLASS = /border-error/
test.describe('Vue Node Error', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -17,21 +17,16 @@ test.describe('Vue Node Error', () => {
await comfyPage.setup()
await comfyPage.loadWorkflow('missing/missing_nodes')
// Close missing nodes warning dialog
await comfyPage.page.getByRole('button', { name: 'Close' }).click()
await comfyPage.page.waitForSelector('.comfy-missing-nodes', {
state: 'hidden'
})
// Expect error state on missing unknown node
const unknownNode = comfyPage.page.locator('[data-node-id]').filter({
hasText: 'UNKNOWN NODE'
})
await expect(unknownNode).toHaveClass(ERROR_CLASS)
})
test('should display error state when node causes execution error', async ({
comfyPage
}) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.runButton.click()
const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error')
await expect(raiseErrorNode).toHaveClass(ERROR_CLASS)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -4,7 +4,7 @@ import {
} from '../../../fixtures/ComfyPage'
const MUTE_HOTKEY = 'Control+m'
const MUTE_OPACITY = '0.5'
const MUTE_CLASS = /opacity-50/
test.describe('Vue Node Mute', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -19,11 +19,10 @@ test.describe('Vue Node Mute', () => {
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png')
await expect(checkpointNode).toHaveClass(MUTE_CLASS)
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
await expect(checkpointNode).not.toHaveClass(MUTE_CLASS)
})
test('should allow toggling mute on multiple selected nodes with hotkey', async ({
@@ -36,11 +35,11 @@ test.describe('Vue Node Mute', () => {
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(ksamplerNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(checkpointNode).toHaveClass(MUTE_CLASS)
await expect(ksamplerNode).toHaveClass(MUTE_CLASS)
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
await expect(ksamplerNode).not.toHaveCSS('opacity', MUTE_OPACITY)
await expect(checkpointNode).not.toHaveClass(MUTE_CLASS)
await expect(ksamplerNode).not.toHaveClass(MUTE_CLASS)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,51 +0,0 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Widget Reactivity', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('Should display added widgets', async ({ comfyPage }) => {
const loadCheckpointNode = comfyPage.page.locator(
'css=[data-testid="node-body-4"] > .lg-node-widgets > div'
)
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['4']
node.widgets.push(node.widgets[0])
})
await expect(loadCheckpointNode).toHaveCount(2)
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['4']
node.widgets[2] = node.widgets[0]
})
await expect(loadCheckpointNode).toHaveCount(3)
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['4']
node.widgets.splice(0, 0, node.widgets[0])
})
await expect(loadCheckpointNode).toHaveCount(4)
})
test('Should hide removed widgets', async ({ comfyPage }) => {
const loadCheckpointNode = comfyPage.page.locator(
'css=[data-testid="node-body-3"] > .lg-node-widgets > div'
)
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['3']
node.widgets.pop()
})
await expect(loadCheckpointNode).toHaveCount(5)
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['3']
node.widgets.length--
})
await expect(loadCheckpointNode).toHaveCount(4)
await comfyPage.page.evaluate(() => {
const node = window['graph']._nodes_by_id['3']
node.widgets.splice(0, 1)
})
await expect(loadCheckpointNode).toHaveCount(3)
})
})

View File

@@ -1,7 +1,6 @@
// 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'
@@ -24,17 +23,10 @@ const commonGlobals = {
} as const
const settings = {
'import-x/resolver-next': [
createTypeScriptImportResolver({
alwaysTryTypes: true,
project: [
'./tsconfig.json',
'./apps/*/tsconfig.json',
'./packages/*/tsconfig.json'
],
noWarnOnMultipleProjects: true
})
],
'import/resolver': {
typescript: true,
node: true
},
tailwindcss: {
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
functions: ['cn', 'clsx', 'tw']
@@ -60,7 +52,6 @@ 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',
@@ -76,8 +67,11 @@ export default defineConfig([
...commonParserOptions,
projectService: {
allowDefaultProject: [
'vite.config.mts',
'vite.electron.config.mts',
'vite.types.config.mts'
'vite.types.config.mts',
'playwright.config.ts',
'playwright.i18n.config.ts'
]
}
}
@@ -255,17 +249,5 @@ 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,12 +4,6 @@ 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,10 +12,6 @@ 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}']
},
@@ -34,16 +30,16 @@ const config: KnipConfig = {
'@primeuix/forms',
'@primeuix/styled',
'@primeuix/utils',
'@primevue/icons'
'@primevue/icons',
// Dev
'@trivago/prettier-plugin-sort-imports'
],
ignore: [
// Auto generated manager types
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts',
// Service worker - registered at runtime via navigator.serviceWorker.register()
'public/auth-sw.js'
'src/scripts/ui/components/splitButton.ts'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -8,10 +8,8 @@ export default {
}
function formatAndEslint(fileNames) {
// Convert absolute paths to relative paths for better ESLint resolution
const relativePaths = fileNames.map((f) => f.replace(process.cwd() + '/', ''))
return [
`pnpm exec eslint --cache --fix ${relativePaths.join(' ')}`,
`pnpm exec prettier --cache --write ${relativePaths.join(' ')}`
`pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.30.2",
"version": "1.29.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -11,10 +11,7 @@
"build:desktop": "nx build @comfyorg/desktop-ui",
"build-storybook": "storybook build",
"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",
@@ -55,7 +52,6 @@
"@nx/vite": "catalog:",
"@pinia/testing": "catalog:",
"@playwright/test": "catalog:",
"@prettier/plugin-oxc": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/vue3": "catalog:",
"@storybook/vue3-vite": "catalog:",
@@ -89,14 +85,9 @@
"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:",
"tailwindcss": "catalog:",

View File

@@ -24,14 +24,11 @@
--text-xxs: 0.625rem;
--text-xxs--line-height: calc(1 / 0.625);
/* Spacing */
--spacing-xs: 8px;
/* Font Families */
--font-inter: 'Inter', sans-serif;
/* Palette Colors */
--color-charcoal-100: #171718;
--color-charcoal-100: #55565e;
--color-charcoal-200: #494a50;
--color-charcoal-300: #3c3d42;
--color-charcoal-400: #313235;
@@ -63,7 +60,6 @@
--color-sand-200: #d6cfc2;
--color-sand-300: #888682;
--color-pure-black: #000000;
--color-pure-white: #ffffff;
--color-slate-100: #9c9eab;
@@ -89,29 +85,10 @@
--color-bypass: #6a246a;
--color-error: #962a2a;
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
--text-xxxs: 0.5625rem;
--text-xxxs--line-height: calc(1 / 0.5625);
--color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3);
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
--color-alpha-charcoal-600-30: color-mix(
in srgb,
var(--color-charcoal-600) 30%,
transparent
);
--color-alpha-stone-100-20: color-mix(
in srgb,
var(--color-stone-100) 20%,
transparent
);
--color-alpha-gray-500-50: color-mix(
in srgb,
var(--color-gray-500) 50%,
transparent
);
/* PrimeVue pulled colors */
--color-muted: var(--p-text-muted-color);
@@ -145,9 +122,6 @@
--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,21 +129,9 @@
/* --- */
--accent-primary: var(--color-charcoal-700);
--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);
--interface-menu-keybind-surface-default: var(--color-gray-500);
--interface-panel-surface: var(--color-pure-white);
--interface-stroke: var(--color-gray-300);
--nav-background: var(--color-pure-white);
--node-border: var(--color-gray-300);
--node-component-border: var(--color-gray-400);
--node-component-disabled: var(--color-alpha-stone-100-20);
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-stone-200);
@@ -181,7 +143,7 @@
--node-component-slot-dot-outline: var(--color-black);
--node-component-slot-text: var(--color-stone-200);
--node-component-surface-highlight: var(--color-stone-100);
--node-component-surface-hovered: var(--color-gray-200);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-white);
--node-component-tooltip: var(--color-charcoal-700);
@@ -192,37 +154,13 @@
from var(--color-zinc-500) r g b / 10%
);
--node-component-widget-skeleton-surface: var(--color-zinc-300);
--node-divider: var(--color-sand-100);
--node-icon-disabled: var(--color-alpha-gray-500-50);
--node-stroke: var(--color-gray-400);
--node-stroke-selected: var(--color-accent-primary);
--node-stroke-error: var(--color-error);
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-stone-100);
--text-primary: var(--color-charcoal-700);
--input-surface: rgba(0, 0, 0, 0.15);
--node-stroke: var(--color-stone-100);
}
.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);
--interface-menu-keybind-surface-default: var(--color-charcoal-200);
--interface-panel-surface: var(--color-charcoal-100);
--interface-stroke: var(--color-charcoal-400);
--nav-background: var(--color-charcoal-100);
--node-border: var(--color-charcoal-500);
--node-component-border: var(--color-stone-200);
--node-component-border-error: var(--color-danger-100);
--node-component-border-executing: var(--color-blue-500);
--node-component-border-selected: var(--color-charcoal-200);
--node-component-header-icon: var(--color-slate-300);
--node-component-header-surface: var(--color-charcoal-800);
--node-component-outline: var(--color-white);
@@ -231,40 +169,19 @@
--node-component-slot-dot-outline: var(--color-white);
--node-component-slot-text: var(--color-slate-200);
--node-component-surface-highlight: var(--color-slate-100);
--node-component-surface-hovered: var(--color-charcoal-600);
--node-component-surface-hovered: var(--color-charcoal-400);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-charcoal-800);
--node-component-tooltip: var(--color-white);
--node-component-tooltip-border: var(--color-slate-300);
--node-component-tooltip-surface: var(--color-charcoal-800);
--node-component-widget-skeleton-surface: var(--color-zinc-800);
--node-component-disabled: var(--color-alpha-charcoal-600-30);
--node-divider: var(--color-charcoal-500);
--node-icon-disabled: var(--color-alpha-stone-100-20);
--node-stroke: var(--color-stone-200);
--node-stroke-selected: var(--color-pure-white);
--node-stroke-error: var(--color-error);
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-slate-100);
--text-primary: var(--color-pure-white);
--input-surface: rgba(130, 130, 130, 0.1);
--node-stroke: var(--color-slate-100);
}
@theme inline {
--color-backdrop: var(--backdrop);
--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);
--color-interface-menu-keybind-surface-default: var(--interface-menu-keybind-surface-default);
--color-interface-panel-surface: var(--interface-panel-surface);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
--color-node-border: var(--node-border);
--color-node-component-border: var(--node-component-border);
--color-node-component-executing: var(--node-component-executing);
--color-node-component-header: var(--node-component-header);
@@ -296,16 +213,7 @@
--color-node-component-widget-skeleton-surface: var(
--node-component-widget-skeleton-surface
);
--color-node-component-disabled: var(--node-component-disabled);
--color-node-divider: var(--node-divider);
--color-node-icon-disabled: var(--node-icon-disabled);
--color-node-stroke: var(--node-stroke);
--color-node-stroke-selected: var(--node-stroke-selected);
--color-node-stroke-error: var(--node-stroke-error);
--color-node-stroke-executing: var(--node-stroke-executing);
--color-text-secondary: var(--text-secondary);
--color-text-primary: var(--text-primary);
--color-input-surface: var(--input-surface);
}
@custom-variant dark-theme {

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="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"/>
<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"/>
</g>
<defs>
<clipPath id="clip0_704_2695">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 964 B

After

Width:  |  Height:  |  Size: 938 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.3" d="m4 2 9.333 6L4 14V2Z"/></svg>

Before

Width:  |  Height:  |  Size: 221 B

View File

@@ -82,7 +82,7 @@ export function formatSize(value?: number) {
* - filename: 'file'
* - suffix: 'txt'
*/
export function getFilenameDetails(fullFilename: string) {
function getFilenameDetails(fullFilename: string) {
if (fullFilename.includes('.')) {
return {
filename: fullFilename.split('.').slice(0, -1).join('.'),
@@ -451,26 +451,3 @@ export function stringToLocale(locale: string): SupportedLocale {
? (locale as SupportedLocale)
: 'en'
}
export function formatDuration(milliseconds: number): string {
if (!milliseconds || milliseconds < 0) return '0s'
const totalSeconds = Math.floor(milliseconds / 1000)
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const remainingSeconds = Math.floor(totalSeconds % 60)
const parts: string[] = []
if (hours > 0) {
parts.push(`${hours}h`)
}
if (minutes > 0) {
parts.push(`${minutes}m`)
}
if (remainingSeconds > 0 || parts.length === 0) {
parts.push(`${remainingSeconds}s`)
}
return parts.join(' ')
}

431
pnpm-lock.yaml generated
View File

@@ -45,9 +45,6 @@ catalogs:
'@playwright/test':
specifier: ^1.52.0
version: 1.52.0
'@prettier/plugin-oxc':
specifier: ^0.0.4
version: 0.0.4
'@primeuix/forms':
specifier: 0.0.2
version: 0.0.2
@@ -186,18 +183,9 @@ 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
@@ -205,20 +193,14 @@ catalogs:
specifier: ^1.8.0
version: 1.8.0
prettier:
specifier: ^3.6.2
version: 3.6.2
pretty-bytes:
specifier: ^7.1.0
version: 7.1.0
specifier: ^3.3.2
version: 3.3.2
primeicons:
specifier: ^7.0.0
version: 7.0.0
primevue:
specifier: ^4.2.5
version: 4.2.5
rollup-plugin-visualizer:
specifier: ^6.0.4
version: 6.0.4
storybook:
specifier: ^9.1.6
version: 9.1.6
@@ -269,7 +251,7 @@ catalogs:
version: 3.5.13
vue-component-type-helpers:
specifier: ^3.0.7
version: 3.1.1
version: 3.1.0
vue-eslint-parser:
specifier: ^10.2.0
version: 10.2.0
@@ -491,7 +473,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.6.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.3.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)
@@ -501,24 +483,21 @@ importers:
'@playwright/test':
specifier: 'catalog:'
version: 1.52.0
'@prettier/plugin-oxc':
specifier: 'catalog:'
version: 0.0.4
'@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.6.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.3.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.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))
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))
'@storybook/vue3-vite':
specifier: 'catalog:'
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))
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))
'@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.6.2)
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)
'@types/eslint-plugin-tailwindcss':
specifier: 'catalog:'
version: 3.17.0
@@ -566,10 +545,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.6.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.3.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.6.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.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-tailwindcss:
specifier: 'catalog:'
version: 4.0.0-beta.0(tailwindcss@4.1.12)
@@ -603,33 +582,18 @@ 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.6.2
pretty-bytes:
specifier: 'catalog:'
version: 7.1.0
rollup-plugin-visualizer:
specifier: 'catalog:'
version: 6.0.4(rollup@4.22.4)
version: 3.3.2
storybook:
specifier: 'catalog:'
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))
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))
stylelint:
specifier: 'catalog:'
version: 16.24.0(typescript@5.9.2)
@@ -677,7 +641,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.1
version: 3.1.0
vue-eslint-parser:
specifier: 'catalog:'
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
@@ -2213,21 +2177,6 @@ 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==}
@@ -2354,98 +2303,6 @@ packages:
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
'@oxc-parser/binding-android-arm64@0.74.0':
resolution: {integrity: sha512-lgq8TJq22eyfojfa2jBFy2m66ckAo7iNRYDdyn9reXYA3I6Wx7tgGWVx1JAp1lO+aUiqdqP/uPlDaETL9tqRcg==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [android]
'@oxc-parser/binding-darwin-arm64@0.74.0':
resolution: {integrity: sha512-xbY/io/hkARggbpYEMFX6CwFzb7f4iS6WuBoBeZtdqRWfIEi7sm/uYWXfyVeB8uqOATvJ07WRFC2upI8PSI83g==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [darwin]
'@oxc-parser/binding-darwin-x64@0.74.0':
resolution: {integrity: sha512-FIj2gAGtFaW0Zk+TnGyenMUoRu1ju+kJ/h71D77xc1owOItbFZFGa+4WSVck1H8rTtceeJlK+kux+vCjGFCl9Q==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [darwin]
'@oxc-parser/binding-freebsd-x64@0.74.0':
resolution: {integrity: sha512-W1I+g5TJg0TRRMHgEWNWsTIfe782V3QuaPgZxnfPNmDMywYdtlzllzclBgaDq6qzvZCCQc/UhvNb37KWTCTj8A==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [freebsd]
'@oxc-parser/binding-linux-arm-gnueabihf@0.74.0':
resolution: {integrity: sha512-gxqkyRGApeVI8dgvJ19SYe59XASW3uVxF1YUgkE7peW/XIg5QRAOVTFKyTjI9acYuK1MF6OJHqx30cmxmZLtiQ==}
engines: {node: '>=20.0.0'}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm-musleabihf@0.74.0':
resolution: {integrity: sha512-jpnAUP4Fa93VdPPDzxxBguJmldj/Gpz7wTXKFzpAueqBMfZsy9KNC+0qT2uZ9HGUDMzNuKw0Se3bPCpL/gfD2Q==}
engines: {node: '>=20.0.0'}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm64-gnu@0.74.0':
resolution: {integrity: sha512-fcWyM7BNfCkHqIf3kll8fJctbR/PseL4RnS2isD9Y3FFBhp4efGAzhDaxIUK5GK7kIcFh1P+puIRig8WJ6IMVQ==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-arm64-musl@0.74.0':
resolution: {integrity: sha512-AMY30z/C77HgiRRJX7YtVUaelKq1ex0aaj28XoJu4SCezdS8i0IftUNTtGS1UzGjGZB8zQz5SFwVy4dRu4GLwg==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-riscv64-gnu@0.74.0':
resolution: {integrity: sha512-/RZAP24TgZo4vV/01TBlzRqs0R7E6xvatww4LnmZEBBulQBU/SkypDywfriFqWuFoa61WFXPV7sLcTjJGjim/w==}
engines: {node: '>=20.0.0'}
cpu: [riscv64]
os: [linux]
'@oxc-parser/binding-linux-s390x-gnu@0.74.0':
resolution: {integrity: sha512-620J1beNAlGSPBD+Msb3ptvrwxu04B8iULCH03zlf0JSLy/5sqlD6qBs0XUVkUJv1vbakUw1gfVnUQqv0UTuEg==}
engines: {node: '>=20.0.0'}
cpu: [s390x]
os: [linux]
'@oxc-parser/binding-linux-x64-gnu@0.74.0':
resolution: {integrity: sha512-WBFgQmGtFnPNzHyLKbC1wkYGaRIBxXGofO0+hz1xrrkPgbxbJS1Ukva1EB8sPaVBBQ52Bdc2GjLSp721NWRvww==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-linux-x64-musl@0.74.0':
resolution: {integrity: sha512-y4mapxi0RGqlp3t6Sm+knJlAEqdKDYrEue2LlXOka/F2i4sRN0XhEMPiSOB3ppHmvK4I2zY2XBYTsX1Fel0fAg==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-wasm32-wasi@0.74.0':
resolution: {integrity: sha512-yDS9bRDh5ymobiS2xBmjlrGdUuU61IZoJBaJC5fELdYT5LJNBXlbr3Yc6m2PWfRJwkH6Aq5fRvxAZ4wCbkGa8w==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@oxc-parser/binding-win32-arm64-msvc@0.74.0':
resolution: {integrity: sha512-XFWY52Rfb4N5wEbMCTSBMxRkDLGbAI9CBSL24BIDywwDJMl31gHEVlmHdCDRoXAmanCI6gwbXYTrWe0HvXJ7Aw==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [win32]
'@oxc-parser/binding-win32-x64-msvc@0.74.0':
resolution: {integrity: sha512-1D3x6iU2apLyfTQHygbdaNbX3nZaHu4yaXpD7ilYpoLo7f0MX0tUuoDrqJyJrVGqvyXgc0uz4yXz9tH9ZZhvvg==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@oxc-project/types@0.74.0':
resolution: {integrity: sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ==}
'@oxc-resolver/binding-android-arm-eabi@11.6.1':
resolution: {integrity: sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==}
cpu: [arm]
@@ -2579,10 +2436,6 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@prettier/plugin-oxc@0.0.4':
resolution: {integrity: sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ==}
engines: {node: '>=14'}
'@primeuix/forms@0.0.2':
resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==}
engines: {node: '>=12.11.0'}
@@ -3143,9 +2996,6 @@ 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==}
@@ -3644,9 +3494,6 @@ 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:
@@ -3929,10 +3776,6 @@ 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==}
@@ -5043,6 +4886,9 @@ 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'}
@@ -6120,9 +5966,6 @@ 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'}
@@ -6150,6 +5993,11 @@ 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}
@@ -6309,10 +6157,6 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
oxc-parser@0.74.0:
resolution: {integrity: sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw==}
engines: {node: '>=20.0.0'}
oxc-resolver@11.6.1:
resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==}
@@ -6501,6 +6345,10 @@ 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}
@@ -6513,15 +6361,11 @@ packages:
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
engines: {node: '>=6.0.0'}
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
prettier@3.3.2:
resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==}
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}
@@ -6824,19 +6668,6 @@ packages:
rfdc@1.4.1:
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
rollup-plugin-visualizer@6.0.4:
resolution: {integrity: sha512-q8Q7J/6YofkmaGW1sH/fPRAz37x/+pd7VBuaUU7lwvOS/YikuiiEU9jeb9PH8XHiq50XFrUsBbOxeAMYQ7KZkg==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
rolldown: 1.x || ^1.0.0-beta
rollup: 2.x || 3.x || 4.x
peerDependenciesMeta:
rolldown:
optional: true
rollup:
optional: true
rollup@4.22.4:
resolution: {integrity: sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@@ -6991,10 +6822,6 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
speakingurl@14.0.1:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
@@ -7623,6 +7450,9 @@ 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==}
@@ -9639,29 +9469,6 @@ 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
@@ -9838,7 +9645,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.6.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.3.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)
@@ -9846,7 +9653,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.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.3.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'
@@ -9902,55 +9709,6 @@ snapshots:
'@one-ini/wasm@0.1.1': {}
'@oxc-parser/binding-android-arm64@0.74.0':
optional: true
'@oxc-parser/binding-darwin-arm64@0.74.0':
optional: true
'@oxc-parser/binding-darwin-x64@0.74.0':
optional: true
'@oxc-parser/binding-freebsd-x64@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm-gnueabihf@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm-musleabihf@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm64-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm64-musl@0.74.0':
optional: true
'@oxc-parser/binding-linux-riscv64-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-s390x-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-x64-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-x64-musl@0.74.0':
optional: true
'@oxc-parser/binding-wasm32-wasi@0.74.0':
dependencies:
'@napi-rs/wasm-runtime': 0.2.12
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.74.0':
optional: true
'@oxc-parser/binding-win32-x64-msvc@0.74.0':
optional: true
'@oxc-project/types@0.74.0': {}
'@oxc-resolver/binding-android-arm-eabi@11.6.1':
optional: true
@@ -10046,10 +9804,6 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@prettier/plugin-oxc@0.0.4':
dependencies:
oxc-parser: 0.74.0
'@primeuix/forms@0.0.2':
dependencies:
'@primeuix/utils': 0.3.2
@@ -10250,29 +10004,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.6.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.3.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.6.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.3.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.6.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.3.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.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.3.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.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/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))':
dependencies:
'@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))
'@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))
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.6.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.3.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.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.3.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': {}
@@ -10282,19 +10036,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.6.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.3.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.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.3.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.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))':
'@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))':
dependencies:
'@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))
'@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))
find-package-json: 1.2.0
magic-string: 0.30.19
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.3.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)
@@ -10302,10 +10056,10 @@ snapshots:
transitivePeerDependencies:
- vue
'@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))':
'@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))':
dependencies:
'@storybook/global': 5.0.0
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.3.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
@@ -10570,7 +10324,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.6.2)':
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)':
dependencies:
'@babel/generator': 7.28.3
'@babel/parser': 7.28.4
@@ -10578,7 +10332,7 @@ snapshots:
'@babel/types': 7.28.4
javascript-natural-sort: 0.7.1
lodash: 4.17.21
prettier: 3.6.2
prettier: 3.3.2
optionalDependencies:
'@vue/compiler-sfc': 3.5.13
transitivePeerDependencies:
@@ -10603,8 +10357,6 @@ 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
@@ -11198,8 +10950,6 @@ 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
@@ -11516,8 +11266,6 @@ snapshots:
balanced-match@2.0.0: {}
base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {}
better-opn@3.0.2:
@@ -12362,20 +12110,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.6.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.3.2):
dependencies:
eslint: 9.35.0(jiti@2.4.2)
prettier: 3.6.2
prettier: 3.3.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.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-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):
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.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.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
transitivePeerDependencies:
- supports-color
- typescript
@@ -12814,6 +12562,10 @@ 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
@@ -14086,10 +13838,6 @@ 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:
@@ -14112,6 +13860,8 @@ snapshots:
nanoid@3.3.11: {}
nanoid@3.3.8: {}
nanoid@5.1.5: {}
napi-postinstall@0.3.3: {}
@@ -14332,26 +14082,6 @@ snapshots:
safe-push-apply: 1.0.0
optional: true
oxc-parser@0.74.0:
dependencies:
'@oxc-project/types': 0.74.0
optionalDependencies:
'@oxc-parser/binding-android-arm64': 0.74.0
'@oxc-parser/binding-darwin-arm64': 0.74.0
'@oxc-parser/binding-darwin-x64': 0.74.0
'@oxc-parser/binding-freebsd-x64': 0.74.0
'@oxc-parser/binding-linux-arm-gnueabihf': 0.74.0
'@oxc-parser/binding-linux-arm-musleabihf': 0.74.0
'@oxc-parser/binding-linux-arm64-gnu': 0.74.0
'@oxc-parser/binding-linux-arm64-musl': 0.74.0
'@oxc-parser/binding-linux-riscv64-gnu': 0.74.0
'@oxc-parser/binding-linux-s390x-gnu': 0.74.0
'@oxc-parser/binding-linux-x64-gnu': 0.74.0
'@oxc-parser/binding-linux-x64-musl': 0.74.0
'@oxc-parser/binding-wasm32-wasi': 0.74.0
'@oxc-parser/binding-win32-arm64-msvc': 0.74.0
'@oxc-parser/binding-win32-x64-msvc': 0.74.0
oxc-resolver@11.6.1:
dependencies:
napi-postinstall: 0.3.3
@@ -14511,14 +14241,14 @@ snapshots:
dependencies:
htmlparser2: 8.0.2
js-tokens: 9.0.1
postcss: 8.5.6
postcss-safe-parser: 6.0.0(postcss@8.5.6)
postcss: 8.5.1
postcss-safe-parser: 6.0.0(postcss@8.5.1)
postcss-resolve-nested-selector@0.1.6: {}
postcss-safe-parser@6.0.0(postcss@8.5.6):
postcss-safe-parser@6.0.0(postcss@8.5.1):
dependencies:
postcss: 8.5.6
postcss: 8.5.1
postcss-safe-parser@7.0.1(postcss@8.5.6):
dependencies:
@@ -14536,6 +14266,12 @@ 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
@@ -14548,9 +14284,7 @@ snapshots:
dependencies:
fast-diff: 1.3.0
prettier@3.6.2: {}
pretty-bytes@7.1.0: {}
prettier@3.3.2: {}
pretty-format@27.5.1:
dependencies:
@@ -14999,15 +14733,6 @@ snapshots:
rfdc@1.4.1: {}
rollup-plugin-visualizer@6.0.4(rollup@4.22.4):
dependencies:
open: 8.4.2
picomatch: 4.0.3
source-map: 0.7.6
yargs: 17.7.2
optionalDependencies:
rollup: 4.22.4
rollup@4.22.4:
dependencies:
'@types/estree': 1.0.5
@@ -15199,8 +14924,6 @@ snapshots:
source-map@0.6.1: {}
source-map@0.7.6: {}
speakingurl@14.0.1: {}
sprintf-js@1.0.3: {}
@@ -15227,7 +14950,7 @@ snapshots:
internal-slot: 1.1.0
optional: true
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.3.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
@@ -15242,7 +14965,7 @@ snapshots:
semver: 7.7.2
ws: 8.18.3
optionalDependencies:
prettier: 3.6.2
prettier: 3.3.2
transitivePeerDependencies:
- '@testing-library/dom'
- bufferutil
@@ -15564,7 +15287,7 @@ snapshots:
tsx@4.19.4:
dependencies:
esbuild: 0.25.5
get-tsconfig: 4.10.1
get-tsconfig: 4.7.5
optionalDependencies:
fsevents: 2.3.3
@@ -15916,7 +15639,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.6
postcss: 8.5.1
rollup: 4.22.4
optionalDependencies:
'@types/node': 20.14.10
@@ -15981,6 +15704,8 @@ 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)):

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