[merge] Merge origin/main

This commit is contained in:
snomiao
2025-08-19 04:55:46 +00:00
44 changed files with 1170 additions and 395 deletions

1
.gitignore vendored
View File

@@ -75,3 +75,4 @@ vite.config.mts.timestamp-*.mjs
*storybook.log
storybook-static

View File

@@ -1,63 +1,96 @@
import type { StorybookConfig } from '@storybook/vue3-vite'
import vue from '@vitejs/plugin-vue'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import type { InlineConfig } from 'vite'
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-actions',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds'
],
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-docs'],
framework: {
name: '@storybook/vue3-vite',
options: {}
},
viteFinal: async (config) => {
async viteFinal(config) {
// Use dynamic import to avoid CJS deprecation warning
const { mergeConfig } = await import('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
})
}
return mergeConfig(config, {
// Replace plugins entirely to avoid inheritance issues
plugins: [
vue(),
// Only include plugins we explicitly need for Storybook
Icons({
compiler: 'vue3',
customCollections: {
comfy: FileSystemIconLoader('../src/assets/icons/custom')
comfy: FileSystemIconLoader(
process.cwd() + '/src/assets/icons/custom'
)
}
}),
Components({
dts: false, // Disable DTS generation for Storybook
dts: false, // Disable dts generation in Storybook
resolvers: [
IconsResolver({
customCollections: ['comfy']
})
],
dirs: ['../src/components', '../src/layout', '../src/views'],
dirs: [
process.cwd() + '/src/components',
process.cwd() + '/src/layout',
process.cwd() + '/src/views'
],
deep: true,
extensions: ['vue']
})
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
],
server: {
allowedHosts: true
},
resolve: {
alias: {
'@': new URL('../src', import.meta.url).pathname
'@': process.cwd() + '/src'
}
},
define: {
...config.define,
global: 'globalThis',
__COMFYUI_FRONTEND_VERSION__: JSON.stringify('1.26.4'),
__SENTRY_ENABLED__: JSON.stringify(false),
__SENTRY_DSN__: JSON.stringify(''),
__ALGOLIA_APP_ID__: JSON.stringify(''),
__ALGOLIA_API_KEY__: JSON.stringify(''),
__USE_PROD_CONFIG__: JSON.stringify(false)
build: {
rollupOptions: {
external: () => {
// Don't externalize any modules in Storybook build
// This ensures PrimeVue and other dependencies are bundled
return false
},
onwarn: (warning, warn) => {
// Suppress specific warnings
if (
warning.code === 'UNUSED_EXTERNAL_IMPORT' &&
warning.message?.includes('resolveComponent')
) {
return
}
// Suppress Storybook font asset warnings
if (
warning.code === 'UNRESOLVED_IMPORT' &&
warning.message?.includes('nunito-sans')
) {
return
}
warn(warning)
}
},
chunkSizeWarningLimit: 1000
}
})
} satisfies InlineConfig)
}
}
export default config

View File

@@ -1,57 +1,53 @@
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import { setup } from '@storybook/vue3'
import type { Preview } from '@storybook/vue3-vite'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import { createI18n } from 'vue-i18n'
import '../public/materialdesignicons.min.css'
// Import styles
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'
// Mock Firebase for Storybook
const mockFirebase = {
auth: () => ({
currentUser: null,
onAuthStateChanged: () => () => {}
}),
firestore: () => ({})
}
const ComfyUIPreset = definePreset(Aura, {
semantic: {
// @ts-expect-error fix me
primary: Aura['primitive'].blue
}
})
// Setup Vue plugins for Storybook
// Setup Vue app for Storybook
setup((app) => {
app.directive('tooltip', Tooltip)
const pinia = createPinia()
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'en',
messages: {
en: {
g: {
searchSettings: 'Search Settings',
noResultsFound: 'No Results Found',
searchFailedMessage: 'Try adjusting your search terms',
experimental: 'Experimental',
loadingPanel: 'Loading {panel}...'
},
settings: {},
settingsCategories: {}
}
}
})
app.use(pinia)
// Initialize stores
useColorPaletteStore(pinia)
useWidgetStore(pinia)
app.use(i18n)
app.use(PrimeVue, {
theme: {
preset: null // Will use CSS for theming
preset: ComfyUIPreset,
options: {
prefix: 'p',
cssLayer: {
name: 'primevue',
order: 'primevue, tailwind-utilities'
},
darkModeSelector: '.dark-theme, :root:has(.dark-theme)'
}
}
})
app.use(ConfirmationService)
app.use(ToastService)
app.directive('tooltip', Tooltip)
// Provide mock services
app.provide('firebase', mockFirebase)
})
// Dark theme decorator
@@ -81,40 +77,9 @@ const preview: Preview = {
backgrounds: {
default: 'light',
values: [
{
name: 'light',
value: '#ffffff'
},
{
name: 'dark',
value: '#1a1a1a'
}
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#0a0a0a' }
]
},
viewport: {
viewports: {
small: {
name: 'Small',
styles: {
width: '640px',
height: '480px'
}
},
medium: {
name: 'Medium',
styles: {
width: '768px',
height: '1024px'
}
},
large: {
name: 'Large',
styles: {
width: '1024px',
height: '768px'
}
}
}
}
},
globalTypes: {

View File

@@ -42,7 +42,14 @@ const config: KnipConfig = {
'vite.electron.config.mts',
'vite.types.config.mts',
// Auto generated manager types
'src/types/generatedManagerTypes.ts'
'src/types/generatedManagerTypes.ts',
// Design system components (may not be used immediately)
'src/components/button/IconGroup.vue',
'src/components/button/MoreButton.vue',
'src/components/button/TextButton.vue',
'src/components/card/CardTitle.vue',
'src/components/card/CardDescription.vue',
'src/components/input/SingleSelect.vue'
],
ignoreExportsUsedInFile: true,
// Vue-specific configuration

237
package-lock.json generated
View File

@@ -59,13 +59,8 @@
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",
"@storybook/addon-actions": "^9.0.8",
"@storybook/addon-backgrounds": "^9.0.8",
"@storybook/addon-controls": "^9.0.8",
"@storybook/addon-docs": "^9.1.2",
"@storybook/addon-onboarding": "^9.1.2",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/vue3-vite": "^9.1.2",
"@storybook/addon-docs": "^9.1.1",
"@storybook/vue3-vite": "^9.1.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
"@types/fs-extra": "^11.0.4",
@@ -79,7 +74,7 @@
"eslint": "^9.12.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-storybook": "^9.1.2",
"eslint-plugin-storybook": "^9.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-vue": "^9.27.0",
"fs-extra": "^11.2.0",
@@ -91,7 +86,7 @@
"lint-staged": "^15.2.7",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"storybook": "^9.1.2",
"storybook": "^9.1.1",
"tailwindcss": "^3.4.4",
"tsx": "^4.15.6",
"typescript": "^5.4.5",
@@ -144,9 +139,9 @@
"dev": true
},
"node_modules/@adobe/css-tools": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz",
"integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==",
"dev": true,
"license": "MIT"
},
@@ -4330,50 +4325,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@storybook/addon-actions": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-9.0.8.tgz",
"integrity": "sha512-LFePu7PPnWN0Il/uoUpmA5T0J0C7d6haJIbg0pXrjxW2MQVSYXE4S4LSUz8fOImltBDV3xAl6tLPYHFj6VcrOA==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/addon-backgrounds": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-9.0.8.tgz",
"integrity": "sha512-4Vvr4wYHtiZ8UVWdCahK0XEMU4zNgInnNcVQ31YkUg41MVSY+aoZqtNuxOuRbFzUtjL9/aVsbY0Sg9Lp1/EJ4g==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/addon-controls": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-9.0.8.tgz",
"integrity": "sha512-6MY9QeBv2vNmBXH+ONmbpp/Gu/odSxriN1+BAY+il9OyXZBMq3OiDsjoH7xY5V7PGr+0XhZfOLkamvx3q+lQTg==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/addon-docs": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.2.tgz",
"integrity": "sha512-U3eHJ8lQFfEZ/OcgdKkUBbW2Y2tpAsHfy8lQOBgs5Pgj9biHEJcUmq+drOS/sJhle673eoBcUFmspXulI4KP1w==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.1.1.tgz",
"integrity": "sha512-CzgvTy3V5X4fe+VPkiZVwPKARlpEBDAKte8ajLAlHJQLFpADdYrBRQ0se6I+kcxva7rZQzdhuH7qjXMDRVcfnw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@mdx-js/react": "^3.0.0",
"@storybook/csf-plugin": "9.1.2",
"@storybook/csf-plugin": "9.1.1",
"@storybook/icons": "^1.4.0",
"@storybook/react-dom-shim": "9.1.2",
"@storybook/react-dom-shim": "9.1.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"ts-dedent": "^2.0.0"
@@ -4383,7 +4345,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^9.1.2"
"storybook": "^9.1.1"
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/icons": {
@@ -4401,9 +4363,9 @@
}
},
"node_modules/@storybook/addon-docs/node_modules/@storybook/react-dom-shim": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.2.tgz",
"integrity": "sha512-nw7BLAHCJswPZGsuL0Gs2AvFUWriusCTgPBmcHppSw/AqvT4XRFRDE+5q3j04/XKuZBrAA2sC4L+HuC0uzEChQ==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.1.1.tgz",
"integrity": "sha512-L+HCOXvOP+PwKrVS8od9aF+F4hO7zA0Nt1vnpbg2LeAHCxYghrjFVtioe7gSlzrlYdozQrPLY98a4OkDB7KGrw==",
"dev": true,
"license": "MIT",
"funding": {
@@ -4413,7 +4375,7 @@
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"storybook": "^9.1.2"
"storybook": "^9.1.1"
}
},
"node_modules/@storybook/addon-docs/node_modules/react": {
@@ -4446,39 +4408,14 @@
"dev": true,
"license": "MIT"
},
"node_modules/@storybook/addon-onboarding": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-9.1.2.tgz",
"integrity": "sha512-WfYIBmRtwUF13Hcu6BdsqATsAuBK0dwsz7O4tL0FGrIwY/vdzZ5jNzYvzzgilzlu9QiPvzEIBvs6X4BVulN3LQ==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^9.1.2"
}
},
"node_modules/@storybook/addon-viewport": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-9.0.8.tgz",
"integrity": "sha512-HgIFDzNXvMx0zQBM5mhwBoAJlrF9KRlxNCZnJbqrFLCJO4Ps2PMtB0HRGHcg0gm3RLcqyps0DpiF7wll3udb7Q==",
"dev": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@storybook/builder-vite": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.2.tgz",
"integrity": "sha512-5Y7e5wnSzFxCGP63UNRRZVoxHe1znU4dYXazJBobAlEcUPBk7A0sH2716tA6bS4oz92oG9tgvn1g996hRrw4ow==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.1.1.tgz",
"integrity": "sha512-rM0QOfykr39SFBRQnoAa5PU3xTHnJE1R5tigvjved1o7sumcfjrhqmEyAgNZv1SoRztOO92jwkTi7En6yheOKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/csf-plugin": "9.1.2",
"@storybook/csf-plugin": "9.1.1",
"ts-dedent": "^2.0.0"
},
"funding": {
@@ -4486,14 +4423,14 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^9.1.2",
"storybook": "^9.1.1",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@storybook/csf-plugin": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.2.tgz",
"integrity": "sha512-bfMh6r+RieBLPWtqqYN70le2uTE4JzOYPMYSCagHykUti3uM/1vRFaZNkZtUsRy5GwEzE5jLdDXioG1lOEeT2Q==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.1.1.tgz",
"integrity": "sha512-MwdtvzzFpkard06pCfDrgRXZiBfWAQICdKh7kzpv1L8SwewsRgUr5WZQuEAVfYdSvCFJbWnNN4KirzPhe5ENCg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4504,7 +4441,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^9.1.2"
"storybook": "^9.1.1"
}
},
"node_modules/@storybook/csf-plugin/node_modules/unplugin": {
@@ -4529,9 +4466,9 @@
"license": "MIT"
},
"node_modules/@storybook/vue3": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@storybook/vue3/-/vue3-9.1.2.tgz",
"integrity": "sha512-aYLh6/DZEuoOtsn/qePb9I/kuzwZGy+mS/ELlFoj72vpJc4d21hKZfiepO5bZ3z73XK7nLmdMVQ2tIwvsin4Vw==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@storybook/vue3/-/vue3-9.1.1.tgz",
"integrity": "sha512-eKY1wKKmFrO8IpgHIV7XAyv7WRvI9rdvni4niy0bcho7QLD27trmJ9lJ3mAwZ8rEpUjgYOSDi6i5/jangbZc4w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4547,19 +4484,19 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^9.1.2",
"storybook": "^9.1.1",
"vue": "^3.0.0"
}
},
"node_modules/@storybook/vue3-vite": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@storybook/vue3-vite/-/vue3-vite-9.1.2.tgz",
"integrity": "sha512-MSXNtSbY8dnlcSzmbjkzJs1gAmDVRH1b4lBYU8TPHb8YmFz9vdYi8JNjOFztEjcnFe6VPVeCys69MpzZqGF31g==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@storybook/vue3-vite/-/vue3-vite-9.1.1.tgz",
"integrity": "sha512-JdQPPYCVxvw+hXEd27JH5ESmP7o86/dwNGiWvFUZLUp1utjrtXfr68QiFWRWjWRCe/4RvNgypX3tKoZMZ3ay6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@storybook/builder-vite": "9.1.2",
"@storybook/vue3": "9.1.2",
"@storybook/builder-vite": "9.1.1",
"@storybook/vue3": "9.1.1",
"find-package-json": "^1.2.0",
"magic-string": "^0.30.0",
"typescript": "^5.8.3",
@@ -4574,7 +4511,7 @@
"url": "https://opencollective.com/storybook"
},
"peerDependencies": {
"storybook": "^9.1.2",
"storybook": "^9.1.1",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
@@ -4624,9 +4561,9 @@
"peer": true
},
"node_modules/@testing-library/jest-dom": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz",
"integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==",
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.4.tgz",
"integrity": "sha512-xDXgLjVunjHqczScfkCJ9iyjdNOVHvvCdqHSSxwM9L0l/wHkTRum67SDc020uAlCoqktJplgO2AAQeLP1wgqDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4634,6 +4571,7 @@
"aria-query": "^5.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"lodash": "^4.17.21",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
@@ -5273,9 +5211,9 @@
}
},
"node_modules/@types/react": {
"version": "19.1.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz",
"integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==",
"version": "19.1.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -5416,14 +5354,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz",
"integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.39.1",
"@typescript-eslint/types": "^8.39.1",
"@typescript-eslint/tsconfig-utils": "^8.39.0",
"@typescript-eslint/types": "^8.39.0",
"debug": "^4.3.4"
},
"engines": {
@@ -5438,9 +5376,9 @@
}
},
"node_modules/@typescript-eslint/project-service/node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz",
"integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5470,9 +5408,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz",
"integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8484,6 +8422,7 @@
"version": "1.39.9",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.9.tgz",
"integrity": "sha512-9OtbkZmTA2Qc9groyA1PUNeb6knVTkvB2RSdr/LcJXDL8IdEakaxwXLHXa7VX/Wj0GmdMJPR3WhnPGhiP3E+qg==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
@@ -8688,9 +8627,9 @@
}
},
"node_modules/eslint-plugin-storybook": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.2.tgz",
"integrity": "sha512-EQa/kChrYrekxv36q3pvW57anqxMlAP4EdPXEDyA/EDrCQJaaTbWEdsMnVZtD744RjPP0M5wzaUjHbMhNooAwQ==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.1.1.tgz",
"integrity": "sha512-g4/i9yW6cl4TCEMzYyALNvO3d/jB6TDvSs/Pmye7dHDrra2B7dgZJGzmEWILD62brVrLVHNoXgy2dNPtx80kmw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8701,18 +8640,18 @@
},
"peerDependencies": {
"eslint": ">=8",
"storybook": "^9.1.2"
"storybook": "^9.1.1"
}
},
"node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/scope-manager": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz",
"integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1"
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -8723,9 +8662,9 @@
}
},
"node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/types": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz",
"integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8737,16 +8676,16 @@
}
},
"node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/typescript-estree": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz",
"integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.39.1",
"@typescript-eslint/tsconfig-utils": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/visitor-keys": "8.39.1",
"@typescript-eslint/project-service": "8.39.0",
"@typescript-eslint/tsconfig-utils": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/visitor-keys": "8.39.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -8766,16 +8705,16 @@
}
},
"node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/utils": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz",
"integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.39.1",
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/typescript-estree": "8.39.1"
"@typescript-eslint/scope-manager": "8.39.0",
"@typescript-eslint/types": "8.39.0",
"@typescript-eslint/typescript-estree": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -8790,13 +8729,13 @@
}
},
"node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
"version": "8.39.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz",
"integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.39.1",
"@typescript-eslint/types": "8.39.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -16224,9 +16163,9 @@
"license": "MIT"
},
"node_modules/storybook": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.2.tgz",
"integrity": "sha512-TYcq7WmgfVCAQge/KueGkVlM/+g33sQcmbATlC3X6y/g2FEeSSLGrb6E6d3iemht8oio+aY6ld3YOdAnMwx45Q==",
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/storybook/-/storybook-9.1.1.tgz",
"integrity": "sha512-q6GaGZdVZh6rjOdGnc+4hGTu8ECyhyjQDw4EZNxKtQjDO8kqtuxbFm8l/IP2l+zLVJAatGWKkaX9Qcd7QZxz+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19484,4 +19423,4 @@
}
}
}
}
}

View File

@@ -40,13 +40,8 @@
"@lobehub/i18n-cli": "^1.20.0",
"@pinia/testing": "^0.1.5",
"@playwright/test": "^1.52.0",
"@storybook/addon-actions": "^9.0.8",
"@storybook/addon-backgrounds": "^9.0.8",
"@storybook/addon-controls": "^9.0.8",
"@storybook/addon-docs": "^9.1.2",
"@storybook/addon-onboarding": "^9.1.2",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/vue3-vite": "^9.1.2",
"@storybook/addon-docs": "^9.1.1",
"@storybook/vue3-vite": "^9.1.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/dompurify": "^3.0.5",
"@types/fs-extra": "^11.0.4",
@@ -60,7 +55,7 @@
"eslint": "^9.12.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-storybook": "^9.1.2",
"eslint-plugin-storybook": "^9.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
"eslint-plugin-vue": "^9.27.0",
"fs-extra": "^11.2.0",
@@ -72,7 +67,7 @@
"lint-staged": "^15.2.7",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"storybook": "^9.1.2",
"storybook": "^9.1.1",
"tailwindcss": "^3.4.4",
"tsx": "^4.15.6",
"typescript": "^5.4.5",
@@ -131,4 +126,4 @@
"zod": "^3.23.8",
"zod-validation-error": "^3.3.0"
}
}
}

View File

@@ -16,8 +16,8 @@ export default defineConfig({
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Retry on CI only - increased for better flaky test handling */
retries: process.env.CI ? 3 : 0,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

View File

@@ -0,0 +1,38 @@
<template>
<Button unstyled :class="buttonStyle" @click="onClick">
<slot></slot>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getButtonTypeClasses,
getIconButtonSizeClasses
} from '@/types/buttonTypes'
interface IconButtonProps extends BaseButtonProps {
onClick: (event: Event) => void
}
const {
size = 'md',
type = 'secondary',
class: className,
onClick
} = defineProps<IconButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} p-0`
const sizeClasses = getIconButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)
.join(' ')
})
</script>

View File

@@ -0,0 +1,7 @@
<template>
<div
class="flex justify-center items-center flex-shrink-0 outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white rounded-lg cursor-pointer"
>
<slot></slot>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<template>
<Button unstyled :class="buttonStyle" @click="onClick">
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></slot>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
interface IconTextButtonProps extends BaseButtonProps {
iconPosition?: 'left' | 'right'
label: string
onClick: () => void
}
const {
size = 'md',
type = 'primary',
class: className,
iconPosition = 'left',
label,
onClick
} = defineProps<IconTextButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} !justify-start gap-2`
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)
.join(' ')
})
</script>

View File

@@ -0,0 +1,51 @@
<template>
<div class="relative inline-flex items-center">
<IconButton @click="toggle">
<i-lucide:more-vertical class="text-sm" />
</IconButton>
<Popover
ref="popover"
:append-to="'body'"
:auto-z-index="true"
:base-z-index="1000"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="pt"
>
<div class="flex flex-col gap-1 p-2 min-w-40">
<slot :close="hide" />
</div>
</Popover>
</div>
</template>
<script setup lang="ts">
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import IconButton from './IconButton.vue'
const popover = ref<InstanceType<typeof Popover>>()
const toggle = (event: Event) => {
popover.value?.toggle(event)
}
const hide = () => {
popover.value?.hide()
}
const pt = computed(() => ({
root: {
class: 'absolute z-50'
},
content: {
class: [
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg',
'shadow-lg border border-zinc-200 dark-theme:border-zinc-700'
]
}
}))
</script>

View File

@@ -0,0 +1,40 @@
<template>
<Button unstyled :class="buttonStyle" role="button" @click="onClick">
<span>{{ label }}</span>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
interface TextButtonProps extends BaseButtonProps {
label: string
onClick: () => void
}
const {
size = 'md',
type = 'primary',
class: className,
label,
onClick
} = defineProps<TextButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = getBaseButtonClasses()
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = getButtonTypeClasses(type)
return [baseClasses, sizeClasses, typeClasses, className]
.filter(Boolean)
.join(' ')
})
</script>

View File

@@ -0,0 +1,7 @@
<template>
<div class="flex-1 w-full h-full">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,38 @@
<template>
<div :class="containerClasses" :style="containerStyle">
<slot name="top"></slot>
<slot name="bottom"></slot>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const {
ratio = 'square',
maxWidth,
minWidth
} = defineProps<{
maxWidth: number
minWidth: number
ratio?: 'square' | 'portrait' | 'tallPortrait'
}>()
const containerClasses = computed(() => {
const baseClasses =
'flex flex-col bg-white dark-theme:bg-zinc-800 rounded-lg shadow-sm border border-zinc-200 dark-theme:border-zinc-700 overflow-hidden'
const ratioClasses = {
square: 'aspect-[256/308]',
portrait: 'aspect-[256/325]',
tallPortrait: 'aspect-[256/353]'
}
return `${baseClasses} ${ratioClasses[ratio]}`
})
const containerStyle = computed(() => ({
maxWidth: `${maxWidth}px`,
minWidth: `${minWidth}px`
}))
</script>

View File

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

View File

@@ -0,0 +1,7 @@
<template>
<div class="text-neutral text-sm">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View File

@@ -0,0 +1,40 @@
<template>
<div :class="topStyle">
<slot class="absolute top-0 left-0 w-full h-full"></slot>
<div class="absolute top-2 left-2 flex gap-2">
<slot name="top-left"></slot>
</div>
<div class="absolute top-2 right-2 flex gap-2">
<slot name="top-right"></slot>
</div>
<div class="absolute bottom-2 left-2 flex gap-2">
<slot name="bottom-left"></slot>
</div>
<div class="absolute bottom-2 right-2 flex gap-2">
<slot name="bottom-right"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const { ratio = 'square' } = defineProps<{
ratio?: 'square' | 'landscape'
}>()
const topStyle = computed(() => {
const baseClasses = 'relative p-0'
const ratioClasses = {
square: 'aspect-[1/1]',
landscape: 'aspect-[48/27]'
}
return `${baseClasses} ${ratioClasses[ratio]}`
})
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div
class="inline-flex justify-center items-center gap-1 flex-shrink-0 py-1 px-2 text-xs bg-[#D9D9D966]/40 rounded font-bold text-white/90"
>
<slot name="icon" class="text-xs text-white/90"></slot>
<span>{{ label }}</span>
</div>
</template>
<script setup lang="ts">
const { label } = defineProps<{
label: string
}>()
</script>

View File

@@ -1,15 +0,0 @@
<template>
<button
class="flex justify-center items-center outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white w-8 h-8 rounded-lg cursor-pointer"
role="button"
@click="onClick"
>
<slot></slot>
</button>
</template>
<script setup lang="ts">
const { onClick } = defineProps<{
onClick: () => void
}>()
</script>

View File

@@ -1,67 +0,0 @@
<template>
<BaseWidgetLayout>
<template #leftPanel>
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
<template #header-icon>
<i-lucide:puzzle class="text-neutral" />
</template>
<template #header-title>
<span class="text-neutral text-base">{{ t('g.title') }}</span>
</template>
</LeftSidePanel>
</template>
<template #header>
<!-- here -->
</template>
<template #content>
<!-- here -->
</template>
<template #rightPanel>
<RightSidePanel></RightSidePanel>
</template>
</BaseWidgetLayout>
</template>
<script setup lang="ts">
import { provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
import BaseWidgetLayout from './layout/BaseWidgetLayout.vue'
import LeftSidePanel from './panel/LeftSidePanel.vue'
import RightSidePanel from './panel/RightSidePanel.vue'
const { t } = useI18n()
const { onClose } = defineProps<{
onClose: () => void
}>()
provide(OnCloseKey, onClose)
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
{ id: 'installed', label: 'Installed' },
{
title: 'TAGS',
items: [
{ id: 'tag-sd15', label: 'SD 1.5' },
{ id: 'tag-sdxl', label: 'SDXL' },
{ id: 'tag-utility', label: 'Utility' }
]
},
{
title: 'CATEGORIES',
items: [
{ id: 'cat-models', label: 'Models' },
{ id: 'cat-nodes', label: 'Nodes' }
]
}
])
const selectedNavItem = ref<string | null>('installed')
</script>

View File

@@ -29,12 +29,19 @@
</div>
</Message>
</template>
<div class="mb-3 flex gap-2">
<SelectButton v-model="filterType" :options="filterTypes" />
</div>
<DataTable
:value="extensionStore.extensions"
v-model:selection="selectedExtensions"
:value="filteredExtensions"
striped-rows
size="small"
:filters="filters"
selection-mode="multiple"
data-key="name"
>
<Column selection-mode="multiple" :frozen="true" style="width: 3rem" />
<Column :header="$t('g.extensionName')" sortable field="name">
<template #body="slotProps">
{{ slotProps.data.name }}
@@ -42,6 +49,7 @@
v-if="extensionStore.isCoreExtension(slotProps.data.name)"
value="Core"
/>
<Tag v-else value="Custom" severity="info" />
</template>
</Column>
<Column
@@ -78,6 +86,7 @@ import Column from 'primevue/column'
import ContextMenu from 'primevue/contextmenu'
import DataTable from 'primevue/datatable'
import Message from 'primevue/message'
import SelectButton from 'primevue/selectbutton'
import Tag from 'primevue/tag'
import ToggleSwitch from 'primevue/toggleswitch'
import { computed, onMounted, ref } from 'vue'
@@ -88,6 +97,10 @@ import { useSettingStore } from '@/stores/settingStore'
import PanelTemplate from './PanelTemplate.vue'
const filterTypes = ['All', 'Core', 'Custom']
const filterType = ref('All')
const selectedExtensions = ref<Array<any>>([])
const filters = ref({
global: { value: '', matchMode: FilterMatchMode.CONTAINS }
})
@@ -97,6 +110,22 @@ const settingStore = useSettingStore()
const editingEnabledExtensions = ref<Record<string, boolean>>({})
const filteredExtensions = computed(() => {
const extensions = extensionStore.extensions
switch (filterType.value) {
case 'Core':
return extensions.filter((ext) =>
extensionStore.isCoreExtension(ext.name)
)
case 'Custom':
return extensions.filter(
(ext) => !extensionStore.isCoreExtension(ext.name)
)
default:
return extensions
}
})
onMounted(() => {
extensionStore.extensions.forEach((ext) => {
editingEnabledExtensions.value[ext.name] =
@@ -163,6 +192,33 @@ const applyChanges = () => {
const menu = ref<InstanceType<typeof ContextMenu>>()
const contextMenuItems = [
{
label: 'Enable Selected',
icon: 'pi pi-check',
command: async () => {
selectedExtensions.value.forEach((ext) => {
if (!extensionStore.isExtensionReadOnly(ext.name)) {
editingEnabledExtensions.value[ext.name] = true
}
})
await updateExtensionStatus()
}
},
{
label: 'Disable Selected',
icon: 'pi pi-times',
command: async () => {
selectedExtensions.value.forEach((ext) => {
if (!extensionStore.isExtensionReadOnly(ext.name)) {
editingEnabledExtensions.value[ext.name] = false
}
})
await updateExtensionStatus()
}
},
{
separator: true
},
{
label: 'Enable All',
icon: 'pi pi-check',

View File

@@ -0,0 +1,123 @@
<template>
<div class="relative inline-block">
<MultiSelect
v-model="selectedItems"
:options="options"
option-label="name"
unstyled
:placeholder="label"
:max-selected-labels="0"
:pt="pt"
>
<!-- Trigger value (keep text scale identical) -->
<template #value>
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
</template>
<!-- Chevron size identical to current -->
<template #dropdownicon>
<i-lucide:chevron-down class="text-lg text-neutral-400" />
</template>
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
<template #option="slotProps">
<div class="flex items-center gap-2">
<div
class="flex h-4 w-4 p-0.5 flex-shrink-0 items-center justify-center rounded border-[3px] transition-all duration-200"
:class="
slotProps.selected
? 'border-blue-400 bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
: 'border-neutral-300 dark-theme:border-zinc-600 bg-neutral-100 dark-theme:bg-zinc-700'
"
>
<i-lucide:check
v-if="slotProps.selected"
class="text-xs text-bold text-white"
/>
</div>
<span>{{ slotProps.option.name }}</span>
</div>
</template>
</MultiSelect>
<!-- Selected count badge (unchanged) -->
<div
v-if="selectedCount > 0"
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
>
{{ selectedCount }}
</div>
</div>
</template>
<script setup lang="ts">
import MultiSelect, {
MultiSelectPassThroughMethodOptions
} from 'primevue/multiselect'
import { computed } from 'vue'
const { label, options } = defineProps<{
label?: string
options: { name: string; value: string }[]
}>()
const selectedItems = defineModel<{ name: string; value: string }[]>({
required: true
})
const selectedCount = computed(() => selectedItems.value.length)
/**
* Pure unstyled mode using only the PrimeVue PT API.
* All PrimeVue built-in checkboxes/headers are hidden via PT (no :deep hacks).
* Visual output matches the previous version exactly.
*/
const pt = computed(() => ({
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
class: [
'relative inline-flex cursor-pointer select-none w-full',
'rounded-lg bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
'transition-all duration-200 ease-in-out',
'border-[2.5px] border-solid',
selectedCount.value > 0
? 'border-blue-400 dark-theme:border-blue-500'
: 'border-transparent',
{ 'opacity-60 cursor-default': props.disabled }
]
}),
labelContainer: {
class:
'flex-1 flex items-center overflow-hidden whitespace-nowrap pl-4 py-2 '
},
label: {
class: 'p-0'
},
dropdown: {
class: 'flex shrink-0 cursor-pointer items-center justify-center px-3'
},
header: { class: 'hidden' },
// Overlay & list visuals unchanged
overlay:
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100',
list: {
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
},
// Option row hover tone identical
option:
'flex gap-1 items-center p-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
// Hide built-in checkboxes entirely via PT (no :deep)
pcHeaderCheckbox: {
root: { class: 'hidden' },
style: 'display: none !important'
},
pcOptionCheckbox: {
root: { class: 'hidden' },
style: 'display: none !important'
}
}))
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div
class="flex w-full items-center rounded-lg px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800"
>
<i-lucide:search class="text-neutral" />
<InputText
v-model="searchQuery"
:placeholder="placeHolder || 'Search...'"
type="text"
unstyled
class="w-full p-0 border-none outline-none bg-transparent text-xs text-neutral dark-theme:text-white"
/>
</div>
</template>
<script setup lang="ts">
import InputText from 'primevue/inputtext'
import { defineModel } from 'vue'
const { placeHolder } = defineProps<{
placeHolder?: string
}>()
const searchQuery = defineModel<string>('')
</script>

View File

@@ -0,0 +1,132 @@
<template>
<div class="relative inline-flex items-center">
<Select
v-model="selectedItem"
:options="options"
option-label="name"
option-value="value"
unstyled
:placeholder="label"
:pt="pt"
>
<!-- Trigger value -->
<template #value="slotProps">
<div class="flex items-center gap-2 text-sm">
<slot name="icon" />
<span
v-if="slotProps.value !== null && slotProps.value !== undefined"
class="text-zinc-700 dark-theme:text-gray-200"
>
{{ getLabel(slotProps.value) }}
</span>
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
{{ label }}
</span>
</div>
</template>
<!-- Trigger caret -->
<template #dropdownicon>
<i-lucide:chevron-down
class="text-base text-neutral-400 dark-theme:text-gray-300"
/>
</template>
<!-- Option row -->
<template #option="{ option, selected }">
<div class="flex items-center justify-between gap-3 w-full">
<span class="truncate">{{ option.name }}</span>
<i-lucide:check
v-if="selected"
class="text-neutral-900 dark-theme:text-white"
/>
</div>
</template>
</Select>
</div>
</template>
<script setup lang="ts">
import Select, { SelectPassThroughMethodOptions } from 'primevue/select'
import { computed } from 'vue'
const { label, options } = defineProps<{
label?: string
options: {
name: string
value: string
}[]
}>()
const selectedItem = defineModel<string | null>({ required: true })
const getLabel = (val: string | null | undefined) => {
if (val == null) return label ?? ''
const found = options.find((o) => o.value === val)
return found ? found.name : label ?? ''
}
/**
* Unstyled + PT API only
* - No background/border (same as page background)
* - Text/icon scale: compact size matching MultiSelect
*/
const pt = computed(() => ({
root: ({
props
}: SelectPassThroughMethodOptions<{ name: string; value: string }>) => ({
class: [
// container
'relative inline-flex w-full cursor-pointer select-none items-center',
// trigger surface
'rounded-md',
'bg-transparent text-neutral dark-theme:text-white',
'border-0',
// disabled
{ 'opacity-60 cursor-default': props.disabled }
]
}),
label: {
class:
// Align with MultiSelect labelContainer spacing
'flex-1 flex items-center overflow-hidden whitespace-nowrap pl-4 py-2 outline-none'
},
dropdown: {
class:
// Right chevron touch area
'flex shrink-0 items-center justify-center px-3 py-2'
},
overlay: {
class: [
// dropdown panel
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg'
]
},
list: {
class:
// Same list tone/size as MultiSelect
'flex flex-col gap-1 p-0 list-none border-none text-xs'
},
option: ({
context
}: SelectPassThroughMethodOptions<{ name: string; value: string }>) => ({
class: [
// Row layout
'flex items-center justify-between gap-3 px-3 py-2',
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
// Selected state + check icon
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.selected }
]
}),
optionLabel: {
class: 'truncate'
},
optionGroupLabel: {
class:
'px-3 py-2 text-xs uppercase tracking-wide text-zinc-500 dark-theme:text-zinc-400'
},
emptyMessage: {
class: 'px-3 py-2 text-sm text-zinc-500 dark-theme:text-zinc-400'
}
}))
</script>

View File

@@ -0,0 +1,205 @@
<template>
<BaseWidgetLayout :content-title="$t('Checkpoints')">
<template #leftPanel>
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
<template #header-icon>
<i-lucide:puzzle class="text-neutral" />
</template>
<template #header-title>
<span class="text-neutral text-base">{{ t('g.title') }}</span>
</template>
</LeftSidePanel>
</template>
<template #header>
<SearchBox v-model:="searchQuery" class="max-w-[384px]" />
</template>
<template #header-right-area>
<div class="flex gap-2">
<IconTextButton type="primary" label="Upload Model" @click="() => {}">
<template #icon>
<i-lucide:upload />
</template>
</IconTextButton>
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="secondary"
label="Settings"
@click="
() => {
close()
}
"
>
<template #icon>
<i-lucide:download />
</template>
</IconTextButton>
<IconTextButton
type="primary"
label="Profile"
@click="
() => {
close()
}
"
>
<template #icon>
<i-lucide:scroll />
</template>
</IconTextButton>
</template>
</MoreButton>
</div>
</template>
<template #contentFilter>
<div class="relative px-6 pt-2 pb-4 flex gap-2">
<MultiSelect
v-model="selectedFrameworks"
label="Select Frameworks"
:options="frameworkOptions"
/>
<MultiSelect
v-model="selectedProjects"
label="Select Projects"
:options="projectOptions"
/>
<SingleSelect
v-model="selectedSort"
label="Sorting Type"
:options="sortOptions"
class="w-[135px]"
>
<template #icon>
<i-lucide:filter />
</template>
</SingleSelect>
</div>
</template>
<template #content>
<!-- Card Examples -->
<!-- <div class="min-h-0 px-6 py-4 overflow-y-auto scrollbar-hide"> -->
<!-- <h2 class="text-xxl py-4 pt-0 m-0">{{ $t('Checkpoints') }}</h2> -->
<div class="flex flex-wrap gap-2">
<CardContainer
v-for="i in 100"
:key="i"
ratio="square"
:max-width="480"
:min-width="230"
>
<template #top>
<CardTop ratio="landscape">
<template #default>
<div class="w-full h-full bg-blue-500"></div>
</template>
<template #top-right>
<IconButton
class="!bg-white !text-neutral-900"
@click="() => {}"
>
<i-lucide:info />
</IconButton>
</template>
<template #bottom-right>
<SquareChip label="png" />
<SquareChip label="1.2 MB" />
<SquareChip label="LoRA">
<template #icon>
<i-lucide:folder />
</template>
</SquareChip>
</template>
</CardTop>
</template>
<template #bottom>
<CardBottom></CardBottom>
</template>
</CardContainer>
</div>
<!-- </div> -->
</template>
<template #rightPanel>
<RightSidePanel></RightSidePanel>
</template>
</BaseWidgetLayout>
</template>
<script setup lang="ts">
import { provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import CardBottom from '@/components/card/CardBottom.vue'
import CardContainer from '@/components/card/CardContainer.vue'
import CardTop from '@/components/card/CardTop.vue'
import SquareChip from '@/components/chip/SquareChip.vue'
import MultiSelect from '@/components/input/MultiSelect.vue'
import SearchBox from '@/components/input/SearchBox.vue'
import SingleSelect from '@/components/input/SingleSelect.vue'
import BaseWidgetLayout from '@/components/widget/layout/BaseWidgetLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
import RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'
import { NavGroupData, NavItemData } from '@/types/navTypes'
import { OnCloseKey } from '@/types/widgetTypes'
const frameworkOptions = ref([
{ name: 'Vue', value: 'vue' },
{ name: 'React', value: 'react' },
{ name: 'Angular', value: 'angular' },
{ name: 'Svelte', value: 'svelte' }
])
const projectOptions = ref([
{ name: 'Project A', value: 'proj-a' },
{ name: 'Project B', value: 'proj-b' },
{ name: 'Project C', value: 'proj-c' }
])
const sortOptions = ref([
{ name: 'Popular', value: 'popular' },
{ name: 'Latest', value: 'latest' },
{ name: 'A → Z', value: 'az' }
])
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
{ id: 'installed', label: 'Installed' },
{
title: 'TAGS',
items: [
{ id: 'tag-sd15', label: 'SD 1.5' },
{ id: 'tag-sdxl', label: 'SDXL' },
{ id: 'tag-utility', label: 'Utility' }
]
},
{
title: 'CATEGORIES',
items: [
{ id: 'cat-models', label: 'Models' },
{ id: 'cat-nodes', label: 'Nodes' }
]
}
])
const { t } = useI18n()
const { onClose } = defineProps<{
onClose: () => void
}>()
provide(OnCloseKey, onClose)
const searchQuery = ref<string>('')
const selectedFrameworks = ref([])
const selectedProjects = ref([])
const selectedSort = ref<string>('popular')
const selectedNavItem = ref<string | null>('installed')
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-100 dark-theme:bg-zinc-800"
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-50 dark-theme:bg-zinc-800"
>
<IconButton
v-show="!isRightPanelOpen && hasRightPanel"
@@ -45,8 +45,13 @@
</IconButton>
<slot name="header"></slot>
</div>
<div class="flex justify-end gap-2 min-w-20">
<slot name="header-right-area"></slot>
<slot name="header-right-area"></slot>
<div
class="flex justify-end gap-2 w-0"
:class="
hasRightPanel && !isRightPanelOpen ? 'min-w-18' : 'min-w-8'
"
>
<IconButton
v-if="isRightPanelOpen && hasRightPanel"
@click="toggleRightPanel"
@@ -55,18 +60,24 @@
</IconButton>
</div>
</header>
<main class="flex-1">
<slot name="content"></slot>
<main class="flex flex-col flex-1 min-h-0">
<!-- Fallback title bar when no leftPanel is provided -->
<slot name="contentFilter"></slot>
<h2 v-if="!$slots.leftPanel" class="text-xxl px-6 pt-2 pb-6 m-0">
{{ contentTitle }}
</h2>
<div class="min-h-0 px-6 pt-0 pb-10 overflow-y-auto scrollbar-hide">
<slot name="content"></slot>
</div>
</main>
</div>
<Transition name="slide-panel-right">
<aside
v-if="hasRightPanel && isRightPanelOpen"
class="w-1/4 min-w-40 max-w-80"
>
<slot name="rightPanel"></slot>
</aside>
</Transition>
<aside
v-if="hasRightPanel && isRightPanelOpen"
class="w-1/4 min-w-40 max-w-80"
>
<slot name="rightPanel"></slot>
</aside>
</div>
</div>
</div>
@@ -76,10 +87,14 @@
import { useBreakpoints } from '@vueuse/core'
import { computed, inject, ref, useSlots, watch } from 'vue'
import IconButton from '@/components/custom/button/IconButton.vue'
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
import IconButton from '@/components/button/IconButton.vue'
import { OnCloseKey } from '@/types/widgetTypes'
const BREAKPOINTS = { sm: 480 }
const { contentTitle } = defineProps<{
contentTitle: string
}>()
const BREAKPOINTS = { md: 880 }
const PANEL_SIZES = {
width: 'w-1/3',
minWidth: 'min-w-40',
@@ -90,7 +105,7 @@ const slots = useSlots()
const closeDialog = inject(OnCloseKey, () => {})
const breakpoints = useBreakpoints(BREAKPOINTS)
const notMobile = breakpoints.greater('sm')
const notMobile = breakpoints.greater('md')
const isLeftPanelOpen = ref<boolean>(true)
const isRightPanelOpen = ref<boolean>(false)
@@ -160,17 +175,4 @@ const toggleRightPanel = () => {
.slide-panel-leave-to {
transform: translateX(-100%);
}
/* Slide transition for right panel */
.slide-panel-right-enter-active,
.slide-panel-right-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
backface-visibility: hidden;
}
.slide-panel-right-enter-from,
.slide-panel-right-leave-to {
transform: translateX(100%);
}
</style>

View File

@@ -9,15 +9,20 @@
role="button"
@click="onClick"
>
<i-lucide:folder class="text-xs text-neutral" />
<span>
<i-lucide:folder v-if="hasFolderIcon" class="text-xs text-neutral" />
<span class="flex items-center">
<slot></slot>
</span>
</div>
</template>
<script setup lang="ts">
const { active, onClick } = defineProps<{
const {
hasFolderIcon = true,
active,
onClick
} = defineProps<{
hasFolderIcon?: boolean
active?: boolean
onClick: () => void
}>()

View File

@@ -1,6 +1,6 @@
<template>
<h3
class="m-0 px-3 py-0 pt-5 text-sm font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
class="m-0 px-3 py-0 pt-5 text-xxs font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
>
{{ title }}
</h3>

View File

@@ -7,7 +7,7 @@
<slot name="header-title"></slot>
</PanelHeader>
<nav class="flex-1 px-3 py-4 flex flex-col gap-2">
<nav class="flex-1 px-3 py-4 flex flex-col gap-1">
<template v-for="(item, index) in navItems" :key="index">
<div v-if="'items' in item" class="flex flex-col gap-2">
<NavTitle :title="item.title" />
@@ -36,10 +36,10 @@
<script setup lang="ts">
import { computed } from 'vue'
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
import NavItem from '@/components/widget/nav/NavItem.vue'
import NavTitle from '@/components/widget/nav/NavTitle.vue'
import { NavGroupData, NavItemData } from '@/types/navTypes'
import NavItem from '../nav/NavItem.vue'
import NavTitle from '../nav/NavTitle.vue'
import PanelHeader from './PanelHeader.vue'
const { navItems = [], modelValue } = defineProps<{

View File

@@ -1,4 +1,4 @@
import ModelSelector from '@/components/custom/widget/ModelSelector.vue'
import ModelSelector from '@/components/widget/ModelSelector.vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'

View File

@@ -0,0 +1,8 @@
/**
* Constants for group node type prefixes and separators
*
* v1 Prefix + Separator: workflow/
* v2 Prefix + Separator: workflow> (ComfyUI_frontend v1.2.63)
*/
export const PREFIX = 'workflow'
export const SEPARATOR = '>'

View File

@@ -1,3 +1,4 @@
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { t } from '@/i18n'
import { type NodeId } from '@/lib/litegraph/src/LGraphNode'
import {
@@ -35,11 +36,6 @@ type GroupNodeWorkflowData = {
nodes: ComfyNode[]
}
// v1 Prefix + Separator: workflow/
// v2 Prefix + Separator: workflow> (ComfyUI_frontend v1.2.63)
const PREFIX = 'workflow'
const SEPARATOR = '>'
const Workflow = {
InUse: {
Free: 0,

View File

@@ -1,3 +1,4 @@
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import {
type LGraphNode,
type LGraphNodeConstructor,
@@ -13,8 +14,6 @@ import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
import './groupNodeManage.css'
const ORDER: symbol = Symbol()
const PREFIX = 'workflow'
const SEPARATOR = '>'
// @ts-expect-error fixme ts strict error
function merge(target, source) {

View File

@@ -1,5 +1,6 @@
import { toString } from 'es-toolkit/compat'
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
import { CanvasPointer } from './CanvasPointer'
@@ -8102,7 +8103,6 @@ export class LGraphCanvas
| IContextMenuValue<(typeof LiteGraph.VALID_SHAPES)[number]>
| null
)[]
if (node.getMenuOptions) {
options = node.getMenuOptions(this)
} else {
@@ -8110,9 +8110,38 @@ export class LGraphCanvas
{
content: 'Convert to Subgraph 🆕',
callback: () => {
if (!this.selectedItems.size)
// find groupnodes, degroup and select children
if (this.selectedItems.size) {
let hasGroups = false
for (const item of this.selectedItems) {
const node = item as LGraphNode
const isGroup =
typeof node.type === 'string' &&
node.type.startsWith(`${PREFIX}${SEPARATOR}`)
if (isGroup && node.convertToNodes) {
hasGroups = true
const nodes = node.convertToNodes()
requestAnimationFrame(() => {
this.selectItems(nodes, true)
if (!this.selectedItems.size)
throw new Error('Convert to Subgraph: Nothing selected.')
this._graph.convertToSubgraph(this.selectedItems)
})
return
}
}
// If no groups were found, continue normally
if (!hasGroups) {
if (!this.selectedItems.size)
throw new Error('Convert to Subgraph: Nothing selected.')
this._graph.convertToSubgraph(this.selectedItems)
}
} else {
throw new Error('Convert to Subgraph: Nothing selected.')
this._graph.convertToSubgraph(this.selectedItems)
}
}
},
{

View File

@@ -439,12 +439,17 @@ export const useDialogService = () => {
}) {
const layoutDefaultProps: DialogComponentProps = {
headless: true,
unstyled: true,
modal: true,
closable: false,
pt: {
mask: {
class: 'bg-black bg-opacity-40'
root: {
class: 'rounded-2xl overflow-hidden'
},
header: {
class: '!p-0 hidden'
},
content: {
class: '!p-0 !m-0'
}
}
}

42
src/types/buttonTypes.ts Normal file
View File

@@ -0,0 +1,42 @@
import type { HTMLAttributes } from 'vue'
export interface BaseButtonProps {
size?: 'sm' | 'md'
type?: 'primary' | 'secondary' | 'transparent'
class?: HTMLAttributes['class']
}
export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
const sizeClasses = {
sm: 'px-2 py-1.5 text-xs',
md: 'px-2.5 py-2 text-sm'
}
return sizeClasses[size]
}
export const getButtonTypeClasses = (
type: BaseButtonProps['type'] = 'primary'
) => {
const typeClasses = {
primary:
'bg-neutral-900 text-white dark-theme:bg-white dark-theme:text-neutral-900',
secondary:
'bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
transparent: 'bg-transparent text-neutral-600 dark-theme:text-neutral-400'
}
return typeClasses[type]
}
export const getIconButtonSizeClasses = (
size: BaseButtonProps['size'] = 'md'
) => {
const sizeClasses = {
sm: 'w-6 h-6 text-xs !rounded-md',
md: 'w-8 h-8 text-sm'
}
return sizeClasses[size]
}
export const getBaseButtonClasses = () => {
return 'flex items-center justify-center flex-shrink-0 outline-none border-none rounded-lg cursor-pointer transition-all duration-200'
}

View File

@@ -1,2 +0,0 @@
export * from './navTypes'
export * from './widgetTypes'

View File

@@ -8,6 +8,7 @@ export default {
theme: {
fontSize: {
xxs: '0.625rem',
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
@@ -82,7 +83,7 @@ export default {
100: '#8282821a',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
400: '#A1A3AE',
500: '#71717a',
600: '#52525b',
700: '#38393b',

View File

@@ -16,7 +16,6 @@
"scripts/**/*.js",
"scripts/**/*.ts",
"tests-ui/**/*.ts",
".storybook/**/*.ts",
"src/**/*.stories.ts"
".storybook/**/*.ts"
]
}

View File

@@ -17,6 +17,7 @@ export default defineConfig({
globals: true,
environment: 'happy-dom',
setupFiles: ['./vitest.setup.ts'],
retry: process.env.CI ? 2 : 0,
include: [
'tests-ui/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
'src/components/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'