Compare commits

..

13 Commits

Author SHA1 Message Date
bymyself
92793177bd fix: Replace eslint-plugin-tailwindcss with v4-compatible plugin
The eslint-plugin-tailwindcss@4.0.0-beta.0 plugin is incompatible with
Tailwind v4 as it tries to access removed v3 internal files, causing:
"Cannot find module 'tailwindcss/dist/lib/setupContextUtils.js'"

This replaces it with eslint-plugin-tailwind-v4@1.0.13 which is
specifically designed for Tailwind v4 compatibility.
2025-10-27 12:46:10 -07:00
Christian Byrne
234fc3433c use prompt id rather than queue index for history reconciliation (#6327)
## Summary

Changed history reconciliation to use `promptId` instead of `queueIndex`
to support cloud environments that don't have queue indices.

Refactored into pure functions with expressive naming.

## Changes

- Use `promptId` (unique UUID) for reconciliation instead of
`queueIndex` (not available in cloud)
- Extract `reconcileHistoryWithServer` with helpers: `extractPromptIds`,
`isAddedAfter`, `sortNewestFirst`
- Added 34 tests covering reconciliation edge cases, sorting, and limits

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6327-use-prompt-id-rather-than-queue-index-for-history-reconciliation-2996d73d3650818e9471dbeb53cc23bb)
by [Unito](https://www.unito.io)
2025-10-26 23:58:29 -07:00
Christian Byrne
0a957fb2ac ci: leave comment and abort early if commit already exists on target branch in backport workflow (#6326)
Follow up on https://github.com/Comfy-Org/ComfyUI_frontend/pull/6317:
Fixes confusing "merge conflicts detected" message when commit already
exists on target branch (e.g., PRs #6294 and #6307).

## Changes

- Added check to detect if merge commit already exists on target branch
before attempting cherry-pick
- New failure reason `already-exists` for this case
- Clear comment message: "Commit already exists on branch, no backport
needed" instead of confusing "Merge conflicts detected" with empty file
list

## Before

When a commit already existed on the target branch, users would see:
> @user Backport to `core/1.30` failed: Merge conflicts detected.
> Please manually cherry-pick commit `abc123` to the `core/1.30` branch.
> <details><summary>Conflicting files</summary>
> 
> </details>

This was confusing because there were no actual conflicts - the commit
was already present.

## After

Users now see:
> @user Commit `abc123` already exists on branch `core/1.30`. No
backport needed.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6326-ci-leave-comment-and-abort-early-if-commit-already-exists-on-target-branch-in-backport-w-2996d73d36508167aff3e6783e832c74)
by [Unito](https://www.unito.io)
2025-10-26 23:37:22 -07:00
Christian Byrne
28a6089a94 ci: automate cloud release branch tagging (#6321)
Changes the RC minor version branch release automation to create paired
`core/x.y` and `cloud/x.y` branches whenever a release bump merges.

Then changes the backport workflow to accept labels that match those
branch names directly, allowing engineers to route fixes to either OSS
or cloud release lines without extra labels or artifacts.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6321-ci-automate-cloud-release-branch-tagging-2996d73d365081b0b036ebd3f088354b)
by [Unito](https://www.unito.io)
2025-10-26 22:47:15 -07:00
Christian Byrne
298b3c629b ci: update automated backport workflow to skip if backport already in-flight or completed (#6317)
Updates the backport workflow so it filters out branches that have
already been backported, allowing new labels to trigger fresh
cherry-picks without reprocessing completed targets.

Also adds an automatic cleanup step that removes the `needs-backport`
trigger label once a run succeeds, aligning its behavior with the other
label-driven automations.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6317-ci-update-automated-backport-workflow-to-skip-if-backport-already-in-flight-or-completed-2996d73d36508113b90df43b1f68344f)
by [Unito](https://www.unito.io)
2025-10-26 22:14:30 -07:00
sno
20a1a9eda2 [bugfix] fix @intlify/vue-i18n/no-raw-text linting errors (#6280)
## Summary

This PR fixes all @intlify/vue-i18n/no-raw-text linting errors
identified in #5625 by replacing raw text strings with proper i18n
translation function calls.

## Changes

Fixed i18n linting errors in the following files:
- `src/components/widget/SampleModelSelector.vue` - "Upload Model" →
`$t('g.upload')`
- `src/components/topbar/CurrentUserButton.vue` - "user profile" →
`$t('g.currentUser')`
- `src/components/sidebar/tabs/nodeLibrary/NodeHelpPage.vue` - "Loading
help" → `$t('g.loading')`
- `src/components/sidebar/SidebarShortcutsToggleButton.vue` -
"shortcuts.shortcuts" → `$t('shortcuts.shortcuts')`
- `src/components/sidebar/SidebarLogoutIcon.vue` - "sideToolbar.logout"
→ `$t('sideToolbar.logout')`
- `src/components/sidebar/SidebarHelpCenterIcon.vue` - "menu.help" →
`$t('menu.help')`
- `src/components/sidebar/SidebarBottomPanelToggleButton.vue` -
"sideToolbar.labels.console" → `$t('sideToolbar.labels.console')`
- `src/components/load3d/controls/viewer/ViewerCameraControls.vue` -
"fov" → `t('load3d.fov')`
- `src/components/helpcenter/HelpCenterMenuContent.vue` - "Help Center
Menu" and "Recent releases" → `$t()` calls

All raw text strings have been replaced with appropriate i18n
translation keys that already exist in `src/locales/en/main.json`.

## Related Issue

Fixes errors reported in CI job:
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/18705105609/job/53341658467?pr=5625

This PR aims to help #5625 pass CI/CD checks.

## Test Plan

- All i18n linting errors should be resolved
- No functionality changes - only proper use of i18n system
- Existing translation keys are used from the locale files

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6280-bugfix-fix-intlify-vue-i18n-no-raw-text-linting-errors-2976d73d365081369b43de01486fb409)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-10-26 22:12:45 -07:00
Christian Byrne
ca45b2c4d6 [bugfix] use raw template ID for workflow_name in telemetry tracking (#6320)
## Summary

Fixes `trackTemplate` calls to use raw template ID instead of translated
workflow name for analytics tracking.

## Problem

When users load templates with non-English locale, the `workflow_name`
field was being tracked in their language (e.g., "默认工作流" for Chinese
users) instead of English. This makes statistical analysis difficult.

## Changes

- Updated both `trackTemplate` calls in `useTemplateWorkflows.ts` to use
raw `id` instead of translated `workflowName`
- The translated name is still used for display purposes in
`loadGraphData`

**Before:**
```typescript
useTelemetry()?.trackTemplate({
  workflow_name: workflowName, // Could be "默认工作流"
  template_source: sourceModule
})
```

**After:**
```typescript
useTelemetry()?.trackTemplate({
  workflow_name: id, // Always "default"
  template_source: sourceModule
})
```

## Testing

- [x] Type checking passes
- [x] Verified translated names still used for display

## Related

Part of comprehensive i18n audit for telemetry tracking. Related to
template metadata English tracking in PR #6304.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6320-bugfix-use-raw-template-ID-for-workflow_name-in-telemetry-tracking-2996d73d365081d8aef3fa2529a10247)
by [Unito](https://www.unito.io)
2025-10-26 22:02:27 -07:00
Christian Byrne
1453afad12 refactor: rename size report workflows to match naming pattern of other workflows (#6322)
## Summary

Changes gh workflow names and job names to match the unified naming
style of the other workflows.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6322-refactor-rename-size-report-workflows-to-match-naming-pattern-of-other-workflows-2996d73d365081c79cfcdfcb9013c3e1)
by [Unito](https://www.unito.io)
2025-10-26 21:51:38 -07:00
Christian Byrne
5de1a91f02 ci: fix "size report" (bundle size) markdown table comment formatting (#6318)
## Summary

Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/6136 by
adding empty lines between html and markdown in the bundle size report
comments.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6318-ci-fix-size-report-bundle-size-markdown-table-comment-formatting-2996d73d36508104872be56c238339a8)
by [Unito](https://www.unito.io)
2025-10-26 21:06:59 -07:00
Christian Byrne
b3eee54abb fix: claude-review bails on cancelled and skipped checks (#6316)
## Summary

Expands the wait step in `pr-claude-review.yaml` so the
`lewagon/wait-on-check-action` accepts every terminal GitHub conclusion
instead of failing on outcomes like cancelled or failure. After the wait
completes, the `check-status` script still inspects those check runs and
only marks `should-proceed=true` when the tracked jobs finished
successfully.

Fixes what happened here:
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/18828179272/job/53714488799?pr=6298
2025-10-26 20:39:18 -07:00
Christian Byrne
1ee33673ab [bugfix] fix survey properties mapping to match actual survey data (#6314)
## Summary

Updates `SurveyResponses` interface to match actual survey fields 1-to-1
based on analytics requirements.

## Changes

- Remove `team_size` property (no corresponding survey question exists)
- Change `use_case` to `useCase` to match actual survey field name
- Remove `intended_use` property (doesn't exist in actual survey)
- Add `making` field array for content generation type tracking (video,
image, etc.)

This fixes a Mixpanel analytics issue where survey properties weren't
mapping 1-to-1 with actual survey questions, making statistical analysis
difficult.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6314-bugfix-fix-survey-properties-mapping-to-match-actual-survey-data-2996d73d36508158a335e6b73e3e14ef)
by [Unito](https://www.unito.io)
2025-10-26 18:04:44 -07:00
Terry Jia
9f5245dc80 [refactor] Split mask editor into smaller files (#6308)
## Summary

Split mask editor structure into smaller files

## Changes

This PR is a prerequisite step for [the issue - refactoring the mask
editor using
Vue](https://github.com/Comfy-Org/ComfyUI_frontend/issues/5956). It
splits the current monolithic maskeditor.ts (about 5700 lines) into
separate files; otherwise, the original single file would be very
difficult to analyze or maintain.

This PR itself does not introduce any Vue, nor should it have any
functional changes or modifications. It's purely a code-level split,
with all related files placed in the maskeditor folder.

The original maskeditor.ts has been renamed to maskeditor.ts.backup for
future reference.

## Review Focus

Since this PR is only for splitting purposes, all logic remains
consistent with the original. Therefore, for any reviewer: any code
logic improvements should happen in the subsequent Vue-based
refactoring, not in this PR.

Following this PR, I will perform a Vue-based refactoring of the mask
editor to align with the frontend's overall Vue architecture and provide
better cloud-related support.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6308-refactor-Split-mask-editor-into-smaller-files-2986d73d36508131937dd43e465a47bd)
by [Unito](https://www.unito.io)
2025-10-26 17:34:36 -07:00
Comfy Org PR Bot
cacd7e3251 1.31.0 (#6313)
Minor version increment to 1.31.0

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6313-1-31-0-2986d73d365081e9ac6ccb91b4ddad0a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-10-26 17:20:40 -07:00
245 changed files with 7681 additions and 6715 deletions

View File

@@ -1,4 +1,4 @@
name: size data
name: "CI: Size Data"
on:
push:

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -5,6 +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'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types available for this plugin
import tailwindV4Plugin from 'eslint-plugin-tailwind-v4'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
import { defineConfig } from 'eslint/config'
@@ -33,7 +36,11 @@ const settings = {
],
noWarnOnMultipleProjects: true
})
]
],
tailwindcss: {
config: `${import.meta.dirname}/packages/design-system/src/css/style.css`,
functions: ['cn', 'clsx', 'tw']
}
} as const
const commonParserOptions = {
@@ -100,7 +107,8 @@ export default defineConfig([
plugins: {
'unused-imports': unusedImports,
// @ts-expect-error Bad types in the plugin
'@intlify/vue-i18n': pluginI18n
'@intlify/vue-i18n': pluginI18n,
'tailwind-v4': tailwindV4Plugin
},
rules: {
'@typescript-eslint/no-floating-promises': 'error',
@@ -120,6 +128,7 @@ export default defineConfig([
'import-x/no-relative-packages': 'error',
'unused-imports/no-unused-imports': 'error',
'no-console': ['error', { allow: ['warn', 'error'] }],
'tailwind-v4/no-custom-classname': 'off', // TODO: fix
'vue/no-v-html': 'off',
// Enforce dark-theme: instead of dark: prefix
'vue/no-restricted-class': ['error', '/^dark:/'],

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.30.4",
"version": "1.31.0",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -77,6 +77,7 @@
"eslint-plugin-import-x": "catalog:",
"eslint-plugin-prettier": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-tailwind-v4": "catalog:",
"eslint-plugin-unused-imports": "catalog:",
"eslint-plugin-vue": "catalog:",
"fs-extra": "^11.2.0",

View File

@@ -9,18 +9,29 @@
@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);
--text-xxxs: 0.5625rem;
--text-xxxs--line-height: calc(1 / 0.5625);
/* Spacing */
--spacing-xs: 8px;
/* Font Families */
--font-inter: 'Inter', sans-serif;
/* Palette Colors */
--color-charcoal-100: #55565e;
--color-charcoal-100: #171718;
--color-charcoal-200: #494a50;
--color-charcoal-300: #3c3d42;
--color-charcoal-400: #313235;
@@ -31,60 +42,43 @@
--color-neutral-550: #636363;
--color-ash-300: #bbbbbb;
--color-ash-500: #828282;
--color-ash-800: #444444;
--color-stone-100: #828282;
--color-stone-200: #444444;
--color-stone-300: #bbbbbb;
--color-ivory-100: #fdfbfa;
--color-ivory-200: #faf9f5;
--color-ivory-300: #f0eee6;
--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-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-sand-100: #e1ded5;
--color-sand-200: #fff7d5;
--color-sand-200: #d6cfc2;
--color-sand-300: #888682;
--color-sand-400: #eed7ac;
--color-pure-black: #000000;
--color-pure-white: #ffffff;
--color-slate-100: #9c9eab;
--color-slate-200: #9fa2bd;
--color-slate-300: #5b5e7d;
--color-white: #ffffff;
--color-black: #000000;
--color-electric-400: #f0ff41;
--color-sapphire-700: #172dd7;
--color-brand-yellow: var(--color-electric-400);
--color-brand-blue: var(--color-sapphire-700);
--color-azure-300: #78bae9;
--color-azure-400: #31b9f4;
--color-azure-600: #0b8ce9;
--color-cobalt-800: #185a8b;
--color-jade-400: #47e469;
--color-jade-600: #00cd72;
--color-gold-400: #fcbf64;
--color-gold-500: #fdab34;
--color-gold-600: #fd9903;
--color-coral-500: #f75951;
--color-coral-600: #e04e48;
--color-coral-700: #b33a3a;
--color-magenta-300: #ceaac9;
--color-magenta-700: #6a246a;
--color-brand-yellow: #f0ff41;
--color-brand-blue: #172dd7;
--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;
@@ -96,31 +90,28 @@
--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-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-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3);
--color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15);
--color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1);
--color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4);
--color-alpha-charcoal-600-30: color-mix(
in srgb,
var(--color-charcoal-600) 30%,
transparent
);
--color-alpha-ash-500-20: color-mix(
--color-alpha-stone-100-20: color-mix(
in srgb,
var(--color-ash-500) 20%,
var(--color-stone-100) 20%,
transparent
);
--color-alpha-smoke-500-50: color-mix(
--color-alpha-gray-500-50: color-mix(
in srgb,
var(--color-smoke-500) 50%,
var(--color-gray-500) 50%,
transparent
);
--color-alpha-smoke-500-20: #c5c5c533;
--color-alpha-smoke-400-40: #d9d9d966;
--color-alpha-azure-600-30: #0b8ce94d;
--color-alpha-magenta-700-60: #6a246a99;
--color-alpha-magenta-300-60: #ceaac999;
/* PrimeVue pulled colors */
--color-muted: var(--p-text-muted-color);
@@ -154,8 +145,8 @@
--content-hover-bg: #adadad;
--content-hover-fg: #000;
--button-surface: var(--color-white);
--button-surface-contrast: var(--color-black);
--button-surface: var(--color-pure-white);
--button-surface-contrast: var(--color-pure-black);
/* Code styling colors for help menu*/
--code-text-color: rgb(0 122 255 / 1);
@@ -166,36 +157,31 @@
--accent-primary: var(--color-charcoal-700);
--backdrop: var(--color-white);
--button-hover-surface: var(--color-smoke-200);
--button-active-surface: var(--color-smoke-400);
--button-icon: var(--color-smoke-600);
--button-hover-surface: var(--color-gray-200);
--button-active-surface: var(--color-gray-400);
--button-icon: var(--color-gray-600);
--dialog-surface: var(--color-neutral-200);
--interface-menu-component-surface-hovered: var(--color-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);
--interface-menu-component-surface-hovered: var(--color-gray-200);
--interface-menu-component-surface-selected: var(--color-gray-400);
--interface-menu-keybind-surface-default: var(--color-gray-500);
--interface-panel-surface: var(--color-pure-white);
--interface-stroke: var(--color-gray-300);
--nav-background: var(--color-pure-white);
--node-border: var(--color-gray-300);
--node-component-border: var(--color-gray-400);
--node-component-disabled: var(--color-alpha-stone-100-20);
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-ash-800);
--node-component-header-icon: var(--color-stone-200);
--node-component-header-surface: var(--color-white);
--node-component-outline: var(--color-black);
--node-component-ring: rgb(from var(--color-smoke-500) r g b / 50%);
--node-component-ring: rgb(from var(--color-gray-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-ash-800);
--node-component-surface-highlight: var(--color-ash-500);
--node-component-surface-hovered: var(--color-smoke-200);
--node-component-slot-text: var(--color-stone-200);
--node-component-surface-highlight: var(--color-stone-100);
--node-component-surface-hovered: var(--color-gray-200);
--node-component-surface-selected: var(--color-charcoal-200);
--node-component-surface: var(--color-white);
--node-component-tooltip: var(--color-charcoal-700);
@@ -207,86 +193,40 @@
);
--node-component-widget-skeleton-surface: var(--color-zinc-300);
--node-divider: var(--color-sand-100);
--node-icon-disabled: var(--color-alpha-smoke-500-50);
--node-stroke: var(--color-smoke-400);
--node-icon-disabled: var(--color-alpha-gray-500-50);
--node-stroke: var(--color-gray-400);
--node-stroke-selected: var(--color-accent-primary);
--node-stroke-error: var(--color-error);
--node-stroke-executing: var(--color-azure-600);
--text-secondary: var(--color-ash-500);
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-stone-100);
--text-primary: var(--color-charcoal-700);
--input-surface: rgb(0 0 0 / 0.15);
/* Semantic tokens - light mode */
--muted-foreground: var(--color-charcoal-200);
--base-foreground: var(--color-charcoal-800);
--brand-yellow: var(--color-electric-400);
--brand-blue: var(--color-sapphire-700);
--secondary-background: var(--color-smoke-200);
--secondary-background-hover: var(--color-smoke-400);
--secondary-background-selected: var(--color-smoke-600);
--base-background: var(--color-white);
--primary-background: var(--color-azure-400);
--primary-background-hover: var(--color-cobalt-800);
--destructive-background: var(--color-coral-500);
--destructive-background-hover: var(--color-coral-600);
--inverted-background-hover: var(--color-charcoal-600);
--warning-background: var(--color-gold-400);
--warning-background-hover: var(--color-gold-500);
--border-default: var(--color-smoke-600);
--border-subtle: var(--color-smoke-400);
--muted-background: var(--color-smoke-700);
--accent-background: var(--color-smoke-800);
/* Default UI element color palette variables */
--palette-contrast-mix-color: #fff;
--palette-interface-panel-surface: var(--comfy-menu-bg);
--palette-interface-stroke: color-mix(in srgb, var(--interface-panel-surface) 75.5%, var(--contrast-mix-color));
--palette-interface-panel-box-shadow: 1px 1px 8px 0 rgb(0 0 0 / 0.4);
--palette-interface-panel-drop-shadow: 1px 1px 4px rgb(0 0 0 / 0.4);
--palette-interface-panel-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 92.5%, var(--contrast-mix-color));
--palette-interface-panel-selected-surface: color-mix(in srgb, var(--interface-panel-surface) 87.5%, var(--contrast-mix-color));
--palette-interface-button-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 82%, var(--contrast-mix-color));
}
.dark-theme {
--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);
--accent-primary: var(--color-pure-white);
--backdrop: var(--color-neutral-900);
--button-surface: var(--color-charcoal-600);
--button-surface-contrast: var(--color-white);
--button-surface-contrast: var(--color-pure-white);
--button-hover-surface: var(--color-charcoal-600);
--button-active-surface: var(--color-charcoal-600);
--button-icon: var(--color-smoke-800);
--button-icon: var(--color-gray-800);
--dialog-surface: var(--color-neutral-700);
--interface-menu-component-surface-hovered: var(--color-charcoal-400);
--interface-menu-component-surface-selected: var(--color-charcoal-300);
--interface-menu-keybind-surface-default: var(--color-charcoal-200);
--interface-panel-surface: var(--color-charcoal-800);
--interface-panel-surface: var(--color-charcoal-100);
--interface-stroke: var(--color-charcoal-400);
--nav-background: var(--color-charcoal-800);
--nav-background: var(--color-charcoal-100);
--node-border: var(--color-charcoal-500);
--node-component-border: var(--color-ash-800);
--node-component-border: var(--color-stone-200);
--node-component-border-error: var(--color-danger-100);
--node-component-border-executing: var(--color-blue-500);
--node-component-border-selected: var(--color-charcoal-200);
--node-component-header-icon: var(--color-slate-300);
--node-component-header-surface: var(--color-charcoal-800);
--node-component-outline: var(--color-white);
--node-component-ring: rgb(var(--color-smoke-500) / 20%);
--node-component-ring: rgb(var(--color-gray-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);
@@ -300,37 +240,14 @@
--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-ash-500-20);
--node-stroke: var(--color-ash-800);
--node-stroke-selected: var(--color-white);
--node-icon-disabled: var(--color-alpha-stone-100-20);
--node-stroke: var(--color-stone-200);
--node-stroke-selected: var(--color-pure-white);
--node-stroke-error: var(--color-error);
--node-stroke-executing: var(--color-azure-600);
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-slate-100);
--text-primary: var(--color-white);
--text-primary: var(--color-pure-white);
--input-surface: rgb(130 130 130 / 0.1);
/* Semantic tokens - dark mode */
--muted-foreground: var(--color-smoke-800);
--base-foreground: var(--color-white);
--brand-yellow: var(--color-electric-400);
--brand-blue: var(--color-sapphire-700);
--secondary-background: var(--color-charcoal-600);
--secondary-background-hover: var(--color-charcoal-400);
--secondary-background-selected: var(--color-charcoal-200);
--base-background: var(--color-charcoal-800);
--primary-background: var(--color-azure-600);
--primary-background-hover: var(--color-azure-400);
--destructive-background: var(--color-coral-700);
--destructive-background-hover: var(--color-coral-600);
--inverted-background-hover: var(--color-smoke-200);
--warning-background: var(--color-gold-600);
--warning-background-hover: var(--color-gold-500);
--border-default: var(--color-charcoal-200);
--border-subtle: var(--color-charcoal-300);
--muted-background: var(--color-charcoal-100);
--accent-background: var(--color-charcoal-100);
}
@theme inline {
@@ -351,14 +268,6 @@
--interface-menu-keybind-surface-default
);
--color-interface-panel-surface: var(--interface-panel-surface);
--color-interface-panel-hover-surface: var(--interface-panel-hover-surface);
--color-interface-panel-selected-surface: var(
--interface-panel-selected-surface
);
--color-interface-button-hover-surface: var(
--interface-button-hover-surface
);
--color-comfy-menu-bg: var(--comfy-menu-bg);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
--color-node-border: var(--node-border);
@@ -403,27 +312,6 @@
--color-text-secondary: var(--text-secondary);
--color-text-primary: var(--text-primary);
--color-input-surface: var(--input-surface);
/* Semantic tokens */
--color-base-foreground: var(--base-foreground);
--color-muted-foreground: var(--muted-foreground);
--color-base-background: var(--base-background);
--color-secondary-background: var(--secondary-background);
--color-secondary-background-hover: var(--secondary-background-hover);
--color-secondary-background-selected: var(--secondary-background-selected);
--color-primary-background: var(--primary-background);
--color-primary-background-hover: var(--primary-background-hover);
--color-destructive-background: var(--destructive-background);
--color-destructive-background-hover: var(--destructive-background-hover);
--color-inverted-background-hover: var(--inverted-background-hover);
--color-warning-background: var(--warning-background);
--color-warning-background-hover: var(--warning-background-hover);
--color-border-default: var(--border-default);
--color-border-subtle: var(--border-subtle);
--color-muted-background: var(--muted-background);
--color-accent-background: var(--accent-background);
--color-brand-yellow: var(--brand-yellow);
--color-brand-blue: var(--brand-blue);
}
@custom-variant dark-theme {
@@ -442,6 +330,7 @@
}
}
/* ===================== Scrollbar Utilities (Tailwind) =====================
Usage: Add `scrollbar-custom` class to scrollable containers.
The scrollbar styling adapts to light/dark theme automatically.

View File

@@ -4,13 +4,6 @@ import { addDynamicIconSelectors } from '@iconify/tailwind'
import { iconCollection } from './src/iconCollection'
export default {
theme: {
extend: {
boxShadow: {
interface: 'var(--interface-panel-box-shadow)'
}
}
},
plugins: [
addDynamicIconSelectors({
iconSets: {

View File

@@ -474,13 +474,3 @@ export function formatDuration(milliseconds: number): string {
return parts.join(' ')
}
/**
* Truncates a filename for display purposes.
* Currently returns the filename as-is since truncation is handled by CSS.
* @param filename The filename to truncate
* @returns The display-ready filename
*/
export function truncateFilename(filename: string): string {
return filename
}

View File

@@ -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-smoke-500')"
:class="cn('px-4 py-2', isActive ? 'bg-blue-500' : 'bg-gray-500')"
/>
```

24
pnpm-lock.yaml generated
View File

@@ -150,6 +150,9 @@ catalogs:
eslint-plugin-storybook:
specifier: ^9.1.6
version: 9.1.6
eslint-plugin-tailwind-v4:
specifier: ^1.0.13
version: 1.0.13
eslint-plugin-unused-imports:
specifier: ^4.2.0
version: 4.2.0
@@ -561,6 +564,9 @@ 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-tailwind-v4:
specifier: 'catalog:'
version: 1.0.13(eslint@9.35.0(jiti@2.4.2))
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))
@@ -4685,6 +4691,12 @@ packages:
eslint: '>=8'
storybook: ^9.1.6
eslint-plugin-tailwind-v4@1.0.13:
resolution: {integrity: sha512-IXCv/Miug48/mXaipqdxypsozGvbgLz/8C0ZMCf0WFW1FBXJz4xPaRJFjbsY9vu1Vbf4+xfyLqz8inbQyc6MZw==}
engines: {node: '>=14.0.0'}
peerDependencies:
eslint: '>=8.0.0'
eslint-plugin-unused-imports@4.2.0:
resolution: {integrity: sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==}
peerDependencies:
@@ -7600,8 +7612,8 @@ packages:
vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
vue-component-type-helpers@3.1.3:
resolution: {integrity: sha512-V1dOD8XYfstOKCnXbWyEJIrhTBMwSyNjv271L1Jlx9ExpNlCSuqOs3OdWrGJ0V544zXufKbcYabi/o+gK8lyfQ==}
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==}
@@ -10285,7 +10297,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.3
vue-component-type-helpers: 3.1.2
'@swc/helpers@0.5.17':
dependencies:
@@ -12355,6 +12367,10 @@ snapshots:
- supports-color
- typescript
eslint-plugin-tailwind-v4@1.0.13(eslint@9.35.0(jiti@2.4.2)):
dependencies:
eslint: 9.35.0(jiti@2.4.2)
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)
@@ -15943,7 +15959,7 @@ snapshots:
vue-component-type-helpers@3.1.1: {}
vue-component-type-helpers@3.1.3: {}
vue-component-type-helpers@3.1.2: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:

View File

@@ -51,6 +51,7 @@ catalog:
eslint-plugin-import-x: ^4.16.1
eslint-plugin-prettier: ^5.5.4
eslint-plugin-storybook: ^9.1.6
eslint-plugin-tailwind-v4: ^1.0.13
eslint-plugin-unused-imports: ^4.2.0
eslint-plugin-vue: ^10.4.0
firebase: ^11.6.0
@@ -62,7 +63,6 @@ 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
@@ -98,6 +98,7 @@ 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

View File

@@ -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')
}

View File

@@ -22,7 +22,7 @@
},
"litegraph_base": {
"BACKGROUND_IMAGE": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=",
"CLEAR_BACKGROUND_COLOR": "#141414",
"CLEAR_BACKGROUND_COLOR": "#222",
"NODE_TITLE_COLOR": "#999",
"NODE_SELECTED_TITLE_COLOR": "#FFF",
"NODE_TEXT_SIZE": 14,
@@ -52,7 +52,7 @@
"comfy_base": {
"fg-color": "#fff",
"bg-color": "#202020",
"comfy-menu-bg": "#171718",
"comfy-menu-bg": "#353535",
"comfy-menu-secondary-bg": "#303030",
"comfy-input-bg": "#222",
"input-text": "#ddd",

View File

@@ -68,12 +68,7 @@
"content-fg": "#222",
"content-hover-bg": "#adadad",
"content-hover-fg": "#222",
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem",
"interface-panel-box-shadow": "1px 1px 8px 0 rgba(0, 0, 0, 0.2)",
"interface-panel-drop-shadow": "1px 1px 4px rgba(0, 0, 0, 0.4)",
"interface-panel-hover-surface": "var(--color-gray-200)",
"interface-panel-selected-surface": "color-mix(in srgb, var(--interface-panel-surface) 78%, var(--contrast-mix-color))",
"contrast-mix-color": "#000"
"bar-shadow": "rgba(16, 16, 16, 0.25) 0 0 0.5rem"
}
}
}

View File

@@ -42,7 +42,7 @@
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
<Splitter
class="splitter-overlay splitter-overlay-bottom mx-1 mb-1 flex-1"
class="splitter-overlay splitter-overlay-bottom mr-1 mb-1 ml-1 flex-1"
layout="vertical"
:pt:gutter="
'rounded-tl-lg rounded-tr-lg ' +

View File

@@ -1,7 +1,7 @@
<template>
<div
v-show="workspaceState.focusMode"
class="comfy-menu-hamburger no-drag right-0 top-0"
class="comfy-menu-hamburger no-drag top-0 right-0"
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"

View File

@@ -1,11 +1,11 @@
<template>
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-1">
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
class="actionbar-container pointer-events-auto mx-1 flex h-12 items-center rounded-lg px-2 shadow-md"
>
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
@@ -48,5 +48,6 @@ onMounted(() => {
<style scoped>
.actionbar-container {
background-color: var(--comfy-menu-bg);
border: 1px solid var(--p-panel-border-color);
}
</style>

View File

@@ -10,7 +10,7 @@
</div>
<Panel
class="z-1000 pointer-events-auto"
class="pointer-events-auto z-1000"
:style="style"
:class="panelClass"
:pt="{
@@ -18,7 +18,7 @@
content: { class: isDocked ? 'p-0' : 'p-1' }
}"
>
<div ref="panelRef" class="flex select-none items-center">
<div ref="panelRef" class="flex items-center select-none">
<span
ref="dragHandleRef"
:class="
@@ -258,9 +258,7 @@ const panelClass = computed(() =>
cn(
'actionbar pointer-events-auto z1000',
isDragging.value && 'select-none pointer-events-none',
isDocked.value
? 'p-0 static mr-2 border-none bg-transparent'
: 'fixed shadow-interface'
isDocked.value ? 'p-0 static mr-2 border-none bg-transparent' : 'fixed'
)
)
</script>

View File

@@ -7,7 +7,7 @@
class="flex flex-col"
>
<h3
class="subcategory-title text-surface-600 dark-theme:text-surface-400 mb-4 text-xs font-bold uppercase tracking-wide"
class="subcategory-title mb-4 text-xs font-bold tracking-wide text-surface-600 uppercase dark-theme:text-surface-400"
>
{{ getSubcategoryTitle(subcategory) }}
</h3>
@@ -16,7 +16,7 @@
<div
v-for="command in subcategoryCommands"
:key="command.id"
class="shortcut-item hover:bg-surface-100 dark-theme:hover:bg-surface-700 flex items-center justify-between rounded py-2 transition-colors duration-200"
class="shortcut-item flex items-center justify-between rounded py-2 transition-colors duration-200 hover:bg-surface-100 dark-theme:hover:bg-surface-700"
>
<div class="shortcut-info grow pr-4">
<div class="shortcut-name text-sm font-medium">
@@ -32,7 +32,7 @@
<span
v-for="key in command.keybinding!.combo.getKeySequences()"
:key="key"
class="key-badge bg-surface-200 dark-theme:bg-surface-600 min-w-6 rounded border px-2 py-1 text-center font-mono text-xs"
class="key-badge min-w-6 rounded border bg-surface-200 px-2 py-1 text-center font-mono text-xs dark-theme:bg-surface-600"
>
{{ formatKey(key) }}
</span>

View File

@@ -1,6 +1,9 @@
<template>
<div ref="rootEl" class="relative size-full overflow-hidden bg-neutral-900">
<div class="p-terminal size-full rounded-none p-2">
<div
ref="rootEl"
class="relative h-full w-full overflow-hidden bg-neutral-900"
>
<div class="p-terminal h-full w-full rounded-none p-2">
<div ref="terminalEl" class="terminal-host h-full" />
</div>
<Button

View File

@@ -1,5 +1,5 @@
<template>
<div class="size-full bg-transparent">
<div class="h-full w-full bg-transparent">
<p v-if="errorMessage" class="p-4 text-center">
{{ errorMessage }}
</p>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="subgraph-breadcrumb w-auto drop-shadow-[var(--interface-panel-drop-shadow)]"
class="subgraph-breadcrumb w-auto drop-shadow-md"
:class="{
'subgraph-breadcrumb-collapse': collapseTabs,
'subgraph-breadcrumb-overflow': overflowingTabs
@@ -201,8 +201,8 @@ onUpdated(() => {
:deep(.p-breadcrumb-separator),
:deep(.p-breadcrumb-item) {
@apply h-12;
border-top: 1px solid var(--interface-stroke);
border-bottom: 1px solid var(--interface-stroke);
border-top: 1px solid var(--p-panel-border-color);
border-bottom: 1px solid var(--p-panel-border-color);
background-color: var(--comfy-menu-bg);
}
@@ -214,7 +214,7 @@ onUpdated(() => {
@apply rounded-l-lg;
/* Then collapse the root workflow */
flex-shrink: 5000;
border-left: 1px solid var(--interface-stroke);
border-left: 1px solid var(--p-panel-border-color);
.p-breadcrumb-item-link {
padding-left: var(--p-breadcrumb-item-padding);
@@ -225,7 +225,7 @@ onUpdated(() => {
@apply rounded-r-lg;
/* Then collapse the active item */
flex-shrink: 1;
border-right: 1px solid var(--interface-stroke);
border-right: 1px solid var(--p-panel-border-color);
}
:deep(.p-breadcrumb-item-link:hover),

View File

@@ -38,7 +38,7 @@
v-if="isEditing"
ref="itemInputRef"
v-model="itemLabel"
class="z-10000 fixed p-2 text-[.8rem]"
class="fixed z-10000 px-2 py-2 text-[.8rem]"
@blur="inputBlur(false)"
@click.stop
@keydown.enter="inputBlur(true)"

View File

@@ -1,5 +1,5 @@
<template>
<div class="dark-theme:text-zinc-400 line-clamp-2 h-7 text-xs text-zinc-500">
<div class="line-clamp-2 h-7 text-xs text-zinc-500 dark-theme:text-zinc-400">
<slot></slot>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<div :class="topStyle">
<slot class="absolute left-0 top-0 size-full"></slot>
<slot class="absolute top-0 left-0 h-full w-full"></slot>
<div v-if="slots['top-left']" :class="slotClasses['top-left']">
<slot name="top-left"></slot>

View File

@@ -2,15 +2,15 @@
<div class="image-upload-wrapper">
<div class="flex items-center gap-2">
<div
class="preview-box flex size-16 items-center justify-center rounded border p-2"
:class="{ 'bg-smoke-100 dark-theme:bg-smoke-800': !modelValue }"
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 }"
>
<img
v-if="modelValue"
:src="modelValue"
class="max-h-full max-w-full object-contain"
/>
<i v-else class="pi pi-image text-smoke-400 text-xl" />
<i v-else class="pi pi-image text-xl text-gray-400" />
</div>
<div class="flex flex-col gap-2">

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="containerRef"
class="relative flex size-full items-center justify-center overflow-hidden"
class="relative flex h-full w-full items-center justify-center overflow-hidden"
:class="containerClass"
>
<Skeleton
@@ -23,7 +23,7 @@
/>
<div
v-if="hasError"
class="bg-surface-50 text-muted dark-theme:bg-surface-800 absolute inset-0 flex items-center justify-center"
class="absolute inset-0 flex items-center justify-center bg-surface-50 text-muted dark-theme:bg-surface-800"
>
<img
src="/assets/images/default-template.png"

View File

@@ -5,7 +5,7 @@
<div class="flex flex-col items-center">
<i :class="icon" style="font-size: 3rem; margin-bottom: 1rem" />
<h3>{{ title }}</h3>
<p :class="textClass" class="whitespace-pre-line text-center">
<p :class="textClass" class="text-center whitespace-pre-line">
{{ message }}
</p>
<Button

View File

@@ -56,7 +56,7 @@ describe('UserAvatar', () => {
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('renders with default icon when provided photo Url is null', () => {
@@ -67,7 +67,7 @@ describe('UserAvatar', () => {
const avatar = wrapper.findComponent(Avatar)
expect(avatar.exists()).toBe(true)
expect(avatar.props('image')).toBeNull()
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('falls back to icon when image fails to load', async () => {
@@ -82,7 +82,7 @@ describe('UserAvatar', () => {
avatar.vm.$emit('error')
await nextTick()
expect(avatar.props('icon')).toBe('icon-[lucide--user]')
expect(avatar.props('icon')).toBe('pi pi-user')
})
it('uses provided ariaLabel', () => {

View File

@@ -1,9 +1,7 @@
<template>
<Avatar
class="bg-interface-panel-selected-surface"
:image="photoUrl ?? undefined"
:icon="hasAvatar ? undefined : 'icon-[lucide--user]'"
:pt:icon:class="{ 'size-4': !hasAvatar }"
:icon="hasAvatar ? undefined : 'pi pi-user'"
shape="circle"
:aria-label="ariaLabel ?? $t('auth.login.userAvatar')"
@error="handleImageError"

View File

@@ -36,7 +36,7 @@
</template>
<template #contentFilter>
<div class="relative flex flex-wrap gap-2 px-6 pb-4 pt-2">
<div class="relative flex flex-wrap gap-2 px-6 pt-2 pb-4">
<!-- Model Filter -->
<MultiSelect
v-model="selectedModelObjects"
@@ -97,7 +97,7 @@
</div>
<div
v-if="!isLoading"
class="text-neutral px-6 pb-2 pt-4 text-2xl font-semibold"
class="text-neutral px-6 pt-4 pb-2 text-2xl font-semibold"
>
<span>
{{ pageTitle }}
@@ -111,7 +111,7 @@
v-if="!isLoading && filteredTemplates.length === 0"
class="flex h-64 flex-col items-center justify-center text-neutral-500"
>
<i class="icon-[lucide--search] mb-4 size-12 opacity-50" />
<i class="mb-4 icon-[lucide--search] h-12 w-12 opacity-50" />
<p class="mb-2 text-lg">
{{ $t('templateWorkflows.noResults', 'No templates found') }}
</p>
@@ -128,7 +128,7 @@
<!-- Title -->
<span
v-if="isLoading"
class="bg-dialog-surface inline-block h-8 w-48 animate-pulse rounded"
class="inline-block h-8 w-48 animate-pulse rounded bg-dialog-surface"
></span>
<!-- Template Cards Grid -->
@@ -144,12 +144,14 @@
size="compact"
variant="ghost"
rounded="lg"
class="hover:bg-base-background"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
>
<template #top>
<CardTop ratio="landscape">
<template #default>
<div class="bg-dialog-surface size-full animate-pulse"></div>
<div
class="h-full w-full animate-pulse bg-dialog-surface"
></div>
</template>
</CardTop>
</template>
@@ -157,10 +159,10 @@
<CardBottom>
<div class="px-4 py-3">
<div
class="bg-dialog-surface mb-2 h-6 animate-pulse rounded"
class="mb-2 h-6 animate-pulse rounded bg-dialog-surface"
></div>
<div
class="bg-dialog-surface h-4 animate-pulse rounded"
class="h-4 animate-pulse rounded bg-dialog-surface"
></div>
</div>
</CardBottom>
@@ -176,7 +178,7 @@
variant="ghost"
rounded="lg"
:data-testid="`template-workflow-${template.name}`"
class="hover:bg-base-background"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
@mouseenter="hoveredTemplate = template.name"
@mouseleave="hoveredTemplate = null"
@click="onLoadWorkflow(template)"
@@ -185,7 +187,9 @@
<CardTop ratio="square">
<template #default>
<!-- Template Thumbnail -->
<div class="relative size-full overflow-hidden rounded-lg">
<div
class="relative h-full w-full overflow-hidden rounded-lg"
>
<template v-if="template.mediaType === 'audio'">
<AudioThumbnail :src="getBaseThumbnailSrc(template)" />
</template>
@@ -248,7 +252,7 @@
</template>
<ProgressSpinner
v-if="loadingTemplate === template.name"
class="absolute inset-0 z-10 m-auto size-12"
class="absolute inset-0 z-10 m-auto h-12 w-12"
/>
</div>
</template>
@@ -285,7 +289,7 @@
<div class="flex justify-between gap-2">
<div class="flex-1">
<p
class="text-muted m-0 line-clamp-2 text-sm"
class="m-0 line-clamp-2 text-sm text-muted"
:title="getTemplateDescription(template)"
>
{{ getTemplateDescription(template) }}
@@ -319,12 +323,14 @@
size="compact"
variant="ghost"
rounded="lg"
class="hover:bg-base-background"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
>
<template #top>
<CardTop ratio="square">
<template #default>
<div class="bg-dialog-surface size-full animate-pulse"></div>
<div
class="h-full w-full animate-pulse bg-dialog-surface"
></div>
</template>
</CardTop>
</template>
@@ -332,10 +338,10 @@
<CardBottom>
<div class="px-4 py-3">
<div
class="bg-dialog-surface mb-2 h-6 animate-pulse rounded"
class="mb-2 h-6 animate-pulse rounded bg-dialog-surface"
></div>
<div
class="bg-dialog-surface h-4 animate-pulse rounded"
class="h-4 animate-pulse rounded bg-dialog-surface"
></div>
</div>
</CardBottom>
@@ -350,7 +356,7 @@
ref="loadTrigger"
class="mt-4 flex h-4 w-full items-center justify-center"
>
<div v-if="isLoadingMore" class="text-muted text-sm">
<div v-if="isLoadingMore" class="text-sm text-muted">
{{ $t('templateWorkflows.loadingMore', 'Loading more...') }}
</div>
</div>
@@ -358,7 +364,7 @@
<!-- Results Summary -->
<div
v-if="!isLoading"
class="dark-theme:text-neutral-400 mt-6 px-6 text-sm text-neutral-600"
class="mt-6 px-6 text-sm text-neutral-600 dark-theme:text-neutral-400"
>
{{
$t('templateWorkflows.resultsCount', {

View File

@@ -1,5 +1,5 @@
<template>
<div class="h-110 flex max-w-96 flex-col gap-4 p-2">
<div class="flex h-110 max-w-96 flex-col gap-4 p-2">
<div class="mb-2 text-2xl font-medium">
{{ t('apiNodesSignInDialog.title') }}
</div>

View File

@@ -30,7 +30,7 @@
<template v-if="reportOpen">
<Divider />
<ScrollPanel class="h-[400px] w-full max-w-[80vw]">
<pre class="whitespace-pre-wrap break-words">{{ reportContent }}</pre>
<pre class="break-words whitespace-pre-wrap">{{ reportContent }}</pre>
</ScrollPanel>
<Divider />
</template>

View File

@@ -3,7 +3,7 @@
v-if="hasMissingCoreNodes"
severity="info"
icon="pi pi-info-circle"
class="m-2"
class="mx-2 my-2"
:pt="{
root: { class: 'flex-col' },
text: { class: 'flex-1' }
@@ -25,7 +25,7 @@
class="ml-4"
>
<div
class="text-surface-600 dark-theme:text-surface-400 text-sm font-medium"
class="text-sm font-medium text-surface-600 dark-theme:text-surface-400"
>
{{
$t('loadWorkflowWarning.coreNodesFromVersion', {
@@ -33,7 +33,7 @@
})
}}
</div>
<div class="text-surface-500 dark-theme:text-surface-500 ml-4 text-sm">
<div class="ml-4 text-sm text-surface-500 dark-theme:text-surface-500">
{{ getUniqueNodeNames(nodes).join(', ') }}
</div>
</div>

View File

@@ -8,7 +8,7 @@
<template v-else>
<!-- Header -->
<div class="mb-8 flex flex-col gap-4">
<h1 class="my-0 text-2xl font-medium leading-normal">
<h1 class="my-0 text-2xl leading-normal font-medium">
{{ isSignIn ? t('auth.login.title') : t('auth.signup.title') }}
</h1>
<p class="my-0 text-base">
@@ -88,12 +88,12 @@
>
<img
src="/assets/images/comfy-logo-mono.svg"
class="mr-2 size-5"
class="mr-2 h-5 w-5"
:alt="$t('g.comfy')"
/>
{{ t('auth.login.useApiKey') }}
</Button>
<small class="text-muted text-center">
<small class="text-center text-muted">
{{ t('auth.apiKey.helpText') }}
<a
:href="`${COMFY_PLATFORM_BASE_URL}/login`"
@@ -115,7 +115,7 @@
</div>
<!-- Terms & Contact -->
<p class="text-muted mt-8 text-xs">
<p class="mt-8 text-xs text-muted">
{{ t('auth.login.termsText') }}
<a
href="https://www.comfy.org/terms-of-service"

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex w-96 flex-col gap-10 p-2">
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
<h1 class="my-0 text-2xl font-medium leading-normal">
<h1 class="my-0 text-2xl leading-normal font-medium">
{{ $t('credits.topUp.insufficientTitle') }}
</h1>
<p class="my-0 text-base">
@@ -12,7 +12,7 @@
<!-- Balance Section -->
<div class="flex items-center justify-between">
<div class="flex w-full flex-col gap-2">
<div class="text-muted text-base">
<div class="text-base text-muted">
{{ $t('credits.yourCreditBalance') }}
</div>
<div class="flex w-full items-center justify-between">
@@ -30,7 +30,7 @@
<!-- Amount Input Section -->
<div class="flex flex-col gap-2">
<span class="text-muted text-sm"
<span class="text-sm text-muted"
>{{ $t('credits.topUp.quickPurchase') }}:</span
>
<div class="grid grid-cols-[2fr_1fr] gap-2">

View File

@@ -21,7 +21,7 @@
/>
<span v-else class="text-xl">{{ amount }}</span>
</div>
<ProgressSpinner v-if="loading" class="size-8" />
<ProgressSpinner v-if="loading" class="h-8 w-8" />
<Button
v-else
:severity="preselected ? 'primary' : 'secondary'"

View File

@@ -8,7 +8,7 @@
<Divider />
<div class="flex flex-col gap-2">
<h3 class="text-muted text-sm font-medium">
<h3 class="text-sm font-medium text-muted">
{{ $t('credits.yourCreditBalance') }}
</h3>
<div class="flex items-center justify-between">
@@ -28,7 +28,7 @@
height="1rem"
class="text-xs"
/>
<div v-else-if="formattedLastUpdateTime" class="text-muted text-xs">
<div v-else-if="formattedLastUpdateTime" class="text-xs text-muted">
{{ $t('credits.lastUpdated') }}: {{ formattedLastUpdateTime }}
</div>
<Button
@@ -59,7 +59,7 @@
<Column field="title" :header="$t('g.name')">
<template #body="{ data }">
<div class="text-sm font-medium">{{ data.title }}</div>
<div class="text-muted text-xs">{{ data.timestamp }}</div>
<div class="text-xs text-muted">{{ data.timestamp }}</div>
</template>
</Column>
<Column field="amount" :header="$t('g.amount')">

View File

@@ -1,6 +1,6 @@
<template>
<TabPanel :value="props.value" class="size-full" :class="props.class">
<div class="flex size-full flex-col gap-2">
<TabPanel :value="props.value" class="h-full w-full" :class="props.class">
<div class="flex h-full w-full flex-col gap-2">
<slot name="header" />
<ScrollPanel class="h-0 grow pr-2">
<slot />

View File

@@ -50,7 +50,7 @@
<div class="font-semibold">
{{ data.params?.api_name || 'API' }}
</div>
<div class="text-smoke-400 text-sm">
<div class="text-sm text-gray-400">
{{ $t('credits.model') }}: {{ data.params?.model || '-' }}
</div>
</div>

View File

@@ -35,7 +35,7 @@
<h3 class="font-medium">
{{ $t('userSettings.provider') }}
</h3>
<div class="text-muted flex items-center gap-1">
<div class="flex items-center gap-1 text-muted">
<i :class="providerIcon" />
{{ providerName }}
<Button
@@ -54,7 +54,7 @@
<ProgressSpinner
v-if="loading"
class="mt-4 size-8"
class="mt-4 h-8 w-8"
style="--pc-spinner-color: #000"
/>
<div v-else class="mt-4 flex flex-col gap-2">
@@ -78,7 +78,7 @@
<!-- Login Section -->
<div v-else class="flex flex-col gap-4">
<p class="text-smoke-600">
<p class="text-gray-600">
{{ $t('auth.login.title') }}
</p>

View File

@@ -1,11 +1,11 @@
<template>
<div class="flex flex-col gap-6">
<div class="mb-8 flex flex-col gap-4">
<h1 class="my-0 text-2xl font-medium leading-normal">
<h1 class="my-0 text-2xl leading-normal font-medium">
{{ t('auth.apiKey.title') }}
</h1>
<div class="flex flex-col gap-2">
<p class="text-muted my-0 text-base">
<p class="my-0 text-base text-muted">
{{ t('auth.apiKey.description') }}
</p>
<a

View File

@@ -34,7 +34,7 @@
{{ t('auth.login.passwordLabel') }}
</label>
<span
class="text-muted cursor-pointer select-none text-base font-medium"
class="cursor-pointer text-base font-medium text-muted select-none"
:class="{
'text-link-disabled': !$form.email?.value || $form.email?.invalid
}"
@@ -60,7 +60,7 @@
</div>
<!-- Submit Button -->
<ProgressSpinner v-if="loading" class="size-8" />
<ProgressSpinner v-if="loading" class="h-8 w-8" />
<Button
v-else
type="submit"

View File

@@ -2,18 +2,18 @@
<Button
ref="buttonRef"
severity="secondary"
class="group h-8 rounded-none! bg-comfy-menu-bg p-0 transition-none! hover:rounded-lg! hover:bg-interface-button-hover-surface!"
class="group h-8 rounded-none! bg-interface-panel-surface p-0 transition-none! hover:rounded-lg! hover:bg-button-hover-surface!"
:style="buttonStyles"
@click="toggle"
>
<template #default>
<div class="flex items-center gap-1 pr-0.5">
<div
class="rounded-lg bg-interface-panel-selected-surface p-2 group-hover:bg-interface-button-hover-surface"
class="rounded-lg bg-button-active-surface p-2 group-hover:bg-button-hover-surface"
>
<i :class="currentModeIcon" class="block size-4" />
<i :class="currentModeIcon" class="block h-4 w-4" />
</div>
<i class="icon-[lucide--chevron-down] block size-4 pr-1.5" />
<i class="icon-[lucide--chevron-down] block h-4 w-4 pr-1.5" />
</div>
</template>
</Button>
@@ -29,27 +29,27 @@
>
<div class="flex flex-col gap-1">
<div
class="hover:bg-node-component-surface-hovered flex cursor-pointer items-center justify-between px-3 py-2 text-sm"
class="flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="setMode('select')"
>
<div class="flex items-center gap-2">
<i class="icon-[lucide--mouse-pointer-2] size-4" />
<i class="icon-[lucide--mouse-pointer-2] h-4 w-4" />
<span>{{ $t('graphCanvasMenu.select') }}</span>
</div>
<span class="text-text-primary text-[9px]">{{
<span class="text-[9px] text-text-primary">{{
unlockCommandText
}}</span>
</div>
<div
class="hover:bg-node-component-surface-hovered flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm"
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="setMode('hand')"
>
<div class="flex items-center gap-2">
<i class="icon-[lucide--hand] size-4" />
<i class="icon-[lucide--hand] h-4 w-4" />
<span>{{ $t('graphCanvasMenu.hand') }}</span>
</div>
<span class="text-text-primary text-[9px]">{{ lockCommandText }}</span>
<span class="text-[9px] text-text-primary">{{ lockCommandText }}</span>
</div>
</div>
</Popover>
@@ -114,7 +114,7 @@ const popoverPt = computed(() => ({
content: {
class: [
'mb-2 text-text-primary',
'shadow-lg border border-interface-stroke',
'shadow-lg border border-node-border',
'bg-nav-background',
'rounded-lg',
'p-2 px-3',

View File

@@ -5,12 +5,12 @@
<LiteGraphCanvasSplitterOverlay v-if="comfyAppReady">
<template v-if="showUI && workflowTabsPosition === 'Topbar'" #workflow-tabs>
<div
class="workflow-tabs-container h-9.5 pointer-events-auto relative w-full"
class="workflow-tabs-container pointer-events-auto relative h-9.5 w-full"
>
<!-- Native drag area for Electron -->
<div
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
class="app-drag fixed left-0 top-0 z-10 h-[var(--comfy-topbar-height)] w-full"
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
/>
<div class="flex h-full items-center">
<WorkflowTabs />
@@ -23,7 +23,7 @@
</template>
<template v-if="showUI" #side-bar-panel>
<div
class="sidebar-content-container size-full overflow-y-auto overflow-x-hidden"
class="sidebar-content-container h-full w-full overflow-x-hidden overflow-y-auto"
>
<ExtensionSlot v-if="activeSidebarTab" :extension="activeSidebarTab" />
</div>

View File

@@ -5,22 +5,20 @@
<!-- Backdrop -->
<div
v-if="hasActivePopup"
class="z-1200 fixed inset-0"
class="fixed inset-0 z-1200"
@click="hideModal"
></div>
<ButtonGroup
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-interface-stroke bg-comfy-menu-bg p-2"
:style="{
...stringifiedMinimapStyles.buttonGroupStyles
}"
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>
<CanvasModeSelector
:button-styles="stringifiedMinimapStyles.buttonStyles"
/>
<div class="bg-node-divider h-[27px] w-px self-center" />
<div class="h-[27px] w-[1px] self-center bg-node-divider" />
<Button
v-tooltip.top="fitViewTooltip"
@@ -28,11 +26,11 @@
icon="pi pi-expand"
:aria-label="fitViewTooltip"
:style="stringifiedMinimapStyles.buttonStyles"
class="h-8 w-8 bg-comfy-menu-bg p-0 hover:bg-interface-button-hover-surface!"
class="h-8 w-8 bg-interface-panel-surface p-0 hover:bg-button-hover-surface!"
@click="() => commandStore.execute('Comfy.Canvas.FitView')"
>
<template #icon>
<i class="icon-[lucide--focus] size-4" />
<i class="icon-[lucide--focus] h-4 w-4" />
</template>
</Button>
@@ -49,11 +47,11 @@
>
<span class="inline-flex items-center gap-1 px-2 text-xs">
<span>{{ canvasStore.appScalePercentage }}%</span>
<i class="icon-[lucide--chevron-down] size-4" />
<i class="icon-[lucide--chevron-down] h-4 w-4" />
</span>
</Button>
<div class="bg-node-divider h-[27px] w-px self-center" />
<div class="h-[27px] w-[1px] self-center bg-node-divider" />
<Button
ref="minimapButton"
@@ -66,7 +64,7 @@
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
>
<template #icon>
<i class="icon-[lucide--map] size-4" />
<i class="icon-[lucide--map] h-4 w-4" />
</template>
</Button>
@@ -87,7 +85,7 @@
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
>
<template #icon>
<i class="icon-[lucide--route-off] size-4" />
<i class="icon-[lucide--route-off] h-4 w-4" />
</template>
</Button>
</ButtonGroup>
@@ -165,18 +163,18 @@ const minimapCommandText = computed(() =>
// Computed properties for button classes and states
const zoomButtonClass = computed(() => [
'bg-comfy-menu-bg',
isModalVisible.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
'hover:bg-interface-button-hover-surface!',
'bg-interface-panel-surface',
isModalVisible.value ? 'not-active:bg-button-active-surface!' : '',
'hover:bg-button-hover-surface!',
'p-0',
'h-8',
'w-15'
])
const minimapButtonClass = computed(() => ({
'bg-comfy-menu-bg': true,
'hover:bg-interface-button-hover-surface!': true,
'not-active:bg-interface-panel-selected-surface!': settingStore.get(
'bg-interface-panel-surface': true,
'hover:bg-button-hover-surface!': true,
'not-active:bg-button-active-surface!': settingStore.get(
'Comfy.Minimap.Visible'
),
'p-0': true,
@@ -208,9 +206,9 @@ const linkVisibilityAriaLabel = computed(() =>
: t('graphCanvasMenu.hideLinks')
)
const linkVisibleClass = computed(() => [
'bg-comfy-menu-bg',
linkHidden.value ? 'not-active:bg-interface-panel-selected-surface!' : '',
'hover:bg-interface-button-hover-surface!',
'bg-interface-panel-surface',
linkHidden.value ? 'not-active:bg-button-active-surface!' : '',
'hover:bg-button-hover-surface!',
'p-0',
'w-8',
'h-8'

View File

@@ -2,12 +2,12 @@
<div
ref="toolboxRef"
style="transform: translate(var(--tb-x), var(--tb-y))"
class="pointer-events-none fixed left-0 top-0 z-40"
class="pointer-events-none fixed top-0 left-0 z-40"
>
<Transition name="slide-up">
<Panel
v-if="visible"
class="selection-toolbox border-interface-stroke bg-interface-panel-surface pointer-events-auto rounded-lg border"
class="selection-toolbox pointer-events-auto rounded-lg border border-interface-stroke bg-interface-panel-surface"
:pt="{
header: 'hidden',
content: 'p-2 h-12 flex flex-row gap-1'

View File

@@ -1,51 +1,51 @@
<template>
<div
v-if="visible"
class="z-1300 border-0! bg-inherit! absolute bottom-[62px] right-0 flex w-[250px] justify-center"
class="absolute right-0 bottom-[62px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
>
<div
class="w-4/5 rounded-lg border border-interface-stroke bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
class="w-4/5 rounded-lg border border-node-border bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"
:style="filteredMinimapStyles"
@click.stop
>
<div class="flex flex-col gap-1">
<div
class="hover:bg-node-component-surface-hovered flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm"
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@mousedown="startRepeat('Comfy.Canvas.ZoomIn')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<span class="font-medium">{{ $t('graphCanvasMenu.zoomIn') }}</span>
<span class="text-text-primary text-[9px]">{{
<span class="text-[9px] text-text-primary">{{
zoomInCommandText
}}</span>
</div>
<div
class="hover:bg-node-component-surface-hovered flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm"
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@mousedown="startRepeat('Comfy.Canvas.ZoomOut')"
@mouseup="stopRepeat"
@mouseleave="stopRepeat"
>
<span class="font-medium">{{ $t('graphCanvasMenu.zoomOut') }}</span>
<span class="text-text-primary text-[9px]">{{
<span class="text-[9px] text-text-primary">{{
zoomOutCommandText
}}</span>
</div>
<div
class="hover:bg-node-component-surface-hovered flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm"
class="flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered"
@click="executeCommand('Comfy.Canvas.FitView')"
>
<span class="font-medium">{{ $t('zoomControls.zoomToFit') }}</span>
<span class="text-text-primary text-[9px]">{{
<span class="text-[9px] text-text-primary">{{
zoomToFitCommandText
}}</span>
</div>
<div
ref="zoomInputContainer"
class="zoomInputContainer bg-input-surface flex items-center gap-1 rounded p-2"
class="zoomInputContainer flex items-center gap-1 rounded bg-input-surface p-2"
>
<InputNumber
ref="zoomInput"
@@ -60,7 +60,7 @@
@input="applyZoom"
@keyup.enter="applyZoom"
/>
<span class="text-text-primary shrink-0 text-sm">%</span>
<span class="flex-shrink-0 text-sm text-text-primary">%</span>
</div>
</div>
</div>
@@ -148,6 +148,6 @@ watch(
</script>
<style>
.zoomInputContainer:focus-within {
border: 1px solid var(--color-white);
border: 1px solid var(--color-pure-white);
}
</style>

View File

@@ -92,7 +92,7 @@ describe('BypassButton', () => {
const button = wrapper.find('button')
expect(button.classes()).not.toContain(
'dark-theme:[&:not(:active)]:!bg-charcoal-600'
'dark-theme:[&:not(:active)]:!bg-[#262729]'
)
})

View File

@@ -7,11 +7,11 @@
severity="secondary"
text
data-testid="bypass-button"
class="hover:bg-secondary-background"
class="hover:bg-[#E7E6E6] hover:dark-theme:bg-charcoal-600"
@click="toggleBypass"
>
<template #icon>
<i class="icon-[lucide--ban] size-4" />
<i class="icon-[lucide--ban] h-4 w-4" />
</template>
</Button>
</template>

View File

@@ -12,11 +12,11 @@
>
<div class="flex items-center gap-1 px-0">
<i
class="pi pi-circle-fill size-4"
class="pi pi-circle-fill h-4 w-4"
:style="{ color: currentColor ?? '' }"
/>
<i
class="pi pi-chevron-down size-4 py-1"
class="pi pi-chevron-down h-4 w-4 py-1"
:style="{ fontSize: '0.5rem' }"
/>
</div>

View File

@@ -11,7 +11,7 @@
@click="() => commandStore.execute('Comfy.Graph.UnpackSubgraph')"
>
<template #icon>
<i class="icon-[lucide--expand] size-4" />
<i class="icon-[lucide--expand] h-4 w-4" />
</template>
</Button>
<Button

View File

@@ -4,7 +4,7 @@
value: t('selectionToolbox.executeButton.tooltip'),
showDelay: 1000
}"
class="bg-azure-400 dark-theme:bg-azure-600 size-8 !p-0"
class="size-8 bg-[#31B9F4] !p-0 dark-theme:bg-[#0B8CE9]"
text
@mouseenter="() => handleMouseEnter()"
@mouseleave="() => handleMouseLeave()"

View File

@@ -9,7 +9,7 @@
severity="secondary"
@click="frameNodes"
>
<i class="icon-[lucide--frame] size-4" />
<i class="icon-[lucide--frame] h-4 w-4" />
</Button>
</template>

View File

@@ -9,7 +9,7 @@
severity="secondary"
@click="toggleHelp"
>
<i class="icon-[lucide--info] size-4" />
<i class="icon-[lucide--info] h-4 w-4" />
</Button>
</template>

View File

@@ -1,19 +1,19 @@
<template>
<div
v-if="option.type === 'divider'"
class="bg-smoke-200 dark-theme:bg-zinc-700 my-1 h-px"
class="my-1 h-px bg-gray-200 dark-theme:bg-zinc-700"
/>
<div
v-else
role="button"
class="text-text-primary hover:bg-interface-menu-component-surface-hovered group flex cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm"
class="group flex cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-left text-sm text-text-primary hover:bg-interface-menu-component-surface-hovered"
@click="handleClick"
>
<i v-if="option.icon" :class="[option.icon, 'size-4']" />
<i v-if="option.icon" :class="[option.icon, 'h-4 w-4']" />
<span class="flex-1">{{ option.label }}</span>
<span
v-if="option.shortcut"
class="bg-interface-menu-keybind-surface-default text-xxs flex h-3.5 min-w-3.5 items-center justify-center rounded px-1 py-0"
class="flex h-3.5 min-w-3.5 items-center justify-center rounded bg-interface-menu-keybind-surface-default px-1 py-0 text-xxs"
>
{{ option.shortcut }}
</span>
@@ -27,11 +27,11 @@
:severity="option.badge === 'new' ? 'info' : 'secondary'"
:value="t(option.badge)"
:class="{
'rounded-4xl bg-azure-400 dark-theme:bg-azure-600':
'rounded-4xl bg-[#31B9F4] dark-theme:bg-[#0B8CE9]':
option.badge === 'new',
'rounded-4xl dark-theme:bg-black bg-slate-100':
'rounded-4xl bg-[#9C9EAB] dark-theme:bg-[#000]':
option.badge === 'deprecated',
'h-4 gap-2.5 px-1 text-[9px] uppercase text-white': true
'h-4 gap-2.5 px-1 text-[9px] text-white uppercase': true
}"
/>
</div>

View File

@@ -7,11 +7,11 @@
}"
data-testid="more-options-button"
text
class="size-8 px-0"
class="h-8 w-8 px-0"
severity="secondary"
@click="handleClick"
>
<i class="icon-[lucide--more-vertical] size-4" />
<i class="icon-[lucide--more-vertical] h-4 w-4" />
</Button>
</template>

View File

@@ -7,7 +7,7 @@
data-testid="refresh-button"
@click="refreshSelected"
>
<i class="icon-[lucide--refresh-cw] size-4" />
<i class="icon-[lucide--refresh-cw] h-4 w-4" />
</Button>
</template>

View File

@@ -20,23 +20,23 @@
:key="subOption.label"
:class="
isColorSubmenu
? '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'
? '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'
"
:title="subOption.label"
@click="handleSubmenuClick(subOption)"
>
<div
v-if="subOption.color"
class="border-smoke-300 dark-theme:border-zinc-600 size-5 rounded-full border"
class="h-5 w-5 rounded-full border border-gray-300 dark-theme:border-zinc-600"
:style="{ backgroundColor: subOption.color }"
/>
<template v-else-if="!subOption.color">
<i
v-if="isShapeSelected(subOption)"
class="icon-[lucide--check] size-4 shrink-0"
class="icon-[lucide--check] h-4 w-4 flex-shrink-0"
/>
<div v-else class="w-4 shrink-0" />
<div v-else class="w-4 flex-shrink-0" />
<span>{{ subOption.label }}</span>
</template>
</div>

View File

@@ -1,5 +1,3 @@
<template>
<div
class="bg-smoke-300/10 dark-theme:bg-smoke-600/10 h-6 w-px self-center"
/>
<div class="h-6 w-px self-center bg-gray-300/10 dark-theme:bg-gray-600/10" />
</template>

View File

@@ -1,7 +1,7 @@
<template>
<ScrollPanel
ref="scrollPanelRef"
class="min-h-[400px] w-full rounded-lg p-2 text-xs"
class="min-h-[400px] w-full rounded-lg px-2 py-2 text-xs"
:pt="{ content: { id: 'chat-scroll-content' } }"
>
<div v-for="(item, i) in parsedHistory" :key="i" class="mb-4">
@@ -13,12 +13,12 @@
>
<div class="mb-1 flex justify-end">
<div
class="bg-smoke-300 dark-theme:bg-smoke-800 max-w-[80%] rounded-xl px-4 py-1 text-right"
class="max-w-[80%] rounded-xl bg-gray-300 px-4 py-1 text-right dark-theme:bg-gray-800"
>
<div class="break-words text-[12px]">{{ item.prompt }}</div>
<div class="text-[12px] break-words">{{ item.prompt }}</div>
</div>
</div>
<div class="mb-2 mr-1 flex justify-end">
<div class="mr-1 mb-2 flex justify-end">
<CopyButton :text="item.prompt" />
<Button
v-tooltip="
@@ -26,7 +26,7 @@
"
text
rounded
class="h-4! w-4! p-1! text-smoke-400 hover:text-smoke-600 hover:dark-theme:text-smoke-200 transition"
class="h-4! w-4! p-1! text-gray-400 transition hover:text-gray-600 hover:dark-theme:text-gray-200"
pt:icon:class="text-xs!"
:icon="editIndex === i ? 'pi pi-times' : 'pi pi-pencil'"
:aria-label="

View File

@@ -5,7 +5,7 @@
"
text
rounded
class="h-4! w-6! p-1! text-smoke-400 hover:text-smoke-600 hover:dark-theme:text-smoke-200 transition"
class="h-4! w-6! p-1! text-gray-400 transition hover:text-gray-600 hover:dark-theme:text-gray-200"
pt:icon:class="text-xs!"
:icon="copied ? 'pi pi-check' : 'pi pi-copy'"
:aria-label="

View File

@@ -2,7 +2,7 @@
<span>
<div class="mb-1 flex justify-start">
<div class="max-w-[80%] rounded-xl px-4 py-1">
<div class="break-words text-[12px]">
<div class="text-[12px] break-words">
<slot />
</div>
</div>

View File

@@ -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"

View File

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

View File

@@ -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-azure-400 dark-theme:border-blue-700 rounded-lg 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">
<h3 class="text-lg font-semibold mb-2">🎯 Keyboard Navigation Test</h3>
<p class="text-sm text-smoke-600 dark-theme:text-smoke-300 mb-4">
<p class="text-sm text-gray-600 dark-theme:text-gray-300 mb-4">
Use your keyboard to navigate this MultiSelect:
</p>
<ol class="text-sm text-smoke-600 list-decimal list-inside space-y-1">
<ol class="text-sm text-gray-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-smoke-700">
<label class="block text-sm font-medium text-gray-700">
Select Frameworks (Keyboard Navigation Test)
</label>
<MultiSelect v-bind="args" class="w-80" />
<p class="text-xs text-smoke-500">
<p class="text-xs text-gray-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-smoke-600 mb-2">
<p class="text-sm text-gray-600 mb-2">
These dropdowns have proper ARIA attributes and labels for screen readers:
</p>
<ul class="text-sm text-smoke-600 list-disc list-inside space-y-1">
<ul class="text-sm text-gray-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-smoke-700">
<label class="block text-sm font-medium text-gray-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-smoke-500" aria-live="polite">
<p class="text-xs text-gray-500" aria-live="polite">
{{ selectedColors.length }} color(s) selected
</p>
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-smoke-700">
<label class="block text-sm font-medium text-gray-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-smoke-500" aria-live="polite">
<p class="text-xs text-gray-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-smoke-600 dark-theme:text-smoke-300 mb-4">
<p class="text-sm text-gray-600 dark-theme:text-gray-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-smoke-700 mb-1">
<label class="block text-sm font-medium text-gray-700 mb-1">
Before MultiSelect
</label>
<input
type="text"
placeholder="Previous field"
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"
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"
/>
</div>
<div>
<label class="block text-sm font-medium text-smoke-700 mb-1">
<label class="block text-sm font-medium text-gray-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-smoke-700 mb-1">
<label class="block text-sm font-medium text-gray-700 mb-1">
After MultiSelect
</label>
<input
type="text"
placeholder="Next field"
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"
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"
/>
</div>
@@ -307,7 +307,7 @@ export const FocusManagement: Story = {
</button>
</div>
<div class="text-sm text-smoke-600 mt-4">
<div class="text-sm text-gray-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-smoke-200 dark-theme:border-zinc-700 rounded-lg p-6">
<div class="bg-gray-50 dark-theme:bg-zinc-800 border border-gray-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-azure-400 dark-theme:border-blue-700 rounded-lg">
<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">
<h4 class="font-semibold mb-2">🎯 Quick Test</h4>
<p class="text-sm text-smoke-700 dark-theme:text-smoke-300">
<p class="text-sm text-gray-700 dark-theme:text-gray-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>

View File

@@ -24,7 +24,7 @@
v-if="showSearchBox || showSelectedCount || showClearButton"
#header
>
<div class="flex flex-col px-2 pb-0 pt-2">
<div class="flex flex-col px-2 pt-2 pb-0">
<SearchBox
v-if="showSearchBox"
v-model="searchQuery"
@@ -39,7 +39,7 @@
>
<span
v-if="showSelectedCount"
class="dark-theme:text-zinc-500 px-1 text-sm text-neutral-400"
class="px-1 text-sm text-neutral-400 dark-theme:text-zinc-500"
>
{{
selectedCount > 0
@@ -52,22 +52,22 @@
:label="$t('g.clearAll')"
type="transparent"
size="fit-content"
class="dark-theme:text-blue-600 text-sm text-blue-500"
class="text-sm text-blue-500 dark-theme:text-blue-600"
@click.stop="selectedItems = []"
/>
</div>
<div class="dark-theme:bg-zinc-700 my-4 h-px bg-zinc-200"></div>
<div class="my-4 h-px bg-zinc-200 dark-theme:bg-zinc-700"></div>
</div>
</template>
<!-- Trigger value (keep text scale identical) -->
<template #value>
<span class="dark-theme:text-smoke-200 text-sm text-zinc-700">
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
<span
v-if="selectedCount > 0"
class="dark-theme:bg-blue-500 pointer-events-none absolute -right-2 -top-2 z-10 flex size-5 items-center justify-center rounded-full bg-blue-400 text-xs font-semibold text-white"
class="pointer-events-none absolute -top-2 -right-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 text-xs font-semibold text-white dark-theme:bg-blue-500"
>
{{ selectedCount }}
</span>
@@ -82,7 +82,7 @@
<template #option="slotProps">
<div class="flex items-center gap-2" :style="popoverStyle">
<div
class="flex size-4 shrink-0 items-center justify-center rounded p-0.5 transition-all duration-200"
class="flex h-4 w-4 shrink-0 items-center justify-center rounded p-0.5 transition-all duration-200"
:class="
slotProps.selected
? 'bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'

View File

@@ -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-azure-400 dark-theme:border-blue-700 rounded-lg 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">
<h3 class="text-lg font-semibold mb-2">🎯 Keyboard Navigation Test</h3>
<p class="text-sm text-smoke-600 dark-theme:text-smoke-300 mb-4">
<p class="text-sm text-gray-600 dark-theme:text-gray-300 mb-4">
Use your keyboard to navigate these SingleSelect dropdowns:
</p>
<ol class="text-sm text-smoke-600 dark-theme:text-smoke-300 list-decimal list-inside space-y-1">
<ol class="text-sm text-gray-600 dark-theme:text-gray-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-smoke-700 dark-theme:text-smoke-200">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-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-smoke-500">
<p class="text-xs text-gray-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-smoke-700 dark-theme:text-smoke-200">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200">
Task Priority (With Icon)
</label>
<SingleSelect
@@ -147,7 +147,7 @@ export const KeyboardNavigationDemo: Story = {
</svg>
</template>
</SingleSelect>
<p class="text-xs text-smoke-500">
<p class="text-xs text-gray-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-smoke-600 dark-theme:text-smoke-300 mb-2">
<p class="text-sm text-gray-600 dark-theme:text-gray-300 mb-2">
These dropdowns have proper ARIA attributes and labels for screen readers:
</p>
<ul class="text-sm text-smoke-600 dark-theme:text-smoke-300 list-disc list-inside space-y-1">
<ul class="text-sm text-gray-600 dark-theme:text-gray-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-smoke-700 dark-theme:text-smoke-200" id="language-label">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-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-smoke-500" aria-live="polite">
<p class="text-xs text-gray-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-smoke-700 dark-theme:text-smoke-200" id="theme-label">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-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-smoke-500" aria-live="polite">
<p class="text-xs text-gray-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-smoke-600 dark-theme:text-smoke-300 space-y-1">
<ul class="text-sm text-gray-600 dark-theme:text-gray-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-smoke-600 dark-theme:text-smoke-300">
<p class="text-sm text-gray-600 dark-theme:text-gray-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-smoke-700 dark-theme:text-smoke-200 mb-1">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
Title *
</label>
<input
type="text"
required
placeholder="Enter a title"
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"
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"
/>
</div>
<div>
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
Category *
</label>
<SingleSelect
@@ -332,7 +332,7 @@ export const FormIntegration: Story = {
</div>
<div>
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
Status
</label>
<SingleSelect
@@ -344,7 +344,7 @@ export const FormIntegration: Story = {
</div>
<div>
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
Assignee
</label>
<SingleSelect
@@ -356,13 +356,13 @@ export const FormIntegration: Story = {
</div>
<div>
<label class="block text-sm font-medium text-smoke-700 dark-theme:text-smoke-200 mb-1">
<label class="block text-sm font-medium text-gray-700 dark-theme:text-gray-200 mb-1">
Description
</label>
<textarea
rows="4"
placeholder="Enter description"
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"
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"
/>
</div>
@@ -375,16 +375,16 @@ export const FormIntegration: Story = {
</button>
<button
type="button"
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"
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"
>
Cancel
</button>
</div>
</form>
<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">
<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">
<h4 class="font-semibold mb-2">Current Form Data:</h4>
<pre class="text-xs text-smoke-600 dark-theme:text-smoke-300">{{ JSON.stringify(formData, null, 2) }}</pre>
<pre class="text-xs text-gray-600 dark-theme:text-gray-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-smoke-200 dark-theme:border-zinc-700 rounded-lg p-6">
<div class="bg-gray-50 dark-theme:bg-zinc-800 border border-gray-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-azure-400 dark-theme:border-blue-700 rounded-lg">
<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">
<h4 class="font-semibold mb-2">🎯 Quick Test</h4>
<p class="text-sm text-smoke-700 dark-theme:text-smoke-200">
<p class="text-sm text-gray-700 dark-theme:text-gray-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-smoke-700 dark-theme:text-smoke-200">
<p class="text-sm text-gray-700 dark-theme:text-gray-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>

View File

@@ -26,11 +26,11 @@
<slot name="icon" />
<span
v-if="slotProps.value !== null && slotProps.value !== undefined"
class="dark-theme:text-smoke-200 text-zinc-700"
class="text-zinc-700 dark-theme:text-gray-200"
>
{{ getLabel(slotProps.value) }}
</span>
<span v-else class="dark-theme:text-smoke-200 text-zinc-700">
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
</div>
@@ -50,7 +50,7 @@
<span class="truncate">{{ option.name }}</span>
<i
v-if="selected"
class="icon-[lucide--check] dark-theme:text-white text-neutral-600"
class="icon-[lucide--check] text-neutral-600 dark-theme:text-white"
/>
</div>
</template>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="relative size-full"
class="relative h-full w-full"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
@@ -59,7 +59,7 @@
/>
<div
v-if="enable3DViewer"
class="pointer-events-auto absolute right-2 top-12 z-20"
class="pointer-events-auto absolute top-12 right-2 z-20"
>
<ViewerControls :node="node" />
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="relative size-full"
class="relative h-full w-full"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
@@ -34,7 +34,7 @@
@up-direction-change="listenUpDirectionChange"
@recording-status-change="listenRecordingStatusChange"
/>
<div class="pointer-events-none absolute left-0 top-0 size-full">
<div class="pointer-events-none absolute top-0 left-0 h-full w-full">
<Load3DControls
:input-spec="inputSpec"
:background-color="backgroundColor"
@@ -69,7 +69,7 @@
</div>
<div
v-if="showRecordingControls"
class="pointer-events-auto absolute right-2 top-12 z-20"
class="pointer-events-auto absolute top-12 right-2 z-20"
>
<RecordingControls
:node="node"

View File

@@ -1,7 +1,7 @@
<template>
<div
v-if="animations && animations.length > 0"
class="pointer-events-auto absolute left-0 top-0 z-10 flex w-full items-center justify-center gap-2 pt-2"
class="pointer-events-auto absolute top-0 left-0 z-10 flex w-full items-center justify-center gap-2 pt-2"
>
<Button class="p-button-rounded p-button-text" @click="togglePlay">
<i

View File

@@ -1,6 +1,6 @@
<template>
<div
class="bg-smoke-700/30 pointer-events-auto absolute left-2 top-12 z-20 flex flex-col rounded-lg"
class="pointer-events-auto absolute top-12 left-2 z-20 flex flex-col rounded-lg bg-gray-700/30"
>
<div class="show-menu relative">
<Button class="p-button-rounded p-button-text" @click="toggleMenu">
@@ -9,14 +9,14 @@
<div
v-show="isMenuOpen"
class="absolute left-12 top-0 rounded-lg bg-black/50 shadow-lg"
class="absolute top-0 left-12 rounded-lg bg-black/50 shadow-lg"
>
<div class="flex flex-col">
<Button
v-for="category in availableCategories"
:key="category"
class="p-button-text flex w-full items-center justify-start"
:class="{ 'bg-smoke-600': activeCategory === category }"
:class="{ 'bg-gray-600': activeCategory === category }"
@click="selectCategory(category)"
>
<i :class="getCategoryIcon(category)" />
@@ -26,7 +26,7 @@
</div>
</div>
<div v-show="activeCategory" class="bg-smoke-700/30 rounded-lg">
<div v-show="activeCategory" class="rounded-lg bg-gray-700/30">
<SceneControls
v-if="activeCategory === 'scene'"
ref="sceneControlsRef"

View File

@@ -1,5 +1,5 @@
<template>
<div ref="container" class="comfy-load-3d relative size-full">
<div ref="container" class="comfy-load-3d relative h-full w-full">
<LoadingOverlay ref="loadingOverlayRef" />
</div>
</template>

View File

@@ -9,7 +9,7 @@
<div ref="mainContentRef" class="relative flex-1">
<div
ref="containerRef"
class="comfy-load-3d-viewer absolute size-full"
class="comfy-load-3d-viewer absolute h-full w-full"
@resize="viewer.handleResize"
/>
</div>

View File

@@ -18,7 +18,7 @@
</Button>
<div
v-show="showFOV"
class="absolute left-12 top-0 rounded-lg bg-black/50 p-4 shadow-lg"
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
style="width: 150px"
>
<Slider

View File

@@ -15,7 +15,7 @@
</Button>
<div
v-show="showExportFormats"
class="absolute left-12 top-0 rounded-lg bg-black/50 shadow-lg"
class="absolute top-0 left-12 rounded-lg bg-black/50 shadow-lg"
>
<div class="flex flex-col">
<Button

View File

@@ -15,7 +15,7 @@
</Button>
<div
v-show="showLightIntensity"
class="absolute left-12 top-0 rounded-lg bg-black/50 p-4 shadow-lg"
class="absolute top-0 left-12 rounded-lg bg-black/50 p-4 shadow-lg"
style="width: 150px"
>
<Slider

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