diff --git a/.claude/commands/comprehensive-pr-review.md b/.claude/commands/comprehensive-pr-review.md index 84708564e..1b4047e78 100644 --- a/.claude/commands/comprehensive-pr-review.md +++ b/.claude/commands/comprehensive-pr-review.md @@ -67,9 +67,9 @@ This is critical for better file inspection: Use git locally for much faster analysis: -1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt` -2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt` -3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt` +1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt` +2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt` +3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt` ### Step 1.5: Create Analysis Cache diff --git a/.gitattributes b/.gitattributes index de05efbf4..bd0518cde 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,4 +13,4 @@ # Generated files src/types/comfyRegistryTypes.ts linguist-generated=true -src/types/generatedManagerTypes.ts linguist-generated=true +src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml index 907695e57..178bd4ee8 100644 --- a/.github/workflows/backport.yaml +++ b/.github/workflows/backport.yaml @@ -4,10 +4,25 @@ on: pull_request_target: types: [closed, labeled] branches: [main] + workflow_dispatch: + inputs: + pr_number: + description: 'PR number to backport' + required: true + type: string + force_rerun: + description: 'Force rerun even if backports exist' + required: false + type: boolean + default: false jobs: backport: - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport') + if: > + (github.event_name == 'pull_request_target' && + github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'needs-backport')) || + github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest permissions: contents: write @@ -15,6 +30,35 @@ jobs: issues: write steps: + - name: Validate inputs for manual triggers + if: github.event_name == 'workflow_dispatch' + run: | + # Validate PR number format + if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid PR number format. Must be a positive integer." + exit 1 + fi + + # Validate PR exists and is merged + if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then + echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible." + exit 1 + fi + + MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged') + if [ "$MERGED" != "true" ]; then + echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported." + exit 1 + fi + + # Validate PR has needs-backport label + if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then + echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label." + exit 1 + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository uses: actions/checkout@v4 with: @@ -29,7 +73,7 @@ jobs: id: check-existing env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | # Check for existing backport PRs for this PR number EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName') @@ -39,6 +83,13 @@ jobs: exit 0 fi + # For manual triggers with force_rerun, proceed anyway + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then + echo "skip=false" >> $GITHUB_OUTPUT + echo "::warning::Force rerun requested - existing backports will be updated" + exit 0 + fi + echo "Found existing backport PRs:" echo "$EXISTING_BACKPORTS" echo "skip=true" >> $GITHUB_OUTPUT @@ -50,8 +101,17 @@ jobs: run: | # Extract version labels (e.g., "1.24", "1.22") VERSIONS="" - LABELS='${{ toJSON(github.event.pull_request.labels) }}' - for label in $(echo "$LABELS" | jq -r '.[].name'); do + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # For manual triggers, get labels from the PR + LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name') + else + # For automatic triggers, extract from PR event + LABELS='${{ toJSON(github.event.pull_request.labels) }}' + LABELS=$(echo "$LABELS" | jq -r '.[].name') + fi + + for label in $LABELS; do # Match version labels like "1.24" (major.minor only) if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then # Validate the branch exists before adding to list @@ -75,12 +135,20 @@ jobs: if: steps.check-existing.outputs.skip != 'true' id: backport env: - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_TITLE: ${{ github.event.pull_request.title }} - MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | FAILED="" SUCCESS="" + + # Get PR data for manual triggers + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit) + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') + else + PR_TITLE="${{ github.event.pull_request.title }}" + MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + fi for version in ${{ steps.versions.outputs.versions }}; do echo "::group::Backporting to core/${version}" @@ -133,10 +201,18 @@ jobs: if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success env: GH_TOKEN: ${{ secrets.PR_GH_TOKEN }} - PR_TITLE: ${{ github.event.pull_request.title }} - PR_NUMBER: ${{ github.event.pull_request.number }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} run: | + # Get PR data for manual triggers + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author) + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title') + PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') + else + PR_TITLE="${{ github.event.pull_request.title }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + fi + for backport in ${{ steps.backport.outputs.success }}; do IFS=':' read -r version branch <<< "${backport}" @@ -165,9 +241,16 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - PR_NUMBER="${{ github.event.pull_request.number }}" - PR_AUTHOR="${{ github.event.pull_request.user.login }}" - MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit) + PR_NUMBER="${{ inputs.pr_number }}" + PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login') + MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid') + else + PR_NUMBER="${{ github.event.pull_request.number }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}" + fi for failure in ${{ steps.backport.outputs.failed }}; do IFS=':' read -r version reason conflicts <<< "${failure}" diff --git a/.github/workflows/publish-frontend-types.yaml b/.github/workflows/publish-frontend-types.yaml index 398f5e0a7..142a22a93 100644 --- a/.github/workflows/publish-frontend-types.yaml +++ b/.github/workflows/publish-frontend-types.yaml @@ -88,6 +88,8 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + env: + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' - name: Build types run: pnpm build:types @@ -131,7 +133,7 @@ jobs: - name: Publish package if: steps.check_npm.outputs.exists == 'false' - run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" + run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks working-directory: dist env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 5a58d1b1a..32e1b6624 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ components.d.ts tests-ui/data/* tests-ui/ComfyUI_examples tests-ui/workflows/examples +coverage/ # Browser tests /test-results/ @@ -78,8 +79,8 @@ vite.config.mts.timestamp-*.mjs *storybook.log storybook-static - - +# MCP Servers +.playwright-mcp/* .nx/cache .nx/workspace-data diff --git a/.i18nrc.cjs b/.i18nrc.cjs index 0429c3578..2efe5f966 100644 --- a/.i18nrc.cjs +++ b/.i18nrc.cjs @@ -9,7 +9,7 @@ module.exports = defineConfig({ entry: 'src/locales/en', entryLocale: 'en', output: 'src/locales', - outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'], + outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'], reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream. 'latent' is the short form of 'latent space'. 'mask' is in the context of image processing. diff --git a/.storybook/main.ts b/.storybook/main.ts index a799ec143..aa6bb1fbd 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -15,21 +15,32 @@ const config: StorybookConfig = { async viteFinal(config) { // Use dynamic import to avoid CJS deprecation warning const { mergeConfig } = await import('vite') + const { default: tailwindcss } = await import('@tailwindcss/vite') // Filter out any plugins that might generate import maps if (config.plugins) { - config.plugins = config.plugins.filter((plugin: any) => { - if (plugin && plugin.name && plugin.name.includes('import-map')) { - return false - } - return true - }) + config.plugins = config.plugins + // Type guard: ensure we have valid plugin objects with names + .filter( + (plugin): plugin is NonNullable & { name: string } => { + return ( + plugin !== null && + plugin !== undefined && + typeof plugin === 'object' && + 'name' in plugin && + typeof plugin.name === 'string' + ) + } + ) + // Business logic: filter out import-map plugins + .filter((plugin) => !plugin.name.includes('import-map')) } return mergeConfig(config, { // Replace plugins entirely to avoid inheritance issues plugins: [ // Only include plugins we explicitly need for Storybook + tailwindcss(), Icons({ compiler: 'vue3', customCollections: { diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 747bbe802..bfe81f431 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,7 +1,7 @@ import { definePreset } from '@primevue/themes' import Aura from '@primevue/themes/aura' import { setup } from '@storybook/vue3' -import type { Preview } from '@storybook/vue3-vite' +import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite' import { createPinia } from 'pinia' import 'primeicons/primeicons.css' import PrimeVue from 'primevue/config' @@ -9,11 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice' import ToastService from 'primevue/toastservice' import Tooltip from 'primevue/tooltip' -import '../src/assets/css/style.css' -import { i18n } from '../src/i18n' -import '../src/lib/litegraph/public/css/litegraph.css' -import { useWidgetStore } from '../src/stores/widgetStore' -import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore' +import '@/assets/css/style.css' +import { i18n } from '@/i18n' +import '@/lib/litegraph/public/css/litegraph.css' const ComfyUIPreset = definePreset(Aura, { semantic: { @@ -25,13 +23,11 @@ const ComfyUIPreset = definePreset(Aura, { // Setup Vue app for Storybook setup((app) => { app.directive('tooltip', Tooltip) + + // Create Pinia instance const pinia = createPinia() + app.use(pinia) - - // Initialize stores - useColorPaletteStore(pinia) - useWidgetStore(pinia) - app.use(i18n) app.use(PrimeVue, { theme: { @@ -50,8 +46,8 @@ setup((app) => { app.use(ToastService) }) -// Dark theme decorator -export const withTheme = (Story: any, context: any) => { +// Theme and dialog decorator +export const withTheme = (Story: StoryFn, context: StoryContext) => { const theme = context.globals.theme || 'light' // Apply theme class to document root @@ -63,7 +59,7 @@ export const withTheme = (Story: any, context: any) => { document.body.classList.remove('dark-theme') } - return Story() + return Story(context.args, context) } const preview: Preview = { diff --git a/CODEOWNERS b/CODEOWNERS index 8d4e4a90f..cd1b4e508 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,17 +1,61 @@ -# Admins -* @Comfy-Org/comfy_frontend_devs +# Desktop/Electron +/src/types/desktop/ @webfiltered +/src/constants/desktopDialogs.ts @webfiltered +/src/constants/desktopMaintenanceTasks.ts @webfiltered +/src/stores/electronDownloadStore.ts @webfiltered +/src/extensions/core/electronAdapter.ts @webfiltered +/src/views/DesktopDialogView.vue @webfiltered +/src/components/install/ @webfiltered +/src/components/maintenance/ @webfiltered +/vite.electron.config.mts @webfiltered -# Maintainers -*.md @Comfy-Org/comfy_maintainer -/tests-ui/ @Comfy-Org/comfy_maintainer -/browser_tests/ @Comfy-Org/comfy_maintainer -/.env_example @Comfy-Org/comfy_maintainer +# Common UI Components +/src/components/chip/ @viva-jinyi +/src/components/card/ @viva-jinyi +/src/components/button/ @viva-jinyi +/src/components/input/ @viva-jinyi -# Translations (AIGODLIKE team + shinshin86) -/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer +# Topbar +/src/components/topbar/ @pythongosssss -# Load 3D extension -/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs +# Thumbnail +/src/renderer/core/thumbnail/ @pythongosssss -# Mask Editor extension -/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs +# Legacy UI +/scripts/ui/ @pythongosssss + +# Link rendering +/src/renderer/core/canvas/links/ @benceruleanlu + +# Node help system +/src/utils/nodeHelpUtil.ts @benceruleanlu +/src/stores/workspace/nodeHelpStore.ts @benceruleanlu +/src/services/nodeHelpService.ts @benceruleanlu + +# Selection toolbox +/src/components/graph/selectionToolbox/ @Myestery + +# Minimap +/src/renderer/extensions/minimap/ @jtydhr88 + +# Assets +/src/platform/assets/ @arjansingh + +# Workflow Templates +/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki +/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki + +# Mask Editor +/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp +/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp +/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp + +# 3D +/src/extensions/core/load3d.ts @jtydhr88 +/src/components/load3d/ @jtydhr88 + +# Manager +/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata + +# Translations +/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer diff --git a/browser_tests/assets/vueNodes/simple-triple.json b/browser_tests/assets/vueNodes/simple-triple.json new file mode 100644 index 000000000..9b665191d --- /dev/null +++ b/browser_tests/assets/vueNodes/simple-triple.json @@ -0,0 +1 @@ +{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4} \ No newline at end of file diff --git a/browser_tests/fixtures/UserSelectPage.ts b/browser_tests/fixtures/UserSelectPage.ts index 62a961375..ff0735e17 100644 --- a/browser_tests/fixtures/UserSelectPage.ts +++ b/browser_tests/fixtures/UserSelectPage.ts @@ -1,4 +1,5 @@ -import { Page, test as base } from '@playwright/test' +import type { Page } from '@playwright/test' +import { test as base } from '@playwright/test' export class UserSelectPage { constructor( diff --git a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts index 23dc104cf..fd40ca911 100644 --- a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts +++ b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' export class ComfyNodeSearchFilterSelectionPanel { constructor(public readonly page: Page) {} diff --git a/browser_tests/fixtures/components/SettingDialog.ts b/browser_tests/fixtures/components/SettingDialog.ts index afaf86154..e9040a3a9 100644 --- a/browser_tests/fixtures/components/SettingDialog.ts +++ b/browser_tests/fixtures/components/SettingDialog.ts @@ -1,6 +1,6 @@ -import { Page } from '@playwright/test' +import type { Page } from '@playwright/test' -import { ComfyPage } from '../ComfyPage' +import type { ComfyPage } from '../ComfyPage' export class SettingDialog { constructor( diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts index 7baaa1ef9..f3fbe42cf 100644 --- a/browser_tests/fixtures/components/SidebarTab.ts +++ b/browser_tests/fixtures/components/SidebarTab.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' class SidebarTab { constructor( diff --git a/browser_tests/fixtures/components/Topbar.ts b/browser_tests/fixtures/components/Topbar.ts index 04a9117ce..6d0cd1fb3 100644 --- a/browser_tests/fixtures/components/Topbar.ts +++ b/browser_tests/fixtures/components/Topbar.ts @@ -1,4 +1,5 @@ -import { Locator, Page, expect } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' export class Topbar { private readonly menuLocator: Locator diff --git a/browser_tests/fixtures/ws.ts b/browser_tests/fixtures/ws.ts index e12c53465..f1ab1a538 100644 --- a/browser_tests/fixtures/ws.ts +++ b/browser_tests/fixtures/ws.ts @@ -12,9 +12,10 @@ export const webSocketFixture = base.extend<{ // so we can look it up to trigger messages const store: Record = ((window as any).__ws__ = {}) window.WebSocket = class extends window.WebSocket { - constructor() { - // @ts-expect-error - super(...arguments) + constructor( + ...rest: ConstructorParameters + ) { + super(...rest) store[this.url] = this } } diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts index 12033fce3..881ef11c4 100644 --- a/browser_tests/globalSetup.ts +++ b/browser_tests/globalSetup.ts @@ -1,4 +1,4 @@ -import { FullConfig } from '@playwright/test' +import type { FullConfig } from '@playwright/test' import dotenv from 'dotenv' import { backupPath } from './utils/backupUtils' diff --git a/browser_tests/globalTeardown.ts b/browser_tests/globalTeardown.ts index 47bab3db9..aeed77294 100644 --- a/browser_tests/globalTeardown.ts +++ b/browser_tests/globalTeardown.ts @@ -1,4 +1,4 @@ -import { FullConfig } from '@playwright/test' +import type { FullConfig } from '@playwright/test' import dotenv from 'dotenv' import { restorePath } from './utils/backupUtils' diff --git a/browser_tests/helpers/fitToView.ts b/browser_tests/helpers/fitToView.ts new file mode 100644 index 000000000..af6c10e9d --- /dev/null +++ b/browser_tests/helpers/fitToView.ts @@ -0,0 +1,104 @@ +import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces' +import type { ComfyPage } from '../fixtures/ComfyPage' + +interface FitToViewOptions { + selectionOnly?: boolean + zoom?: number + padding?: number +} + +/** + * Instantly fits the canvas view to graph content without waiting for UI animation. + * + * Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented. + */ +export async function fitToViewInstant( + comfyPage: ComfyPage, + options: FitToViewOptions = {} +) { + const { selectionOnly = false, zoom = 0.75, padding = 10 } = options + + const rectangles = await comfyPage.page.evaluate< + ReadOnlyRect[] | null, + { selectionOnly: boolean } + >( + ({ selectionOnly }) => { + const app = window['app'] + if (!app?.canvas) return null + + const canvas = app.canvas + const items = (() => { + if (selectionOnly && canvas.selectedItems?.size) { + return Array.from(canvas.selectedItems) + } + try { + return Array.from(canvas.positionableItems ?? []) + } catch { + return [] + } + })() + + if (!items.length) return null + + const rects: ReadOnlyRect[] = [] + + for (const item of items) { + const rect = item?.boundingRect + if (!rect) continue + + const x = Number(rect[0]) + const y = Number(rect[1]) + const width = Number(rect[2]) + const height = Number(rect[3]) + + rects.push([x, y, width, height] as const) + } + + return rects.length ? rects : null + }, + { selectionOnly } + ) + + if (!rectangles || rectangles.length === 0) return + + let minX = Infinity + let minY = Infinity + let maxX = -Infinity + let maxY = -Infinity + + for (const [x, y, width, height] of rectangles) { + minX = Math.min(minX, Number(x)) + minY = Math.min(minY, Number(y)) + maxX = Math.max(maxX, Number(x) + Number(width)) + maxY = Math.max(maxY, Number(y) + Number(height)) + } + + const hasFiniteBounds = + Number.isFinite(minX) && + Number.isFinite(minY) && + Number.isFinite(maxX) && + Number.isFinite(maxY) + + if (!hasFiniteBounds) return + + const bounds: ReadOnlyRect = [ + minX - padding, + minY - padding, + maxX - minX + 2 * padding, + maxY - minY + 2 * padding + ] + + await comfyPage.page.evaluate( + ({ bounds, zoom }) => { + const app = window['app'] + if (!app?.canvas) return + + const canvas = app.canvas + canvas.ds.fitToBounds(bounds, { zoom }) + canvas.setDirty(true, true) + }, + { bounds, zoom } + ) + + await comfyPage.nextFrame() +} diff --git a/browser_tests/helpers/manageGroupNode.ts b/browser_tests/helpers/manageGroupNode.ts index a444a97c6..45010b979 100644 --- a/browser_tests/helpers/manageGroupNode.ts +++ b/browser_tests/helpers/manageGroupNode.ts @@ -1,4 +1,4 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' export class ManageGroupNode { footer: Locator diff --git a/browser_tests/helpers/templates.ts b/browser_tests/helpers/templates.ts index 0d2c9f31e..c690b8702 100644 --- a/browser_tests/helpers/templates.ts +++ b/browser_tests/helpers/templates.ts @@ -1,7 +1,7 @@ -import { Locator, Page } from '@playwright/test' +import type { Locator, Page } from '@playwright/test' import path from 'path' -import { +import type { TemplateInfo, WorkflowTemplates } from '../../src/platform/workflow/templates/types/template' diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts index a504ea4fc..b23e4466d 100644 --- a/browser_tests/tests/actionbar.spec.ts +++ b/browser_tests/tests/actionbar.spec.ts @@ -29,9 +29,9 @@ test.describe('Actionbar', () => { // Intercept the prompt queue endpoint let promptNumber = 0 - comfyPage.page.route('**/api/prompt', async (route, req) => { + await comfyPage.page.route('**/api/prompt', async (route, req) => { await new Promise((r) => setTimeout(r, 100)) - route.fulfill({ + await route.fulfill({ status: 200, body: JSON.stringify({ prompt_id: promptNumber, diff --git a/browser_tests/tests/changeTracker.spec.ts b/browser_tests/tests/changeTracker.spec.ts index 7a32833e4..8c23c835a 100644 --- a/browser_tests/tests/changeTracker.spec.ts +++ b/browser_tests/tests/changeTracker.spec.ts @@ -1,5 +1,5 @@ +import type { ComfyPage } from '../fixtures/ComfyPage' import { - ComfyPage, comfyExpect as expect, comfyPageFixture as test } from '../fixtures/ComfyPage' diff --git a/browser_tests/tests/chatHistory.spec.ts b/browser_tests/tests/chatHistory.spec.ts index db3397514..7d1bf6c10 100644 --- a/browser_tests/tests/chatHistory.spec.ts +++ b/browser_tests/tests/chatHistory.spec.ts @@ -1,4 +1,5 @@ -import { Page, expect } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index cf2e5e6be..c86466215 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -1,4 +1,5 @@ -import { Locator, expect } from '@playwright/test' +import type { Locator } from '@playwright/test' +import { expect } from '@playwright/test' import type { Keybinding } from '../../src/schemas/keyBindingSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' diff --git a/browser_tests/tests/extensionAPI.spec.ts b/browser_tests/tests/extensionAPI.spec.ts index 09a08384c..38f4a6c1d 100644 --- a/browser_tests/tests/extensionAPI.spec.ts +++ b/browser_tests/tests/extensionAPI.spec.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test' -import { SettingParams } from '../../src/platform/settings/types' +import type { SettingParams } from '../../src/platform/settings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Topbar commands', () => { diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts index 41b50224a..fc8dbd646 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test' -import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.describe('Group Node', () => { diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index de46bca2e..bd14f91ad 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -1,12 +1,13 @@ -import { Locator, expect } from '@playwright/test' -import { Position } from '@vueuse/core' +import type { Locator } from '@playwright/test' +import { expect } from '@playwright/test' +import type { Position } from '@vueuse/core' import { type ComfyPage, comfyPageFixture as test, testComfySnapToGridGridSize } from '../fixtures/ComfyPage' -import { type NodeReference } from '../fixtures/utils/litegraphUtils' +import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.describe('Item Interaction', () => { test('Can select/delete all items', async ({ comfyPage }) => { @@ -1012,6 +1013,8 @@ test.describe('Canvas Navigation', () => { test('Shift + mouse wheel should pan canvas horizontally', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning') + await comfyPage.page.click('canvas') await comfyPage.nextFrame() diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png index 57b6438ae..a9d0efb74 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-center-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png index a9d0efb74..57a92edc5 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-left-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png index 57b6438ae..e607294e3 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/standard-shift-wheel-pan-right-chromium-linux.png differ diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts index 05bb578df..7a54cae07 100644 --- a/browser_tests/tests/remoteWidgets.spec.ts +++ b/browser_tests/tests/remoteWidgets.spec.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test' -import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Remote COMBO Widget', () => { const mockOptions = ['d', 'c', 'b', 'a'] diff --git a/browser_tests/tests/sidebar/queue.spec.ts b/browser_tests/tests/sidebar/queue.spec.ts index 2d9dd10ba..39e2ced6e 100644 --- a/browser_tests/tests/sidebar/queue.spec.ts +++ b/browser_tests/tests/sidebar/queue.spec.ts @@ -160,7 +160,9 @@ test.describe.skip('Queue sidebar', () => { comfyPage }) => { await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(firstImage) + ).toBeVisible() }) test('maintains active gallery item when new tasks are added', async ({ @@ -174,7 +176,9 @@ test.describe.skip('Queue sidebar', () => { const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage) await newTask.waitFor({ state: 'visible' }) // The active gallery item should still be the initial image - expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(firstImage) + ).toBeVisible() }) test.describe('Gallery navigation', () => { @@ -196,7 +200,9 @@ test.describe.skip('Queue sidebar', () => { delay: 256 }) await comfyPage.nextFrame() - expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible() + await expect( + comfyPage.menu.queueTab.getGalleryImage(end) + ).toBeVisible() }) }) }) diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index f2c2e2bb5..9141e9135 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -1,4 +1,5 @@ -import { Page, expect } from '@playwright/test' +import type { Page } from '@playwright/test' +import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' diff --git a/browser_tests/tests/versionMismatchWarnings.spec.ts b/browser_tests/tests/versionMismatchWarnings.spec.ts index d85f18723..b2c62aeb0 100644 --- a/browser_tests/tests/versionMismatchWarnings.spec.ts +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test' -import { SystemStats } from '../../src/schemas/apiSchema' +import type { SystemStats } from '../../src/schemas/apiSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Version Mismatch Warnings', () => { diff --git a/browser_tests/tests/vueNodes/NodeHeader.spec.ts b/browser_tests/tests/vueNodes/NodeHeader.spec.ts index 7a8ae5dd2..336e2672d 100644 --- a/browser_tests/tests/vueNodes/NodeHeader.spec.ts +++ b/browser_tests/tests/vueNodes/NodeHeader.spec.ts @@ -6,7 +6,7 @@ import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures' test.describe('NodeHeader', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled') + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) diff --git a/browser_tests/tests/vueNodes/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/linkInteraction.spec.ts new file mode 100644 index 000000000..d6b1bccc1 --- /dev/null +++ b/browser_tests/tests/vueNodes/linkInteraction.spec.ts @@ -0,0 +1,221 @@ +import type { Locator } from '@playwright/test' + +import { getSlotKey } from '../../../src/renderer/core/layout/slots/slotIdentifier' +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../fixtures/ComfyPage' +import { fitToViewInstant } from '../../helpers/fitToView' + +async function getCenter(locator: Locator): Promise<{ x: number; y: number }> { + const box = await locator.boundingBox() + if (!box) throw new Error('Slot bounding box not available') + return { + x: box.x + box.width / 2, + y: box.y + box.height / 2 + } +} + +test.describe('Vue Node Link Interaction', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.setup() + await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.vueNodes.waitForNodes() + await fitToViewInstant(comfyPage) + }) + + test('should show a link dragging out from a slot when dragging on a slot', async ({ + comfyPage, + comfyMouse + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + + const samplerNode = samplerNodes[0] + const outputSlot = await samplerNode.getOutput(0) + await outputSlot.removeLinks() + await comfyPage.nextFrame() + + const slotKey = getSlotKey(String(samplerNode.id), 0, false) + const slotLocator = comfyPage.page.locator(`[data-slot-key="${slotKey}"]`) + await expect(slotLocator).toBeVisible() + + const start = await getCenter(slotLocator) + const canvasBox = await comfyPage.canvas.boundingBox() + if (!canvasBox) throw new Error('Canvas bounding box not available') + + // Arbitrary value + const dragTarget = { + x: start.x + 180, + y: start.y - 140 + } + + await comfyMouse.move(start) + await comfyMouse.drag(dragTarget) + await comfyPage.nextFrame() + + try { + await expect(comfyPage.canvas).toHaveScreenshot( + 'vue-node-dragging-link.png' + ) + } finally { + await comfyMouse.drop() + } + }) + + test('should create a link when dropping on a compatible slot', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + expect(vaeNodes.length).toBeGreaterThan(0) + const vaeNode = vaeNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const vaeInput = await vaeNode.getInput(0) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(vaeNode.id), 0, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(1) + expect(await vaeInput.getLinkCount()).toBe(1) + + const linkDetails = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph ?? app?.graph + if (!graph) return null + + const source = graph.getNodeById(sourceId) + if (!source) return null + + const linkId = source.outputs[0]?.links?.[0] + if (linkId == null) return null + + const link = graph.links[linkId] + if (!link) return null + + return { + originId: link.origin_id, + originSlot: link.origin_slot, + targetId: link.target_id, + targetSlot: link.target_slot + } + }, samplerNode.id) + + expect(linkDetails).not.toBeNull() + expect(linkDetails).toMatchObject({ + originId: samplerNode.id, + originSlot: 0, + targetId: vaeNode.id, + targetSlot: 0 + }) + }) + + test('should not create a link when slot types are incompatible', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + expect(clipNodes.length).toBeGreaterThan(0) + const clipNode = clipNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const clipInput = await clipNode.getInput(0) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(clipNode.id), 0, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(0) + expect(await clipInput.getLinkCount()).toBe(0) + + const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph ?? app?.graph + if (!graph) return 0 + + const source = graph.getNodeById(sourceId) + if (!source) return 0 + + return source.outputs[0]?.links?.length ?? 0 + }, samplerNode.id) + + expect(graphLinkCount).toBe(0) + }) + + test('should not create a link when dropping onto a slot on the same node', async ({ + comfyPage + }) => { + const samplerNodes = await comfyPage.getNodeRefsByType('KSampler') + expect(samplerNodes.length).toBeGreaterThan(0) + const samplerNode = samplerNodes[0] + + const samplerOutput = await samplerNode.getOutput(0) + const samplerInput = await samplerNode.getInput(3) + + const outputSlotKey = getSlotKey(String(samplerNode.id), 0, false) + const inputSlotKey = getSlotKey(String(samplerNode.id), 3, true) + + const outputSlot = comfyPage.page.locator( + `[data-slot-key="${outputSlotKey}"]` + ) + const inputSlot = comfyPage.page.locator( + `[data-slot-key="${inputSlotKey}"]` + ) + + await expect(outputSlot).toBeVisible() + await expect(inputSlot).toBeVisible() + + await outputSlot.dragTo(inputSlot) + await comfyPage.nextFrame() + + expect(await samplerOutput.getLinkCount()).toBe(0) + expect(await samplerInput.getLinkCount()).toBe(0) + + const graphLinkCount = await comfyPage.page.evaluate((sourceId) => { + const app = window['app'] + const graph = app?.canvas?.graph ?? app?.graph + if (!graph) return 0 + + const source = graph.getNodeById(sourceId) + if (!source) return 0 + + return source.outputs[0]?.links?.length ?? 0 + }, samplerNode.id) + + expect(graphLinkCount).toBe(0) + }) +}) diff --git a/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png new file mode 100644 index 000000000..d4c32b4ea Binary files /dev/null and b/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeInteractions/selectionState.spec.ts b/browser_tests/tests/vueNodes/nodeInteractions/selectionState.spec.ts new file mode 100644 index 000000000..ff8b6f951 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeInteractions/selectionState.spec.ts @@ -0,0 +1,47 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +test.describe('Vue Node Selection', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + const modifiers = [ + { key: 'Control', name: 'ctrl' }, + { key: 'Shift', name: 'shift' } + ] as const + + for (const { key: modifier, name } of modifiers) { + test(`should allow selecting multiple nodes with ${name}+click`, async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + + await comfyPage.page.getByText('Empty Latent Image').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2) + + await comfyPage.page.getByText('KSampler').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3) + }) + + test(`should allow de-selecting nodes with ${name}+click`, async ({ + comfyPage + }) => { + await comfyPage.page.getByText('Load Checkpoint').click() + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1) + + await comfyPage.page.getByText('Load Checkpoint').click({ + modifiers: [modifier] + }) + expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0) + }) + } +}) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts new file mode 100644 index 000000000..c80a86503 --- /dev/null +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -0,0 +1,49 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../fixtures/ComfyPage' + +const BYPASS_HOTKEY = 'Control+b' +const BYPASS_CLASS = /before:bg-bypass\/60/ + +test.describe('Vue Node Bypass', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.vueNodes.waitForNodes() + }) + + test('should allow toggling bypass on a selected node with hotkey', async ({ + comfyPage + }) => { + const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({ + hasText: 'Load Checkpoint' + }) + await checkpointNode.getByText('Load Checkpoint').click() + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) + }) + + test('should allow toggling bypass on multiple selected nodes with hotkey', async ({ + comfyPage + }) => { + const checkpointNode = comfyPage.page.locator('[data-node-id]').filter({ + hasText: 'Load Checkpoint' + }) + const ksamplerNode = comfyPage.page.locator('[data-node-id]').filter({ + hasText: 'KSampler' + }) + + await checkpointNode.getByText('Load Checkpoint').click() + await ksamplerNode.getByText('KSampler').click({ modifiers: ['Control'] }) + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).toHaveClass(BYPASS_CLASS) + await expect(ksamplerNode).toHaveClass(BYPASS_CLASS) + + await comfyPage.page.keyboard.press(BYPASS_HOTKEY) + await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS) + await expect(ksamplerNode).not.toHaveClass(BYPASS_CLASS) + }) +}) diff --git a/tsconfig.eslint.json b/browser_tests/tsconfig.json similarity index 53% rename from tsconfig.eslint.json rename to browser_tests/tsconfig.json index 9b1635700..f600c4a7f 100644 --- a/tsconfig.eslint.json +++ b/browser_tests/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.json", + "extends": "../tsconfig.json", "compilerOptions": { /* Test files should not be compiled */ "noEmit": true, @@ -9,13 +9,6 @@ "resolveJsonModule": true }, "include": [ - "*.ts", - "*.mts", - "*.config.js", - "browser_tests/**/*.ts", - "scripts/**/*.js", - "scripts/**/*.ts", - "tests-ui/**/*.ts", - ".storybook/**/*.ts" + "**/*.ts", ] } diff --git a/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts index 3f795a219..5b7f4bec4 100644 --- a/build/plugins/comfyAPIPlugin.ts +++ b/build/plugins/comfyAPIPlugin.ts @@ -1,5 +1,5 @@ import path from 'path' -import { Plugin } from 'vite' +import type { Plugin } from 'vite' interface ShimResult { code: string diff --git a/build/plugins/generateImportMapPlugin.ts b/build/plugins/generateImportMapPlugin.ts index 80ccb6c9f..bbbf14c2c 100644 --- a/build/plugins/generateImportMapPlugin.ts +++ b/build/plugins/generateImportMapPlugin.ts @@ -1,7 +1,7 @@ import glob from 'fast-glob' import fs from 'fs-extra' import { dirname, join } from 'node:path' -import { HtmlTagDescriptor, Plugin, normalizePath } from 'vite' +import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite' interface ImportMapSource { name: string diff --git a/eslint.config.js b/eslint.config.ts similarity index 70% rename from eslint.config.js rename to eslint.config.ts index cddba3bbd..04f4b2578 100644 --- a/eslint.config.js +++ b/eslint.config.ts @@ -5,13 +5,14 @@ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' import storybook from 'eslint-plugin-storybook' import unusedImports from 'eslint-plugin-unused-imports' import pluginVue from 'eslint-plugin-vue' +import { defineConfig } from 'eslint/config' import globals from 'globals' import tseslint from 'typescript-eslint' +import vueParser from 'vue-eslint-parser' -export default [ - { - files: ['src/**/*.{js,mjs,cjs,ts,vue}'] - }, +const extraFileExtensions = ['.vue'] + +export default defineConfig([ { ignores: [ 'src/scripts/*', @@ -24,35 +25,49 @@ export default [ ] }, { + files: ['./**/*.{ts,mts}'], languageOptions: { globals: { ...globals.browser, __COMFYUI_FRONTEND_VERSION__: 'readonly' }, - parser: tseslint.parser, parserOptions: { - project: ['./tsconfig.json', './tsconfig.eslint.json'], + parser: tseslint.parser, + projectService: true, + tsConfigRootDir: import.meta.dirname, ecmaVersion: 2020, sourceType: 'module', - extraFileExtensions: ['.vue'] + extraFileExtensions + } + } + }, + { + files: ['./**/*.vue'], + languageOptions: { + globals: { + ...globals.browser, + __COMFYUI_FRONTEND_VERSION__: 'readonly' + }, + parser: vueParser, + parserOptions: { + parser: tseslint.parser, + projectService: true, + tsConfigRootDir: import.meta.dirname, + ecmaVersion: 2020, + sourceType: 'module', + extraFileExtensions } } }, pluginJs.configs.recommended, - ...tseslint.configs.recommended, - ...pluginVue.configs['flat/recommended'], + tseslint.configs.recommended, + pluginVue.configs['flat/recommended'], eslintPluginPrettierRecommended, - { - files: ['src/**/*.vue'], - languageOptions: { - parserOptions: { - parser: tseslint.parser - } - } - }, + storybook.configs['flat/recommended'], { plugins: { 'unused-imports': unusedImports, + // @ts-expect-error Bad types in the plugin '@intlify/vue-i18n': pluginI18n }, rules: { @@ -60,13 +75,29 @@ export default [ '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': 'off', '@typescript-eslint/prefer-as-const': 'off', + '@typescript-eslint/consistent-type-imports': 'error', + '@typescript-eslint/no-import-type-side-effects': 'error', + '@typescript-eslint/no-empty-object-type': [ + 'error', + { + allowInterfaces: 'always' + } + ], 'unused-imports/no-unused-imports': 'error', 'vue/no-v-html': 'off', // Enforce dark-theme: instead of dark: prefix 'vue/no-restricted-class': ['error', '/^dark:/'], 'vue/multi-word-component-names': 'off', // TODO: fix 'vue/no-template-shadow': 'off', // TODO: fix + /* Toggle on to do additional until we can clean up existing violations. + 'vue/no-unused-emit-declarations': 'error', + 'vue/no-unused-properties': 'error', + 'vue/no-unused-refs': 'error', + 'vue/no-use-v-else-with-v-for': 'error', + 'vue/no-useless-v-bind': 'error', + // */ 'vue/one-component-per-file': 'off', // TODO: fix + 'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile // Restrict deprecated PrimeVue components 'no-restricted-imports': [ 'error', @@ -136,5 +167,13 @@ export default [ ] } }, - ...storybook.configs['flat/recommended'] -] + { + files: ['tests-ui/**/*'], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false } + ] + } + } +]) diff --git a/index.html b/index.html index de7710c63..8684af476 100644 --- a/index.html +++ b/index.html @@ -8,8 +8,8 @@ - - + + diff --git a/knip.config.ts b/knip.config.ts index 9df077d77..0dcbf7d50 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -22,10 +22,12 @@ const config: KnipConfig = { ], ignore: [ // Auto generated manager types - 'src/types/generatedManagerTypes.ts', + 'src/workbench/extensions/manager/types/generatedManagerTypes.ts', 'src/types/comfyRegistryTypes.ts', // Used by a custom node (that should move off of this) - 'src/scripts/ui/components/splitButton.ts' + 'src/scripts/ui/components/splitButton.ts', + // Staged for for use with subgraph widget promotion + 'src/lib/litegraph/src/widgets/DisconnectedWidget.ts' ], compilers: { // https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199 diff --git a/lint-staged.config.js b/lint-staged.config.js index 2d1a6f051..0f3808700 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -3,13 +3,13 @@ export default { './**/*.{ts,tsx,vue,mts}': (stagedFiles) => [ ...formatAndEslint(stagedFiles), - 'vue-tsc --noEmit' + 'pnpm typecheck' ] } function formatAndEslint(fileNames) { return [ - `eslint --fix ${fileNames.join(' ')}`, - `prettier --write ${fileNames.join(' ')}` + `pnpm exec eslint --cache --fix ${fileNames.join(' ')}`, + `pnpm exec prettier --cache --write ${fileNames.join(' ')}` ] } diff --git a/package.json b/package.json index e568820c0..923f04b7e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.27.4", + "version": "1.28.0", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", @@ -14,9 +14,9 @@ "build:types": "nx build --config vite.types.config.mts && node scripts/prepare-types.js", "zipdist": "node scripts/zipdist.js", "typecheck": "vue-tsc --noEmit", - "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache", + "format": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --cache --list-different", "format:check": "prettier --check './**/*.{js,ts,tsx,vue,mts}' --cache", - "format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}'", + "format:no-cache": "prettier --write './**/*.{js,ts,tsx,vue,mts}' --list-different", "format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'", "test:browser": "npx nx e2e", "test:unit": "nx run test tests-ui/tests", @@ -27,6 +27,8 @@ "preview": "nx preview", "lint": "eslint src --cache", "lint:fix": "eslint src --cache --fix", + "lint:unstaged": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache", + "lint:unstaged:fix": "git diff --name-only HEAD | grep -E '\\.(js|ts|vue|mts)$' | xargs -r eslint --cache --fix", "lint:no-cache": "eslint src", "lint:fix:no-cache": "eslint src --fix", "knip": "knip --cache", @@ -38,10 +40,10 @@ "build-storybook": "storybook build" }, "devDependencies": { - "@eslint/js": "^9.8.0", + "@eslint/js": "^9.35.0", "@iconify-json/lucide": "^1.2.66", "@iconify/tailwind": "^1.2.0", - "@intlify/eslint-plugin-vue-i18n": "^3.2.0", + "@intlify/eslint-plugin-vue-i18n": "^4.1.0", "@lobehub/i18n-cli": "^1.25.1", "@nx/eslint": "21.4.1", "@nx/playwright": "21.4.1", @@ -64,11 +66,11 @@ "@vitest/ui": "^3.0.0", "@vue/test-utils": "^2.4.6", "eslint": "^9.34.0", - "eslint-config-prettier": "^10.1.2", - "eslint-plugin-prettier": "^5.2.6", - "eslint-plugin-storybook": "^9.1.1", - "eslint-plugin-unused-imports": "^4.1.4", - "eslint-plugin-vue": "^9.27.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-storybook": "^9.1.6", + "eslint-plugin-unused-imports": "^4.2.0", + "eslint-plugin-vue": "^10.4.0", "fs-extra": "^11.2.0", "globals": "^15.9.0", "happy-dom": "^15.11.0", @@ -79,22 +81,24 @@ "lint-staged": "^15.2.7", "nx": "21.4.1", "prettier": "^3.3.2", - "storybook": "^9.1.1", + "storybook": "^9.1.6", "tailwindcss": "^4.1.12", "tailwindcss-primeui": "^0.6.1", "tsx": "^4.15.6", "tw-animate-css": "^1.3.8", "typescript": "^5.4.5", - "typescript-eslint": "^8.42.0", + "typescript-eslint": "^8.44.0", "unplugin-icons": "^0.22.0", "unplugin-vue-components": "^0.28.0", "uuid": "^11.1.0", "vite": "^5.4.19", - "vite-plugin-dts": "^4.3.0", + "vite-plugin-dts": "^4.5.4", "vite-plugin-html": "^3.2.2", "vite-plugin-vue-devtools": "^7.7.6", "vitest": "^3.2.4", - "vue-tsc": "^2.1.10", + "vue-component-type-helpers": "^3.0.7", + "vue-eslint-parser": "^10.2.0", + "vue-tsc": "^3.0.7", "zip-dir": "^2.0.0", "zod-to-json-schema": "^3.24.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acc01cc0c..6ce1f4d34 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,10 +94,10 @@ importers: version: 3.2.5 dotenv: specifier: ^16.4.5 - version: 16.4.5 + version: 16.6.1 es-toolkit: specifier: ^1.39.9 - version: 1.39.9 + version: 1.39.10 extendable-media-recorder: specifier: ^9.2.27 version: 9.2.27 @@ -172,8 +172,8 @@ importers: version: 3.3.0(zod@3.24.1) devDependencies: '@eslint/js': - specifier: ^9.8.0 - version: 9.12.0 + specifier: ^9.35.0 + version: 9.35.0 '@iconify-json/lucide': specifier: ^1.2.66 version: 1.2.66 @@ -181,8 +181,8 @@ importers: specifier: ^1.2.0 version: 1.2.0 '@intlify/eslint-plugin-vue-i18n': - specifier: ^3.2.0 - version: 3.2.0(eslint@9.35.0(jiti@2.4.2)) + specifier: ^4.1.0 + version: 4.1.0(eslint@9.35.0(jiti@2.4.2))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.4.2)))(yaml-eslint-parser@1.3.0) '@lobehub/i18n-cli': specifier: ^1.25.1 version: 1.25.1(@types/react@19.1.9)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.1.1))(ws@8.18.3)(zod@3.24.1) @@ -194,7 +194,7 @@ importers: version: 21.4.1(@babel/traverse@7.28.3)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2) '@nx/storybook': specifier: 21.4.1 - version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2) + version: 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2) '@nx/vite': specifier: 21.4.1 version: 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vitest@3.2.4) @@ -206,13 +206,13 @@ importers: version: 1.52.0 '@storybook/addon-docs': specifier: ^9.1.1 - version: 9.1.1(@types/react@19.1.9)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) + version: 9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) '@storybook/vue3': specifier: ^9.1.1 - version: 9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2)) + version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2)) '@storybook/vue3-vite': specifier: ^9.1.1 - version: 9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2)) + version: 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2)) '@tailwindcss/vite': specifier: ^4.1.12 version: 4.1.12(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) @@ -250,20 +250,20 @@ importers: specifier: ^9.34.0 version: 9.35.0(jiti@2.4.2) eslint-config-prettier: - specifier: ^10.1.2 - version: 10.1.2(eslint@9.35.0(jiti@2.4.2)) + specifier: ^10.1.8 + version: 10.1.8(eslint@9.35.0(jiti@2.4.2)) eslint-plugin-prettier: - specifier: ^5.2.6 - version: 5.2.6(eslint-config-prettier@10.1.2(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2) + specifier: ^5.5.4 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2) eslint-plugin-storybook: - specifier: ^9.1.1 - version: 9.1.1(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2) + specifier: ^9.1.6 + version: 9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2) eslint-plugin-unused-imports: - specifier: ^4.1.4 - version: 4.1.4(@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2)) + specifier: ^4.2.0 + version: 4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2)) eslint-plugin-vue: - specifier: ^9.27.0 - version: 9.27.0(eslint@9.35.0(jiti@2.4.2)) + specifier: ^10.4.0 + version: 10.4.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.4.2))) fs-extra: specifier: ^11.2.0 version: 11.2.0 @@ -295,8 +295,8 @@ importers: specifier: ^3.3.2 version: 3.3.2 storybook: - specifier: ^9.1.1 - version: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + specifier: ^9.1.6 + version: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) tailwindcss: specifier: ^4.1.12 version: 4.1.12 @@ -313,14 +313,14 @@ importers: specifier: ^5.4.5 version: 5.9.2 typescript-eslint: - specifier: ^8.42.0 - version: 8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + specifier: ^8.44.0 + version: 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) unplugin-icons: specifier: ^0.22.0 version: 0.22.0(@vue/compiler-sfc@3.5.13) unplugin-vue-components: specifier: ^0.28.0 - version: 0.28.0(@babel/parser@7.28.3)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)) + version: 0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)) uuid: specifier: ^11.1.0 version: 11.1.0 @@ -328,8 +328,8 @@ importers: specifier: ^5.4.19 version: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2) vite-plugin-dts: - specifier: ^4.3.0 - version: 4.3.0(@types/node@20.14.10)(rollup@4.22.4)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + specifier: ^4.5.4 + version: 4.5.4(@types/node@20.14.10)(rollup@4.22.4)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) vite-plugin-html: specifier: ^3.2.2 version: 3.2.2(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) @@ -339,9 +339,15 @@ importers: vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@20.14.10)(@vitest/ui@3.2.4)(happy-dom@15.11.0)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.2) + vue-component-type-helpers: + specifier: ^3.0.7 + version: 3.0.7 + vue-eslint-parser: + specifier: ^10.2.0 + version: 10.2.0(eslint@9.35.0(jiti@2.4.2)) vue-tsc: - specifier: ^2.1.10 - version: 2.1.10(typescript@5.9.2) + specifier: ^3.0.7 + version: 3.0.7(typescript@5.9.2) zip-dir: specifier: ^2.0.0 version: 2.0.0 @@ -544,6 +550,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} @@ -962,10 +973,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.27.6': - resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -982,6 +989,10 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} @@ -1314,12 +1325,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.7.0': - resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/eslint-utils@4.8.0': resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1342,18 +1347,10 @@ packages: resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.1.0': - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.12.0': - resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.35.0': resolution: {integrity: sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1634,20 +1631,35 @@ packages: '@internationalized/number@3.6.5': resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + '@intlify/core-base@11.1.12': + resolution: {integrity: sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==} + engines: {node: '>= 16'} + '@intlify/core-base@9.14.3': resolution: {integrity: sha512-nbJ7pKTlXFnaXPblyfiH6awAx1C0PWNNuqXAR74yRwgi5A/Re/8/5fErLY0pv4R8+EHj3ZaThMHdnuC/5OBa6g==} engines: {node: '>= 16'} - '@intlify/eslint-plugin-vue-i18n@3.2.0': - resolution: {integrity: sha512-TOIrD4tJE48WMyVIB8bNeQJJPYo1Prpqnm9Xpn1UZmcqlELhm8hmP8QyJnkgesfbG7hyiX/kvo63W7ClEQmhpg==} + '@intlify/eslint-plugin-vue-i18n@4.1.0': + resolution: {integrity: sha512-MPAr3LGTrkB5CZBHN5eUf4kASUEiSaDM371jADmxNbTL1Ew7IAyCIBGm3+/1sWcvsfVHe4wz8RFoo6FpeQZ4Nw==} engines: {node: '>=18.0.0'} peerDependencies: eslint: ^8.0.0 || ^9.0.0-0 + jsonc-eslint-parser: ^2.3.0 + vue-eslint-parser: ^10.0.0 + yaml-eslint-parser: ^1.2.2 + + '@intlify/message-compiler@11.1.12': + resolution: {integrity: sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==} + engines: {node: '>= 16'} '@intlify/message-compiler@9.14.3': resolution: {integrity: sha512-ANwC226BQdd+MpJ36rOYkChSESfPwu3Ss2Faw0RHTOknYLoHTX6V6e/JjIKVDMbzs0/H/df/rO6yU0SPiWHqNg==} engines: {node: '>= 16'} + '@intlify/shared@11.1.12': + resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==} + engines: {node: '>= 16'} + '@intlify/shared@9.14.3': resolution: {integrity: sha512-hJXz9LA5VG7qNE00t50bdzDv8Z4q9fpcL81wj4y4duKavrv0KM8YNLTwXNEFINHjTsfrG9TXvPuEjVaAvZ7yWg==} engines: {node: '>= 16'} @@ -1697,9 +1709,6 @@ packages: '@jridgewell/source-map@0.3.6': resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1724,11 +1733,11 @@ packages: '@types/react': '>=16' react: '>=16' - '@microsoft/api-extractor-model@7.30.0': - resolution: {integrity: sha512-26/LJZBrsWDKAkOWRiQbdVgcfd1F3nyJnAiJzsAgpouPk7LtOIj7PK9aJtBaw/pUXrkotEg27RrT+Jm/q0bbug==} + '@microsoft/api-extractor-model@7.30.7': + resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==} - '@microsoft/api-extractor@7.48.0': - resolution: {integrity: sha512-FMFgPjoilMUWeZXqYRlJ3gCVRhB7WU/HN88n8OLqEsmsG4zBdX/KQdtJfhq95LQTQ++zfu0Em1LLb73NqRCLYQ==} + '@microsoft/api-extractor@7.52.13': + resolution: {integrity: sha512-K6/bBt8zZfn9yc06gNvA+/NlBGJC/iJlObpdufXHEJtqcD4Dln4ITCLZpwP3DNZ5NyBFeTkKdv596go3V72qlA==} hasBin: true '@microsoft/tsdoc-config@0.17.1': @@ -1969,12 +1978,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@pkgr/core@0.1.2': - resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - - '@pkgr/core@0.2.2': - resolution: {integrity: sha512-25L86MyPvnlQoX2MTIV2OiUcb6vJ6aRbFa9pbwByn95INKD5mFH2smgjDhq+fwJoqAgvgbdJLj6Tz7V9X5CFAQ==} + '@pkgr/core@0.2.9': + resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} '@playwright/test@1.52.0': @@ -2074,6 +2079,15 @@ packages: rollup: optional: true + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.22.4': resolution: {integrity: sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==} cpu: [arm] @@ -2154,8 +2168,8 @@ packages: cpu: [x64] os: [win32] - '@rushstack/node-core-library@5.10.0': - resolution: {integrity: sha512-2pPLCuS/3x7DCd7liZkqOewGM0OzLyCacdvOe8j6Yrx9LkETGnxul1t7603bIaB8nUAooORcct9fFDOQMbWAgw==} + '@rushstack/node-core-library@5.14.0': + resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} peerDependencies: '@types/node': '*' peerDependenciesMeta: @@ -2165,16 +2179,16 @@ packages: '@rushstack/rig-package@0.5.3': resolution: {integrity: sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow==} - '@rushstack/terminal@0.14.3': - resolution: {integrity: sha512-csXbZsAdab/v8DbU1sz7WC2aNaKArcdS/FPmXMOXEj/JBBZMvDK0+1b4Qao0kkG0ciB1Qe86/Mb68GjH6/TnMw==} + '@rushstack/terminal@0.16.0': + resolution: {integrity: sha512-WEvNuKkoR1PXorr9SxO0dqFdSp1BA+xzDrIm/Bwlc5YHg2FFg6oS+uCTYjerOhFuqCW+A3vKBm6EmKWSHfgx/A==} peerDependencies: '@types/node': '*' peerDependenciesMeta: '@types/node': optional: true - '@rushstack/ts-command-line@4.23.1': - resolution: {integrity: sha512-40jTmYoiu/xlIpkkRsVfENtBq4CW3R4azbL0Vmda+fMwHWqss6wwf/Cy/UJmMqIzpfYc2OTnjYP1ZLD3CmyeCA==} + '@rushstack/ts-command-line@5.0.3': + resolution: {integrity: sha512-bgPhQEqLVv/2hwKLYv/XvsTWNZ9B/+X1zJ7WgQE9rO5oiLzrOZvkIW4pk13yOQBhHyjcND5qMOa6p83t+Z66iQ==} '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -2650,100 +2664,63 @@ packages: '@types/webxr@0.5.20': resolution: {integrity: sha512-JGpU6qiIJQKUuVSKx1GtQnHJGxRjtfGIhzO2ilq43VZZS//f1h1Sgexbdk+Lq+7569a6EYhOWrUpIruR/1Enmg==} - '@typescript-eslint/eslint-plugin@8.42.0': - resolution: {integrity: sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==} + '@typescript-eslint/eslint-plugin@8.44.0': + resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.42.0 + '@typescript-eslint/parser': ^8.44.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.42.0': - resolution: {integrity: sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==} + '@typescript-eslint/parser@8.44.0': + resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.39.0': - resolution: {integrity: sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==} + '@typescript-eslint/project-service@8.44.0': + resolution: {integrity: sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.42.0': - resolution: {integrity: sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==} + '@typescript-eslint/scope-manager@8.44.0': + resolution: {integrity: sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.44.0': + resolution: {integrity: sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.39.0': - resolution: {integrity: sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/scope-manager@8.42.0': - resolution: {integrity: sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.39.0': - resolution: {integrity: sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/tsconfig-utils@8.42.0': - resolution: {integrity: sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/type-utils@8.42.0': - resolution: {integrity: sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==} + '@typescript-eslint/type-utils@8.44.0': + resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.39.0': - resolution: {integrity: sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==} + '@typescript-eslint/types@8.44.0': + resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.42.0': - resolution: {integrity: sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.39.0': - resolution: {integrity: sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==} + '@typescript-eslint/typescript-estree@8.44.0': + resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.42.0': - resolution: {integrity: sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/utils@8.39.0': - resolution: {integrity: sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==} + '@typescript-eslint/utils@8.44.0': + resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.42.0': - resolution: {integrity: sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' - - '@typescript-eslint/visitor-keys@8.39.0': - resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/visitor-keys@8.42.0': - resolution: {integrity: sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==} + '@typescript-eslint/visitor-keys@8.44.0': + resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-vue@5.1.4': @@ -2799,12 +2776,21 @@ packages: '@volar/language-core@2.4.15': resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + '@volar/source-map@2.4.15': resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + '@volar/typescript@2.4.15': resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + '@vue/babel-helper-vue-transform-on@1.4.0': resolution: {integrity: sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==} @@ -2824,9 +2810,15 @@ packages: '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + '@vue/compiler-core@3.5.21': + resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==} + '@vue/compiler-dom@3.5.13': resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + '@vue/compiler-dom@3.5.21': + resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==} + '@vue/compiler-sfc@3.5.13': resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} @@ -2850,16 +2842,8 @@ packages: '@vue/devtools-shared@7.7.6': resolution: {integrity: sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==} - '@vue/language-core@2.1.10': - resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@vue/language-core@2.1.6': - resolution: {integrity: sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==} + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -2874,6 +2858,14 @@ packages: typescript: optional: true + '@vue/language-core@3.0.7': + resolution: {integrity: sha512-0sqqyqJ0Gn33JH3TdIsZLCZZ8Gr4kwlg8iYOnOrDDkJKSjFurlQY/bEFQx5zs7SX2C/bjMkmPYq/NiyY1fTOkw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@vue/reactivity@3.5.13': resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} @@ -2891,6 +2883,9 @@ packages: '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vue/shared@3.5.21': + resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} + '@vue/test-utils@2.4.6': resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} @@ -2960,11 +2955,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -3014,12 +3004,15 @@ packages: resolution: {integrity: sha512-hexLq2lSO1K5SW9j21Ubc+q9Ptx7dyRTY7se19U8lhIlVMLCNXWCyQ6C22p9ez8ccX0v7QVmwkl2l1CnuGoO2Q==} engines: {node: '>= 14.0.0'} - alien-signals@0.2.2: - resolution: {integrity: sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==} + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + alien-signals@2.0.7: + resolution: {integrity: sha512-wE7y3jmYeb0+h6mr5BOovuqhFv22O/MV9j5p0ndJsa7z1zJNPGQ4ph5pQk/kTTCWRC3xsA4SmtwmkzQO+7NCNg==} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -3370,9 +3363,6 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - computeds@0.0.1: - resolution: {integrity: sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -3495,6 +3485,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} @@ -3708,9 +3707,6 @@ packages: es-toolkit@1.39.10: resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} - es-toolkit@1.39.9: - resolution: {integrity: sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==} - esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -3756,14 +3752,14 @@ packages: peerDependencies: eslint: '>=6.0.0' - eslint-config-prettier@10.1.2: - resolution: {integrity: sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==} + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} hasBin: true peerDependencies: eslint: '>=7.0.0' - eslint-plugin-prettier@5.2.6: - resolution: {integrity: sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==} + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -3776,15 +3772,15 @@ packages: eslint-config-prettier: optional: true - eslint-plugin-storybook@9.1.1: - resolution: {integrity: sha512-g4/i9yW6cl4TCEMzYyALNvO3d/jB6TDvSs/Pmye7dHDrra2B7dgZJGzmEWILD62brVrLVHNoXgy2dNPtx80kmw==} + eslint-plugin-storybook@9.1.6: + resolution: {integrity: sha512-4NLf8lOT7Nl+m9aipVHJczyt/Dp6BzHzyNq4nhaEUjoZFGKMhPa52vSbuLyQYX7IrcrYPlM37X8dFGo/EIE9JA==} engines: {node: '>=20.0.0'} peerDependencies: eslint: '>=8' - storybook: ^9.1.1 + storybook: ^9.1.6 - eslint-plugin-unused-imports@4.1.4: - resolution: {integrity: sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==} + eslint-plugin-unused-imports@4.2.0: + resolution: {integrity: sha512-hLbJ2/wnjKq4kGA9AUaExVFIbNzyxYdVo49QZmKCnhk5pc9wcYRbfgLHvWJ8tnsdcseGhoUAddm9gn/lt+d74w==} peerDependencies: '@typescript-eslint/eslint-plugin': ^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0 eslint: ^9.0.0 || ^8.0.0 @@ -3792,15 +3788,16 @@ packages: '@typescript-eslint/eslint-plugin': optional: true - eslint-plugin-vue@9.27.0: - resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==} - engines: {node: ^14.17.0 || >=16.0.0} + eslint-plugin-vue@10.4.0: + resolution: {integrity: sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} @@ -3827,10 +3824,6 @@ packages: esm-resolve@1.0.11: resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==} - espree@10.2.0: - resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4004,10 +3997,6 @@ packages: debug: optional: true - foreground-child@3.2.1: - resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} - engines: {node: '>=14'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -4046,9 +4035,9 @@ packages: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -4119,10 +4108,6 @@ packages: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -4131,6 +4116,10 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + globals@16.4.0: + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -4241,10 +4230,6 @@ packages: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} - ignore@6.0.2: - resolution: {integrity: sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==} - engines: {node: '>= 4'} - ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -4252,10 +4237,6 @@ packages: immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -4570,12 +4551,12 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonify@0.0.1: resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} @@ -4771,10 +4752,6 @@ packages: lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - lru-cache@10.3.0: - resolution: {integrity: sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==} - engines: {node: 14 || >=16.14} - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -4797,12 +4774,12 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.18: resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} @@ -5006,9 +4983,6 @@ packages: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} - minimatch@3.0.8: - resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -5050,6 +5024,9 @@ packages: mlly@1.7.4: resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -5254,9 +5231,6 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} - parse5@7.1.2: - resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -5657,10 +5631,6 @@ packages: engines: {node: '>= 0.4'} hasBin: true - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -5806,8 +5776,8 @@ packages: std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} - storybook@9.1.1: - resolution: {integrity: sha512-q6GaGZdVZh6rjOdGnc+4hGTu8ECyhyjQDw4EZNxKtQjDO8kqtuxbFm8l/IP2l+zLVJAatGWKkaX9Qcd7QZxz+Q==} + storybook@9.1.6: + resolution: {integrity: sha512-iIcMaDKkjR5nN+JYBy9hhoxZhjX4TXhyJgUBed+toJOlfrl+QvxpBjImAi7qKyLR3hng3uoigEP0P8+vYtXpOg==} hasBin: true peerDependencies: prettier: ^2 || ^3 @@ -5910,12 +5880,12 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.3: - resolution: {integrity: sha512-szhWDqNNI9etJUvbZ1/cx1StnZx8yMmFxme48SwR4dty4ioSY50KEZlpv0qAfgc1fpRzuh9hBXEzoCpJ779dLg==} + synckit@0.10.4: + resolution: {integrity: sha512-2SG1TnJGjMkD4+gblONMGYSrwAzYi+ymOitD+Jb/iMYm57nH20PlkVeMQRah3yDMKEa0QQYUF/QPWpdW7C6zNg==} engines: {node: ^14.18.0 || >=16.0.0} - synckit@0.9.3: - resolution: {integrity: sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} tailwind-merge@3.3.1: @@ -6058,10 +6028,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} @@ -6070,15 +6036,15 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} - typescript-eslint@8.42.0: - resolution: {integrity: sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==} + typescript-eslint@8.44.0: + resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript@5.4.2: - resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==} + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} hasBin: true @@ -6098,6 +6064,9 @@ packages: ufo@1.5.4: resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} @@ -6143,10 +6112,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -6243,9 +6208,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-plugin-dts@4.3.0: - resolution: {integrity: sha512-LkBJh9IbLwL6/rxh0C1/bOurDrIEmRE7joC+jFdOEEciAFPbpEKOLSAr5nNh5R7CJ45cMbksTrFfy52szzC5eA==} - engines: {node: ^14.18.0 || >=16.0.0} + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: typescript: '*' vite: '*' @@ -6375,11 +6339,11 @@ packages: peerDependencies: vue: '>=2' - vue-eslint-parser@9.4.3: - resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} - engines: {node: ^14.17.0 || >=16.0.0} + vue-eslint-parser@10.2.0: + resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: '>=6.0.0' + eslint: ^8.57.0 || ^9.0.0 vue-i18n@9.14.3: resolution: {integrity: sha512-C+E0KE8ihKjdYCQx8oUkXX+8tBItrYNMnGJuzEPevBARQFUN2tKez6ZVOvBrWH0+KT5wEk3vOWjNk7ygb2u9ig==} @@ -6397,8 +6361,8 @@ packages: peerDependencies: vue: ^3.2.0 - vue-tsc@2.1.10: - resolution: {integrity: sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==} + vue-tsc@3.0.7: + resolution: {integrity: sha512-BSMmW8GGEgHykrv7mRk6zfTdK+tw4MBZY/x6fFa7IkdXK3s/8hQRacPjG9/8YKFDIWGhBocwi6PlkQQ/93OgIQ==} hasBin: true peerDependencies: typescript: '>=5.0.0' @@ -6528,18 +6492,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -6777,7 +6729,7 @@ snapshots: '@atlaskit/pragmatic-drag-and-drop@1.3.1': dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 bind-event-listener: 3.0.0 raf-schd: 4.0.3 @@ -6939,6 +6891,10 @@ snapshots: dependencies: '@babel/types': 7.28.2 + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.27.1)': dependencies: '@babel/core': 7.27.1 @@ -7473,8 +7429,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/runtime@7.27.6': {} - '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': @@ -7500,6 +7454,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@bcoe/v8-coverage@1.0.2': {} '@comfyorg/comfyui-electron-types@0.4.73-0': {} @@ -7681,11 +7640,6 @@ snapshots: '@esbuild/win32-x64@0.25.5': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.35.0(jiti@2.4.2))': - dependencies: - eslint: 9.35.0(jiti@2.4.2) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.8.0(eslint@9.35.0(jiti@2.4.2))': dependencies: eslint: 9.35.0(jiti@2.4.2) @@ -7707,20 +7661,6 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.1.0': - dependencies: - ajv: 6.12.6 - debug: 4.4.1 - espree: 10.2.0 - globals: 14.0.0 - ignore: 5.3.1 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 @@ -7735,8 +7675,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.12.0': {} - '@eslint/js@9.35.0': {} '@eslint/object-schema@2.1.6': {} @@ -8143,41 +8081,53 @@ snapshots: dependencies: '@swc/helpers': 0.5.17 + '@intlify/core-base@11.1.12': + dependencies: + '@intlify/message-compiler': 11.1.12 + '@intlify/shared': 11.1.12 + '@intlify/core-base@9.14.3': dependencies: '@intlify/message-compiler': 9.14.3 '@intlify/shared': 9.14.3 - '@intlify/eslint-plugin-vue-i18n@3.2.0(eslint@9.35.0(jiti@2.4.2))': + '@intlify/eslint-plugin-vue-i18n@4.1.0(eslint@9.35.0(jiti@2.4.2))(jsonc-eslint-parser@2.4.0)(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.4.2)))(yaml-eslint-parser@1.3.0)': dependencies: - '@eslint/eslintrc': 3.1.0 - '@intlify/core-base': 9.14.3 - '@intlify/message-compiler': 9.14.3 + '@eslint/eslintrc': 3.3.1 + '@intlify/core-base': 11.1.12 + '@intlify/message-compiler': 11.1.12 debug: 4.4.1 eslint: 9.35.0(jiti@2.4.2) eslint-compat-utils: 0.6.5(eslint@9.35.0(jiti@2.4.2)) glob: 10.4.5 - globals: 15.15.0 - ignore: 6.0.2 - import-fresh: 3.3.0 + globals: 16.4.0 + ignore: 7.0.5 + import-fresh: 3.3.1 is-language-code: 3.1.0 js-yaml: 4.1.0 json5: 2.2.3 jsonc-eslint-parser: 2.4.0 lodash: 4.17.21 - parse5: 7.1.2 + parse5: 7.3.0 semver: 7.7.2 - synckit: 0.9.3 - vue-eslint-parser: 9.4.3(eslint@9.35.0(jiti@2.4.2)) + synckit: 0.10.4 + vue-eslint-parser: 10.2.0(eslint@9.35.0(jiti@2.4.2)) yaml-eslint-parser: 1.3.0 transitivePeerDependencies: - supports-color + '@intlify/message-compiler@11.1.12': + dependencies: + '@intlify/shared': 11.1.12 + source-map-js: 1.2.1 + '@intlify/message-compiler@9.14.3': dependencies: '@intlify/shared': 9.14.3 source-map-js: 1.2.1 + '@intlify/shared@11.1.12': {} + '@intlify/shared@9.14.3': {} '@isaacs/balanced-match@4.0.1': {} @@ -8226,8 +8176,6 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.30 - '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.30': @@ -8240,7 +8188,7 @@ snapshots: '@lobehub/cli-ui@1.13.0(@types/react@19.1.9)': dependencies: arr-rotate: 1.0.0 - chalk: 5.3.0 + chalk: 5.6.0 cli-spinners: 3.2.0 consola: 3.4.2 deepmerge: 4.3.1 @@ -8305,29 +8253,29 @@ snapshots: '@types/react': 19.1.9 react: 19.1.1 - '@microsoft/api-extractor-model@7.30.0(@types/node@20.14.10)': + '@microsoft/api-extractor-model@7.30.7(@types/node@20.14.10)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@20.14.10) + '@rushstack/node-core-library': 5.14.0(@types/node@20.14.10) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.48.0(@types/node@20.14.10)': + '@microsoft/api-extractor@7.52.13(@types/node@20.14.10)': dependencies: - '@microsoft/api-extractor-model': 7.30.0(@types/node@20.14.10) + '@microsoft/api-extractor-model': 7.30.7(@types/node@20.14.10) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.10.0(@types/node@20.14.10) + '@rushstack/node-core-library': 5.14.0(@types/node@20.14.10) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.14.3(@types/node@20.14.10) - '@rushstack/ts-command-line': 4.23.1(@types/node@20.14.10) + '@rushstack/terminal': 0.16.0(@types/node@20.14.10) + '@rushstack/ts-command-line': 5.0.3(@types/node@20.14.10) lodash: 4.17.21 - minimatch: 3.0.8 - resolve: 1.22.8 + minimatch: 10.0.3 + resolve: 1.22.10 semver: 7.5.4 source-map: 0.6.1 - typescript: 5.4.2 + typescript: 5.8.2 transitivePeerDependencies: - '@types/node' @@ -8426,7 +8374,7 @@ snapshots: '@babel/plugin-transform-runtime': 7.28.3(@babel/core@7.27.1) '@babel/preset-env': 7.28.3(@babel/core@7.27.1) '@babel/preset-typescript': 7.27.1(@babel/core@7.27.1) - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 '@nx/devkit': 21.4.1(nx@21.4.1) '@nx/workspace': 21.4.1 '@zkochan/js-yaml': 0.0.7 @@ -8509,7 +8457,7 @@ snapshots: - typescript - verdaccio - '@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)': + '@nx/storybook@21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2)': dependencies: '@nx/cypress': 21.4.1(@babel/traverse@7.28.3)(@zkochan/js-yaml@0.0.7)(eslint@9.35.0(jiti@2.4.2))(nx@21.4.1)(typescript@5.9.2) '@nx/devkit': 21.4.1(nx@21.4.1) @@ -8517,7 +8465,7 @@ snapshots: '@nx/js': 21.4.1(@babel/traverse@7.28.3)(nx@21.4.1) '@phenomnomnominal/tsquery': 5.0.1(typescript@5.9.2) semver: 7.7.2 - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) tslib: 2.8.1 transitivePeerDependencies: - '@babel/traverse' @@ -8648,9 +8596,7 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@pkgr/core@0.1.2': {} - - '@pkgr/core@0.2.2': {} + '@pkgr/core@0.2.9': {} '@playwright/test@1.52.0': dependencies: @@ -8743,6 +8689,14 @@ snapshots: optionalDependencies: rollup: 4.22.4 + '@rollup/pluginutils@5.3.0(rollup@4.22.4)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.22.4 + '@rollup/rollup-android-arm-eabi@4.22.4': optional: true @@ -8791,12 +8745,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.22.4': optional: true - '@rushstack/node-core-library@5.10.0(@types/node@20.14.10)': + '@rushstack/node-core-library@5.14.0(@types/node@20.14.10)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) ajv-formats: 3.0.1(ajv@8.13.0) - fs-extra: 7.0.1 + fs-extra: 11.3.2 import-lazy: 4.0.0 jju: 1.4.0 resolve: 1.22.10 @@ -8809,16 +8763,16 @@ snapshots: resolve: 1.22.10 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.14.3(@types/node@20.14.10)': + '@rushstack/terminal@0.16.0(@types/node@20.14.10)': dependencies: - '@rushstack/node-core-library': 5.10.0(@types/node@20.14.10) + '@rushstack/node-core-library': 5.14.0(@types/node@20.14.10) supports-color: 8.1.1 optionalDependencies: '@types/node': 20.14.10 - '@rushstack/ts-command-line@4.23.1(@types/node@20.14.10)': + '@rushstack/ts-command-line@5.0.3(@types/node@20.14.10)': dependencies: - '@rushstack/terminal': 0.14.3(@types/node@20.14.10) + '@rushstack/terminal': 0.16.0(@types/node@20.14.10) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -8867,29 +8821,29 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))': + '@storybook/addon-docs@9.1.1(@types/react@19.1.9)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))': dependencies: '@mdx-js/react': 3.1.0(@types/react@19.1.9)(react@19.1.1) - '@storybook/csf-plugin': 9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) + '@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) '@storybook/icons': 1.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) - '@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) + '@storybook/react-dom-shim': 9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' - '@storybook/builder-vite@9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))': + '@storybook/builder-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))': dependencies: - '@storybook/csf-plugin': 9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + '@storybook/csf-plugin': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) ts-dedent: 2.2.0 vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2) - '@storybook/csf-plugin@9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))': + '@storybook/csf-plugin@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))': dependencies: - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) unplugin: 1.16.1 '@storybook/global@5.0.0': {} @@ -8899,19 +8853,19 @@ snapshots: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - '@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))': + '@storybook/react-dom-shim@9.1.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))': dependencies: react: 19.1.1 react-dom: 19.1.1(react@19.1.1) - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) - '@storybook/vue3-vite@9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))': + '@storybook/vue3-vite@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))': dependencies: - '@storybook/builder-vite': 9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) - '@storybook/vue3': 9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2)) + '@storybook/builder-vite': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + '@storybook/vue3': 9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2)) find-package-json: 1.2.0 magic-string: 0.30.18 - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) typescript: 5.9.2 vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2) vue-component-meta: 2.2.12(typescript@5.9.2) @@ -8919,10 +8873,10 @@ snapshots: transitivePeerDependencies: - vue - '@storybook/vue3@9.1.1(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))': + '@storybook/vue3@9.1.1(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(vue@3.5.13(typescript@5.9.2))': dependencies: '@storybook/global': 5.0.0 - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) type-fest: 2.19.0 vue: 3.5.13(typescript@5.9.2) vue-component-type-helpers: 3.0.7 @@ -9241,7 +9195,7 @@ snapshots: dependencies: '@types/node': 20.14.10 '@types/tough-cookie': 4.0.5 - parse5: 7.1.2 + parse5: 7.3.0 '@types/json-schema@7.0.15': {} @@ -9320,14 +9274,14 @@ snapshots: '@types/webxr@0.5.20': {} - '@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': + '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/scope-manager': 8.42.0 - '@typescript-eslint/type-utils': 8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/utils': 8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.42.0 + '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/type-utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.44.0 eslint: 9.35.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 7.0.5 @@ -9337,59 +9291,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': + '@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 8.42.0 - '@typescript-eslint/types': 8.42.0 - '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 8.42.0 + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.44.0 debug: 4.4.1 eslint: 9.35.0(jiti@2.4.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.39.0(typescript@5.9.2)': + '@typescript-eslint/project-service@8.44.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.39.0(typescript@5.9.2) - '@typescript-eslint/types': 8.39.0 + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) + '@typescript-eslint/types': 8.44.0 debug: 4.4.1 typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.42.0(typescript@5.9.2)': + '@typescript-eslint/scope-manager@8.44.0': dependencies: - '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.9.2) - '@typescript-eslint/types': 8.42.0 - debug: 4.4.1 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 - '@typescript-eslint/scope-manager@8.39.0': - dependencies: - '@typescript-eslint/types': 8.39.0 - '@typescript-eslint/visitor-keys': 8.39.0 - - '@typescript-eslint/scope-manager@8.42.0': - dependencies: - '@typescript-eslint/types': 8.42.0 - '@typescript-eslint/visitor-keys': 8.42.0 - - '@typescript-eslint/tsconfig-utils@8.39.0(typescript@5.9.2)': + '@typescript-eslint/tsconfig-utils@8.44.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 - '@typescript-eslint/tsconfig-utils@8.42.0(typescript@5.9.2)': + '@typescript-eslint/type-utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': dependencies: - typescript: 5.9.2 - - '@typescript-eslint/type-utils@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': - dependencies: - '@typescript-eslint/types': 8.42.0 - '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) debug: 4.4.1 eslint: 9.35.0(jiti@2.4.2) ts-api-utils: 2.1.0(typescript@5.9.2) @@ -9397,16 +9333,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.39.0': {} + '@typescript-eslint/types@8.44.0': {} - '@typescript-eslint/types@8.42.0': {} - - '@typescript-eslint/typescript-estree@8.39.0(typescript@5.9.2)': + '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.39.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.39.0(typescript@5.9.2) - '@typescript-eslint/types': 8.39.0 - '@typescript-eslint/visitor-keys': 8.39.0 + '@typescript-eslint/project-service': 8.44.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -9417,52 +9351,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.42.0(typescript@5.9.2)': - dependencies: - '@typescript-eslint/project-service': 8.42.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.9.2) - '@typescript-eslint/types': 8.42.0 - '@typescript-eslint/visitor-keys': 8.42.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.39.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.35.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.39.0 - '@typescript-eslint/types': 8.39.0 - '@typescript-eslint/typescript-estree': 8.39.0(typescript@5.9.2) - eslint: 9.35.0(jiti@2.4.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': + '@typescript-eslint/utils@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0(jiti@2.4.2)) - '@typescript-eslint/scope-manager': 8.42.0 - '@typescript-eslint/types': 8.42.0 - '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) eslint: 9.35.0(jiti@2.4.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.39.0': + '@typescript-eslint/visitor-keys@8.44.0': dependencies: - '@typescript-eslint/types': 8.39.0 - eslint-visitor-keys: 4.2.1 - - '@typescript-eslint/visitor-keys@8.42.0': - dependencies: - '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/types': 8.44.0 eslint-visitor-keys: 4.2.1 '@vitejs/plugin-vue@5.1.4(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))(vue@3.5.13(typescript@5.9.2))': @@ -9546,14 +9448,26 @@ snapshots: dependencies: '@volar/source-map': 2.4.15 + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + '@volar/source-map@2.4.15': {} + '@volar/source-map@2.4.23': {} + '@volar/typescript@2.4.15': dependencies: '@volar/language-core': 2.4.15 path-browserify: 1.0.1 vscode-uri: 3.0.8 + '@volar/typescript@2.4.23': + dependencies: + '@volar/language-core': 2.4.23 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + '@vue/babel-helper-vue-transform-on@1.4.0': {} '@vue/babel-plugin-jsx@1.4.0(@babel/core@7.27.1)': @@ -9591,11 +9505,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.21': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.21 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.13': dependencies: '@vue/compiler-core': 3.5.13 '@vue/shared': 3.5.13 + '@vue/compiler-dom@3.5.21': + dependencies: + '@vue/compiler-core': 3.5.21 + '@vue/shared': 3.5.21 + '@vue/compiler-sfc@3.5.13': dependencies: '@babel/parser': 7.28.3 @@ -9646,26 +9573,13 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/language-core@2.1.10(typescript@5.9.2)': + '@vue/language-core@2.2.0(typescript@5.9.2)': dependencies: - '@volar/language-core': 2.4.15 - '@vue/compiler-dom': 3.5.13 + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.21 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.13 - alien-signals: 0.2.2 - minimatch: 9.0.5 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - optionalDependencies: - typescript: 5.9.2 - - '@vue/language-core@2.1.6(typescript@5.9.2)': - dependencies: - '@volar/language-core': 2.4.15 - '@vue/compiler-dom': 3.5.13 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.13 - computeds: 0.0.1 + '@vue/shared': 3.5.21 + alien-signals: 0.4.14 minimatch: 9.0.5 muggle-string: 0.4.1 path-browserify: 1.0.1 @@ -9685,6 +9599,19 @@ snapshots: optionalDependencies: typescript: 5.9.2 + '@vue/language-core@3.0.7(typescript@5.9.2)': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + alien-signals: 2.0.7 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + optionalDependencies: + typescript: 5.9.2 + '@vue/reactivity@3.5.13': dependencies: '@vue/shared': 3.5.13 @@ -9709,6 +9636,8 @@ snapshots: '@vue/shared@3.5.13': {} + '@vue/shared@3.5.21': {} + '@vue/test-utils@2.4.6': dependencies: js-beautify: 1.15.1 @@ -9781,18 +9710,12 @@ snapshots: dependencies: event-target-shim: 5.0.1 - acorn-jsx@5.3.2(acorn@8.14.1): - dependencies: - acorn: 8.14.1 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 acorn@7.4.1: {} - acorn@8.14.1: {} - acorn@8.15.0: {} address@1.2.2: {} @@ -9859,10 +9782,12 @@ snapshots: '@algolia/requester-fetch': 5.21.0 '@algolia/requester-node-http': 5.21.0 - alien-signals@0.2.2: {} + alien-signals@0.4.14: {} alien-signals@1.0.13: {} + alien-signals@2.0.7: {} + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -9937,7 +9862,7 @@ snapshots: automation-events@7.1.11: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 tslib: 2.8.1 axios@1.11.0: @@ -9959,7 +9884,7 @@ snapshots: babel-plugin-macros@3.1.0: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 resolve: 1.22.10 @@ -10026,7 +9951,7 @@ snapshots: dependencies: ansi-align: 3.0.1 camelcase: 8.0.0 - chalk: 5.3.0 + chalk: 5.6.0 cli-boxes: 3.0.0 string-width: 7.2.0 type-fest: 4.41.0 @@ -10048,7 +9973,7 @@ snapshots: broker-factory@3.1.7: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 fast-unique-numbers: 9.0.22 tslib: 2.8.1 worker-factory: 7.0.43 @@ -10214,8 +10139,6 @@ snapshots: compare-versions@6.1.1: {} - computeds@0.0.1: {} - concat-map@0.0.1: {} conf@13.1.0: @@ -10336,6 +10259,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decimal.js@10.6.0: {} decode-named-character-reference@1.2.0: @@ -10437,7 +10364,7 @@ snapshots: dotenv-expand@11.0.7: dependencies: - dotenv: 16.4.5 + dotenv: 16.6.1 dotenv-expand@8.0.3: {} @@ -10522,8 +10449,6 @@ snapshots: es-toolkit@1.39.10: {} - es-toolkit@1.39.9: {} - esbuild-register@3.6.0(esbuild@0.25.5): dependencies: debug: 4.4.1 @@ -10602,52 +10527,46 @@ snapshots: eslint: 9.35.0(jiti@2.4.2) semver: 7.7.2 - eslint-config-prettier@10.1.2(eslint@9.35.0(jiti@2.4.2)): + eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)): dependencies: eslint: 9.35.0(jiti@2.4.2) - eslint-plugin-prettier@5.2.6(eslint-config-prettier@10.1.2(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2): + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.35.0(jiti@2.4.2)))(eslint@9.35.0(jiti@2.4.2))(prettier@3.3.2): dependencies: eslint: 9.35.0(jiti@2.4.2) prettier: 3.3.2 prettier-linter-helpers: 1.0.0 - synckit: 0.11.3 + synckit: 0.11.11 optionalDependencies: - eslint-config-prettier: 10.1.2(eslint@9.35.0(jiti@2.4.2)) + eslint-config-prettier: 10.1.8(eslint@9.35.0(jiti@2.4.2)) - eslint-plugin-storybook@9.1.1(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2): + eslint-plugin-storybook@9.1.6(eslint@9.35.0(jiti@2.4.2))(storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)))(typescript@5.9.2): dependencies: - '@typescript-eslint/utils': 8.39.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) eslint: 9.35.0(jiti@2.4.2) - storybook: 9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) + storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)) transitivePeerDependencies: - supports-color - typescript - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2)): + eslint-plugin-unused-imports@4.2.0(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2)): dependencies: eslint: 9.35.0(jiti@2.4.2) optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) - eslint-plugin-vue@9.27.0(eslint@9.35.0(jiti@2.4.2)): + eslint-plugin-vue@10.4.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.4.2))): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.35.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.8.0(eslint@9.35.0(jiti@2.4.2)) eslint: 9.35.0(jiti@2.4.2) - globals: 13.24.0 natural-compare: 1.4.0 nth-check: 2.1.1 postcss-selector-parser: 6.1.0 semver: 7.7.2 - vue-eslint-parser: 9.4.3(eslint@9.35.0(jiti@2.4.2)) + vue-eslint-parser: 10.2.0(eslint@9.35.0(jiti@2.4.2)) xml-name-validator: 4.0.0 - transitivePeerDependencies: - - supports-color - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 + optionalDependencies: + '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) eslint-scope@8.4.0: dependencies: @@ -10702,12 +10621,6 @@ snapshots: esm-resolve@1.0.11: {} - espree@10.2.0: - dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.1 - espree@10.4.0: dependencies: acorn: 8.15.0 @@ -10716,8 +10629,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -10783,27 +10696,27 @@ snapshots: extendable-media-recorder-wav-encoder-broker@7.0.119: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 broker-factory: 3.1.7 extendable-media-recorder-wav-encoder-worker: 8.0.116 tslib: 2.8.1 extendable-media-recorder-wav-encoder-worker@8.0.116: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 tslib: 2.8.1 worker-factory: 7.0.43 extendable-media-recorder-wav-encoder@7.0.129: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 extendable-media-recorder-wav-encoder-broker: 7.0.119 extendable-media-recorder-wav-encoder-worker: 8.0.116 tslib: 2.8.1 extendable-media-recorder@9.2.27: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 media-encoder-host: 9.0.20 multi-buffer-data-view: 6.0.22 recorder-audio-worklet: 6.0.48 @@ -10829,7 +10742,7 @@ snapshots: fast-unique-numbers@9.0.22: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 tslib: 2.8.1 fast-uri@3.0.3: {} @@ -10927,11 +10840,6 @@ snapshots: follow-redirects@1.15.6: {} - foreground-child@3.2.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -10976,11 +10884,11 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 - fs-extra@7.0.1: + fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 + jsonfile: 6.2.0 + universalify: 2.0.1 fsevents@2.3.2: optional: true @@ -11037,7 +10945,7 @@ snapshots: glob@10.4.5: dependencies: - foreground-child: 3.2.1 + foreground-child: 3.3.1 jackspeak: 3.4.0 minimatch: 9.0.5 minipass: 7.1.2 @@ -11057,14 +10965,12 @@ snapshots: dependencies: ini: 4.1.1 - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - globals@14.0.0: {} globals@15.15.0: {} + globals@16.4.0: {} + gopd@1.2.0: {} gpt-tokenizer@2.9.0: {} @@ -11112,7 +11018,7 @@ snapshots: hosted-git-info@7.0.2: dependencies: - lru-cache: 10.3.0 + lru-cache: 10.4.3 html-encoding-sniffer@4.0.0: dependencies: @@ -11166,17 +11072,10 @@ snapshots: ignore@5.3.1: {} - ignore@6.0.2: {} - ignore@7.0.5: {} immediate@3.0.6: {} - import-fresh@3.3.0: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -11281,7 +11180,7 @@ snapshots: is-language-code@3.1.0: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 is-npm@6.0.0: {} @@ -11461,7 +11360,7 @@ snapshots: jsonc-eslint-parser@2.4.0: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 eslint-visitor-keys: 3.4.3 espree: 9.6.1 semver: 7.7.2 @@ -11471,14 +11370,16 @@ snapshots: jsondiffpatch@0.6.0: dependencies: '@types/diff-match-patch': 1.0.36 - chalk: 5.3.0 + chalk: 5.6.0 diff-match-patch: 1.0.5 - jsonfile@4.0.0: + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 - jsonfile@6.1.0: + jsonfile@6.2.0: dependencies: universalify: 2.0.1 optionalDependencies: @@ -11635,7 +11536,7 @@ snapshots: local-pkg@1.1.2: dependencies: - mlly: 1.7.4 + mlly: 1.8.0 pkg-types: 2.3.0 quansync: 0.2.11 @@ -11678,8 +11579,6 @@ snapshots: dependencies: tslib: 2.8.1 - lru-cache@10.3.0: {} - lru-cache@10.4.3: {} lru-cache@11.1.0: {} @@ -11696,14 +11595,14 @@ snapshots: lz-string@1.5.0: {} - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - magic-string@0.30.18: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: dependencies: '@babel/parser': 7.28.3 @@ -11848,7 +11747,7 @@ snapshots: media-encoder-host-broker@8.0.19: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 broker-factory: 3.1.7 fast-unique-numbers: 9.0.22 media-encoder-host-worker: 10.0.19 @@ -11856,14 +11755,14 @@ snapshots: media-encoder-host-worker@10.0.19: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 extendable-media-recorder-wav-encoder-broker: 7.0.119 tslib: 2.8.1 worker-factory: 7.0.43 media-encoder-host@9.0.20: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 media-encoder-host-broker: 8.0.19 media-encoder-host-worker: 10.0.19 tslib: 2.8.1 @@ -12095,10 +11994,6 @@ snapshots: dependencies: '@isaacs/brace-expansion': 5.0.0 - minimatch@3.0.8: - dependencies: - brace-expansion: 1.1.11 - minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -12133,11 +12028,18 @@ snapshots: mlly@1.7.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.5.4 + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + mrmime@2.0.1: {} ms@2.1.3: {} @@ -12146,7 +12048,7 @@ snapshots: multi-buffer-data-view@6.0.22: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 tslib: 2.8.1 nanoid@3.3.8: {} @@ -12399,10 +12301,6 @@ snapshots: parse-ms@4.0.0: {} - parse5@7.1.2: - dependencies: - entities: 4.5.0 - parse5@7.3.0: dependencies: entities: 6.0.1 @@ -12426,7 +12324,7 @@ snapshots: path-scurry@1.11.1: dependencies: - lru-cache: 10.3.0 + lru-cache: 10.4.3 minipass: 7.1.2 path-scurry@2.0.0: @@ -12794,12 +12692,12 @@ snapshots: recorder-audio-worklet-processor@5.0.35: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 tslib: 2.8.1 recorder-audio-worklet@6.0.48: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 broker-factory: 3.1.7 fast-unique-numbers: 9.0.22 recorder-audio-worklet-processor: 5.0.35 @@ -12912,12 +12810,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@1.22.8: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - restore-cursor@3.1.0: dependencies: onetime: 5.1.2 @@ -13058,13 +12950,13 @@ snapshots: standardized-audio-context@25.3.77: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 automation-events: 7.1.11 tslib: 2.8.1 std-env@3.9.0: {} - storybook@9.1.1(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)): + storybook@9.1.6(@testing-library/dom@10.4.1)(prettier@3.3.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)): dependencies: '@storybook/global': 5.0.0 '@testing-library/jest-dom': 6.6.4 @@ -13077,7 +12969,7 @@ snapshots: esbuild-register: 3.6.0(esbuild@0.25.5) recast: 0.23.11 semver: 7.7.2 - ws: 8.18.0 + ws: 8.18.3 optionalDependencies: prettier: 3.3.2 transitivePeerDependencies: @@ -13148,7 +13040,7 @@ snapshots: subscribable-things@2.1.53: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 rxjs-interop: 2.0.0 tslib: 2.8.1 @@ -13174,15 +13066,14 @@ snapshots: symbol-tree@3.2.4: {} - synckit@0.11.3: + synckit@0.10.4: dependencies: - '@pkgr/core': 0.2.2 + '@pkgr/core': 0.2.9 tslib: 2.8.1 - synckit@0.9.3: + synckit@0.11.11: dependencies: - '@pkgr/core': 0.1.2 - tslib: 2.8.1 + '@pkgr/core': 0.2.9 tailwind-merge@3.3.1: {} @@ -13214,7 +13105,7 @@ snapshots: terser@5.39.2: dependencies: '@jridgewell/source-map': 0.3.6 - acorn: 8.14.1 + acorn: 8.15.0 commander: 2.20.3 source-map-support: 0.5.21 @@ -13312,24 +13203,22 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-fest@0.20.2: {} - type-fest@2.19.0: {} type-fest@4.41.0: {} - typescript-eslint@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2): + typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/parser': 8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) - '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) - '@typescript-eslint/utils': 8.42.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/parser': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2) eslint: 9.35.0(jiti@2.4.2) typescript: 5.9.2 transitivePeerDependencies: - supports-color - typescript@5.4.2: {} + typescript@5.8.2: {} typescript@5.8.3: {} @@ -13339,6 +13228,8 @@ snapshots: ufo@1.5.4: {} + ufo@1.6.1: {} + uint8array-extras@1.5.0: {} undici-types@5.26.5: {} @@ -13389,8 +13280,6 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - universalify@0.1.2: {} - universalify@2.0.1: {} unplugin-icons@0.22.0(@vue/compiler-sfc@3.5.13): @@ -13407,7 +13296,7 @@ snapshots: transitivePeerDependencies: - supports-color - unplugin-vue-components@0.28.0(@babel/parser@7.28.3)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)): + unplugin-vue-components@0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.1.4(rollup@4.22.4) @@ -13415,25 +13304,25 @@ snapshots: debug: 4.4.1 fast-glob: 3.3.3 local-pkg: 0.5.1 - magic-string: 0.30.17 + magic-string: 0.30.18 minimatch: 9.0.5 mlly: 1.7.4 unplugin: 2.3.5 vue: 3.5.13(typescript@5.9.2) optionalDependencies: - '@babel/parser': 7.28.3 + '@babel/parser': 7.28.4 transitivePeerDependencies: - rollup - supports-color unplugin@1.16.1: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 webpack-virtual-modules: 0.6.2 unplugin@2.3.5: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 @@ -13446,7 +13335,7 @@ snapshots: update-notifier@7.3.1: dependencies: boxen: 8.0.1 - chalk: 5.3.0 + chalk: 5.6.0 configstore: 7.0.0 is-in-ci: 1.0.0 is-installed-globally: 1.0.0 @@ -13504,17 +13393,17 @@ snapshots: - supports-color - terser - vite-plugin-dts@4.3.0(@types/node@20.14.10)(rollup@4.22.4)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)): + vite-plugin-dts@4.5.4(@types/node@20.14.10)(rollup@4.22.4)(typescript@5.9.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2)): dependencies: - '@microsoft/api-extractor': 7.48.0(@types/node@20.14.10) - '@rollup/pluginutils': 5.1.4(rollup@4.22.4) - '@volar/typescript': 2.4.15 - '@vue/language-core': 2.1.6(typescript@5.9.2) + '@microsoft/api-extractor': 7.52.13(@types/node@20.14.10) + '@rollup/pluginutils': 5.3.0(rollup@4.22.4) + '@volar/typescript': 2.4.23 + '@vue/language-core': 2.2.0(typescript@5.9.2) compare-versions: 6.1.1 - debug: 4.4.1 + debug: 4.4.3 kolorist: 1.8.0 - local-pkg: 0.5.1 - magic-string: 0.30.17 + local-pkg: 1.1.2 + magic-string: 0.30.19 typescript: 5.9.2 optionalDependencies: vite: 5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2) @@ -13529,7 +13418,7 @@ snapshots: colorette: 2.0.20 connect-history-api-fallback: 1.6.0 consola: 2.15.3 - dotenv: 16.4.5 + dotenv: 16.6.1 dotenv-expand: 8.0.3 ejs: 3.1.10 fast-glob: 3.3.3 @@ -13676,15 +13565,14 @@ snapshots: vue: 3.5.13(typescript@5.9.2) vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.13(typescript@5.9.2)) - vue-eslint-parser@9.4.3(eslint@9.35.0(jiti@2.4.2)): + vue-eslint-parser@10.2.0(eslint@9.35.0(jiti@2.4.2)): dependencies: debug: 4.4.1 eslint: 9.35.0(jiti@2.4.2) - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 esquery: 1.6.0 - lodash: 4.17.21 semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -13705,11 +13593,10 @@ snapshots: '@vue/devtools-api': 6.6.3 vue: 3.5.13(typescript@5.9.2) - vue-tsc@2.1.10(typescript@5.9.2): + vue-tsc@3.0.7(typescript@5.9.2): dependencies: - '@volar/typescript': 2.4.15 - '@vue/language-core': 2.1.10(typescript@5.9.2) - semver: 7.7.2 + '@volar/typescript': 2.4.23 + '@vue/language-core': 3.0.7(typescript@5.9.2) typescript: 5.9.2 vue@3.5.13(typescript@5.9.2): @@ -13804,7 +13691,7 @@ snapshots: worker-factory@7.0.43: dependencies: - '@babel/runtime': 7.27.6 + '@babel/runtime': 7.28.4 fast-unique-numbers: 9.0.22 tslib: 2.8.1 @@ -13828,8 +13715,6 @@ snapshots: wrappy@1.0.2: {} - ws@8.18.0: {} - ws@8.18.3: {} xdg-basedir@5.1.0: {} @@ -13851,7 +13736,7 @@ snapshots: yaml-eslint-parser@1.3.0: dependencies: eslint-visitor-keys: 3.4.3 - yaml: 2.4.5 + yaml: 2.8.1 yaml@1.10.2: {} diff --git a/public/fonts/inter-latin-italic.woff2 b/public/fonts/inter-latin-italic.woff2 new file mode 100644 index 000000000..39eb63673 Binary files /dev/null and b/public/fonts/inter-latin-italic.woff2 differ diff --git a/public/fonts/inter-latin-normal.woff2 b/public/fonts/inter-latin-normal.woff2 new file mode 100644 index 000000000..b0d0e2e5c Binary files /dev/null and b/public/fonts/inter-latin-normal.woff2 differ diff --git a/scripts/collect-i18n-general.ts b/scripts/collect-i18n-general.ts index f0b6dde0c..53c813fb7 100644 --- a/scripts/collect-i18n-general.ts +++ b/scripts/collect-i18n-general.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage' import { CORE_MENU_COMMANDS } from '../src/constants/coreMenuCommands' +import { DESKTOP_DIALOGS } from '../src/constants/desktopDialogs' import { SERVER_CONFIG_ITEMS } from '../src/constants/serverConfig' import type { FormItem, SettingParams } from '../src/platform/settings/types' import type { ComfyCommandImpl } from '../src/stores/commandStore' @@ -131,6 +132,23 @@ test('collect-i18n-general', async ({ comfyPage }) => { ]) ) + // Desktop Dialogs + const allDesktopDialogsLocale = Object.fromEntries( + Object.values(DESKTOP_DIALOGS).map((dialog) => [ + normalizeI18nKey(dialog.id), + { + title: dialog.title, + message: dialog.message, + buttons: Object.fromEntries( + dialog.buttons.map((button) => [ + normalizeI18nKey(button.label), + button.label + ]) + ) + } + ]) + ) + fs.writeFileSync( localePath, JSON.stringify( @@ -144,7 +162,8 @@ test('collect-i18n-general', async ({ comfyPage }) => { ...allSettingCategoriesLocale }, serverConfigItems: allServerConfigsLocale, - serverConfigCategories: allServerConfigCategoriesLocale + serverConfigCategories: allServerConfigCategoriesLocale, + desktopDialogs: allDesktopDialogsLocale }, null, 2 diff --git a/src/assets/css/fonts.css b/src/assets/css/fonts.css new file mode 100644 index 000000000..cea388ee7 --- /dev/null +++ b/src/assets/css/fonts.css @@ -0,0 +1,17 @@ +/* Inter Font Family */ + +@font-face { + font-family: 'Inter'; + src: url('/fonts/inter-latin-normal.woff2') format('woff2'); + font-weight: 100 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Inter'; + src: url('/fonts/inter-latin-italic.woff2') format('woff2'); + font-weight: 100 900; + font-style: italic; + font-display: swap; +} diff --git a/src/assets/css/style.css b/src/assets/css/style.css index 70b6bf0d3..cad8a1b3b 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -1,5 +1,6 @@ @layer theme, base, primevue, components, utilities; +@import './fonts.css'; @import 'tailwindcss/theme' layer(theme); @import 'tailwindcss/utilities' layer(utilities); @import 'tw-animate-css'; @@ -52,15 +53,20 @@ --text-xxs: 0.625rem; --text-xxs--line-height: calc(1 / 0.625); + /* Font Families */ + --font-inter: 'Inter', sans-serif; + /* Palette Colors */ - --color-charcoal-100: #171718; - --color-charcoal-200: #202121; - --color-charcoal-300: #262729; - --color-charcoal-400: #2d2e32; - --color-charcoal-500: #313235; - --color-charcoal-600: #3c3d42; - --color-charcoal-700: #494a50; - --color-charcoal-800: #55565e; + --color-charcoal-100: #55565e; + --color-charcoal-200: #494a50; + --color-charcoal-300: #3c3d42; + --color-charcoal-400: #313235; + --color-charcoal-500: #2d2e32; + --color-charcoal-600: #262729; + --color-charcoal-700: #202121; + --color-charcoal-800: #171718; + + --color-neutral-550: #636363; --color-stone-100: #444444; --color-stone-200: #828282; @@ -99,12 +105,16 @@ --color-danger-100: #c02323; --color-danger-200: #d62952; - --color-bypass: #6A246A; + --color-coral-red-600: #973a40; + --color-coral-red-500: #c53f49; + --color-coral-red-400: #dd424e; + + --color-bypass: #6a246a; --color-error: #962a2a; - --color-blue-selection: rgb( from var(--color-blue-100) r g b / 0.3); - --color-node-hover-100: rgb( from var(--color-charcoal-800) r g b/ 0.15); - --color-node-hover-200: rgb(from var(--color-charcoal-800) r g b/ 0.1); + --color-blue-selection: rgb(from var(--color-blue-100) r g b / 0.3); + --color-node-hover-100: rgb(from var(--color-charcoal-100) r g b/ 0.15); + --color-node-hover-200: rgb(from var(--color-charcoal-100) r g b/ 0.1); --color-modal-tag: rgb(from var(--color-gray-400) r g b/ 0.4); /* PrimeVue pulled colors */ @@ -117,10 +127,10 @@ } @theme inline { - --color-node-component-surface: var(--color-charcoal-300); + --color-node-component-surface: var(--color-charcoal-600); --color-node-component-surface-highlight: var(--color-slate-100); - --color-node-component-surface-hovered: var(--color-charcoal-500); - --color-node-component-surface-selected: var(--color-charcoal-700); + --color-node-component-surface-hovered: var(--color-charcoal-400); + --color-node-component-surface-selected: var(--color-charcoal-200); --color-node-stroke: var(--color-stone-100); } @@ -132,7 +142,7 @@ @utility scrollbar-hide { scrollbar-width: none; - &::-webkit-scrollbar { + &::-webkit-scrollbar { width: 1px; } &::-webkit-scrollbar-thumb { diff --git a/src/base/common/async.ts b/src/base/common/async.ts new file mode 100644 index 000000000..a97f6f1bd --- /dev/null +++ b/src/base/common/async.ts @@ -0,0 +1,98 @@ +/** + * Cross-browser async utilities for scheduling tasks during browser idle time + * with proper fallbacks for browsers that don't support requestIdleCallback. + * + * Implementation based on: + * https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts + */ + +interface IdleDeadline { + didTimeout: boolean + timeRemaining(): number +} + +interface IDisposable { + dispose(): void +} + +/** + * Internal implementation function that handles the actual scheduling logic. + * Uses feature detection to determine whether to use native requestIdleCallback + * or fall back to setTimeout-based implementation. + */ +let _runWhenIdle: ( + targetWindow: any, + callback: (idle: IdleDeadline) => void, + timeout?: number +) => IDisposable + +/** + * Execute the callback during the next browser idle period. + * Falls back to setTimeout-based scheduling in browsers without native support. + */ +export let runWhenGlobalIdle: ( + callback: (idle: IdleDeadline) => void, + timeout?: number + ) => IDisposable + + // Self-invoking function to set up the idle callback implementation +;(function () { + const safeGlobal: any = globalThis + + if ( + typeof safeGlobal.requestIdleCallback !== 'function' || + typeof safeGlobal.cancelIdleCallback !== 'function' + ) { + // Fallback implementation for browsers without native support (e.g., Safari) + _runWhenIdle = (_targetWindow, runner, _timeout?) => { + setTimeout(() => { + if (disposed) { + return + } + + // Simulate IdleDeadline - give 15ms window (one frame at ~64fps) + const end = Date.now() + 15 + const deadline: IdleDeadline = { + didTimeout: true, + timeRemaining() { + return Math.max(0, end - Date.now()) + } + } + + runner(Object.freeze(deadline)) + }) + + let disposed = false + return { + dispose() { + if (disposed) { + return + } + disposed = true + } + } + } + } else { + // Native requestIdleCallback implementation + _runWhenIdle = (targetWindow: typeof safeGlobal, runner, timeout?) => { + const handle: number = targetWindow.requestIdleCallback( + runner, + typeof timeout === 'number' ? { timeout } : undefined + ) + + let disposed = false + return { + dispose() { + if (disposed) { + return + } + disposed = true + targetWindow.cancelIdleCallback(handle) + } + } + } + } + + runWhenGlobalIdle = (runner, timeout) => + _runWhenIdle(globalThis, runner, timeout) +})() diff --git a/src/components/MenuHamburger.vue b/src/components/MenuHamburger.vue index b46c27e27..d0856362f 100644 --- a/src/components/MenuHamburger.vue +++ b/src/components/MenuHamburger.vue @@ -21,7 +21,8 @@ diff --git a/src/components/dialog/content/setting/UsageLogsTable.vue b/src/components/dialog/content/setting/UsageLogsTable.vue index 161268b91..082fb200c 100644 --- a/src/components/dialog/content/setting/UsageLogsTable.vue +++ b/src/components/dialog/content/setting/UsageLogsTable.vue @@ -96,8 +96,8 @@ import Message from 'primevue/message' import ProgressSpinner from 'primevue/progressspinner' import { computed, ref } from 'vue' +import type { AuditLog } from '@/services/customerEventsService' import { - AuditLog, EventType, useCustomerEventsService } from '@/services/customerEventsService' diff --git a/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue index 0947af884..ded8cb18b 100644 --- a/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue +++ b/src/components/dialog/content/setting/keybinding/KeyComboDisplay.vue @@ -13,7 +13,7 @@ import Tag from 'primevue/tag' import { computed } from 'vue' -import { KeyComboImpl } from '@/stores/keybindingStore' +import type { KeyComboImpl } from '@/stores/keybindingStore' const { keyCombo, isModified = false } = defineProps<{ keyCombo: KeyComboImpl diff --git a/src/components/dialog/content/signin/ApiKeyForm.vue b/src/components/dialog/content/signin/ApiKeyForm.vue index 01e2e0a3f..dc0b196a4 100644 --- a/src/components/dialog/content/signin/ApiKeyForm.vue +++ b/src/components/dialog/content/signin/ApiKeyForm.vue @@ -79,7 +79,8 @@ diff --git a/src/platform/assets/components/AssetBrowserModal.stories.ts b/src/platform/assets/components/AssetBrowserModal.stories.ts new file mode 100644 index 000000000..9d2321d57 --- /dev/null +++ b/src/platform/assets/components/AssetBrowserModal.stories.ts @@ -0,0 +1,179 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue' +import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser' +import { + createMockAssets, + mockAssets +} from '@/platform/assets/fixtures/ui-mock-assets' + +// Story arguments interface +interface StoryArgs { + nodeType: string + inputName: string + currentValue: string + showLeftPanel?: boolean +} + +const meta: Meta = { + title: 'Platform/Assets/AssetBrowserModal', + component: AssetBrowserModal, + parameters: { + layout: 'fullscreen' + }, + argTypes: { + nodeType: { + control: 'select', + options: ['CheckpointLoaderSimple', 'VAELoader', 'ControlNetLoader'], + description: 'ComfyUI node type for context' + }, + inputName: { + control: 'select', + options: ['ckpt_name', 'vae_name', 'control_net_name'], + description: 'Widget input name' + }, + currentValue: { + control: 'text', + description: 'Current selected asset value' + }, + showLeftPanel: { + control: 'boolean', + description: 'Whether to show the left panel with categories' + } + } +} + +export default meta +type Story = StoryObj + +// Modal Layout Stories +export const Default: Story = { + args: { + nodeType: 'CheckpointLoaderSimple', + inputName: 'ckpt_name', + currentValue: '', + showLeftPanel: false + }, + render: (args) => ({ + components: { AssetBrowserModal }, + setup() { + const onAssetSelect = (asset: AssetDisplayItem) => { + console.log('Selected asset:', asset) + } + const onClose = () => { + console.log('Modal closed') + } + + return { + ...args, + onAssetSelect, + onClose, + assets: mockAssets + } + }, + template: ` +
+ +
+ ` + }) +} + +// Story demonstrating single asset type (auto-hides left panel) +export const SingleAssetType: Story = { + args: { + nodeType: 'CheckpointLoaderSimple', + inputName: 'ckpt_name', + currentValue: '', + showLeftPanel: false + }, + render: (args) => ({ + components: { AssetBrowserModal }, + setup() { + const onAssetSelect = (asset: AssetDisplayItem) => { + console.log('Selected asset:', asset) + } + const onClose = () => { + console.log('Modal closed') + } + + // Create assets with only one type (checkpoints) + const singleTypeAssets = createMockAssets(15).map((asset) => ({ + ...asset, + type: 'checkpoint' + })) + + return { ...args, onAssetSelect, onClose, assets: singleTypeAssets } + }, + template: ` +
+ +
+ ` + }), + parameters: { + docs: { + description: { + story: + 'Modal with assets of only one type (checkpoint) - left panel auto-hidden.' + } + } + } +} + +// Story with left panel explicitly hidden +export const NoLeftPanel: Story = { + args: { + nodeType: 'CheckpointLoaderSimple', + inputName: 'ckpt_name', + currentValue: '', + showLeftPanel: false + }, + render: (args) => ({ + components: { AssetBrowserModal }, + setup() { + const onAssetSelect = (asset: AssetDisplayItem) => { + console.log('Selected asset:', asset) + } + const onClose = () => { + console.log('Modal closed') + } + + return { ...args, onAssetSelect, onClose, assets: mockAssets } + }, + template: ` +
+ +
+ ` + }), + parameters: { + docs: { + description: { + story: + 'Modal with left panel explicitly disabled via showLeftPanel=false.' + } + } + } +} diff --git a/src/platform/assets/components/AssetBrowserModal.vue b/src/platform/assets/components/AssetBrowserModal.vue new file mode 100644 index 000000000..cb45f38ba --- /dev/null +++ b/src/platform/assets/components/AssetBrowserModal.vue @@ -0,0 +1,89 @@ + + + diff --git a/src/platform/assets/components/AssetCard.stories.ts b/src/platform/assets/components/AssetCard.stories.ts new file mode 100644 index 000000000..2b3532a05 --- /dev/null +++ b/src/platform/assets/components/AssetCard.stories.ts @@ -0,0 +1,182 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import AssetCard from '@/platform/assets/components/AssetCard.vue' +import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser' +import { mockAssets } from '@/platform/assets/fixtures/ui-mock-assets' + +// Use the first mock asset as base and transform it to display format +const baseAsset = mockAssets[0] +const createAssetData = ( + overrides: Partial = {} +): AssetDisplayItem => ({ + ...baseAsset, + description: + 'High-quality realistic images with perfect detail and natural lighting effects for professional photography', + formattedSize: '2.1 GB', + badges: [ + { label: 'checkpoints', type: 'type' }, + { label: '2.1 GB', type: 'size' } + ], + stats: { + formattedDate: '3/15/25', + downloadCount: '1.8k', + stars: '4.2k' + }, + ...overrides +}) + +const meta: Meta = { + title: 'Platform/Assets/AssetCard', + component: AssetCard, + parameters: { + layout: 'centered' + }, + decorators: [ + () => ({ + template: + '
' + }) + ] +} + +export default meta +type Story = StoryObj + +export const Interactive: Story = { + args: { + asset: createAssetData(), + interactive: true + }, + decorators: [ + () => ({ + template: + '
' + }) + ], + parameters: { + docs: { + description: { + story: + 'Default AssetCard with complete data including badges and all stats.' + } + } + } +} + +export const NonInteractive: Story = { + args: { + asset: createAssetData(), + interactive: false + }, + decorators: [ + () => ({ + template: + '
' + }) + ], + parameters: { + docs: { + description: { + story: + 'AssetCard in non-interactive mode - renders as div without button semantics.' + } + } + } +} + +export const EdgeCases: Story = { + render: () => ({ + components: { AssetCard }, + setup() { + const edgeCases = [ + // Default case for comparison + createAssetData({ + name: 'Complete Data', + description: 'Asset with all data present for comparison' + }), + // No badges + createAssetData({ + id: 'no-badges', + name: 'No Badges', + description: 'Testing graceful handling when badges are not provided', + badges: [] + }), + // No stars + createAssetData({ + id: 'no-stars', + name: 'No Stars', + description: 'Testing missing stars data gracefully', + stats: { + downloadCount: '1.8k', + formattedDate: '3/15/25' + } + }), + // No downloads + createAssetData({ + id: 'no-downloads', + name: 'No Downloads', + description: 'Testing missing downloads data gracefully', + stats: { + stars: '4.2k', + formattedDate: '3/15/25' + } + }), + // No date + createAssetData({ + id: 'no-date', + name: 'No Date', + description: 'Testing missing date data gracefully', + stats: { + stars: '4.2k', + downloadCount: '1.8k' + } + }), + // No stats at all + createAssetData({ + id: 'no-stats', + name: 'No Stats', + description: 'Testing when all stats are missing', + stats: {} + }), + // Long description + createAssetData({ + id: 'long-desc', + name: 'Long Description', + description: + 'This is a very long description that should demonstrate how the component handles text overflow and truncation with ellipsis. The description continues with even more content to ensure we test the 2-line clamp behavior properly and see how it renders when there is significantly more text than can fit in the allocated space.' + }), + // Minimal data + createAssetData({ + id: 'minimal', + name: 'Minimal', + description: 'Basic model', + tags: ['models'], + badges: [], + stats: {} + }) + ] + + return { edgeCases } + }, + template: ` +
+ +
+ ` + }), + parameters: { + layout: 'fullscreen', + docs: { + description: { + story: + 'All AssetCard edge cases in a grid layout to test graceful handling of missing data, badges, stats, and long descriptions.' + } + } + } +} diff --git a/src/platform/assets/components/AssetCard.vue b/src/platform/assets/components/AssetCard.vue new file mode 100644 index 000000000..be7c45ca5 --- /dev/null +++ b/src/platform/assets/components/AssetCard.vue @@ -0,0 +1,111 @@ + + + diff --git a/src/platform/assets/components/AssetFilterBar.vue b/src/platform/assets/components/AssetFilterBar.vue new file mode 100644 index 000000000..904ce3e82 --- /dev/null +++ b/src/platform/assets/components/AssetFilterBar.vue @@ -0,0 +1,103 @@ + + + diff --git a/src/platform/assets/components/AssetGrid.vue b/src/platform/assets/components/AssetGrid.vue new file mode 100644 index 000000000..35122fd52 --- /dev/null +++ b/src/platform/assets/components/AssetGrid.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/platform/assets/composables/useAssetBrowser.ts b/src/platform/assets/composables/useAssetBrowser.ts new file mode 100644 index 000000000..97ed9746e --- /dev/null +++ b/src/platform/assets/composables/useAssetBrowser.ts @@ -0,0 +1,217 @@ +import { computed, ref } from 'vue' + +import { d, t } from '@/i18n' +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +import { assetFilenameSchema } from '@/platform/assets/schemas/assetSchema' +import { assetService } from '@/platform/assets/services/assetService' +import { + getAssetBaseModel, + getAssetDescription +} from '@/platform/assets/utils/assetMetadataUtils' +import { formatSize } from '@/utils/formatUtil' + +type AssetBadge = { + label: string + type: 'type' | 'base' | 'size' +} + +// Display properties for transformed assets +export interface AssetDisplayItem extends AssetItem { + description: string + formattedSize: string + badges: AssetBadge[] + stats: { + formattedDate?: string + downloadCount?: string + stars?: string + } +} + +/** + * Asset Browser composable + * Manages search, filtering, asset transformation and selection logic + */ +export function useAssetBrowser(assets: AssetItem[] = []) { + // State + const searchQuery = ref('') + const selectedCategory = ref('all') + const sortBy = ref('name') + + // Transform API asset to display asset + function transformAssetForDisplay(asset: AssetItem): AssetDisplayItem { + // Extract description from metadata or create from tags + const typeTag = asset.tags.find((tag) => tag !== 'models') + const description = + getAssetDescription(asset) || + `${typeTag || t('assetBrowser.unknown')} model` + + // Format file size + const formattedSize = formatSize(asset.size) + + // Create badges from tags and metadata + const badges: AssetBadge[] = [] + + // Type badge from non-root tag + if (typeTag) { + badges.push({ label: typeTag, type: 'type' }) + } + + // Base model badge from metadata + const baseModel = getAssetBaseModel(asset) + if (baseModel) { + badges.push({ + label: baseModel, + type: 'base' + }) + } + + // Size badge + badges.push({ label: formattedSize, type: 'size' }) + + // Create display stats from API data + const stats = { + formattedDate: d(new Date(asset.created_at), { dateStyle: 'short' }), + downloadCount: undefined, // Not available in API + stars: undefined // Not available in API + } + + return { + ...asset, + description, + formattedSize, + badges, + stats + } + } + + // Extract available categories from assets + const availableCategories = computed(() => { + const categorySet = new Set() + + assets.forEach((asset) => { + // Second tag is the category (after 'models' root tag) + if (asset.tags.length > 1 && asset.tags[0] === 'models') { + categorySet.add(asset.tags[1]) + } + }) + + return [ + { + id: 'all', + label: t('assetBrowser.allModels'), + icon: 'icon-[lucide--folder]' + }, + ...Array.from(categorySet) + .sort() + .map((category) => ({ + id: category, + label: category.charAt(0).toUpperCase() + category.slice(1), + icon: 'icon-[lucide--package]' + })) + ] + }) + + // Compute content title from selected category + const contentTitle = computed(() => { + if (selectedCategory.value === 'all') { + return t('assetBrowser.allModels') + } + + const category = availableCategories.value.find( + (cat) => cat.id === selectedCategory.value + ) + return category?.label || t('assetBrowser.assets') + }) + + // Filter functions + const filterByCategory = (category: string) => (asset: AssetItem) => { + if (category === 'all') return true + return asset.tags.includes(category) + } + + const filterByQuery = (query: string) => (asset: AssetItem) => { + if (!query) return true + const lowerQuery = query.toLowerCase() + const description = getAssetDescription(asset) + return ( + asset.name.toLowerCase().includes(lowerQuery) || + (description && description.toLowerCase().includes(lowerQuery)) || + asset.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)) + ) + } + + // Computed filtered and transformed assets + const filteredAssets = computed(() => { + const filtered = assets + .filter(filterByCategory(selectedCategory.value)) + .filter(filterByQuery(searchQuery.value)) + + // Sort assets + filtered.sort((a, b) => { + switch (sortBy.value) { + case 'date': + return ( + new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ) + case 'name': + default: + return a.name.localeCompare(b.name) + } + }) + + // Transform to display format + return filtered.map(transformAssetForDisplay) + }) + + /** + * Asset selection that fetches full details and executes callback with filename + * @param assetId - The asset ID to select and fetch details for + * @param onSelect - Optional callback to execute with the asset filename + */ + async function selectAssetWithCallback( + assetId: string, + onSelect?: (filename: string) => void + ): Promise { + if (import.meta.env.DEV) { + console.debug('Asset selected:', assetId) + } + + if (!onSelect) { + return + } + + try { + const detailAsset = await assetService.getAssetDetails(assetId) + const filename = detailAsset.user_metadata?.filename + const validatedFilename = assetFilenameSchema.safeParse(filename) + if (!validatedFilename.success) { + console.error( + 'Invalid asset filename:', + validatedFilename.error.errors, + 'for asset:', + assetId + ) + return + } + + onSelect(validatedFilename.data) + } catch (error) { + console.error(`Failed to fetch asset details for ${assetId}:`, error) + } + } + + return { + // State + searchQuery, + selectedCategory, + sortBy, + + // Computed + availableCategories, + contentTitle, + filteredAssets, + + // Actions + selectAssetWithCallback + } +} diff --git a/src/platform/assets/composables/useAssetBrowserDialog.stories.ts b/src/platform/assets/composables/useAssetBrowserDialog.stories.ts new file mode 100644 index 000000000..e0095b619 --- /dev/null +++ b/src/platform/assets/composables/useAssetBrowserDialog.stories.ts @@ -0,0 +1,203 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue' +import { mockAssets } from '@/platform/assets/fixtures/ui-mock-assets' + +// Component that simulates the useAssetBrowserDialog functionality with working close +const DialogDemoComponent = { + components: { AssetBrowserModal }, + setup() { + const isDialogOpen = ref(false) + const currentNodeType = ref('CheckpointLoaderSimple') + const currentInputName = ref('ckpt_name') + const currentValue = ref('') + + const handleOpenDialog = ( + nodeType: string, + inputName: string, + value = '' + ) => { + currentNodeType.value = nodeType + currentInputName.value = inputName + currentValue.value = value + isDialogOpen.value = true + } + + const handleCloseDialog = () => { + isDialogOpen.value = false + } + + const handleAssetSelected = (assetPath: string) => { + console.log('Asset selected:', assetPath) + alert(`Selected asset: ${assetPath}`) + isDialogOpen.value = false // Auto-close like the real composable + } + + const handleOpenWithCurrentValue = () => { + handleOpenDialog( + 'CheckpointLoaderSimple', + 'ckpt_name', + 'realistic_vision_v5.safetensors' + ) + } + + return { + isDialogOpen, + currentNodeType, + currentInputName, + currentValue, + handleOpenDialog, + handleOpenWithCurrentValue, + handleCloseDialog, + handleAssetSelected, + mockAssets + } + }, + template: ` +
+
+

Asset Browser Dialog Demo

+ +
+
+

Different Node Types

+
+ + + +
+
+ +
+

With Current Value

+ +

+ Opens with "realistic_vision_v5.safetensors" as current value +

+
+ +
+

Instructions:

+
    +
  • • Click any button to open the Asset Browser dialog
  • +
  • • Select an asset to see the callback in action
  • +
  • • Check the browser console for logged events
  • +
  • • Try toggling the left panel with different asset types
  • +
  • • Close button will work properly in this demo
  • +
+
+
+
+ + +
+
+ +
+
+
+ ` +} + +const meta: Meta = { + title: 'Platform/Assets/useAssetBrowserDialog', + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: + 'Demonstrates the AssetBrowserModal functionality as used by the useAssetBrowserDialog composable.' + } + } + } +} + +export default meta +type Story = StoryObj + +export const Demo: Story = { + render: () => ({ + components: { DialogDemoComponent }, + template: ` +
+ + + +
+

Code Example

+

+ This is how you would use the composable in your component: +

+
+
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
+
+export default {
+  setup() {
+    const assetBrowserDialog = useAssetBrowserDialog()
+
+    const openBrowser = () => {
+      assetBrowserDialog.show({
+        nodeType: 'CheckpointLoaderSimple',
+        inputName: 'ckpt_name',
+        currentValue: '',
+        onAssetSelected: (assetPath) => {
+          console.log('Selected:', assetPath)
+          // Update your component state
+        }
+      })
+    }
+
+    return { openBrowser }
+  }
+}
+
+
+

+ 💡 Try it: Use the interactive buttons above to see this code in action! +

+
+
+
+ ` + }), + parameters: { + docs: { + description: { + story: + 'Complete demo showing both interactive functionality and code examples for using useAssetBrowserDialog to open the Asset Browser modal programmatically.' + } + } + } +} diff --git a/src/platform/assets/composables/useAssetBrowserDialog.ts b/src/platform/assets/composables/useAssetBrowserDialog.ts new file mode 100644 index 000000000..31f75c353 --- /dev/null +++ b/src/platform/assets/composables/useAssetBrowserDialog.ts @@ -0,0 +1,73 @@ +import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue' +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +import { assetService } from '@/platform/assets/services/assetService' +import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore' + +interface AssetBrowserDialogProps { + /** ComfyUI node type for context (e.g., 'CheckpointLoaderSimple') */ + nodeType: string + /** Widget input name (e.g., 'ckpt_name') */ + inputName: string + /** Current selected asset value */ + currentValue?: string + /** + * Callback for when an asset is selected + * @param {string} filename - The validated filename from user_metadata.filename + */ + onAssetSelected?: (filename: string) => void +} + +export const useAssetBrowserDialog = () => { + const dialogStore = useDialogStore() + const dialogKey = 'global-asset-browser' + + async function show(props: AssetBrowserDialogProps) { + const handleAssetSelected = (filename: string) => { + props.onAssetSelected?.(filename) + dialogStore.closeDialog({ key: dialogKey }) + } + const dialogComponentProps: DialogComponentProps = { + headless: true, + modal: true, + closable: true, + pt: { + root: { + class: 'rounded-2xl overflow-hidden asset-browser-dialog' + }, + header: { + class: 'p-0 hidden' + }, + content: { + class: 'p-0 m-0 h-full w-full' + } + } + } + + const assets: AssetItem[] = await assetService + .getAssetsForNodeType(props.nodeType) + .catch((error) => { + console.error( + 'Failed to fetch assets for node type:', + props.nodeType, + error + ) + return [] + }) + + dialogStore.showDialog({ + key: dialogKey, + component: AssetBrowserModal, + props: { + nodeType: props.nodeType, + inputName: props.inputName, + currentValue: props.currentValue, + assets, + onSelect: handleAssetSelected, + onClose: () => dialogStore.closeDialog({ key: dialogKey }) + }, + dialogComponentProps + }) + } + + return { show } +} diff --git a/src/platform/assets/composables/useAssetFilterOptions.ts b/src/platform/assets/composables/useAssetFilterOptions.ts new file mode 100644 index 000000000..30572d8c9 --- /dev/null +++ b/src/platform/assets/composables/useAssetFilterOptions.ts @@ -0,0 +1,56 @@ +import { uniqWith } from 'es-toolkit' +import { computed } from 'vue' + +import type { SelectOption } from '@/components/input/types' +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' + +/** + * Composable that extracts available filter options from asset data + * Provides reactive computed properties for file formats and base models + */ +export function useAssetFilterOptions(assets: AssetItem[] = []) { + /** + * Extract unique file formats from asset names + * Returns sorted SelectOption array with extensions + */ + const availableFileFormats = computed(() => { + const extensions = assets + .map((asset) => { + const extension = asset.name.split('.').pop() + return extension && extension !== asset.name ? extension : null + }) + .filter((extension): extension is string => extension !== null) + + const uniqueExtensions = uniqWith(extensions, (a, b) => a === b) + + return uniqueExtensions.sort().map((format) => ({ + name: `.${format}`, + value: format + })) + }) + + /** + * Extract unique base models from asset user metadata + * Returns sorted SelectOption array with base model names + */ + const availableBaseModels = computed(() => { + const models = assets + .map((asset) => asset.user_metadata?.base_model) + .filter( + (baseModel): baseModel is string => + baseModel !== undefined && typeof baseModel === 'string' + ) + + const uniqueModels = uniqWith(models, (a, b) => a === b) + + return uniqueModels.sort().map((model) => ({ + name: model, + value: model + })) + }) + + return { + availableFileFormats, + availableBaseModels + } +} diff --git a/src/platform/assets/fixtures/ui-mock-assets.ts b/src/platform/assets/fixtures/ui-mock-assets.ts new file mode 100644 index 000000000..6c7284386 --- /dev/null +++ b/src/platform/assets/fixtures/ui-mock-assets.ts @@ -0,0 +1,128 @@ +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' + +// 🎭 OBVIOUSLY FAKE MOCK DATA - DO NOT USE IN PRODUCTION! 🎭 +const fakeFunnyModelNames = [ + '🎯_totally_real_model_v420.69', + '🚀_definitely_not_fake_v999', + '🎪_super_legit_checkpoint_pro_max', + '🦄_unicorn_dreams_totally_real.model', + '🍕_pizza_generator_supreme', + '🎸_rock_star_fake_data_v1337', + '🌮_taco_tuesday_model_deluxe', + '🦖_dino_nugget_generator_v3', + '🎮_gamer_fuel_checkpoint_xl', + '🍄_mushroom_kingdom_diffusion', + '🏴‍☠️_pirate_treasure_model_arr', + '🦋_butterfly_effect_generator', + '🎺_jazz_hands_checkpoint_pro', + '🥨_pretzel_logic_model_v2', + '🌙_midnight_snack_generator', + '🎭_drama_llama_checkpoint', + '🧙‍♀️_wizard_hat_diffusion_xl', + '🎪_circus_peanut_model_v4', + '🦒_giraffe_neck_generator', + '🎲_random_stuff_checkpoint_max' +] + +const obviouslyFakeDescriptions = [ + '⚠️ FAKE DATA: Generates 100% authentic fake images with premium mock quality', + '🎭 MOCK ALERT: This totally real model creates absolutely genuine fake content', + '🚨 NOT REAL: Professional-grade fake imagery for your mock data needs', + '🎪 DEMO ONLY: Circus-quality fake generation with extra mock seasoning', + '🍕 FAKE FOOD: Generates delicious fake pizzas (not edible in reality)', + "🎸 MOCK ROCK: Creates fake rock stars who definitely don't exist", + '🌮 TACO FAKERY: Tuesday-themed fake tacos for your mock appetite', + '🦖 PREHISTORIC FAKE: Generates extinct fake dinosaurs for demo purposes', + '🎮 FAKE GAMING: Level up your mock data with obviously fake content', + '🍄 FUNGI FICTION: Magical fake mushrooms from the demo dimension', + '🏴‍☠️ FAKE TREASURE: Arr! This be mock data for ye demo needs, matey!', + '🦋 DEMO EFFECT: Small fake changes create big mock differences', + '🎺 JAZZ FAKERY: Smooth fake jazz for your mock listening pleasure', + '🥨 MOCK LOGIC: Twisted fake reasoning for your demo requirements', + '🌙 MIDNIGHT MOCK: Late-night fake snacks for your demo hunger', + '🎭 FAKE DRAMA: Over-the-top mock emotions for demo entertainment', + '🧙‍♀️ WIZARD MOCK: Magically fake spells cast with demo ingredients', + '🎪 CIRCUS FAKE: Big top mock entertainment under the demo tent', + '🦒 TALL FAKE: Reaches new heights of obviously fake content', + '🎲 RANDOM MOCK: Generates random fake stuff for your demo pleasure' +] + +// API-compliant tag structure: first tag must be root (models/input/output), second is category +const modelCategories = ['checkpoints', 'loras', 'embeddings', 'vae'] +const baseModels = ['sd15', 'sdxl', 'sd35'] +const fileExtensions = ['.safetensors', '.ckpt', '.pt'] +const mimeTypes = [ + 'application/octet-stream', + 'application/x-pytorch', + 'application/x-safetensors' +] + +function getRandomElement(array: T[]): T { + return array[Math.floor(Math.random() * array.length)] +} + +function getRandomNumber(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +function getRandomISODate(): string { + const start = new Date('2024-01-01').getTime() + const end = new Date('2024-12-31').getTime() + const randomTime = start + Math.random() * (end - start) + return new Date(randomTime).toISOString() +} + +function generateFakeAssetHash(): string { + const chars = '0123456789abcdef' + let hash = 'blake3:' + for (let i = 0; i < 64; i++) { + hash += chars[Math.floor(Math.random() * chars.length)] + } + return hash +} + +// 🎭 CREATES OBVIOUSLY FAKE ASSETS FOR DEMO/TEST PURPOSES ONLY! 🎭 +export function createMockAssets(count: number = 20): AssetItem[] { + return Array.from({ length: count }, (_, index) => { + const category = getRandomElement(modelCategories) + const baseModel = getRandomElement(baseModels) + const extension = getRandomElement(fileExtensions) + const mimeType = getRandomElement(mimeTypes) + const sizeInBytes = getRandomNumber( + 500 * 1024 * 1024, + 8 * 1024 * 1024 * 1024 + ) // 500MB to 8GB + const createdAt = getRandomISODate() + const updatedAt = createdAt + const lastAccessTime = getRandomISODate() + + const fakeFileName = `${fakeFunnyModelNames[index]}${extension}` + + return { + id: `mock-asset-uuid-${(index + 1).toString().padStart(3, '0')}-fake`, + name: fakeFileName, + asset_hash: generateFakeAssetHash(), + size: sizeInBytes, + mime_type: mimeType, + tags: [ + 'models', // Root tag (required first) + category, // Category tag (required second for models) + 'fake-data', // Obviously fake tag + ...(Math.random() > 0.5 ? ['demo-mode'] : ['test-only']), + ...(Math.random() > 0.7 ? ['obviously-mock'] : []) + ], + preview_url: `/api/assets/mock-asset-uuid-${(index + 1).toString().padStart(3, '0')}-fake/content`, + created_at: createdAt, + updated_at: updatedAt, + last_access_time: lastAccessTime, + user_metadata: { + description: obviouslyFakeDescriptions[index], + base_model: baseModel, + original_name: fakeFunnyModelNames[index], + warning: '🚨 THIS IS FAKE DEMO DATA - NOT A REAL MODEL! 🚨' + } + } + }) +} + +export const mockAssets = createMockAssets(20) diff --git a/src/platform/assets/schemas/assetSchema.ts b/src/platform/assets/schemas/assetSchema.ts index 277efcbb0..2c051a30d 100644 --- a/src/platform/assets/schemas/assetSchema.ts +++ b/src/platform/assets/schemas/assetSchema.ts @@ -1,12 +1,19 @@ import { z } from 'zod' -// Zod schemas for asset API validation +// Zod schemas for asset API validation matching ComfyUI Assets REST API spec const zAsset = z.object({ id: z.string(), name: z.string(), - tags: z.array(z.string()), + asset_hash: z.string().nullable(), size: z.number(), - created_at: z.string().optional() + mime_type: z.string().nullable(), + tags: z.array(z.string()), + preview_url: z.string().optional(), + created_at: z.string(), + updated_at: z.string().optional(), + last_access_time: z.string(), + user_metadata: z.record(z.unknown()).optional(), // API allows arbitrary key-value pairs + preview_id: z.string().nullable().optional() }) const zAssetResponse = z.object({ @@ -20,19 +27,30 @@ const zModelFolder = z.object({ folders: z.array(z.string()) }) +// Zod schema for ModelFile to align with interface +const zModelFile = z.object({ + name: z.string(), + pathIndex: z.number() +}) + +// Filename validation schema +export const assetFilenameSchema = z + .string() + .min(1, 'Filename cannot be empty') + .regex(/^[^\\:*?"<>|]+$/, 'Invalid filename characters') // Allow forward slashes, block backslashes and other unsafe chars + .regex(/^(?!\/|.*\.\.)/, 'Path must not start with / or contain ..') // Prevent absolute paths and directory traversal + .trim() + // Export schemas following repository patterns export const assetResponseSchema = zAssetResponse // Export types derived from Zod schemas +export type AssetItem = z.infer export type AssetResponse = z.infer export type ModelFolder = z.infer +export type ModelFile = z.infer -// Common interfaces for API responses -export interface ModelFile { - name: string - pathIndex: number -} - +// Legacy interface for backward compatibility (now aligned with Zod schema) export interface ModelFolderInfo { name: string folders: string[] diff --git a/src/platform/assets/services/assetService.ts b/src/platform/assets/services/assetService.ts index 344209bf7..7d0f82cbb 100644 --- a/src/platform/assets/services/assetService.ts +++ b/src/platform/assets/services/assetService.ts @@ -1,6 +1,7 @@ import { fromZodError } from 'zod-validation-error' import { + type AssetItem, type AssetResponse, type ModelFile, type ModelFolder, @@ -67,7 +68,7 @@ function createAssetService() { ) // Blacklist directories we don't want to show - const blacklistedDirectories = ['configs'] + const blacklistedDirectories = new Set(['configs']) // Extract directory names from assets that actually exist, exclude missing assets const discoveredFolders = new Set( @@ -75,7 +76,7 @@ function createAssetService() { ?.filter((asset) => !asset.tags.includes(MISSING_TAG)) ?.flatMap((asset) => asset.tags) ?.filter( - (tag) => tag !== MODELS_TAG && !blacklistedDirectories.includes(tag) + (tag) => tag !== MODELS_TAG && !blacklistedDirectories.has(tag) ) ?? [] ) @@ -127,10 +128,75 @@ function createAssetService() { ) } + /** + * Gets assets for a specific node type by finding the matching category + * and fetching all assets with that category tag + * + * @param nodeType - The ComfyUI node type (e.g., 'CheckpointLoaderSimple') + * @returns Promise - Full asset objects with preserved metadata + */ + async function getAssetsForNodeType(nodeType: string): Promise { + if (!nodeType || typeof nodeType !== 'string') { + return [] + } + + // Find the category for this node type using efficient O(1) lookup + const modelToNodeStore = useModelToNodeStore() + const category = modelToNodeStore.getCategoryForNodeType(nodeType) + + if (!category) { + return [] + } + + // Fetch assets for this category using same API pattern as getAssetModels + const data = await handleAssetRequest( + `${ASSETS_ENDPOINT}?include_tags=${MODELS_TAG},${category}`, + `assets for ${nodeType}` + ) + + // Return full AssetItem[] objects (don't strip like getAssetModels does) + return ( + data?.assets?.filter( + (asset) => + !asset.tags.includes(MISSING_TAG) && asset.tags.includes(category) + ) ?? [] + ) + } + + /** + * Gets complete details for a specific asset by ID + * Calls the detail endpoint which includes user_metadata and all fields + * + * @param id - The asset ID + * @returns Promise - Complete asset object with user_metadata + */ + async function getAssetDetails(id: string): Promise { + const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`) + if (!res.ok) { + throw new Error( + `Unable to load asset details for ${id}: Server returned ${res.status}. Please try again.` + ) + } + const data = await res.json() + + // Validate the single asset response against our schema + const result = assetResponseSchema.safeParse({ assets: [data] }) + if (result.success && result.data.assets?.[0]) { + return result.data.assets[0] + } + + const error = result.error + ? fromZodError(result.error) + : 'Unknown validation error' + throw new Error(`Invalid asset response against zod schema:\n${error}`) + } + return { getAssetModelFolders, getAssetModels, - isAssetBrowserEligible + isAssetBrowserEligible, + getAssetsForNodeType, + getAssetDetails } } diff --git a/src/platform/assets/utils/assetMetadataUtils.ts b/src/platform/assets/utils/assetMetadataUtils.ts new file mode 100644 index 000000000..2d32fa07f --- /dev/null +++ b/src/platform/assets/utils/assetMetadataUtils.ts @@ -0,0 +1,27 @@ +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' + +/** + * Type-safe utilities for extracting metadata from assets + */ + +/** + * Safely extracts string description from asset metadata + * @param asset - The asset to extract description from + * @returns The description string or null if not present/not a string + */ +export function getAssetDescription(asset: AssetItem): string | null { + return typeof asset.user_metadata?.description === 'string' + ? asset.user_metadata.description + : null +} + +/** + * Safely extracts string base_model from asset metadata + * @param asset - The asset to extract base_model from + * @returns The base_model string or null if not present/not a string + */ +export function getAssetBaseModel(asset: AssetItem): string | null { + return typeof asset.user_metadata?.base_model === 'string' + ? asset.user_metadata.base_model + : null +} diff --git a/src/platform/settings/components/ServerConfigPanel.vue b/src/platform/settings/components/ServerConfigPanel.vue index 08f929ded..8f2cd1cd7 100644 --- a/src/platform/settings/components/ServerConfigPanel.vue +++ b/src/platform/settings/components/ServerConfigPanel.vue @@ -54,7 +54,7 @@
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue' import SettingGroup from '@/platform/settings/components/SettingGroup.vue' -import { ISettingGroup } from '@/platform/settings/types' +import type { ISettingGroup } from '@/platform/settings/types' const props = defineProps<{ settingGroups: ISettingGroup[] diff --git a/src/platform/settings/composables/useLitegraphSettings.ts b/src/platform/settings/composables/useLitegraphSettings.ts index 347e0289e..468c7d339 100644 --- a/src/platform/settings/composables/useLitegraphSettings.ts +++ b/src/platform/settings/composables/useLitegraphSettings.ts @@ -131,11 +131,26 @@ export const useLitegraphSettings = () => { const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode') as | 'standard' | 'legacy' + | 'custom' LiteGraph.canvasNavigationMode = navigationMode LiteGraph.macTrackpadGestures = navigationMode === 'standard' }) + watchEffect(() => { + const leftMouseBehavior = settingStore.get( + 'Comfy.Canvas.LeftMouseClickBehavior' + ) as 'panning' | 'select' + LiteGraph.leftMouseClickBehavior = leftMouseBehavior + }) + + watchEffect(() => { + const mouseWheelScroll = settingStore.get( + 'Comfy.Canvas.MouseWheelScroll' + ) as 'panning' | 'zoom' + LiteGraph.mouseWheelScroll = mouseWheelScroll + }) + watchEffect(() => { LiteGraph.saveViewportWithGraph = settingStore.get( 'Comfy.EnableWorkflowViewRestore' diff --git a/src/platform/settings/composables/useSettingSearch.ts b/src/platform/settings/composables/useSettingSearch.ts index c401cc415..c2bf3cfe8 100644 --- a/src/platform/settings/composables/useSettingSearch.ts +++ b/src/platform/settings/composables/useSettingSearch.ts @@ -1,12 +1,12 @@ import { computed, ref, watch } from 'vue' import { st } from '@/i18n' +import type { SettingTreeNode } from '@/platform/settings/settingStore' import { - SettingTreeNode, getSettingInfo, useSettingStore } from '@/platform/settings/settingStore' -import { ISettingGroup, SettingParams } from '@/platform/settings/types' +import type { ISettingGroup, SettingParams } from '@/platform/settings/types' import { normalizeI18nKey } from '@/utils/formatUtil' export function useSettingSearch() { diff --git a/src/platform/settings/composables/useSettingUI.ts b/src/platform/settings/composables/useSettingUI.ts index 9aa8ea316..8a01ec1e2 100644 --- a/src/platform/settings/composables/useSettingUI.ts +++ b/src/platform/settings/composables/useSettingUI.ts @@ -8,10 +8,8 @@ import { import { useI18n } from 'vue-i18n' import { useCurrentUser } from '@/composables/auth/useCurrentUser' -import { - SettingTreeNode, - useSettingStore -} from '@/platform/settings/settingStore' +import type { SettingTreeNode } from '@/platform/settings/settingStore' +import { useSettingStore } from '@/platform/settings/settingStore' import type { SettingParams } from '@/platform/settings/types' import { isElectron } from '@/utils/envUtil' import { normalizeI18nKey } from '@/utils/formatUtil' diff --git a/src/platform/settings/constants/coreSettings.ts b/src/platform/settings/constants/coreSettings.ts index cdb2fe709..4adf2db9d 100644 --- a/src/platform/settings/constants/coreSettings.ts +++ b/src/platform/settings/constants/coreSettings.ts @@ -1,4 +1,5 @@ import { LinkMarkerShape, LiteGraph } from '@/lib/litegraph/src/litegraph' +import { useSettingStore } from '@/platform/settings/settingStore' import type { SettingParams } from '@/platform/settings/types' import type { ColorPalettes } from '@/schemas/colorPaletteSchema' import type { Keybinding } from '@/schemas/keyBindingSchema' @@ -138,6 +139,95 @@ export const CORE_SETTINGS: SettingParams[] = [ type: 'boolean', defaultValue: false }, + { + id: 'Comfy.Canvas.NavigationMode', + category: ['LiteGraph', 'Canvas Navigation', 'NavigationMode'], + name: 'Navigation Mode', + defaultValue: 'legacy', + type: 'combo', + sortOrder: 100, + options: [ + { value: 'standard', text: 'Standard (New)' }, + { value: 'legacy', text: 'Drag Navigation' }, + { value: 'custom', text: 'Custom' } + ], + versionAdded: '1.25.0', + defaultsByInstallVersion: { + '1.25.0': 'legacy' + }, + onChange: async (newValue: string) => { + const settingStore = useSettingStore() + + if (newValue === 'standard') { + // Update related settings to match standard mode - select + panning + await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'select') + await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'panning') + } else if (newValue === 'legacy') { + // Update related settings to match legacy mode - panning + zoom + await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'panning') + await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'zoom') + } + } + }, + { + id: 'Comfy.Canvas.LeftMouseClickBehavior', + category: ['LiteGraph', 'Canvas Navigation', 'LeftMouseClickBehavior'], + name: 'Left Mouse Click Behavior', + defaultValue: 'panning', + type: 'radio', + sortOrder: 50, + options: [ + { value: 'panning', text: 'Panning' }, + { value: 'select', text: 'Select' } + ], + versionAdded: '1.27.4', + onChange: async (newValue: string) => { + const settingStore = useSettingStore() + + const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode') + + if (navigationMode !== 'custom') { + if ( + (newValue === 'select' && navigationMode === 'standard') || + (newValue === 'panning' && navigationMode === 'legacy') + ) { + return + } + + // only set to custom if it doesn't match the preset modes + await settingStore.set('Comfy.Canvas.NavigationMode', 'custom') + } + } + }, + { + id: 'Comfy.Canvas.MouseWheelScroll', + category: ['LiteGraph', 'Canvas Navigation', 'MouseWheelScroll'], + name: 'Mouse Wheel Scroll', + defaultValue: 'zoom', + type: 'radio', + options: [ + { value: 'panning', text: 'Panning' }, + { value: 'zoom', text: 'Zoom in/out' } + ], + versionAdded: '1.27.4', + onChange: async (newValue: string) => { + const settingStore = useSettingStore() + + const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode') + + if (navigationMode !== 'custom') { + if ( + (newValue === 'panning' && navigationMode === 'standard') || + (newValue === 'zoom' && navigationMode === 'legacy') + ) { + return + } + + // only set to custom if it doesn't match the preset modes + await settingStore.set('Comfy.Canvas.NavigationMode', 'custom') + } + } + }, { id: 'Comfy.Graph.CanvasInfo', category: ['LiteGraph', 'Canvas', 'CanvasInfo'], @@ -314,7 +404,8 @@ export const CORE_SETTINGS: SettingParams[] = [ { value: 'ko', text: '한국어' }, { value: 'fr', text: 'Français' }, { value: 'es', text: 'Español' }, - { value: 'ar', text: 'عربي' } + { value: 'ar', text: 'عربي' }, + { value: 'tr', text: 'Türkçe' } ], defaultValue: () => navigator.language.split('-')[0] || 'en' }, @@ -504,7 +595,7 @@ export const CORE_SETTINGS: SettingParams[] = [ migrateDeprecatedValue: (value: any[]) => { return value.map((keybinding) => { if (keybinding['targetSelector'] === '#graph-canvas') { - keybinding['targetElementId'] = 'graph-canvas' + keybinding['targetElementId'] = 'graph-canvas-container' } return keybinding }) @@ -813,21 +904,6 @@ export const CORE_SETTINGS: SettingParams[] = [ defaultValue: 8, versionAdded: '1.26.7' }, - { - id: 'Comfy.Canvas.NavigationMode', - category: ['LiteGraph', 'Canvas', 'CanvasNavigationMode'], - name: 'Canvas Navigation Mode', - defaultValue: 'legacy', - type: 'combo', - options: [ - { value: 'standard', text: 'Standard (New)' }, - { value: 'legacy', text: 'Drag Navigation' } - ], - versionAdded: '1.25.0', - defaultsByInstallVersion: { - '1.25.0': 'legacy' - } - }, { id: 'Comfy.Canvas.SelectionToolbox', category: ['LiteGraph', 'Canvas', 'SelectionToolbox'], diff --git a/src/platform/settings/settingStore.ts b/src/platform/settings/settingStore.ts index 2d94e38e5..5a1573efb 100644 --- a/src/platform/settings/settingStore.ts +++ b/src/platform/settings/settingStore.ts @@ -1,5 +1,6 @@ import _ from 'es-toolkit/compat' import { defineStore } from 'pinia' +import { compare, valid } from 'semver' import { ref } from 'vue' import type { SettingParams } from '@/platform/settings/types' @@ -7,7 +8,6 @@ import type { Settings } from '@/schemas/apiSchema' import { api } from '@/scripts/api' import { app } from '@/scripts/app' import type { TreeNode } from '@/types/treeExplorerTypes' -import { compareVersions, isSemVer } from '@/utils/formatUtil' export const getSettingInfo = (setting: SettingParams) => { const parts = setting.category || setting.id.split('.') @@ -132,20 +132,25 @@ export const useSettingStore = defineStore('setting', () => { if (installedVersion) { const sortedVersions = Object.keys(defaultsByInstallVersion).sort( - (a, b) => compareVersions(b, a) + (a, b) => compare(b, a) ) for (const version of sortedVersions) { // Ensure the version is in a valid format before comparing - if (!isSemVer(version)) { + if (!valid(version)) { continue } - if (compareVersions(installedVersion, version) >= 0) { - const versionedDefault = defaultsByInstallVersion[version] - return typeof versionedDefault === 'function' - ? versionedDefault() - : versionedDefault + if (compare(installedVersion, version) >= 0) { + const versionedDefault = + defaultsByInstallVersion[ + version as keyof typeof defaultsByInstallVersion + ] + if (versionedDefault !== undefined) { + return typeof versionedDefault === 'function' + ? versionedDefault() + : versionedDefault + } } } } diff --git a/src/platform/updates/common/releaseService.ts b/src/platform/updates/common/releaseService.ts index 7c55ae8f0..189421678 100644 --- a/src/platform/updates/common/releaseService.ts +++ b/src/platform/updates/common/releaseService.ts @@ -1,4 +1,5 @@ -import axios, { AxiosError, AxiosResponse } from 'axios' +import type { AxiosError, AxiosResponse } from 'axios' +import axios from 'axios' import { ref } from 'vue' import { COMFY_API_BASE_URL } from '@/config/comfyApi' diff --git a/src/platform/updates/common/releaseStore.ts b/src/platform/updates/common/releaseStore.ts index f34e525f5..470c92272 100644 --- a/src/platform/updates/common/releaseStore.ts +++ b/src/platform/updates/common/releaseStore.ts @@ -1,11 +1,12 @@ import { until } from '@vueuse/core' import { defineStore } from 'pinia' +import { compare } from 'semver' import { computed, ref } from 'vue' import { useSettingStore } from '@/platform/settings/settingStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' import { isElectron } from '@/utils/envUtil' -import { compareVersions, stringToLocale } from '@/utils/formatUtil' +import { stringToLocale } from '@/utils/formatUtil' import { type ReleaseNote, useReleaseService } from './releaseService' @@ -56,16 +57,19 @@ export const useReleaseStore = defineStore('release', () => { const isNewVersionAvailable = computed( () => !!recentRelease.value && - compareVersions( + compare( recentRelease.value.version, - currentComfyUIVersion.value + currentComfyUIVersion.value || '0.0.0' ) > 0 ) const isLatestVersion = computed( () => !!recentRelease.value && - !compareVersions(recentRelease.value.version, currentComfyUIVersion.value) + compare( + recentRelease.value.version, + currentComfyUIVersion.value || '0.0.0' + ) === 0 ) const hasMediumOrHighAttention = computed(() => diff --git a/src/platform/updates/common/versionCompatibilityStore.ts b/src/platform/updates/common/versionCompatibilityStore.ts index 46b25cf33..cc85f945b 100644 --- a/src/platform/updates/common/versionCompatibilityStore.ts +++ b/src/platform/updates/common/versionCompatibilityStore.ts @@ -1,6 +1,6 @@ import { until, useStorage } from '@vueuse/core' import { defineStore } from 'pinia' -import * as semver from 'semver' +import { gt, valid } from 'semver' import { computed } from 'vue' import config from '@/config' @@ -26,13 +26,13 @@ export const useVersionCompatibilityStore = defineStore( if ( !frontendVersion.value || !requiredFrontendVersion.value || - !semver.valid(frontendVersion.value) || - !semver.valid(requiredFrontendVersion.value) + !valid(frontendVersion.value) || + !valid(requiredFrontendVersion.value) ) { return false } // Returns true if required version is greater than frontend version - return semver.gt(requiredFrontendVersion.value, frontendVersion.value) + return gt(requiredFrontendVersion.value, frontendVersion.value) }) const isFrontendNewer = computed(() => { diff --git a/src/platform/workflow/core/services/workflowService.ts b/src/platform/workflow/core/services/workflowService.ts index 943177986..c4234cf45 100644 --- a/src/platform/workflow/core/services/workflowService.ts +++ b/src/platform/workflow/core/services/workflowService.ts @@ -2,14 +2,14 @@ import { toRaw } from 'vue' import { t } from '@/i18n' import { LGraph, LGraphCanvas } from '@/lib/litegraph/src/litegraph' -import type { SerialisableGraph, Vector2 } from '@/lib/litegraph/src/litegraph' +import type { Point, SerialisableGraph } from '@/lib/litegraph/src/litegraph' import { useSettingStore } from '@/platform/settings/settingStore' import { useToastStore } from '@/platform/updates/common/toastStore' import { ComfyWorkflow, useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' -import { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' +import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail' import { app } from '@/scripts/app' import { blankGraph, defaultGraph } from '@/scripts/defaultGraph' @@ -17,7 +17,7 @@ import { downloadBlob } from '@/scripts/utils' import { useDialogService } from '@/services/dialogService' import { useDomWidgetStore } from '@/stores/domWidgetStore' import { useWorkspaceStore } from '@/stores/workspaceStore' -import { appendJsonExt, generateUUID } from '@/utils/formatUtil' +import { appendJsonExt } from '@/utils/formatUtil' export const useWorkflowService = () => { const settingStore = useSettingStore() @@ -112,13 +112,6 @@ export const useWorkflowService = () => { await renameWorkflow(workflow, newPath) await workflowStore.saveWorkflow(workflow) } else { - // Generate new id when saving existing workflow as a new file - const id = generateUUID() - const state = JSON.parse( - JSON.stringify(workflow.activeState) - ) as ComfyWorkflowJSON - state.id = id - const tempWorkflow = workflowStore.saveAs(workflow, newPath) await openWorkflow(tempWorkflow) await workflowStore.saveWorkflow(tempWorkflow) @@ -346,7 +339,7 @@ export const useWorkflowService = () => { */ const insertWorkflow = async ( workflow: ComfyWorkflow, - options: { position?: Vector2 } = {} + options: { position?: Point } = {} ) => { const loadedWorkflow = await workflow.load() const workflowJSON = toRaw(loadedWorkflow.initialState) diff --git a/src/platform/workflow/management/stores/workflowStore.ts b/src/platform/workflow/management/stores/workflowStore.ts index 254698b66..02b48c55b 100644 --- a/src/platform/workflow/management/stores/workflowStore.ts +++ b/src/platform/workflow/management/stores/workflowStore.ts @@ -4,7 +4,7 @@ import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue' import { t } from '@/i18n' import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph' -import { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' +import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail' import { api } from '@/scripts/api' @@ -20,7 +20,7 @@ import { parseNodeExecutionId, parseNodeLocatorId } from '@/types/nodeIdentification' -import { getPathDetails } from '@/utils/formatUtil' +import { generateUUID, getPathDetails } from '@/utils/formatUtil' import { syncEntities } from '@/utils/syncUtil' import { isSubgraph } from '@/utils/typeGuardUtil' @@ -320,12 +320,19 @@ export const useWorkflowStore = defineStore('workflow', () => { existingWorkflow: ComfyWorkflow, path: string ): ComfyWorkflow => { + // Generate new id when saving existing workflow as a new file + const id = generateUUID() + const state = JSON.parse( + JSON.stringify(existingWorkflow.activeState) + ) as ComfyWorkflowJSON + state.id = id + const workflow: ComfyWorkflow = new (existingWorkflow.constructor as any)({ path, modified: Date.now(), size: -1 }) - workflow.originalContent = workflow.content = existingWorkflow.content + workflow.originalContent = workflow.content = JSON.stringify(state) workflowLookup.value[workflow.path] = workflow return workflow } diff --git a/src/renderer/core/canvas/canvasStore.ts b/src/renderer/core/canvas/canvasStore.ts index 6e09d95a3..ec38940fe 100644 --- a/src/renderer/core/canvas/canvasStore.ts +++ b/src/renderer/core/canvas/canvasStore.ts @@ -1,8 +1,10 @@ +import { useEventListener, whenever } from '@vueuse/core' import { defineStore } from 'pinia' import { type Raw, computed, markRaw, ref, shallowRef } from 'vue' import type { Point, Positionable } from '@/lib/litegraph/src/interfaces' import type { + LGraph, LGraphCanvas, LGraphGroup, LGraphNode @@ -94,9 +96,43 @@ export const useCanvasStore = defineStore('canvas', () => { appScalePercentage.value = Math.round(newScale * 100) } + const currentGraph = shallowRef(null) + const isInSubgraph = ref(false) + + // Provide selection state to all Vue nodes + const selectedNodeIds = computed( + () => + new Set( + selectedItems.value + .filter((item) => item.id !== undefined) + .map((item) => String(item.id)) + ) + ) + + whenever( + () => canvas.value, + (newCanvas) => { + useEventListener( + newCanvas.canvas, + 'litegraph:set-graph', + (event: CustomEvent<{ newGraph: LGraph; oldGraph: LGraph }>) => { + const newGraph = event.detail?.newGraph || app.canvas?.graph + currentGraph.value = newGraph + isInSubgraph.value = Boolean(app.canvas?.subgraph) + } + ) + + useEventListener(newCanvas.canvas, 'subgraph-opened', () => { + isInSubgraph.value = true + }) + }, + { immediate: true } + ) + return { canvas, selectedItems, + selectedNodeIds, nodeSelected, groupSelected, rerouteSelected, @@ -105,6 +141,8 @@ export const useCanvasStore = defineStore('canvas', () => { getCanvas, setAppZoomFromPercentage, initScaleSync, - cleanupScaleSync + cleanupScaleSync, + currentGraph, + isInSubgraph } }) diff --git a/src/renderer/core/canvas/injectionKeys.ts b/src/renderer/core/canvas/injectionKeys.ts deleted file mode 100644 index 5c850c100..000000000 --- a/src/renderer/core/canvas/injectionKeys.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { InjectionKey, Ref } from 'vue' - -import type { NodeProgressState } from '@/schemas/apiSchema' - -/** - * Injection key for providing selected node IDs to Vue node components. - * Contains a reactive Set of selected node IDs (as strings). - */ -export const SelectedNodeIdsKey: InjectionKey>> = - Symbol('selectedNodeIds') - -/** - * Injection key for providing executing node IDs to Vue node components. - * Contains a reactive Set of currently executing node IDs (as strings). - */ -export const ExecutingNodeIdsKey: InjectionKey>> = - Symbol('executingNodeIds') - -/** - * Injection key for providing node progress states to Vue node components. - * Contains a reactive Record of node IDs to their current progress state. - */ -export const NodeProgressStatesKey: InjectionKey< - Ref> -> = Symbol('nodeProgressStates') diff --git a/src/renderer/core/canvas/links/slotLinkCompatibility.ts b/src/renderer/core/canvas/links/slotLinkCompatibility.ts new file mode 100644 index 000000000..b8beffc38 --- /dev/null +++ b/src/renderer/core/canvas/links/slotLinkCompatibility.ts @@ -0,0 +1,73 @@ +import { getActivePinia } from 'pinia' + +import type { + INodeInputSlot, + INodeOutputSlot +} from '@/lib/litegraph/src/interfaces' +import type { LGraphNode, NodeId } from '@/lib/litegraph/src/litegraph' +import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import type { + SlotDragSource, + SlotDropCandidate +} from '@/renderer/core/canvas/links/slotLinkDragState' +import { app } from '@/scripts/app' + +interface CompatibilityResult { + allowable: boolean + targetNode?: LGraphNode + targetSlot?: INodeInputSlot | INodeOutputSlot +} + +function resolveNode(nodeId: NodeId) { + const pinia = getActivePinia() + const canvasStore = pinia ? useCanvasStore() : null + const graph = canvasStore?.canvas?.graph ?? app.canvas?.graph + if (!graph) return null + const id = typeof nodeId === 'string' ? Number(nodeId) : nodeId + if (Number.isNaN(id)) return null + return graph.getNodeById(id) +} + +export function evaluateCompatibility( + source: SlotDragSource, + candidate: SlotDropCandidate +): CompatibilityResult { + if (candidate.layout.nodeId === source.nodeId) { + return { allowable: false } + } + + const isOutputToInput = + source.type === 'output' && candidate.layout.type === 'input' + const isInputToOutput = + source.type === 'input' && candidate.layout.type === 'output' + + if (!isOutputToInput && !isInputToOutput) { + return { allowable: false } + } + + const sourceNode = resolveNode(source.nodeId) + const targetNode = resolveNode(candidate.layout.nodeId) + if (!sourceNode || !targetNode) { + return { allowable: false } + } + + if (isOutputToInput) { + const outputSlot = sourceNode.outputs?.[source.slotIndex] + const inputSlot = targetNode.inputs?.[candidate.layout.index] + if (!outputSlot || !inputSlot) { + return { allowable: false } + } + + const allowable = sourceNode.canConnectTo(targetNode, inputSlot, outputSlot) + return { allowable, targetNode, targetSlot: inputSlot } + } + + const inputSlot = sourceNode.inputs?.[source.slotIndex] + const outputSlot = targetNode.outputs?.[candidate.layout.index] + if (!inputSlot || !outputSlot) { + return { allowable: false } + } + + const allowable = targetNode.canConnectTo(sourceNode, inputSlot, outputSlot) + return { allowable, targetNode, targetSlot: outputSlot } +} diff --git a/src/renderer/core/canvas/links/slotLinkDragState.ts b/src/renderer/core/canvas/links/slotLinkDragState.ts new file mode 100644 index 000000000..5d2bbcfc4 --- /dev/null +++ b/src/renderer/core/canvas/links/slotLinkDragState.ts @@ -0,0 +1,95 @@ +import { reactive, readonly } from 'vue' + +import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { Point, SlotLayout } from '@/renderer/core/layout/types' + +type SlotDragType = 'input' | 'output' + +export interface SlotDragSource { + nodeId: string + slotIndex: number + type: SlotDragType + direction: LinkDirection + position: Readonly +} + +export interface SlotDropCandidate { + layout: SlotLayout + compatible: boolean +} + +interface PointerPosition { + client: Point + canvas: Point +} + +interface SlotDragState { + active: boolean + pointerId: number | null + source: SlotDragSource | null + pointer: PointerPosition + candidate: SlotDropCandidate | null +} + +const state = reactive({ + active: false, + pointerId: null, + source: null, + pointer: { + client: { x: 0, y: 0 }, + canvas: { x: 0, y: 0 } + }, + candidate: null +}) + +function updatePointerPosition( + clientX: number, + clientY: number, + canvasX: number, + canvasY: number +) { + state.pointer.client.x = clientX + state.pointer.client.y = clientY + state.pointer.canvas.x = canvasX + state.pointer.canvas.y = canvasY +} + +function setCandidate(candidate: SlotDropCandidate | null) { + state.candidate = candidate +} + +function beginDrag(source: SlotDragSource, pointerId: number) { + state.active = true + state.source = source + state.pointerId = pointerId + state.candidate = null +} + +function endDrag() { + state.active = false + state.pointerId = null + state.source = null + state.pointer.client.x = 0 + state.pointer.client.y = 0 + state.pointer.canvas.x = 0 + state.pointer.canvas.y = 0 + state.candidate = null +} + +function getSlotLayout(nodeId: string, slotIndex: number, isInput: boolean) { + const slotKey = getSlotKey(nodeId, slotIndex, isInput) + return layoutStore.getSlotLayout(slotKey) +} + +export function useSlotLinkDragState() { + return { + state: readonly(state), + beginDrag, + endDrag, + updatePointerPosition, + setCandidate, + getSlotLayout + } +} diff --git a/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts b/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts new file mode 100644 index 000000000..b69cd9b7a --- /dev/null +++ b/src/renderer/core/canvas/links/slotLinkPreviewRenderer.ts @@ -0,0 +1,95 @@ +import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import type { + INodeInputSlot, + INodeOutputSlot, + ReadOnlyPoint +} from '@/lib/litegraph/src/interfaces' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import { resolveConnectingLinkColor } from '@/lib/litegraph/src/utils/linkColors' +import { + type SlotDragSource, + useSlotLinkDragState +} from '@/renderer/core/canvas/links/slotLinkDragState' +import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' + +function buildContext(canvas: LGraphCanvas): LinkRenderContext { + return { + renderMode: canvas.links_render_mode, + connectionWidth: canvas.connections_width, + renderBorder: canvas.render_connections_border, + lowQuality: canvas.low_quality, + highQualityRender: canvas.highquality_render, + scale: canvas.ds.scale, + linkMarkerShape: canvas.linkMarkerShape, + renderConnectionArrows: canvas.render_connection_arrows, + highlightedLinks: new Set(Object.keys(canvas.highlighted_links)), + defaultLinkColor: canvas.default_link_color, + linkTypeColors: (canvas.constructor as typeof LGraphCanvas) + .link_type_colors, + disabledPattern: canvas._pattern + } +} + +export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) { + const originalOnDrawForeground = canvas.onDrawForeground?.bind(canvas) + const patched = ( + ctx: CanvasRenderingContext2D, + area: LGraphCanvas['visible_area'] + ) => { + originalOnDrawForeground?.(ctx, area) + + const { state } = useSlotLinkDragState() + if (!state.active || !state.source) return + + const { pointer, source } = state + const start = source.position + const sourceSlot = resolveSourceSlot(canvas, source) + + const linkRenderer = canvas.linkRenderer + if (!linkRenderer) return + + const context = buildContext(canvas) + + const from: ReadOnlyPoint = [start.x, start.y] + const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y] + + const startDir = source.direction ?? LinkDirection.RIGHT + const endDir = LinkDirection.CENTER + + const colour = resolveConnectingLinkColor(sourceSlot?.type) + + ctx.save() + + linkRenderer.renderDraggingLink( + ctx, + from, + to, + colour, + startDir, + endDir, + context + ) + + ctx.restore() + } + + canvas.onDrawForeground = patched +} + +function resolveSourceSlot( + canvas: LGraphCanvas, + source: SlotDragSource +): INodeInputSlot | INodeOutputSlot | undefined { + const graph = canvas.graph + if (!graph) return undefined + + const nodeId = Number(source.nodeId) + if (!Number.isFinite(nodeId)) return undefined + + const node = graph.getNodeById(nodeId) + if (!node) return undefined + + return source.type === 'output' + ? node.outputs?.[source.slotIndex] + : node.inputs?.[source.slotIndex] +} diff --git a/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts b/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts index 349ad903b..1bb3f7dae 100644 --- a/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts +++ b/src/renderer/core/canvas/litegraph/litegraphLinkAdapter.ts @@ -7,13 +7,10 @@ * Maintains backward compatibility with existing litegraph integration. */ import type { LGraph } from '@/lib/litegraph/src/LGraph' -import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' -import { LLink } from '@/lib/litegraph/src/LLink' +import type { LLink } from '@/lib/litegraph/src/LLink' import type { Reroute } from '@/lib/litegraph/src/Reroute' import type { CanvasColour, - INodeInputSlot, - INodeOutputSlot, ReadOnlyPoint } from '@/lib/litegraph/src/interfaces' import { LiteGraph } from '@/lib/litegraph/src/litegraph' @@ -27,7 +24,6 @@ import { type ArrowShape, CanvasPathRenderer, type Direction, - type DragLinkData, type LinkRenderData, type RenderContext as PathRenderContext, type Point, @@ -209,7 +205,6 @@ export class LitegraphLinkAdapter { case LinkDirection.DOWN: return 'down' case LinkDirection.CENTER: - case LinkDirection.NONE: return 'none' default: return 'right' @@ -502,57 +497,33 @@ export class LitegraphLinkAdapter { } } - /** - * Render a link being dragged from a slot to mouse position - * Used during link creation/reconnection - */ renderDraggingLink( ctx: CanvasRenderingContext2D, - fromNode: LGraphNode | null, - fromSlot: INodeOutputSlot | INodeInputSlot, - fromSlotIndex: number, - toPosition: ReadOnlyPoint, - context: LinkRenderContext, - options: { - fromInput?: boolean - color?: CanvasColour - disabled?: boolean - } = {} + from: ReadOnlyPoint, + to: ReadOnlyPoint, + colour: CanvasColour, + startDir: LinkDirection, + endDir: LinkDirection, + context: LinkRenderContext ): void { - if (!fromNode) return - - // Get slot position using layout tree if available - const slotPos = getSlotPosition( - fromNode, - fromSlotIndex, - options.fromInput || false + this.renderLinkDirect( + ctx, + from, + to, + null, + false, + null, + colour, + startDir, + endDir, + { + ...context, + linkMarkerShape: LinkMarkerShape.None + }, + { + disabled: false + } ) - if (!slotPos) return - - // Get slot direction - const slotDir = - fromSlot.dir || - (options.fromInput ? LinkDirection.LEFT : LinkDirection.RIGHT) - - // Create drag data - const dragData: DragLinkData = { - fixedPoint: { x: slotPos[0], y: slotPos[1] }, - fixedDirection: this.convertDirection(slotDir), - dragPoint: { x: toPosition[0], y: toPosition[1] }, - color: options.color ? String(options.color) : undefined, - type: fromSlot.type !== undefined ? String(fromSlot.type) : undefined, - disabled: options.disabled || false, - fromInput: options.fromInput || false - } - - // Convert context - const pathContext = this.convertToPathRenderContext(context) - - // Hide center marker when dragging links - pathContext.style.showCenterMarker = false - - // Render using pure renderer - this.pathRenderer.drawDraggingLink(ctx, dragData, pathContext) } /** diff --git a/src/renderer/core/canvas/pathRenderer.ts b/src/renderer/core/canvas/pathRenderer.ts index 29f406cad..126b98e00 100644 --- a/src/renderer/core/canvas/pathRenderer.ts +++ b/src/renderer/core/canvas/pathRenderer.ts @@ -70,7 +70,7 @@ export interface RenderContext { highlightedIds?: Set } -export interface DragLinkData { +interface DragLinkData { /** Fixed end - the slot being dragged from */ fixedPoint: Point fixedDirection: Direction diff --git a/src/composables/graph/useCanvasInteractions.ts b/src/renderer/core/canvas/useCanvasInteractions.ts similarity index 87% rename from src/composables/graph/useCanvasInteractions.ts rename to src/renderer/core/canvas/useCanvasInteractions.ts index be807b771..cddc50d08 100644 --- a/src/composables/graph/useCanvasInteractions.ts +++ b/src/renderer/core/canvas/useCanvasInteractions.ts @@ -11,12 +11,21 @@ import { app } from '@/scripts/app' */ export function useCanvasInteractions() { const settingStore = useSettingStore() - const { getCanvas } = useCanvasStore() + const canvasStore = useCanvasStore() + const { getCanvas } = canvasStore const isStandardNavMode = computed( () => settingStore.get('Comfy.Canvas.NavigationMode') === 'standard' ) + /** + * Whether Vue node components should handle pointer events. + * Returns false when canvas is in read-only/panning mode (e.g., space key held for panning). + */ + const shouldHandleNodePointerEvents = computed( + () => !(canvasStore.canvas?.read_only ?? false) + ) + /** * Handles wheel events from UI components that should be forwarded to canvas * when appropriate (e.g., Ctrl+wheel for zoom in standard mode) @@ -97,6 +106,7 @@ export function useCanvasInteractions() { return { handleWheel, handlePointer, - forwardEventToCanvas + forwardEventToCanvas, + shouldHandleNodePointerEvents } } diff --git a/src/renderer/core/layout/injectionKeys.ts b/src/renderer/core/layout/injectionKeys.ts new file mode 100644 index 000000000..8e0e0e1d6 --- /dev/null +++ b/src/renderer/core/layout/injectionKeys.ts @@ -0,0 +1,31 @@ +import type { InjectionKey } from 'vue' + +import type { useTransformState } from '@/renderer/core/layout/transform/useTransformState' + +/** + * Lightweight, injectable transform state used by layout-aware components. + * + * Consumers use this interface to convert coordinates between LiteGraph's + * canvas space and the DOM's screen space, access the current pan/zoom + * (camera), and perform basic viewport culling checks. + * + * Coordinate mapping: + * - screen = (canvas + offset) * scale + * - canvas = screen / scale - offset + * + * The full implementation and additional helpers live in + * `useTransformState()`. This interface deliberately exposes only the + * minimal surface needed outside that composable. + * + * @example + * const state = inject(TransformStateKey)! + * const screen = state.canvasToScreen({ x: 100, y: 50 }) + */ +interface TransformState + extends Pick< + ReturnType, + 'screenToCanvas' | 'canvasToScreen' | 'camera' | 'isNodeInViewport' + > {} + +export const TransformStateKey: InjectionKey = + Symbol('transformState') diff --git a/src/renderer/core/layout/operations/layoutMutations.ts b/src/renderer/core/layout/operations/layoutMutations.ts index 0c656899d..f8eb7cfad 100644 --- a/src/renderer/core/layout/operations/layoutMutations.ts +++ b/src/renderer/core/layout/operations/layoutMutations.ts @@ -8,13 +8,13 @@ import log from 'loglevel' import type { NodeId } from '@/lib/litegraph/src/LGraphNode' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' -import { - LayoutSource, - type LinkId, - type NodeLayout, - type Point, - type RerouteId, - type Size +import type { LayoutSource } from '@/renderer/core/layout/types' +import type { + LinkId, + NodeLayout, + Point, + RerouteId, + Size } from '@/renderer/core/layout/types' const logger = log.getLogger('LayoutMutations') diff --git a/src/renderer/core/layout/slots/useDomSlotRegistration.ts b/src/renderer/core/layout/slots/useDomSlotRegistration.ts deleted file mode 100644 index 94a1f09e5..000000000 --- a/src/renderer/core/layout/slots/useDomSlotRegistration.ts +++ /dev/null @@ -1,229 +0,0 @@ -/** - * DOM-based slot registration with performance optimization - * - * Measures the actual DOM position of a Vue slot connector and registers it - * into the LayoutStore so hit-testing and link rendering use the true position. - * - * Performance strategy: - * - Cache slot offset relative to node (avoids DOM reads during drag) - * - No measurements during pan/zoom (camera transforms don't change canvas coords) - * - Batch DOM reads via requestAnimationFrame - * - Only remeasure on structural changes (resize, collapse, LOD) - */ -import { - type Ref, - type WatchStopHandle, - nextTick, - onMounted, - onUnmounted, - ref, - watch -} from 'vue' - -import { LiteGraph } from '@/lib/litegraph/src/litegraph' -import { layoutStore } from '@/renderer/core/layout/store/layoutStore' -import type { Point as LayoutPoint } from '@/renderer/core/layout/types' - -import { getSlotKey } from './slotIdentifier' - -export type TransformState = { - screenToCanvas: (p: LayoutPoint) => LayoutPoint -} - -// Shared RAF queue for batching measurements -const measureQueue = new Set<() => void>() -let rafId: number | null = null -// Track mounted components to prevent execution on unmounted ones -const mountedComponents = new WeakSet() - -function scheduleMeasurement(fn: () => void) { - measureQueue.add(fn) - if (rafId === null) { - rafId = requestAnimationFrame(() => { - rafId = null - const batch = Array.from(measureQueue) - measureQueue.clear() - batch.forEach((measure) => measure()) - }) - } -} - -const cleanupFunctions = new WeakMap< - Ref, - { - stopWatcher?: WatchStopHandle - handleResize?: () => void - } ->() - -interface SlotRegistrationOptions { - nodeId: string - slotIndex: number - isInput: boolean - element: Ref - transform?: TransformState -} - -export function useDomSlotRegistration(options: SlotRegistrationOptions) { - const { nodeId, slotIndex, isInput, element: elRef, transform } = options - - // Early return if no nodeId - if (!nodeId || nodeId === '') { - return { - remeasure: () => {} - } - } - const slotKey = getSlotKey(nodeId, slotIndex, isInput) - // Track if this component is mounted - const componentToken = {} - - // Cached offset from node position (avoids DOM reads during drag) - const cachedOffset = ref(null) - const lastMeasuredBounds = ref(null) - - // Measure DOM and cache offset (expensive, minimize calls) - const measureAndCacheOffset = () => { - // Skip if component was unmounted - if (!mountedComponents.has(componentToken)) return - - const el = elRef.value - if (!el || !transform?.screenToCanvas) return - - const rect = el.getBoundingClientRect() - - // Skip if bounds haven't changed significantly (within 0.5px) - if (lastMeasuredBounds.value) { - const prev = lastMeasuredBounds.value - if ( - Math.abs(rect.left - prev.left) < 0.5 && - Math.abs(rect.top - prev.top) < 0.5 && - Math.abs(rect.width - prev.width) < 0.5 && - Math.abs(rect.height - prev.height) < 0.5 - ) { - return // No significant change - skip update - } - } - - lastMeasuredBounds.value = rect - - // Center of the visual connector (dot) in screen coords - const centerScreen = { - x: rect.left + rect.width / 2, - y: rect.top + rect.height / 2 - } - const centerCanvas = transform.screenToCanvas(centerScreen) - - // Cache offset from node position for fast updates during drag - const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value - if (nodeLayout) { - cachedOffset.value = { - x: centerCanvas.x - nodeLayout.position.x, - y: centerCanvas.y - nodeLayout.position.y - } - } - - updateSlotPosition(centerCanvas) - } - - // Fast update using cached offset (no DOM read) - const updateFromCachedOffset = () => { - if (!cachedOffset.value) { - // No cached offset yet, need to measure - scheduleMeasurement(measureAndCacheOffset) - return - } - - const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value - if (!nodeLayout) { - return - } - - // Calculate absolute position from node position + cached offset - const centerCanvas = { - x: nodeLayout.position.x + cachedOffset.value.x, - y: nodeLayout.position.y + cachedOffset.value.y - } - - updateSlotPosition(centerCanvas) - } - - // Update slot position in layout store - const updateSlotPosition = (centerCanvas: LayoutPoint) => { - const size = LiteGraph.NODE_SLOT_HEIGHT - const half = size / 2 - - layoutStore.updateSlotLayout(slotKey, { - nodeId, - index: slotIndex, - type: isInput ? 'input' : 'output', - position: { x: centerCanvas.x, y: centerCanvas.y }, - bounds: { - x: centerCanvas.x - half, - y: centerCanvas.y - half, - width: size, - height: size - } - }) - } - - onMounted(async () => { - // Mark component as mounted - mountedComponents.add(componentToken) - - // Initial measure after mount - await nextTick() - measureAndCacheOffset() - - // Subscribe to node position changes for fast cached updates - const nodeRef = layoutStore.getNodeLayoutRef(nodeId) - - const stopWatcher = watch( - nodeRef, - (newLayout) => { - if (newLayout) { - // Node moved/resized - update using cached offset - updateFromCachedOffset() - } - }, - { immediate: false } - ) - - // Store cleanup functions without type assertions - const cleanup = cleanupFunctions.get(elRef) || {} - cleanup.stopWatcher = stopWatcher - - // Window resize - remeasure as viewport changed - const handleResize = () => { - scheduleMeasurement(measureAndCacheOffset) - } - window.addEventListener('resize', handleResize, { passive: true }) - cleanup.handleResize = handleResize - cleanupFunctions.set(elRef, cleanup) - }) - - onUnmounted(() => { - // Mark component as unmounted - mountedComponents.delete(componentToken) - - // Clean up watchers and listeners - const cleanup = cleanupFunctions.get(elRef) - if (cleanup) { - if (cleanup.stopWatcher) cleanup.stopWatcher() - if (cleanup.handleResize) { - window.removeEventListener('resize', cleanup.handleResize) - } - cleanupFunctions.delete(elRef) - } - - // Remove from layout store - layoutStore.deleteSlotLayout(slotKey) - - // Remove from measurement queue if pending - measureQueue.delete(measureAndCacheOffset) - }) - - return { - // Expose for forced remeasure on structural changes - remeasure: () => scheduleMeasurement(measureAndCacheOffset) - } -} diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index 673344086..254b27a2c 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -5,7 +5,7 @@ * CRDT ensures conflict-free operations for both single and multi-user scenarios. */ import log from 'loglevel' -import { type ComputedRef, type Ref, computed, customRef } from 'vue' +import { type ComputedRef, type Ref, computed, customRef, ref } from 'vue' import * as Y from 'yjs' import { ACTOR_CONFIG } from '@/renderer/core/layout/constants' @@ -38,6 +38,10 @@ import { type RerouteLayout, type SlotLayout } from '@/renderer/core/layout/types' +import { + isBoundsEqual, + isPointEqual +} from '@/renderer/core/layout/utils/geometry' import { REROUTE_RADIUS, boundsIntersect, @@ -130,6 +134,9 @@ class LayoutStoreImpl implements LayoutStore { private slotSpatialIndex: SpatialIndexManager // For slots private rerouteSpatialIndex: SpatialIndexManager // For reroutes + // Vue dragging state for selection toolbox (public ref for direct mutation) + public isDraggingVueNodes = ref(false) + constructor() { // Initialize Yjs data structures this.ynodes = this.ydoc.getMap('nodes') @@ -392,12 +399,8 @@ class LayoutStoreImpl implements LayoutStore { // Short-circuit if bounds and centerPos unchanged if ( existing && - existing.bounds.x === layout.bounds.x && - existing.bounds.y === layout.bounds.y && - existing.bounds.width === layout.bounds.width && - existing.bounds.height === layout.bounds.height && - existing.centerPos.x === layout.centerPos.x && - existing.centerPos.y === layout.centerPos.y + isBoundsEqual(existing.bounds, layout.bounds) && + isPointEqual(existing.centerPos, layout.centerPos) ) { // Only update path if provided (for hit detection) if (layout.path) { @@ -436,6 +439,13 @@ class LayoutStoreImpl implements LayoutStore { const existing = this.slotLayouts.get(key) if (existing) { + // Short-circuit if geometry is unchanged + if ( + isPointEqual(existing.position, layout.position) && + isBoundsEqual(existing.bounds, layout.bounds) + ) { + return + } // Update spatial index this.slotSpatialIndex.update(key, layout.bounds) } else { @@ -446,6 +456,34 @@ class LayoutStoreImpl implements LayoutStore { this.slotLayouts.set(key, layout) } + /** + * Batch update slot layouts and spatial index in one pass + */ + batchUpdateSlotLayouts( + updates: Array<{ key: string; layout: SlotLayout }> + ): void { + if (!updates.length) return + + // Update spatial index and map entries (skip unchanged) + for (const { key, layout } of updates) { + const existing = this.slotLayouts.get(key) + + if (existing) { + // Short-circuit if geometry is unchanged + if ( + isPointEqual(existing.position, layout.position) && + isBoundsEqual(existing.bounds, layout.bounds) + ) { + continue + } + this.slotSpatialIndex.update(key, layout.bounds) + } else { + this.slotSpatialIndex.insert(key, layout.bounds) + } + this.slotLayouts.set(key, layout) + } + } + /** * Delete slot layout data */ @@ -554,12 +592,8 @@ class LayoutStoreImpl implements LayoutStore { // Short-circuit if bounds and centerPos unchanged (prevents spatial index churn) if ( existing && - existing.bounds.x === layout.bounds.x && - existing.bounds.y === layout.bounds.y && - existing.bounds.width === layout.bounds.width && - existing.bounds.height === layout.bounds.height && - existing.centerPos.x === layout.centerPos.x && - existing.centerPos.y === layout.centerPos.y + isBoundsEqual(existing.bounds, layout.bounds) && + isPointEqual(existing.centerPos, layout.centerPos) ) { // Only update path if provided (for hit detection) if (layout.path) { @@ -968,9 +1002,6 @@ class LayoutStoreImpl implements LayoutStore { // Hit detection queries can run before CRDT updates complete this.spatialIndex.update(operation.nodeId, newBounds) - // Update associated slot positions synchronously - this.updateNodeSlotPositions(operation.nodeId, operation.position) - // Then update CRDT ynode.set('position', operation.position) this.updateNodeBounds(ynode, operation.position, size) @@ -997,9 +1028,6 @@ class LayoutStoreImpl implements LayoutStore { // Hit detection queries can run before CRDT updates complete this.spatialIndex.update(operation.nodeId, newBounds) - // Update associated slot positions synchronously (size changes may affect slot positions) - this.updateNodeSlotPositions(operation.nodeId, position) - // Then update CRDT ynode.set('size', operation.size) this.updateNodeBounds(ynode, position, operation.size) @@ -1280,29 +1308,6 @@ class LayoutStoreImpl implements LayoutStore { } } - /** - * Update slot positions when a node moves - * TODO: This should be handled by the layout sync system (useSlotLayoutSync) - * rather than manually here. For now, we'll mark affected slots as needing recalculation. - */ - private updateNodeSlotPositions(nodeId: NodeId, _nodePosition: Point): void { - // Mark all slots for this node as potentially stale - // The layout sync system will recalculate positions on the next frame - const slotsToRemove: string[] = [] - - for (const [key, slotLayout] of this.slotLayouts) { - if (slotLayout.nodeId === nodeId) { - slotsToRemove.push(key) - } - } - - // Remove from spatial index so they'll be recalculated - for (const key of slotsToRemove) { - this.slotSpatialIndex.remove(key) - this.slotLayouts.delete(key) - } - } - // Helper methods private notifyChange(change: LayoutChange): void { diff --git a/src/renderer/core/layout/sync/useSlotLayoutSync.ts b/src/renderer/core/layout/sync/useSlotLayoutSync.ts index 55a464140..281199e8b 100644 --- a/src/renderer/core/layout/sync/useSlotLayoutSync.ts +++ b/src/renderer/core/layout/sync/useSlotLayoutSync.ts @@ -7,8 +7,9 @@ import { onUnmounted } from 'vue' import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' -import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph' -import { type SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations' +import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations' import { registerNodeSlots } from '@/renderer/core/layout/slots/register' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' @@ -133,7 +134,11 @@ export function useSlotLayoutSync() { restoreHandlers = () => { graph.onNodeAdded = origNodeAdded || undefined graph.onNodeRemoved = origNodeRemoved || undefined - graph.onTrigger = origTrigger || undefined + // Only restore onTrigger if Vue nodes are not active + // Vue node manager sets its own onTrigger handler + if (!LiteGraph.vueNodesMode) { + graph.onTrigger = origTrigger || undefined + } graph.onAfterChange = origAfterChange || undefined } diff --git a/src/renderer/core/layout/transform/TransformPane.vue b/src/renderer/core/layout/transform/TransformPane.vue index 0f88b177d..29abc1262 100644 --- a/src/renderer/core/layout/transform/TransformPane.vue +++ b/src/renderer/core/layout/transform/TransformPane.vue @@ -13,7 +13,8 @@ diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index dd8c7cd5b..f764a82bf 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -4,13 +4,16 @@
diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.spec.ts b/src/renderer/extensions/vueNodes/components/NodeHeader.spec.ts index 240a51071..775dd6ba6 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.spec.ts +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.spec.ts @@ -1,12 +1,16 @@ import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createPinia, setActivePinia } from 'pinia' import PrimeVue from 'primevue/config' import InputText from 'primevue/inputtext' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import enMessages from '@/locales/en/main.json' +import { useSettingStore } from '@/platform/settings/settingStore' +import type { Settings } from '@/schemas/apiSchema' +import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' +import { ComfyNodeDefImpl, useNodeDefStore } from '@/stores/nodeDefStore' import NodeHeader from './NodeHeader.vue' @@ -24,19 +28,94 @@ const makeNodeData = (overrides: Partial = {}): VueNodeData => ({ ...overrides }) -const mountHeader = ( - props?: Partial['$props']> -) => { +const setupMockStores = () => { + const pinia = createPinia() + setActivePinia(pinia) + + const settingStore = useSettingStore() + const nodeDefStore = useNodeDefStore() + + // Mock tooltip delay setting + vi.spyOn(settingStore, 'get').mockImplementation( + (key: K): Settings[K] => { + switch (key) { + case 'Comfy.EnableTooltips': + return true as Settings[K] + case 'LiteGraph.Node.TooltipDelay': + return 500 as Settings[K] + default: + return undefined as Settings[K] + } + } + ) + + // Mock node definition store + const baseMockNodeDef: ComfyNodeDef = { + name: 'KSampler', + display_name: 'KSampler', + category: 'sampling', + python_module: 'test_module', + description: 'Advanced sampling node for diffusion models', + input: { + required: { + model: ['MODEL', {}], + positive: ['CONDITIONING', {}], + negative: ['CONDITIONING', {}] + }, + optional: {}, + hidden: {} + }, + output: ['LATENT'], + output_is_list: [false], + output_name: ['samples'], + output_node: false, + deprecated: false, + experimental: false + } + + const mockNodeDef = new ComfyNodeDefImpl(baseMockNodeDef) + + vi.spyOn(nodeDefStore, 'nodeDefsByName', 'get').mockReturnValue({ + KSampler: mockNodeDef + }) + + return { settingStore, nodeDefStore, pinia } +} + +const createMountConfig = () => { const i18n = createI18n({ legacy: false, locale: 'en', messages: { en: enMessages } }) - return mount(NodeHeader, { + + const { pinia } = setupMockStores() + + return { global: { - plugins: [PrimeVue, i18n, createPinia()], - components: { InputText } - }, + plugins: [PrimeVue, i18n, pinia], + components: { InputText }, + directives: { + tooltip: { + mounted: vi.fn(), + updated: vi.fn(), + unmounted: vi.fn() + } + }, + provide: { + tooltipContainer: { value: document.createElement('div') } + } + } + } +} + +const mountHeader = ( + props?: Partial['$props']> +) => { + const config = createMountConfig() + + return mount(NodeHeader, { + ...config, props: { nodeData: makeNodeData(), readonly: false, @@ -126,4 +205,68 @@ describe('NodeHeader.vue', () => { const collapsedIcon = wrapper.get('i') expect(collapsedIcon.classes()).toContain('pi-chevron-right') }) + + describe('Tooltips', () => { + it('applies tooltip directive to node title with correct configuration', () => { + const wrapper = mountHeader({ + nodeData: makeNodeData({ type: 'KSampler' }) + }) + + const titleElement = wrapper.find('[data-testid="node-title"]') + expect(titleElement.exists()).toBe(true) + + // Check that v-tooltip directive was applied + const directive = wrapper.vm.$el.querySelector( + '[data-testid="node-title"]' + ) + expect(directive).toBeTruthy() + }) + + it('disables tooltip when in readonly mode', () => { + const wrapper = mountHeader({ + readonly: true, + nodeData: makeNodeData({ type: 'KSampler' }) + }) + + const titleElement = wrapper.find('[data-testid="node-title"]') + expect(titleElement.exists()).toBe(true) + }) + + it('disables tooltip when editing is active', async () => { + const wrapper = mountHeader({ + nodeData: makeNodeData({ type: 'KSampler' }) + }) + + // Enter edit mode + await wrapper.get('[data-testid="node-header-1"]').trigger('dblclick') + + // Tooltip should be disabled during editing + const titleElement = wrapper.find('[data-testid="node-title"]') + expect(titleElement.exists()).toBe(true) + }) + + it('creates tooltip configuration when component mounts', () => { + const wrapper = mountHeader({ + nodeData: makeNodeData({ type: 'KSampler' }) + }) + + // Verify tooltip directive is applied to the title element + const titleElement = wrapper.find('[data-testid="node-title"]') + expect(titleElement.exists()).toBe(true) + + // The tooltip composable should be initialized + expect(wrapper.vm).toBeDefined() + }) + + it('uses tooltip container from provide/inject', () => { + const wrapper = mountHeader({ + nodeData: makeNodeData({ type: 'KSampler' }) + }) + + expect(wrapper.exists()).toBe(true) + // Container should be provided through inject + const titleElement = wrapper.find('[data-testid="node-title"]') + expect(titleElement.exists()).toBe(true) + }) + }) }) diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue index 286c7ee4b..40b8a7fe0 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue @@ -4,8 +4,8 @@
@@ -23,7 +23,11 @@ -
+
+ + +
+ + + +
diff --git a/src/renderer/extensions/vueNodes/components/NodeSlots.vue b/src/renderer/extensions/vueNodes/components/NodeSlots.vue index 68f247932..26187899d 100644 --- a/src/renderer/extensions/vueNodes/components/NodeSlots.vue +++ b/src/renderer/extensions/vueNodes/components/NodeSlots.vue @@ -8,7 +8,8 @@ v-for="(input, index) in filteredInputs" :key="`input-${index}`" :slot-data="input" - :node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''" + :node-type="nodeData?.type || ''" + :node-id="nodeData?.id != null ? String(nodeData.id) : ''" :index="getActualInputIndex(input, index)" :readonly="readonly" /> @@ -19,7 +20,8 @@ v-for="(output, index) in filteredOutputs" :key="`output-${index}`" :slot-data="output" - :node-id="nodeInfo?.id != null ? String(nodeInfo.id) : ''" + :node-type="nodeData?.type || ''" + :node-id="nodeData?.id != null ? String(nodeData.id) : ''" :index="index" :readonly="readonly" /> @@ -32,29 +34,24 @@ import { computed, onErrorCaptured, ref } from 'vue' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' import { useErrorHandling } from '@/composables/useErrorHandling' -import type { INodeSlot, LGraphNode } from '@/lib/litegraph/src/litegraph' -import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD' +import type { INodeSlot } from '@/lib/litegraph/src/litegraph' import { isSlotObject } from '@/utils/typeGuardUtil' import InputSlot from './InputSlot.vue' import OutputSlot from './OutputSlot.vue' interface NodeSlotsProps { - node?: LGraphNode // For backwards compatibility - nodeData?: VueNodeData // New clean data structure + nodeData?: VueNodeData readonly?: boolean - lodLevel?: LODLevel } -const props = defineProps() - -const nodeInfo = computed(() => props.nodeData || props.node || null) +const { nodeData = null, readonly } = defineProps() // Filter out input slots that have corresponding widgets const filteredInputs = computed(() => { - if (!nodeInfo.value?.inputs) return [] + if (!nodeData?.inputs) return [] - return nodeInfo.value.inputs + return nodeData.inputs .filter((input) => { // Check if this slot has a widget property (indicating it has a corresponding widget) if (isSlotObject(input) && 'widget' in input && input.widget) { @@ -76,7 +73,7 @@ const filteredInputs = computed(() => { // Outputs don't have widgets, so we don't need to filter them const filteredOutputs = computed(() => { - const outputs = nodeInfo.value?.outputs || [] + const outputs = nodeData?.outputs || [] return outputs.map((output) => isSlotObject(output) ? output @@ -94,10 +91,10 @@ const getActualInputIndex = ( input: INodeSlot, filteredIndex: number ): number => { - if (!nodeInfo.value?.inputs) return filteredIndex + if (!nodeData?.inputs) return filteredIndex // Find the actual index in the unfiltered inputs array - const actualIndex = nodeInfo.value.inputs.findIndex((i) => i === input) + const actualIndex = nodeData.inputs.findIndex((i) => i === input) return actualIndex !== -1 ? actualIndex : filteredIndex } diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index 0cd7a59cc..4645429da 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -2,7 +2,20 @@
{{ $t('Node Widgets Error') }}
-
+
diff --git a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts index 844649676..e3ab1c66c 100644 --- a/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts +++ b/src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts @@ -8,19 +8,19 @@ * - Layout mutations for visual feedback * - Integration with LiteGraph canvas selection system */ -import type { Ref } from 'vue' +import { createSharedComposable } from '@vueuse/core' import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' +import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex' -interface NodeManager { - getNode: (id: string) => any -} - -export function useNodeEventHandlers(nodeManager: Ref) { +function useNodeEventHandlersIndividual() { const canvasStore = useCanvasStore() + const { nodeManager } = useVueNodeLifecycle() const { bringNodeToFront } = useNodeZIndex() + const { shouldHandleNodePointerEvents } = useCanvasInteractions() /** * Handle node selection events @@ -31,12 +31,14 @@ export function useNodeEventHandlers(nodeManager: Ref) { nodeData: VueNodeData, wasDragging: boolean ) => { + if (!shouldHandleNodePointerEvents.value) return + if (!canvasStore.canvas || !nodeManager.value) return const node = nodeManager.value.getNode(nodeData.id) if (!node) return - const isMultiSelect = event.ctrlKey || event.metaKey + const isMultiSelect = event.ctrlKey || event.metaKey || event.shiftKey if (isMultiSelect) { // Ctrl/Cmd+click -> toggle selection @@ -69,6 +71,8 @@ export function useNodeEventHandlers(nodeManager: Ref) { * Uses LiteGraph's native collapse method for proper state management */ const handleNodeCollapse = (nodeId: string, collapsed: boolean) => { + if (!shouldHandleNodePointerEvents.value) return + if (!nodeManager.value) return const node = nodeManager.value.getNode(nodeId) @@ -78,6 +82,7 @@ export function useNodeEventHandlers(nodeManager: Ref) { const currentCollapsed = node.flags?.collapsed ?? false if (currentCollapsed !== collapsed) { node.collapse() + nodeManager.value.scheduleUpdate(nodeId, 'critical') } } @@ -86,6 +91,8 @@ export function useNodeEventHandlers(nodeManager: Ref) { * Updates the title in LiteGraph for persistence across sessions */ const handleNodeTitleUpdate = (nodeId: string, newTitle: string) => { + if (!shouldHandleNodePointerEvents.value) return + if (!nodeManager.value) return const node = nodeManager.value.getNode(nodeId) @@ -103,6 +110,8 @@ export function useNodeEventHandlers(nodeManager: Ref) { event: PointerEvent, nodeData: VueNodeData ) => { + if (!shouldHandleNodePointerEvents.value) return + if (!canvasStore.canvas || !nodeManager.value) return const node = nodeManager.value.getNode(nodeData.id) @@ -123,6 +132,8 @@ export function useNodeEventHandlers(nodeManager: Ref) { * Integrates with LiteGraph's context menu system */ const handleNodeRightClick = (event: PointerEvent, nodeData: VueNodeData) => { + if (!shouldHandleNodePointerEvents.value) return + if (!canvasStore.canvas || !nodeManager.value) return const node = nodeManager.value.getNode(nodeData.id) @@ -145,6 +156,8 @@ export function useNodeEventHandlers(nodeManager: Ref) { * Prepares node for dragging and sets appropriate visual state */ const handleNodeDragStart = (event: DragEvent, nodeData: VueNodeData) => { + if (!shouldHandleNodePointerEvents.value) return + if (!canvasStore.canvas || !nodeManager.value) return const node = nodeManager.value.getNode(nodeData.id) @@ -173,6 +186,8 @@ export function useNodeEventHandlers(nodeManager: Ref) { * Useful for selection toolbox or area selection */ const selectNodes = (nodeIds: string[], addToSelection = false) => { + if (!shouldHandleNodePointerEvents.value) return + if (!canvasStore.canvas || !nodeManager.value) return if (!addToSelection) { @@ -193,6 +208,8 @@ export function useNodeEventHandlers(nodeManager: Ref) { * Deselect specific nodes */ const deselectNodes = (nodeIds: string[]) => { + if (!shouldHandleNodePointerEvents.value) return + if (!canvasStore.canvas || !nodeManager.value) return nodeIds.forEach((nodeId) => { @@ -219,3 +236,7 @@ export function useNodeEventHandlers(nodeManager: Ref) { deselectNodes } } + +export const useNodeEventHandlers = createSharedComposable( + useNodeEventHandlersIndividual +) diff --git a/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts new file mode 100644 index 000000000..f5ba08374 --- /dev/null +++ b/src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts @@ -0,0 +1,93 @@ +import { type MaybeRefOrGetter, computed, ref, toValue } from 'vue' + +import type { VueNodeData } from '@/composables/graph/useGraphNodeManager' +import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout' + +// Treat tiny pointer jitter as a click, not a drag +const DRAG_THRESHOLD_PX = 4 + +export function useNodePointerInteractions( + nodeDataMaybe: MaybeRefOrGetter, + onPointerUp: ( + event: PointerEvent, + nodeData: VueNodeData, + wasDragging: boolean + ) => void +) { + const nodeData = toValue(nodeDataMaybe) + + const { startDrag, endDrag, handleDrag } = useNodeLayout(nodeData.id) + // Use canvas interactions for proper wheel event handling and pointer event capture control + const { forwardEventToCanvas, shouldHandleNodePointerEvents } = + useCanvasInteractions() + + // Drag state for styling + const isDragging = ref(false) + const dragStyle = computed(() => ({ + cursor: isDragging.value ? 'grabbing' : 'grab' + })) + const lastX = ref(0) + const lastY = ref(0) + + const handlePointerDown = (event: PointerEvent) => { + if (!nodeData) { + console.warn( + 'LGraphNode: nodeData is null/undefined in handlePointerDown' + ) + return + } + + // Don't handle pointer events when canvas is in panning mode - forward to canvas instead + if (!shouldHandleNodePointerEvents.value) { + forwardEventToCanvas(event) + return + } + + // Start drag using layout system + isDragging.value = true + + // Set Vue node dragging state for selection toolbox + layoutStore.isDraggingVueNodes.value = true + + startDrag(event) + lastY.value = event.clientY + lastX.value = event.clientX + } + + const handlePointerMove = (event: PointerEvent) => { + if (isDragging.value) { + void handleDrag(event) + } + } + + const handlePointerUp = (event: PointerEvent) => { + if (isDragging.value) { + isDragging.value = false + void endDrag(event) + + // Clear Vue node dragging state for selection toolbox + layoutStore.isDraggingVueNodes.value = false + } + + // Don't emit node-click when canvas is in panning mode - forward to canvas instead + if (!shouldHandleNodePointerEvents.value) { + forwardEventToCanvas(event) + return + } + + // Emit node-click for selection handling in GraphCanvas + const dx = event.clientX - lastX.value + const dy = event.clientY - lastY.value + const wasDragging = Math.hypot(dx, dy) > DRAG_THRESHOLD_PX + onPointerUp(event, nodeData, wasDragging) + } + return { + isDragging, + dragStyle, + handlePointerMove, + handlePointerDown, + handlePointerUp + } +} diff --git a/src/renderer/extensions/vueNodes/composables/useNodeTooltips.ts b/src/renderer/extensions/vueNodes/composables/useNodeTooltips.ts new file mode 100644 index 000000000..034047471 --- /dev/null +++ b/src/renderer/extensions/vueNodes/composables/useNodeTooltips.ts @@ -0,0 +1,120 @@ +import { type MaybeRef, type Ref, computed, unref } from 'vue' + +import type { SafeWidgetData } from '@/composables/graph/useGraphNodeManager' +import { st } from '@/i18n' +import { useSettingStore } from '@/platform/settings/settingStore' +import { useNodeDefStore } from '@/stores/nodeDefStore' +import { normalizeI18nKey } from '@/utils/formatUtil' + +/** + * Composable for managing Vue node tooltips + * Provides tooltip text for node headers, slots, and widgets + */ +export function useNodeTooltips( + nodeType: MaybeRef, + containerRef?: Ref +) { + const nodeDefStore = useNodeDefStore() + const settingsStore = useSettingStore() + + // Check if tooltips are globally enabled + const tooltipsEnabled = computed(() => + settingsStore.get('Comfy.EnableTooltips') + ) + + // Get node definition for tooltip data + const nodeDef = computed(() => nodeDefStore.nodeDefsByName[unref(nodeType)]) + + /** + * Get tooltip text for node description (header hover) + */ + const getNodeDescription = computed(() => { + if (!tooltipsEnabled.value || !nodeDef.value) return '' + + const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.description` + return st(key, nodeDef.value.description || '') + }) + + /** + * Get tooltip text for input slots + */ + const getInputSlotTooltip = (slotName: string) => { + if (!tooltipsEnabled.value || !nodeDef.value) return '' + + const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.inputs.${normalizeI18nKey(slotName)}.tooltip` + const inputTooltip = nodeDef.value.inputs?.[slotName]?.tooltip ?? '' + return st(key, inputTooltip) + } + + /** + * Get tooltip text for output slots + */ + const getOutputSlotTooltip = (slotIndex: number) => { + if (!tooltipsEnabled.value || !nodeDef.value) return '' + + const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.outputs.${slotIndex}.tooltip` + const outputTooltip = nodeDef.value.outputs?.[slotIndex]?.tooltip ?? '' + return st(key, outputTooltip) + } + + /** + * Get tooltip text for widgets + */ + const getWidgetTooltip = (widget: SafeWidgetData) => { + if (!tooltipsEnabled.value || !nodeDef.value) return '' + + // First try widget-specific tooltip + const widgetTooltip = (widget as { tooltip?: string }).tooltip + if (widgetTooltip) return widgetTooltip + + // Then try input-based tooltip lookup + const key = `nodeDefs.${normalizeI18nKey(unref(nodeType))}.inputs.${normalizeI18nKey(widget.name)}.tooltip` + const inputTooltip = nodeDef.value.inputs?.[widget.name]?.tooltip ?? '' + return st(key, inputTooltip) + } + + /** + * Create tooltip configuration object for v-tooltip directive + */ + const createTooltipConfig = (text: string) => { + const tooltipDelay = settingsStore.get('LiteGraph.Node.TooltipDelay') + const tooltipText = text || '' + + const config: { + value: string + showDelay: number + disabled: boolean + appendTo?: HTMLElement + pt?: any + } = { + value: tooltipText, + showDelay: tooltipDelay as number, + disabled: !tooltipsEnabled.value || !tooltipText, + pt: { + text: { + class: + 'bg-charcoal-800 border border-slate-300 rounded-md px-4 py-2 text-white text-sm font-normal leading-tight max-w-75 shadow-none' + }, + arrow: { + class: 'before:border-slate-300' + } + } + } + + // If we have a container reference, append tooltips to it + if (containerRef?.value) { + config.appendTo = containerRef.value + } + + return config + } + + return { + tooltipsEnabled, + getNodeDescription, + getInputSlotTooltip, + getOutputSlotTooltip, + getWidgetTooltip, + createTooltipConfig + } +} diff --git a/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts new file mode 100644 index 000000000..4b6cbf811 --- /dev/null +++ b/src/renderer/extensions/vueNodes/composables/useSlotElementTracking.ts @@ -0,0 +1,224 @@ +/** + * Centralized Slot Element Tracking + * + * Registers slot connector DOM elements per node, measures their canvas-space + * positions in a single batched pass, and caches offsets so that node moves + * update slot positions without DOM reads. + */ +import { type Ref, onMounted, onUnmounted, watch } from 'vue' + +import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' +import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { SlotLayout } from '@/renderer/core/layout/types' +import { + isPointEqual, + isSizeEqual +} from '@/renderer/core/layout/utils/geometry' +import { useNodeSlotRegistryStore } from '@/renderer/extensions/vueNodes/stores/nodeSlotRegistryStore' + +// RAF batching +const pendingNodes = new Set() +let rafId: number | null = null + +function scheduleSlotLayoutSync(nodeId: string) { + pendingNodes.add(nodeId) + if (rafId == null) { + rafId = requestAnimationFrame(() => { + rafId = null + flushScheduledSlotLayoutSync() + }) + } +} + +function flushScheduledSlotLayoutSync() { + if (pendingNodes.size === 0) return + const conv = useSharedCanvasPositionConversion() + for (const nodeId of Array.from(pendingNodes)) { + pendingNodes.delete(nodeId) + syncNodeSlotLayoutsFromDOM(nodeId, conv) + } +} + +export function syncNodeSlotLayoutsFromDOM( + nodeId: string, + conv?: ReturnType +) { + const nodeSlotRegistryStore = useNodeSlotRegistryStore() + const node = nodeSlotRegistryStore.getNode(nodeId) + if (!node) return + const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value + if (!nodeLayout) return + + const batch: Array<{ key: string; layout: SlotLayout }> = [] + + for (const [slotKey, entry] of node.slots) { + const rect = entry.el.getBoundingClientRect() + const screenCenter: [number, number] = [ + rect.left + rect.width / 2, + rect.top + rect.height / 2 + ] + const [x, y] = ( + conv ?? useSharedCanvasPositionConversion() + ).clientPosToCanvasPos(screenCenter) + const centerCanvas = { x, y } + + // Cache offset relative to node position for fast updates later + entry.cachedOffset = { + x: centerCanvas.x - nodeLayout.position.x, + y: centerCanvas.y - nodeLayout.position.y + } + + // Persist layout in canvas coordinates + const size = LiteGraph.NODE_SLOT_HEIGHT + const half = size / 2 + batch.push({ + key: slotKey, + layout: { + nodeId, + index: entry.index, + type: entry.type, + position: { x: centerCanvas.x, y: centerCanvas.y }, + bounds: { + x: centerCanvas.x - half, + y: centerCanvas.y - half, + width: size, + height: size + } + } + }) + } + if (batch.length) layoutStore.batchUpdateSlotLayouts(batch) +} + +function updateNodeSlotsFromCache(nodeId: string) { + const nodeSlotRegistryStore = useNodeSlotRegistryStore() + const node = nodeSlotRegistryStore.getNode(nodeId) + if (!node) return + const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value + if (!nodeLayout) return + + const batch: Array<{ key: string; layout: SlotLayout }> = [] + + for (const [slotKey, entry] of node.slots) { + if (!entry.cachedOffset) { + // schedule a sync to seed offset + scheduleSlotLayoutSync(nodeId) + continue + } + + const centerCanvas = { + x: nodeLayout.position.x + entry.cachedOffset.x, + y: nodeLayout.position.y + entry.cachedOffset.y + } + const size = LiteGraph.NODE_SLOT_HEIGHT + const half = size / 2 + batch.push({ + key: slotKey, + layout: { + nodeId, + index: entry.index, + type: entry.type, + position: { x: centerCanvas.x, y: centerCanvas.y }, + bounds: { + x: centerCanvas.x - half, + y: centerCanvas.y - half, + width: size, + height: size + } + } + }) + } + + if (batch.length) layoutStore.batchUpdateSlotLayouts(batch) +} + +export function useSlotElementTracking(options: { + nodeId: string + index: number + type: 'input' | 'output' + element: Ref +}) { + const { nodeId, index, type, element } = options + const nodeSlotRegistryStore = useNodeSlotRegistryStore() + + onMounted(() => { + if (!nodeId) return + const stop = watch( + element, + (el) => { + if (!el) return + + const node = nodeSlotRegistryStore.ensureNode(nodeId) + + if (!node.stopWatch) { + const layoutRef = layoutStore.getNodeLayoutRef(nodeId) + + const stopPositionWatch = watch( + () => layoutRef.value?.position, + (newPosition, oldPosition) => { + if (!newPosition) return + if (!oldPosition || !isPointEqual(newPosition, oldPosition)) { + updateNodeSlotsFromCache(nodeId) + } + } + ) + + const stopSizeWatch = watch( + () => layoutRef.value?.size, + (newSize, oldSize) => { + if (!newSize) return + if (!oldSize || !isSizeEqual(newSize, oldSize)) { + scheduleSlotLayoutSync(nodeId) + } + } + ) + + node.stopWatch = () => { + stopPositionWatch() + stopSizeWatch() + } + } + + // Register slot + const slotKey = getSlotKey(nodeId, index, type === 'input') + + el.dataset.slotKey = slotKey + node.slots.set(slotKey, { el, index, type }) + + // Seed initial sync from DOM + scheduleSlotLayoutSync(nodeId) + + // Stop watching once registered + stop() + }, + { immediate: true, flush: 'post' } + ) + }) + + onUnmounted(() => { + if (!nodeId) return + const node = nodeSlotRegistryStore.getNode(nodeId) + if (!node) return + + // Remove this slot from registry and layout + const slotKey = getSlotKey(nodeId, index, type === 'input') + const entry = node.slots.get(slotKey) + if (entry) { + delete entry.el.dataset.slotKey + node.slots.delete(slotKey) + } + layoutStore.deleteSlotLayout(slotKey) + + // If node has no more slots, clean up + if (node.slots.size === 0) { + if (node.stopWatch) node.stopWatch() + nodeSlotRegistryStore.deleteNode(nodeId) + } + }) + + return { + requestSlotLayoutSync: () => scheduleSlotLayoutSync(nodeId) + } +} diff --git a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts new file mode 100644 index 000000000..f82deab7d --- /dev/null +++ b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts @@ -0,0 +1,247 @@ +import { type Fn, useEventListener } from '@vueuse/core' +import { onBeforeUnmount } from 'vue' + +import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion' +import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' +import { evaluateCompatibility } from '@/renderer/core/canvas/links/slotLinkCompatibility' +import { + type SlotDropCandidate, + useSlotLinkDragState +} from '@/renderer/core/canvas/links/slotLinkDragState' +import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' +import { layoutStore } from '@/renderer/core/layout/store/layoutStore' +import type { SlotLayout } from '@/renderer/core/layout/types' +import { app } from '@/scripts/app' + +interface SlotInteractionOptions { + nodeId: string + index: number + type: 'input' | 'output' + readonly?: boolean +} + +interface SlotInteractionHandlers { + onPointerDown: (event: PointerEvent) => void +} + +interface PointerSession { + begin: (pointerId: number) => void + register: (...stops: Array) => void + matches: (event: PointerEvent) => boolean + isActive: () => boolean + clear: () => void +} + +function createPointerSession(): PointerSession { + let pointerId: number | null = null + let stops: Fn[] = [] + + const begin = (id: number) => { + pointerId = id + } + + const register = (...newStops: Array) => { + for (const stop of newStops) { + if (typeof stop === 'function') { + stops.push(stop) + } + } + } + + const matches = (event: PointerEvent) => + pointerId !== null && event.pointerId === pointerId + + const isActive = () => pointerId !== null + + const clear = () => { + for (const stop of stops) { + stop() + } + stops = [] + pointerId = null + } + + return { begin, register, matches, isActive, clear } +} + +export function useSlotLinkInteraction({ + nodeId, + index, + type, + readonly +}: SlotInteractionOptions): SlotInteractionHandlers { + if (readonly) { + return { + onPointerDown: () => {} + } + } + + const { state, beginDrag, endDrag, updatePointerPosition } = + useSlotLinkDragState() + + function candidateFromTarget( + target: EventTarget | null + ): SlotDropCandidate | null { + if (!(target instanceof HTMLElement)) return null + const key = target.dataset['slotKey'] + if (!key) return null + + const layout = layoutStore.getSlotLayout(key) + if (!layout) return null + + const candidate: SlotDropCandidate = { layout, compatible: false } + + if (state.source) { + candidate.compatible = evaluateCompatibility( + state.source, + candidate + ).allowable + } + + return candidate + } + + const conversion = useSharedCanvasPositionConversion() + + const pointerSession = createPointerSession() + + const cleanupInteraction = () => { + pointerSession.clear() + endDrag() + } + + const updatePointerState = (event: PointerEvent) => { + const clientX = event.clientX + const clientY = event.clientY + const [canvasX, canvasY] = conversion.clientPosToCanvasPos([ + clientX, + clientY + ]) + + updatePointerPosition(clientX, clientY, canvasX, canvasY) + } + + const handlePointerMove = (event: PointerEvent) => { + if (!pointerSession.matches(event)) return + updatePointerState(event) + app.canvas?.setDirty(true) + } + + const connectSlots = (slotLayout: SlotLayout) => { + const canvas = app.canvas + const graph = canvas?.graph + const source = state.source + if (!canvas || !graph || !source) return + + const sourceNode = graph.getNodeById(Number(source.nodeId)) + const targetNode = graph.getNodeById(Number(slotLayout.nodeId)) + if (!sourceNode || !targetNode) return + + if (source.type === 'output' && slotLayout.type === 'input') { + const outputSlot = sourceNode.outputs?.[source.slotIndex] + const inputSlot = targetNode.inputs?.[slotLayout.index] + if (!outputSlot || !inputSlot) return + graph.beforeChange() + sourceNode.connectSlots(outputSlot, targetNode, inputSlot, undefined) + return + } + + if (source.type === 'input' && slotLayout.type === 'output') { + const inputSlot = sourceNode.inputs?.[source.slotIndex] + const outputSlot = targetNode.outputs?.[slotLayout.index] + if (!inputSlot || !outputSlot) return + graph.beforeChange() + sourceNode.disconnectInput(source.slotIndex, true) + targetNode.connectSlots(outputSlot, sourceNode, inputSlot, undefined) + } + } + + const finishInteraction = (event: PointerEvent) => { + if (!pointerSession.matches(event)) return + event.preventDefault() + + if (state.source) { + const candidate = candidateFromTarget(event.target) + if (candidate?.compatible) { + connectSlots(candidate.layout) + } + } + + cleanupInteraction() + app.canvas?.setDirty(true) + } + + const handlePointerUp = (event: PointerEvent) => { + finishInteraction(event) + } + + const handlePointerCancel = (event: PointerEvent) => { + if (!pointerSession.matches(event)) return + cleanupInteraction() + app.canvas?.setDirty(true) + } + + const onPointerDown = (event: PointerEvent) => { + if (event.button !== 0) return + if (!nodeId) return + if (pointerSession.isActive()) return + + const canvas = app.canvas + const graph = canvas?.graph + if (!canvas || !graph) return + + const layout = layoutStore.getSlotLayout( + getSlotKey(nodeId, index, type === 'input') + ) + if (!layout) return + + const resolvedNode = graph.getNodeById(Number(nodeId)) + const slot = + type === 'input' + ? resolvedNode?.inputs?.[index] + : resolvedNode?.outputs?.[index] + + const direction = + slot?.dir ?? (type === 'input' ? LinkDirection.LEFT : LinkDirection.RIGHT) + + beginDrag( + { + nodeId, + slotIndex: index, + type, + direction, + position: layout.position + }, + event.pointerId + ) + + pointerSession.begin(event.pointerId) + + updatePointerState(event) + + pointerSession.register( + useEventListener(window, 'pointermove', handlePointerMove, { + capture: true + }), + useEventListener(window, 'pointerup', handlePointerUp, { + capture: true + }), + useEventListener(window, 'pointercancel', handlePointerCancel, { + capture: true + }) + ) + app.canvas?.setDirty(true) + event.preventDefault() + event.stopPropagation() + } + + onBeforeUnmount(() => { + if (pointerSession.isActive()) { + cleanupInteraction() + } + }) + + return { + onPointerDown + } +} diff --git a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts index c6be50285..e8c38164d 100644 --- a/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts +++ b/src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts @@ -8,11 +8,21 @@ * Supports different element types (nodes, slots, widgets, etc.) with * customizable data attributes and update handlers. */ -import { getCurrentInstance, onMounted, onUnmounted } from 'vue' +import { + type MaybeRefOrGetter, + getCurrentInstance, + onMounted, + onUnmounted, + toValue +} from 'vue' +import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import type { Bounds, NodeId } from '@/renderer/core/layout/types' +import { syncNodeSlotLayoutsFromDOM } from './useSlotElementTracking' + /** * Generic update item for element bounds tracking */ @@ -54,8 +64,12 @@ const trackingConfigs: Map = new Map([ // Single ResizeObserver instance for all Vue elements const resizeObserver = new ResizeObserver((entries) => { - // Group updates by element type + // Canvas is ready when this code runs; no defensive guards needed. + const conv = useSharedCanvasPositionConversion() + // Group updates by type, then flush via each config's handler const updatesByType = new Map() + // Track nodes whose slots should be resynced after node size changes + const nodesNeedingSlotResync = new Set() for (const entry of entries) { if (!(entry.target instanceof HTMLElement)) continue @@ -76,30 +90,50 @@ const resizeObserver = new ResizeObserver((entries) => { if (!elementType || !elementId) continue - const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0] + // Use contentBoxSize when available; fall back to contentRect for older engines/tests + const contentBox = Array.isArray(entry.contentBoxSize) + ? entry.contentBoxSize[0] + : { + inlineSize: entry.contentRect.width, + blockSize: entry.contentRect.height + } + const width = contentBox.inlineSize + const height = contentBox.blockSize + + // Screen-space rect const rect = element.getBoundingClientRect() - + const [cx, cy] = conv.clientPosToCanvasPos([rect.left, rect.top]) + const topLeftCanvas = { x: cx, y: cy } const bounds: Bounds = { - x: rect.left, - y: rect.top, - width, - height: height + x: topLeftCanvas.x, + y: topLeftCanvas.y + LiteGraph.NODE_TITLE_HEIGHT, + width: Math.max(0, width), + height: Math.max(0, height - LiteGraph.NODE_TITLE_HEIGHT) } - if (!updatesByType.has(elementType)) { - updatesByType.set(elementType, []) + let updates = updatesByType.get(elementType) + if (!updates) { + updates = [] + updatesByType.set(elementType, updates) } - const updates = updatesByType.get(elementType) - if (updates) { - updates.push({ id: elementId, bounds }) + updates.push({ id: elementId, bounds }) + + // If this entry is a node, mark it for slot layout resync + if (elementType === 'node' && elementId) { + nodesNeedingSlotResync.add(elementId) } } - // Process updates by type + // Flush per-type for (const [type, updates] of updatesByType) { const config = trackingConfigs.get(type) - if (config && updates.length > 0) { - config.updateHandler(updates) + if (config && updates.length) config.updateHandler(updates) + } + + // After node bounds are updated, refresh slot cached offsets and layouts + if (nodesNeedingSlotResync.size > 0) { + for (const nodeId of nodesNeedingSlotResync) { + syncNodeSlotLayoutsFromDOM(nodeId) } } }) @@ -126,19 +160,20 @@ const resizeObserver = new ResizeObserver((entries) => { * ``` */ export function useVueElementTracking( - appIdentifier: string, + appIdentifierMaybe: MaybeRefOrGetter, trackingType: string ) { + const appIdentifier = toValue(appIdentifierMaybe) onMounted(() => { const element = getCurrentInstance()?.proxy?.$el if (!(element instanceof HTMLElement) || !appIdentifier) return const config = trackingConfigs.get(trackingType) - if (config) { - // Set the appropriate data attribute - element.dataset[config.dataAttribute] = appIdentifier - resizeObserver.observe(element) - } + if (!config) return + + // Set the data attribute expected by the RO pipeline for this type + element.dataset[config.dataAttribute] = appIdentifier + resizeObserver.observe(element) }) onUnmounted(() => { @@ -146,10 +181,10 @@ export function useVueElementTracking( if (!(element instanceof HTMLElement)) return const config = trackingConfigs.get(trackingType) - if (config) { - // Remove the data attribute - delete element.dataset[config.dataAttribute] - resizeObserver.unobserve(element) - } + if (!config) return + + // Remove the data attribute and observer + delete element.dataset[config.dataAttribute] + resizeObserver.unobserve(element) }) } diff --git a/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts b/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts deleted file mode 100644 index aae08298a..000000000 --- a/src/renderer/extensions/vueNodes/execution/useExecutionStateProvider.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { storeToRefs } from 'pinia' -import { computed, provide } from 'vue' - -import { - ExecutingNodeIdsKey, - NodeProgressStatesKey -} from '@/renderer/core/canvas/injectionKeys' -import { useExecutionStore } from '@/stores/executionStore' - -/** - * Composable for providing execution state to Vue node children - * - * This composable sets up the execution state providers that can be injected - * by child Vue nodes using useNodeExecutionState. - * - * Should be used in the parent component that manages Vue nodes (e.g., GraphCanvas). - */ -export const useExecutionStateProvider = () => { - const executionStore = useExecutionStore() - const { executingNodeIds: storeExecutingNodeIds, nodeProgressStates } = - storeToRefs(executionStore) - - // Convert execution store data to the format expected by Vue nodes - const executingNodeIds = computed( - () => new Set(storeExecutingNodeIds.value.map(String)) - ) - - // Provide the execution state to all child Vue nodes - provide(ExecutingNodeIdsKey, executingNodeIds) - provide(NodeProgressStatesKey, nodeProgressStates) - - return { - executingNodeIds, - nodeProgressStates - } -} diff --git a/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts b/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts index 8f03e29e1..aa4867db9 100644 --- a/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts +++ b/src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts @@ -1,10 +1,7 @@ -import { computed, inject, ref } from 'vue' +import { storeToRefs } from 'pinia' +import { type MaybeRefOrGetter, computed, toValue } from 'vue' -import { - ExecutingNodeIdsKey, - NodeProgressStatesKey -} from '@/renderer/core/canvas/injectionKeys' -import type { NodeProgressState } from '@/schemas/apiSchema' +import { useExecutionStore } from '@/stores/executionStore' /** * Composable for managing execution state of Vue-based nodes @@ -12,18 +9,18 @@ import type { NodeProgressState } from '@/schemas/apiSchema' * Provides reactive access to execution state and progress for a specific node * by injecting execution data from the parent GraphCanvas provider. * - * @param nodeId - The ID of the node to track execution state for + * @param nodeIdMaybe - The ID of the node to track execution state for * @returns Object containing reactive execution state and progress */ -export const useNodeExecutionState = (nodeId: string) => { - const executingNodeIds = inject(ExecutingNodeIdsKey, ref(new Set())) - const nodeProgressStates = inject( - NodeProgressStatesKey, - ref>({}) - ) +export const useNodeExecutionState = ( + nodeIdMaybe: MaybeRefOrGetter +) => { + const nodeId = toValue(nodeIdMaybe) + const { uniqueExecutingNodeIdStrings, nodeProgressStates } = + storeToRefs(useExecutionStore()) const executing = computed(() => { - return executingNodeIds.value.has(nodeId) + return uniqueExecutingNodeIdStrings.value.has(nodeId) }) const progress = computed(() => { diff --git a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts index 995d83d6f..60e5a7fd8 100644 --- a/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts +++ b/src/renderer/extensions/vueNodes/layout/useNodeLayout.ts @@ -1,12 +1,14 @@ -/** - * Composable for individual Vue node components - * - * Uses customRef for shared write access with Canvas renderer. - * Provides dragging functionality and reactive layout state. - */ -import { computed, inject } from 'vue' +import { storeToRefs } from 'pinia' +import { + type CSSProperties, + type MaybeRefOrGetter, + computed, + inject, + toValue +} from 'vue' -import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys' +import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { TransformStateKey } from '@/renderer/core/layout/injectionKeys' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { LayoutSource, type Point } from '@/renderer/core/layout/types' @@ -15,20 +17,16 @@ import { LayoutSource, type Point } from '@/renderer/core/layout/types' * Composable for individual Vue node components * Uses customRef for shared write access with Canvas renderer */ -export function useNodeLayout(nodeId: string) { - const store = layoutStore +export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter) { + const nodeId = toValue(nodeIdMaybe) const mutations = useLayoutMutations() + const { selectedNodeIds } = storeToRefs(useCanvasStore()) // Get transform utilities from TransformPane if available - const transformState = inject('transformState') as - | { - canvasToScreen: (point: Point) => Point - screenToCanvas: (point: Point) => Point - } - | undefined + const transformState = inject(TransformStateKey) // Get the customRef for this node (shared write access) - const layoutRef = store.getNodeLayoutRef(nodeId) + const layoutRef = layoutStore.getNodeLayoutRef(nodeId) // Computed properties for easy access const position = computed(() => { @@ -57,8 +55,6 @@ export function useNodeLayout(nodeId: string) { let dragStartMouse: Point | null = null let otherSelectedNodesStartPositions: Map | null = null - const selectedNodeIds = inject(SelectedNodeIdsKey, null) - /** * Start dragging the node */ @@ -192,14 +188,16 @@ export function useNodeLayout(nodeId: string) { endDrag, // Computed styles for Vue templates - nodeStyle: computed(() => ({ - position: 'absolute' as const, - left: `${position.value.x}px`, - top: `${position.value.y}px`, - width: `${size.value.width}px`, - height: `${size.value.height}px`, - zIndex: zIndex.value, - cursor: isDragging ? 'grabbing' : 'grab' - })) + nodeStyle: computed( + (): CSSProperties => ({ + position: 'absolute' as const, + left: `${position.value.x}px`, + top: `${position.value.y}px`, + width: `${size.value.width}px`, + height: `${size.value.height}px`, + zIndex: zIndex.value, + cursor: isDragging ? 'grabbing' : 'grab' + }) + ) } } diff --git a/src/renderer/extensions/vueNodes/lod/useLOD.ts b/src/renderer/extensions/vueNodes/lod/useLOD.ts index 584e21f9a..87c1bb865 100644 --- a/src/renderer/extensions/vueNodes/lod/useLOD.ts +++ b/src/renderer/extensions/vueNodes/lod/useLOD.ts @@ -27,7 +27,7 @@ * * ``` */ -import { type Ref, computed, readonly } from 'vue' +import { type MaybeRefOrGetter, computed, readonly, toRef } from 'vue' export enum LODLevel { MINIMAL = 'minimal', // zoom <= 0.4 @@ -78,7 +78,8 @@ const LOD_CONFIGS: Record = { * @param zoomRef - Reactive reference to current zoom level (camera.z) * @returns LOD state and configuration */ -export function useLOD(zoomRef: Ref) { +export function useLOD(zoomRefMaybe: MaybeRefOrGetter) { + const zoomRef = toRef(zoomRefMaybe) // Continuous LOD score (0-1) for smooth transitions const lodScore = computed(() => { const zoom = zoomRef.value diff --git a/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts new file mode 100644 index 000000000..8fc82147a --- /dev/null +++ b/src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts @@ -0,0 +1,52 @@ +import { storeToRefs } from 'pinia' +import { type MaybeRefOrGetter, type Ref, computed, toValue } from 'vue' + +import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { useNodeOutputStore } from '@/stores/imagePreviewStore' + +export const useNodePreviewState = ( + nodeIdMaybe: MaybeRefOrGetter, + options?: { + isMinimalLOD?: Ref + isCollapsed?: Ref + } +) => { + const nodeId = toValue(nodeIdMaybe) + const workflowStore = useWorkflowStore() + const { nodePreviewImages } = storeToRefs(useNodeOutputStore()) + + const locatorId = computed(() => workflowStore.nodeIdToNodeLocatorId(nodeId)) + + const previewUrls = computed(() => { + const key = locatorId.value + if (!key) return undefined + const urls = nodePreviewImages.value[key] + return urls?.length ? urls : undefined + }) + + const hasPreview = computed(() => !!previewUrls.value?.length) + + const latestPreviewUrl = computed(() => { + const urls = previewUrls.value + return urls?.length ? urls.at(-1) : '' + }) + + const shouldShowPreviewImg = computed(() => { + if (!options?.isMinimalLOD || !options?.isCollapsed) { + return hasPreview.value + } + return ( + !options.isMinimalLOD.value && + !options.isCollapsed.value && + hasPreview.value + ) + }) + + return { + locatorId, + previewUrls, + hasPreview, + latestPreviewUrl, + shouldShowPreviewImg + } +} diff --git a/src/renderer/extensions/vueNodes/stores/nodeSlotRegistryStore.ts b/src/renderer/extensions/vueNodes/stores/nodeSlotRegistryStore.ts new file mode 100644 index 000000000..c5e76d4b4 --- /dev/null +++ b/src/renderer/extensions/vueNodes/stores/nodeSlotRegistryStore.ts @@ -0,0 +1,50 @@ +import { defineStore } from 'pinia' +import { markRaw } from 'vue' + +type SlotEntry = { + el: HTMLElement + index: number + type: 'input' | 'output' + cachedOffset?: { x: number; y: number } +} + +type NodeEntry = { + nodeId: string + slots: Map + stopWatch?: () => void +} + +export const useNodeSlotRegistryStore = defineStore('nodeSlotRegistry', () => { + const registry = markRaw(new Map()) + + function getNode(nodeId: string) { + return registry.get(nodeId) + } + + function ensureNode(nodeId: string) { + let node = registry.get(nodeId) + if (!node) { + node = { + nodeId, + slots: markRaw(new Map()) + } + registry.set(nodeId, node) + } + return node + } + + function deleteNode(nodeId: string) { + registry.delete(nodeId) + } + + function clear() { + registry.clear() + } + + return { + getNode, + ensureNode, + deleteNode, + clear + } +}) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue index 81e47985f..28be93117 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetGalleria.vue @@ -1,7 +1,7 @@