diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 6f43623a3d..0000000000 --- a/.cursorrules +++ /dev/null @@ -1,61 +0,0 @@ -# Vue 3 Composition API Project Rules - -## Vue 3 Composition API Best Practices -- Use setup() function for component logic -- Utilize ref and reactive for reactive state -- Implement computed properties with computed() -- Use watch and watchEffect for side effects -- Implement lifecycle hooks with onMounted, onUpdated, etc. -- Utilize provide/inject for dependency injection -- Use vue 3.5 style of default prop declaration. Example: - -```typescript -const { nodes, showTotal = true } = defineProps<{ - nodes: ApiNodeCost[] - showTotal?: boolean -}>() -``` - -- Organize vue component in diff --git a/src/components/MenuHamburger.vue b/src/components/MenuHamburger.vue index 467561b54b..fa99e032c2 100644 --- a/src/components/MenuHamburger.vue +++ b/src/components/MenuHamburger.vue @@ -1,27 +1,27 @@ - - diff --git a/tests-ui/tests/components/TopMenuSection.test.ts b/src/components/TopMenuSection.test.ts similarity index 100% rename from tests-ui/tests/components/TopMenuSection.test.ts rename to src/components/TopMenuSection.test.ts diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 55c78dd5af..448a081f9e 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -10,40 +10,63 @@
-
- - +
- - - - - {{ queuedCount }} - - - - + + +
+ +
+ + +
+ + + + + +
- - diff --git a/src/components/actionbar/ComfyActionbar.vue b/src/components/actionbar/ComfyActionbar.vue index d7d92c7ded..1d24e48385 100644 --- a/src/components/actionbar/ComfyActionbar.vue +++ b/src/components/actionbar/ComfyActionbar.vue @@ -10,7 +10,7 @@
-
+
- - + + + +
@@ -43,17 +54,25 @@ import { watchDebounced } from '@vueuse/core' import { clamp } from 'es-toolkit/compat' +import { storeToRefs } from 'pinia' import Panel from 'primevue/panel' -import { computed, nextTick, onMounted, ref, watch } from 'vue' +import { computed, nextTick, ref, watch } from 'vue' +import { useI18n } from 'vue-i18n' -import { t } from '@/i18n' +import Button from '@/components/ui/button/Button.vue' +import { buildTooltipConfig } from '@/composables/useTooltipConfig' import { useSettingStore } from '@/platform/settings/settingStore' import { useTelemetry } from '@/platform/telemetry' +import { useCommandStore } from '@/stores/commandStore' +import { useExecutionStore } from '@/stores/executionStore' import { cn } from '@/utils/tailwindUtil' import ComfyRunButton from './ComfyRunButton' const settingsStore = useSettingStore() +const commandStore = useCommandStore() +const { t } = useI18n() +const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore()) const position = computed(() => settingsStore.get('Comfy.UseNewMenu')) const visible = computed(() => position.value !== 'Disabled') @@ -66,12 +85,7 @@ const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', { x: 0, y: 0 }) -const { - x, - y, - style: style, - isDragging -} = useDraggable(panelRef, { +const { x, y, style, isDragging } = useDraggable(panelRef, { initialValue: { x: 0, y: 0 }, handle: dragHandleRef, containerElement: document.body, @@ -126,7 +140,14 @@ const setInitialPosition = () => { } } } -onMounted(setInitialPosition) + +//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component. +//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition() +async function comfyRunButtonResolved() { + await nextTick() + setInitialPosition() +} + watch(visible, async (newVisible) => { if (newVisible) { await nextTick(setInitialPosition) @@ -255,6 +276,16 @@ watch(isDragging, (dragging) => { isMouseOverDropZone.value = false } }) + +const cancelJobTooltipConfig = computed(() => + buildTooltipConfig(t('menu.interrupt')) +) + +const cancelCurrentJob = async () => { + if (isExecutionIdle.value) return + await commandStore.execute('Comfy.Interrupt') +} + const actionbarClass = computed(() => cn( 'w-[200px] border-dashed border-blue-500 opacity-80', @@ -267,10 +298,10 @@ const actionbarClass = computed(() => ) const panelClass = computed(() => cn( - 'actionbar pointer-events-auto z1000', + 'actionbar pointer-events-auto z-1300', isDragging.value && 'select-none pointer-events-none', isDocked.value - ? 'p-0 static mr-2 border-none bg-transparent' + ? 'p-0 static border-none bg-transparent' : 'fixed shadow-interface' ) ) diff --git a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue index c9e8495549..4c0ea84e43 100644 --- a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue +++ b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue @@ -22,12 +22,13 @@ value: item.tooltip, showDelay: 600 }" - :label="String(item.label ?? '')" - :icon="item.icon" - :severity="item.key === queueMode ? 'primary' : 'secondary'" - size="small" - text - /> + :variant="item.key === queueMode ? 'primary' : 'secondary'" + size="sm" + class="w-full justify-start" + > + + {{ String(item.label ?? '') }} + @@ -36,12 +37,12 @@ diff --git a/src/components/button/IconGroup.stories.ts b/src/components/button/IconGroup.stories.ts index c2fa1b96df..2cf407c744 100644 --- a/src/components/button/IconGroup.stories.ts +++ b/src/components/button/IconGroup.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite' -import IconButton from './IconButton.vue' +import Button from '@/components/ui/button/Button.vue' import IconGroup from './IconGroup.vue' const meta: Meta = { @@ -16,18 +16,18 @@ type Story = StoryObj export const Basic: Story = { render: () => ({ - components: { IconGroup, IconButton }, + components: { IconGroup, Button }, template: ` - + + + ` }) diff --git a/src/components/button/IconGroup.vue b/src/components/button/IconGroup.vue index bec0ac7fb3..2575d37aea 100644 --- a/src/components/button/IconGroup.vue +++ b/src/components/button/IconGroup.vue @@ -1,17 +1,15 @@ diff --git a/src/components/button/IconTextButton.stories.ts b/src/components/button/IconTextButton.stories.ts deleted file mode 100644 index 0139b9bd69..0000000000 --- a/src/components/button/IconTextButton.stories.ts +++ /dev/null @@ -1,213 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/vue3-vite' - -import IconTextButton from './IconTextButton.vue' - -const meta: Meta = { - title: 'Components/Button/IconTextButton', - component: IconTextButton, - tags: ['autodocs'], - argTypes: { - label: { - control: 'text' - }, - size: { - control: { type: 'select' }, - options: ['sm', 'md'] - }, - type: { - control: { type: 'select' }, - options: ['primary', 'secondary', 'transparent'] - }, - border: { - control: 'boolean', - description: 'Toggle border attribute' - }, - disabled: { - control: 'boolean', - description: 'Toggle disable status' - }, - iconPosition: { - control: { type: 'select' }, - options: ['left', 'right'] - }, - onClick: { action: 'clicked' } - } -} - -export default meta -type Story = StoryObj - -export const Primary: Story = { - render: (args) => ({ - components: { IconTextButton }, - setup() { - return { args } - }, - template: ` - - - - ` - }), - args: { - label: 'Deploy', - type: 'primary', - size: 'md' - } -} - -export const Secondary: Story = { - render: (args) => ({ - components: { IconTextButton }, - setup() { - return { args } - }, - template: ` - - - - ` - }), - args: { - label: 'Settings', - type: 'secondary', - size: 'md' - } -} - -export const Transparent: Story = { - render: (args) => ({ - components: { IconTextButton }, - setup() { - return { args } - }, - template: ` - - - - ` - }), - args: { - label: 'Cancel', - type: 'transparent', - size: 'md' - } -} - -export const WithIconRight: Story = { - render: (args) => ({ - components: { IconTextButton }, - setup() { - return { args } - }, - template: ` - - - - ` - }), - args: { - label: 'Next', - type: 'primary', - size: 'md', - iconPosition: 'right' - } -} - -export const Small: Story = { - render: (args) => ({ - components: { IconTextButton }, - setup() { - return { args } - }, - template: ` - - - - ` - }), - args: { - label: 'Save', - type: 'primary', - size: 'sm' - } -} - -export const AllVariants: Story = { - render: () => ({ - components: { - IconTextButton - }, - template: ` -
-
- - - - - - -
-
- - - - - - -
-
- - - - - - -
-
- - - - - - - - - -
-
- ` - }), - parameters: { - controls: { disable: true }, - actions: { disable: true } - } -} diff --git a/src/components/button/IconTextButton.vue b/src/components/button/IconTextButton.vue deleted file mode 100644 index 132622643e..0000000000 --- a/src/components/button/IconTextButton.vue +++ /dev/null @@ -1,58 +0,0 @@ - - - diff --git a/src/components/button/MoreButton.stories.ts b/src/components/button/MoreButton.stories.ts index ae1f865451..a0110722f9 100644 --- a/src/components/button/MoreButton.stories.ts +++ b/src/components/button/MoreButton.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite' -import IconTextButton from './IconTextButton.vue' +import Button from '@/components/ui/button/Button.vue' import MoreButton from './MoreButton.vue' const meta: Meta = { @@ -17,30 +17,26 @@ type Story = StoryObj export const Basic: Story = { render: () => ({ - components: { MoreButton, IconTextButton }, + components: { MoreButton, Button }, template: `
diff --git a/src/components/button/MoreButton.vue b/src/components/button/MoreButton.vue index 0b0e6f2094..d192efb90c 100644 --- a/src/components/button/MoreButton.vue +++ b/src/components/button/MoreButton.vue @@ -1,9 +1,17 @@ @@ -21,19 +27,42 @@ import Skeleton from 'primevue/skeleton' import Tag from 'primevue/tag' import { computed } from 'vue' +import { useI18n } from 'vue-i18n' +import { formatCreditsFromCents } from '@/base/credits/comfyCredits' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' -import { formatMetronomeCurrency } from '@/utils/formatUtil' -const { textClass } = defineProps<{ +const { textClass, showCreditsOnly } = defineProps<{ textClass?: string + showCreditsOnly?: boolean }>() const authStore = useFirebaseAuthStore() const balanceLoading = computed(() => authStore.isFetchingBalance) +const { t, locale } = useI18n() const formattedBalance = computed(() => { - if (!authStore.balance) return '0.00' - return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd') + const cents = + authStore.balance?.effective_balance_micros ?? + authStore.balance?.amount_micros ?? + 0 + const amount = formatCreditsFromCents({ + cents, + locale: locale.value + }) + return `${amount} ${t('credits.credits')}` +}) + +const formattedCreditsOnly = computed(() => { + const cents = + authStore.balance?.effective_balance_micros ?? + authStore.balance?.amount_micros ?? + 0 + const amount = formatCreditsFromCents({ + cents, + locale: locale.value, + numberOptions: { minimumFractionDigits: 0, maximumFractionDigits: 0 } + }) + return amount }) diff --git a/src/components/common/VirtualGrid.vue b/src/components/common/VirtualGrid.vue index 89de6e421b..134778778a 100644 --- a/src/components/common/VirtualGrid.vue +++ b/src/components/common/VirtualGrid.vue @@ -117,16 +117,7 @@ onBeforeUnmount(() => { .scroll-container { height: 100%; overflow-y: auto; - - /* Firefox */ - scrollbar-width: none; - - &::-webkit-scrollbar { - width: 1px; - } - - &::-webkit-scrollbar-thumb { - background-color: transparent; - } + scrollbar-width: thin; + scrollbar-color: var(--dialog-surface) transparent; } diff --git a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue index a1174aa100..5f96fe4c26 100644 --- a/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue +++ b/src/components/custom/widget/WorkflowTemplateSelectorDialog.vue @@ -22,16 +22,17 @@ @@ -301,16 +302,16 @@ v-if="template.tutorialUrl" class="flex flex-col-reverse justify-center" > - - + @@ -382,19 +383,18 @@ import ProgressSpinner from 'primevue/progressspinner' import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' -import IconButton from '@/components/button/IconButton.vue' -import IconTextButton from '@/components/button/IconTextButton.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 SearchBox from '@/components/common/SearchBox.vue' import MultiSelect from '@/components/input/MultiSelect.vue' -import SearchBox from '@/components/input/SearchBox.vue' import SingleSelect from '@/components/input/SingleSelect.vue' import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue' import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue' import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue' import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue' +import Button from '@/components/ui/button/Button.vue' import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue' import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue' import { useIntersectionObserver } from '@/composables/useIntersectionObserver' diff --git a/src/components/dialog/GlobalDialog.vue b/src/components/dialog/GlobalDialog.vue index 4566b06842..2074132b5b 100644 --- a/src/components/dialog/GlobalDialog.vue +++ b/src/components/dialog/GlobalDialog.vue @@ -14,6 +14,7 @@

diff --git a/src/components/dialog/confirm/ConfirmBody.vue b/src/components/dialog/confirm/ConfirmBody.vue new file mode 100644 index 0000000000..9a1cd5980d --- /dev/null +++ b/src/components/dialog/confirm/ConfirmBody.vue @@ -0,0 +1,19 @@ + + diff --git a/src/components/dialog/confirm/ConfirmFooter.vue b/src/components/dialog/confirm/ConfirmFooter.vue new file mode 100644 index 0000000000..9cdd6e37b8 --- /dev/null +++ b/src/components/dialog/confirm/ConfirmFooter.vue @@ -0,0 +1,40 @@ + + diff --git a/src/components/dialog/confirm/ConfirmHeader.vue b/src/components/dialog/confirm/ConfirmHeader.vue new file mode 100644 index 0000000000..3c83377331 --- /dev/null +++ b/src/components/dialog/confirm/ConfirmHeader.vue @@ -0,0 +1,12 @@ + + diff --git a/src/components/dialog/confirm/confirmDialog.ts b/src/components/dialog/confirm/confirmDialog.ts new file mode 100644 index 0000000000..c615e6475d --- /dev/null +++ b/src/components/dialog/confirm/confirmDialog.ts @@ -0,0 +1,31 @@ +import ConfirmBody from '@/components/dialog/confirm/ConfirmBody.vue' +import ConfirmFooter from '@/components/dialog/confirm/ConfirmFooter.vue' +import ConfirmHeader from '@/components/dialog/confirm/ConfirmHeader.vue' +import { useDialogStore } from '@/stores/dialogStore' +import type { ComponentAttrs } from 'vue-component-type-helpers' + +interface ConfirmDialogOptions { + headerProps?: ComponentAttrs + props?: ComponentAttrs + footerProps?: ComponentAttrs +} + +export function showConfirmDialog(options: ConfirmDialogOptions = {}) { + const dialogStore = useDialogStore() + const { headerProps, props, footerProps } = options + return dialogStore.showDialog({ + headerComponent: ConfirmHeader, + component: ConfirmBody, + footerComponent: ConfirmFooter, + headerProps, + props, + footerProps, + dialogComponentProps: { + pt: { + header: 'py-0! px-0!', + content: 'p-0!', + footer: 'p-0!' + } + } + }) +} diff --git a/src/components/dialog/content/ApiNodesSignInContent.vue b/src/components/dialog/content/ApiNodesSignInContent.vue index a3139daaf9..41ad903c7b 100644 --- a/src/components/dialog/content/ApiNodesSignInContent.vue +++ b/src/components/dialog/content/ApiNodesSignInContent.vue @@ -11,24 +11,25 @@
-
- +
diff --git a/src/components/dialog/content/UpdatePasswordContent.vue b/src/components/dialog/content/UpdatePasswordContent.vue index dc116e9c21..ef99a77880 100644 --- a/src/components/dialog/content/UpdatePasswordContent.vue +++ b/src/components/dialog/content/UpdatePasswordContent.vue @@ -7,12 +7,9 @@ - @@ -20,10 +17,10 @@ import type { FormSubmitEvent } from '@primevue/forms' import { Form } from '@primevue/forms' import { zodResolver } from '@primevue/forms/resolvers/zod' -import Button from 'primevue/button' import { ref } from 'vue' import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue' +import Button from '@/components/ui/button/Button.vue' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { updatePasswordSchema } from '@/schemas/signInSchema' diff --git a/src/components/dialog/content/credit/CreditTopUpOption.test.ts b/src/components/dialog/content/credit/CreditTopUpOption.test.ts new file mode 100644 index 0000000000..7faf432e72 --- /dev/null +++ b/src/components/dialog/content/credit/CreditTopUpOption.test.ts @@ -0,0 +1,48 @@ +import { mount } from '@vue/test-utils' +import { createI18n } from 'vue-i18n' +import { describe, expect, it } from 'vitest' + +import CreditTopUpOption from '@/components/dialog/content/credit/CreditTopUpOption.vue' + +const i18n = createI18n({ + legacy: false, + locale: 'en', + messages: { en: {} } +}) + +const mountOption = ( + props?: Partial<{ credits: number; description: string; selected: boolean }> +) => + mount(CreditTopUpOption, { + props: { + credits: 1000, + description: '~100 videos*', + selected: false, + ...props + }, + global: { + plugins: [i18n] + } + }) + +describe('CreditTopUpOption', () => { + it('renders credit amount and description', () => { + const wrapper = mountOption({ credits: 5000, description: '~500 videos*' }) + expect(wrapper.text()).toContain('5,000') + expect(wrapper.text()).toContain('~500 videos*') + }) + + it('applies unselected styling when not selected', () => { + const wrapper = mountOption({ selected: false }) + expect(wrapper.find('div').classes()).toContain( + 'bg-component-node-disabled' + ) + expect(wrapper.find('div').classes()).toContain('border-transparent') + }) + + it('emits select event when clicked', async () => { + const wrapper = mountOption() + await wrapper.find('div').trigger('click') + expect(wrapper.emitted('select')).toHaveLength(1) + }) +}) diff --git a/src/components/dialog/content/credit/CreditTopUpOption.vue b/src/components/dialog/content/credit/CreditTopUpOption.vue index f134aba5e2..c67c273a27 100644 --- a/src/components/dialog/content/credit/CreditTopUpOption.vue +++ b/src/components/dialog/content/credit/CreditTopUpOption.vue @@ -1,81 +1,45 @@ diff --git a/src/components/dialog/content/error/FindIssueButton.vue b/src/components/dialog/content/error/FindIssueButton.vue index 20f4f64896..767202251f 100644 --- a/src/components/dialog/content/error/FindIssueButton.vue +++ b/src/components/dialog/content/error/FindIssueButton.vue @@ -1,16 +1,14 @@ diff --git a/src/components/dialog/header/SettingDialogHeader.vue b/src/components/dialog/header/SettingDialogHeader.vue index 66765846ff..959cfa14da 100644 --- a/src/components/dialog/header/SettingDialogHeader.vue +++ b/src/components/dialog/header/SettingDialogHeader.vue @@ -15,9 +15,7 @@ diff --git a/src/components/node/NodePreview.vue b/src/components/node/NodePreview.vue index 42b486e467..107ee34713 100644 --- a/src/components/node/NodePreview.vue +++ b/src/components/node/NodePreview.vue @@ -2,7 +2,11 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c683087a3e168db/app/js/functions/sb_fn.js#L149 --> diff --git a/src/components/sidebar/tabs/AssetsSidebarTab.vue b/src/components/sidebar/tabs/AssetsSidebarTab.vue index b9e9e05ae0..827bd68516 100644 --- a/src/components/sidebar/tabs/AssetsSidebarTab.vue +++ b/src/components/sidebar/tabs/AssetsSidebarTab.vue @@ -1,19 +1,21 @@ @@ -19,10 +15,10 @@ diff --git a/src/components/topbar/ActionBarButtons.vue b/src/components/topbar/ActionBarButtons.vue index 81b5c42ad0..832bc179b8 100644 --- a/src/components/topbar/ActionBarButtons.vue +++ b/src/components/topbar/ActionBarButtons.vue @@ -4,26 +4,27 @@ v-for="(button, index) in actionBarButtonStore.buttons" :key="index" v-tooltip.bottom="button.tooltip" - :label="button.label" :aria-label="button.tooltip || button.label" :class="button.class" - text - rounded - severity="secondary" - class="h-7" + variant="muted-textonly" + size="sm" + class="h-7 rounded-full" @click="button.onClick" > - + + {{ button.label }} diff --git a/src/components/topbar/CurrentUserButton.test.ts b/src/components/topbar/CurrentUserButton.test.ts index 60c46ff4a1..db5349b49b 100644 --- a/src/components/topbar/CurrentUserButton.test.ts +++ b/src/components/topbar/CurrentUserButton.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import Button from 'primevue/button' +import Button from '@/components/ui/button/Button.vue' import { beforeEach, describe, expect, it, vi } from 'vitest' import { h } from 'vue' import { createI18n } from 'vue-i18n' diff --git a/src/components/topbar/CurrentUserButton.vue b/src/components/topbar/CurrentUserButton.vue index d64a6dfe26..39c4f1dd3f 100644 --- a/src/components/topbar/CurrentUserButton.vue +++ b/src/components/topbar/CurrentUserButton.vue @@ -3,9 +3,8 @@
- + diff --git a/src/components/topbar/WorkflowOverflowMenu.vue b/src/components/topbar/WorkflowOverflowMenu.vue index 33b8475a3a..f22ebfe13f 100644 --- a/src/components/topbar/WorkflowOverflowMenu.vue +++ b/src/components/topbar/WorkflowOverflowMenu.vue @@ -2,13 +2,14 @@
+ + diff --git a/src/components/ui/button/button.variants.ts b/src/components/ui/button/button.variants.ts new file mode 100644 index 0000000000..2d36d17b6d --- /dev/null +++ b/src/components/ui/button/button.variants.ts @@ -0,0 +1,53 @@ +import type { VariantProps } from 'cva' +import { cva } from 'cva' + +export const buttonVariants = cva({ + base: 'inline-flex items-center justify-center gap-2 cursor-pointer whitespace-nowrap appearance-none border-none rounded-md text-sm font-medium font-inter transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', + variants: { + variant: { + secondary: + 'bg-secondary-background text-secondary-foreground hover:bg-secondary-background-hover', + primary: + 'bg-primary-background text-base-foreground hover:bg-primary-background-hover', + inverted: + 'bg-base-foreground text-base-background hover:bg-base-foreground/80', + destructive: + 'bg-destructive-background text-base-foreground hover:bg-destructive-background-hover', + textonly: + 'text-base-foreground bg-transparent hover:bg-secondary-background-hover', + 'muted-textonly': + 'text-muted-foreground bg-transparent hover:bg-secondary-background-hover', + 'destructive-textonly': + 'text-destructive-background bg-transparent hover:bg-destructive-background/10' + }, + size: { + sm: 'h-6 rounded-sm px-2 py-1 text-xs', + md: 'h-8 rounded-lg p-2 text-xs', + lg: 'h-10 rounded-lg px-4 py-2 text-sm', + icon: 'size-8', + 'icon-sm': 'size-5 p-0' + } + }, + + defaultVariants: { + variant: 'secondary', + size: 'md' + } +}) + +export type ButtonVariants = VariantProps + +const variants = [ + 'secondary', + 'primary', + 'inverted', + 'destructive', + 'textonly', + 'muted-textonly', + 'destructive-textonly' +] as const satisfies Array +const sizes = ['sm', 'md', 'lg', 'icon', 'icon-sm'] as const satisfies Array< + ButtonVariants['size'] +> + +export const FOR_STORIES = { variants, sizes } as const diff --git a/src/components/ui/slider/Slider.vue b/src/components/ui/slider/Slider.vue index 2ecfc794a7..7d7a951212 100644 --- a/src/components/ui/slider/Slider.vue +++ b/src/components/ui/slider/Slider.vue @@ -14,6 +14,7 @@ import type { HTMLAttributes } from 'vue' import { cn } from '@/utils/tailwindUtil' const props = defineProps< + // eslint-disable-next-line vue/no-unused-properties SliderRootProps & { class?: HTMLAttributes['class'] } >() diff --git a/src/components/widget/SampleModelSelector.vue b/src/components/widget/SampleModelSelector.vue index 7f0147a3b8..6e1db7e7c7 100644 --- a/src/components/widget/SampleModelSelector.vue +++ b/src/components/widget/SampleModelSelector.vue @@ -17,43 +17,34 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue index ee9eec6fa2..a7f1463552 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue @@ -39,6 +39,7 @@ import { filterWidgetProps } from '@/utils/widgetPropFilter' +import { useNumberStepCalculation } from '../composables/useNumberStepCalculation' import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt' import { WidgetInputBaseClass } from './layout' import WidgetLayoutField from './layout/WidgetLayoutField.vue' @@ -56,7 +57,7 @@ const updateLocalValue = (newValue: number[] | undefined): void => { } const handleNumberInputUpdate = (newValue: number | undefined) => { - if (newValue) { + if (newValue !== undefined) { updateLocalValue([newValue]) return } @@ -67,33 +68,11 @@ const filteredProps = computed(() => filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS) ) -// Get the precision value for proper number formatting -const precision = computed(() => { - const p = widget.options?.precision - // Treat negative or non-numeric precision as undefined - return typeof p === 'number' && p >= 0 ? p : undefined -}) +const p = widget.options?.precision +const precision = typeof p === 'number' && p >= 0 ? p : undefined // Calculate the step value based on precision or widget options -const stepValue = computed(() => { - // Use step2 (correct input spec value) instead of step (legacy 10x value) - if (widget.options?.step2 !== undefined) { - return widget.options.step2 - } - - // Otherwise, derive from precision - if (precision.value === undefined) { - return undefined - } - - if (precision.value === 0) { - return 1 - } - - // For precision > 0, step = 1 / (10^precision) - // precision 1 → 0.1, precision 2 → 0.01, etc. - return 1 / Math.pow(10, precision.value) -}) +const stepValue = useNumberStepCalculation(widget.options, precision, true) const sliderNumberPt = useNumberWidgetButtonPt({ roundedLeft: true, diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue index 3d3fb9237c..64b673519a 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue @@ -6,18 +6,18 @@ import { useChainCallback } from '@/composables/functional/useChainCallback' import { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer' import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' -import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' +import { augmentToCanvasPointerEvent } from '@/renderer/extensions/vueNodes/utils/eventUtils' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import type { SimplifiedWidget } from '@/types/simplifiedWidget' const props = defineProps<{ widget: SimplifiedWidget - readonly?: boolean }>() const canvasEl = ref() +const containerHeight = ref(20) const canvas: LGraphCanvas = useCanvasStore().canvas as LGraphCanvas let node: LGraphNode | undefined @@ -53,9 +53,19 @@ onBeforeUnmount(() => { function draw() { if (!widgetInstance || !node) return const width = canvasEl.value.parentElement.clientWidth - const height = widgetInstance.computeSize - ? widgetInstance.computeSize(width)[1] - : 20 + // Priority: computedHeight (from litegraph) > computeLayoutSize > computeSize + let height = 20 + if (widgetInstance.computedHeight) { + height = widgetInstance.computedHeight + } else if (widgetInstance.computeLayoutSize) { + height = widgetInstance.computeLayoutSize(node).minHeight + } else if (widgetInstance.computeSize) { + height = widgetInstance.computeSize(width)[1] + } + containerHeight.value = height + // Set node.canvasHeight for legacy widgets that use it (e.g., Impact Pack) + // @ts-expect-error canvasHeight is a custom property used by some extensions + node.canvasHeight = height widgetInstance.y = 0 canvasEl.value.height = (height + 2) * scaleFactor canvasEl.value.width = width * scaleFactor @@ -64,16 +74,10 @@ function draw() { ctx.scale(scaleFactor, scaleFactor) widgetInstance.draw?.(ctx, node, width, 1, height) } -function translateEvent(e: PointerEvent): asserts e is CanvasPointerEvent { - if (!node) return - canvas.adjustMouseEvent(e) - canvas.graph_mouse[0] = e.offsetX + node.pos[0] - canvas.graph_mouse[1] = e.offsetY + node.pos[1] -} //See LGraphCanvas.processWidgetClick function handleDown(e: PointerEvent) { if (!node || !widgetInstance || !pointer) return - translateEvent(e) + augmentToCanvasPointerEvent(e, node, canvas) pointer.down(e) if (widgetInstance.mouse) pointer.onDrag = (e) => @@ -82,19 +86,22 @@ function handleDown(e: PointerEvent) { canvas.processWidgetClick(e, node, widgetInstance, pointer) } function handleUp(e: PointerEvent) { - if (!pointer) return - translateEvent(e) + if (!pointer || !node) return + augmentToCanvasPointerEvent(e, node, canvas) e.click_time = e.timeStamp - (pointer?.eDown?.timeStamp ?? 0) pointer.up(e) } function handleMove(e: PointerEvent) { - if (!pointer) return - translateEvent(e) + if (!pointer || !node) return + augmentToCanvasPointerEvent(e, node, canvas) pointer.move(e) } diff --git a/tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts similarity index 100% rename from tests-ui/tests/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts rename to src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue index e891b9bbdb..f460390a23 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue @@ -151,6 +151,7 @@ const allItems = computed(() => { } return [...inputItems.value, ...outputItems.value] }) + const dropdownItems = computed(() => { if (props.isAssetMode) { return allItems.value @@ -163,7 +164,7 @@ const dropdownItems = computed(() => { return outputItems.value case 'all': default: - return allItems.value + return [...inputItems.value, ...outputItems.value] } }) @@ -213,12 +214,13 @@ const acceptTypes = computed(() => { const layoutMode = ref(props.defaultLayoutMode ?? 'grid') watch( - modelValue, - (currentValue) => { + [modelValue, dropdownItems], + ([currentValue, _dropdownItems]) => { if (currentValue === undefined) { selectedSet.value.clear() return } + const item = dropdownItems.value.find((item) => item.name === currentValue) if (item) { selectedSet.value.clear() diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts index 64289b811d..e15f329449 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.test.ts @@ -179,8 +179,8 @@ describe('WidgetTextarea Value Binding', () => { const widget = createMockWidget('test') const wrapper = mountComponent(widget, 'test') - const textarea = wrapper.find('textarea') - expect(textarea.attributes('placeholder')).toBe('test_textarea') + const textareaLabel = wrapper.find('label') + expect(textareaLabel.text()).toBe('test_textarea') }) it('uses provided placeholder when specified', () => { diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue index 42bf3d77c2..25876f5395 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue @@ -1,11 +1,19 @@