diff --git a/.gitattributes b/.gitattributes index af4b6adbc..749554ee1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,9 +2,13 @@ * text=auto # Force TS to LF to make the unixy scripts not break on Windows +*.cjs text eol=lf +*.js text eol=lf +*.json text eol=lf +*.mjs text eol=lf +*.mts text eol=lf *.ts text eol=lf *.vue text eol=lf -*.js text eol=lf # Generated files src/types/comfyRegistryTypes.ts linguist-generated=true diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index d4a309bf8..4405aad1f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -1,6 +1,5 @@ name: Bug Report description: 'Report something that is not working correctly' -title: '[Bug]: ' labels: ['Potential Bug'] type: Bug diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml index a32598374..0d8173b28 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -1,7 +1,6 @@ name: Feature Request description: Report a problem or limitation you're experiencing -title: '[Feature]: ' -labels: ['enhancement'] +labels: [] type: Feature body: diff --git a/.github/workflows/chromatic.yaml b/.github/workflows/chromatic.yaml index 558f04fee..51f8a4fde 100644 --- a/.github/workflows/chromatic.yaml +++ b/.github/workflows/chromatic.yaml @@ -3,14 +3,15 @@ name: 'Chromatic' # - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ ) on: - push: - branches: [main] + workflow_dispatch: # Allow manual triggering pull_request: branches: [main] jobs: chromatic-deployment: runs-on: ubuntu-latest + # Only run for PRs from version-bump-* branches or manual triggers + if: github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'version-bump-') permissions: pull-requests: write issues: write @@ -32,6 +33,7 @@ jobs: - name: Comment PR - Build Started if: github.event_name == 'pull_request' + continue-on-error: true uses: edumserrano/find-create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} @@ -49,6 +51,19 @@ jobs: --- *This comment will be updated when the build completes* + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + storybook-static + tsconfig.tsbuildinfo + key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }} + restore-keys: | + storybook-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}- + storybook-cache-${{ runner.os }}- + storybook-tools-cache-${{ runner.os }}- + - name: Install dependencies run: npm ci @@ -68,6 +83,7 @@ jobs: - name: Comment PR - Build Complete if: github.event_name == 'pull_request' && always() + continue-on-error: true uses: edumserrano/find-create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/dev-release.yaml b/.github/workflows/dev-release.yaml index 177c01df9..4b4e26067 100644 --- a/.github/workflows/dev-release.yaml +++ b/.github/workflows/dev-release.yaml @@ -19,6 +19,19 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 'lts/*' + cache: 'npm' + + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + dist + tsconfig.tsbuildinfo + key: dev-release-tools-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + dev-release-tools-cache-${{ runner.os }}- + - name: Get current version id: current_version run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT diff --git a/.github/workflows/i18n.yaml b/.github/workflows/i18n.yaml index 3482e74c6..c823770ad 100644 --- a/.github/workflows/i18n.yaml +++ b/.github/workflows/i18n.yaml @@ -17,6 +17,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3 + + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + ComfyUI_frontend/.cache + ComfyUI_frontend/.cache + key: i18n-tools-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/package-lock.json') }} + restore-keys: | + i18n-tools-cache-${{ runner.os }}- - name: Install Playwright Browsers run: npx playwright install chromium --with-deps working-directory: ComfyUI_frontend diff --git a/.github/workflows/lint-and-format.yaml b/.github/workflows/lint-and-format.yaml index 5d9a50d83..7e398510f 100644 --- a/.github/workflows/lint-and-format.yaml +++ b/.github/workflows/lint-and-format.yaml @@ -25,6 +25,21 @@ jobs: node-version: 'lts/*' cache: 'npm' + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + .eslintcache + tsconfig.tsbuildinfo + .prettierCache + .knip-cache + key: lint-format-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**/*.{ts,vue,js,mts}', '*.config.*', '.eslintrc.*', '.prettierrc.*', 'tsconfig.json') }} + restore-keys: | + lint-format-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}- + lint-format-cache-${{ runner.os }}- + ci-tools-cache-${{ runner.os }}- + - name: Install dependencies run: npm ci @@ -60,6 +75,7 @@ jobs: - name: Comment on PR about auto-fix if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository + continue-on-error: true uses: actions/github-script@v7 with: script: | @@ -72,6 +88,7 @@ jobs: - name: Comment on PR about manual fix needed if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository + continue-on-error: true uses: actions/github-script@v7 with: script: | diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 55c088163..0cac6ef3a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,6 +22,18 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 'lts/*' + cache: 'npm' + + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + tsconfig.tsbuildinfo + key: release-tools-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + release-tools-cache-${{ runner.os }}- + - name: Get current version id: current_version run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT @@ -116,7 +128,20 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 'lts/*' + cache: 'npm' registry-url: https://registry.npmjs.org + + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + tsconfig.tsbuildinfo + dist + key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + types-tools-cache-${{ runner.os }}- + - run: npm ci - run: npm run build:types - name: Publish package diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index 4a5157301..b09cf0a07 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -40,6 +40,8 @@ jobs: - uses: actions/setup-node@v4 with: node-version: lts/* + cache: 'npm' + cache-dependency-path: 'ComfyUI_frontend/package-lock.json' - name: Get current time id: current-time @@ -47,6 +49,7 @@ jobs: - name: Comment PR - Tests Started if: github.event_name == 'pull_request' + continue-on-error: true uses: edumserrano/find-create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} @@ -64,6 +67,18 @@ jobs: --- *This comment will be updated when tests complete* + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + ComfyUI_frontend/.cache + ComfyUI_frontend/tsconfig.tsbuildinfo + key: playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/package-lock.json') }}-${{ hashFiles('ComfyUI_frontend/src/**/*.{ts,vue,js}', 'ComfyUI_frontend/*.config.*') }} + restore-keys: | + playwright-setup-cache-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/package-lock.json') }}- + playwright-setup-cache-${{ runner.os }}- + playwright-tools-cache-${{ runner.os }}- + - name: Build ComfyUI_frontend run: | npm ci @@ -117,6 +132,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' + cache: 'pip' - name: Get current time id: current-time @@ -134,6 +150,7 @@ jobs: - name: Comment PR - Browser Test Started if: github.event_name == 'pull_request' + continue-on-error: true uses: edumserrano/find-create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} @@ -158,10 +175,22 @@ jobs: wait-for-it --service 127.0.0.1:8188 -t 600 working-directory: ComfyUI + - name: Cache Playwright browsers + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/package-lock.json') }}-${{ matrix.browser }} + restore-keys: | + playwright-browsers-${{ runner.os }}-${{ hashFiles('ComfyUI_frontend/package-lock.json') }}- + playwright-browsers-${{ runner.os }}- + - name: Install Playwright Browsers run: npx playwright install chromium --with-deps working-directory: ComfyUI_frontend + - name: Install Wrangler + run: npm install -g wrangler + - name: Run Playwright tests (${{ matrix.browser }}) id: playwright run: npx playwright test --project=${{ matrix.browser }} --reporter=html @@ -177,11 +206,36 @@ jobs: - name: Deploy to Cloudflare Pages (${{ matrix.browser }}) id: cloudflare-deploy if: always() - uses: cloudflare/wrangler-action@v3 - with: - apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }} + continue-on-error: true + run: | + # Retry logic for wrangler deploy (3 attempts) + RETRY_COUNT=0 + MAX_RETRIES=3 + SUCCESS=false + + while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..." + + if npx wrangler pages deploy ComfyUI_frontend/playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then + SUCCESS=true + echo "Deployment successful on attempt $RETRY_COUNT" + else + echo "Deployment failed on attempt $RETRY_COUNT" + if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then + echo "Retrying in 10 seconds..." + sleep 10 + fi + fi + done + + if [ $SUCCESS = false ]; then + echo "All deployment attempts failed" + exit 1 + fi + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - name: Save deployment info for summary if: always() @@ -211,6 +265,7 @@ jobs: - name: Comment PR - Browser Test Complete if: always() && github.event_name == 'pull_request' + continue-on-error: true uses: edumserrano/find-create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} @@ -296,6 +351,7 @@ jobs: fi - name: Comment PR - Tests Complete + continue-on-error: true uses: edumserrano/find-create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/update-electron-types.yaml b/.github/workflows/update-electron-types.yaml index 2430cf5e5..642490c61 100644 --- a/.github/workflows/update-electron-types.yaml +++ b/.github/workflows/update-electron-types.yaml @@ -20,6 +20,15 @@ jobs: node-version: lts/* cache: 'npm' + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + key: electron-types-tools-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + electron-types-tools-cache-${{ runner.os }}- + - name: Update electron types run: npm install @comfyorg/comfyui-electron-types@latest diff --git a/.github/workflows/update-manager-types.yaml b/.github/workflows/update-manager-types.yaml index 7933c7cbe..e31fbe050 100644 --- a/.github/workflows/update-manager-types.yaml +++ b/.github/workflows/update-manager-types.yaml @@ -25,9 +25,26 @@ jobs: node-version: lts/* cache: 'npm' + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + key: update-manager-tools-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + update-manager-tools-cache-${{ runner.os }}- + - name: Install dependencies run: npm ci + - name: Cache ComfyUI-Manager repository + uses: actions/cache@v4 + with: + path: ComfyUI-Manager + key: comfyui-manager-repo-${{ runner.os }}-${{ github.run_id }} + restore-keys: | + comfyui-manager-repo-${{ runner.os }}- + - name: Checkout ComfyUI-Manager repository uses: actions/checkout@v4 with: diff --git a/.github/workflows/update-registry-types.yaml b/.github/workflows/update-registry-types.yaml index 39a653697..c950101cb 100644 --- a/.github/workflows/update-registry-types.yaml +++ b/.github/workflows/update-registry-types.yaml @@ -24,9 +24,26 @@ jobs: node-version: lts/* cache: 'npm' + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + key: update-registry-tools-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + update-registry-tools-cache-${{ runner.os }}- + - name: Install dependencies run: npm ci + - name: Cache comfy-api repository + uses: actions/cache@v4 + with: + path: comfy-api + key: comfy-api-repo-${{ runner.os }}-${{ github.run_id }} + restore-keys: | + comfy-api-repo-${{ runner.os }}- + - name: Checkout comfy-api repository uses: actions/checkout@v4 with: diff --git a/.github/workflows/vitest.yaml b/.github/workflows/vitest.yaml index 788b6aba4..e684ebbe4 100644 --- a/.github/workflows/vitest.yaml +++ b/.github/workflows/vitest.yaml @@ -17,6 +17,20 @@ jobs: uses: actions/setup-node@v4 with: node-version: 'lts/*' + cache: 'npm' + + - name: Cache tool outputs + uses: actions/cache@v4 + with: + path: | + .cache + coverage + .vitest-cache + key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }} + restore-keys: | + vitest-cache-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}- + vitest-cache-${{ runner.os }}- + test-tools-cache-${{ runner.os }}- - name: Install dependencies run: npm ci diff --git a/.gitignore b/.gitignore index 8d19ceec5..60cca8f98 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,6 @@ dist-ssr *.local # Claude configuration .claude/*.local.json -.claude/settings.json # Editor directories and files .vscode/* diff --git a/.storybook/README.md b/.storybook/README.md index eff19a67d..902397471 100644 --- a/.storybook/README.md +++ b/.storybook/README.md @@ -209,4 +209,22 @@ This Storybook setup includes: - PrimeVue component library integration - Proper alias resolution for `@/` imports -For component-specific examples, see the NodePreview stories in `src/components/node/`. +## Icon Usage in Storybook + +In this project, the `` syntax from unplugin-icons is not supported in Storybook. + +**Example:** + +```vue + + + +``` + +This approach ensures icons render correctly in Storybook and remain consistent with the rest of the app. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c25dc9674..8280246b7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -265,7 +265,7 @@ The project supports three types of icons, all with automatic imports (no manual 2. **Iconify Icons** - 200,000+ icons from various libraries: ``, `` 3. **Custom Icons** - Your own SVG icons: `` -Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/`. +Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/` and processed by `build/customIconCollection.ts` with automatic validation. For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md). diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 738f5719c..f64ca5c94 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -124,6 +124,7 @@ export class ComfyPage { public readonly url: string // All canvas position operations are based on default view of canvas. public readonly canvas: Locator + public readonly selectionToolbox: Locator public readonly widgetTextBox: Locator // Buttons @@ -158,6 +159,7 @@ export class ComfyPage { ) { this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188' this.canvas = page.locator('#graph-canvas') + this.selectionToolbox = page.locator('.selection-toolbox') this.widgetTextBox = page.getByPlaceholder('text').nth(1) this.resetViewButton = page.getByRole('button', { name: 'Reset View' }) this.queueButton = page.getByRole('button', { name: 'Queue Prompt' }) diff --git a/browser_tests/fixtures/components/Topbar.ts b/browser_tests/fixtures/components/Topbar.ts index b138ff7b7..f2c9dfa16 100644 --- a/browser_tests/fixtures/components/Topbar.ts +++ b/browser_tests/fixtures/components/Topbar.ts @@ -65,6 +65,7 @@ export class Topbar { } async openTopbarMenu() { + await this.page.waitForTimeout(1000) await this.page.locator('.comfyui-logo-wrapper').click() const menu = this.page.locator('.comfy-command-menu') await menu.waitFor({ state: 'visible' }) diff --git a/browser_tests/tests/customIcons.spec.ts b/browser_tests/tests/customIcons.spec.ts new file mode 100644 index 000000000..da640fd89 --- /dev/null +++ b/browser_tests/tests/customIcons.spec.ts @@ -0,0 +1,57 @@ +import { expect } from '@playwright/test' +import type { Locator } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +async function verifyCustomIconSvg(iconElement: Locator) { + const svgVariable = await iconElement.evaluate((element) => { + const styles = getComputedStyle(element) + return styles.getPropertyValue('--svg') + }) + + expect(svgVariable).toBeTruthy() + const dataUrlMatch = svgVariable.match( + /url\("data:image\/svg\+xml,([^"]+)"\)/ + ) + expect(dataUrlMatch).toBeTruthy() + + const encodedSvg = dataUrlMatch![1] + const decodedSvg = decodeURIComponent(encodedSvg) + + // Check for SVG header to confirm it's a valid SVG + expect(decodedSvg).toContain(" { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + }) + + test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => { + // Find the icon in the sidebar + const icon = comfyPage.page.locator( + '.icon-\\[comfy--ai-model\\].side-bar-button-icon' + ) + await expect(icon).toBeVisible() + + // Verify the custom SVG content + await verifyCustomIconSvg(icon) + }) + + test('Browse Templates menu item uses custom template icon', async ({ + comfyPage + }) => { + // Open the topbar menu + await comfyPage.menu.topbar.openTopbarMenu() + const menuItem = comfyPage.menu.topbar.getMenuItem('Browse Templates') + + // Find the icon as a previous sibling of the menu item label + const templateIcon = menuItem + .locator('..') + .locator('.icon-\\[comfy--template\\]') + await expect(templateIcon).toBeVisible() + + // Verify the custom SVG content + await verifyCustomIconSvg(templateIcon) + }) +}) diff --git a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png index 765aa1126..3acc073ff 100644 Binary files a/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png and b/browser_tests/tests/domWidget.spec.ts-snapshots/focus-mode-on-chromium-linux.png differ diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index e8994e4f0..9ae090975 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -7,13 +7,11 @@ test.describe('Graph Canvas Menu', () => { // Set link render mode to spline to make sure it's not affected by other tests' // side effects. await comfyPage.setSetting('Comfy.LinkRenderMode', 2) + // Enable canvas menu for all tests + await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) }) test('Can toggle link visibility', async ({ comfyPage }) => { - // Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup. - // so no cleanup is needed. - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) - const button = comfyPage.page.getByTestId('toggle-link-visibility-button') await button.click() await comfyPage.nextFrame() @@ -36,4 +34,45 @@ test.describe('Graph Canvas Menu', () => { hiddenLinkRenderMode ) }) + + test('Focus mode button is clickable and has correct test id', async ({ + comfyPage + }) => { + const focusButton = comfyPage.page.getByTestId('focus-mode-button') + await expect(focusButton).toBeVisible() + await expect(focusButton).toBeEnabled() + + // Test that the button can be clicked without error + await focusButton.click() + await comfyPage.nextFrame() + }) + + test('Zoom controls popup opens and closes', async ({ comfyPage }) => { + // Find the zoom button by its percentage text content + const zoomButton = comfyPage.page.locator('button').filter({ + hasText: '%' + }) + await expect(zoomButton).toBeVisible() + + // Click to open zoom controls + await zoomButton.click() + await comfyPage.nextFrame() + + // Zoom controls modal should be visible + const zoomModal = comfyPage.page + .locator('div') + .filter({ + hasText: 'Zoom To Fit' + }) + .first() + await expect(zoomModal).toBeVisible() + + // Click backdrop to close + const backdrop = comfyPage.page.locator('.fixed.inset-0').first() + await backdrop.click() + await comfyPage.nextFrame() + + // Modal should be hidden + await expect(zoomModal).not.toBeVisible() + }) }) diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png index 2b1678631..72749caf3 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png differ diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png index fb46d991f..3d86bdfc0 100644 Binary files a/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png and b/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index 2ddf9e086..de46bca2e 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -780,9 +780,18 @@ test.describe('Viewport settings', () => { // Screenshot the canvas element await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') + // Open zoom controls dropdown first + const zoomControlsButton = comfyPage.page.getByTestId( + 'zoom-controls-button' + ) + await zoomControlsButton.click() + + const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') await toggleButton.click() + // close zoom menu + await zoomControlsButton.click() + await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.menu.topbar.saveWorkflow('Workflow A') await comfyPage.nextFrame() diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png index f0ca5e712..603c598ad 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/group-selected-nodes-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png index f5c4af9bc..7cc712f74 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-bypassed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png index 11356a8da..895e429a1 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png index dfccbf641..a37ea3f8c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unbypassed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png index dfccbf641..a37ea3f8c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/nodes-unpinned-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png index dfccbf641..a37ea3f8c 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-left-drag-select-chromium-linux.png differ diff --git a/browser_tests/tests/minimap.spec.ts b/browser_tests/tests/minimap.spec.ts index 366a6634d..df967d911 100644 --- a/browser_tests/tests/minimap.spec.ts +++ b/browser_tests/tests/minimap.spec.ts @@ -35,34 +35,44 @@ test.describe('Minimap', () => { }) test('Validate minimap toggle button state', async ({ comfyPage }) => { + // Open zoom controls dropdown first + const zoomControlsButton = comfyPage.page.getByTestId( + 'zoom-controls-button' + ) + await zoomControlsButton.click() + const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') await expect(toggleButton).toBeVisible() - await expect(toggleButton).toHaveClass(/minimap-active/) - const minimapContainer = comfyPage.page.locator('.litegraph-minimap') await expect(minimapContainer).toBeVisible() }) test('Validate minimap can be toggled off and on', async ({ comfyPage }) => { const minimapContainer = comfyPage.page.locator('.litegraph-minimap') + + // Open zoom controls dropdown first + const zoomControlsButton = comfyPage.page.getByTestId( + 'zoom-controls-button' + ) + await zoomControlsButton.click() + const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') await expect(minimapContainer).toBeVisible() - await expect(toggleButton).toHaveClass(/minimap-active/) await toggleButton.click() await comfyPage.nextFrame() await expect(minimapContainer).not.toBeVisible() - await expect(toggleButton).not.toHaveClass(/minimap-active/) + await expect(toggleButton).toContainText('Show Minimap') await toggleButton.click() await comfyPage.nextFrame() await expect(minimapContainer).toBeVisible() - await expect(toggleButton).toHaveClass(/minimap-active/) + await expect(toggleButton).toContainText('Hide Minimap') }) test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => { diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts index ced74c903..68ce7b8d5 100644 --- a/browser_tests/tests/nodeHelp.spec.ts +++ b/browser_tests/tests/nodeHelp.spec.ts @@ -41,15 +41,12 @@ test.describe('Node Help', () => { // Select the node with panning to ensure toolbox is visible await selectNodeWithPan(comfyPage, ksamplerNodes[0]) - // Wait for selection overlay container and toolbox to appear - await expect( - comfyPage.page.locator('.selection-overlay-container') - ).toBeVisible() - await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible() + // Wait for selection toolbox to appear + await expect(comfyPage.selectionToolbox).toBeVisible() // Click the help button in the selection toolbox - const helpButton = comfyPage.page.locator( - '.selection-toolbox button:has(.pi-question-circle)' + const helpButton = comfyPage.selectionToolbox.locator( + 'button:has(.pi-question-circle)' ) await expect(helpButton).toBeVisible() await helpButton.click() diff --git a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png index e059ce642..cf3fe09a1 100644 Binary files a/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png and b/browser_tests/tests/rerouteNode.spec.ts-snapshots/reroute-inserted-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png index dfccbf641..a37ea3f8c 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-2-nodes-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png index d67b73fc3..4512bc9da 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-pinned-chromium-linux.png differ diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png index d6976c27e..821129dcb 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/selected-nodes-unpinned-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts b/browser_tests/tests/selectionToolbox.spec.ts index 72264e8b3..90568e3aa 100644 --- a/browser_tests/tests/selectionToolbox.spec.ts +++ b/browser_tests/tests/selectionToolbox.spec.ts @@ -14,20 +14,17 @@ test.describe('Selection Toolbox', () => { test('shows selection toolbox', async ({ comfyPage }) => { // By default, selection toolbox should be enabled - expect( - await comfyPage.page.locator('.selection-overlay-container').isVisible() - ).toBe(false) + await expect(comfyPage.selectionToolbox).not.toBeVisible() // Select multiple nodes await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) // Selection toolbox should be visible with multiple nodes selected - await expect( - comfyPage.page.locator('.selection-overlay-container') - ).toBeVisible() - await expect( - comfyPage.page.locator('.selection-overlay-container.show-border') - ).toBeVisible() + await expect(comfyPage.selectionToolbox).toBeVisible() + // Border is now drawn on canvas, check via screenshot + await expect(comfyPage.canvas).toHaveScreenshot( + 'selection-toolbox-multiple-nodes-border.png' + ) }) test('shows at correct position when node is pasted', async ({ @@ -39,18 +36,16 @@ test.describe('Selection Toolbox', () => { await comfyPage.page.mouse.move(100, 100) await comfyPage.ctrlV() - const overlayContainer = comfyPage.page.locator( - '.selection-overlay-container' - ) - await expect(overlayContainer).toBeVisible() + const toolboxContainer = comfyPage.selectionToolbox + await expect(toolboxContainer).toBeVisible() - // Verify the absolute position - const boundingBox = await overlayContainer.boundingBox() + // Verify toolbox is positioned (canvas-based positioning has different coordinates) + const boundingBox = await toolboxContainer.boundingBox() expect(boundingBox).not.toBeNull() - // 10px offset for the pasted node - expect(Math.round(boundingBox!.x)).toBeCloseTo(90, -1) // Allow ~10px tolerance - // 30px offset of node title height - expect(Math.round(boundingBox!.y)).toBeCloseTo(60, -1) + // Canvas-based positioning can vary, just verify toolbox appears in reasonable bounds + expect(boundingBox!.x).toBeGreaterThan(-100) // Not too far off-screen left + expect(boundingBox!.x).toBeLessThan(1000) // Not too far off-screen right + expect(boundingBox!.y).toBeGreaterThan(-100) // Not too far off-screen top }) test('hide when select and drag happen at the same time', async ({ @@ -65,38 +60,35 @@ test.describe('Selection Toolbox', () => { await comfyPage.page.mouse.down() await comfyPage.page.mouse.move(nodePos.x + 200, nodePos.y + 200) await comfyPage.nextFrame() - await expect( - comfyPage.page.locator('.selection-overlay-container') - ).not.toBeVisible() + await expect(comfyPage.selectionToolbox).not.toBeVisible() }) test('shows border only with multiple selections', async ({ comfyPage }) => { // Select single node await comfyPage.selectNodes(['KSampler']) - // Selection overlay should be visible but without border - await expect( - comfyPage.page.locator('.selection-overlay-container') - ).toBeVisible() - await expect( - comfyPage.page.locator('.selection-overlay-container.show-border') - ).not.toBeVisible() + // Selection toolbox should be visible but without border + await expect(comfyPage.selectionToolbox).toBeVisible() + // Border is now drawn on canvas, check via screenshot + await expect(comfyPage.canvas).toHaveScreenshot( + 'selection-toolbox-single-node-no-border.png' + ) // Select multiple nodes await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) - // Selection overlay should show border with multiple selections - await expect( - comfyPage.page.locator('.selection-overlay-container.show-border') - ).toBeVisible() + // Selection border should show with multiple selections (canvas-based) + await expect(comfyPage.canvas).toHaveScreenshot( + 'selection-toolbox-multiple-selections-border.png' + ) // Deselect to single node await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) - // Border should be hidden again - await expect( - comfyPage.page.locator('.selection-overlay-container.show-border') - ).not.toBeVisible() + // Border should be hidden again (canvas-based) + await expect(comfyPage.canvas).toHaveScreenshot( + 'selection-toolbox-single-selection-no-border.png' + ) }) test('displays bypass button in toolbox when nodes are selected', async ({ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png new file mode 100644 index 000000000..12215637f Binary files /dev/null and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-nodes-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png new file mode 100644 index 000000000..a2d11d350 Binary files /dev/null and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-multiple-selections-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png new file mode 100644 index 000000000..93924ff73 Binary files /dev/null and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-node-no-border-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-selection-no-border-chromium-linux.png b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-selection-no-border-chromium-linux.png new file mode 100644 index 000000000..8017b8f49 Binary files /dev/null and b/browser_tests/tests/selectionToolbox.spec.ts-snapshots/selection-toolbox-single-selection-no-border-chromium-linux.png differ diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts index 658d9bb34..1b3f21ff4 100644 --- a/browser_tests/tests/sidebar/workflows.spec.ts +++ b/browser_tests/tests/sidebar/workflows.spec.ts @@ -193,6 +193,7 @@ test.describe('Workflows sidebar', () => { await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json') await comfyPage.confirmDialog.click('overwrite') + await comfyPage.page.waitForTimeout(200) expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ 'workflow5.json' ]) diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index 0d8978c79..b23faabfc 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -256,6 +256,7 @@ test.describe('Animated image widget', () => { await comfyPage.dragAndDropFile('animated_webp.webp', { dropPosition: { x, y } }) + await comfyPage.page.waitForTimeout(200) // Expect the filename combo value to be updated const fileComboWidget = await loadAnimatedWebpNode.getWidget(0) diff --git a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png b/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png index c1fb924d4..4b948f017 100644 Binary files a/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png and b/browser_tests/tests/widget.spec.ts-snapshots/animated-image-preview-saved-webp-chromium-linux.png differ diff --git a/build/customIconCollection.ts b/build/customIconCollection.ts new file mode 100644 index 000000000..f2d823ed5 --- /dev/null +++ b/build/customIconCollection.ts @@ -0,0 +1,100 @@ +import { existsSync, readFileSync, readdirSync } from 'fs' +import { join } from 'path' +import { dirname } from 'path' +import { fileURLToPath } from 'url' + +const fileName = fileURLToPath(import.meta.url) +const dirName = dirname(fileName) +const customIconsPath = join(dirName, '..', 'src', 'assets', 'icons', 'custom') + +// Iconify collection structure +interface IconifyIcon { + body: string + width?: number + height?: number +} + +interface IconifyCollection { + prefix: string + icons: Record + width?: number + height?: number +} + +// Create an Iconify collection for custom icons +export const iconCollection: IconifyCollection = { + prefix: 'comfy', + icons: {}, + width: 16, + height: 16 +} + +/** + * Validates that an SVG file contains valid SVG content + */ +function validateSvgContent(content: string, filename: string): void { + if (!content.trim()) { + throw new Error(`Empty SVG file: ${filename}`) + } + + if (!content.includes(' tag): ${filename}`) + } + + // Basic XML structure validation + const openTags = (content.match(/]*>/g) || []).length + const closeTags = (content.match(/<\/svg>/g) || []).length + + if (openTags !== closeTags) { + throw new Error(`Malformed SVG file (mismatched svg tags): ${filename}`) + } +} + +/** + * Loads custom SVG icons from the icons directory + */ +function loadCustomIcons(): void { + if (!existsSync(customIconsPath)) { + console.warn(`Custom icons directory not found: ${customIconsPath}`) + return + } + + try { + const files = readdirSync(customIconsPath) + const svgFiles = files.filter((file) => file.endsWith('.svg')) + + if (svgFiles.length === 0) { + console.warn('No SVG files found in custom icons directory') + return + } + + svgFiles.forEach((file) => { + const name = file.replace('.svg', '') + const filePath = join(customIconsPath, file) + + try { + const content = readFileSync(filePath, 'utf-8') + validateSvgContent(content, file) + + iconCollection.icons[name] = { + body: content + } + } catch (error) { + console.error( + `Failed to load custom icon ${file}:`, + error instanceof Error ? error.message : error + ) + // Continue loading other icons instead of failing the entire build + } + }) + } catch (error) { + console.error( + 'Failed to read custom icons directory:', + error instanceof Error ? error.message : error + ) + // Don't throw here - allow build to continue without custom icons + } +} + +// Load icons when this module is imported +loadCustomIcons() diff --git a/docs/adr/0002-monorepo-conversion.md b/docs/adr/0002-monorepo-conversion.md new file mode 100644 index 000000000..d1ada909e --- /dev/null +++ b/docs/adr/0002-monorepo-conversion.md @@ -0,0 +1,50 @@ +# 2. Restructure ComfyUI_frontend as a monorepo + +Date: 2025-08-25 + +## Status + +Proposed + + + +## Context + +[Most of the context is in here](https://github.com/Comfy-Org/ComfyUI_frontend/issues/4661) + +TL;DR: As we're merging more subprojects like litegraph, devtools, and soon a fork of PrimeVue, + a monorepo structure will help a lot with code sharing and organization. + +For more information on Monorepos, check out [monorepo.tools](https://monorepo.tools/) + +## Decision + +- Swap out NPM for PNPM +- Add a workspace for the PrimeVue fork +- Move the frontend code into its own app workspace +- Longer term: Extract and reorganize common infrastructure to take advantage of the new monorepo tooling + +### Tools proposed + +[PNPM](https://pnpm.io/) and [PNPM workspaces](https://pnpm.io/workspaces) + +For monorepo management, I'd probably go with [Nx](https://nx.dev/), but I could be conviced otherwise. +There's a [whole list here](https://monorepo.tools/#tools-review) if you're interested. + +## Consequences + +### Positive + +- Adding new projects with shared dependencies becomes really easy +- Makes the process of forking and customizing projects more structured, if not strictly easier +- It *could* speed up the build and development process (not guaranteed) +- It would let us cleanly organize and release packages like `comfyui-frontend-types` + +### Negative + +- Monorepos take some getting used to +- Reviews and code contribution management has to account for the different projects' situations and constraints + + \ No newline at end of file diff --git a/docs/adr/README.md b/docs/adr/README.md index 67ef529de..3ebf340d5 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -11,6 +11,7 @@ An Architecture Decision Record captures an important architectural decision mad | ADR | Title | Status | Date | |-----|-------|--------|------| | [0001](0001-merge-litegraph-into-frontend.md) | Merge LiteGraph.js into ComfyUI Frontend | Accepted | 2025-08-05 | +| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Proposed | 2025-08-25 | ## Creating a New ADR diff --git a/package-lock.json b/package-lock.json index 75311a1c8..2193dd5fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@comfyorg/comfyui-frontend", - "version": "1.26.5", + "version": "1.26.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@comfyorg/comfyui-frontend", - "version": "1.26.5", + "version": "1.26.6", "license": "GPL-3.0-only", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -55,6 +55,7 @@ "@eslint/js": "^9.8.0", "@executeautomation/playwright-mcp-server": "^1.0.5", "@iconify/json": "^2.2.245", + "@iconify/tailwind": "^1.2.0", "@intlify/eslint-plugin-vue-i18n": "^3.2.0", "@lobehub/i18n-cli": "^1.20.0", "@pinia/testing": "^0.1.5", @@ -84,6 +85,7 @@ "identity-obj-proxy": "^3.0.0", "knip": "^5.62.0", "lint-staged": "^15.2.7", + "lucide-vue-next": "^0.540.0", "postcss": "^8.4.39", "prettier": "^3.3.2", "storybook": "^9.1.1", @@ -2337,119 +2339,24 @@ "pathe": "^1.1.2" } }, + "node_modules/@iconify/tailwind": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@iconify/tailwind/-/tailwind-1.2.0.tgz", + "integrity": "sha512-KgpIHWOTcRYw1XcoUqyNSrmYyfLLqZYu3AmP8zdfLk0F5TqRO8YerhlvlQmGfn7rJXgPeZN569xPAJnJ53zZxA==", + "dev": true, + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", "dev": true }, - "node_modules/@iconify/utils": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", - "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", - "dev": true, - "dependencies": { - "@antfu/install-pkg": "^1.0.0", - "@antfu/utils": "^8.1.0", - "@iconify/types": "^2.0.0", - "debug": "^4.4.0", - "globals": "^15.14.0", - "kolorist": "^1.8.0", - "local-pkg": "^1.0.0", - "mlly": "^1.7.4" - } - }, - "node_modules/@iconify/utils/node_modules/@antfu/install-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", - "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", - "dev": true, - "dependencies": { - "package-manager-detector": "^1.3.0", - "tinyexec": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@iconify/utils/node_modules/@antfu/utils": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", - "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@iconify/utils/node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "dev": true - }, - "node_modules/@iconify/utils/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@iconify/utils/node_modules/local-pkg": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", - "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", - "dev": true, - "dependencies": { - "mlly": "^1.7.4", - "pkg-types": "^2.0.1", - "quansync": "^0.2.8" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@iconify/utils/node_modules/package-manager-detector": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", - "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", - "dev": true - }, - "node_modules/@iconify/utils/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, - "node_modules/@iconify/utils/node_modules/pkg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", - "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", - "dev": true, - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" - } - }, - "node_modules/@iconify/utils/node_modules/tinyexec": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", - "dev": true - }, "node_modules/@inkjs/ui": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@inkjs/ui/-/ui-1.0.0.tgz", @@ -6579,13 +6486,13 @@ } }, "node_modules/axios": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.2.tgz", - "integrity": "sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -6982,7 +6889,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -7134,6 +7040,201 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/cheerio/node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -8232,7 +8333,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -8345,6 +8445,37 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -8391,7 +8522,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -8400,7 +8530,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8410,7 +8539,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -8418,6 +8546,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-toolkit": { "version": "1.39.9", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz", @@ -9750,12 +9893,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -9880,7 +10026,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -9926,7 +10071,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -9950,7 +10094,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -10103,7 +10246,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -10193,7 +10335,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -10205,7 +10346,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -10228,7 +10368,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -10282,6 +10421,80 @@ "node": ">= 12" } }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/htmlparser2/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -12224,6 +12437,16 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-vue-next": { + "version": "0.540.0", + "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.540.0.tgz", + "integrity": "sha512-H7qhKVNKLyoFMo05pWcGSWBiLPiI3zJmWV65SuXWHlrIGIcvDer10xAyWcRJ0KLzIH5k5+yi7AGw/Xi1VF8Pbw==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "vue": ">=3.0.1" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -12300,7 +12523,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "engines": { "node": ">= 0.4" } @@ -14044,6 +14266,55 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -14998,9 +15269,9 @@ } }, "node_modules/quansync": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", - "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", "dev": true, "funding": [ { @@ -15011,7 +15282,8 @@ "type": "individual", "url": "https://github.com/sponsors/sxzz" } - ] + ], + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -17642,6 +17914,72 @@ } } }, + "node_modules/unplugin-icons/node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/unplugin-icons/node_modules/@iconify/utils/node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-icons/node_modules/@iconify/utils/node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-icons/node_modules/@iconify/utils/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-icons/node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/unplugin-icons/node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", @@ -17659,6 +17997,39 @@ } } }, + "node_modules/unplugin-icons/node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin-icons/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin-icons/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/unplugin-icons/node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "dev": true, + "license": "MIT" + }, "node_modules/unplugin-vue-components": { "version": "0.28.0", "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.28.0.tgz", diff --git a/package.json b/package.json index 5f9f81745..5380f73a5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.26.5", + "version": "1.26.6", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -39,6 +39,7 @@ "@eslint/js": "^9.8.0", "@executeautomation/playwright-mcp-server": "^1.0.5", "@iconify/json": "^2.2.245", + "@iconify/tailwind": "^1.2.0", "@intlify/eslint-plugin-vue-i18n": "^3.2.0", "@lobehub/i18n-cli": "^1.20.0", "@pinia/testing": "^0.1.5", @@ -68,6 +69,7 @@ "identity-obj-proxy": "^3.0.0", "knip": "^5.62.0", "lint-staged": "^15.2.7", + "lucide-vue-next": "^0.540.0", "postcss": "^8.4.39", "prettier": "^3.3.2", "storybook": "^9.1.1", diff --git a/src/assets/icons/README.md b/src/assets/icons/README.md index cd92f1ddf..b01a3e3ef 100644 --- a/src/assets/icons/README.md +++ b/src/assets/icons/README.md @@ -247,9 +247,29 @@ Icons are automatically imported using `unplugin-icons` - no manual imports need ### Configuration -The icon system is configured in `vite.config.mts`: +The icon system has two layers: + +1. **Build-time Processing** (`build/customIconCollection.ts`): + - Scans `src/assets/icons/custom/` for SVG files + - Validates SVG content and structure + - Creates Iconify collection for Tailwind CSS + - Provides error handling for malformed files + +2. **Vite Runtime** (`vite.config.mts`): + - Enables direct SVG import as Vue components + - Supports dynamic icon loading ```typescript +// Build script creates Iconify collection +export const iconCollection: IconifyCollection = { + prefix: 'comfy', + icons: { + 'workflow': { body: '...' }, + 'node': { body: '...' } + } +} + +// Vite configuration for component-based usage Icons({ compiler: 'vue3', customCollections: { @@ -271,8 +291,9 @@ Icons are fully typed. If TypeScript doesn't recognize a new custom icon: ### Icon Not Showing 1. **Check filename**: Must be kebab-case without special characters 2. **Restart dev server**: Required after adding new icons -3. **Verify SVG**: Ensure it's valid SVG syntax +3. **Verify SVG**: Ensure it's valid SVG syntax (build script validates automatically) 4. **Check console**: Look for Vue component resolution errors +5. **Build script errors**: Check console during build - malformed SVGs are logged but don't break builds ### Icon Wrong Color - Replace hardcoded colors with `currentColor` diff --git a/src/assets/icons/custom/ai-model.svg b/src/assets/icons/custom/ai-model.svg index ede8e5c7e..a9f987088 100644 --- a/src/assets/icons/custom/ai-model.svg +++ b/src/assets/icons/custom/ai-model.svg @@ -1,6 +1 @@ - - - - - - \ No newline at end of file + diff --git a/src/assets/icons/custom/node.svg b/src/assets/icons/custom/node.svg index 3239b59bd..f31c03c6b 100644 --- a/src/assets/icons/custom/node.svg +++ b/src/assets/icons/custom/node.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + diff --git a/src/assets/icons/custom/template.svg b/src/assets/icons/custom/template.svg index 2a2a75f8d..8c1a66caf 100644 --- a/src/assets/icons/custom/template.svg +++ b/src/assets/icons/custom/template.svg @@ -1,5 +1 @@ - - - - - \ No newline at end of file + diff --git a/src/assets/icons/custom/workflow.svg b/src/assets/icons/custom/workflow.svg index 043d24e7b..b91b58b69 100644 --- a/src/assets/icons/custom/workflow.svg +++ b/src/assets/icons/custom/workflow.svg @@ -1,3 +1 @@ - - - + diff --git a/src/components/button/IconButton.stories.ts b/src/components/button/IconButton.stories.ts new file mode 100644 index 000000000..a0194a240 --- /dev/null +++ b/src/components/button/IconButton.stories.ts @@ -0,0 +1,145 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { Bell, Download, Heart, Settings, Trophy, X } from 'lucide-vue-next' + +import IconButton from './IconButton.vue' + +const meta: Meta = { + title: 'Components/Button/IconButton', + component: IconButton, + tags: ['autodocs'], + argTypes: { + size: { + control: { type: 'select' }, + options: ['sm', 'md'] + }, + type: { + control: { type: 'select' }, + options: ['primary', 'secondary', 'transparent'] + }, + onClick: { action: 'clicked' } + } +} + +export default meta +type Story = StoryObj + +export const Primary: Story = { + render: (args) => ({ + components: { IconButton, Trophy }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'primary', + size: 'md' + } +} + +export const Secondary: Story = { + render: (args) => ({ + components: { IconButton, Settings }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'secondary', + size: 'md' + } +} + +export const Transparent: Story = { + render: (args) => ({ + components: { IconButton, X }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'transparent', + size: 'md' + } +} + +export const Small: Story = { + render: (args) => ({ + components: { IconButton, Bell }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + type: 'secondary', + size: 'sm' + } +} + +export const AllVariants: Story = { + render: () => ({ + components: { IconButton, Trophy, Settings, X, Bell, Heart, Download }, + template: ` +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + + + + +
+
+ ` + }), + parameters: { + controls: { disable: true }, + actions: { disable: true } + } +} diff --git a/src/components/button/IconGroup.stories.ts b/src/components/button/IconGroup.stories.ts new file mode 100644 index 000000000..1eb0d6e0a --- /dev/null +++ b/src/components/button/IconGroup.stories.ts @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { Download, ExternalLink, Heart } from 'lucide-vue-next' + +import IconButton from './IconButton.vue' +import IconGroup from './IconGroup.vue' + +const meta: Meta = { + title: 'Components/Button/IconGroup', + component: IconGroup, + parameters: { + layout: 'centered' + } +} +export default meta + +type Story = StoryObj + +export const Basic: Story = { + render: () => ({ + components: { IconGroup, IconButton, Download, ExternalLink, Heart }, + template: ` + + + + + + + + + + + + ` + }) +} diff --git a/src/components/button/IconTextButton.stories.ts b/src/components/button/IconTextButton.stories.ts new file mode 100644 index 000000000..3c08c418a --- /dev/null +++ b/src/components/button/IconTextButton.stories.ts @@ -0,0 +1,221 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { + ChevronLeft, + ChevronRight, + Download, + Package, + Save, + Settings, + Trash2, + X +} from 'lucide-vue-next' + +import IconTextButton from './IconTextButton.vue' + +const meta: Meta = { + title: 'Components/Button/IconTextButton', + component: IconTextButton, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md'] + }, + type: { + control: { type: 'select' }, + options: ['primary', 'secondary', 'transparent'] + }, + iconPosition: { + control: { type: 'select' }, + options: ['left', 'right'] + }, + onClick: { action: 'clicked' } + } +} + +export default meta +type Story = StoryObj + +export const Primary: Story = { + render: (args) => ({ + components: { IconTextButton, Package }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Deploy', + type: 'primary', + size: 'md' + } +} + +export const Secondary: Story = { + render: (args) => ({ + components: { IconTextButton, Settings }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Settings', + type: 'secondary', + size: 'md' + } +} + +export const Transparent: Story = { + render: (args) => ({ + components: { IconTextButton, X }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Cancel', + type: 'transparent', + size: 'md' + } +} + +export const WithIconRight: Story = { + render: (args) => ({ + components: { IconTextButton, ChevronRight }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Next', + type: 'primary', + size: 'md', + iconPosition: 'right' + } +} + +export const Small: Story = { + render: (args) => ({ + components: { IconTextButton, Save }, + setup() { + return { args } + }, + template: ` + + + + ` + }), + args: { + label: 'Save', + type: 'primary', + size: 'sm' + } +} + +export const AllVariants: Story = { + render: () => ({ + components: { + IconTextButton, + Download, + Settings, + Trash2, + ChevronRight, + ChevronLeft, + Save + }, + template: ` +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + +
+
+ + + + + + + + + +
+
+ ` + }), + parameters: { + controls: { disable: true }, + actions: { disable: true } + } +} diff --git a/src/components/button/MoreButton.stories.ts b/src/components/button/MoreButton.stories.ts new file mode 100644 index 000000000..1a2171b09 --- /dev/null +++ b/src/components/button/MoreButton.stories.ts @@ -0,0 +1,50 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { Download, ScrollText } from 'lucide-vue-next' + +import IconTextButton from './IconTextButton.vue' +import MoreButton from './MoreButton.vue' + +const meta: Meta = { + title: 'Components/Button/MoreButton', + component: MoreButton, + parameters: { + layout: 'centered' + }, + argTypes: {} +} +export default meta + +type Story = StoryObj + +export const Basic: Story = { + render: () => ({ + components: { MoreButton, IconTextButton, Download, ScrollText }, + template: ` +
+ + + +
+ ` + }) +} diff --git a/src/components/button/TextButton.stories.ts b/src/components/button/TextButton.stories.ts new file mode 100644 index 000000000..c21dc280e --- /dev/null +++ b/src/components/button/TextButton.stories.ts @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import TextButton from './TextButton.vue' + +const meta: Meta = { + title: 'Components/Button/TextButton', + component: TextButton, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text', + defaultValue: 'Click me' + }, + size: { + control: { type: 'select' }, + options: ['sm', 'md'], + defaultValue: 'md' + }, + type: { + control: { type: 'select' }, + options: ['primary', 'secondary', 'transparent'], + defaultValue: 'primary' + }, + onClick: { action: 'clicked' } + } +} + +export default meta +type Story = StoryObj + +export const Primary: Story = { + args: { + label: 'Primary Button', + type: 'primary', + size: 'md' + } +} + +export const Secondary: Story = { + args: { + label: 'Secondary Button', + type: 'secondary', + size: 'md' + } +} + +export const Transparent: Story = { + args: { + label: 'Transparent Button', + type: 'transparent', + size: 'md' + } +} + +export const Small: Story = { + args: { + label: 'Small Button', + type: 'primary', + size: 'sm' + } +} + +export const AllVariants: Story = { + render: () => ({ + components: { TextButton }, + template: ` +
+
+ + +
+
+ + +
+
+ + +
+
+ ` + }) +} diff --git a/src/components/card/Card.stories.ts b/src/components/card/Card.stories.ts new file mode 100644 index 000000000..0d8bf4385 --- /dev/null +++ b/src/components/card/Card.stories.ts @@ -0,0 +1,665 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { + Download, + Folder, + Heart, + Info, + MoreVertical, + Star, + Upload +} from 'lucide-vue-next' +import { ref } from 'vue' + +import IconButton from '../button/IconButton.vue' +import SquareChip from '../chip/SquareChip.vue' +import CardBottom from './CardBottom.vue' +import CardContainer from './CardContainer.vue' +import CardDescription from './CardDescription.vue' +import CardTitle from './CardTitle.vue' +import CardTop from './CardTop.vue' + +interface CardStoryArgs { + // CardContainer props + containerRatio: 'square' | 'portrait' | 'tallPortrait' + maxWidth: number + minWidth: number + + // CardTop props + topRatio: 'square' | 'landscape' + + // Content props + showTopLeft: boolean + showTopRight: boolean + showBottomLeft: boolean + showBottomRight: boolean + showTitle: boolean + showDescription: boolean + title: string + description: string + + // Visual props + backgroundColor: string + showImage: boolean + imageUrl: string + + // Tag props + tags: string[] + showFileSize: boolean + fileSize: string + showFileType: boolean + fileType: string +} + +const meta: Meta = { + title: 'Components/Card/Card', + argTypes: { + containerRatio: { + control: 'select', + options: ['square', 'portrait', 'tallPortrait'], + description: 'Card container aspect ratio' + }, + maxWidth: { + control: { type: 'range', min: 200, max: 600, step: 10 }, + description: 'Maximum width in pixels' + }, + minWidth: { + control: { type: 'range', min: 150, max: 400, step: 10 }, + description: 'Minimum width in pixels' + }, + topRatio: { + control: 'select', + options: ['square', 'landscape'], + description: 'Top section aspect ratio' + }, + showTopLeft: { + control: 'boolean', + description: 'Show top-left slot content' + }, + showTopRight: { + control: 'boolean', + description: 'Show top-right slot content' + }, + showBottomLeft: { + control: 'boolean', + description: 'Show bottom-left slot content' + }, + showBottomRight: { + control: 'boolean', + description: 'Show bottom-right slot content' + }, + showTitle: { + control: 'boolean', + description: 'Show card title' + }, + showDescription: { + control: 'boolean', + description: 'Show card description' + }, + title: { + control: 'text', + description: 'Card title text' + }, + description: { + control: 'text', + description: 'Card description text' + }, + backgroundColor: { + control: 'color', + description: 'Background color for card top' + }, + showImage: { + control: 'boolean', + description: 'Show image instead of color background' + }, + imageUrl: { + control: 'text', + description: 'Image URL for card top' + }, + tags: { + control: 'object', + description: 'Tags to display (array of strings)' + }, + showFileSize: { + control: 'boolean', + description: 'Show file size tag' + }, + fileSize: { + control: 'text', + description: 'File size text' + }, + showFileType: { + control: 'boolean', + description: 'Show file type tag' + }, + fileType: { + control: 'text', + description: 'File type text' + } + } +} + +export default meta +type Story = StoryObj + +const createCardTemplate = (args: CardStoryArgs) => ({ + components: { + CardContainer, + CardTop, + CardBottom, + CardTitle, + CardDescription, + IconButton, + SquareChip, + Info, + Folder, + Heart, + Download, + Star, + Upload, + MoreVertical + }, + setup() { + const favorited = ref(false) + const toggleFavorite = () => { + favorited.value = !favorited.value + } + + return { + args, + favorited, + toggleFavorite + } + }, + template: ` +
+ + + + + +
+ ` +}) + +export const Default: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'portrait', + maxWidth: 300, + minWidth: 200, + topRatio: 'square', + showTopLeft: false, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Model Name', + description: + 'This is a detailed description of the model that can span multiple lines', + backgroundColor: '#3b82f6', + showImage: false, + imageUrl: '', + tags: ['LoRA', 'SDXL'], + showFileSize: true, + fileSize: '1.2 MB', + showFileType: true, + fileType: 'safetensors' + } +} + +export const SquareCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'square', + maxWidth: 400, + minWidth: 250, + topRatio: 'landscape', + showTopLeft: false, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Workflow Bundle', + description: + 'Complete workflow for image generation with all necessary nodes', + backgroundColor: '#10b981', + showImage: false, + imageUrl: '', + tags: ['Workflow'], + showFileSize: true, + fileSize: '245 KB', + showFileType: true, + fileType: 'json' + } +} + +export const TallPortraitCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'tallPortrait', + maxWidth: 280, + minWidth: 180, + topRatio: 'square', + showTopLeft: true, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Premium Model', + description: + 'High-quality photorealistic model trained on professional photography', + backgroundColor: '#8b5cf6', + showImage: false, + imageUrl: '', + tags: ['SD 1.5', 'Checkpoint'], + showFileSize: true, + fileSize: '2.1 GB', + showFileType: true, + fileType: 'ckpt' + } +} + +export const ImageCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'portrait', + maxWidth: 350, + minWidth: 220, + topRatio: 'square', + showTopLeft: false, + showTopRight: true, + showBottomLeft: false, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Generated Image', + description: 'Created with DreamShaper XL', + backgroundColor: '#3b82f6', + showImage: true, + imageUrl: 'https://picsum.photos/400/400', + tags: ['Output'], + showFileSize: true, + fileSize: '856 KB', + showFileType: true, + fileType: 'png' + } +} + +export const MinimalCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'square', + maxWidth: 300, + minWidth: 200, + topRatio: 'landscape', + showTopLeft: false, + showTopRight: false, + showBottomLeft: false, + showBottomRight: false, + showTitle: true, + showDescription: false, + title: 'Simple Card', + description: '', + backgroundColor: '#64748b', + showImage: false, + imageUrl: '', + tags: [], + showFileSize: false, + fileSize: '', + showFileType: false, + fileType: '' + } +} + +export const FullFeaturedCard: Story = { + render: (args: CardStoryArgs) => createCardTemplate(args), + args: { + containerRatio: 'tallPortrait', + maxWidth: 320, + minWidth: 240, + topRatio: 'square', + showTopLeft: true, + showTopRight: true, + showBottomLeft: true, + showBottomRight: true, + showTitle: true, + showDescription: true, + title: 'Ultimate Model Pack', + description: + 'Complete collection with checkpoints, LoRAs, embeddings, and VAE models for professional use', + backgroundColor: '#ef4444', + showImage: false, + imageUrl: '', + tags: ['Bundle', 'Premium', 'SDXL'], + showFileSize: true, + fileSize: '5.4 GB', + showFileType: true, + fileType: 'pack' + } +} + +export const GridOfCards: Story = { + render: () => ({ + components: { + CardContainer, + CardTop, + CardBottom, + CardTitle, + CardDescription, + IconButton, + SquareChip, + Info, + Folder, + Heart, + Download + }, + setup() { + const cards = ref([ + { + id: 1, + title: 'Realistic Vision', + description: 'Photorealistic model for portraits', + color: 'from-blue-400 to-blue-600', + ratio: 'portrait' as const, + tags: ['SD 1.5'], + size: '2.1 GB' + }, + { + id: 2, + title: 'DreamShaper XL', + description: 'Artistic style model with enhanced details', + color: 'from-purple-400 to-pink-600', + ratio: 'portrait' as const, + tags: ['SDXL'], + size: '6.5 GB' + }, + { + id: 3, + title: 'Anime LoRA', + description: 'Character style LoRA', + color: 'from-green-400 to-teal-600', + ratio: 'portrait' as const, + tags: ['LoRA'], + size: '144 MB' + }, + { + id: 4, + title: 'VAE Model', + description: 'Enhanced color VAE', + color: 'from-orange-400 to-red-600', + ratio: 'portrait' as const, + tags: ['VAE'], + size: '335 MB' + }, + { + id: 5, + title: 'Workflow Bundle', + description: 'Complete workflow setup', + color: 'from-indigo-400 to-blue-600', + ratio: 'portrait' as const, + tags: ['Workflow'], + size: '45 KB' + }, + { + id: 6, + title: 'Embedding Pack', + description: 'Negative embeddings collection', + color: 'from-yellow-400 to-orange-600', + ratio: 'portrait' as const, + tags: ['Embedding'], + size: '2.3 MB' + } + ]) + + return { cards } + }, + template: ` +
+

Model Gallery

+
+ + + + + +
+
+ ` + }) +} + +export const ResponsiveGrid: Story = { + render: () => ({ + components: { + CardContainer, + CardTop, + CardBottom, + CardTitle, + CardDescription, + SquareChip + }, + setup() { + const generateCards = ( + count: number, + ratio: 'square' | 'portrait' | 'tallPortrait' + ) => { + return Array.from({ length: count }, (_, i) => ({ + id: i + 1, + title: `Model ${i + 1}`, + description: `Description for model ${i + 1}`, + ratio, + color: `hsl(${(i * 60) % 360}, 70%, 60%)` + })) + } + + const squareCards = ref(generateCards(4, 'square')) + const portraitCards = ref(generateCards(6, 'portrait')) + const tallCards = ref(generateCards(5, 'tallPortrait')) + + return { + squareCards, + portraitCards, + tallCards + } + }, + template: ` +
+
+

Square Cards (1:1)

+
+ + + + +
+
+ +
+

Portrait Cards (2:3)

+
+ + + + +
+
+ +
+

Tall Portrait Cards (2:4)

+
+ + + + +
+
+
+ ` + }), + parameters: { + controls: { disable: true }, + actions: { disable: true } + } +} diff --git a/src/components/card/CardContainer.vue b/src/components/card/CardContainer.vue index 597429a9e..ebcac78e8 100644 --- a/src/components/card/CardContainer.vue +++ b/src/components/card/CardContainer.vue @@ -13,8 +13,8 @@ const { maxWidth, minWidth } = defineProps<{ - maxWidth: number - minWidth: number + maxWidth?: number + minWidth?: number ratio?: 'square' | 'portrait' | 'tallPortrait' }>() @@ -31,8 +31,12 @@ const containerClasses = computed(() => { return `${baseClasses} ${ratioClasses[ratio]}` }) -const containerStyle = computed(() => ({ - maxWidth: `${maxWidth}px`, - minWidth: `${minWidth}px` -})) +const containerStyle = computed(() => + maxWidth || minWidth + ? { + maxWidth: `${maxWidth}px`, + minWidth: `${minWidth}px` + } + : {} +) diff --git a/src/components/chip/SquareChip.stories.ts b/src/components/chip/SquareChip.stories.ts new file mode 100644 index 000000000..6ae12b1e9 --- /dev/null +++ b/src/components/chip/SquareChip.stories.ts @@ -0,0 +1,36 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import SquareChip from './SquareChip.vue' + +const meta: Meta = { + title: 'Components/SquareChip', + component: SquareChip, + tags: ['autodocs'], + argTypes: { + label: { + control: 'text', + defaultValue: 'Tag' + } + } +} + +export default meta +type Story = StoryObj + +export const TagList: Story = { + render: () => ({ + components: { SquareChip }, + template: ` +
+ + + + + + + + +
+ ` + }) +} diff --git a/src/components/dialog/content/SettingDialogContent.vue b/src/components/dialog/content/SettingDialogContent.vue index 9d676ed0c..c9c71f25a 100644 --- a/src/components/dialog/content/SettingDialogContent.vue +++ b/src/components/dialog/content/SettingDialogContent.vue @@ -41,7 +41,6 @@ > @@ -76,7 +75,6 @@ import { flattenTree } from '@/utils/treeUtil' import ColorPaletteMessage from './setting/ColorPaletteMessage.vue' import CurrentUserMessage from './setting/CurrentUserMessage.vue' -import FirstTimeUIMessage from './setting/FirstTimeUIMessage.vue' import PanelTemplate from './setting/PanelTemplate.vue' import SettingsPanel from './setting/SettingsPanel.vue' diff --git a/src/components/dialog/content/setting/FirstTimeUIMessage.vue b/src/components/dialog/content/setting/FirstTimeUIMessage.vue deleted file mode 100644 index 82794923f..000000000 --- a/src/components/dialog/content/setting/FirstTimeUIMessage.vue +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 335e6427f..b8b8baa4f 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -2,13 +2,11 @@ - -