diff --git a/.gitignore b/.gitignore index 5a58d1b1a..5473190ea 100644 --- a/.gitignore +++ b/.gitignore @@ -78,8 +78,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/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/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 0271ebefc..4be3df029 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/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/eslint.config.ts b/eslint.config.ts index 191e07ba9..94f8bb5f2 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -75,6 +75,8 @@ export default defineConfig([ '@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', 'unused-imports/no-unused-imports': 'error', 'vue/no-v-html': 'off', // Enforce dark-theme: instead of dark: prefix @@ -151,5 +153,14 @@ export default defineConfig([ } ] } + }, + { + files: ['tests-ui/**/*'], + rules: { + '@typescript-eslint/consistent-type-imports': [ + 'error', + { disallowTypeAnnotations: false } + ] + } } ]) diff --git a/package.json b/package.json index f8d72daeb..770ef7e04 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", @@ -90,7 +90,7 @@ "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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c16baf940..75fc42327 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,7 +320,7 @@ importers: 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)) @@ -547,6 +547,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'} @@ -981,6 +986,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'} @@ -1721,11 +1730,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': @@ -2067,6 +2076,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] @@ -2147,8 +2165,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: @@ -2158,16 +2176,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==} @@ -2789,9 +2807,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==} @@ -2815,8 +2839,8 @@ packages: '@vue/devtools-shared@7.7.6': resolution: {integrity: sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA==} - '@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: @@ -2856,6 +2880,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==} @@ -2974,6 +3001,9 @@ packages: resolution: {integrity: sha512-hexLq2lSO1K5SW9j21Ubc+q9Ptx7dyRTY7se19U8lhIlVMLCNXWCyQ6C22p9ez8ccX0v7QVmwkl2l1CnuGoO2Q==} engines: {node: '>= 14.0.0'} + 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==} @@ -3330,9 +3360,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==} @@ -3455,6 +3482,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==} @@ -3996,9 +4032,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==} @@ -4512,12 +4548,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==} @@ -4738,6 +4774,9 @@ packages: 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==} @@ -4941,9 +4980,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==} @@ -4985,6 +5021,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'} @@ -6001,8 +6040,8 @@ packages: 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 @@ -6022,6 +6061,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'} @@ -6067,10 +6109,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'} @@ -6167,9 +6205,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: '*' @@ -6851,6 +6888,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 @@ -7410,6 +7451,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': {} @@ -8204,29 +8250,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 + 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' @@ -8640,6 +8686,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 @@ -8688,12 +8742,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 @@ -8706,16 +8760,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 @@ -9448,11 +9502,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 @@ -9503,13 +9570,13 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/language-core@2.1.6(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 - 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 @@ -9566,6 +9633,8 @@ snapshots: '@vue/shared@3.5.13': {} + '@vue/shared@3.5.21': {} + '@vue/test-utils@2.4.6': dependencies: js-beautify: 1.15.1 @@ -9710,6 +9779,8 @@ snapshots: '@algolia/requester-fetch': 5.21.0 '@algolia/requester-node-http': 5.21.0 + alien-signals@0.4.14: {} + alien-signals@1.0.13: {} alien-signals@2.0.7: {} @@ -10065,8 +10136,6 @@ snapshots: compare-versions@6.1.1: {} - computeds@0.0.1: {} - concat-map@0.0.1: {} conf@13.1.0: @@ -10187,6 +10256,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: @@ -10808,11 +10881,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 @@ -11297,11 +11370,13 @@ snapshots: 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: @@ -11458,7 +11533,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 @@ -11521,6 +11596,10 @@ snapshots: 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 @@ -11912,10 +11991,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 @@ -11955,6 +12030,13 @@ snapshots: 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: {} @@ -13133,7 +13215,7 @@ snapshots: transitivePeerDependencies: - supports-color - typescript@5.4.2: {} + typescript@5.8.2: {} typescript@5.8.3: {} @@ -13143,6 +13225,8 @@ snapshots: ufo@1.5.4: {} + ufo@1.6.1: {} + uint8array-extras@1.5.0: {} undici-types@5.26.5: {} @@ -13193,8 +13277,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): @@ -13211,7 +13293,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) @@ -13225,7 +13307,7 @@ snapshots: 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 @@ -13308,17 +13390,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.18 + 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) 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/style.css b/src/assets/css/style.css index 747c3d1f6..cad8a1b3b 100644 --- a/src/assets/css/style.css +++ b/src/assets/css/style.css @@ -66,6 +66,8 @@ --color-charcoal-700: #202121; --color-charcoal-800: #171718; + --color-neutral-550: #636363; + --color-stone-100: #444444; --color-stone-200: #828282; --color-stone-300: #bbbbbb; @@ -103,6 +105,10 @@ --color-danger-100: #c02323; --color-danger-200: #d62952; + --color-coral-red-600: #973a40; + --color-coral-red-500: #c53f49; + --color-coral-red-400: #dd424e; + --color-bypass: #6a246a; --color-error: #962a2a; 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/platform/assets/components/AssetBrowserModal.stories.ts b/src/platform/assets/components/AssetBrowserModal.stories.ts new file mode 100644 index 000000000..acc93181d --- /dev/null +++ b/src/platform/assets/components/AssetBrowserModal.stories.ts @@ -0,0 +1,178 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue' +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: any) => { + 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: any) => { + 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: any) => { + 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..de05f437d --- /dev/null +++ b/src/platform/assets/components/AssetBrowserModal.vue @@ -0,0 +1,95 @@ + + + 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..e379099c1 --- /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..1f3295b43 --- /dev/null +++ b/src/platform/assets/components/AssetFilterBar.vue @@ -0,0 +1,102 @@ + + + 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..22af0cf4e --- /dev/null +++ b/src/platform/assets/composables/useAssetBrowser.ts @@ -0,0 +1,188 @@ +import { computed, ref } from 'vue' + +import { t } from '@/i18n' +import type { UUID } from '@/lib/litegraph/src/utils/uuid' +import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +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: new Date(asset.created_at).toLocaleDateString(), + 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) + }) + + // Actions + function selectAsset(asset: AssetDisplayItem): UUID { + if (import.meta.env.DEV) { + console.log('Asset selected:', asset.id, asset.name) + } + return asset.id + } + + return { + // State + searchQuery, + selectedCategory, + sortBy, + + // Computed + availableCategories, + contentTitle, + filteredAssets, + + // Actions + selectAsset, + transformAssetForDisplay + } +} 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..e5f63eead --- /dev/null +++ b/src/platform/assets/composables/useAssetBrowserDialog.ts @@ -0,0 +1,66 @@ +import AssetBrowserModal from '@/platform/assets/components/AssetBrowserModal.vue' +import { 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 */ + onAssetSelected?: (assetPath: string) => void +} + +export const useAssetBrowserDialog = () => { + const dialogStore = useDialogStore() + const dialogKey = 'global-asset-browser' + + function hide() { + dialogStore.closeDialog({ key: dialogKey }) + } + + function show(props: AssetBrowserDialogProps) { + const handleAssetSelected = (assetPath: string) => { + props.onAssetSelected?.(assetPath) + hide() // Auto-close on selection + } + + const handleClose = () => { + hide() + } + + // Default dialog configuration for AssetBrowserModal + const dialogComponentProps = { + headless: true, + modal: true, + closable: false, + pt: { + root: { + class: 'rounded-2xl overflow-hidden' + }, + header: { + class: 'p-0 hidden' + }, + content: { + class: 'p-0 m-0 h-full w-full' + } + } + } + + dialogStore.showDialog({ + key: dialogKey, + component: AssetBrowserModal, + props: { + nodeType: props.nodeType, + inputName: props.inputName, + currentValue: props.currentValue, + onSelect: handleAssetSelected, + onClose: handleClose + }, + dialogComponentProps + }) + } + + return { show, hide } +} 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..fab41649a 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(), size: z.number(), - created_at: z.string().optional() + mime_type: z.string(), + tags: z.array(z.string()), + preview_url: z.string().optional(), + created_at: z.string(), + updated_at: z.string(), + 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,22 @@ 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() +}) + // 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..74b20a753 100644 --- a/src/platform/assets/services/assetService.ts +++ b/src/platform/assets/services/assetService.ts @@ -67,7 +67,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 +75,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) ) ?? [] ) 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/SettingDialogContent.vue b/src/platform/settings/components/SettingDialogContent.vue index 7aa78cd04..bbeb53ca5 100644 --- a/src/platform/settings/components/SettingDialogContent.vue +++ b/src/platform/settings/components/SettingDialogContent.vue @@ -73,8 +73,8 @@ import ColorPaletteMessage from '@/platform/settings/components/ColorPaletteMess import SettingsPanel from '@/platform/settings/components/SettingsPanel.vue' import { useSettingSearch } from '@/platform/settings/composables/useSettingSearch' import { useSettingUI } from '@/platform/settings/composables/useSettingUI' -import { SettingTreeNode } from '@/platform/settings/settingStore' -import { ISettingGroup, SettingParams } from '@/platform/settings/types' +import type { SettingTreeNode } from '@/platform/settings/settingStore' +import type { ISettingGroup, SettingParams } from '@/platform/settings/types' import { flattenTree } from '@/utils/treeUtil' const { defaultPanel } = defineProps<{ diff --git a/src/platform/settings/components/SettingGroup.vue b/src/platform/settings/components/SettingGroup.vue index 2d25d1fe7..ea18aa569 100644 --- a/src/platform/settings/components/SettingGroup.vue +++ b/src/platform/settings/components/SettingGroup.vue @@ -20,7 +20,7 @@ import Divider from 'primevue/divider' import SettingItem from '@/platform/settings/components/SettingItem.vue' -import { SettingParams } from '@/platform/settings/types' +import type { SettingParams } from '@/platform/settings/types' import { normalizeI18nKey } from '@/utils/formatUtil' defineProps<{ diff --git a/src/platform/settings/components/SettingsPanel.vue b/src/platform/settings/components/SettingsPanel.vue index 997908e12..58b686a96 100644 --- a/src/platform/settings/components/SettingsPanel.vue +++ b/src/platform/settings/components/SettingsPanel.vue @@ -18,7 +18,7 @@ diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index c2596a1b4..5d67d0de7 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -10,7 +10,9 @@ 'bg-white dark-theme:bg-charcoal-800', 'lg-node absolute rounded-2xl', 'border border-solid border-sand-100 dark-theme:border-charcoal-600', - 'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20', + // hover (only when node should handle events) + shouldHandleNodePointerEvents && + 'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20', 'outline-transparent -outline-offset-2 outline-2', borderClass, outlineClass, @@ -21,7 +23,9 @@ 'will-change-transform': isDragging }, lodCssClass, - 'pointer-events-auto' + shouldHandleNodePointerEvents + ? 'pointer-events-auto' + : 'pointer-events-none' ) " :style="[ @@ -34,6 +38,7 @@ @pointerdown="handlePointerDown" @pointermove="handlePointerMove" @pointerup="handlePointerUp" + @wheel="handleWheel" >