fix: resolve merge conflict in knip.config.ts
Merged both sets of ignore entries from sno-deploy-ghpage and main branches.
@@ -458,15 +458,15 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
3. **IMMEDIATELY CHECK**: Did release workflow trigger?
|
||||
```bash
|
||||
sleep 10
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
```
|
||||
4. **For Minor/Major Version Releases**: The create-release-candidate-branch workflow will automatically:
|
||||
4. **For Minor/Major Version Releases**: The release-branch-create workflow will automatically:
|
||||
- Create a `core/x.yy` branch for the PREVIOUS minor version
|
||||
- Apply branch protection rules
|
||||
- Document the feature freeze policy
|
||||
```bash
|
||||
# Monitor branch creation (for minor/major releases)
|
||||
gh run list --workflow=create-release-candidate-branch.yaml --limit=1
|
||||
gh run list --workflow=release-branch-create.yaml --limit=1
|
||||
```
|
||||
4. If workflow didn't trigger due to [skip ci]:
|
||||
```bash
|
||||
@@ -477,7 +477,7 @@ echo "Workflow triggered. Waiting for PR creation..."
|
||||
```
|
||||
5. If workflow triggered, monitor execution:
|
||||
```bash
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
WORKFLOW_RUN_ID=$(gh run list --workflow=release-draft-create.yaml --limit=1 --json databaseId --jq '.[0].databaseId')
|
||||
gh run watch ${WORKFLOW_RUN_ID}
|
||||
```
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ For each commit:
|
||||
3. Merge the PR: `gh pr merge --merge`
|
||||
4. Monitor release workflow:
|
||||
```bash
|
||||
gh run list --workflow=release.yaml --limit=1
|
||||
gh run list --workflow=release-draft-create.yaml --limit=1
|
||||
gh run watch
|
||||
```
|
||||
5. Track progress:
|
||||
|
||||
21
.github/workflows/README.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# GitHub Workflows
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Workflow files follow a consistent naming pattern: `<prefix>-<descriptive-name>.yaml`
|
||||
|
||||
### Category Prefixes
|
||||
|
||||
| Prefix | Purpose | Example |
|
||||
| ---------- | ----------------------------------- | ------------------------------------ |
|
||||
| `ci-` | Testing, linting, validation | `ci-tests-e2e.yaml` |
|
||||
| `release-` | Version management, publishing | `release-version-bump.yaml` |
|
||||
| `pr-` | PR automation (triggered by labels) | `pr-claude-review.yaml` |
|
||||
| `api-` | External Api type generation | `api-update-registry-api-types.yaml` |
|
||||
| `i18n-` | Internationalization updates | `i18n-update-core.yaml` |
|
||||
|
||||
## Documentation
|
||||
|
||||
Each workflow file contains comments explaining its purpose, triggers, and behavior. For specific details about what each workflow does, refer to the comments at the top of each `.yaml` file.
|
||||
|
||||
For GitHub Actions documentation, see [Events that trigger workflows](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows).
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Electron Types
|
||||
name: 'Api: Update Electron API Types'
|
||||
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -25,15 +26,6 @@ jobs:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
key: electron-types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
electron-types-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Update electron types
|
||||
run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update ComfyUI-Manager API Types
|
||||
name: 'Api: Update Manager API Types'
|
||||
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
@@ -30,26 +31,9 @@ jobs:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
key: update-manager-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
update-manager-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Cache ComfyUI-Manager repository
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ComfyUI-Manager
|
||||
key: comfyui-manager-repo-${{ runner.os }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
comfyui-manager-repo-${{ runner.os }}-
|
||||
|
||||
- name: Checkout ComfyUI-Manager repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Comfy Registry API Types
|
||||
name: 'Api: Update Registry API Types'
|
||||
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
|
||||
|
||||
on:
|
||||
# Manual trigger
|
||||
@@ -29,26 +30,9 @@ jobs:
|
||||
node-version: lts/*
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
key: update-registry-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
update-registry-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Cache comfy-api repository
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: comfy-api
|
||||
key: comfy-api-repo-${{ runner.os }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
comfy-api-repo-${{ runner.os }}-
|
||||
|
||||
- name: Checkout comfy-api repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Validate JSON
|
||||
name: "CI: JSON Validation"
|
||||
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Lint and Format
|
||||
name: "CI: Lint Format"
|
||||
description: "Linting and code formatting validation for pull requests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -32,21 +33,6 @@ jobs:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
.eslintcache
|
||||
tsconfig.tsbuildinfo
|
||||
.prettierCache
|
||||
.knip-cache
|
||||
key: lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js,mts}', '*.config.*', '.eslintrc.*', '.prettierrc.*', 'tsconfig.json') }}
|
||||
restore-keys: |
|
||||
lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
lint-format-cache-${{ runner.os }}-
|
||||
ci-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Devtools Python Check
|
||||
name: "CI: Python Validation"
|
||||
description: "Validates Python code in tools/devtools directory"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,8 +1,9 @@
|
||||
name: PR Playwright Deploy (Forks)
|
||||
name: "CI: Tests E2E (Deploy for Forks)"
|
||||
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tests CI"]
|
||||
workflows: ["CI: Tests E2E"]
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Tests CI
|
||||
name: "CI: Tests E2E"
|
||||
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -1,8 +1,9 @@
|
||||
name: PR Storybook Deploy (Forks)
|
||||
name: "CI: Tests Storybook (Deploy for Forks)"
|
||||
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Storybook and Chromatic CI']
|
||||
workflows: ["CI: Tests Storybook"]
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
@@ -1,6 +1,5 @@
|
||||
name: Storybook and Chromatic CI
|
||||
|
||||
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
|
||||
name: "CI: Tests Storybook"
|
||||
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Allow manual triggering
|
||||
@@ -53,19 +52,6 @@ jobs:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
storybook-static
|
||||
tsconfig.tsbuildinfo
|
||||
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
|
||||
restore-keys: |
|
||||
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
storybook-cache-${{ runner.os }}-
|
||||
storybook-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -118,19 +104,6 @@ jobs:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
storybook-static
|
||||
tsconfig.tsbuildinfo
|
||||
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
|
||||
restore-keys: |
|
||||
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
storybook-cache-${{ runner.os }}-
|
||||
storybook-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Vitest Tests
|
||||
name: "CI: Tests Unit"
|
||||
description: "Unit and component testing with Vitest"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -28,19 +29,6 @@ jobs:
|
||||
node-version: "lts/*"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
coverage
|
||||
.vitest-cache
|
||||
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
|
||||
restore-keys: |
|
||||
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
vitest-cache-${{ runner.os }}-
|
||||
test-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Update Locales
|
||||
name: "i18n: Update Core"
|
||||
description: "Generates and updates translations for core ComfyUI components using OpenAI"
|
||||
|
||||
on:
|
||||
# Manual dispatch for urgent translation updates
|
||||
@@ -54,5 +55,5 @@ jobs:
|
||||
# Apply the stashed changes if any
|
||||
git stash pop || true
|
||||
git add src/locales/
|
||||
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
|
||||
git diff --staged --quiet || git commit -m "Update locales"
|
||||
git push origin HEAD:${{ github.head_ref }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Update Locales for given custom node repository
|
||||
name: i18n Update Custom Nodes
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Update Node Definitions Locales
|
||||
name: i18n Update Nodes
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Auto Backport
|
||||
name: PR Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
@@ -95,41 +95,61 @@ jobs:
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||
|
||||
- name: Extract version labels
|
||||
- name: Collect backport targets
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: versions
|
||||
id: targets
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
|
||||
TARGETS=()
|
||||
declare -A SEEN=()
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# For manual triggers, get labels from the PR
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
# For automatic triggers, extract from PR event
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
for label in $LABELS; do
|
||||
# Match version labels like "1.24" (major.minor only)
|
||||
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# Validate the branch exists before adding to list
|
||||
if git ls-remote --exit-code origin "core/${label}" >/dev/null 2>&1; then
|
||||
VERSIONS="${VERSIONS}${label} "
|
||||
else
|
||||
echo "::warning::Label '${label}' found but branch 'core/${label}' does not exist"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$VERSIONS" ]; then
|
||||
echo "::error::No version labels found (e.g., 1.24, 1.22)"
|
||||
add_target() {
|
||||
local label="$1"
|
||||
local target="$2"
|
||||
|
||||
if [ -z "$target" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
target=$(echo "$target" | xargs)
|
||||
|
||||
if [ -z "$target" ] || [ -n "${SEEN[$target]}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if git ls-remote --exit-code origin "$target" >/dev/null 2>&1; then
|
||||
TARGETS+=("$target")
|
||||
SEEN["$target"]=1
|
||||
else
|
||||
echo "::warning::Label '${label}' references missing branch '${target}'"
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS= read -r label; do
|
||||
[ -z "$label" ] && continue
|
||||
|
||||
if [[ "$label" =~ ^branch:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
add_target "$label" "core/${label}"
|
||||
fi
|
||||
done <<< "$LABELS"
|
||||
|
||||
if [ "${#TARGETS[@]}" -eq 0 ]; then
|
||||
echo "::error::No backport targets found (use labels like '1.24' or 'branch:release/hotfix')"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "versions=${VERSIONS}" >> $GITHUB_OUTPUT
|
||||
echo "Found version labels: ${VERSIONS}"
|
||||
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
|
||||
echo "Found backport targets: ${TARGETS[*]}"
|
||||
|
||||
- name: Backport commits
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
@@ -150,16 +170,17 @@ jobs:
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
for target in ${{ steps.targets.outputs.targets }}; do
|
||||
TARGET_BRANCH="${target}"
|
||||
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
|
||||
TARGET_BRANCH="core/${version}"
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${version}"
|
||||
echo "::group::Backporting to ${TARGET_BRANCH}"
|
||||
|
||||
# Fetch target branch (fail if doesn't exist)
|
||||
if ! git fetch origin "${TARGET_BRANCH}"; then
|
||||
echo "::error::Target branch ${TARGET_BRANCH} does not exist"
|
||||
FAILED="${FAILED}${version}:branch-missing "
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:branch-missing "
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
@@ -170,7 +191,7 @@ jobs:
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
SUCCESS="${SUCCESS}${version}:${BACKPORT_BRANCH} "
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
git checkout main
|
||||
@@ -180,7 +201,7 @@ jobs:
|
||||
git cherry-pick --abort
|
||||
|
||||
echo "::error::Cherry-pick failed due to conflicts"
|
||||
FAILED="${FAILED}${version}:conflicts:${CONFLICTS} "
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
|
||||
|
||||
# Clean up the failed branch
|
||||
git checkout main
|
||||
@@ -214,13 +235,13 @@ jobs:
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
IFS=':' read -r target branch <<< "${backport}"
|
||||
|
||||
if PR_URL=$(gh pr create \
|
||||
--base "core/${version}" \
|
||||
--base "${target}" \
|
||||
--head "${branch}" \
|
||||
--title "[backport ${version}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`core/${version}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--title "[backport ${target}] ${PR_TITLE}" \
|
||||
--body "Backport of #${PR_NUMBER} to \`${target}\`"$'\n\n'"Automatically created by backport workflow." \
|
||||
--label "backport" 2>&1); then
|
||||
|
||||
# Extract PR number from URL
|
||||
@@ -230,9 +251,9 @@ jobs:
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Successfully backported to #${PR_NUM}"
|
||||
fi
|
||||
else
|
||||
echo "::error::Failed to create PR for ${version}: ${PR_URL}"
|
||||
echo "::error::Failed to create PR for ${target}: ${PR_URL}"
|
||||
# Still try to comment on the original PR about the failure
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`core/${version}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport branch created but PR creation failed for \`${target}\`. Please create the PR manually from branch \`${branch}\`"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -253,16 +274,16 @@ jobs:
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
IFS=':' read -r target reason conflicts <<< "${failure}"
|
||||
|
||||
if [ "${reason}" = "branch-missing" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`core/${version}\` does not exist"
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
|
||||
|
||||
elif [ "${reason}" = "conflicts" ]; then
|
||||
# Convert comma-separated conflicts back to newlines for display
|
||||
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
|
||||
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`core/${version}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`core/${version}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
|
||||
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
||||
fi
|
||||
done
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Claude PR Review
|
||||
name: "PR: Claude Review"
|
||||
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -1,5 +1,5 @@
|
||||
# Setting test expectation screenshots for Playwright
|
||||
name: Update Playwright Expectations
|
||||
name: "PR: Update Playwright Expectations"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
1
.github/workflows/publish-desktop-ui.yaml
vendored
@@ -44,6 +44,7 @@ jobs:
|
||||
contents: read
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
ENABLE_MINIFY: 'true'
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
env:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Create Release Branch
|
||||
name: Release Branch Create
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Create Release Draft
|
||||
name: Release Draft Create
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -28,16 +28,6 @@ jobs:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
tsconfig.tsbuildinfo
|
||||
key: release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
release-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
@@ -55,6 +45,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
@@ -126,7 +117,7 @@ jobs:
|
||||
|
||||
publish_types:
|
||||
needs: build
|
||||
uses: ./.github/workflows/publish-frontend-types.yaml
|
||||
uses: ./.github/workflows/release-npm-types.yaml
|
||||
with:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Publish Frontend Types
|
||||
name: Release NPM Types
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Create Dev PyPI Package
|
||||
name: Release PyPI Dev
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -25,17 +25,6 @@ jobs:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
dist
|
||||
tsconfig.tsbuildinfo
|
||||
key: dev-release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
dev-release-tools-cache-${{ runner.os }}-
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
|
||||
@@ -44,6 +33,7 @@ jobs:
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
|
||||
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
|
||||
ENABLE_MINIFY: 'true'
|
||||
USE_PROD_CONFIG: 'true'
|
||||
run: |
|
||||
pnpm install --frozen-lockfile
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Version Bump
|
||||
name: "Release: Version Bump"
|
||||
description: "Manual workflow to increment package version with semantic versioning support"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,6 +15,11 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
@@ -25,6 +31,24 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -58,7 +82,9 @@ jobs:
|
||||
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
52
.github/workflows/size-data.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: size data
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
collect:
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
|
||||
- name: Collect size data
|
||||
run: node scripts/size-collect.js
|
||||
|
||||
- name: Save PR number & base branch
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
echo ${{ github.event.number }} > ./temp/size/number.txt
|
||||
echo ${{ github.base_ref }} > ./temp/size/base.txt
|
||||
|
||||
- name: Upload size data
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: size-data
|
||||
path: temp/size
|
||||
104
.github/workflows/size-report.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: size report
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['size data']
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to report on'
|
||||
required: true
|
||||
type: number
|
||||
run_id:
|
||||
description: 'Size data workflow run ID'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size-report:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
(
|
||||
(github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success') ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
)
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.1.0
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: '24.x'
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Download size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
name: size-data
|
||||
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
|
||||
path: temp/size
|
||||
|
||||
- name: Set PR number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Set base branch
|
||||
id: pr-base
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||
echo "content=main" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Download previous size data
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
branch: ${{ steps.pr-base.outputs.content }}
|
||||
workflow: size-data.yml
|
||||
event: push
|
||||
name: size-data
|
||||
path: temp/size-prev
|
||||
if_no_artifact_found: warn
|
||||
|
||||
- name: Generate size report
|
||||
run: node scripts/size-report.js > size-report.md
|
||||
|
||||
- name: Read size report
|
||||
id: size-report
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ./size-report.md
|
||||
|
||||
- name: Create or update PR comment
|
||||
uses: actions-cool/maintain-one-comment@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
number: ${{ steps.pr-number.outputs.content }}
|
||||
body: |
|
||||
${{ steps.size-report.outputs.content }}
|
||||
<!-- COMFYUI_FRONTEND_SIZE -->
|
||||
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'
|
||||
26
.github/workflows/version-bump-desktop-ui.yaml
vendored
@@ -14,6 +14,11 @@ on:
|
||||
required: false
|
||||
default: ''
|
||||
type: string
|
||||
branch:
|
||||
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
bump-version-desktop-ui:
|
||||
@@ -26,8 +31,25 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Validate branch exists
|
||||
run: |
|
||||
BRANCH="${{ github.event.inputs.branch }}"
|
||||
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
|
||||
echo "❌ Branch '$BRANCH' does not exist"
|
||||
echo ""
|
||||
echo "Available core branches:"
|
||||
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
|
||||
echo ""
|
||||
echo "Main branch:"
|
||||
echo " - main"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Branch '$BRANCH' exists"
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -64,8 +86,10 @@ jobs:
|
||||
title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
body: |
|
||||
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
|
||||
**Base branch:** `${{ github.event.inputs.branch }}`
|
||||
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
|
||||
base: main
|
||||
base: ${{ github.event.inputs.branch }}
|
||||
labels: |
|
||||
Release
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
class="task-div max-w-48 min-h-52 grid relative"
|
||||
class="task-div relative grid min-h-52 max-w-48"
|
||||
:class="{ 'opacity-75': isLoading }"
|
||||
>
|
||||
<Card
|
||||
class="max-w-48 relative h-full overflow-hidden"
|
||||
class="relative h-full max-w-48 overflow-hidden"
|
||||
:class="{ 'opacity-65': runner.state !== 'error' }"
|
||||
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
|
||||
>
|
||||
<template #header>
|
||||
<i
|
||||
v-if="runner.state === 'error'"
|
||||
class="pi pi-exclamation-triangle text-red-500 absolute m-2 top-0 -right-14 opacity-15"
|
||||
class="pi pi-exclamation-triangle absolute top-0 -right-14 m-2 text-red-500 opacity-15"
|
||||
style="font-size: 10rem"
|
||||
/>
|
||||
<img
|
||||
v-if="task.headerImg"
|
||||
:src="task.headerImg"
|
||||
class="object-contain w-full h-full opacity-25 pt-4 px-4"
|
||||
class="h-full w-full object-contain px-4 pt-4 opacity-25"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
@@ -27,7 +27,7 @@
|
||||
{{ description }}
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="flex gap-4 mt-1">
|
||||
<div class="mt-1 flex gap-4">
|
||||
<Button
|
||||
:icon="task.button?.icon"
|
||||
:label="task.button?.text"
|
||||
@@ -73,7 +73,7 @@ defineEmits<{
|
||||
// Bindings
|
||||
const description = computed(() =>
|
||||
runner.value.state === 'error'
|
||||
? props.task.errorDescription ?? props.task.shortDescription
|
||||
? (props.task.errorDescription ?? props.task.shortDescription)
|
||||
: props.task.shortDescription
|
||||
)
|
||||
|
||||
|
||||
@@ -1,67 +1,163 @@
|
||||
import arCommands from '@frontend-locales/ar/commands.json' with { type: 'json' }
|
||||
import ar from '@frontend-locales/ar/main.json' with { type: 'json' }
|
||||
import arNodes from '@frontend-locales/ar/nodeDefs.json' with { type: 'json' }
|
||||
import arSettings from '@frontend-locales/ar/settings.json' with { type: 'json' }
|
||||
// Import only English locale eagerly as the default/fallback
|
||||
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
|
||||
// but these are properly configured in tsconfig.json and resolved by Vite at build time.
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import en from '@frontend-locales/en/main.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' }
|
||||
// eslint-disable-next-line import-x/no-unresolved
|
||||
import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
|
||||
import esCommands from '@frontend-locales/es/commands.json' with { type: 'json' }
|
||||
import es from '@frontend-locales/es/main.json' with { type: 'json' }
|
||||
import esNodes from '@frontend-locales/es/nodeDefs.json' with { type: 'json' }
|
||||
import esSettings from '@frontend-locales/es/settings.json' with { type: 'json' }
|
||||
import frCommands from '@frontend-locales/fr/commands.json' with { type: 'json' }
|
||||
import fr from '@frontend-locales/fr/main.json' with { type: 'json' }
|
||||
import frNodes from '@frontend-locales/fr/nodeDefs.json' with { type: 'json' }
|
||||
import frSettings from '@frontend-locales/fr/settings.json' with { type: 'json' }
|
||||
import jaCommands from '@frontend-locales/ja/commands.json' with { type: 'json' }
|
||||
import ja from '@frontend-locales/ja/main.json' with { type: 'json' }
|
||||
import jaNodes from '@frontend-locales/ja/nodeDefs.json' with { type: 'json' }
|
||||
import jaSettings from '@frontend-locales/ja/settings.json' with { type: 'json' }
|
||||
import koCommands from '@frontend-locales/ko/commands.json' with { type: 'json' }
|
||||
import ko from '@frontend-locales/ko/main.json' with { type: 'json' }
|
||||
import koNodes from '@frontend-locales/ko/nodeDefs.json' with { type: 'json' }
|
||||
import koSettings from '@frontend-locales/ko/settings.json' with { type: 'json' }
|
||||
import ruCommands from '@frontend-locales/ru/commands.json' with { type: 'json' }
|
||||
import ru from '@frontend-locales/ru/main.json' with { type: 'json' }
|
||||
import ruNodes from '@frontend-locales/ru/nodeDefs.json' with { type: 'json' }
|
||||
import ruSettings from '@frontend-locales/ru/settings.json' with { type: 'json' }
|
||||
import trCommands from '@frontend-locales/tr/commands.json' with { type: 'json' }
|
||||
import tr from '@frontend-locales/tr/main.json' with { type: 'json' }
|
||||
import trNodes from '@frontend-locales/tr/nodeDefs.json' with { type: 'json' }
|
||||
import trSettings from '@frontend-locales/tr/settings.json' with { type: 'json' }
|
||||
import zhTWCommands from '@frontend-locales/zh-TW/commands.json' with { type: 'json' }
|
||||
import zhTW from '@frontend-locales/zh-TW/main.json' with { type: 'json' }
|
||||
import zhTWNodes from '@frontend-locales/zh-TW/nodeDefs.json' with { type: 'json' }
|
||||
import zhTWSettings from '@frontend-locales/zh-TW/settings.json' with { type: 'json' }
|
||||
import zhCommands from '@frontend-locales/zh/commands.json' with { type: 'json' }
|
||||
import zh from '@frontend-locales/zh/main.json' with { type: 'json' }
|
||||
import zhNodes from '@frontend-locales/zh/nodeDefs.json' with { type: 'json' }
|
||||
import zhSettings from '@frontend-locales/zh/settings.json' with { type: 'json' }
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
|
||||
function buildLocale<
|
||||
M extends Record<string, unknown>,
|
||||
N extends Record<string, unknown>,
|
||||
C extends Record<string, unknown>,
|
||||
S extends Record<string, unknown>
|
||||
>(main: M, nodes: N, commands: C, settings: S) {
|
||||
return {
|
||||
...main,
|
||||
nodeDefs: nodes,
|
||||
commands: commands,
|
||||
settings: settings
|
||||
}
|
||||
} as M & { nodeDefs: N; commands: C; settings: S }
|
||||
}
|
||||
|
||||
const messages = {
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings),
|
||||
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
|
||||
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
|
||||
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
|
||||
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
|
||||
ko: buildLocale(ko, koNodes, koCommands, koSettings),
|
||||
fr: buildLocale(fr, frNodes, frCommands, frSettings),
|
||||
es: buildLocale(es, esNodes, esCommands, esSettings),
|
||||
ar: buildLocale(ar, arNodes, arCommands, arSettings),
|
||||
tr: buildLocale(tr, trNodes, trCommands, trSettings)
|
||||
// Locale loader map - dynamically import locales only when needed
|
||||
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
|
||||
/* eslint-disable import-x/no-unresolved */
|
||||
const localeLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/main.json'),
|
||||
es: () => import('@frontend-locales/es/main.json'),
|
||||
fr: () => import('@frontend-locales/fr/main.json'),
|
||||
ja: () => import('@frontend-locales/ja/main.json'),
|
||||
ko: () => import('@frontend-locales/ko/main.json'),
|
||||
ru: () => import('@frontend-locales/ru/main.json'),
|
||||
tr: () => import('@frontend-locales/tr/main.json'),
|
||||
zh: () => import('@frontend-locales/zh/main.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
|
||||
}
|
||||
|
||||
const nodeDefsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/nodeDefs.json'),
|
||||
es: () => import('@frontend-locales/es/nodeDefs.json'),
|
||||
fr: () => import('@frontend-locales/fr/nodeDefs.json'),
|
||||
ja: () => import('@frontend-locales/ja/nodeDefs.json'),
|
||||
ko: () => import('@frontend-locales/ko/nodeDefs.json'),
|
||||
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
|
||||
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
|
||||
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
|
||||
}
|
||||
|
||||
const commandsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/commands.json'),
|
||||
es: () => import('@frontend-locales/es/commands.json'),
|
||||
fr: () => import('@frontend-locales/fr/commands.json'),
|
||||
ja: () => import('@frontend-locales/ja/commands.json'),
|
||||
ko: () => import('@frontend-locales/ko/commands.json'),
|
||||
ru: () => import('@frontend-locales/ru/commands.json'),
|
||||
tr: () => import('@frontend-locales/tr/commands.json'),
|
||||
zh: () => import('@frontend-locales/zh/commands.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
|
||||
}
|
||||
|
||||
const settingsLoaders: Record<
|
||||
string,
|
||||
() => Promise<{ default: Record<string, unknown> }>
|
||||
> = {
|
||||
ar: () => import('@frontend-locales/ar/settings.json'),
|
||||
es: () => import('@frontend-locales/es/settings.json'),
|
||||
fr: () => import('@frontend-locales/fr/settings.json'),
|
||||
ja: () => import('@frontend-locales/ja/settings.json'),
|
||||
ko: () => import('@frontend-locales/ko/settings.json'),
|
||||
ru: () => import('@frontend-locales/ru/settings.json'),
|
||||
tr: () => import('@frontend-locales/tr/settings.json'),
|
||||
zh: () => import('@frontend-locales/zh/settings.json'),
|
||||
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
|
||||
}
|
||||
|
||||
// Track which locales have been loaded
|
||||
const loadedLocales = new Set<string>(['en'])
|
||||
|
||||
// Track locales currently being loaded to prevent race conditions
|
||||
const loadingLocales = new Map<string, Promise<void>>()
|
||||
|
||||
/**
|
||||
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
|
||||
*/
|
||||
export async function loadLocale(locale: string): Promise<void> {
|
||||
if (loadedLocales.has(locale)) {
|
||||
return
|
||||
}
|
||||
|
||||
// If already loading, return the existing promise to prevent duplicate loads
|
||||
const existingLoad = loadingLocales.get(locale)
|
||||
if (existingLoad) {
|
||||
return existingLoad
|
||||
}
|
||||
|
||||
const loader = localeLoaders[locale]
|
||||
const nodeDefsLoader = nodeDefsLoaders[locale]
|
||||
const commandsLoader = commandsLoaders[locale]
|
||||
const settingsLoader = settingsLoaders[locale]
|
||||
|
||||
if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) {
|
||||
console.warn(`Locale "${locale}" is not supported`)
|
||||
return
|
||||
}
|
||||
|
||||
// Create and track the loading promise
|
||||
const loadPromise = (async () => {
|
||||
try {
|
||||
const [main, nodes, commands, settings] = await Promise.all([
|
||||
loader(),
|
||||
nodeDefsLoader(),
|
||||
commandsLoader(),
|
||||
settingsLoader()
|
||||
])
|
||||
|
||||
const messages = buildLocale(
|
||||
main.default,
|
||||
nodes.default,
|
||||
commands.default,
|
||||
settings.default
|
||||
)
|
||||
|
||||
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
|
||||
loadedLocales.add(locale)
|
||||
} catch (error) {
|
||||
console.error(`Failed to load locale "${locale}":`, error)
|
||||
throw error
|
||||
} finally {
|
||||
// Clean up the loading promise once complete
|
||||
loadingLocales.delete(locale)
|
||||
}
|
||||
})()
|
||||
|
||||
loadingLocales.set(locale, loadPromise)
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
// Only include English in the initial bundle
|
||||
const messages = {
|
||||
en: buildLocale(en, enNodes, enCommands, enSettings)
|
||||
}
|
||||
|
||||
// Type for locale messages - inferred from the English locale structure
|
||||
type LocaleMessages = typeof messages.en
|
||||
|
||||
export const i18n = createI18n({
|
||||
// Must set `false`, as Vue I18n Legacy API is for Vue 2
|
||||
legacy: false,
|
||||
|
||||
@@ -46,6 +46,10 @@ class ComfyMenu {
|
||||
.nth(0)
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
return this.sideToolbar.locator('.side-bar-button')
|
||||
}
|
||||
|
||||
get nodeLibraryTab() {
|
||||
this._nodeLibraryTab ??= new NodeLibrarySidebarTab(this.page)
|
||||
return this._nodeLibraryTab
|
||||
|
||||
@@ -7,7 +7,7 @@ export class Topbar {
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.menuLocator = page.locator('.comfy-command-menu')
|
||||
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
|
||||
this.menuTrigger = page.locator('.comfy-menu-button-wrapper')
|
||||
}
|
||||
|
||||
async getTabNames(): Promise<string[]> {
|
||||
@@ -105,7 +105,7 @@ export class Topbar {
|
||||
* Close the topbar menu by clicking outside
|
||||
*/
|
||||
async closeTopbarMenu() {
|
||||
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||
await this.page.locator('body').click({ position: { x: 300, y: 10 } })
|
||||
await expect(this.menuLocator).not.toBeVisible()
|
||||
}
|
||||
|
||||
|
||||
@@ -116,9 +116,10 @@ test.describe('Actionbar', () => {
|
||||
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
|
||||
await comfyPage.page.dragAndDrop(
|
||||
'.actionbar .drag-handle',
|
||||
'.comfyui-menu',
|
||||
'.actionbar-container',
|
||||
{
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
targetPosition: { x: 50, y: 20 },
|
||||
force: true
|
||||
}
|
||||
)
|
||||
expect(await comfyPage.actionbar.isDocked()).toBe(true)
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -233,6 +233,7 @@ test.describe('Group Node', () => {
|
||||
}
|
||||
|
||||
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
|
||||
.getFolder(GROUP_NODE_CATEGORY)
|
||||
.count()
|
||||
@@ -253,8 +254,6 @@ test.describe('Group Node', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.loadWorkflow(WORKFLOW_NAME)
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
|
||||
groupNode = await comfyPage.getFirstNodeRef()
|
||||
if (!groupNode)
|
||||
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
|
||||
|
||||
@@ -792,10 +792,19 @@ test.describe('Viewport settings', () => {
|
||||
|
||||
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
|
||||
await comfyPage.nextFrame()
|
||||
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
|
||||
|
||||
// Save workflow as a new file, then zoom out before screen shot
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('Workflow B')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
await changeTab(tabA)
|
||||
|
||||
const screenshotA = (await comfyPage.canvas.screenshot()).toString('base64')
|
||||
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
await changeTab(tabB)
|
||||
|
||||
await comfyMouse.move(comfyPage.emptySpace)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await comfyMouse.wheel(0, 60)
|
||||
@@ -807,9 +816,6 @@ test.describe('Viewport settings', () => {
|
||||
// Ensure that the screenshots are different due to zoom level
|
||||
expect(screenshotB).not.toBe(screenshotA)
|
||||
|
||||
const tabA = comfyPage.menu.topbar.getWorkflowTab('Workflow A')
|
||||
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
|
||||
|
||||
// Go back to Workflow A
|
||||
await changeTab(tabA)
|
||||
expect((await comfyPage.canvas.screenshot()).toString('base64')).toBe(
|
||||
|
||||
@@ -8,9 +8,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
test('Can register sidebar tab', async ({ comfyPage }) => {
|
||||
const initialChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
const initialChildrenCount = await comfyPage.menu.buttons.count()
|
||||
|
||||
await comfyPage.page.evaluate(async () => {
|
||||
window['app'].extensionManager.registerSidebarTab({
|
||||
@@ -26,9 +24,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newChildrenCount = await comfyPage.menu.sideToolbar.evaluate(
|
||||
(el) => el.children.length
|
||||
)
|
||||
const newChildrenCount = await comfyPage.menu.buttons.count()
|
||||
expect(newChildrenCount).toBe(initialChildrenCount + 1)
|
||||
})
|
||||
|
||||
|
||||
28
browser_tests/tests/recordAudio.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Record Audio Node', () => {
|
||||
test('should add a record audio node and take a screenshot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Open the search box by double clicking on the canvas
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await expect(comfyPage.searchBox.input).toHaveCount(1)
|
||||
|
||||
// Search for and add the RecordAudio node
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('RecordAudio')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the RecordAudio node was added
|
||||
const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio')
|
||||
expect(recordAudioNodes.length).toBe(1)
|
||||
|
||||
// Take a screenshot of the canvas with the RecordAudio node
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('record_audio_node.png')
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 21 KiB |
@@ -12,21 +12,26 @@ test.describe('Vue Node Bypass', () => {
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should allow toggling bypass on a selected node with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
test.fixme(
|
||||
'should allow toggling bypass on a selected node with hotkey',
|
||||
async ({ comfyPage }) => {
|
||||
await comfyPage.setup()
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
const checkpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await comfyPage.page.mouse.click(400, 300)
|
||||
await comfyPage.page.waitForTimeout(128)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-bypassed-state.png'
|
||||
)
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
})
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
}
|
||||
)
|
||||
|
||||
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
|
||||
comfyPage
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 83 KiB |
@@ -1,6 +1,7 @@
|
||||
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
|
||||
import pluginJs from '@eslint/js'
|
||||
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
@@ -23,10 +24,17 @@ const commonGlobals = {
|
||||
} as const
|
||||
|
||||
const settings = {
|
||||
'import/resolver': {
|
||||
typescript: true,
|
||||
node: true
|
||||
},
|
||||
'import-x/resolver-next': [
|
||||
createTypeScriptImportResolver({
|
||||
alwaysTryTypes: true,
|
||||
project: [
|
||||
'./tsconfig.json',
|
||||
'./apps/*/tsconfig.json',
|
||||
'./packages/*/tsconfig.json'
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
],
|
||||
tailwindcss: {
|
||||
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
|
||||
functions: ['cn', 'clsx', 'tw']
|
||||
@@ -52,6 +60,7 @@ export default defineConfig([
|
||||
'**/vite.config.*.timestamp*',
|
||||
'**/vitest.config.*.timestamp*',
|
||||
'packages/registry-types/src/comfyRegistryTypes.ts',
|
||||
'public/auth-sw.js',
|
||||
'src/extensions/core/*',
|
||||
'src/scripts/*',
|
||||
'src/types/generatedManagerTypes.ts',
|
||||
@@ -67,11 +76,8 @@ export default defineConfig([
|
||||
...commonParserOptions,
|
||||
projectService: {
|
||||
allowDefaultProject: [
|
||||
'vite.config.mts',
|
||||
'vite.electron.config.mts',
|
||||
'vite.types.config.mts',
|
||||
'playwright.config.ts',
|
||||
'playwright.i18n.config.ts'
|
||||
'vite.types.config.mts'
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -249,5 +255,17 @@ export default defineConfig([
|
||||
rules: {
|
||||
'no-console': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['scripts/**/*.js'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'no-console': 'off'
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
6
global.d.ts
vendored
@@ -4,6 +4,12 @@ declare const __SENTRY_DSN__: string
|
||||
declare const __ALGOLIA_APP_ID__: string
|
||||
declare const __ALGOLIA_API_KEY__: string
|
||||
declare const __USE_PROD_CONFIG__: boolean
|
||||
declare const __MIXPANEL_TOKEN__: string
|
||||
|
||||
type BuildFeatureFlags = {
|
||||
REQUIRE_SUBSCRIPTION: boolean
|
||||
}
|
||||
declare const __BUILD_FLAGS__: BuildFeatureFlags
|
||||
|
||||
interface Navigator {
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,10 @@ const config: KnipConfig = {
|
||||
],
|
||||
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
|
||||
},
|
||||
'apps/desktop-ui': {
|
||||
entry: ['src/main.ts', 'src/i18n.ts'],
|
||||
project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}']
|
||||
},
|
||||
'packages/tailwind-utils': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
},
|
||||
@@ -54,7 +58,9 @@ const config: KnipConfig = {
|
||||
'src/renderer/extensions/vueNodes/composables/slotLinkDragContext.ts',
|
||||
'src/types/spatialIndex.ts',
|
||||
'src/lib/litegraph/src/litegraph.ts',
|
||||
'src/utils/vintageClipboard.ts'
|
||||
'src/utils/vintageClipboard.ts',
|
||||
// Service worker - registered at runtime via navigator.serviceWorker.register()
|
||||
'public/auth-sw.js'
|
||||
],
|
||||
compilers: {
|
||||
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.29.2",
|
||||
"version": "1.30.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -13,6 +13,8 @@
|
||||
"build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js",
|
||||
"build:analyze": "cross-env ANALYZE_BUNDLE=true pnpm build",
|
||||
"build": "cross-env NODE_OPTIONS='--max-old-space-size=8192' pnpm typecheck && nx build",
|
||||
"size:collect": "node scripts/size-collect.js",
|
||||
"size:report": "node scripts/size-report.js",
|
||||
"collect-i18n": "pnpm exec playwright test --config=playwright.i18n.config.ts",
|
||||
"dev:desktop": "nx dev @comfyorg/desktop-ui",
|
||||
"dev:electron": "nx serve --config vite.electron.config.mts",
|
||||
@@ -88,9 +90,13 @@
|
||||
"jsdom": "catalog:",
|
||||
"knip": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-table": "catalog:",
|
||||
"mixpanel-browser": "catalog:",
|
||||
"nx": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"postcss-html": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"pretty-bytes": "catalog:",
|
||||
"rollup-plugin-visualizer": "catalog:",
|
||||
"storybook": "catalog:",
|
||||
"stylelint": "catalog:",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
--color-sand-200: #d6cfc2;
|
||||
--color-sand-300: #888682;
|
||||
|
||||
--color-pure-black: #000000;
|
||||
--color-pure-white: #ffffff;
|
||||
|
||||
--color-slate-100: #9c9eab;
|
||||
@@ -144,6 +145,9 @@
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
--button-surface: var(--color-pure-white);
|
||||
--button-surface-contrast: var(--color-pure-black);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
--code-bg-color: rgb(96 165 250 / 0.2);
|
||||
@@ -155,6 +159,7 @@
|
||||
--backdrop: var(--color-white);
|
||||
--button-hover-surface: var(--color-gray-200);
|
||||
--button-active-surface: var(--color-gray-400);
|
||||
--button-icon: var(--color-gray-600);
|
||||
--dialog-surface: var(--color-neutral-200);
|
||||
--interface-menu-component-surface-hovered: var(--color-gray-200);
|
||||
--interface-menu-component-surface-selected: var(--color-gray-400);
|
||||
@@ -201,8 +206,11 @@
|
||||
.dark-theme {
|
||||
--accent-primary: var(--color-pure-white);
|
||||
--backdrop: var(--color-neutral-900);
|
||||
--button-surface: var(--color-charcoal-600);
|
||||
--button-surface-contrast: var(--color-pure-white);
|
||||
--button-hover-surface: var(--color-charcoal-600);
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-gray-800);
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
|
||||
--interface-menu-component-surface-selected: var(--color-charcoal-300);
|
||||
@@ -244,8 +252,11 @@
|
||||
|
||||
@theme inline {
|
||||
--color-backdrop: var(--backdrop);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-active-surface: var(--button-active-surface);
|
||||
--color-button-hover-surface: var(--button-hover-surface);
|
||||
--color-button-icon: var(--button-icon);
|
||||
--color-button-surface: var(--button-surface);
|
||||
--color-button-surface-contrast: var(--button-surface-contrast);
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-interface-menu-component-surface-hovered: var(--interface-menu-component-surface-hovered);
|
||||
--color-interface-menu-component-surface-selected: var(--interface-menu-component-surface-selected);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="" viewBox="0 0 16 16" fill="none">
|
||||
<g clip-path="url(#clip0_704_2695)">
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="#9C9EAB" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="#9C9EAB" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.05048 2C5.52055 7.29512 9.23033 10.4722 14 9.94267" stroke="currentColor" stroke-width="1.3"/>
|
||||
<path d="M6.5 5.5L10 2" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M8 8L12.5 3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="square"/>
|
||||
<path d="M10.5 9.5L14 6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M7.99992 14.6667C11.6818 14.6667 14.6666 11.6819 14.6666 8.00004C14.6666 4.31814 11.6818 1.33337 7.99992 1.33337C4.31802 1.33337 1.33325 4.31814 1.33325 8.00004C1.33325 11.6819 4.31802 14.6667 7.99992 14.6667Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_704_2695">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 938 B After Width: | Height: | Size: 964 B |
218
pnpm-lock.yaml
generated
@@ -183,9 +183,18 @@ catalogs:
|
||||
lint-staged:
|
||||
specifier: ^15.2.7
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
mixpanel-browser:
|
||||
specifier: ^2.71.0
|
||||
version: 2.71.0
|
||||
nx:
|
||||
specifier: 21.4.1
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
pinia:
|
||||
specifier: ^2.1.7
|
||||
version: 2.2.2
|
||||
@@ -193,8 +202,11 @@ catalogs:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
specifier: ^3.6.2
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
primeicons:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
@@ -254,7 +266,7 @@ catalogs:
|
||||
version: 3.5.13
|
||||
vue-component-type-helpers:
|
||||
specifier: ^3.0.7
|
||||
version: 3.1.0
|
||||
version: 3.1.1
|
||||
vue-eslint-parser:
|
||||
specifier: ^10.2.0
|
||||
version: 10.2.0
|
||||
@@ -476,7 +488,7 @@ importers:
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/storybook':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
'@nx/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vitest@3.2.4)
|
||||
@@ -488,19 +500,19 @@ importers:
|
||||
version: 1.52.0
|
||||
'@storybook/addon-docs':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/vue3':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/vue3-vite':
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))
|
||||
'@tailwindcss/vite':
|
||||
specifier: 'catalog:'
|
||||
version: 4.1.12(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.0
|
||||
@@ -548,10 +560,10 @@ importers:
|
||||
version: 4.16.1(@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.35.0(jiti@2.4.2))
|
||||
eslint-plugin-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2)
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2)
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0-beta.0(tailwindcss@4.1.12)
|
||||
@@ -585,21 +597,33 @@ importers:
|
||||
lint-staged:
|
||||
specifier: 'catalog:'
|
||||
version: 15.2.7
|
||||
markdown-table:
|
||||
specifier: 'catalog:'
|
||||
version: 3.0.4
|
||||
mixpanel-browser:
|
||||
specifier: 'catalog:'
|
||||
version: 2.71.0
|
||||
nx:
|
||||
specifier: 'catalog:'
|
||||
version: 21.4.1
|
||||
picocolors:
|
||||
specifier: 'catalog:'
|
||||
version: 1.1.1
|
||||
postcss-html:
|
||||
specifier: 'catalog:'
|
||||
version: 1.8.0
|
||||
prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.2
|
||||
version: 3.6.2
|
||||
pretty-bytes:
|
||||
specifier: 'catalog:'
|
||||
version: 7.1.0
|
||||
rollup-plugin-visualizer:
|
||||
specifier: 'catalog:'
|
||||
version: 6.0.4(rollup@4.22.4)
|
||||
storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
stylelint:
|
||||
specifier: 'catalog:'
|
||||
version: 16.24.0(typescript@5.9.2)
|
||||
@@ -647,7 +671,7 @@ importers:
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-type-helpers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0
|
||||
version: 3.1.1
|
||||
vue-eslint-parser:
|
||||
specifier: 'catalog:'
|
||||
version: 10.2.0(eslint@9.35.0(jiti@2.4.2))
|
||||
@@ -2183,6 +2207,21 @@ packages:
|
||||
'@microsoft/tsdoc@0.15.1':
|
||||
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
|
||||
|
||||
'@mixpanel/rrdom@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-vX/tbnS14ZzzatC7vOyvAm9tOLU8tof0BuppBlphzEx1YHTSw8DQiAmyAc0AmXidchLV0W+cUHV/WsehPLh2hQ==}
|
||||
|
||||
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-2kSnjZZ3QZ9zOz/isOt8s54mXUUDgXk/u0eEi/rE0xBWDeuA0NHrBcqiMc+w4F/yWWUpo5F5zcuPeYpc6ufAsw==}
|
||||
|
||||
'@mixpanel/rrweb-types@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-ucIYe1mfJ2UksvXW+d3bOySTB2/0yUSqQJlUydvbBz6OO2Bhq3nJHyLXV9ExkgUMZm1ZyDcvvmNUd1+5tAXlpA==}
|
||||
|
||||
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-OomKIB6GTx5xvCLJ7iic2khT/t/tnCJUex13aEqsbSqIT/UzUUsqf+LTrgUK5ex+f6odmkCNjre2y5jvpNqn+g==}
|
||||
|
||||
'@mixpanel/rrweb@2.0.0-alpha.18.2':
|
||||
resolution: {integrity: sha512-J3dVTEu6Z4p8di7y9KKvUooNuBjX97DdG6XGWoPEPi07A9512h9M8MEtvlY3mK0PGfuC0Mz5Pv/Ws6gjGYfKQg==}
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
|
||||
|
||||
@@ -3002,6 +3041,9 @@ packages:
|
||||
'@types/chai@5.2.2':
|
||||
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
|
||||
|
||||
'@types/css-font-loading-module@0.0.7':
|
||||
resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
@@ -3500,6 +3542,9 @@ packages:
|
||||
'@webgpu/types@0.1.51':
|
||||
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
|
||||
|
||||
'@xstate/fsm@1.6.5':
|
||||
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
|
||||
|
||||
'@xterm/addon-fit@0.10.0':
|
||||
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
|
||||
peerDependencies:
|
||||
@@ -3782,6 +3827,10 @@ packages:
|
||||
balanced-match@2.0.0:
|
||||
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
|
||||
|
||||
base64-arraybuffer@1.0.2:
|
||||
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
@@ -4892,9 +4941,6 @@ packages:
|
||||
get-tsconfig@4.10.1:
|
||||
resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==}
|
||||
|
||||
glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -5972,6 +6018,9 @@ packages:
|
||||
mitt@3.0.1:
|
||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||
|
||||
mixpanel-browser@2.71.0:
|
||||
resolution: {integrity: sha512-jKmDXe68/oQFgk/9ns9Z36bA0CJ31PH8Y77XTLLGfJvhsUPbvu+7Se9e281NejZF6+OMqx7cE+zFxToozYyNrA==}
|
||||
|
||||
mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5999,11 +6048,6 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
nanoid@5.1.5:
|
||||
resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==}
|
||||
engines: {node: ^18 || >=20}
|
||||
@@ -6351,10 +6395,6 @@ packages:
|
||||
postcss-value-parser@4.2.0:
|
||||
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
|
||||
|
||||
postcss@8.5.1:
|
||||
resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
postcss@8.5.6:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -6367,11 +6407,15 @@ packages:
|
||||
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
prettier@3.3.2:
|
||||
resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==}
|
||||
prettier@3.6.2:
|
||||
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
pretty-bytes@7.1.0:
|
||||
resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
@@ -7473,9 +7517,6 @@ packages:
|
||||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-component-type-helpers@3.1.0:
|
||||
resolution: {integrity: sha512-cC1pYNRZkSS1iCvdlaMbbg2sjDwxX098FucEjtz9Yig73zYjWzQsnMe5M9H8dRNv55hAIDGUI29hF2BEUA4FMQ==}
|
||||
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
@@ -9492,6 +9533,29 @@ snapshots:
|
||||
|
||||
'@microsoft/tsdoc@0.15.1': {}
|
||||
|
||||
'@mixpanel/rrdom@2.0.0-alpha.18.2':
|
||||
dependencies:
|
||||
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
|
||||
|
||||
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
|
||||
dependencies:
|
||||
postcss: 8.5.6
|
||||
|
||||
'@mixpanel/rrweb-types@2.0.0-alpha.18.2': {}
|
||||
|
||||
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2': {}
|
||||
|
||||
'@mixpanel/rrweb@2.0.0-alpha.18.2':
|
||||
dependencies:
|
||||
'@mixpanel/rrdom': 2.0.0-alpha.18.2
|
||||
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
|
||||
'@mixpanel/rrweb-types': 2.0.0-alpha.18.2
|
||||
'@mixpanel/rrweb-utils': 2.0.0-alpha.18.2
|
||||
'@types/css-font-loading-module': 0.0.7
|
||||
'@xstate/fsm': 1.6.5
|
||||
base64-arraybuffer: 1.0.2
|
||||
mitt: 3.0.1
|
||||
|
||||
'@napi-rs/wasm-runtime@0.2.12':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.4.5
|
||||
@@ -9668,7 +9732,7 @@ snapshots:
|
||||
- typescript
|
||||
- verdaccio
|
||||
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
'@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@nx/cypress': 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2)
|
||||
'@nx/devkit': 21.4.1(nx@21.4.1)
|
||||
@@ -9676,7 +9740,7 @@ snapshots:
|
||||
'@nx/js': 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)
|
||||
'@phenomnomnominal/tsquery': 5.0.1(typescript@5.9.2)
|
||||
semver: 7.7.2
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
@@ -10027,29 +10091,29 @@ snapshots:
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.0(@types/react@19.1.9)(react@19.1.1)
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
'@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
'@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))':
|
||||
dependencies:
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
ts-dedent: 2.2.0
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
unplugin: 1.16.1
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
@@ -10059,19 +10123,19 @@ snapshots:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
'@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))':
|
||||
dependencies:
|
||||
react: 19.1.1
|
||||
react-dom: 19.1.1(react@19.1.1)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
'@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
'@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))
|
||||
find-package-json: 1.2.0
|
||||
magic-string: 0.30.19
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
typescript: 5.9.2
|
||||
vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)
|
||||
vue-component-meta: 2.2.12(typescript@5.9.2)
|
||||
@@ -10079,10 +10143,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
'@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.1
|
||||
@@ -10347,7 +10411,7 @@ snapshots:
|
||||
'@tiptap/extension-text-style': 2.10.4(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
|
||||
'@tiptap/pm': 2.10.4
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.3.2)':
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.28.3
|
||||
'@babel/parser': 7.28.4
|
||||
@@ -10355,7 +10419,7 @@ snapshots:
|
||||
'@babel/types': 7.28.4
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: 4.17.21
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
transitivePeerDependencies:
|
||||
@@ -10380,6 +10444,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/deep-eql': 4.0.2
|
||||
|
||||
'@types/css-font-loading-module@0.0.7': {}
|
||||
|
||||
'@types/debug@4.1.12':
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
@@ -10973,6 +11039,8 @@ snapshots:
|
||||
|
||||
'@webgpu/types@0.1.51': {}
|
||||
|
||||
'@xstate/fsm@1.6.5': {}
|
||||
|
||||
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
|
||||
dependencies:
|
||||
'@xterm/xterm': 5.5.0
|
||||
@@ -11289,6 +11357,8 @@ snapshots:
|
||||
|
||||
balanced-match@2.0.0: {}
|
||||
|
||||
base64-arraybuffer@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
better-opn@3.0.2:
|
||||
@@ -12133,20 +12203,20 @@ snapshots:
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2):
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.6.2):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.11.11
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.8(eslint@9.35.0(jiti@2.4.2))
|
||||
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
@@ -12585,10 +12655,6 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
get-tsconfig@4.7.5:
|
||||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
glob-parent@5.1.2:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
@@ -13861,6 +13927,10 @@ snapshots:
|
||||
|
||||
mitt@3.0.1: {}
|
||||
|
||||
mixpanel-browser@2.71.0:
|
||||
dependencies:
|
||||
'@mixpanel/rrweb': 2.0.0-alpha.18.2
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
mlly@1.8.0:
|
||||
@@ -13883,8 +13953,6 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.8: {}
|
||||
|
||||
nanoid@5.1.5: {}
|
||||
|
||||
napi-postinstall@0.3.3: {}
|
||||
@@ -14264,14 +14332,14 @@ snapshots:
|
||||
dependencies:
|
||||
htmlparser2: 8.0.2
|
||||
js-tokens: 9.0.1
|
||||
postcss: 8.5.1
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.1)
|
||||
postcss: 8.5.6
|
||||
postcss-safe-parser: 6.0.0(postcss@8.5.6)
|
||||
|
||||
postcss-resolve-nested-selector@0.1.6: {}
|
||||
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.1):
|
||||
postcss-safe-parser@6.0.0(postcss@8.5.6):
|
||||
dependencies:
|
||||
postcss: 8.5.1
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-safe-parser@7.0.1(postcss@8.5.6):
|
||||
dependencies:
|
||||
@@ -14289,12 +14357,6 @@ snapshots:
|
||||
|
||||
postcss-value-parser@4.2.0: {}
|
||||
|
||||
postcss@8.5.1:
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.6:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@@ -14307,7 +14369,9 @@ snapshots:
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
|
||||
prettier@3.3.2: {}
|
||||
prettier@3.6.2: {}
|
||||
|
||||
pretty-bytes@7.1.0: {}
|
||||
|
||||
pretty-format@27.5.1:
|
||||
dependencies:
|
||||
@@ -14984,7 +15048,7 @@ snapshots:
|
||||
internal-slot: 1.1.0
|
||||
optional: true
|
||||
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)):
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@testing-library/jest-dom': 6.6.4
|
||||
@@ -14999,7 +15063,7 @@ snapshots:
|
||||
semver: 7.7.2
|
||||
ws: 8.18.3
|
||||
optionalDependencies:
|
||||
prettier: 3.3.2
|
||||
prettier: 3.6.2
|
||||
transitivePeerDependencies:
|
||||
- '@testing-library/dom'
|
||||
- bufferutil
|
||||
@@ -15321,7 +15385,7 @@ snapshots:
|
||||
tsx@4.19.4:
|
||||
dependencies:
|
||||
esbuild: 0.25.5
|
||||
get-tsconfig: 4.7.5
|
||||
get-tsconfig: 4.10.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
@@ -15673,7 +15737,7 @@ snapshots:
|
||||
vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.1
|
||||
postcss: 8.5.6
|
||||
rollup: 4.22.4
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.10
|
||||
@@ -15738,8 +15802,6 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-component-type-helpers@3.1.0: {}
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
|
||||
@@ -62,10 +62,13 @@ catalog:
|
||||
jsdom: ^26.1.0
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
markdown-table: ^3.0.4
|
||||
nx: 21.4.1
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.3.2
|
||||
prettier: ^3.6.2
|
||||
pretty-bytes: ^7.1.0
|
||||
primeicons: ^7.0.0
|
||||
primevue: ^4.2.5
|
||||
rollup-plugin-visualizer: ^6.0.4
|
||||
@@ -95,12 +98,10 @@ catalog:
|
||||
zod: ^3.23.8
|
||||
zod-to-json-schema: ^3.24.1
|
||||
zod-validation-error: ^3.3.0
|
||||
mixpanel-browser: ^2.71.0
|
||||
|
||||
cleanupUnusedCatalogs: true
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- '@firebase/util'
|
||||
- protobufjs
|
||||
@@ -115,3 +116,6 @@ onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- nx
|
||||
- oxc-resolver
|
||||
|
||||
overrides:
|
||||
'@types/eslint': '-'
|
||||
|
||||
BIN
public/assets/images/cloud-subscription.webm
Normal file
147
public/auth-sw.js
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @fileoverview Authentication Service Worker
|
||||
* Intercepts /api/view requests and adds Firebase authentication headers.
|
||||
* Required for browser-native requests (img, video, audio) that cannot send custom headers.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} AuthHeader
|
||||
* @property {string} Authorization - Bearer token for authentication
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CachedAuth
|
||||
* @property {AuthHeader|null} header
|
||||
* @property {number} expiresAt - Timestamp when cache expires
|
||||
*/
|
||||
|
||||
const CACHE_TTL_MS = 50 * 60 * 1000 // 50 minutes (Firebase tokens expire in 1 hour)
|
||||
|
||||
/** @type {CachedAuth|null} */
|
||||
let authCache = null
|
||||
|
||||
/** @type {Promise<AuthHeader|null>|null} */
|
||||
let authRequestInFlight = null
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'INVALIDATE_AUTH_HEADER') {
|
||||
authCache = null
|
||||
authRequestInFlight = null
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const url = new URL(event.request.url)
|
||||
|
||||
if (
|
||||
!url.pathname.startsWith('/api/view') &&
|
||||
!url.pathname.startsWith('/api/viewvideo')
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
const authHeader = await getAuthHeader()
|
||||
|
||||
if (!authHeader) {
|
||||
return fetch(event.request)
|
||||
}
|
||||
|
||||
const headers = new Headers(event.request.headers)
|
||||
for (const [key, value] of Object.entries(authHeader)) {
|
||||
headers.set(key, value)
|
||||
}
|
||||
|
||||
return fetch(
|
||||
new Request(event.request.url, {
|
||||
method: event.request.method,
|
||||
headers: headers,
|
||||
mode: 'same-origin',
|
||||
credentials: event.request.credentials,
|
||||
cache: 'no-store',
|
||||
redirect: event.request.redirect,
|
||||
referrer: event.request.referrer,
|
||||
integrity: event.request.integrity
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[Auth SW] Request failed:', error)
|
||||
return fetch(event.request)
|
||||
}
|
||||
})()
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Gets auth header from cache or requests from main thread
|
||||
* @returns {Promise<AuthHeader|null>}
|
||||
*/
|
||||
async function getAuthHeader() {
|
||||
// Return cached value if valid
|
||||
if (authCache && authCache.expiresAt > Date.now()) {
|
||||
return authCache.header
|
||||
}
|
||||
|
||||
// Clear expired cache
|
||||
if (authCache) {
|
||||
authCache = null
|
||||
}
|
||||
|
||||
// Deduplicate concurrent requests
|
||||
if (authRequestInFlight) {
|
||||
return authRequestInFlight
|
||||
}
|
||||
|
||||
authRequestInFlight = requestAuthHeaderFromMainThread()
|
||||
const header = await authRequestInFlight
|
||||
authRequestInFlight = null
|
||||
|
||||
// Cache the result
|
||||
if (header) {
|
||||
authCache = {
|
||||
header,
|
||||
expiresAt: Date.now() + CACHE_TTL_MS
|
||||
}
|
||||
}
|
||||
|
||||
return header
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests auth header from main thread via MessageChannel
|
||||
* @returns {Promise<AuthHeader|null>}
|
||||
*/
|
||||
async function requestAuthHeaderFromMainThread() {
|
||||
const clients = await self.clients.matchAll()
|
||||
if (clients.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const messageChannel = new MessageChannel()
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let timeoutId
|
||||
|
||||
messageChannel.port1.onmessage = (event) => {
|
||||
clearTimeout(timeoutId)
|
||||
resolve(event.data.authHeader)
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
console.error(
|
||||
'[Auth SW] Timeout waiting for auth header from main thread'
|
||||
)
|
||||
resolve(null)
|
||||
}, 1000)
|
||||
|
||||
clients[0].postMessage({ type: 'REQUEST_AUTH_HEADER' }, [
|
||||
messageChannel.port2
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
130
scripts/bundle-categories.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Bundle categorization configuration
|
||||
*
|
||||
* This file defines how bundles are categorized in size reports.
|
||||
* Categories help identify which parts of the application are growing.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleCategory
|
||||
* @property {string} name - Display name of the category
|
||||
* @property {string} description - Description of what this category includes
|
||||
* @property {RegExp[]} patterns - Regex patterns to match bundle files
|
||||
* @property {number} order - Sort order for display (lower = first)
|
||||
*/
|
||||
|
||||
/** @type {BundleCategory[]} */
|
||||
export const BUNDLE_CATEGORIES = [
|
||||
{
|
||||
name: 'App Entry Points',
|
||||
description: 'Main entry bundles and manifests',
|
||||
patterns: [/^index-.*\.js$/i, /^manifest-.*\.js$/i],
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
name: 'Graph Workspace',
|
||||
description: 'Graph editor runtime, canvas, workflow orchestration',
|
||||
patterns: [
|
||||
/Graph(View|State)?-.*\.js$/i,
|
||||
/(Canvas|Workflow|History|NodeGraph|Compositor)-.*\.js$/i
|
||||
],
|
||||
order: 2
|
||||
},
|
||||
{
|
||||
name: 'Views & Navigation',
|
||||
description: 'Top-level views, pages, and routed surfaces',
|
||||
patterns: [/.*(View|Page|Layout|Screen|Route)-.*\.js$/i],
|
||||
order: 3
|
||||
},
|
||||
{
|
||||
name: 'Panels & Settings',
|
||||
description: 'Configuration panels, inspectors, and settings screens',
|
||||
patterns: [/.*(Panel|Settings|Config|Preferences|Manager)-.*\.js$/i],
|
||||
order: 4
|
||||
},
|
||||
{
|
||||
name: 'User & Accounts',
|
||||
description: 'Authentication, profile, and account management bundles',
|
||||
patterns: [
|
||||
/.*((User(Panel|Select|Auth|Account|Profile|Settings|Preferences|Manager|List|Menu|Modal))|Account|Auth|Profile|Login|Signup|Password).*-.+\.js$/i
|
||||
],
|
||||
order: 5
|
||||
},
|
||||
{
|
||||
name: 'Editors & Dialogs',
|
||||
description: 'Modals, dialogs, drawers, and in-app editors',
|
||||
patterns: [/.*(Modal|Dialog|Drawer|Editor)-.*\.js$/i],
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
name: 'UI Components',
|
||||
description: 'Reusable component library chunks',
|
||||
patterns: [
|
||||
/.*(Button|Avatar|Badge|Dropdown|Tabs|Table|List|Card|Form|Input|Toggle|Menu|Toolbar|Sidebar)-.*\.js$/i,
|
||||
/.*\.vue_vue_type_script_setup_true_lang-.*\.js$/i
|
||||
],
|
||||
order: 7
|
||||
},
|
||||
{
|
||||
name: 'Data & Services',
|
||||
description: 'Stores, services, APIs, and repositories',
|
||||
patterns: [/.*(Service|Store|Api|Repository)-.*\.js$/i],
|
||||
order: 8
|
||||
},
|
||||
{
|
||||
name: 'Utilities & Hooks',
|
||||
description: 'Helpers, composables, and utility bundles',
|
||||
patterns: [
|
||||
/.*(Util|Utils|Helper|Composable|Hook)-.*\.js$/i,
|
||||
/use[A-Z].*\.js$/
|
||||
],
|
||||
order: 9
|
||||
},
|
||||
{
|
||||
name: 'Vendor & Third-Party',
|
||||
description: 'External libraries and shared vendor chunks',
|
||||
patterns: [
|
||||
/^(chunk|vendor|prime|three|lodash|chart|firebase|yjs|axios|uuid)-.*\.js$/i
|
||||
],
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
description: 'Bundles that do not match a named category',
|
||||
patterns: [/.*/],
|
||||
order: 99
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* Categorize a bundle file based on its name
|
||||
*
|
||||
* @param {string} fileName - The bundle file name (e.g., "assets/GraphView-BnV6iF9h.js")
|
||||
* @returns {string} - The category name
|
||||
*/
|
||||
export function categorizeBundle(fileName) {
|
||||
// Extract just the file name without path
|
||||
const baseName = fileName.split('/').pop() || fileName
|
||||
|
||||
// Find the first matching category
|
||||
for (const category of BUNDLE_CATEGORIES) {
|
||||
for (const pattern of category.patterns) {
|
||||
if (pattern.test(baseName)) {
|
||||
return category.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'Other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category metadata by name
|
||||
*
|
||||
* @param {string} categoryName - The category name
|
||||
* @returns {BundleCategory | undefined} - The category metadata
|
||||
*/
|
||||
export function getCategoryMetadata(categoryName) {
|
||||
return BUNDLE_CATEGORIES.find((cat) => cat.name === categoryName)
|
||||
}
|
||||
90
scripts/size-collect.js
Normal file
@@ -0,0 +1,90 @@
|
||||
// @ts-check
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { brotliCompressSync, gzipSync } from 'node:zlib'
|
||||
import pico from 'picocolors'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { categorizeBundle } from './bundle-categories.js'
|
||||
|
||||
const distDir = path.resolve('dist')
|
||||
const sizeDir = path.resolve('temp/size')
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {string} file
|
||||
* @property {string} category
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main function to collect bundle size data
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(distDir)) {
|
||||
console.error(pico.red('Error: dist directory does not exist'))
|
||||
console.error(pico.yellow('Please run "pnpm build" first'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(pico.blue('\nCollecting bundle size data...\n'))
|
||||
|
||||
// Collect main bundle files from dist/assets
|
||||
const assetsDir = path.join(distDir, 'assets')
|
||||
const bundles = []
|
||||
|
||||
if (existsSync(assetsDir)) {
|
||||
const files = await readdir(assetsDir)
|
||||
const jsFiles = files.filter(
|
||||
(file) => file.endsWith('.js') && !file.includes('legacy')
|
||||
)
|
||||
|
||||
for (const file of jsFiles) {
|
||||
const filePath = path.join(assetsDir, file)
|
||||
const content = await readFile(filePath, 'utf-8')
|
||||
const size = Buffer.byteLength(content)
|
||||
const gzip = gzipSync(content).length
|
||||
const brotli = brotliCompressSync(content).length
|
||||
const fileName = `assets/${file}`
|
||||
const category = categorizeBundle(fileName)
|
||||
|
||||
bundles.push({
|
||||
file: fileName,
|
||||
category,
|
||||
size,
|
||||
gzip,
|
||||
brotli
|
||||
})
|
||||
|
||||
console.log(
|
||||
`${pico.green(file)} ${pico.dim(`[${category}]`)} - ` +
|
||||
`Size: ${prettyBytes(size)} / ` +
|
||||
`Gzip: ${prettyBytes(gzip)} / ` +
|
||||
`Brotli: ${prettyBytes(brotli)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create temp/size directory
|
||||
await mkdir(sizeDir, { recursive: true })
|
||||
|
||||
// Write individual bundle files
|
||||
for (const bundle of bundles) {
|
||||
const fileName = bundle.file.replace(/[/\\]/g, '_').replace('.js', '.json')
|
||||
await writeFile(
|
||||
path.join(sizeDir, fileName),
|
||||
JSON.stringify(bundle, null, 2),
|
||||
'utf-8'
|
||||
)
|
||||
}
|
||||
|
||||
console.log(
|
||||
pico.green(`\n✓ Collected size data for ${bundles.length} bundles\n`)
|
||||
)
|
||||
console.log(pico.blue(`Data saved to: ${sizeDir}\n`))
|
||||
}
|
||||
560
scripts/size-report.js
Normal file
@@ -0,0 +1,560 @@
|
||||
// @ts-check
|
||||
import { markdownTable } from 'markdown-table'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { getCategoryMetadata } from './bundle-categories.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeMetrics
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SizeResult
|
||||
* @property {number} size
|
||||
* @property {number} gzip
|
||||
* @property {number} brotli
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {SizeResult & { file: string, category?: string }} BundleResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'added' | 'removed' | 'increased' | 'decreased' | 'unchanged'} BundleStatus
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleDiff
|
||||
* @property {string} fileName
|
||||
* @property {BundleResult | undefined} curr
|
||||
* @property {BundleResult | undefined} prev
|
||||
* @property {SizeMetrics} diff
|
||||
* @property {BundleStatus} status
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CountSummary
|
||||
* @property {number} added
|
||||
* @property {number} removed
|
||||
* @property {number} increased
|
||||
* @property {number} decreased
|
||||
* @property {number} unchanged
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} CategoryReport
|
||||
* @property {string} name
|
||||
* @property {string | undefined} description
|
||||
* @property {number} order
|
||||
* @property {{ current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }} metrics
|
||||
* @property {CountSummary} counts
|
||||
* @property {BundleDiff[]} bundles
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BundleReport
|
||||
* @property {CategoryReport[]} categories
|
||||
* @property {{ currentBundles: number, baselineBundles: number, metrics: { current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }, counts: CountSummary }} overall
|
||||
* @property {boolean} hasBaseline
|
||||
*/
|
||||
|
||||
const currDir = path.resolve('temp/size')
|
||||
const prevDir = path.resolve('temp/size-prev')
|
||||
|
||||
run()
|
||||
|
||||
/**
|
||||
* Main entry for generating the size report
|
||||
*/
|
||||
async function run() {
|
||||
if (!existsSync(currDir)) {
|
||||
console.error('Error: temp/size directory does not exist')
|
||||
console.error('Please run "pnpm size:collect" first')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const report = await buildBundleReport()
|
||||
const output = renderReport(report)
|
||||
process.stdout.write(output)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build bundle comparison data from current and baseline artifacts
|
||||
* @returns {Promise<BundleReport>}
|
||||
*/
|
||||
async function buildBundleReport() {
|
||||
/**
|
||||
* @param {string[]} files
|
||||
* @returns {string[]}
|
||||
*/
|
||||
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
|
||||
|
||||
const currFiles = filterFiles(await readdir(currDir))
|
||||
const baselineFiles = existsSync(prevDir)
|
||||
? filterFiles(await readdir(prevDir))
|
||||
: []
|
||||
const fileList = new Set([...currFiles, ...baselineFiles])
|
||||
|
||||
/** @type {Map<string, CategoryReport>} */
|
||||
const categories = new Map()
|
||||
|
||||
const overall = {
|
||||
currentBundles: 0,
|
||||
baselineBundles: 0,
|
||||
metrics: {
|
||||
current: createMetrics(),
|
||||
baseline: createMetrics(),
|
||||
diff: createMetrics()
|
||||
},
|
||||
counts: createCounts()
|
||||
}
|
||||
|
||||
for (const file of fileList) {
|
||||
const currPath = path.resolve(currDir, file)
|
||||
const prevPath = path.resolve(prevDir, file)
|
||||
|
||||
const curr = await importJSON(currPath)
|
||||
const prev = await importJSON(prevPath)
|
||||
const fileName = curr?.file || prev?.file
|
||||
if (!fileName) continue
|
||||
|
||||
const categoryName = curr?.category || prev?.category || 'Other'
|
||||
const category = ensureCategoryEntry(categories, categoryName)
|
||||
|
||||
const currMetrics = toMetrics(curr)
|
||||
const baselineMetrics = toMetrics(prev)
|
||||
const diffMetrics = subtractMetrics(currMetrics, baselineMetrics)
|
||||
const status = getStatus(curr, prev, diffMetrics.size)
|
||||
|
||||
if (curr) {
|
||||
overall.currentBundles++
|
||||
}
|
||||
if (prev) {
|
||||
overall.baselineBundles++
|
||||
}
|
||||
|
||||
addMetrics(overall.metrics.current, currMetrics)
|
||||
addMetrics(overall.metrics.baseline, baselineMetrics)
|
||||
addMetrics(overall.metrics.diff, diffMetrics)
|
||||
incrementStatus(overall.counts, status)
|
||||
|
||||
addMetrics(category.metrics.current, currMetrics)
|
||||
addMetrics(category.metrics.baseline, baselineMetrics)
|
||||
addMetrics(category.metrics.diff, diffMetrics)
|
||||
incrementStatus(category.counts, status)
|
||||
|
||||
category.bundles.push({
|
||||
fileName,
|
||||
curr,
|
||||
prev,
|
||||
diff: diffMetrics,
|
||||
status
|
||||
})
|
||||
}
|
||||
|
||||
const sortedCategories = Array.from(categories.values()).sort(
|
||||
(a, b) => a.order - b.order
|
||||
)
|
||||
|
||||
return {
|
||||
categories: sortedCategories,
|
||||
overall,
|
||||
hasBaseline: baselineFiles.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the complete report in markdown
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderReport(report) {
|
||||
const parts = ['## Bundle Size Report\n']
|
||||
|
||||
parts.push(renderSummary(report))
|
||||
|
||||
if (report.categories.length > 0) {
|
||||
const glance = renderCategoryGlance(report)
|
||||
if (glance) {
|
||||
parts.push('\n' + glance)
|
||||
}
|
||||
parts.push('\n' + renderCategoryDetails(report))
|
||||
}
|
||||
|
||||
return (
|
||||
parts
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trimEnd() + '\n'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render overall summary bullets
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderSummary(report) {
|
||||
const { overall, hasBaseline } = report
|
||||
const lines = ['**Summary**']
|
||||
|
||||
const rawLineParts = [
|
||||
`- Raw size: ${prettyBytes(overall.metrics.current.size)}`
|
||||
]
|
||||
if (hasBaseline) {
|
||||
rawLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.size)}`)
|
||||
rawLineParts.push(`— ${formatDiffIndicator(overall.metrics.diff.size)}`)
|
||||
}
|
||||
lines.push(rawLineParts.join(' '))
|
||||
|
||||
const gzipLineParts = [`- Gzip: ${prettyBytes(overall.metrics.current.gzip)}`]
|
||||
if (hasBaseline) {
|
||||
gzipLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.gzip)}`)
|
||||
gzipLineParts.push(`— ${formatDiffIndicator(overall.metrics.diff.gzip)}`)
|
||||
}
|
||||
lines.push(gzipLineParts.join(' '))
|
||||
|
||||
const brotliLineParts = [
|
||||
`- Brotli: ${prettyBytes(overall.metrics.current.brotli)}`
|
||||
]
|
||||
if (hasBaseline) {
|
||||
brotliLineParts.push(
|
||||
`baseline ${prettyBytes(overall.metrics.baseline.brotli)}`
|
||||
)
|
||||
brotliLineParts.push(
|
||||
`— ${formatDiffIndicator(overall.metrics.diff.brotli)}`
|
||||
)
|
||||
}
|
||||
lines.push(brotliLineParts.join(' '))
|
||||
|
||||
const bundleStats = [`${overall.currentBundles} current`]
|
||||
if (hasBaseline) {
|
||||
bundleStats.push(`${overall.baselineBundles} baseline`)
|
||||
}
|
||||
|
||||
const statusParts = []
|
||||
if (overall.counts.added) statusParts.push(`${overall.counts.added} added`)
|
||||
if (overall.counts.removed)
|
||||
statusParts.push(`${overall.counts.removed} removed`)
|
||||
if (overall.counts.increased)
|
||||
statusParts.push(`${overall.counts.increased} grew`)
|
||||
if (overall.counts.decreased)
|
||||
statusParts.push(`${overall.counts.decreased} shrank`)
|
||||
|
||||
let bundlesLine = `- Bundles: ${bundleStats.join(' • ')}`
|
||||
if (statusParts.length > 0) {
|
||||
bundlesLine += ` • ${statusParts.join(' / ')}`
|
||||
}
|
||||
lines.push(bundlesLine)
|
||||
|
||||
if (!hasBaseline) {
|
||||
lines.push(
|
||||
'_Baseline artifact not found; showing current bundle sizes only._'
|
||||
)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a compact category glance line
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderCategoryGlance(report) {
|
||||
const { categories, hasBaseline } = report
|
||||
const relevant = categories.filter(
|
||||
(category) =>
|
||||
category.metrics.current.size > 0 ||
|
||||
(hasBaseline && category.metrics.baseline.size > 0)
|
||||
)
|
||||
|
||||
if (relevant.length === 0) return ''
|
||||
|
||||
const sorted = relevant.slice().sort((a, b) => {
|
||||
if (hasBaseline) {
|
||||
return (
|
||||
Math.abs(b.metrics.diff.size) - Math.abs(a.metrics.diff.size) ||
|
||||
b.metrics.current.size - a.metrics.current.size
|
||||
)
|
||||
}
|
||||
return b.metrics.current.size - a.metrics.current.size
|
||||
})
|
||||
|
||||
const limit = 6
|
||||
const trimmed = sorted.slice(0, limit)
|
||||
const parts = trimmed.map((category) => {
|
||||
const currentStr = prettyBytes(category.metrics.current.size)
|
||||
if (hasBaseline) {
|
||||
return `${category.name} ${formatDiffIndicator(category.metrics.diff.size)} (${currentStr})`
|
||||
}
|
||||
return `${category.name} ${currentStr}`
|
||||
})
|
||||
|
||||
if (sorted.length > limit) {
|
||||
parts.push(`+ ${sorted.length - limit} more`)
|
||||
}
|
||||
|
||||
return `**Category Glance**\n${parts.join(' · ')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Render per-category detail tables wrapped in collapsible sections
|
||||
* @param {BundleReport} report
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderCategoryDetails(report) {
|
||||
const lines = ['<details>', '<summary>Per-category breakdown</summary>', '']
|
||||
|
||||
for (const category of report.categories) {
|
||||
lines.push(renderCategoryBlock(category, report.hasBaseline))
|
||||
}
|
||||
|
||||
lines.push('</details>')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single category block with its table
|
||||
* @param {CategoryReport} category
|
||||
* @param {boolean} hasBaseline
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderCategoryBlock(category, hasBaseline) {
|
||||
const lines = ['<details>']
|
||||
const currentStr = prettyBytes(category.metrics.current.size)
|
||||
const summaryParts = [`<summary>${category.name} — ${currentStr}`]
|
||||
|
||||
if (hasBaseline) {
|
||||
summaryParts.push(
|
||||
` (baseline ${prettyBytes(category.metrics.baseline.size)}) • ${formatDiffIndicator(category.metrics.diff.size)}`
|
||||
)
|
||||
}
|
||||
|
||||
summaryParts.push('</summary>')
|
||||
lines.push(summaryParts.join(''))
|
||||
|
||||
if (category.description) {
|
||||
lines.push(`_${category.description}_`)
|
||||
}
|
||||
|
||||
if (category.bundles.length === 0) {
|
||||
lines.push('No bundles matched this category.\n')
|
||||
lines.push('</details>\n')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
const headers = hasBaseline
|
||||
? ['File', 'Before', 'After', 'Δ Raw', 'Δ Gzip', 'Δ Brotli']
|
||||
: ['File', 'Size', 'Gzip', 'Brotli']
|
||||
|
||||
const rows = category.bundles
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const diffMagnitude = Math.abs(b.diff.size) - Math.abs(a.diff.size)
|
||||
if (diffMagnitude !== 0) return diffMagnitude
|
||||
return a.fileName.localeCompare(b.fileName)
|
||||
})
|
||||
.map((bundle) => {
|
||||
if (hasBaseline) {
|
||||
return [
|
||||
formatFileLabel(bundle),
|
||||
formatSize(bundle.prev?.size),
|
||||
formatSize(bundle.curr?.size),
|
||||
formatDiffIndicator(bundle.diff.size),
|
||||
formatDiffIndicator(bundle.diff.gzip),
|
||||
formatDiffIndicator(bundle.diff.brotli)
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
formatFileLabel(bundle),
|
||||
formatSize(bundle.curr?.size),
|
||||
formatSize(bundle.curr?.gzip),
|
||||
formatSize(bundle.curr?.brotli)
|
||||
]
|
||||
})
|
||||
|
||||
lines.push(markdownTable([headers, ...rows]))
|
||||
|
||||
const statusParts = []
|
||||
if (category.counts.added) statusParts.push(`${category.counts.added} added`)
|
||||
if (category.counts.removed)
|
||||
statusParts.push(`${category.counts.removed} removed`)
|
||||
if (category.counts.increased)
|
||||
statusParts.push(`${category.counts.increased} grew`)
|
||||
if (category.counts.decreased)
|
||||
statusParts.push(`${category.counts.decreased} shrank`)
|
||||
|
||||
if (statusParts.length > 0) {
|
||||
lines.push(`\n_Status:_ ${statusParts.join(' / ')}`)
|
||||
}
|
||||
|
||||
lines.push('</details>\n')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a category entry exists in the map
|
||||
* @param {Map<string, CategoryReport>} categories
|
||||
* @param {string} categoryName
|
||||
* @returns {CategoryReport}
|
||||
*/
|
||||
function ensureCategoryEntry(categories, categoryName) {
|
||||
if (!categories.has(categoryName)) {
|
||||
const meta = getCategoryMetadata(categoryName)
|
||||
categories.set(categoryName, {
|
||||
name: categoryName,
|
||||
description: meta?.description,
|
||||
order: meta?.order ?? 99,
|
||||
metrics: {
|
||||
current: createMetrics(),
|
||||
baseline: createMetrics(),
|
||||
diff: createMetrics()
|
||||
},
|
||||
counts: createCounts(),
|
||||
bundles: []
|
||||
})
|
||||
}
|
||||
// @ts-expect-error - ensured by check above
|
||||
return categories.get(categoryName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bundle result to metrics
|
||||
* @param {BundleResult | undefined} bundle
|
||||
* @returns {SizeMetrics}
|
||||
*/
|
||||
function toMetrics(bundle) {
|
||||
if (!bundle) return createMetrics()
|
||||
return {
|
||||
size: bundle.size,
|
||||
gzip: bundle.gzip,
|
||||
brotli: bundle.brotli
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty metrics object
|
||||
* @returns {SizeMetrics}
|
||||
*/
|
||||
function createMetrics() {
|
||||
return { size: 0, gzip: 0, brotli: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add source metrics into target metrics
|
||||
* @param {SizeMetrics} target
|
||||
* @param {SizeMetrics} source
|
||||
*/
|
||||
function addMetrics(target, source) {
|
||||
target.size += source.size
|
||||
target.gzip += source.gzip
|
||||
target.brotli += source.brotli
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract baseline metrics from current metrics
|
||||
* @param {SizeMetrics} current
|
||||
* @param {SizeMetrics} baseline
|
||||
* @returns {SizeMetrics}
|
||||
*/
|
||||
function subtractMetrics(current, baseline) {
|
||||
return {
|
||||
size: current.size - baseline.size,
|
||||
gzip: current.gzip - baseline.gzip,
|
||||
brotli: current.brotli - baseline.brotli
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty counts object
|
||||
* @returns {CountSummary}
|
||||
*/
|
||||
function createCounts() {
|
||||
return { added: 0, removed: 0, increased: 0, decreased: 0, unchanged: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment status counters
|
||||
* @param {CountSummary} counts
|
||||
* @param {BundleStatus} status
|
||||
*/
|
||||
function incrementStatus(counts, status) {
|
||||
counts[status] += 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine bundle status for reporting
|
||||
* @param {BundleResult | undefined} curr
|
||||
* @param {BundleResult | undefined} prev
|
||||
* @param {number} sizeDiff
|
||||
* @returns {BundleStatus}
|
||||
*/
|
||||
function getStatus(curr, prev, sizeDiff) {
|
||||
if (curr && prev) {
|
||||
if (sizeDiff > 0) return 'increased'
|
||||
if (sizeDiff < 0) return 'decreased'
|
||||
return 'unchanged'
|
||||
}
|
||||
if (curr && !prev) return 'added'
|
||||
if (!curr && prev) return 'removed'
|
||||
return 'unchanged'
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file label with status hints
|
||||
* @param {BundleDiff} bundle
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatFileLabel(bundle) {
|
||||
if (bundle.status === 'added') {
|
||||
return `**${bundle.fileName}** _(new)_`
|
||||
}
|
||||
if (bundle.status === 'removed') {
|
||||
return `~~${bundle.fileName}~~ _(removed)_`
|
||||
}
|
||||
return bundle.fileName
|
||||
}
|
||||
|
||||
/**
|
||||
* Format size for table output
|
||||
* @param {number | undefined} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatSize(value) {
|
||||
if (value === undefined) return '—'
|
||||
return prettyBytes(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a diff with an indicator emoji
|
||||
* @param {number} diff
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDiffIndicator(diff) {
|
||||
if (diff > 0) {
|
||||
return `:red_circle: +${prettyBytes(diff)}`
|
||||
}
|
||||
if (diff < 0) {
|
||||
return `:green_circle: -${prettyBytes(Math.abs(diff))}`
|
||||
}
|
||||
return ':white_circle: 0 B'
|
||||
}
|
||||
|
||||
/**
|
||||
* Import JSON data if it exists
|
||||
* @template T
|
||||
* @param {string} filePath
|
||||
* @returns {Promise<T | undefined>}
|
||||
*/
|
||||
async function importJSON(filePath) {
|
||||
if (!existsSync(filePath)) return undefined
|
||||
return (await import(filePath, { with: { type: 'json' } })).default
|
||||
}
|
||||
@@ -53,7 +53,7 @@
|
||||
"comfy_base": {
|
||||
"fg-color": "#222",
|
||||
"bg-color": "#DDD",
|
||||
"comfy-menu-bg": "#F5F5F5",
|
||||
"comfy-menu-bg": "#FFFFFF",
|
||||
"comfy-menu-hover-bg": "#ccc",
|
||||
"comfy-menu-secondary-bg": "#EEE",
|
||||
"comfy-input-bg": "#C9C9C9",
|
||||
|
||||
@@ -1,48 +1,88 @@
|
||||
<template>
|
||||
<Splitter
|
||||
:key="sidebarStateKey"
|
||||
class="splitter-overlay-root splitter-overlay"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
<div class="splitter-overlay-root pointer-events-none flex flex-col">
|
||||
<slot name="workflow-tabs" />
|
||||
|
||||
<div
|
||||
class="pointer-events-none flex flex-1 overflow-hidden"
|
||||
:class="{
|
||||
'flex-row': sidebarLocation === 'left',
|
||||
'flex-row-reverse': sidebarLocation === 'right'
|
||||
}"
|
||||
>
|
||||
<div class="side-toolbar-container">
|
||||
<slot name="side-toolbar" />
|
||||
</div>
|
||||
|
||||
<SplitterPanel :size="100">
|
||||
<Splitter
|
||||
class="splitter-overlay max-w-full"
|
||||
layout="vertical"
|
||||
:pt:gutter="bottomPanelVisible ? '' : 'hidden'"
|
||||
state-key="bottom-panel-splitter"
|
||||
key="main-splitter-stable"
|
||||
class="splitter-overlay flex-1 overflow-hidden"
|
||||
:pt:gutter="sidebarPanelVisible ? '' : 'hidden'"
|
||||
:state-key="sidebarStateKey || 'main-splitter'"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'left'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'left'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel v-show="bottomPanelVisible" class="bottom-panel">
|
||||
<slot name="bottom-panel" />
|
||||
|
||||
<SplitterPanel :size="80" class="flex flex-col">
|
||||
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
|
||||
|
||||
<Splitter
|
||||
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
|
||||
layout="vertical"
|
||||
:pt:gutter="
|
||||
'rounded-tl-lg rounded-tr-lg ' +
|
||||
(bottomPanelVisible ? '' : 'hidden')
|
||||
"
|
||||
state-key="bottom-panel-splitter"
|
||||
state-storage="local"
|
||||
>
|
||||
<SplitterPanel class="graph-canvas-panel relative">
|
||||
<slot name="graph-canvas-panel" />
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
v-show="bottomPanelVisible"
|
||||
class="bottom-panel pointer-events-auto rounded-lg"
|
||||
>
|
||||
<slot name="bottom-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel pointer-events-auto"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'right'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel
|
||||
v-show="sidebarPanelVisible"
|
||||
v-if="sidebarLocation === 'right'"
|
||||
class="side-bar-panel"
|
||||
:min-size="10"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="side-bar-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -74,7 +114,11 @@ const activeSidebarTabId = computed(
|
||||
)
|
||||
|
||||
const sidebarStateKey = computed(() => {
|
||||
return unifiedWidth.value ? 'unified-sidebar' : activeSidebarTabId.value ?? ''
|
||||
if (unifiedWidth.value) {
|
||||
return 'unified-sidebar'
|
||||
}
|
||||
// When no tab is active, use a default key to maintain state
|
||||
return activeSidebarTabId.value ?? 'default-sidebar'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -93,12 +137,17 @@ const sidebarStateKey = computed(() => {
|
||||
|
||||
.side-bar-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bottom-panel {
|
||||
background-color: var(--bg-color);
|
||||
pointer-events: auto;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.splitter-overlay-bottom :deep(.p-splitter-gutter) {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
|
||||
.splitter-overlay {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-show="workspaceState.focusMode"
|
||||
class="comfy-menu-hamburger no-drag"
|
||||
:style="positionCSS"
|
||||
class="comfy-menu-hamburger no-drag top-0 right-0"
|
||||
>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
@@ -15,14 +14,13 @@
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
<div v-show="menuSetting !== 'Bottom'" class="window-actions-spacer" />
|
||||
<div class="window-actions-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import type { CSSProperties } from 'vue'
|
||||
import { computed, watchEffect } from 'vue'
|
||||
import { watchEffect } from 'vue'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
@@ -45,15 +43,6 @@ watchEffect(() => {
|
||||
app.ui.menuContainer.style.display = 'block'
|
||||
}
|
||||
})
|
||||
|
||||
const menuSetting = computed(() => settingStore.get('Comfy.UseNewMenu'))
|
||||
const positionCSS = computed<CSSProperties>(() =>
|
||||
// 'Bottom' menuSetting shows the hamburger button in the bottom right corner
|
||||
// 'Disabled', 'Top' menuSetting shows the hamburger button in the top right corner
|
||||
menuSetting.value === 'Bottom'
|
||||
? { bottom: '0px', right: '0px' }
|
||||
: { top: '0px', right: '0px' }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
51
src/components/TopMenuSection.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<SubgraphBreadcrumb />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto mx-2 flex h-12 items-center rounded-lg px-2 shadow-md"
|
||||
>
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
<div
|
||||
ref="legacyCommandsContainerRef"
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<LoginButton v-if="!isLoggedIn" />
|
||||
<CurrentUserButton v-else class="shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
|
||||
// Maintain support for legacy topbar elements attached by custom scripts
|
||||
const legacyCommandsContainerRef = ref<HTMLElement>()
|
||||
onMounted(() => {
|
||||
if (legacyCommandsContainerRef.value) {
|
||||
app.menu.element.style.width = 'fit-content'
|
||||
legacyCommandsContainerRef.value.appendChild(app.menu.element)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.actionbar-container {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +1,69 @@
|
||||
<template>
|
||||
<Panel
|
||||
class="actionbar w-fit"
|
||||
:style="style"
|
||||
:class="{ 'is-dragging': isDragging, 'is-docked': isDocked }"
|
||||
>
|
||||
<div ref="panelRef" class="actionbar-content flex items-center select-none">
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<ComfyQueueButton />
|
||||
<div class="flex h-full items-center">
|
||||
<div
|
||||
v-if="isDragging && !isDocked"
|
||||
class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
|
||||
:class="{
|
||||
'drop-zone-active': isMouseOverDropZone
|
||||
}"
|
||||
@mouseenter="onMouseEnterDropZone"
|
||||
@mouseleave="onMouseLeaveDropZone"
|
||||
>
|
||||
{{ t('actionbar.dockToTop') }}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<Panel
|
||||
class="actionbar"
|
||||
:style="style"
|
||||
:class="{
|
||||
fixed: !isDocked,
|
||||
'is-dragging': isDragging,
|
||||
'is-docked static mr-2 border-none bg-transparent p-0': isDocked
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="panelRef"
|
||||
class="actionbar-content flex items-center select-none"
|
||||
>
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
/>
|
||||
|
||||
<ComfyRunButton />
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
useDraggable,
|
||||
useElementBounding,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useLocalStorage,
|
||||
watchDebounced
|
||||
} from '@vueuse/core'
|
||||
import { clamp } from 'es-toolkit/compat'
|
||||
import Panel from 'primevue/panel'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import ComfyQueueButton from './ComfyQueueButton.vue'
|
||||
import ComfyRunButton from './ComfyRunButton'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
|
||||
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
||||
|
||||
const visible = computed(() => position.value !== 'Disabled')
|
||||
|
||||
const topMenuRef = inject<Ref<HTMLDivElement | null>>('topMenuRef')
|
||||
const tabContainer = document.querySelector('.workflow-tabs-container')
|
||||
const panelRef = ref<HTMLElement | null>(null)
|
||||
const dragHandleRef = ref<HTMLElement | null>(null)
|
||||
const isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)
|
||||
@@ -63,11 +82,9 @@ const {
|
||||
containerElement: document.body,
|
||||
onMove: (event) => {
|
||||
// Prevent dragging the menu over the top of the tabs
|
||||
if (position.value === 'Top') {
|
||||
const minY = topMenuRef?.value?.getBoundingClientRect().top ?? 40
|
||||
if (event.y < minY) {
|
||||
event.y = minY
|
||||
}
|
||||
const minY = tabContainer?.getBoundingClientRect().bottom ?? 40
|
||||
if (event.y < minY) {
|
||||
event.y = minY
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -202,39 +219,38 @@ const adjustMenuPosition = () => {
|
||||
|
||||
useEventListener(window, 'resize', adjustMenuPosition)
|
||||
|
||||
const topMenuBounds = useElementBounding(topMenuRef)
|
||||
const overlapThreshold = 20 // pixels
|
||||
const isOverlappingWithTopMenu = computed(() => {
|
||||
if (!panelRef.value) {
|
||||
return false
|
||||
// Drop zone state
|
||||
const isMouseOverDropZone = ref(false)
|
||||
|
||||
// Mouse event handlers for self-contained drop zone
|
||||
const onMouseEnterDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = true
|
||||
}
|
||||
const { height } = panelRef.value.getBoundingClientRect()
|
||||
const actionbarBottom = y.value + height
|
||||
const topMenuBottom = topMenuBounds.bottom.value
|
||||
}
|
||||
|
||||
const overlapPixels =
|
||||
Math.min(actionbarBottom, topMenuBottom) -
|
||||
Math.max(y.value, topMenuBounds.top.value)
|
||||
return overlapPixels > overlapThreshold
|
||||
})
|
||||
const onMouseLeaveDropZone = () => {
|
||||
if (isDragging.value) {
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDragging, (newIsDragging) => {
|
||||
if (!newIsDragging) {
|
||||
// Stop dragging
|
||||
isDocked.value = isOverlappingWithTopMenu.value
|
||||
// Handle drag state changes
|
||||
watch(isDragging, (dragging) => {
|
||||
if (dragging) {
|
||||
// Starting to drag - undock if docked
|
||||
if (isDocked.value) {
|
||||
isDocked.value = false
|
||||
}
|
||||
} else {
|
||||
// Start dragging
|
||||
isDocked.value = false
|
||||
// Stopped dragging - dock if mouse is over drop zone
|
||||
if (isMouseOverDropZone.value) {
|
||||
isDocked.value = true
|
||||
}
|
||||
// Reset drop zone state
|
||||
isMouseOverDropZone.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const eventBus = useEventBus<string>('topMenu')
|
||||
watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
|
||||
eventBus.emit('updateHighlight', {
|
||||
isDragging: dragging,
|
||||
isOverlapping: overlapping
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -242,17 +258,27 @@ watch([isDragging, isOverlappingWithTopMenu], ([dragging, overlapping]) => {
|
||||
|
||||
.actionbar {
|
||||
pointer-events: all;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.actionbar.is-docked {
|
||||
position: static;
|
||||
@apply bg-transparent border-none p-0;
|
||||
.actionbar-drop-zone {
|
||||
width: 265px;
|
||||
border: 2px dashed var(--p-primary-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.actionbar-drop-zone.drop-zone-active {
|
||||
background: var(--p-highlight-background-focus);
|
||||
border-color: var(--p-primary-color);
|
||||
border-width: 3px;
|
||||
box-shadow: 0 0 20px var(--p-primary-color);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.actionbar.is-dragging {
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.p-panel-content) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<component
|
||||
:is="currentButton"
|
||||
:key="isActiveSubscription ? 'queue' : 'subscribe'"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueButton.vue'
|
||||
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
|
||||
const currentButton = computed(() =>
|
||||
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
|
||||
)
|
||||
</script>
|
||||
@@ -8,7 +8,7 @@
|
||||
showDelay: 600
|
||||
}"
|
||||
class="comfyui-queue-button"
|
||||
:label="activeQueueModeMenuItem.label"
|
||||
:label="String(activeQueueModeMenuItem?.label ?? '')"
|
||||
severity="primary"
|
||||
size="small"
|
||||
:model="queueModeMenuItems"
|
||||
@@ -33,7 +33,7 @@
|
||||
value: item.tooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
:label="String(item.label)"
|
||||
:label="String(item.label ?? '')"
|
||||
:icon="item.icon"
|
||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
@@ -82,10 +82,13 @@
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import ButtonGroup from 'primevue/buttongroup'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import {
|
||||
useQueuePendingTaskCountStore,
|
||||
@@ -93,43 +96,52 @@ import {
|
||||
} from '@/stores/queueStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
import BatchCountEdit from './BatchCountEdit.vue'
|
||||
import BatchCountEdit from '../BatchCountEdit.vue'
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
|
||||
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
|
||||
|
||||
const { t } = useI18n()
|
||||
const queueModeMenuItemLookup = computed(() => ({
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
instant: {
|
||||
key: 'instant',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'instant'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'change'
|
||||
const queueModeMenuItemLookup = computed(() => {
|
||||
const items: Record<string, MenuItem> = {
|
||||
disabled: {
|
||||
key: 'disabled',
|
||||
label: t('menu.run'),
|
||||
tooltip: t('menu.disabledTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'disabled'
|
||||
}
|
||||
},
|
||||
change: {
|
||||
key: 'change',
|
||||
label: `${t('menu.run')} (${t('menu.onChange')})`,
|
||||
tooltip: t('menu.onChangeTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'change'
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
if (!isCloud) {
|
||||
items.instant = {
|
||||
key: 'instant',
|
||||
label: `${t('menu.run')} (${t('menu.instant')})`,
|
||||
tooltip: t('menu.instantTooltip'),
|
||||
command: () => {
|
||||
queueMode.value = 'instant'
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const activeQueueModeMenuItem = computed(
|
||||
() => queueModeMenuItemLookup.value[queueMode.value]
|
||||
)
|
||||
const activeQueueModeMenuItem = computed(() => {
|
||||
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
|
||||
return (
|
||||
queueModeMenuItemLookup.value[queueMode.value] ||
|
||||
queueModeMenuItemLookup.value.disabled
|
||||
)
|
||||
})
|
||||
const queueModeMenuItems = computed(() =>
|
||||
Object.values(queueModeMenuItemLookup.value)
|
||||
)
|
||||
@@ -141,10 +153,15 @@ const hasPendingTasks = computed(
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const queuePrompt = async (e: Event) => {
|
||||
const commandId =
|
||||
'shiftKey' in e && e.shiftKey
|
||||
? 'Comfy.QueuePromptFront'
|
||||
: 'Comfy.QueuePrompt'
|
||||
const isShiftPressed = 'shiftKey' in e && e.shiftKey
|
||||
const commandId = isShiftPressed
|
||||
? 'Comfy.QueuePromptFront'
|
||||
: 'Comfy.QueuePrompt'
|
||||
|
||||
if (isCloud) {
|
||||
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
|
||||
}
|
||||
|
||||
await commandStore.execute(commandId)
|
||||
}
|
||||
</script>
|
||||
7
src/components/actionbar/ComfyRunButton/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
export default isCloud && __BUILD_FLAGS__.REQUIRE_SUBSCRIPTION
|
||||
? defineAsyncComponent(() => import('./CloudRunButtonWrapper.vue'))
|
||||
: defineAsyncComponent(() => import('./ComfyQueueButton.vue'))
|
||||
@@ -3,17 +3,42 @@
|
||||
<Tabs
|
||||
:key="$i18n.locale"
|
||||
v-model:value="bottomPanelStore.activeBottomPanelTabId"
|
||||
style="--p-tabs-tablist-background: var(--comfy-menu-bg)"
|
||||
>
|
||||
<TabList pt:tab-list="border-none">
|
||||
<TabList
|
||||
pt:tab-list="border-none h-full flex items-center py-2 border-b-1 border-solid"
|
||||
class="bg-transparent"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class="tabs-container">
|
||||
<Tab
|
||||
v-for="tab in bottomPanelStore.bottomPanelTabs"
|
||||
:key="tab.id"
|
||||
:value="tab.id"
|
||||
class="border-none p-3"
|
||||
class="m-1 mx-2 border-none"
|
||||
:class="{
|
||||
'tab-list-single-item':
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
}"
|
||||
:pt:root="
|
||||
(x: TabPassThroughMethodOptions) => ({
|
||||
class: {
|
||||
'p-3 rounded-lg': true,
|
||||
'pointer-events-none':
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
},
|
||||
style: {
|
||||
color: 'var(--fg-color)',
|
||||
backgroundColor:
|
||||
!x.context.active ||
|
||||
bottomPanelStore.bottomPanelTabs.length === 1
|
||||
? ''
|
||||
: 'var(--bg-color)'
|
||||
}
|
||||
})
|
||||
"
|
||||
>
|
||||
<span class="font-bold">
|
||||
<span class="font-normal">
|
||||
{{ getTabDisplayTitle(tab) }}
|
||||
</span>
|
||||
</Tab>
|
||||
@@ -56,6 +81,7 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import Tab from 'primevue/tab'
|
||||
import type { TabPassThroughMethodOptions } from 'primevue/tab'
|
||||
import TabList from 'primevue/tablist'
|
||||
import Tabs from 'primevue/tabs'
|
||||
import { computed } from 'vue'
|
||||
@@ -95,3 +121,9 @@ const closeBottomPanel = () => {
|
||||
bottomPanelStore.activePanel = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-tablist-active-bar) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -64,7 +64,6 @@ const terminalCreated = (
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-full w-full bg-black">
|
||||
<div class="h-full w-full bg-transparent">
|
||||
<p v-if="errorMessage" class="p-4 text-center">
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
@@ -94,7 +94,6 @@ const terminalCreated = (
|
||||
}
|
||||
|
||||
:deep(.p-terminal) .xterm-screen {
|
||||
background-color: black;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div
|
||||
class="subgraph-breadcrumb w-auto"
|
||||
class="subgraph-breadcrumb w-auto drop-shadow-md"
|
||||
:class="{
|
||||
'subgraph-breadcrumb-collapse': collapseTabs,
|
||||
'subgraph-breadcrumb-overflow': overflowingTabs
|
||||
}"
|
||||
:style="{
|
||||
'--p-breadcrumb-gap': `${ITEM_GAP}px`,
|
||||
'--p-breadcrumb-gap': `0px`,
|
||||
'--p-breadcrumb-item-margin': `${ITEM_GAP / 2}px`,
|
||||
'--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,
|
||||
'--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,
|
||||
'--p-breadcrumb-icon-width': `${ICON_WIDTH}px`
|
||||
@@ -14,8 +15,9 @@
|
||||
>
|
||||
<Breadcrumb
|
||||
ref="breadcrumbRef"
|
||||
class="bg-transparent p-0"
|
||||
class="w-fit rounded-lg p-0"
|
||||
:model="items"
|
||||
:pt="{ item: { class: 'pointer-events-auto' } }"
|
||||
aria-label="Graph navigation"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
@@ -174,30 +176,65 @@ onUpdated(() => {
|
||||
@apply overflow-hidden;
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb) {
|
||||
width: 100%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item) {
|
||||
@apply flex items-center rounded-lg overflow-hidden;
|
||||
@apply flex items-center overflow-hidden;
|
||||
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem);
|
||||
/* Collapse middle items first */
|
||||
flex-shrink: 10000;
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-separator) {
|
||||
display: flex;
|
||||
padding: 0 var(--p-breadcrumb-item-margin);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item-link) {
|
||||
padding: 0
|
||||
calc(var(--p-breadcrumb-item-margin) + var(--p-breadcrumb-item-padding));
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-separator),
|
||||
:deep(.p-breadcrumb-item) {
|
||||
@apply h-12;
|
||||
border-top: 1px solid var(--p-panel-border-color);
|
||||
border-bottom: 1px solid var(--p-panel-border-color);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-icon-visible)) {
|
||||
min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem + 20px);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:first-child) {
|
||||
@apply rounded-l-lg;
|
||||
/* Then collapse the root workflow */
|
||||
flex-shrink: 5000;
|
||||
border-left: 1px solid var(--p-panel-border-color);
|
||||
|
||||
.p-breadcrumb-item-link {
|
||||
padding-left: var(--p-breadcrumb-item-padding);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:last-child) {
|
||||
@apply rounded-r-lg;
|
||||
/* Then collapse the active item */
|
||||
flex-shrink: 1;
|
||||
border-right: 1px solid var(--p-panel-border-color);
|
||||
}
|
||||
|
||||
:deep(.p-breadcrumb-item:hover),
|
||||
:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-menu-visible)) {
|
||||
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
|
||||
:deep(.p-breadcrumb-item-link:hover),
|
||||
:deep(.p-breadcrumb-item-link-menu-visible) {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--fg-color) 10%,
|
||||
var(--comfy-menu-bg)
|
||||
) !important;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
</style>
|
||||
@@ -214,7 +251,7 @@ onUpdated(() => {
|
||||
.p-breadcrumb-item:nth-last-child(3),
|
||||
.p-breadcrumb-separator:nth-last-child(2),
|
||||
.p-breadcrumb-item:nth-last-child(1) {
|
||||
@apply block;
|
||||
@apply flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
showDelay: 512
|
||||
}"
|
||||
href="#"
|
||||
class="p-breadcrumb-item-link cursor-pointer"
|
||||
class="p-breadcrumb-item-link h-12 cursor-pointer px-2"
|
||||
:class="{
|
||||
'flex items-center gap-1': isActive,
|
||||
'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,
|
||||
@@ -15,7 +15,7 @@
|
||||
}"
|
||||
@click="handleClick"
|
||||
>
|
||||
<span class="p-breadcrumb-item-label">{{ item.label }}</span>
|
||||
<span class="p-breadcrumb-item-label px-2">{{ item.label }}</span>
|
||||
<Tag v-if="item.isBlueprint" :value="'Blueprint'" severity="primary" />
|
||||
<i v-if="isActive" class="pi pi-angle-down text-[10px]"></i>
|
||||
</a>
|
||||
@@ -26,7 +26,7 @@
|
||||
:popup="true"
|
||||
:pt="{
|
||||
root: {
|
||||
style: 'background-color: var(--comfy-menu-secondary-bg)'
|
||||
style: 'background-color: var(--comfy-menu-bg)'
|
||||
},
|
||||
itemLink: {
|
||||
class: 'py-2'
|
||||
@@ -38,7 +38,7 @@
|
||||
ref="itemInputRef"
|
||||
v-model="itemLabel"
|
||||
class="fixed z-10000 px-2 py-2 text-[.8rem]"
|
||||
@blur="inputBlur(true)"
|
||||
@blur="inputBlur(false)"
|
||||
@click.stop
|
||||
@keydown.enter="inputBlur(true)"
|
||||
@keydown.esc="inputBlur(false)"
|
||||
@@ -240,7 +240,6 @@ const inputBlur = async (doRename: boolean) => {
|
||||
|
||||
.p-breadcrumb-item-link {
|
||||
@apply overflow-hidden;
|
||||
padding: var(--p-breadcrumb-item-padding);
|
||||
}
|
||||
|
||||
.p-breadcrumb-item-label {
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
/>
|
||||
<Button
|
||||
v-if="!isApiKeyLogin"
|
||||
class="w-32"
|
||||
class="w-fit"
|
||||
variant="text"
|
||||
severity="danger"
|
||||
:label="$t('auth.deleteAccount.deleteAccount')"
|
||||
icon="pi pi-trash"
|
||||
@click="handleDeleteAccount"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,28 +2,46 @@
|
||||
<!-- Load splitter overlay only after comfyApp is ready. -->
|
||||
<!-- If load immediately, the top-level splitter stateKey won't be correctly
|
||||
synced with the stateStorage (localStorage). -->
|
||||
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady && betaMenuEnabled">
|
||||
<template v-if="!workspaceStore.focusMode" #side-bar-panel>
|
||||
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
|
||||
<template v-if="showUI && workflowTabsPosition === 'Topbar'" #workflow-tabs>
|
||||
<div
|
||||
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
|
||||
>
|
||||
<!-- Native drag area for Electron -->
|
||||
<div
|
||||
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
|
||||
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
|
||||
/>
|
||||
<div class="flex">
|
||||
<WorkflowTabs />
|
||||
<TopbarBadges />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showUI" #side-toolbar>
|
||||
<SideToolbar />
|
||||
</template>
|
||||
<template v-if="!workspaceStore.focusMode" #bottom-panel>
|
||||
<template v-if="showUI" #side-bar-panel>
|
||||
<div
|
||||
class="sidebar-content-container h-full w-full overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="showUI" #topmenu>
|
||||
<TopMenuSection />
|
||||
</template>
|
||||
<template v-if="showUI" #bottom-panel>
|
||||
<BottomPanel />
|
||||
</template>
|
||||
<template #graph-canvas-panel>
|
||||
<div class="pointer-events-auto absolute top-0 left-0 w-auto max-w-full">
|
||||
<SecondRowWorkflowTabs
|
||||
v-if="workflowTabsPosition === 'Topbar (2nd-row)'"
|
||||
/>
|
||||
</div>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
||||
|
||||
<MiniMap
|
||||
v-if="comfyAppReady && minimapEnabled"
|
||||
v-if="comfyAppReady && minimapEnabled && showUI"
|
||||
class="pointer-events-auto"
|
||||
/>
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
|
||||
<canvas
|
||||
id="graph-canvas"
|
||||
ref="canvasRef"
|
||||
@@ -81,7 +99,9 @@ import {
|
||||
} from 'vue'
|
||||
|
||||
import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'
|
||||
import TopMenuSection from '@/components/TopMenuSection.vue'
|
||||
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
@@ -90,7 +110,8 @@ import TitleEditor from '@/components/graph/TitleEditor.vue'
|
||||
import NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
|
||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
|
||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
|
||||
@@ -129,6 +150,7 @@ import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isNativeWindow } from '@/utils/envUtil'
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: []
|
||||
@@ -160,6 +182,12 @@ const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
|
||||
const selectionToolboxEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Canvas.SelectionToolbox')
|
||||
)
|
||||
const activeSidebarTab = computed(() => {
|
||||
return workspaceStore.sidebarTab.activeSidebarTab
|
||||
})
|
||||
const showUI = computed(
|
||||
() => !workspaceStore.focusMode && betaMenuEnabled.value
|
||||
)
|
||||
|
||||
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
></div>
|
||||
|
||||
<ButtonGroup
|
||||
class="absolute right-2 bottom-2 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
|
||||
:style="stringifiedMinimapStyles.buttonGroupStyles"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
|
||||
@@ -75,7 +75,7 @@ let promptInput = findPromptInput()
|
||||
const previousPromptInput = ref<string | null>(null)
|
||||
|
||||
const getPreviousResponseId = (index: number) =>
|
||||
index > 0 ? parsedHistory.value[index - 1]?.response_id ?? '' : ''
|
||||
index > 0 ? (parsedHistory.value[index - 1]?.response_id ?? '') : ''
|
||||
|
||||
const storePromptInput = () => {
|
||||
promptInput ??= widget?.node.widgets?.find((w) => w.name === 'prompt')
|
||||
|
||||
32
src/components/icons/ComfyLogoTransparent.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="iconClass"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.8193 0.600586C15.1248 0.600586 15.3296 0.70893 15.459 0.881836C15.5914 1.05888 15.6471 1.33774 15.5527 1.66895L14.8037 4.30176C14.7063 4.64386 14.4729 4.97024 14.1641 5.21191C13.8544 5.45415 13.496 5.58984 13.1699 5.58984H13.1689L9.5791 5.59668H7.90625C7.52654 5.59668 7.19496 5.84986 7.09082 6.21289L5.69434 11.0889C5.63007 11.3133 5.66134 11.5534 5.77734 11.7529L5.83203 11.8359C5.99177 12.0491 6.24252 12.1758 6.50977 12.1758H6.51074L8.88281 12.1709H11.4971C11.7643 12.171 11.9541 12.254 12.084 12.3906L12.1357 12.4521C12.2685 12.6295 12.3249 12.9089 12.2305 13.2402L11.4805 15.8721C11.383 16.2144 11.1498 16.5415 10.8408 16.7832C10.5314 17.0252 10.1736 17.161 9.84766 17.1611H9.84668L6.25684 17.168H3.64258C3.33762 17.1679 3.13349 17.0588 3.00391 16.8857C2.87135 16.7087 2.81482 16.43 2.90918 16.0986L3.39551 14.3887C3.46841 14.1327 3.41794 13.8576 3.25879 13.6445V13.6436C3.09901 13.4303 2.84745 13.3037 2.58008 13.3037H1.18066C0.875088 13.3037 0.670398 13.1953 0.541016 13.0225C0.408483 12.8451 0.351891 12.5655 0.446289 12.2344L2.11914 6.38965L2.30371 5.74707V5.74609C2.40139 5.40341 2.63456 5.07671 2.94336 4.83496C3.25302 4.59258 3.61143 4.45705 3.9375 4.45703H5.6123C5.94484 4.45703 6.24083 4.26316 6.37891 3.9707L6.42773 3.83984L6.98145 1.89551C7.07894 1.55317 7.31212 1.22614 7.62109 0.984375C7.93074 0.742127 8.2892 0.606445 8.61523 0.606445H8.61621L12.1982 0.600586H14.8193Z"
|
||||
:stroke="color"
|
||||
stroke-width="1"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Props {
|
||||
size?: number | string
|
||||
color?: string
|
||||
class?: string
|
||||
}
|
||||
const {
|
||||
size = 16,
|
||||
color = 'currentColor',
|
||||
class: className
|
||||
} = defineProps<Props>()
|
||||
const iconClass = computed(() => className || '')
|
||||
</script>
|
||||
@@ -106,7 +106,7 @@ const getLabel = (val: string | null | undefined) => {
|
||||
if (val == null) return label ?? ''
|
||||
if (!options) return label ?? ''
|
||||
const found = options.find((o) => o.value === val)
|
||||
return found ? found.name : label ?? ''
|
||||
return found ? found.name : (label ?? '')
|
||||
}
|
||||
|
||||
// Extract complex style logic from template
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
<template>
|
||||
<div
|
||||
class="comfyui-logo-wrapper mr-2 flex cursor-pointer items-center justify-center rounded-md p-1"
|
||||
class="comfy-menu-button-wrapper flex shrink-0 cursor-pointer flex-col items-center justify-center rounded-t-md p-2 transition-colors"
|
||||
:class="{
|
||||
'comfyui-logo-menu-visible': menuRef?.visible
|
||||
}"
|
||||
:style="{
|
||||
minWidth: isLargeSidebar ? '4rem' : 'auto'
|
||||
'comfy-menu-button-active': menuRef?.visible
|
||||
}"
|
||||
@click="menuRef?.toggle($event)"
|
||||
>
|
||||
<img
|
||||
src="/assets/images/comfy-logo-mono.svg"
|
||||
<ComfyLogoTransparent
|
||||
alt="ComfyUI Logo"
|
||||
class="comfyui-logo h-7"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
class="comfyui-logo h-[18px] w-[18px]"
|
||||
/>
|
||||
<i class="pi pi-angle-down ml-1 text-[10px]" />
|
||||
|
||||
<span
|
||||
v-if="!isSmall"
|
||||
class="side-bar-button-label mt-1 text-center text-[10px]"
|
||||
>{{ t('sideToolbar.labels.menu') }}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<TieredMenu
|
||||
ref="menuRef"
|
||||
:model="translatedItems"
|
||||
:popup="true"
|
||||
class="comfy-command-menu"
|
||||
:class="{
|
||||
'comfy-command-menu-top': isTopMenu
|
||||
}"
|
||||
@show="onMenuShow"
|
||||
>
|
||||
<template #item="{ item, props }">
|
||||
@@ -48,7 +46,7 @@
|
||||
v-else-if="
|
||||
item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'
|
||||
"
|
||||
class="p-menubar-item-icon"
|
||||
class="p-menubar-item-icon text-sm"
|
||||
:class="item.icon"
|
||||
/>
|
||||
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
|
||||
@@ -67,8 +65,6 @@
|
||||
</a>
|
||||
</template>
|
||||
</TieredMenu>
|
||||
|
||||
<SubgraphBreadcrumb />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -78,38 +74,34 @@ import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import ComfyLogoTransparent from '@/components/icons/ComfyLogoTransparent.vue'
|
||||
import SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import { useMenuItemStore } from '@/stores/menuItemStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { showNativeSystemMenu } from '@/utils/envUtil'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
import { whileMouseDown } from '@/utils/mouseDownUtil'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const menuItemStore = useMenuItemStore()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const menuItemsStore = useMenuItemStore()
|
||||
const commandStore = useCommandStore()
|
||||
const dialogStore = useDialogStore()
|
||||
const settingStore = useSettingStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const managerState = useManagerState()
|
||||
|
||||
const { isSmall = false } = defineProps<{
|
||||
isSmall?: boolean
|
||||
}>()
|
||||
|
||||
const menuRef = ref<
|
||||
({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null
|
||||
>(null)
|
||||
const isLargeSidebar = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') !== 'small'
|
||||
)
|
||||
const isTopMenu = computed(() => settingStore.get('Comfy.UseNewMenu') === 'Top')
|
||||
|
||||
const translateMenuItem = (item: MenuItem): MenuItem => {
|
||||
const label = typeof item.label === 'function' ? item.label() : item.label
|
||||
@@ -185,7 +177,7 @@ const extraMenuItems = computed(() => [
|
||||
])
|
||||
|
||||
const translatedItems = computed(() => {
|
||||
const items = menuItemsStore.menuItems.map(translateMenuItem)
|
||||
const items = menuItemStore.menuItems.map(translateMenuItem)
|
||||
let helpIndex = items.findIndex((item) => item.key === 'Help')
|
||||
let helpItem: MenuItem | undefined
|
||||
|
||||
@@ -272,16 +264,24 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
return (
|
||||
item.parentPath &&
|
||||
(item.parentPath === 'theme' ||
|
||||
menuItemsStore.menuItemHasActiveStateChildren[item.parentPath])
|
||||
menuItemStore.menuItemHasActiveStateChildren[item.parentPath])
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference '../../assets/css/style.css';
|
||||
.comfy-menu-button-wrapper {
|
||||
width: var(--sidebar-width);
|
||||
height: var(--sidebar-item-height);
|
||||
}
|
||||
|
||||
:deep(.p-menubar-submenu.dropdown-direction-up) {
|
||||
@apply top-auto bottom-full flex-col-reverse;
|
||||
.comfy-menu-button-wrapper:hover {
|
||||
background: var(--p-button-text-secondary-hover-background);
|
||||
}
|
||||
|
||||
.comfy-menu-button-active,
|
||||
.comfy-menu-button-active:hover {
|
||||
background-color: var(--content-hover-bg);
|
||||
}
|
||||
|
||||
.keybinding-tag {
|
||||
@@ -289,11 +289,6 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
border-color: var(--p-content-border-color);
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.comfyui-logo-menu-visible,
|
||||
.comfyui-logo-wrapper:hover {
|
||||
background-color: color-mix(in srgb, var(--fg-color) 10%, transparent);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
@@ -309,22 +304,8 @@ const hasActiveStateSiblings = (item: MenuItem): boolean => {
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.comfy-command-menu ul {
|
||||
background-color: var(--comfy-menu-secondary-bg) !important;
|
||||
}
|
||||
.comfy-command-menu-top .p-tieredmenu-submenu {
|
||||
left: calc(100% + 15px) !important;
|
||||
top: -4px !important;
|
||||
}
|
||||
@media (max-height: 700px) {
|
||||
.comfy-command-menu .p-tieredmenu-submenu {
|
||||
@apply absolute max-h-[90vh] overflow-y-auto;
|
||||
}
|
||||
/* Help (last) submenu upward offset in compact mode */
|
||||
.p-tieredmenu-root-list
|
||||
> .p-tieredmenu-item:last-of-type
|
||||
.p-tieredmenu-submenu {
|
||||
top: -188px !important;
|
||||
}
|
||||
background-color: var(--comfy-menu-bg) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,48 +1,69 @@
|
||||
<template>
|
||||
<teleport :to="teleportTarget">
|
||||
<nav class="side-tool-bar-container" :class="{ 'small-sidebar': isSmall }">
|
||||
<SidebarIcon
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:icon-badge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip-suffix="getTabTooltipSuffix(tab)"
|
||||
:label="tab.label || tab.title"
|
||||
:is-small="isSmall"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
<SidebarTemplatesButton />
|
||||
<div class="side-tool-bar-end">
|
||||
<SidebarLogoutIcon v-if="userStore.isMultiUserServer" />
|
||||
<SidebarHelpCenterIcon />
|
||||
<SidebarBottomPanelToggleButton />
|
||||
<SidebarShortcutsToggleButton />
|
||||
</div>
|
||||
</nav>
|
||||
</teleport>
|
||||
<div
|
||||
v-if="selectedTab"
|
||||
class="sidebar-content-container h-full overflow-x-hidden overflow-y-auto"
|
||||
<nav
|
||||
ref="sideToolbarRef"
|
||||
class="side-tool-bar-container flex h-full flex-col items-center bg-transparent [.floating-sidebar]:-mr-2"
|
||||
:class="{
|
||||
'small-sidebar': isSmall,
|
||||
'connected-sidebar': isConnected,
|
||||
'floating-sidebar': !isConnected,
|
||||
'overflowing-sidebar': isOverflowing
|
||||
}"
|
||||
>
|
||||
<ExtensionSlot :extension="selectedTab" />
|
||||
</div>
|
||||
<div
|
||||
ref="contentMeasureRef"
|
||||
:class="
|
||||
isOverflowing
|
||||
? 'side-tool-bar-container overflow-y-auto'
|
||||
: 'flex flex-col h-full'
|
||||
"
|
||||
>
|
||||
<div ref="topToolbarRef" :class="groupClasses">
|
||||
<ComfyMenuButton :is-small="isSmall" />
|
||||
<SidebarIcon
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:icon="tab.icon"
|
||||
:icon-badge="tab.iconBadge"
|
||||
:tooltip="tab.tooltip"
|
||||
:tooltip-suffix="getTabTooltipSuffix(tab)"
|
||||
:label="tab.label || tab.title"
|
||||
:is-small="isSmall"
|
||||
:selected="tab.id === selectedTab?.id"
|
||||
:class="tab.id + '-tab-button'"
|
||||
@click="onTabClick(tab)"
|
||||
/>
|
||||
<SidebarTemplatesButton />
|
||||
</div>
|
||||
|
||||
<div ref="bottomToolbarRef" class="mt-auto" :class="groupClasses">
|
||||
<SidebarLogoutIcon
|
||||
v-if="userStore.isMultiUserServer"
|
||||
:is-small="isSmall"
|
||||
/>
|
||||
<SidebarHelpCenterIcon :is-small="isSmall" />
|
||||
<SidebarBottomPanelToggleButton :is-small="isSmall" />
|
||||
<SidebarShortcutsToggleButton :is-small="isSmall" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useResizeObserver } from '@vueuse/core'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
|
||||
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
|
||||
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useKeybindingStore } from '@/stores/keybindingStore'
|
||||
import { useUserStore } from '@/stores/userStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'
|
||||
import SidebarIcon from './SidebarIcon.vue'
|
||||
@@ -53,16 +74,25 @@ const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
const userStore = useUserStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
const teleportTarget = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location') === 'left'
|
||||
? '.comfyui-body-left'
|
||||
: '.comfyui-body-right'
|
||||
)
|
||||
const canvasStore = useCanvasStore()
|
||||
const sideToolbarRef = ref<HTMLElement>()
|
||||
const contentMeasureRef = ref<HTMLElement>()
|
||||
const topToolbarRef = ref<HTMLElement>()
|
||||
const bottomToolbarRef = ref<HTMLElement>()
|
||||
|
||||
const isSmall = computed(
|
||||
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
|
||||
)
|
||||
const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
const sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))
|
||||
const isConnected = computed(
|
||||
() =>
|
||||
selectedTab.value ||
|
||||
isOverflowing.value ||
|
||||
sidebarStyle.value === 'connected'
|
||||
)
|
||||
|
||||
const tabs = computed(() => workspaceStore.getSidebarTabs())
|
||||
const selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
@@ -79,6 +109,68 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
)
|
||||
return keybinding ? ` (${keybinding.combo.toString()})` : ''
|
||||
}
|
||||
|
||||
const isOverflowing = ref(false)
|
||||
const groupClasses = computed(() =>
|
||||
cn(
|
||||
'sidebar-item-group pointer-events-auto flex flex-col items-center overflow-hidden flex-shrink-0' +
|
||||
(isConnected.value ? '' : ' rounded-lg shadow-md')
|
||||
)
|
||||
)
|
||||
|
||||
const ENTER_OVERFLOW_MARGIN = 20
|
||||
const EXIT_OVERFLOW_MARGIN = 50
|
||||
|
||||
const checkOverflow = debounce(() => {
|
||||
if (!sideToolbarRef.value || !topToolbarRef.value || !bottomToolbarRef.value)
|
||||
return
|
||||
|
||||
const containerHeight = sideToolbarRef.value.clientHeight
|
||||
const topHeight = topToolbarRef.value.scrollHeight
|
||||
const bottomHeight = bottomToolbarRef.value.scrollHeight
|
||||
const contentHeight = topHeight + bottomHeight
|
||||
|
||||
if (isOverflowing.value) {
|
||||
isOverflowing.value = containerHeight < contentHeight + EXIT_OVERFLOW_MARGIN
|
||||
} else {
|
||||
isOverflowing.value =
|
||||
containerHeight < contentHeight + ENTER_OVERFLOW_MARGIN
|
||||
}
|
||||
}, 16)
|
||||
|
||||
onMounted(() => {
|
||||
if (!sideToolbarRef.value) return
|
||||
|
||||
const overflowObserver = useResizeObserver(
|
||||
sideToolbarRef.value,
|
||||
checkOverflow
|
||||
)
|
||||
|
||||
checkOverflow()
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
overflowObserver.stop()
|
||||
})
|
||||
|
||||
watch(
|
||||
[isSmall, sidebarLocation],
|
||||
async () => {
|
||||
if (canvasStore.canvas) {
|
||||
if (sidebarLocation.value === 'left') {
|
||||
await nextTick()
|
||||
canvasStore.canvas.fpsInfoLocation = [
|
||||
sideToolbarRef.value?.getBoundingClientRect()?.right,
|
||||
null
|
||||
]
|
||||
} else {
|
||||
canvasStore.canvas.fpsInfoLocation = null
|
||||
}
|
||||
canvasStore.canvas.setDirty(false, true)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -88,36 +180,64 @@ const getTabTooltipSuffix = (tab: SidebarTabExtension) => {
|
||||
* but need to reference sidebar dimensions for proper positioning.
|
||||
*/
|
||||
:root {
|
||||
--sidebar-width: 4rem;
|
||||
--sidebar-padding: 8px;
|
||||
--sidebar-icon-size: 1rem;
|
||||
|
||||
--sidebar-default-floating-width: 56px;
|
||||
--sidebar-default-connected-width: calc(
|
||||
var(--sidebar-default-floating-width) + var(--sidebar-padding) * 2
|
||||
);
|
||||
--sidebar-default-item-height: 56px;
|
||||
|
||||
--sidebar-small-floating-width: 48px;
|
||||
--sidebar-small-connected-width: calc(
|
||||
var(--sidebar-small-floating-width) + var(--sidebar-padding) * 2
|
||||
);
|
||||
--sidebar-small-item-height: 48px;
|
||||
|
||||
--sidebar-width: var(--sidebar-default-floating-width);
|
||||
--sidebar-item-height: var(--sidebar-default-item-height);
|
||||
}
|
||||
|
||||
:root:has(.side-tool-bar-container.small-sidebar) {
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-width: var(--sidebar-small-floating-width);
|
||||
--sidebar-item-height: var(--sidebar-small-item-height);
|
||||
}
|
||||
|
||||
:root:has(.side-tool-bar-container.connected-sidebar) {
|
||||
--sidebar-width: var(--sidebar-default-connected-width);
|
||||
}
|
||||
|
||||
:root:has(.side-tool-bar-container.small-sidebar.connected-sidebar) {
|
||||
--sidebar-width: var(--sidebar-small-connected-width);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.side-tool-bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@reference "tailwindcss";
|
||||
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
color: var(--fg-color);
|
||||
box-shadow: var(--bar-shadow);
|
||||
.floating-sidebar {
|
||||
padding: var(--sidebar-padding);
|
||||
}
|
||||
|
||||
.side-tool-bar-container.small-sidebar {
|
||||
--sidebar-width: 2.5rem;
|
||||
--sidebar-icon-size: 1rem;
|
||||
.floating-sidebar .sidebar-item-group {
|
||||
border-color: var(--p-panel-border-color);
|
||||
}
|
||||
|
||||
.side-tool-bar-end {
|
||||
align-self: flex-end;
|
||||
margin-top: auto;
|
||||
.connected-sidebar {
|
||||
padding: var(--sidebar-padding) 0;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.sidebar-item-group {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.overflowing-sidebar :deep(.comfy-menu-button-wrapper) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
label="sideToolbar.labels.console"
|
||||
:tooltip="$t('menu.toggleBottomPanel')"
|
||||
:selected="bottomPanelStore.activePanel == 'terminal'"
|
||||
@click="bottomPanelStore.toggleBottomPanel"
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
<SidebarIcon
|
||||
icon="pi pi-question-circle"
|
||||
class="comfy-help-center-btn"
|
||||
label="menu.help"
|
||||
:tooltip="$t('sideToolbar.helpCenter')"
|
||||
:icon-badge="shouldShowRedDot ? '•' : ''"
|
||||
:is-small="isSmall"
|
||||
@click="toggleHelpCenter"
|
||||
/>
|
||||
|
||||
@@ -16,7 +18,7 @@
|
||||
:class="{
|
||||
'sidebar-left': sidebarLocation === 'left',
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
'small-sidebar': isSmall
|
||||
}"
|
||||
>
|
||||
<HelpCenterMenuContent @close="closeHelpCenter" />
|
||||
@@ -29,7 +31,7 @@
|
||||
:class="{
|
||||
'sidebar-left': sidebarLocation === 'left',
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
'small-sidebar': isSmall
|
||||
}"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -40,7 +42,7 @@
|
||||
:class="{
|
||||
'sidebar-left': sidebarLocation === 'left',
|
||||
'sidebar-right': sidebarLocation === 'right',
|
||||
'small-sidebar': sidebarSize === 'small'
|
||||
'small-sidebar': isSmall
|
||||
}"
|
||||
@whats-new-dismissed="handleWhatsNewDismissed"
|
||||
/>
|
||||
@@ -59,7 +61,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, onMounted, toRefs } from 'vue'
|
||||
|
||||
import HelpCenterMenuContent from '@/components/helpcenter/HelpCenterMenuContent.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
@@ -87,6 +89,11 @@ const { showNodeConflictDialog } = useDialogService()
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =
|
||||
useConflictAcknowledgment()
|
||||
|
||||
const props = defineProps<{
|
||||
isSmall: boolean
|
||||
}>()
|
||||
const { isSmall } = toRefs(props)
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed((): boolean => {
|
||||
const releaseRedDot = showReleaseRedDot.value
|
||||
@@ -97,8 +104,6 @@ const sidebarLocation = computed(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
|
||||
const sidebarSize = computed(() => settingStore.get('Comfy.Sidebar.Size'))
|
||||
|
||||
const toggleHelpCenter = () => {
|
||||
helpCenterStore.toggle()
|
||||
}
|
||||
|
||||