Compare commits
39 Commits
v1.31.0
...
sno-solve-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad295629cd | ||
|
|
4ee91e1244 | ||
|
|
53754bf99b | ||
|
|
9651d2a5df | ||
|
|
22f307b468 | ||
|
|
06ba106f59 | ||
|
|
5f3b8fb8c8 | ||
|
|
133662cdc7 | ||
|
|
a54c1516ae | ||
|
|
32a803c31e | ||
|
|
32688b8e34 | ||
|
|
4ad7531269 | ||
|
|
e8dabd2996 | ||
|
|
f629d325b2 | ||
|
|
38525d8f3a | ||
|
|
c374975ddc | ||
|
|
e7f640b436 | ||
|
|
6e4471ad62 | ||
|
|
b03cf7e11d | ||
|
|
0a80a288c0 | ||
|
|
efed934418 | ||
|
|
b3da6cf1b4 | ||
|
|
6afdb9529d | ||
|
|
d1c9ce5a66 | ||
|
|
d26309c7ab | ||
|
|
d8657aaee3 | ||
|
|
ddbf2cc720 | ||
|
|
ed49a82c20 | ||
|
|
234fc3433c | ||
|
|
0a957fb2ac | ||
|
|
28a6089a94 | ||
|
|
298b3c629b | ||
|
|
20a1a9eda2 | ||
|
|
ca45b2c4d6 | ||
|
|
1453afad12 | ||
|
|
5de1a91f02 | ||
|
|
b3eee54abb | ||
|
|
1ee33673ab | ||
|
|
9f5245dc80 |
1
.gitattributes
vendored
@@ -7,6 +7,7 @@
|
||||
*.json text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.mts text eol=lf
|
||||
*.snap text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.vue text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: size data
|
||||
name: "CI: Size Data"
|
||||
|
||||
on:
|
||||
push:
|
||||
128
.github/workflows/pr-backport.yaml
vendored
@@ -69,34 +69,7 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Check if backports already exist
|
||||
id: check-existing
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
# Check for existing backport PRs for this PR number
|
||||
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
|
||||
|
||||
if [ -z "$EXISTING_BACKPORTS" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For manual triggers with force_rerun, proceed anyway
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Force rerun requested - existing backports will be updated"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found existing backport PRs:"
|
||||
echo "$EXISTING_BACKPORTS"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||
|
||||
- name: Collect backport targets
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: targets
|
||||
run: |
|
||||
TARGETS=()
|
||||
@@ -138,6 +111,14 @@ jobs:
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^backport:(.+)$ ]]; then
|
||||
add_target "$label" "${BASH_REMATCH[1]}"
|
||||
elif [[ "$label" =~ ^core\/([0-9]+)\.([0-9]+)$ ]]; then
|
||||
SAFE_MAJOR="${BASH_REMATCH[1]}"
|
||||
SAFE_MINOR="${BASH_REMATCH[2]}"
|
||||
add_target "$label" "core/${SAFE_MAJOR}.${SAFE_MINOR}"
|
||||
elif [[ "$label" =~ ^cloud\/([0-9]+)\.([0-9]+)$ ]]; then
|
||||
SAFE_MAJOR="${BASH_REMATCH[1]}"
|
||||
SAFE_MINOR="${BASH_REMATCH[2]}"
|
||||
add_target "$label" "cloud/${SAFE_MAJOR}.${SAFE_MINOR}"
|
||||
elif [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
add_target "$label" "core/${label}"
|
||||
fi
|
||||
@@ -151,8 +132,76 @@ jobs:
|
||||
echo "targets=${TARGETS[*]}" >> $GITHUB_OUTPUT
|
||||
echo "Found backport targets: ${TARGETS[*]}"
|
||||
|
||||
- name: Filter already backported targets
|
||||
id: filter-targets
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
FORCE_RERUN_INPUT: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && inputs.force_rerun
|
||||
|| 'false' }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: >-
|
||||
${{ github.event_name == 'workflow_dispatch' && inputs.pr_number
|
||||
|| github.event.pull_request.number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
REQUESTED_TARGETS="${{ steps.targets.outputs.targets }}"
|
||||
if [ -z "$REQUESTED_TARGETS" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "pending-targets=" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
FORCE_RERUN=false
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$FORCE_RERUN_INPUT" = "true" ]; then
|
||||
FORCE_RERUN=true
|
||||
fi
|
||||
|
||||
mapfile -t EXISTING_BRANCHES < <(
|
||||
git ls-remote --heads origin "backport-${PR_NUMBER}-to-*" || true
|
||||
)
|
||||
|
||||
PENDING=()
|
||||
SKIPPED=()
|
||||
|
||||
for target in $REQUESTED_TARGETS; do
|
||||
SAFE_TARGET=$(echo "$target" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
|
||||
if [ "$FORCE_RERUN" = true ]; then
|
||||
PENDING+=("$target")
|
||||
continue
|
||||
fi
|
||||
|
||||
if printf '%s\n' "${EXISTING_BRANCHES[@]:-}" |
|
||||
grep -Fq "refs/heads/${BACKPORT_BRANCH}"; then
|
||||
SKIPPED+=("$target")
|
||||
else
|
||||
PENDING+=("$target")
|
||||
fi
|
||||
done
|
||||
|
||||
SKIPPED_JOINED="${SKIPPED[*]:-}"
|
||||
PENDING_JOINED="${PENDING[*]:-}"
|
||||
|
||||
echo "already-exists=${SKIPPED_JOINED}" >> $GITHUB_OUTPUT
|
||||
echo "pending-targets=${PENDING_JOINED}" >> $GITHUB_OUTPUT
|
||||
|
||||
if [ -z "$PENDING_JOINED" ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
if [ -n "$SKIPPED_JOINED" ]; then
|
||||
echo "::warning::Backport branches already exist for: ${SKIPPED_JOINED}"
|
||||
fi
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
if [ -n "$SKIPPED_JOINED" ]; then
|
||||
echo "::notice::Skipping already backported targets: ${SKIPPED_JOINED}"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Backport commits
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
if: steps.filter-targets.outputs.skip != 'true'
|
||||
id: backport
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
@@ -170,7 +219,7 @@ jobs:
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for target in ${{ steps.targets.outputs.targets }}; do
|
||||
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
|
||||
TARGET_BRANCH="${target}"
|
||||
SAFE_TARGET=$(echo "$TARGET_BRANCH" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
@@ -185,6 +234,14 @@ jobs:
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if commit already exists on target branch
|
||||
if git branch -r --contains "${MERGE_COMMIT}" | grep -q "origin/${TARGET_BRANCH}"; then
|
||||
echo "::notice::Commit ${MERGE_COMMIT} already exists on ${TARGET_BRANCH}, skipping backport"
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:already-exists "
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create backport branch
|
||||
git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
|
||||
|
||||
@@ -219,7 +276,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Create PR for each successful backport
|
||||
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
|
||||
if: steps.filter-targets.outputs.skip != 'true' && steps.backport.outputs.success
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
@@ -258,7 +315,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Comment on failures
|
||||
if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
|
||||
if: steps.filter-targets.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
@@ -279,6 +336,9 @@ jobs:
|
||||
if [ "${reason}" = "branch-missing" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
|
||||
|
||||
elif [ "${reason}" = "already-exists" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
|
||||
|
||||
elif [ "${reason}" = "conflicts" ]; then
|
||||
# Convert comma-separated conflicts back to newlines for display
|
||||
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
|
||||
@@ -287,3 +347,9 @@ jobs:
|
||||
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Remove needs-backport label
|
||||
if: steps.filter-targets.outputs.skip != 'true' && success()
|
||||
run: gh pr edit ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} --remove-label "needs-backport"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
1
.github/workflows/pr-claude-review.yaml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
check-regexp: '^(lint-and-format|test|playwright-tests)'
|
||||
allowed-conclusions: success,skipped,failure,cancelled,neutral,action_required,timed_out,stale
|
||||
wait-interval: 30
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
name: size report
|
||||
name: "PR: Size Report"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['size data']
|
||||
workflows: ['CI: Size Data']
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
@@ -22,7 +22,7 @@ permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
size-report:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
branch: ${{ steps.pr-base.outputs.content }}
|
||||
workflow: size-data.yml
|
||||
workflow: ci-size-data.yaml
|
||||
event: push
|
||||
name: size-data
|
||||
path: temp/size-prev
|
||||
118
.github/workflows/release-branch-create.yaml
vendored
@@ -69,6 +69,9 @@ jobs:
|
||||
echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "prev_minor=$PREV_MINOR" >> $GITHUB_OUTPUT
|
||||
|
||||
BASE_COMMIT=$(git rev-parse HEAD)
|
||||
echo "base_commit=$BASE_COMMIT" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get previous major version for comparison
|
||||
PREV_MAJOR=$(echo $PREV_VERSION | cut -d. -f1)
|
||||
|
||||
@@ -87,13 +90,13 @@ jobs:
|
||||
elif [[ "$MAJOR" -gt "$PREV_MAJOR" && "$MINOR" == "0" && "$PATCH" == "0" ]]; then
|
||||
# Major version bump (e.g., 1.99.x → 2.0.0)
|
||||
echo "is_minor_bump=true" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME="core/${PREV_MAJOR}.${PREV_MINOR}"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
BRANCH_BASE="${PREV_MAJOR}.${PREV_MINOR}"
|
||||
echo "branch_base=$BRANCH_BASE" >> $GITHUB_OUTPUT
|
||||
elif [[ "$MAJOR" == "$PREV_MAJOR" && "$MINOR" -gt "$PREV_MINOR" && "$PATCH" == "0" ]]; then
|
||||
# Minor version bump (e.g., 1.23.x → 1.24.0)
|
||||
echo "is_minor_bump=true" >> $GITHUB_OUTPUT
|
||||
BRANCH_NAME="core/${MAJOR}.${PREV_MINOR}"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
BRANCH_BASE="${MAJOR}.${PREV_MINOR}"
|
||||
echo "branch_base=$BRANCH_BASE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "is_minor_bump=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
@@ -101,64 +104,97 @@ jobs:
|
||||
# Return to main branch
|
||||
git checkout main
|
||||
|
||||
- name: Create release branch
|
||||
- name: Create release branches
|
||||
id: create_branches
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
||||
BRANCH_BASE="${{ steps.check_version.outputs.branch_base }}"
|
||||
PREV_VERSION="${{ steps.check_version.outputs.prev_version }}"
|
||||
|
||||
# Check if branch already exists
|
||||
if git ls-remote --heads origin "$BRANCH_NAME" | grep -q "$BRANCH_NAME"; then
|
||||
echo "⚠️ Branch $BRANCH_NAME already exists, skipping creation"
|
||||
echo "branch_exists=true" >> $GITHUB_ENV
|
||||
exit 0
|
||||
else
|
||||
echo "branch_exists=false" >> $GITHUB_ENV
|
||||
if [[ -z "$BRANCH_BASE" ]]; then
|
||||
echo "::error::Branch base not set; unable to determine release branches"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create branch from the commit BEFORE the version bump
|
||||
# This ensures the release branch has the previous minor version
|
||||
git checkout -b "$BRANCH_NAME" HEAD^1
|
||||
BASE_COMMIT="${{ steps.check_version.outputs.base_commit }}"
|
||||
|
||||
# Push the new branch
|
||||
git push origin "$BRANCH_NAME"
|
||||
if [[ -z "$BASE_COMMIT" ]]; then
|
||||
echo "::error::Base commit not provided; cannot create release branches"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Created release branch: $BRANCH_NAME"
|
||||
echo "This branch is now in feature freeze and will only receive:"
|
||||
echo "- Bug fixes"
|
||||
echo "- Critical security patches"
|
||||
echo "- Documentation updates"
|
||||
RESULTS_FILE=$(mktemp)
|
||||
trap 'rm -f "$RESULTS_FILE"' EXIT
|
||||
|
||||
for PREFIX in core cloud; do
|
||||
BRANCH_NAME="${PREFIX}/${BRANCH_BASE}"
|
||||
|
||||
if git ls-remote --exit-code --heads origin \
|
||||
"$BRANCH_NAME" >/dev/null 2>&1; then
|
||||
echo "⚠️ Branch $BRANCH_NAME already exists"
|
||||
echo "ℹ️ Skipping creation for $BRANCH_NAME"
|
||||
STATUS="exists"
|
||||
else
|
||||
# Create branch from the commit BEFORE the version bump
|
||||
if ! git push origin "$BASE_COMMIT:refs/heads/$BRANCH_NAME"; then
|
||||
echo "::error::Failed to push release branch $BRANCH_NAME"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ Created release branch: $BRANCH_NAME"
|
||||
STATUS="created"
|
||||
fi
|
||||
|
||||
echo "$BRANCH_NAME|$STATUS|$PREV_VERSION" >> "$RESULTS_FILE"
|
||||
done
|
||||
|
||||
{
|
||||
echo "results<<'EOF'"
|
||||
cat "$RESULTS_FILE"
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post summary
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
||||
PREV_VERSION="${{ steps.check_version.outputs.prev_version }}"
|
||||
CURRENT_VERSION="${{ steps.check_version.outputs.current_version }}"
|
||||
RESULTS="${{ steps.create_branches.outputs.results }}"
|
||||
|
||||
if [[ "${{ env.branch_exists }}" == "true" ]]; then
|
||||
if [[ -z "$RESULTS" ]]; then
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
## 🌿 Release Branch Already Exists
|
||||
## 🌿 Release Branch Summary
|
||||
|
||||
The release branch for the previous minor version already exists:
|
||||
EOF
|
||||
else
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
## 🌿 Release Branch Created
|
||||
|
||||
A new release branch has been created for the previous minor version:
|
||||
Release branch creation skipped; no eligible branches were found.
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
## 🌿 Release Branch Summary
|
||||
|
||||
- **Branch**: \`$BRANCH_NAME\`
|
||||
- **Version**: \`$PREV_VERSION\` (feature frozen)
|
||||
- **Main branch**: \`$CURRENT_VERSION\` (active development)
|
||||
|
||||
### Branch Status
|
||||
EOF
|
||||
|
||||
while IFS='|' read -r BRANCH STATUS PREV_VERSION; do
|
||||
if [[ "$STATUS" == "created" ]]; then
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
|
||||
- \`$BRANCH\` created from version \`$PREV_VERSION\`
|
||||
EOF
|
||||
else
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
|
||||
- \`$BRANCH\` already existed (based on version \`$PREV_VERSION\`)
|
||||
EOF
|
||||
fi
|
||||
done <<< "$RESULTS"
|
||||
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
|
||||
### Branch Policy
|
||||
|
||||
The \`$BRANCH_NAME\` branch is now in **feature freeze** and will only accept:
|
||||
Release branches are feature-frozen and only accept:
|
||||
- 🐛 Bug fixes
|
||||
- 🔒 Security patches
|
||||
- 📚 Documentation updates
|
||||
@@ -167,9 +203,9 @@ jobs:
|
||||
|
||||
### Backporting Changes
|
||||
|
||||
To backport a fix to this release branch:
|
||||
To backport a fix:
|
||||
1. Create your fix on \`main\` first
|
||||
2. Cherry-pick to \`$BRANCH_NAME\`
|
||||
3. Create a PR targeting \`$BRANCH_NAME\`
|
||||
4. Use the \`Release\` label on the PR
|
||||
2. Cherry-pick to the target release branch
|
||||
3. Create a PR targeting that branch
|
||||
4. Apply the matching \`core/x.y\` or \`cloud/x.y\` label
|
||||
EOF
|
||||
|
||||
145
.github/workflows/weekly-docs-check.yaml
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
name: "Weekly Documentation Check"
|
||||
description: "Automated weekly documentation accuracy check and update via Claude"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run every Monday at 9 AM UTC
|
||||
- cron: '0 9 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docs-check:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies for analysis tools
|
||||
run: |
|
||||
# Check if packages are already available locally
|
||||
if ! pnpm list typescript @vue/compiler-sfc >/dev/null 2>&1; then
|
||||
echo "Installing TypeScript and Vue compiler globally..."
|
||||
pnpm install -g typescript @vue/compiler-sfc
|
||||
else
|
||||
echo "TypeScript and Vue compiler already available locally"
|
||||
fi
|
||||
|
||||
- name: Run Claude Documentation Review
|
||||
uses: anthropics/claude-code-action@v1.0.6
|
||||
with:
|
||||
prompt: |
|
||||
Is all documentation still 100% accurate?
|
||||
|
||||
INSTRUCTIONS:
|
||||
1. Fact-check all documentation against the current codebase
|
||||
2. Look for:
|
||||
- Outdated API references
|
||||
- Deprecated functions or components still documented
|
||||
- Missing documentation for new features
|
||||
- Incorrect code examples
|
||||
- Broken internal references
|
||||
- Configuration examples that no longer work
|
||||
- Documentation that contradicts actual implementation
|
||||
3. Update any inaccurate or outdated documentation
|
||||
4. Add documentation for significant undocumented features
|
||||
5. Ensure all code examples are valid and tested against current code
|
||||
|
||||
Focus on these key areas:
|
||||
- docs/**/*.md (all documentation files)
|
||||
- CLAUDE.md (project guidelines)
|
||||
- README.md files throughout the repository
|
||||
- .claude/commands/*.md (Claude command documentation)
|
||||
|
||||
Make changes directly to the documentation files as needed.
|
||||
DO NOT modify any source code files unless absolutely necessary for documentation accuracy.
|
||||
|
||||
After making all changes, create a comprehensive PR message summary:
|
||||
1. Write a detailed PR body to /tmp/pr-body-${{ github.run_id }}.md in markdown format
|
||||
2. Include:
|
||||
- ## Summary section with bullet points of what was changed
|
||||
- ## Changes Made section with details organized by category
|
||||
- ## Review Notes section with any important context
|
||||
3. Be specific about which files were updated and why
|
||||
4. If no changes were needed, write a brief message stating documentation is up to date
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: "--max-turns 256 --allowedTools 'Bash(git status),Bash(git diff),Bash(git log),Bash(pnpm:*),Bash(npm:*),Bash(node:*),Bash(tsc:*),Bash(echo:*),Read,Write,Edit,Glob,Grep'"
|
||||
continue-on-error: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if git diff --quiet && git diff --cached --quiet; then
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "No documentation changes needed"
|
||||
else
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "Documentation changes detected"
|
||||
fi
|
||||
|
||||
- name: Create default PR body if not generated
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
run: |
|
||||
if [ ! -f /tmp/pr-body-${{ github.run_id }}.md ]; then
|
||||
cat > /tmp/pr-body-${{ github.run_id }}.md <<'EOF'
|
||||
## Automated Documentation Review
|
||||
|
||||
This PR contains documentation updates identified by the weekly automated review.
|
||||
|
||||
### Review Process
|
||||
- Automated fact-checking against current codebase
|
||||
- Verification of code examples and API references
|
||||
- Detection of outdated or missing documentation
|
||||
|
||||
### What was checked
|
||||
- All markdown documentation in `docs/`
|
||||
- Project guidelines in `CLAUDE.md`
|
||||
- README files throughout the repository
|
||||
- Claude command documentation in `.claude/commands/`
|
||||
|
||||
**Note**: This is an automated PR. Please review all changes carefully before merging.
|
||||
|
||||
🤖 Generated by weekly documentation check workflow
|
||||
EOF
|
||||
fi
|
||||
|
||||
- name: Create or Update Pull Request
|
||||
if: steps.check_changes.outputs.has_changes == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PR_GH_TOKEN }}
|
||||
commit-message: 'docs: weekly documentation accuracy update'
|
||||
branch: docs/weekly-update
|
||||
delete-branch: true
|
||||
title: 'docs: Weekly Documentation Update'
|
||||
body-path: /tmp/pr-body-${{ github.run_id }}.md
|
||||
labels: |
|
||||
documentation
|
||||
automated
|
||||
draft: true
|
||||
assignees: ${{ github.repository_owner }}
|
||||
@@ -62,6 +62,11 @@ Key Nx features:
|
||||
|
||||
## Project Philosophy
|
||||
|
||||
- Follow good software engineering principles
|
||||
- YAGNI
|
||||
- AHA
|
||||
- DRY
|
||||
- SOLID
|
||||
- Clean, stable public APIs
|
||||
- Domain-driven design
|
||||
- Thousands of users and extensions
|
||||
|
||||
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 82 KiB |
@@ -1657,7 +1657,8 @@ export const comfyPageFixture = base.extend<{
|
||||
'Comfy.userId': userId,
|
||||
// Set tutorial completed to true to avoid loading the tutorial workflow.
|
||||
'Comfy.TutorialCompleted': true,
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize
|
||||
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
|
||||
'Comfy.VueNodes.AutoScaleLayout': false
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
@@ -5,9 +5,9 @@ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescrip
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import storybook from 'eslint-plugin-storybook'
|
||||
import tailwind from 'eslint-plugin-tailwindcss'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import type { ESLint, Linter } from 'eslint'
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import globals from 'globals'
|
||||
import {
|
||||
@@ -34,11 +34,7 @@ const settings = {
|
||||
],
|
||||
noWarnOnMultipleProjects: true
|
||||
})
|
||||
],
|
||||
tailwindcss: {
|
||||
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
|
||||
functions: ['cn', 'clsx', 'tw']
|
||||
}
|
||||
]
|
||||
} as const
|
||||
|
||||
const commonParserOptions = {
|
||||
@@ -94,22 +90,15 @@ export default defineConfig([
|
||||
pluginJs.configs.recommended,
|
||||
|
||||
tseslintConfigs.recommended,
|
||||
// Difference in typecheck on CI vs Local
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore Bad types in the plugin
|
||||
tailwind.configs['flat/recommended'],
|
||||
pluginVue.configs['flat/recommended'],
|
||||
...(pluginVue.configs['flat/recommended'] as Linter.Config[]),
|
||||
eslintPluginPrettierRecommended,
|
||||
storybook.configs['flat/recommended'],
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
importX.flatConfigs.recommended,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
importX.flatConfigs.typescript,
|
||||
importX.flatConfigs.recommended as Linter.Config,
|
||||
importX.flatConfigs.typescript as Linter.Config,
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
// @ts-expect-error Bad types in the plugin
|
||||
'@intlify/vue-i18n': pluginI18n
|
||||
'@intlify/vue-i18n': pluginI18n as ESLint.Plugin
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
@@ -129,7 +118,6 @@ export default defineConfig([
|
||||
'import-x/no-relative-packages': 'error',
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
'tailwindcss/no-custom-classname': 'off', // TODO: fix
|
||||
'vue/no-v-html': 'off',
|
||||
// Enforce dark-theme: instead of dark: prefix
|
||||
'vue/no-restricted-class': ['error', '/^dark:/'],
|
||||
|
||||
@@ -14,7 +14,7 @@ const config: KnipConfig = {
|
||||
},
|
||||
'apps/desktop-ui': {
|
||||
entry: ['src/main.ts', 'src/i18n.ts'],
|
||||
project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}']
|
||||
project: ['src/**/*.{js,ts,vue}']
|
||||
},
|
||||
'packages/tailwind-utils': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.31.0",
|
||||
"version": "1.31.1",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -61,7 +61,6 @@
|
||||
"@storybook/vue3-vite": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@trivago/prettier-plugin-sort-imports": "catalog:",
|
||||
"@types/eslint-plugin-tailwindcss": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@types/jsdom": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
@@ -78,7 +77,6 @@
|
||||
"eslint-plugin-import-x": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-storybook": "catalog:",
|
||||
"eslint-plugin-tailwindcss": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
"fs-extra": "^11.2.0",
|
||||
|
||||
@@ -9,29 +9,18 @@
|
||||
|
||||
@config '../../tailwind.config.ts';
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--fg-color: #fff;
|
||||
--bg-color: #202020;
|
||||
--content-bg: #4e4e4e;
|
||||
--content-fg: #fff;
|
||||
--content-hover-bg: #222;
|
||||
--content-hover-fg: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 8px;
|
||||
--text-xxxs: 0.5625rem;
|
||||
--text-xxxs--line-height: calc(1 / 0.5625);
|
||||
|
||||
/* Font Families */
|
||||
--font-inter: 'Inter', sans-serif;
|
||||
|
||||
/* Palette Colors */
|
||||
--color-charcoal-100: #171718;
|
||||
--color-charcoal-100: #55565e;
|
||||
--color-charcoal-200: #494a50;
|
||||
--color-charcoal-300: #3c3d42;
|
||||
--color-charcoal-400: #313235;
|
||||
@@ -42,43 +31,45 @@
|
||||
|
||||
--color-neutral-550: #636363;
|
||||
|
||||
--color-stone-100: #828282;
|
||||
--color-stone-200: #444444;
|
||||
--color-stone-300: #bbbbbb;
|
||||
--color-ash-300: #bbbbbb;
|
||||
--color-ash-500: #828282;
|
||||
--color-ash-800: #444444;
|
||||
|
||||
--color-ivory-100: #fdfbfa;
|
||||
--color-ivory-200: #faf9f5;
|
||||
--color-ivory-300: #f0eee6;
|
||||
|
||||
--color-gray-100: #f3f3f3;
|
||||
--color-gray-200: #e9e9e9;
|
||||
--color-gray-300: #e1e1e1;
|
||||
--color-gray-400: #d9d9d9;
|
||||
--color-gray-500: #c5c5c5;
|
||||
--color-gray-600: #b4b4b4;
|
||||
--color-gray-700: #a0a0a0;
|
||||
--color-gray-800: #8a8a8a;
|
||||
--color-smoke-100: #f3f3f3;
|
||||
--color-smoke-200: #e9e9e9;
|
||||
--color-smoke-300: #e1e1e1;
|
||||
--color-smoke-400: #d9d9d9;
|
||||
--color-smoke-500: #c5c5c5;
|
||||
--color-smoke-600: #b4b4b4;
|
||||
--color-smoke-700: #a0a0a0;
|
||||
--color-smoke-800: #8a8a8a;
|
||||
|
||||
--color-sand-100: #e1ded5;
|
||||
--color-sand-200: #d6cfc2;
|
||||
--color-sand-300: #888682;
|
||||
|
||||
--color-pure-black: #000000;
|
||||
--color-pure-white: #ffffff;
|
||||
|
||||
--color-slate-100: #9c9eab;
|
||||
--color-slate-200: #9fa2bd;
|
||||
--color-slate-300: #5b5e7d;
|
||||
|
||||
--color-brand-yellow: #f0ff41;
|
||||
--color-brand-blue: #172dd7;
|
||||
--color-electric-400: #f0ff41;
|
||||
--color-sapphire-700: #172dd7;
|
||||
--color-brand-yellow: var(--color-electric-400);
|
||||
--color-brand-blue: var(--color-sapphire-700);
|
||||
|
||||
--color-azure-400: #31b9f4;
|
||||
--color-azure-600: #0b8ce9;
|
||||
|
||||
--color-jade-400: #47e469;
|
||||
--color-jade-600: #00cd72;
|
||||
|
||||
--color-gold-400: #fcbf64;
|
||||
--color-gold-600: #fd9903;
|
||||
|
||||
--color-blue-100: #0b8ce9;
|
||||
--color-blue-200: #31b9f4;
|
||||
--color-success-100: #00cd72;
|
||||
--color-success-200: #47e469;
|
||||
--color-warning-100: #fd9903;
|
||||
--color-warning-200: #fcbf64;
|
||||
--color-danger-100: #c02323;
|
||||
--color-danger-200: #d62952;
|
||||
|
||||
@@ -90,26 +81,24 @@
|
||||
--color-error: #962a2a;
|
||||
|
||||
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
|
||||
--text-xxxs: 0.5625rem;
|
||||
--text-xxxs--line-height: calc(1 / 0.5625);
|
||||
|
||||
--color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3);
|
||||
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
|
||||
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
|
||||
--color-blue-selection: rgb(from var(--color-azure-600) r g b / 0.3);
|
||||
--color-node-hover-100: rgb(from var(--color-charcoal-800) r g b/ 0.15);
|
||||
--color-node-hover-200: rgb(from var(--color-charcoal-800) r g b/ 0.1);
|
||||
--color-modal-tag: rgb(from var(--color-smoke-400) r g b/ 0.4);
|
||||
--color-alpha-charcoal-600-30: color-mix(
|
||||
in srgb,
|
||||
var(--color-charcoal-600) 30%,
|
||||
transparent
|
||||
);
|
||||
--color-alpha-stone-100-20: color-mix(
|
||||
--color-alpha-ash-500-20: color-mix(
|
||||
in srgb,
|
||||
var(--color-stone-100) 20%,
|
||||
var(--color-ash-500) 20%,
|
||||
transparent
|
||||
);
|
||||
--color-alpha-gray-500-50: color-mix(
|
||||
--color-alpha-smoke-500-50: color-mix(
|
||||
in srgb,
|
||||
var(--color-gray-500) 50%,
|
||||
var(--color-smoke-500) 50%,
|
||||
transparent
|
||||
);
|
||||
|
||||
@@ -145,8 +134,8 @@
|
||||
--content-hover-bg: #adadad;
|
||||
--content-hover-fg: #000;
|
||||
|
||||
--button-surface: var(--color-pure-white);
|
||||
--button-surface-contrast: var(--color-pure-black);
|
||||
--button-surface: var(--color-white);
|
||||
--button-surface-contrast: var(--color-black);
|
||||
|
||||
/* Code styling colors for help menu*/
|
||||
--code-text-color: rgb(0 122 255 / 1);
|
||||
@@ -157,31 +146,36 @@
|
||||
|
||||
--accent-primary: var(--color-charcoal-700);
|
||||
--backdrop: var(--color-white);
|
||||
--button-hover-surface: var(--color-gray-200);
|
||||
--button-active-surface: var(--color-gray-400);
|
||||
--button-icon: var(--color-gray-600);
|
||||
|
||||
--button-hover-surface: var(--color-smoke-200);
|
||||
--button-active-surface: var(--color-smoke-400);
|
||||
--button-icon: var(--color-smoke-600);
|
||||
|
||||
--dialog-surface: var(--color-neutral-200);
|
||||
--interface-menu-component-surface-hovered: var(--color-gray-200);
|
||||
--interface-menu-component-surface-selected: var(--color-gray-400);
|
||||
--interface-menu-keybind-surface-default: var(--color-gray-500);
|
||||
--interface-panel-surface: var(--color-pure-white);
|
||||
--interface-stroke: var(--color-gray-300);
|
||||
--nav-background: var(--color-pure-white);
|
||||
--node-border: var(--color-gray-300);
|
||||
--node-component-border: var(--color-gray-400);
|
||||
--node-component-disabled: var(--color-alpha-stone-100-20);
|
||||
|
||||
--interface-menu-component-surface-hovered: var(--color-smoke-200);
|
||||
--interface-menu-component-surface-selected: var(--color-smoke-400);
|
||||
--interface-menu-keybind-surface-default: var(--color-smoke-500);
|
||||
--interface-panel-surface: var(--color-white);
|
||||
--interface-stroke: var(--color-smoke-300);
|
||||
|
||||
--nav-background: var(--color-white);
|
||||
|
||||
--node-border: var(--color-smoke-300);
|
||||
--node-component-border: var(--color-smoke-400);
|
||||
--node-component-disabled: var(--color-alpha-ash-500-20);
|
||||
--node-component-executing: var(--color-blue-500);
|
||||
--node-component-header: var(--fg-color);
|
||||
--node-component-header-icon: var(--color-stone-200);
|
||||
--node-component-header-icon: var(--color-ash-800);
|
||||
--node-component-header-surface: var(--color-white);
|
||||
--node-component-outline: var(--color-black);
|
||||
--node-component-ring: rgb(from var(--color-gray-500) r g b / 50%);
|
||||
--node-component-ring: rgb(from var(--color-smoke-500) r g b / 50%);
|
||||
--node-component-slot-dot-outline-opacity-mult: 1;
|
||||
--node-component-slot-dot-outline-opacity: 5%;
|
||||
--node-component-slot-dot-outline: var(--color-black);
|
||||
--node-component-slot-text: var(--color-stone-200);
|
||||
--node-component-surface-highlight: var(--color-stone-100);
|
||||
--node-component-surface-hovered: var(--color-gray-200);
|
||||
--node-component-slot-text: var(--color-ash-800);
|
||||
--node-component-surface-highlight: var(--color-ash-500);
|
||||
--node-component-surface-hovered: var(--color-smoke-200);
|
||||
--node-component-surface-selected: var(--color-charcoal-200);
|
||||
--node-component-surface: var(--color-white);
|
||||
--node-component-tooltip: var(--color-charcoal-700);
|
||||
@@ -193,40 +187,53 @@
|
||||
);
|
||||
--node-component-widget-skeleton-surface: var(--color-zinc-300);
|
||||
--node-divider: var(--color-sand-100);
|
||||
--node-icon-disabled: var(--color-alpha-gray-500-50);
|
||||
--node-stroke: var(--color-gray-400);
|
||||
--node-icon-disabled: var(--color-alpha-smoke-500-50);
|
||||
--node-stroke: var(--color-smoke-400);
|
||||
--node-stroke-selected: var(--color-accent-primary);
|
||||
--node-stroke-error: var(--color-error);
|
||||
--node-stroke-executing: var(--color-blue-100);
|
||||
--text-secondary: var(--color-stone-100);
|
||||
--node-stroke-executing: var(--color-azure-600);
|
||||
|
||||
--text-secondary: var(--color-ash-500);
|
||||
--text-primary: var(--color-charcoal-700);
|
||||
--input-surface: rgb(0 0 0 / 0.15);
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
--accent-primary: var(--color-pure-white);
|
||||
--fg-color: #fff;
|
||||
--bg-color: #202020;
|
||||
--content-bg: #4e4e4e;
|
||||
--content-fg: #fff;
|
||||
--content-hover-bg: #222;
|
||||
--content-hover-fg: #fff;
|
||||
|
||||
--accent-primary: var(--color-white);
|
||||
--backdrop: var(--color-neutral-900);
|
||||
|
||||
--button-surface: var(--color-charcoal-600);
|
||||
--button-surface-contrast: var(--color-pure-white);
|
||||
--button-surface-contrast: var(--color-white);
|
||||
--button-hover-surface: var(--color-charcoal-600);
|
||||
--button-active-surface: var(--color-charcoal-600);
|
||||
--button-icon: var(--color-gray-800);
|
||||
--button-icon: var(--color-smoke-800);
|
||||
|
||||
--dialog-surface: var(--color-neutral-700);
|
||||
|
||||
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
|
||||
--interface-menu-component-surface-selected: var(--color-charcoal-300);
|
||||
--interface-menu-keybind-surface-default: var(--color-charcoal-200);
|
||||
--interface-panel-surface: var(--color-charcoal-100);
|
||||
--interface-panel-surface: var(--color-charcoal-800);
|
||||
--interface-stroke: var(--color-charcoal-400);
|
||||
--nav-background: var(--color-charcoal-100);
|
||||
|
||||
--nav-background: var(--color-charcoal-800);
|
||||
|
||||
--node-border: var(--color-charcoal-500);
|
||||
--node-component-border: var(--color-stone-200);
|
||||
--node-component-border: var(--color-ash-800);
|
||||
--node-component-border-error: var(--color-danger-100);
|
||||
--node-component-border-executing: var(--color-blue-500);
|
||||
--node-component-border-selected: var(--color-charcoal-200);
|
||||
--node-component-header-icon: var(--color-slate-300);
|
||||
--node-component-header-surface: var(--color-charcoal-800);
|
||||
--node-component-outline: var(--color-white);
|
||||
--node-component-ring: rgb(var(--color-gray-500) / 20%);
|
||||
--node-component-ring: rgb(var(--color-smoke-500) / 20%);
|
||||
--node-component-slot-dot-outline-opacity: 10%;
|
||||
--node-component-slot-dot-outline: var(--color-white);
|
||||
--node-component-slot-text: var(--color-slate-200);
|
||||
@@ -240,13 +247,15 @@
|
||||
--node-component-widget-skeleton-surface: var(--color-zinc-800);
|
||||
--node-component-disabled: var(--color-alpha-charcoal-600-30);
|
||||
--node-divider: var(--color-charcoal-500);
|
||||
--node-icon-disabled: var(--color-alpha-stone-100-20);
|
||||
--node-stroke: var(--color-stone-200);
|
||||
--node-stroke-selected: var(--color-pure-white);
|
||||
--node-icon-disabled: var(--color-alpha-ash-500-20);
|
||||
--node-stroke: var(--color-ash-800);
|
||||
--node-stroke-selected: var(--color-white);
|
||||
--node-stroke-error: var(--color-error);
|
||||
--node-stroke-executing: var(--color-blue-100);
|
||||
--node-stroke-executing: var(--color-azure-600);
|
||||
|
||||
--text-secondary: var(--color-slate-100);
|
||||
--text-primary: var(--color-pure-white);
|
||||
--text-primary: var(--color-white);
|
||||
|
||||
--input-surface: rgb(130 130 130 / 0.1);
|
||||
}
|
||||
|
||||
@@ -330,7 +339,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ===================== Scrollbar Utilities (Tailwind) =====================
|
||||
Usage: Add `scrollbar-custom` class to scrollable containers.
|
||||
The scrollbar styling adapts to light/dark theme automatically.
|
||||
|
||||
5
packages/design-system/src/icons/image-ai-edit.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 9.99996L11.9427 7.94263C11.6926 7.69267 11.3536 7.55225 11 7.55225C10.6464 7.55225 10.3074 7.69267 10.0573 7.94263L9 9M8 14H12.6667C13.403 14 14 13.403 14 12.6667V3.33333C14 2.59695 13.403 2 12.6667 2H3.33333C2.59695 2 2 2.59695 2 3.33333V8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5.51377 12.671L4.77612 14.3921C4.67222 14.6346 4.32853 14.6346 4.22463 14.3921L3.48699 12.671C3.45664 12.6002 3.40022 12.5437 3.32942 12.5134L1.60825 11.7757C1.36581 11.6718 1.36581 11.3282 1.60825 11.2243L3.32942 10.4866C3.40022 10.4563 3.45664 10.3998 3.48699 10.329L4.22463 8.60787C4.32853 8.36544 4.67222 8.36544 4.77612 8.60787L5.51377 10.329C5.54411 10.3998 5.60053 10.4563 5.67134 10.4866L7.39251 11.2243C7.63494 11.3282 7.63494 11.6718 7.39251 11.7757L5.67134 12.5134C5.60053 12.5437 5.54411 12.6002 5.51377 12.671Z" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>
|
||||
<path d="M5 5H5.0001" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -474,3 +474,68 @@ export function formatDuration(milliseconds: number): string {
|
||||
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'] as const
|
||||
const VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi'] as const
|
||||
const AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac'] as const
|
||||
const THREE_D_EXTENSIONS = ['obj', 'fbx', 'gltf', 'glb'] as const
|
||||
|
||||
const MEDIA_TYPES = ['image', 'video', 'audio', '3D'] as const
|
||||
type MediaType = (typeof MEDIA_TYPES)[number]
|
||||
|
||||
// Type guard helper for checking array membership
|
||||
type ImageExtension = (typeof IMAGE_EXTENSIONS)[number]
|
||||
type VideoExtension = (typeof VIDEO_EXTENSIONS)[number]
|
||||
type AudioExtension = (typeof AUDIO_EXTENSIONS)[number]
|
||||
type ThreeDExtension = (typeof THREE_D_EXTENSIONS)[number]
|
||||
|
||||
/**
|
||||
* Truncates a filename while preserving the extension
|
||||
* @param filename The filename to truncate
|
||||
* @param maxLength Maximum length for the filename without extension
|
||||
* @returns Truncated filename with extension preserved
|
||||
*/
|
||||
export function truncateFilename(
|
||||
filename: string,
|
||||
maxLength: number = 20
|
||||
): string {
|
||||
if (!filename || filename.length <= maxLength) {
|
||||
return filename
|
||||
}
|
||||
|
||||
const lastDotIndex = filename.lastIndexOf('.')
|
||||
const nameWithoutExt =
|
||||
lastDotIndex > -1 ? filename.substring(0, lastDotIndex) : filename
|
||||
const extension = lastDotIndex > -1 ? filename.substring(lastDotIndex) : ''
|
||||
|
||||
// If the name without extension is short enough, return as is
|
||||
if (nameWithoutExt.length <= maxLength) {
|
||||
return filename
|
||||
}
|
||||
|
||||
// Calculate how to split the truncation
|
||||
const halfLength = Math.floor((maxLength - 3) / 2) // -3 for '...'
|
||||
const start = nameWithoutExt.substring(0, halfLength)
|
||||
const end = nameWithoutExt.substring(nameWithoutExt.length - halfLength)
|
||||
|
||||
return `${start}...${end}${extension}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the media type from a filename's extension (singular form)
|
||||
* @param filename The filename to analyze
|
||||
* @returns The media type: 'image', 'video', 'audio', or '3D'
|
||||
*/
|
||||
export function getMediaTypeFromFilename(filename: string): MediaType {
|
||||
if (!filename) return 'image'
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
if (!ext) return 'image'
|
||||
|
||||
// Type-safe array includes check using type assertion
|
||||
if (IMAGE_EXTENSIONS.includes(ext as ImageExtension)) return 'image'
|
||||
if (VIDEO_EXTENSIONS.includes(ext as VideoExtension)) return 'video'
|
||||
if (AUDIO_EXTENSIONS.includes(ext as AudioExtension)) return 'audio'
|
||||
if (THREE_D_EXTENSIONS.includes(ext as ThreeDExtension)) return '3D'
|
||||
|
||||
return 'image'
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
// Use with conditional classes (ternary)
|
||||
<button
|
||||
:class="cn('px-4 py-2', isActive ? 'bg-blue-500' : 'bg-gray-500')"
|
||||
:class="cn('px-4 py-2', isActive ? 'bg-blue-500' : 'bg-smoke-500')"
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
50
pnpm-lock.yaml
generated
@@ -87,9 +87,6 @@ catalogs:
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.2
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: ^3.17.0
|
||||
version: 3.17.0
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.4
|
||||
version: 11.0.4
|
||||
@@ -153,9 +150,6 @@ catalogs:
|
||||
eslint-plugin-storybook:
|
||||
specifier: ^9.1.6
|
||||
version: 9.1.6
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 4.0.0-beta.0
|
||||
version: 4.0.0-beta.0
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
@@ -519,9 +513,6 @@ importers:
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 'catalog:'
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.6.2)
|
||||
'@types/eslint-plugin-tailwindcss':
|
||||
specifier: 'catalog:'
|
||||
version: 3.17.0
|
||||
'@types/fs-extra':
|
||||
specifier: 'catalog:'
|
||||
version: 11.0.4
|
||||
@@ -570,9 +561,6 @@ importers:
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0-beta.0(tailwindcss@4.1.12)
|
||||
eslint-plugin-unused-imports:
|
||||
specifier: 'catalog:'
|
||||
version: 4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))
|
||||
@@ -3155,9 +3143,6 @@ packages:
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/eslint-plugin-tailwindcss@3.17.0':
|
||||
resolution: {integrity: sha512-ucQGf2YIdTcndYcxRU3UdZgmhUHsOlbIF4BaRtl0op+7k2JmqM2i3aXZ6XIcfZgVq1ZKov7VM5c/BR81ukmkyg==}
|
||||
|
||||
'@types/estree@1.0.5':
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
|
||||
@@ -4700,12 +4685,6 @@ packages:
|
||||
eslint: '>=8'
|
||||
storybook: ^9.1.6
|
||||
|
||||
eslint-plugin-tailwindcss@4.0.0-beta.0:
|
||||
resolution: {integrity: sha512-WWCajZgQu38Sd67ZCl2W6i3MRzqB0d+H8s4qV9iB6lBJbsDOIpIlj6R1Fj2FXkoWErbo05pZnZYbCGIU9o/DsA==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
peerDependencies:
|
||||
tailwindcss: ^3.4.0 || ^4.0.0
|
||||
|
||||
eslint-plugin-unused-imports@4.2.0:
|
||||
resolution: {integrity: sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==}
|
||||
peerDependencies:
|
||||
@@ -7163,11 +7142,6 @@ packages:
|
||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
tailwind-api-utils@1.0.3:
|
||||
resolution: {integrity: sha512-KpzUHkH1ug1sq4394SLJX38ZtpeTiqQ1RVyFTTSY2XuHsNSTWUkRo108KmyyrMWdDbQrLYkSHaNKj/a3bmA4sQ==}
|
||||
peerDependencies:
|
||||
tailwindcss: ^3.3.0 || ^4.0.0 || ^4.0.0-beta
|
||||
|
||||
tailwind-merge@2.6.0:
|
||||
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
|
||||
|
||||
@@ -7626,6 +7600,9 @@ packages:
|
||||
vue-component-type-helpers@3.1.1:
|
||||
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
|
||||
|
||||
vue-component-type-helpers@3.1.2:
|
||||
resolution: {integrity: sha512-ch3/SKBtxdZq18vsEntiGCdSszCRNfhX5QaTxjSacCAXLlNQRXfXo+ANjoQEYJMsJOJy1/vHF6Tkc4s85MS+zw==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10308,7 +10285,7 @@ snapshots:
|
||||
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
|
||||
vue-component-type-helpers: 3.1.2
|
||||
|
||||
'@swc/helpers@0.5.17':
|
||||
dependencies:
|
||||
@@ -10613,8 +10590,6 @@ snapshots:
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/eslint-plugin-tailwindcss@3.17.0': {}
|
||||
|
||||
'@types/estree@1.0.5': {}
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
@@ -12380,14 +12355,6 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
eslint-plugin-tailwindcss@4.0.0-beta.0(tailwindcss@4.1.12):
|
||||
dependencies:
|
||||
fast-glob: 3.3.3
|
||||
postcss: 8.5.6
|
||||
synckit: 0.11.11
|
||||
tailwind-api-utils: 1.0.3(tailwindcss@4.1.12)
|
||||
tailwindcss: 4.1.12
|
||||
|
||||
eslint-plugin-unused-imports@4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2)):
|
||||
dependencies:
|
||||
eslint: 9.35.0(jiti@2.4.2)
|
||||
@@ -15431,13 +15398,6 @@ snapshots:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
tailwind-api-utils@1.0.3(tailwindcss@4.1.12):
|
||||
dependencies:
|
||||
enhanced-resolve: 5.18.3
|
||||
jiti: 2.5.1
|
||||
local-pkg: 1.1.2
|
||||
tailwindcss: 4.1.12
|
||||
|
||||
tailwind-merge@2.6.0: {}
|
||||
|
||||
tailwindcss-primeui@0.6.1(tailwindcss@4.1.12):
|
||||
@@ -15983,6 +15943,8 @@ snapshots:
|
||||
|
||||
vue-component-type-helpers@3.1.1: {}
|
||||
|
||||
vue-component-type-helpers@3.1.2: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
|
||||
@@ -30,7 +30,6 @@ catalog:
|
||||
'@storybook/vue3-vite': ^9.1.1
|
||||
'@tailwindcss/vite': ^4.1.12
|
||||
'@trivago/prettier-plugin-sort-imports': ^5.2.0
|
||||
'@types/eslint-plugin-tailwindcss': ^3.17.0
|
||||
'@types/fs-extra': ^11.0.4
|
||||
'@types/jsdom': ^21.1.7
|
||||
'@types/node': ^20.14.8
|
||||
@@ -52,7 +51,6 @@ catalog:
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-prettier: ^5.5.4
|
||||
eslint-plugin-storybook: ^9.1.6
|
||||
eslint-plugin-tailwindcss: 4.0.0-beta.0
|
||||
eslint-plugin-unused-imports: ^4.2.0
|
||||
eslint-plugin-vue: ^10.4.0
|
||||
firebase: ^11.6.0
|
||||
@@ -64,6 +62,7 @@ catalog:
|
||||
knip: ^5.62.0
|
||||
lint-staged: ^15.2.7
|
||||
markdown-table: ^3.0.4
|
||||
mixpanel-browser: ^2.71.0
|
||||
nx: 21.4.1
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
@@ -99,7 +98,6 @@ 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
|
||||
|
||||
|
||||
@@ -314,6 +314,11 @@ function renderCategoryDetails(report) {
|
||||
|
||||
for (const category of report.categories) {
|
||||
lines.push(renderCategoryBlock(category, report.hasBaseline))
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (report.categories.length > 0) {
|
||||
lines.pop()
|
||||
}
|
||||
|
||||
lines.push('</details>')
|
||||
@@ -339,9 +344,11 @@ function renderCategoryBlock(category, hasBaseline) {
|
||||
|
||||
summaryParts.push('</summary>')
|
||||
lines.push(summaryParts.join(''))
|
||||
lines.push('')
|
||||
|
||||
if (category.description) {
|
||||
lines.push(`_${category.description}_`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
if (category.bundles.length === 0) {
|
||||
@@ -382,6 +389,7 @@ function renderCategoryBlock(category, hasBaseline) {
|
||||
})
|
||||
|
||||
lines.push(markdownTable([headers, ...rows]))
|
||||
lines.push('')
|
||||
|
||||
const statusParts = []
|
||||
if (category.counts.added) statusParts.push(`${category.counts.added} added`)
|
||||
@@ -393,10 +401,11 @@ function renderCategoryBlock(category, hasBaseline) {
|
||||
statusParts.push(`${category.counts.decreased} shrank`)
|
||||
|
||||
if (statusParts.length > 0) {
|
||||
lines.push(`\n_Status:_ ${statusParts.join(' / ')}`)
|
||||
lines.push(`_Status:_ ${statusParts.join(' / ')}`)
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
lines.push('</details>\n')
|
||||
lines.push('</details>')
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ const {
|
||||
}>()
|
||||
|
||||
const topStyle = computed(() => {
|
||||
const baseClasses = 'relative p-0'
|
||||
const baseClasses = 'relative p-0 overflow-hidden'
|
||||
|
||||
const ratioClasses = {
|
||||
square: 'aspect-square',
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="preview-box flex h-16 w-16 items-center justify-center rounded border p-2"
|
||||
:class="{ 'bg-gray-100 dark-theme:bg-gray-800': !modelValue }"
|
||||
:class="{ 'bg-smoke-100 dark-theme:bg-smoke-800': !modelValue }"
|
||||
>
|
||||
<img
|
||||
v-if="modelValue"
|
||||
:src="modelValue"
|
||||
class="max-h-full max-w-full object-contain"
|
||||
/>
|
||||
<i v-else class="pi pi-image text-xl text-gray-400" />
|
||||
<i v-else class="pi pi-image text-xl text-smoke-400" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
|
||||
@@ -36,53 +36,55 @@
|
||||
</template>
|
||||
|
||||
<template #contentFilter>
|
||||
<div class="relative flex flex-wrap gap-2 px-6 pt-2 pb-4">
|
||||
<!-- Model Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedModelObjects"
|
||||
v-model:search-query="modelSearchText"
|
||||
class="w-[250px]"
|
||||
:label="modelFilterLabel"
|
||||
:options="modelOptions"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--cpu]" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
<div class="relative flex flex-wrap justify-between gap-2 px-6 pb-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<!-- Model Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedModelObjects"
|
||||
v-model:search-query="modelSearchText"
|
||||
class="w-[250px]"
|
||||
:label="modelFilterLabel"
|
||||
:options="modelOptions"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--cpu]" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
|
||||
<!-- Use Case Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedUseCaseObjects"
|
||||
:label="useCaseFilterLabel"
|
||||
:options="useCaseOptions"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--target]" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
<!-- Use Case Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedUseCaseObjects"
|
||||
:label="useCaseFilterLabel"
|
||||
:options="useCaseOptions"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--target]" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
|
||||
<!-- License Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedLicenseObjects"
|
||||
:label="licenseFilterLabel"
|
||||
:options="licenseOptions"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--file-text]" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
<!-- License Filter -->
|
||||
<MultiSelect
|
||||
v-model="selectedLicenseObjects"
|
||||
:label="licenseFilterLabel"
|
||||
:options="licenseOptions"
|
||||
:show-search-box="true"
|
||||
:show-selected-count="true"
|
||||
:show-clear-button="true"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--file-text]" />
|
||||
</template>
|
||||
</MultiSelect>
|
||||
</div>
|
||||
|
||||
<!-- Sort Options -->
|
||||
<div class="absolute right-5">
|
||||
<div>
|
||||
<SingleSelect
|
||||
v-model="sortBy"
|
||||
:label="$t('templateWorkflows.sorting', 'Sort by')"
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="font-semibold">
|
||||
{{ data.params?.api_name || 'API' }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-400">
|
||||
<div class="text-sm text-smoke-400">
|
||||
{{ $t('credits.model') }}: {{ data.params?.model || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<!-- Login Section -->
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<p class="text-gray-600">
|
||||
<p class="text-smoke-600">
|
||||
{{ $t('auth.login.title') }}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -148,6 +148,6 @@ watch(
|
||||
</script>
|
||||
<style>
|
||||
.zoomInputContainer:focus-within {
|
||||
border: 1px solid var(--color-pure-white);
|
||||
border: 1px solid var(--color-white);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="option.type === 'divider'"
|
||||
class="my-1 h-px bg-gray-200 dark-theme:bg-zinc-700"
|
||||
class="my-1 h-px bg-smoke-200 dark-theme:bg-zinc-700"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
:key="subOption.label"
|
||||
:class="
|
||||
isColorSubmenu
|
||||
? 'w-7 h-7 flex items-center justify-center hover:bg-gray-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer'
|
||||
: 'flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-gray-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer'
|
||||
? 'w-7 h-7 flex items-center justify-center hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer'
|
||||
: 'flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-smoke-100 dark-theme:hover:bg-zinc-700 rounded cursor-pointer'
|
||||
"
|
||||
:title="subOption.label"
|
||||
@click="handleSubmenuClick(subOption)"
|
||||
>
|
||||
<div
|
||||
v-if="subOption.color"
|
||||
class="h-5 w-5 rounded-full border border-gray-300 dark-theme:border-zinc-600"
|
||||
class="h-5 w-5 rounded-full border border-smoke-300 dark-theme:border-zinc-600"
|
||||
:style="{ backgroundColor: subOption.color }"
|
||||
/>
|
||||
<template v-else-if="!subOption.color">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<template>
|
||||
<div class="h-6 w-px self-center bg-gray-300/10 dark-theme:bg-gray-600/10" />
|
||||
<div
|
||||
class="h-6 w-px self-center bg-smoke-300/10 dark-theme:bg-smoke-600/10"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
>
|
||||
<div class="mb-1 flex justify-end">
|
||||
<div
|
||||
class="max-w-[80%] rounded-xl bg-gray-300 px-4 py-1 text-right dark-theme:bg-gray-800"
|
||||
class="max-w-[80%] rounded-xl bg-smoke-300 px-4 py-1 text-right dark-theme:bg-smoke-800"
|
||||
>
|
||||
<div class="text-[12px] break-words">{{ item.prompt }}</div>
|
||||
</div>
|
||||
@@ -26,7 +26,7 @@
|
||||
"
|
||||
text
|
||||
rounded
|
||||
class="h-4! w-4! p-1! text-gray-400 transition hover:text-gray-600 hover:dark-theme:text-gray-200"
|
||||
class="h-4! w-4! p-1! text-smoke-400 transition hover:text-smoke-600 hover:dark-theme:text-smoke-200"
|
||||
pt:icon:class="text-xs!"
|
||||
:icon="editIndex === i ? 'pi pi-times' : 'pi pi-pencil'"
|
||||
:aria-label="
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"
|
||||
text
|
||||
rounded
|
||||
class="h-4! w-6! p-1! text-gray-400 transition hover:text-gray-600 hover:dark-theme:text-gray-200"
|
||||
class="h-4! w-6! p-1! text-smoke-400 transition hover:text-smoke-600 hover:dark-theme:text-smoke-200"
|
||||
pt:icon:class="text-xs!"
|
||||
:icon="copied ? 'pi pi-check' : 'pi pi-copy'"
|
||||
:aria-label="
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="help-center-menu" role="menu" aria-label="Help Center Menu">
|
||||
<div
|
||||
class="help-center-menu"
|
||||
role="menu"
|
||||
:aria-label="$t('helpCenter.helpFeedback')"
|
||||
>
|
||||
<!-- Main Menu Items -->
|
||||
<nav class="help-menu-section" role="menubar">
|
||||
<button
|
||||
@@ -68,7 +72,11 @@
|
||||
<h3 class="section-description">{{ $t('helpCenter.whatsNew') }}</h3>
|
||||
|
||||
<!-- Release Items -->
|
||||
<div v-if="hasReleases" role="group" aria-label="Recent releases">
|
||||
<div
|
||||
v-if="hasReleases"
|
||||
role="group"
|
||||
:aria-label="$t('helpCenter.recentReleases')"
|
||||
>
|
||||
<article
|
||||
v-for="release in releaseStore.recentReleases"
|
||||
:key="release.id || release.version"
|
||||
|
||||
@@ -119,12 +119,12 @@ export const KeyboardNavigationDemo: Story = {
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="bg-blue-50 dark-theme:bg-blue-900/20 border border-blue-200 dark-theme:border-blue-700 rounded-lg p-4">
|
||||
<div class="bg-blue-50 dark-theme:bg-blue-900/20 border border-azure-400 dark-theme:border-blue-700 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold mb-2">🎯 Keyboard Navigation Test</h3>
|
||||
<p class="text-sm text-gray-600 dark-theme:text-gray-300 mb-4">
|
||||
<p class="text-sm text-smoke-600 dark-theme:text-smoke-300 mb-4">
|
||||
Use your keyboard to navigate this MultiSelect:
|
||||
</p>
|
||||
<ol class="text-sm text-gray-600 list-decimal list-inside space-y-1">
|
||||
<ol class="text-sm text-smoke-600 list-decimal list-inside space-y-1">
|
||||
<li><strong>Tab</strong> to focus the dropdown</li>
|
||||
<li><strong>Enter/Space</strong> to open dropdown</li>
|
||||
<li><strong>Arrow Up/Down</strong> to navigate options</li>
|
||||
@@ -134,11 +134,11 @@ export const KeyboardNavigationDemo: Story = {
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
<label class="block text-sm font-medium text-smoke-700">
|
||||
Select Frameworks (Keyboard Navigation Test)
|
||||
</label>
|
||||
<MultiSelect v-bind="args" class="w-80" />
|
||||
<p class="text-xs text-gray-500">
|
||||
<p class="text-xs text-smoke-500">
|
||||
Selected: {{ selectedFrameworks.map(f => f.name).join(', ') || 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -186,10 +186,10 @@ export const ScreenReaderFriendly: Story = {
|
||||
<div class="space-y-6 p-4">
|
||||
<div class="bg-green-50 dark-theme:bg-green-900/20 border border-green-200 dark-theme:border-green-700 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold mb-2">♿ Screen Reader Test</h3>
|
||||
<p class="text-sm text-gray-600 mb-2">
|
||||
<p class="text-sm text-smoke-600 mb-2">
|
||||
These dropdowns have proper ARIA attributes and labels for screen readers:
|
||||
</p>
|
||||
<ul class="text-sm text-gray-600 list-disc list-inside space-y-1">
|
||||
<ul class="text-sm text-smoke-600 list-disc list-inside space-y-1">
|
||||
<li><code>role="combobox"</code> identifies as dropdown</li>
|
||||
<li><code>aria-haspopup="listbox"</code> indicates popup type</li>
|
||||
<li><code>aria-expanded</code> shows open/closed state</li>
|
||||
@@ -200,7 +200,7 @@ export const ScreenReaderFriendly: Story = {
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
<label class="block text-sm font-medium text-smoke-700">
|
||||
Color Preferences
|
||||
</label>
|
||||
<MultiSelect
|
||||
@@ -211,13 +211,13 @@ export const ScreenReaderFriendly: Story = {
|
||||
:show-clear-button="true"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-gray-500" aria-live="polite">
|
||||
<p class="text-xs text-smoke-500" aria-live="polite">
|
||||
{{ selectedColors.length }} color(s) selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
<label class="block text-sm font-medium text-smoke-700">
|
||||
Size Preferences
|
||||
</label>
|
||||
<MultiSelect
|
||||
@@ -228,7 +228,7 @@ export const ScreenReaderFriendly: Story = {
|
||||
:show-search-box="true"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-gray-500" aria-live="polite">
|
||||
<p class="text-xs text-smoke-500" aria-live="polite">
|
||||
{{ selectedSizes.length }} size(s) selected
|
||||
</p>
|
||||
</div>
|
||||
@@ -259,25 +259,25 @@ export const FocusManagement: Story = {
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="bg-purple-50 dark-theme:bg-purple-900/20 border border-purple-200 dark-theme:border-purple-700 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold mb-2">🎯 Focus Management Test</h3>
|
||||
<p class="text-sm text-gray-600 dark-theme:text-gray-300 mb-4">
|
||||
<p class="text-sm text-smoke-600 dark-theme:text-smoke-300 mb-4">
|
||||
Test focus behavior with multiple form elements:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 mb-1">
|
||||
Before MultiSelect
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Previous field"
|
||||
class="block w-64 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
class="block w-64 px-3 py-2 border border-smoke-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 mb-1">
|
||||
MultiSelect (Test Focus Ring)
|
||||
</label>
|
||||
<MultiSelect
|
||||
@@ -290,13 +290,13 @@ export const FocusManagement: Story = {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 mb-1">
|
||||
After MultiSelect
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Next field"
|
||||
class="block w-64 px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
class="block w-64 px-3 py-2 border border-smoke-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -307,7 +307,7 @@ export const FocusManagement: Story = {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 mt-4">
|
||||
<div class="text-sm text-smoke-600 mt-4">
|
||||
<strong>Test:</strong> Tab through all elements and verify focus rings are visible and logical.
|
||||
</div>
|
||||
</div>
|
||||
@@ -319,7 +319,7 @@ export const AccessibilityChecklist: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="max-w-4xl mx-auto p-6 space-y-6">
|
||||
<div class="bg-gray-50 dark-theme:bg-zinc-800 border border-gray-200 dark-theme:border-zinc-700 rounded-lg p-6">
|
||||
<div class="bg-gray-50 dark-theme:bg-zinc-800 border border-smoke-200 dark-theme:border-zinc-700 rounded-lg p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">♿ MultiSelect Accessibility Checklist</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@@ -366,9 +366,9 @@ export const AccessibilityChecklist: Story = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark-theme:bg-blue-900/20 border border-blue-200 dark-theme:border-blue-700 rounded-lg">
|
||||
<div class="mt-6 p-4 bg-blue-50 dark-theme:bg-blue-900/20 border border-azure-400 dark-theme:border-blue-700 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">🎯 Quick Test</h4>
|
||||
<p class="text-sm text-gray-700 dark-theme:text-gray-300">
|
||||
<p class="text-sm text-smoke-700 dark-theme:text-smoke-300">
|
||||
Close your eyes, use only the keyboard, and try to select multiple options from any dropdown above.
|
||||
If you can successfully navigate and make selections, the accessibility implementation is working!
|
||||
</p>
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
<!-- Trigger value (keep text scale identical) -->
|
||||
<template #value>
|
||||
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
|
||||
<span class="text-sm text-zinc-700 dark-theme:text-smoke-200">
|
||||
{{ label }}
|
||||
</span>
|
||||
<span
|
||||
@@ -242,7 +242,7 @@ const pt = computed(() => ({
|
||||
)
|
||||
},
|
||||
listContainer: () => ({
|
||||
style: { maxHeight: listMaxHeight },
|
||||
style: { maxHeight: `min(${listMaxHeight}, 50vh)` },
|
||||
class: 'scrollbar-custom'
|
||||
}),
|
||||
list: {
|
||||
|
||||
@@ -101,12 +101,12 @@ export const KeyboardNavigationDemo: Story = {
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-6 p-4">
|
||||
<div class="bg-blue-50 dark-theme:bg-blue-900/20 border border-blue-200 dark-theme:border-blue-700 rounded-lg p-4">
|
||||
<div class="bg-blue-50 dark-theme:bg-blue-900/20 border border-azure-400 dark-theme:border-blue-700 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold mb-2">🎯 Keyboard Navigation Test</h3>
|
||||
<p class="text-sm text-gray-600 dark-theme:text-gray-300 mb-4">
|
||||
<p class="text-sm text-smoke-600 dark-theme:text-smoke-300 mb-4">
|
||||
Use your keyboard to navigate these SingleSelect dropdowns:
|
||||
</p>
|
||||
<ol class="text-sm text-gray-600 dark-theme:text-gray-300 list-decimal list-inside space-y-1">
|
||||
<ol class="text-sm text-smoke-600 dark-theme:text-smoke-300 list-decimal list-inside space-y-1">
|
||||
<li><strong>Tab</strong> to focus the dropdown</li>
|
||||
<li><strong>Enter/Space</strong> to open dropdown</li>
|
||||
<li><strong>Arrow Up/Down</strong> to navigate options</li>
|
||||
@@ -117,7 +117,7 @@ export const KeyboardNavigationDemo: Story = {
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200">
|
||||
Sort Order
|
||||
</label>
|
||||
<SingleSelect
|
||||
@@ -126,13 +126,13 @@ export const KeyboardNavigationDemo: Story = {
|
||||
label="Choose sort order"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-xs text-gray-500">
|
||||
<p class="text-xs text-smoke-500">
|
||||
Selected: {{ selectedSort ? sortOptions.find(o => o.value === selectedSort)?.name : 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200">
|
||||
Task Priority (With Icon)
|
||||
</label>
|
||||
<SingleSelect
|
||||
@@ -147,7 +147,7 @@ export const KeyboardNavigationDemo: Story = {
|
||||
</svg>
|
||||
</template>
|
||||
</SingleSelect>
|
||||
<p class="text-xs text-gray-500">
|
||||
<p class="text-xs text-smoke-500">
|
||||
Selected: {{ selectedPriority ? priorityOptions.find(o => o.value === selectedPriority)?.name : 'None' }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -191,10 +191,10 @@ export const ScreenReaderFriendly: Story = {
|
||||
<div class="space-y-6 p-4">
|
||||
<div class="bg-green-50 dark-theme:bg-green-900/20 border border-green-200 dark-theme:border-green-700 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold mb-2">♿ Screen Reader Test</h3>
|
||||
<p class="text-sm text-gray-600 dark-theme:text-gray-300 mb-2">
|
||||
<p class="text-sm text-smoke-600 dark-theme:text-smoke-300 mb-2">
|
||||
These dropdowns have proper ARIA attributes and labels for screen readers:
|
||||
</p>
|
||||
<ul class="text-sm text-gray-600 dark-theme:text-gray-300 list-disc list-inside space-y-1">
|
||||
<ul class="text-sm text-smoke-600 dark-theme:text-smoke-300 list-disc list-inside space-y-1">
|
||||
<li><code>role="combobox"</code> identifies as dropdown</li>
|
||||
<li><code>aria-haspopup="listbox"</code> indicates popup type</li>
|
||||
<li><code>aria-expanded</code> shows open/closed state</li>
|
||||
@@ -205,7 +205,7 @@ export const ScreenReaderFriendly: Story = {
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200" id="language-label">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200" id="language-label">
|
||||
Preferred Language
|
||||
</label>
|
||||
<SingleSelect
|
||||
@@ -215,13 +215,13 @@ export const ScreenReaderFriendly: Story = {
|
||||
class="w-full"
|
||||
aria-labelledby="language-label"
|
||||
/>
|
||||
<p class="text-xs text-gray-500" aria-live="polite">
|
||||
<p class="text-xs text-smoke-500" aria-live="polite">
|
||||
Current: {{ selectedLanguage ? languageOptions.find(o => o.value === selectedLanguage)?.name : 'None selected' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200" id="theme-label">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200" id="theme-label">
|
||||
Interface Theme
|
||||
</label>
|
||||
<SingleSelect
|
||||
@@ -231,7 +231,7 @@ export const ScreenReaderFriendly: Story = {
|
||||
class="w-full"
|
||||
aria-labelledby="theme-label"
|
||||
/>
|
||||
<p class="text-xs text-gray-500" aria-live="polite">
|
||||
<p class="text-xs text-smoke-500" aria-live="polite">
|
||||
Current: {{ selectedTheme ? themeOptions.find(o => o.value === selectedTheme)?.name : 'No theme selected' }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -239,7 +239,7 @@ export const ScreenReaderFriendly: Story = {
|
||||
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<h4 class="font-semibold mb-2">🎧 Screen Reader Testing Tips</h4>
|
||||
<ul class="text-sm text-gray-600 dark-theme:text-gray-300 space-y-1">
|
||||
<ul class="text-sm text-smoke-600 dark-theme:text-smoke-300 space-y-1">
|
||||
<li>• Listen for role announcements when focusing</li>
|
||||
<li>• Verify dropdown state changes are announced</li>
|
||||
<li>• Check that selected values are spoken clearly</li>
|
||||
@@ -299,7 +299,7 @@ export const FormIntegration: Story = {
|
||||
<div class="max-w-2xl mx-auto p-6">
|
||||
<div class="bg-purple-50 dark-theme:bg-purple-900/20 border border-purple-200 dark-theme:border-purple-700 rounded-lg p-4 mb-6">
|
||||
<h3 class="text-lg font-semibold mb-2">📝 Form Integration Test</h3>
|
||||
<p class="text-sm text-gray-600 dark-theme:text-gray-300">
|
||||
<p class="text-sm text-smoke-600 dark-theme:text-smoke-300">
|
||||
Test keyboard navigation through a complete form with SingleSelect components.
|
||||
Tab order should be logical and all elements should be accessible.
|
||||
</p>
|
||||
@@ -307,19 +307,19 @@ export const FormIntegration: Story = {
|
||||
|
||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
|
||||
Title *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="Enter a title"
|
||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
class="block w-full px-3 py-2 border border-smoke-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
|
||||
Category *
|
||||
</label>
|
||||
<SingleSelect
|
||||
@@ -332,7 +332,7 @@ export const FormIntegration: Story = {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
|
||||
Status
|
||||
</label>
|
||||
<SingleSelect
|
||||
@@ -344,7 +344,7 @@ export const FormIntegration: Story = {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
|
||||
Assignee
|
||||
</label>
|
||||
<SingleSelect
|
||||
@@ -356,13 +356,13 @@ export const FormIntegration: Story = {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
|
||||
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
rows="4"
|
||||
placeholder="Enter description"
|
||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
class="block w-full px-3 py-2 border border-smoke-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -375,16 +375,16 @@ export const FormIntegration: Story = {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 bg-gray-300 dark-theme:bg-gray-600 text-gray-700 dark-theme:text-gray-200 rounded-md hover:bg-gray-400 dark-theme:hover:bg-gray-500 focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
|
||||
class="px-4 py-2 bg-smoke-300 dark-theme:bg-smoke-600 text-smoke-700 dark-theme:text-smoke-200 rounded-md hover:bg-smoke-400 dark-theme:hover:bg-smoke-500 focus:ring-2 focus:ring-smoke-500 focus:ring-offset-2"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-50 dark-theme:bg-zinc-800 border border-gray-200 dark-theme:border-zinc-700 rounded-lg">
|
||||
<div class="mt-6 p-4 bg-gray-50 dark-theme:bg-zinc-800 border border-smoke-200 dark-theme:border-zinc-700 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">Current Form Data:</h4>
|
||||
<pre class="text-xs text-gray-600 dark-theme:text-gray-300">{{ JSON.stringify(formData, null, 2) }}</pre>
|
||||
<pre class="text-xs text-smoke-600 dark-theme:text-smoke-300">{{ JSON.stringify(formData, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -395,7 +395,7 @@ export const AccessibilityChecklist: Story = {
|
||||
render: () => ({
|
||||
template: `
|
||||
<div class="max-w-4xl mx-auto p-6 space-y-6">
|
||||
<div class="bg-gray-50 dark-theme:bg-zinc-800 border border-gray-200 dark-theme:border-zinc-700 rounded-lg p-6">
|
||||
<div class="bg-gray-50 dark-theme:bg-zinc-800 border border-smoke-200 dark-theme:border-zinc-700 rounded-lg p-6">
|
||||
<h2 class="text-2xl font-bold mb-4">♿ SingleSelect Accessibility Checklist</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
@@ -442,9 +442,9 @@ export const AccessibilityChecklist: Story = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark-theme:bg-blue-900/20 border border-blue-200 dark-theme:border-blue-700 rounded-lg">
|
||||
<div class="mt-6 p-4 bg-blue-50 dark-theme:bg-blue-900/20 border border-azure-400 dark-theme:border-blue-700 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">🎯 Quick Test</h4>
|
||||
<p class="text-sm text-gray-700 dark-theme:text-gray-200">
|
||||
<p class="text-sm text-smoke-700 dark-theme:text-smoke-200">
|
||||
Close your eyes, use only the keyboard, and try to select different options from any dropdown above.
|
||||
If you can successfully navigate and make selections, the accessibility implementation is working!
|
||||
</p>
|
||||
@@ -452,7 +452,7 @@ export const AccessibilityChecklist: Story = {
|
||||
|
||||
<div class="mt-4 p-4 bg-orange-50 border border-orange-200 rounded-lg">
|
||||
<h4 class="font-semibold mb-2">⚡ Performance Note</h4>
|
||||
<p class="text-sm text-gray-700 dark-theme:text-gray-200">
|
||||
<p class="text-sm text-smoke-700 dark-theme:text-smoke-200">
|
||||
These accessibility features are built into the component with minimal performance impact.
|
||||
The ARIA attributes and keyboard handlers add less than 1KB to the bundle size.
|
||||
</p>
|
||||
|
||||
@@ -26,11 +26,11 @@
|
||||
<slot name="icon" />
|
||||
<span
|
||||
v-if="slotProps.value !== null && slotProps.value !== undefined"
|
||||
class="text-zinc-700 dark-theme:text-gray-200"
|
||||
class="text-zinc-700 dark-theme:text-smoke-200"
|
||||
>
|
||||
{{ getLabel(slotProps.value) }}
|
||||
</span>
|
||||
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
|
||||
<span v-else class="text-zinc-700 dark-theme:text-smoke-200">
|
||||
{{ label }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -158,7 +158,7 @@ const pt = computed(() => ({
|
||||
)
|
||||
},
|
||||
listContainer: () => ({
|
||||
style: `max-height: ${listMaxHeight}`,
|
||||
style: `max-height: min(${listMaxHeight}, 50vh)`,
|
||||
class: 'scrollbar-custom'
|
||||
}),
|
||||
list: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="pointer-events-auto absolute top-12 left-2 z-20 flex flex-col rounded-lg bg-gray-700/30"
|
||||
class="pointer-events-auto absolute top-12 left-2 z-20 flex flex-col rounded-lg bg-smoke-700/30"
|
||||
>
|
||||
<div class="show-menu relative">
|
||||
<Button class="p-button-rounded p-button-text" @click="toggleMenu">
|
||||
@@ -16,7 +16,7 @@
|
||||
v-for="category in availableCategories"
|
||||
:key="category"
|
||||
class="p-button-text flex w-full items-center justify-start"
|
||||
:class="{ 'bg-gray-600': activeCategory === category }"
|
||||
:class="{ 'bg-smoke-600': activeCategory === category }"
|
||||
@click="selectCategory(category)"
|
||||
>
|
||||
<i :class="getCategoryIcon(category)" />
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeCategory" class="rounded-lg bg-gray-700/30">
|
||||
<div v-show="activeCategory" class="rounded-lg bg-smoke-700/30">
|
||||
<SceneControls
|
||||
v-if="activeCategory === 'scene'"
|
||||
ref="sceneControlsRef"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative rounded-lg bg-gray-700/30">
|
||||
<div class="relative rounded-lg bg-smoke-700/30">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative rounded-lg bg-gray-700/30">
|
||||
<div class="relative rounded-lg bg-smoke-700/30">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button class="p-button-rounded p-button-text" @click="openIn3DViewer">
|
||||
<i
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
|
||||
<div v-if="showFOVButton" class="space-y-4">
|
||||
<label>{{ t('load3d.fov') }}</label>
|
||||
<Slider v-model="fov" :min="10" :max="150" :step="1" aria-label="fov" />
|
||||
<Slider
|
||||
v-model="fov"
|
||||
:min="10"
|
||||
:max="150"
|
||||
:step="1"
|
||||
:aria-label="t('load3d.fov')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
label="sideToolbar.labels.console"
|
||||
:label="$t('sideToolbar.labels.console')"
|
||||
:tooltip="$t('menu.toggleBottomPanel')"
|
||||
:selected="bottomPanelStore.activePanel == 'terminal'"
|
||||
@click="bottomPanelStore.toggleBottomPanel"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<SidebarIcon
|
||||
icon="pi pi-question-circle"
|
||||
class="comfy-help-center-btn"
|
||||
label="menu.help"
|
||||
:label="$t('menu.help')"
|
||||
:tooltip="$t('sideToolbar.helpCenter')"
|
||||
:icon-badge="shouldShowRedDot ? '•' : ''"
|
||||
:is-small="isSmall"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<SidebarIcon
|
||||
icon="pi pi-sign-out"
|
||||
:tooltip="tooltip"
|
||||
label="sideToolbar.logout"
|
||||
:label="$t('sideToolbar.logout')"
|
||||
@click="logout"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<SidebarIcon
|
||||
label="shortcuts.shortcuts"
|
||||
:label="$t('shortcuts.shortcuts')"
|
||||
:tooltip="tooltipText"
|
||||
:selected="isShortcutsPanelVisible"
|
||||
@click="toggleShortcutsPanel"
|
||||
|
||||
36
src/components/sidebar/tabs/AssetSidebarTemplate.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex h-full flex-col bg-interface-panel-surface"
|
||||
:class="props.class"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-if="slots.top"
|
||||
class="flex min-h-12 items-center border-b border-interface-stroke px-4 py-2"
|
||||
>
|
||||
<slot name="top" />
|
||||
</div>
|
||||
<div v-if="slots.header" class="px-4">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- h-0 to force scrollpanel to grow -->
|
||||
<ScrollPanel class="h-0 grow">
|
||||
<slot name="body" />
|
||||
</ScrollPanel>
|
||||
<div v-if="slots.footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ScrollPanel from 'primevue/scrollpanel'
|
||||
import { useSlots } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: string
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
</script>
|
||||
398
src/components/sidebar/tabs/AssetsSidebarTab.vue
Normal file
@@ -0,0 +1,398 @@
|
||||
<template>
|
||||
<AssetsSidebarTemplate>
|
||||
<template #top>
|
||||
<span v-if="!isInFolderView" class="font-bold">
|
||||
{{ $t('sideToolbar.mediaAssets') }}
|
||||
</span>
|
||||
<div v-else class="flex w-full items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-bold">{{ $t('Job ID') }}:</span>
|
||||
<span class="text-sm">{{ folderPromptId?.substring(0, 8) }}</span>
|
||||
<button
|
||||
class="m-0 cursor-pointer border-0 bg-transparent p-0 outline-0"
|
||||
role="button"
|
||||
@click="copyJobId"
|
||||
>
|
||||
<i class="mb-1 icon-[lucide--copy] text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ formattedExecutionTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #header>
|
||||
<!-- Job Detail View Header -->
|
||||
<div v-if="isInFolderView" class="pt-4 pb-2">
|
||||
<IconTextButton
|
||||
:label="$t('sideToolbar.backToAssets')"
|
||||
type="secondary"
|
||||
@click="exitFolderView"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--arrow-left] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
<!-- Normal Tab View -->
|
||||
<TabList v-else v-model="activeTab" class="pt-4 pb-1">
|
||||
<Tab value="input">{{ $t('sideToolbar.labels.imported') }}</Tab>
|
||||
<Tab value="output">{{ $t('sideToolbar.labels.generated') }}</Tab>
|
||||
</TabList>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="displayAssets.length" class="relative size-full">
|
||||
<VirtualGrid
|
||||
v-if="displayAssets.length"
|
||||
:items="mediaAssetsWithKey"
|
||||
:grid-style="{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))',
|
||||
padding: '0.5rem',
|
||||
gap: '0.5rem'
|
||||
}"
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<MediaAssetCard
|
||||
:asset="item"
|
||||
:selected="isSelected(item.id)"
|
||||
:show-output-count="shouldShowOutputCount(item)"
|
||||
:output-count="getOutputCount(item)"
|
||||
:show-delete-button="!isInFolderView"
|
||||
@click="handleAssetSelect(item)"
|
||||
@zoom="handleZoomClick(item)"
|
||||
@output-count-click="enterFolderView(item)"
|
||||
@asset-deleted="refreshAssets"
|
||||
/>
|
||||
</template>
|
||||
</VirtualGrid>
|
||||
<div v-else-if="loading">
|
||||
<ProgressSpinner
|
||||
class="absolute left-1/2 w-[50px] -translate-x-1/2"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<NoResultsPlaceholder
|
||||
icon="pi pi-info-circle"
|
||||
:title="
|
||||
$t(
|
||||
activeTab === 'input'
|
||||
? 'sideToolbar.noImportedFiles'
|
||||
: 'sideToolbar.noGeneratedFiles'
|
||||
)
|
||||
"
|
||||
:message="$t('sideToolbar.noFilesFoundMessage')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="hasSelection && activeTab === 'output'"
|
||||
class="flex h-18 w-full items-center justify-between px-4"
|
||||
>
|
||||
<div>
|
||||
<TextButton
|
||||
v-if="isHoveringSelectionCount"
|
||||
:label="$t('mediaAsset.selection.deselectAll')"
|
||||
type="transparent"
|
||||
@click="handleDeselectAll"
|
||||
@mouseleave="isHoveringSelectionCount = false"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="$t('mediaAsset.selection.deselectAll')"
|
||||
class="cursor-pointer px-3 text-sm focus:ring-2 focus:ring-primary focus:outline-none"
|
||||
@mouseenter="isHoveringSelectionCount = true"
|
||||
@keydown.enter="handleDeselectAll"
|
||||
@keydown.space.prevent="handleDeselectAll"
|
||||
>
|
||||
{{
|
||||
$t('mediaAsset.selection.selectedCount', { count: selectedCount })
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<IconTextButton
|
||||
v-if="!isInFolderView"
|
||||
:label="$t('mediaAsset.selection.deleteSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
@click="handleDeleteSelected"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
:label="$t('mediaAsset.selection.downloadSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
@click="handleDownloadSelected"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AssetsSidebarTemplate>
|
||||
<ResultGallery
|
||||
v-model:active-index="galleryActiveIndex"
|
||||
:all-gallery-items="galleryItems"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'
|
||||
import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
import { t } from '@/i18n'
|
||||
import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
|
||||
import { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'
|
||||
import { useAssetSelection } from '@/platform/assets/composables/useAssetSelection'
|
||||
import { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'
|
||||
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { ResultItemImpl } from '@/stores/queueStore'
|
||||
import { formatDuration, getMediaTypeFromFilename } from '@/utils/formatUtil'
|
||||
|
||||
import AssetsSidebarTemplate from './AssetSidebarTemplate.vue'
|
||||
|
||||
const activeTab = ref<'input' | 'output'>('input')
|
||||
const folderPromptId = ref<string | null>(null)
|
||||
const folderExecutionTime = ref<number | undefined>(undefined)
|
||||
const isInFolderView = computed(() => folderPromptId.value !== null)
|
||||
|
||||
const getOutputCount = (item: AssetItem): number => {
|
||||
const count = item.user_metadata?.outputCount
|
||||
return typeof count === 'number' && count > 0 ? count : 0
|
||||
}
|
||||
|
||||
const shouldShowOutputCount = (item: AssetItem): boolean => {
|
||||
if (activeTab.value !== 'output' || isInFolderView.value) {
|
||||
return false
|
||||
}
|
||||
return getOutputCount(item) > 1
|
||||
}
|
||||
|
||||
const formattedExecutionTime = computed(() => {
|
||||
if (!folderExecutionTime.value) return ''
|
||||
return formatDuration(folderExecutionTime.value * 1000)
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const inputAssets = useMediaAssets('input')
|
||||
const outputAssets = useMediaAssets('output')
|
||||
|
||||
// Asset selection
|
||||
const {
|
||||
isSelected,
|
||||
handleAssetClick,
|
||||
hasSelection,
|
||||
selectedCount,
|
||||
clearSelection,
|
||||
getSelectedAssets,
|
||||
activate: activateSelection,
|
||||
deactivate: deactivateSelection
|
||||
} = useAssetSelection()
|
||||
|
||||
const { downloadMultipleAssets, deleteMultipleAssets } = useMediaAssetActions()
|
||||
|
||||
// Hover state for selection count
|
||||
const isHoveringSelectionCount = ref(false)
|
||||
|
||||
const currentAssets = computed(() =>
|
||||
activeTab.value === 'input' ? inputAssets : outputAssets
|
||||
)
|
||||
const loading = computed(() => currentAssets.value.loading.value)
|
||||
const error = computed(() => currentAssets.value.error.value)
|
||||
const mediaAssets = computed(() => currentAssets.value.media.value)
|
||||
|
||||
const galleryActiveIndex = ref(-1)
|
||||
const currentGalleryAssetId = ref<string | null>(null)
|
||||
|
||||
const folderAssets = ref<AssetItem[]>([])
|
||||
|
||||
const displayAssets = computed(() => {
|
||||
if (isInFolderView.value) {
|
||||
return folderAssets.value
|
||||
}
|
||||
return mediaAssets.value
|
||||
})
|
||||
|
||||
watch(displayAssets, (newAssets) => {
|
||||
if (currentGalleryAssetId.value && galleryActiveIndex.value !== -1) {
|
||||
const newIndex = newAssets.findIndex(
|
||||
(asset) => asset.id === currentGalleryAssetId.value
|
||||
)
|
||||
if (newIndex !== -1) {
|
||||
galleryActiveIndex.value = newIndex
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(galleryActiveIndex, (index) => {
|
||||
if (index === -1) {
|
||||
currentGalleryAssetId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const galleryItems = computed(() => {
|
||||
return displayAssets.value.map((asset) => {
|
||||
const mediaType = getMediaTypeFromFilename(asset.name)
|
||||
const resultItem = new ResultItemImpl({
|
||||
filename: asset.name,
|
||||
subfolder: '',
|
||||
type: 'output',
|
||||
nodeId: '0',
|
||||
mediaType: mediaType === 'image' ? 'images' : mediaType
|
||||
})
|
||||
|
||||
Object.defineProperty(resultItem, 'url', {
|
||||
get() {
|
||||
return asset.preview_url || ''
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
|
||||
return resultItem
|
||||
})
|
||||
})
|
||||
|
||||
// Add key property for VirtualGrid
|
||||
const mediaAssetsWithKey = computed(() => {
|
||||
return displayAssets.value.map((asset) => ({
|
||||
...asset,
|
||||
key: asset.id
|
||||
}))
|
||||
})
|
||||
|
||||
const refreshAssets = async () => {
|
||||
await currentAssets.value.fetchMediaList()
|
||||
if (error.value) {
|
||||
console.error('Failed to refresh assets:', error.value)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
activeTab,
|
||||
() => {
|
||||
clearSelection()
|
||||
void refreshAssets()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleAssetSelect = (asset: AssetItem) => {
|
||||
const index = displayAssets.value.findIndex((a) => a.id === asset.id)
|
||||
handleAssetClick(asset, index, displayAssets.value)
|
||||
}
|
||||
|
||||
const handleZoomClick = (asset: AssetItem) => {
|
||||
currentGalleryAssetId.value = asset.id
|
||||
const index = displayAssets.value.findIndex((a) => a.id === asset.id)
|
||||
if (index !== -1) {
|
||||
galleryActiveIndex.value = index
|
||||
}
|
||||
}
|
||||
|
||||
const enterFolderView = (asset: AssetItem) => {
|
||||
const metadata = getOutputAssetMetadata(asset.user_metadata)
|
||||
if (!metadata) {
|
||||
console.warn('Invalid output asset metadata')
|
||||
return
|
||||
}
|
||||
|
||||
const { promptId, allOutputs, executionTimeInSeconds } = metadata
|
||||
|
||||
if (!promptId || !Array.isArray(allOutputs) || allOutputs.length === 0) {
|
||||
console.warn('Missing required folder view data')
|
||||
return
|
||||
}
|
||||
|
||||
folderPromptId.value = promptId
|
||||
folderExecutionTime.value = executionTimeInSeconds
|
||||
|
||||
folderAssets.value = allOutputs.map((output) => ({
|
||||
id: `${output.nodeId}-${output.filename}`,
|
||||
name: output.filename,
|
||||
size: 0,
|
||||
created_at: asset.created_at,
|
||||
tags: ['output'],
|
||||
preview_url: output.url,
|
||||
user_metadata: {
|
||||
promptId,
|
||||
nodeId: output.nodeId,
|
||||
subfolder: output.subfolder,
|
||||
executionTimeInSeconds,
|
||||
workflow: metadata.workflow
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const exitFolderView = () => {
|
||||
folderPromptId.value = null
|
||||
folderExecutionTime.value = undefined
|
||||
folderAssets.value = []
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
activateSelection()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
deactivateSelection()
|
||||
})
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
clearSelection()
|
||||
isHoveringSelectionCount.value = false
|
||||
}
|
||||
|
||||
const copyJobId = async () => {
|
||||
if (folderPromptId.value) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(folderPromptId.value)
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('mediaAsset.jobIdToast.copied'),
|
||||
detail: t('mediaAsset.jobIdToast.jobIdCopied'),
|
||||
life: 2000
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('mediaAsset.jobIdToast.error'),
|
||||
detail: t('mediaAsset.jobIdToast.jobIdCopyFailed'),
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownloadSelected = () => {
|
||||
const selectedAssets = getSelectedAssets(displayAssets.value)
|
||||
downloadMultipleAssets(selectedAssets)
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
const handleDeleteSelected = async () => {
|
||||
const selectedAssets = getSelectedAssets(displayAssets.value)
|
||||
await deleteMultipleAssets(selectedAssets)
|
||||
clearSelection()
|
||||
}
|
||||
</script>
|
||||
@@ -16,7 +16,7 @@
|
||||
<ProgressSpinner
|
||||
v-if="isLoading"
|
||||
class="m-auto"
|
||||
aria-label="Loading help"
|
||||
:aria-label="$t('g.loading')"
|
||||
/>
|
||||
<!-- Markdown fetched successfully -->
|
||||
<div
|
||||
|
||||
48
src/components/tab/Tab.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<button
|
||||
:id="tabId"
|
||||
:class="tabClasses"
|
||||
role="tab"
|
||||
:aria-selected="isActive"
|
||||
:aria-controls="panelId"
|
||||
:tabindex="0"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { value, panelId } = defineProps<{
|
||||
value: string
|
||||
panelId?: string
|
||||
}>()
|
||||
|
||||
const currentValue = inject<Ref<string>>('tabs-value')
|
||||
const updateValue = inject<(value: string) => void>('tabs-update')
|
||||
|
||||
const tabId = computed(() => `tab-${value}`)
|
||||
const isActive = computed(() => currentValue?.value === value)
|
||||
|
||||
const tabClasses = computed(() => {
|
||||
return cn(
|
||||
// Base styles from TextButton
|
||||
'flex items-center justify-center shrink-0',
|
||||
'px-2.5 py-2 text-sm rounded-lg cursor-pointer transition-all duration-200',
|
||||
'outline-hidden border-none',
|
||||
// State styles with semantic tokens
|
||||
isActive.value
|
||||
? 'bg-interface-menu-component-surface-hovered text-text-primary text-bold'
|
||||
: 'bg-transparent text-text-secondary hover:bg-button-hover-surface focus:bg-button-hover-surface'
|
||||
)
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
updateValue?.(value)
|
||||
}
|
||||
</script>
|
||||
153
src/components/tab/TabList.stories.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Tab from './Tab.vue'
|
||||
import TabList from './TabList.vue'
|
||||
|
||||
const meta: Meta<typeof TabList> = {
|
||||
title: 'Components/Tab/TabList',
|
||||
component: TabList,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
modelValue: {
|
||||
control: 'text',
|
||||
description: 'The currently selected tab value'
|
||||
},
|
||||
'onUpdate:modelValue': { action: 'update:modelValue' }
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
components: { TabList, Tab },
|
||||
setup() {
|
||||
const activeTab = ref(args.modelValue || 'tab1')
|
||||
return { activeTab }
|
||||
},
|
||||
template: `
|
||||
<TabList v-model="activeTab">
|
||||
<Tab value="tab1">Tab 1</Tab>
|
||||
<Tab value="tab2">Tab 2</Tab>
|
||||
<Tab value="tab3">Tab 3</Tab>
|
||||
</TabList>
|
||||
<div class="mt-4 p-4 border rounded">
|
||||
Selected tab: {{ activeTab }}
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
modelValue: 'tab1'
|
||||
}
|
||||
}
|
||||
|
||||
export const ManyTabs: Story = {
|
||||
render: () => ({
|
||||
components: { TabList, Tab },
|
||||
setup() {
|
||||
const activeTab = ref('tab1')
|
||||
return { activeTab }
|
||||
},
|
||||
template: `
|
||||
<TabList v-model="activeTab">
|
||||
<Tab value="tab1">Dashboard</Tab>
|
||||
<Tab value="tab2">Analytics</Tab>
|
||||
<Tab value="tab3">Reports</Tab>
|
||||
<Tab value="tab4">Settings</Tab>
|
||||
<Tab value="tab5">Profile</Tab>
|
||||
</TabList>
|
||||
<div class="mt-4 p-4 border rounded">
|
||||
Selected tab: {{ activeTab }}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const WithIcons: Story = {
|
||||
render: () => ({
|
||||
components: { TabList, Tab },
|
||||
setup() {
|
||||
const activeTab = ref('home')
|
||||
return { activeTab }
|
||||
},
|
||||
template: `
|
||||
<TabList v-model="activeTab">
|
||||
<Tab value="home">
|
||||
<i class="pi pi-home mr-2"></i>
|
||||
Home
|
||||
</Tab>
|
||||
<Tab value="users">
|
||||
<i class="pi pi-users mr-2"></i>
|
||||
Users
|
||||
</Tab>
|
||||
<Tab value="settings">
|
||||
<i class="pi pi-cog mr-2"></i>
|
||||
Settings
|
||||
</Tab>
|
||||
</TabList>
|
||||
<div class="mt-4 p-4 border rounded">
|
||||
Selected tab: {{ activeTab }}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const LongLabels: Story = {
|
||||
render: () => ({
|
||||
components: { TabList, Tab },
|
||||
setup() {
|
||||
const activeTab = ref('overview')
|
||||
return { activeTab }
|
||||
},
|
||||
template: `
|
||||
<TabList v-model="activeTab">
|
||||
<Tab value="overview">Project Overview</Tab>
|
||||
<Tab value="documentation">Documentation & Guides</Tab>
|
||||
<Tab value="deployment">Deployment Settings</Tab>
|
||||
<Tab value="monitoring">Monitoring & Analytics</Tab>
|
||||
</TabList>
|
||||
<div class="mt-4 p-4 border rounded">
|
||||
Selected tab: {{ activeTab }}
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
|
||||
export const Interactive: Story = {
|
||||
render: () => ({
|
||||
components: { TabList, Tab },
|
||||
setup() {
|
||||
const activeTab = ref('input')
|
||||
const handleTabChange = (value: string) => {
|
||||
console.log('Tab changed to:', value)
|
||||
}
|
||||
return { activeTab, handleTabChange }
|
||||
},
|
||||
template: `
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold mb-2">Example: Media Assets</h3>
|
||||
<TabList v-model="activeTab" @update:model-value="handleTabChange">
|
||||
<Tab value="input">Imported</Tab>
|
||||
<Tab value="output">Generated</Tab>
|
||||
</TabList>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<div v-if="activeTab === 'input'">
|
||||
<p>Showing imported assets...</p>
|
||||
</div>
|
||||
<div v-else-if="activeTab === 'output'">
|
||||
<p>Showing generated assets...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600">
|
||||
Current tab value: <code>{{ activeTab }}</code>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
17
src/components/tab/TabList.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div role="tablist" class="flex w-full items-center gap-2 pb-1">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide } from 'vue'
|
||||
|
||||
const modelValue = defineModel<string>({ required: true })
|
||||
|
||||
// Provide for child Tab components
|
||||
provide('tabs-value', modelValue)
|
||||
provide('tabs-update', (value: string) => {
|
||||
modelValue.value = value
|
||||
})
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
class="user-profile-button p-1"
|
||||
severity="secondary"
|
||||
text
|
||||
aria-label="user profile"
|
||||
:aria-label="$t('g.currentUser')"
|
||||
@click="popover?.toggle($event)"
|
||||
>
|
||||
<div class="flex items-center rounded-full bg-(--p-content-background)">
|
||||
|
||||
@@ -69,6 +69,22 @@ vi.mock('@/services/dialogService', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the firebaseAuthStore
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ Authorization: 'Bearer mock-token' })
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the useSubscription composable
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: vi.fn(() => ({
|
||||
isActiveSubscription: vi.fn().mockReturnValue(true)
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock UserAvatar component
|
||||
vi.mock('@/components/common/UserAvatar.vue', () => ({
|
||||
default: {
|
||||
|
||||
@@ -67,7 +67,11 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<UserCredit text-class="text-2xl" />
|
||||
<Button :label="$t('credits.topUp.topUp')" @click="handleTopUp" />
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
:label="$t('credits.topUp.topUp')"
|
||||
@click="handleTopUp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,6 +86,7 @@ import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -92,6 +97,7 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const dialogService = useDialogService()
|
||||
const { isActiveSubscription } = useSubscription()
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
dialogService.showSettingsDialog('user')
|
||||
|
||||
@@ -160,8 +160,8 @@ describe('TopbarBadge', () => {
|
||||
'full'
|
||||
)
|
||||
|
||||
expect(wrapper.find('.bg-warning-100').exists()).toBe(true)
|
||||
expect(wrapper.find('.text-warning-100').exists()).toBe(true)
|
||||
expect(wrapper.find('.bg-gold-600').exists()).toBe(true)
|
||||
expect(wrapper.find('.text-gold-600').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('uses default error icon for error variant', () => {
|
||||
|
||||
@@ -162,7 +162,7 @@ const labelClasses = computed(() => {
|
||||
case 'error':
|
||||
return 'bg-danger-100 text-white'
|
||||
case 'warning':
|
||||
return 'bg-warning-100 text-black'
|
||||
return 'bg-gold-600 text-black'
|
||||
case 'info':
|
||||
default:
|
||||
return 'bg-white text-black'
|
||||
@@ -174,10 +174,10 @@ const textClasses = computed(() => {
|
||||
case 'error':
|
||||
return 'text-danger-100'
|
||||
case 'warning':
|
||||
return 'text-warning-100'
|
||||
return 'text-gold-600'
|
||||
case 'info':
|
||||
default:
|
||||
return 'text-slate-100'
|
||||
return 'text-text-primary'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -205,7 +205,7 @@ const dotClasses = computed(() => {
|
||||
case 'error':
|
||||
return 'bg-danger-100'
|
||||
case 'warning':
|
||||
return 'bg-warning-100'
|
||||
return 'bg-gold-600'
|
||||
case 'info':
|
||||
default:
|
||||
return 'bg-slate-100'
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
|
||||
<template #header-right-area>
|
||||
<div class="flex gap-2">
|
||||
<IconTextButton type="primary" label="Upload Model" @click="() => {}">
|
||||
<IconTextButton
|
||||
type="primary"
|
||||
:label="$t('g.upload')"
|
||||
@click="() => {}"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--upload]" />
|
||||
</template>
|
||||
|
||||
@@ -133,7 +133,7 @@ const toggleRightPanel = () => {
|
||||
const layoutClasses = cn(
|
||||
'base-widget-layout',
|
||||
'rounded-2xl overflow-hidden relative',
|
||||
'bg-gray-50 dark-theme:bg-gray-800'
|
||||
'bg-gray-50 dark-theme:bg-smoke-800'
|
||||
)
|
||||
|
||||
const rightPanelButtonClasses = computed(() => {
|
||||
@@ -150,7 +150,7 @@ const closeButtonClasses = cn(
|
||||
|
||||
const mainContainerClasses = cn(
|
||||
'flex-1 flex',
|
||||
'bg-gray-100 dark-theme:bg-neutral-900'
|
||||
'bg-smoke-100 dark-theme:bg-neutral-900'
|
||||
)
|
||||
|
||||
const headerClasses = cn(
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
class="flex cursor-pointer items-center gap-2 rounded-md px-4 py-3 text-sm transition-colors"
|
||||
:class="
|
||||
active
|
||||
? 'bg-gray-400 dark-theme:bg-charcoal-300 text-neutral'
|
||||
: 'text-neutral hover:bg-gray-100 dark-theme:hover:bg-charcoal-300'
|
||||
? 'bg-smoke-400 dark-theme:bg-charcoal-300 text-neutral'
|
||||
: 'text-neutral hover:bg-smoke-100 dark-theme:hover:bg-charcoal-300'
|
||||
"
|
||||
role="button"
|
||||
@click="onClick"
|
||||
|
||||
@@ -4,13 +4,12 @@ import { shallowRef, watch } from 'vue'
|
||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
|
||||
import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync'
|
||||
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
|
||||
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
|
||||
import { app as comfyApp } from '@/scripts/app'
|
||||
|
||||
function useVueNodeLifecycleIndividual() {
|
||||
@@ -21,8 +20,6 @@ function useVueNodeLifecycleIndividual() {
|
||||
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
||||
|
||||
const { startSync } = useLayoutSync()
|
||||
const linkSyncManager = useLinkLayoutSync()
|
||||
const slotSyncManager = useSlotLayoutSync()
|
||||
|
||||
const initializeNodeManager = () => {
|
||||
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
|
||||
@@ -62,10 +59,6 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
||||
startSync(canvasStore.canvas)
|
||||
|
||||
if (comfyApp.canvas) {
|
||||
linkSyncManager.start(comfyApp.canvas)
|
||||
}
|
||||
}
|
||||
|
||||
const disposeNodeManagerAndSyncs = () => {
|
||||
@@ -77,8 +70,6 @@ function useVueNodeLifecycleIndividual() {
|
||||
/* empty */
|
||||
}
|
||||
nodeManager.value = null
|
||||
|
||||
linkSyncManager.stop()
|
||||
}
|
||||
|
||||
// Watch for Vue nodes enabled state changes
|
||||
@@ -87,6 +78,7 @@ function useVueNodeLifecycleIndividual() {
|
||||
(enabled) => {
|
||||
if (enabled) {
|
||||
initializeNodeManager()
|
||||
ensureCorrectLayoutScale()
|
||||
} else {
|
||||
disposeNodeManagerAndSyncs()
|
||||
}
|
||||
@@ -96,25 +88,14 @@ function useVueNodeLifecycleIndividual() {
|
||||
|
||||
// Consolidated watch for slot layout sync management
|
||||
watch(
|
||||
[() => canvasStore.canvas, () => shouldRenderVueNodes.value],
|
||||
([canvas, vueMode], [, oldVueMode]) => {
|
||||
() => shouldRenderVueNodes.value,
|
||||
(vueMode, oldVueMode) => {
|
||||
const modeChanged = vueMode !== oldVueMode
|
||||
|
||||
// Clear stale slot layouts when switching modes
|
||||
if (modeChanged) {
|
||||
layoutStore.clearAllSlotLayouts()
|
||||
}
|
||||
|
||||
// Switching to Vue
|
||||
if (vueMode) {
|
||||
slotSyncManager.stop()
|
||||
}
|
||||
|
||||
// Switching to LG
|
||||
const shouldRun = Boolean(canvas?.graph) && !vueMode
|
||||
if (shouldRun && canvas) {
|
||||
slotSyncManager.attemptStart(canvas as LGraphCanvas)
|
||||
}
|
||||
},
|
||||
{ immediate: true, flush: 'sync' }
|
||||
)
|
||||
@@ -152,8 +133,6 @@ function useVueNodeLifecycleIndividual() {
|
||||
nodeManager.value.cleanup()
|
||||
nodeManager.value = null
|
||||
}
|
||||
slotSyncManager.stop()
|
||||
linkSyncManager.stop()
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import type { ResultItemType } from '@/schemas/apiSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useAssetsStore } from '@/stores/assetsStore'
|
||||
|
||||
const PASTED_IMAGE_EXPIRY_MS = 2000
|
||||
|
||||
@@ -37,6 +38,13 @@ const uploadFile = async (
|
||||
}
|
||||
|
||||
const data = await resp.json()
|
||||
|
||||
// Update AssetsStore input assets when files are uploaded to input folder
|
||||
if (formFields.type === 'input' || (!formFields.type && !isPasted)) {
|
||||
const assetsStore = useAssetsStore()
|
||||
await assetsStore.updateInputs()
|
||||
}
|
||||
|
||||
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name
|
||||
}
|
||||
|
||||
|
||||
16
src/composables/sidebarTabs/useAssetsSidebarTab.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { markRaw } from 'vue'
|
||||
|
||||
import AssetsSidebarTab from '@/components/sidebar/tabs/AssetsSidebarTab.vue'
|
||||
import type { SidebarTabExtension } from '@/types/extensionTypes'
|
||||
|
||||
export const useAssetsSidebarTab = (): SidebarTabExtension => {
|
||||
return {
|
||||
id: 'assets',
|
||||
icon: 'icon-[comfy--image-ai-edit]',
|
||||
title: 'sideToolbar.assets',
|
||||
tooltip: 'sideToolbar.assets',
|
||||
label: 'sideToolbar.labels.assets',
|
||||
component: markRaw(AssetsSidebarTab),
|
||||
type: 'vue'
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { st, te } from '@/i18n'
|
||||
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
|
||||
import type {
|
||||
IContextMenuOptions,
|
||||
IContextMenuValue,
|
||||
@@ -6,18 +7,40 @@ import type {
|
||||
IWidget
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { app } from '@/scripts/app'
|
||||
import { normalizeI18nKey } from '@/utils/formatUtil'
|
||||
|
||||
/**
|
||||
* Add translation for litegraph context menu.
|
||||
*/
|
||||
export const useContextMenuTranslation = () => {
|
||||
const f = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
// Install compatibility layer BEFORE any extensions load
|
||||
legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')
|
||||
|
||||
const { getCanvasMenuOptions } = LGraphCanvas.prototype
|
||||
const getCanvasCenterMenuOptions = function (
|
||||
this: LGraphCanvas,
|
||||
...args: Parameters<typeof f>
|
||||
...args: Parameters<typeof getCanvasMenuOptions>
|
||||
) {
|
||||
const res = f.apply(this, args) as ReturnType<typeof f>
|
||||
const res: IContextMenuValue[] = getCanvasMenuOptions.apply(this, args)
|
||||
|
||||
// Add items from new extension API
|
||||
const newApiItems = app.collectCanvasMenuItems(this)
|
||||
for (const item of newApiItems) {
|
||||
res.push(item)
|
||||
}
|
||||
|
||||
// Add legacy monkey-patched items
|
||||
const legacyItems = legacyMenuCompat.extractLegacyItems(
|
||||
'getCanvasMenuOptions',
|
||||
this,
|
||||
...args
|
||||
)
|
||||
for (const item of legacyItems) {
|
||||
res.push(item)
|
||||
}
|
||||
|
||||
// Translate all items
|
||||
for (const item of res) {
|
||||
if (item?.content) {
|
||||
item.content = st(`contextMenu.${item.content}`, item.content)
|
||||
@@ -28,6 +51,33 @@ export const useContextMenuTranslation = () => {
|
||||
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = getCanvasCenterMenuOptions
|
||||
|
||||
legacyMenuCompat.registerWrapper(
|
||||
'getCanvasMenuOptions',
|
||||
getCanvasCenterMenuOptions,
|
||||
getCanvasMenuOptions,
|
||||
LGraphCanvas.prototype
|
||||
)
|
||||
|
||||
// Wrap getNodeMenuOptions to add new API items
|
||||
const nodeMenuFn = LGraphCanvas.prototype.getNodeMenuOptions
|
||||
const getNodeMenuOptionsWithExtensions = function (
|
||||
this: LGraphCanvas,
|
||||
...args: Parameters<typeof nodeMenuFn>
|
||||
) {
|
||||
const res = nodeMenuFn.apply(this, args)
|
||||
|
||||
// Add items from new extension API
|
||||
const node = args[0]
|
||||
const newApiItems = app.collectNodeMenuItems(node)
|
||||
for (const item of newApiItems) {
|
||||
res.push(item)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
LGraphCanvas.prototype.getNodeMenuOptions = getNodeMenuOptionsWithExtensions
|
||||
|
||||
function translateMenus(
|
||||
values: readonly (IContextMenuValue | string | null)[] | undefined,
|
||||
options: IContextMenuOptions
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import type { Point } from '@/lib/litegraph/src/litegraph'
|
||||
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
|
||||
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { SUPPORT_URL } from '@/platform/support/config'
|
||||
@@ -63,6 +64,8 @@ import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTyp
|
||||
|
||||
import { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelectorDialog'
|
||||
|
||||
const { isActiveSubscription, showSubscriptionDialog } = useSubscription()
|
||||
|
||||
const moveSelectedNodesVersionAdded = '1.22.2'
|
||||
|
||||
export function useCoreCommands(): ComfyCommand[] {
|
||||
@@ -453,6 +456,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
versionAdded: '1.3.7',
|
||||
category: 'essentials' as const,
|
||||
function: async () => {
|
||||
if (!isActiveSubscription.value) {
|
||||
showSubscriptionDialog()
|
||||
return
|
||||
}
|
||||
|
||||
const batchCount = useQueueSettingsStore().batchCount
|
||||
|
||||
if (isCloud) {
|
||||
@@ -469,6 +477,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
versionAdded: '1.3.7',
|
||||
category: 'essentials' as const,
|
||||
function: async () => {
|
||||
if (!isActiveSubscription.value) {
|
||||
showSubscriptionDialog()
|
||||
return
|
||||
}
|
||||
|
||||
const batchCount = useQueueSettingsStore().batchCount
|
||||
|
||||
if (isCloud) {
|
||||
@@ -484,6 +497,11 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Queue Selected Output Nodes',
|
||||
versionAdded: '1.19.6',
|
||||
function: async () => {
|
||||
if (!isActiveSubscription.value) {
|
||||
showSubscriptionDialog()
|
||||
return
|
||||
}
|
||||
|
||||
const batchCount = useQueueSettingsStore().batchCount
|
||||
const selectedNodes = getSelectedNodes()
|
||||
const selectedOutputNodes = filterOutputNodes(selectedNodes)
|
||||
|
||||
@@ -255,7 +255,7 @@ onBeforeUnmount(() => {
|
||||
{{ $t('subgraphStore.shown') }}
|
||||
</div>
|
||||
<a
|
||||
class="cursor-pointer text-right text-[11px] font-normal text-blue-100"
|
||||
class="cursor-pointer text-right text-[11px] font-normal text-azure-600"
|
||||
@click.stop="hideAll"
|
||||
>
|
||||
{{ $t('subgraphStore.hideAll') }}</a
|
||||
@@ -280,7 +280,7 @@ onBeforeUnmount(() => {
|
||||
{{ $t('subgraphStore.hidden') }}
|
||||
</div>
|
||||
<a
|
||||
class="cursor-pointer text-right text-[11px] font-normal text-blue-100"
|
||||
class="cursor-pointer text-right text-[11px] font-normal text-azure-600"
|
||||
@click.stop="showAll"
|
||||
>
|
||||
{{ $t('subgraphStore.showAll') }}</a
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
|
||||
import { t } from '@/i18n'
|
||||
import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
type ExecutableLGraphNode,
|
||||
type ExecutionId,
|
||||
LGraphCanvas,
|
||||
LGraphNode,
|
||||
LiteGraph,
|
||||
SubgraphNode
|
||||
@@ -1630,57 +1630,6 @@ export class GroupNodeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
function addConvertToGroupOptions() {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function addConvertOption(options, index) {
|
||||
const selected = Object.values(app.canvas.selected_nodes ?? {})
|
||||
const disabled =
|
||||
selected.length < 2 ||
|
||||
selected.find((n) => GroupNodeHandler.isGroupNode(n))
|
||||
options.splice(index, null, {
|
||||
content: `Convert to Group Node (Deprecated)`,
|
||||
disabled,
|
||||
callback: convertSelectedNodesToGroupNode
|
||||
})
|
||||
}
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
function addManageOption(options, index) {
|
||||
const groups = app.graph.extra?.groupNodes
|
||||
const disabled = !groups || !Object.keys(groups).length
|
||||
options.splice(index, null, {
|
||||
content: `Manage Group Nodes`,
|
||||
disabled,
|
||||
callback: () => manageGroupNodes()
|
||||
})
|
||||
}
|
||||
|
||||
// Add to canvas
|
||||
const getCanvasMenuOptions = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const options = getCanvasMenuOptions.apply(this, arguments)
|
||||
const index = options.findIndex((o) => o?.content === 'Add Group')
|
||||
const insertAt = index === -1 ? options.length - 1 : index + 2
|
||||
addConvertOption(options, insertAt)
|
||||
addManageOption(options, insertAt + 1)
|
||||
return options
|
||||
}
|
||||
|
||||
// Add to nodes
|
||||
const getNodeMenuOptions = LGraphCanvas.prototype.getNodeMenuOptions
|
||||
LGraphCanvas.prototype.getNodeMenuOptions = function (node) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const options = getNodeMenuOptions.apply(this, arguments)
|
||||
if (!GroupNodeHandler.isGroupNode(node)) {
|
||||
const index = options.findIndex((o) => o?.content === 'Properties')
|
||||
const insertAt = index === -1 ? options.length - 1 : index
|
||||
addConvertOption(options, insertAt)
|
||||
}
|
||||
return options
|
||||
}
|
||||
}
|
||||
|
||||
const replaceLegacySeparators = (nodes: ComfyNode[]): void => {
|
||||
for (const node of nodes) {
|
||||
if (typeof node.type === 'string' && node.type.startsWith('workflow/')) {
|
||||
@@ -1718,6 +1667,9 @@ async function convertSelectedNodesToGroupNode() {
|
||||
return await GroupNodeHandler.fromNodes(nodes)
|
||||
}
|
||||
|
||||
const convertDisabled = (selected: LGraphNode[]) =>
|
||||
selected.length < 2 || !!selected.find((n) => GroupNodeHandler.isGroupNode(n))
|
||||
|
||||
function ungroupSelectedGroupNodes() {
|
||||
const nodes = Object.values(app.canvas.selected_nodes ?? {})
|
||||
for (const node of nodes) {
|
||||
@@ -1776,8 +1728,46 @@ const ext: ComfyExtension = {
|
||||
}
|
||||
}
|
||||
],
|
||||
setup() {
|
||||
addConvertToGroupOptions()
|
||||
|
||||
getCanvasMenuItems(canvas): IContextMenuValue[] {
|
||||
const items: IContextMenuValue[] = []
|
||||
const selected = Object.values(canvas.selected_nodes ?? {})
|
||||
const convertEnabled = !convertDisabled(selected)
|
||||
|
||||
items.push({
|
||||
content: `Convert to Group Node (Deprecated)`,
|
||||
disabled: !convertEnabled,
|
||||
// @ts-expect-error fixme ts strict error - async callback
|
||||
callback: () => convertSelectedNodesToGroupNode()
|
||||
})
|
||||
|
||||
const groups = canvas.graph?.extra?.groupNodes
|
||||
const manageDisabled = !groups || !Object.keys(groups).length
|
||||
items.push({
|
||||
content: `Manage Group Nodes`,
|
||||
disabled: manageDisabled,
|
||||
callback: () => manageGroupNodes()
|
||||
})
|
||||
|
||||
return items
|
||||
},
|
||||
|
||||
getNodeMenuItems(node): IContextMenuValue[] {
|
||||
if (GroupNodeHandler.isGroupNode(node)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const selected = Object.values(app.canvas.selected_nodes ?? {})
|
||||
const convertEnabled = !convertDisabled(selected)
|
||||
|
||||
return [
|
||||
{
|
||||
content: `Convert to Group Node (Deprecated)`,
|
||||
disabled: !convertEnabled,
|
||||
// @ts-expect-error fixme ts strict error - async callback
|
||||
callback: () => convertSelectedNodesToGroupNode()
|
||||
}
|
||||
]
|
||||
},
|
||||
async beforeConfigureGraph(
|
||||
graphData: ComfyWorkflowJSON,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import type { Positionable } from '@/lib/litegraph/src/interfaces'
|
||||
import { LGraphGroup } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
IContextMenuValue,
|
||||
Positionable
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import {
|
||||
LGraphCanvas,
|
||||
LGraphGroup,
|
||||
type LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
|
||||
import { app } from '../../scripts/app'
|
||||
|
||||
@@ -16,220 +22,218 @@ function addNodesToGroup(group: LGraphGroup, items: Iterable<Positionable>) {
|
||||
group.resizeTo([...group.children, ...items], padding)
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
const ext: ComfyExtension = {
|
||||
name: 'Comfy.GroupOptions',
|
||||
setup() {
|
||||
const orig = LGraphCanvas.prototype.getCanvasMenuOptions
|
||||
// graph_mouse
|
||||
LGraphCanvas.prototype.getCanvasMenuOptions = function (
|
||||
this: LGraphCanvas
|
||||
) {
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const options = orig.apply(this, arguments)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const group = this.graph.getGroupOnPos(
|
||||
this.graph_mouse[0],
|
||||
this.graph_mouse[1]
|
||||
)
|
||||
if (!group) {
|
||||
if (this.selectedItems.size > 0) {
|
||||
options.push({
|
||||
content: 'Add Group For Selected Nodes',
|
||||
callback: () => {
|
||||
const group = new LGraphGroup()
|
||||
addNodesToGroup(group, this.selectedItems)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.graph.add(group)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.graph.change()
|
||||
|
||||
group.recomputeInsideNodes()
|
||||
getCanvasMenuItems(canvas: LGraphCanvas): IContextMenuValue[] {
|
||||
const items: IContextMenuValue[] = []
|
||||
|
||||
// @ts-expect-error fixme ts strict error
|
||||
const group = canvas.graph.getGroupOnPos(
|
||||
canvas.graph_mouse[0],
|
||||
canvas.graph_mouse[1]
|
||||
)
|
||||
|
||||
if (!group) {
|
||||
if (canvas.selectedItems.size > 0) {
|
||||
items.push({
|
||||
content: 'Add Group For Selected Nodes',
|
||||
callback: () => {
|
||||
const group = new LGraphGroup()
|
||||
addNodesToGroup(group, canvas.selectedItems)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvas.graph.add(group)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvas.graph.change()
|
||||
|
||||
group.recomputeInsideNodes()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
|
||||
group.recomputeInsideNodes()
|
||||
const nodesInGroup = group.nodes
|
||||
|
||||
items.push({
|
||||
content: 'Add Selected Nodes To Group',
|
||||
disabled: !canvas.selectedItems?.size,
|
||||
callback: () => {
|
||||
addNodesToGroup(group, canvas.selectedItems)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvas.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
// No nodes in group, return default options
|
||||
if (nodesInGroup.length === 0) {
|
||||
return items
|
||||
} else {
|
||||
// Add a separator between the default options and the group options
|
||||
// @ts-expect-error fixme ts strict error
|
||||
items.push(null)
|
||||
}
|
||||
|
||||
// Check if all nodes are the same mode
|
||||
let allNodesAreSameMode = true
|
||||
for (let i = 1; i < nodesInGroup.length; i++) {
|
||||
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
|
||||
allNodesAreSameMode = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
items.push({
|
||||
content: 'Fit Group To Nodes',
|
||||
callback: () => {
|
||||
group.recomputeInsideNodes()
|
||||
const padding = useSettingStore().get(
|
||||
'Comfy.GroupSelectedNodes.Padding'
|
||||
)
|
||||
group.resizeTo(group.children, padding)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvas.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
items.push({
|
||||
content: 'Select Nodes',
|
||||
callback: () => {
|
||||
canvas.selectNodes(nodesInGroup)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
canvas.graph.change()
|
||||
canvas.canvas.focus()
|
||||
}
|
||||
})
|
||||
|
||||
// Modes
|
||||
// 0: Always
|
||||
// 1: On Event
|
||||
// 2: Never
|
||||
// 3: On Trigger
|
||||
// 4: Bypass
|
||||
// If all nodes are the same mode, add a menu option to change the mode
|
||||
if (allNodesAreSameMode) {
|
||||
const mode = nodesInGroup[0].mode
|
||||
switch (mode) {
|
||||
case 0:
|
||||
// All nodes are always, option to disable, and bypass
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
items.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
// All nodes are never, option to enable, and bypass
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
items.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 4:
|
||||
// All nodes are bypass, option to enable, and disable
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
default:
|
||||
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
items.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// Group nodes aren't recomputed until the group is moved, this ensures the nodes are up-to-date
|
||||
group.recomputeInsideNodes()
|
||||
const nodesInGroup = group.nodes
|
||||
|
||||
options.push({
|
||||
content: 'Add Selected Nodes To Group',
|
||||
disabled: !this.selectedItems?.size,
|
||||
callback: () => {
|
||||
addNodesToGroup(group, this.selectedItems)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.graph.change()
|
||||
}
|
||||
})
|
||||
|
||||
// No nodes in group, return default options
|
||||
if (nodesInGroup.length === 0) {
|
||||
return options
|
||||
} else {
|
||||
// Add a separator between the default options and the group options
|
||||
// @ts-expect-error fixme ts strict error
|
||||
options.push(null)
|
||||
}
|
||||
|
||||
// Check if all nodes are the same mode
|
||||
let allNodesAreSameMode = true
|
||||
for (let i = 1; i < nodesInGroup.length; i++) {
|
||||
if (nodesInGroup[i].mode !== nodesInGroup[0].mode) {
|
||||
allNodesAreSameMode = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
options.push({
|
||||
content: 'Fit Group To Nodes',
|
||||
} else {
|
||||
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
group.recomputeInsideNodes()
|
||||
const padding = useSettingStore().get(
|
||||
'Comfy.GroupSelectedNodes.Padding'
|
||||
)
|
||||
group.resizeTo(group.children, padding)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.graph.change()
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
options.push({
|
||||
content: 'Select Nodes',
|
||||
items.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
this.selectNodes(nodesInGroup)
|
||||
// @ts-expect-error fixme ts strict error
|
||||
this.graph.change()
|
||||
this.canvas.focus()
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Modes
|
||||
// 0: Always
|
||||
// 1: On Event
|
||||
// 2: Never
|
||||
// 3: On Trigger
|
||||
// 4: Bypass
|
||||
// If all nodes are the same mode, add a menu option to change the mode
|
||||
if (allNodesAreSameMode) {
|
||||
const mode = nodesInGroup[0].mode
|
||||
switch (mode) {
|
||||
case 0:
|
||||
// All nodes are always, option to disable, and bypass
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 2:
|
||||
// All nodes are never, option to enable, and bypass
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
case 4:
|
||||
// All nodes are bypass, option to enable, and disable
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
default:
|
||||
// All nodes are On Trigger or On Event(Or other?), option to disable, set to always, or bypass
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
break
|
||||
items.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Nodes are not all the same mode, add a menu option to change the mode to always, never, or bypass
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Always',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: 'Set Group Nodes to Never',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 2)
|
||||
}
|
||||
}
|
||||
})
|
||||
options.push({
|
||||
content: 'Bypass Group Nodes',
|
||||
callback: () => {
|
||||
for (const node of nodesInGroup) {
|
||||
setNodeMode(node, 4)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app.registerExtension(ext)
|
||||
|
||||
131
src/extensions/core/maskeditor/CanvasHistory.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { MaskEditorDialog } from './MaskEditorDialog'
|
||||
import type { MessageBroker } from './managers/MessageBroker'
|
||||
|
||||
export class CanvasHistory {
|
||||
// @ts-expect-error unused variable
|
||||
private maskEditor!: MaskEditorDialog
|
||||
private messageBroker!: MessageBroker
|
||||
|
||||
private canvas!: HTMLCanvasElement
|
||||
private ctx!: CanvasRenderingContext2D
|
||||
private rgbCanvas!: HTMLCanvasElement
|
||||
private rgbCtx!: CanvasRenderingContext2D
|
||||
private states: { mask: ImageData; rgb: ImageData }[] = []
|
||||
private currentStateIndex: number = -1
|
||||
private maxStates: number = 20
|
||||
private initialized: boolean = false
|
||||
|
||||
constructor(maskEditor: MaskEditorDialog, maxStates = 20) {
|
||||
this.maskEditor = maskEditor
|
||||
this.messageBroker = maskEditor.getMessageBroker()
|
||||
this.maxStates = maxStates
|
||||
this.createListeners()
|
||||
}
|
||||
|
||||
private async pullCanvas() {
|
||||
this.canvas = await this.messageBroker.pull('maskCanvas')
|
||||
this.ctx = await this.messageBroker.pull('maskCtx')
|
||||
this.rgbCanvas = await this.messageBroker.pull('rgbCanvas')
|
||||
this.rgbCtx = await this.messageBroker.pull('rgbCtx')
|
||||
}
|
||||
|
||||
private createListeners() {
|
||||
this.messageBroker.subscribe('saveState', () => this.saveState())
|
||||
this.messageBroker.subscribe('undo', () => this.undo())
|
||||
this.messageBroker.subscribe('redo', () => this.redo())
|
||||
}
|
||||
|
||||
clearStates() {
|
||||
this.states = []
|
||||
this.currentStateIndex = -1
|
||||
this.initialized = false
|
||||
}
|
||||
|
||||
async saveInitialState() {
|
||||
await this.pullCanvas()
|
||||
if (
|
||||
!this.canvas.width ||
|
||||
!this.canvas.height ||
|
||||
!this.rgbCanvas.width ||
|
||||
!this.rgbCanvas.height
|
||||
) {
|
||||
// Canvas not ready yet, defer initialization
|
||||
requestAnimationFrame(() => this.saveInitialState())
|
||||
return
|
||||
}
|
||||
|
||||
this.clearStates()
|
||||
const maskState = this.ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
)
|
||||
const rgbState = this.rgbCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.rgbCanvas.width,
|
||||
this.rgbCanvas.height
|
||||
)
|
||||
this.states.push({ mask: maskState, rgb: rgbState })
|
||||
this.currentStateIndex = 0
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
saveState() {
|
||||
// Ensure we have an initial state
|
||||
if (!this.initialized || this.currentStateIndex === -1) {
|
||||
this.saveInitialState()
|
||||
return
|
||||
}
|
||||
|
||||
this.states = this.states.slice(0, this.currentStateIndex + 1)
|
||||
const maskState = this.ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.canvas.width,
|
||||
this.canvas.height
|
||||
)
|
||||
const rgbState = this.rgbCtx.getImageData(
|
||||
0,
|
||||
0,
|
||||
this.rgbCanvas.width,
|
||||
this.rgbCanvas.height
|
||||
)
|
||||
this.states.push({ mask: maskState, rgb: rgbState })
|
||||
this.currentStateIndex++
|
||||
|
||||
if (this.states.length > this.maxStates) {
|
||||
this.states.shift()
|
||||
this.currentStateIndex--
|
||||
}
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (this.states.length > 1 && this.currentStateIndex > 0) {
|
||||
this.currentStateIndex--
|
||||
this.restoreState(this.states[this.currentStateIndex])
|
||||
} else {
|
||||
alert('No more undo states available')
|
||||
}
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (
|
||||
this.states.length > 1 &&
|
||||
this.currentStateIndex < this.states.length - 1
|
||||
) {
|
||||
this.currentStateIndex++
|
||||
this.restoreState(this.states[this.currentStateIndex])
|
||||
} else {
|
||||
alert('No more redo states available')
|
||||
}
|
||||
}
|
||||
|
||||
restoreState(state: { mask: ImageData; rgb: ImageData }) {
|
||||
if (state && this.initialized) {
|
||||
this.ctx.putImageData(state.mask, 0, 0)
|
||||
this.rgbCtx.putImageData(state.rgb, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||