diff --git a/src/components/ui/toggle-group/ToggleGroup.vue b/src/components/ui/toggle-group/ToggleGroup.vue new file mode 100644 index 0000000000..48c0ff34bc --- /dev/null +++ b/src/components/ui/toggle-group/ToggleGroup.vue @@ -0,0 +1,39 @@ + + + + + + + diff --git a/src/components/ui/toggle-group/ToggleGroupItem.vue b/src/components/ui/toggle-group/ToggleGroupItem.vue new file mode 100644 index 0000000000..3f75a62d2d --- /dev/null +++ b/src/components/ui/toggle-group/ToggleGroupItem.vue @@ -0,0 +1,47 @@ + + + + + + + + + diff --git a/src/components/ui/toggle-group/toggleGroup.variants.ts b/src/components/ui/toggle-group/toggleGroup.variants.ts new file mode 100644 index 0000000000..fbd7f47779 --- /dev/null +++ b/src/components/ui/toggle-group/toggleGroup.variants.ts @@ -0,0 +1,36 @@ +import type { VariantProps } from 'cva' +import { cva } from 'cva' + +export const toggleGroupVariants = cva({ + base: 'flex gap-[var(--primitive-padding-padding-1,4px)] p-[var(--primitive-padding-padding-1,4px)] rounded-[var(--primitive-border-radius-rounded-sm,4px)] bg-component-node-widget-background' +}) + +export const toggleGroupItemVariants = cva({ + base: 'flex-1 inline-flex items-center justify-center border-0 rounded-[var(--primitive-border-radius-rounded-sm,4px)] px-[var(--primitive-padding-padding-2,8px)] py-[var(--primitive-padding-padding-1,4px)] text-xs font-inter font-normal transition-colors cursor-pointer overflow-hidden', + variants: { + variant: { + primary: [ + 'data-[state=off]:bg-transparent data-[state=off]:text-muted-foreground', + 'data-[state=off]:hover:bg-component-node-widget-background-hovered data-[state=off]:hover:text-white', + 'data-[state=on]:bg-primary-background data-[state=on]:text-white' + ], + secondary: [ + 'data-[state=off]:bg-transparent data-[state=off]:text-muted-foreground', + 'data-[state=off]:hover:bg-component-node-widget-background-hovered data-[state=off]:hover:text-white', + 'data-[state=on]:bg-component-node-widget-background-selected data-[state=on]:text-base-foreground' + ], + inverted: [ + 'data-[state=off]:bg-transparent data-[state=off]:text-muted-foreground', + 'data-[state=off]:hover:bg-component-node-widget-background-hovered data-[state=off]:hover:text-white', + 'data-[state=on]:bg-white data-[state=on]:text-base-background' + ] + } + }, + defaultVariants: { + variant: 'secondary' + } +}) + +export type ToggleGroupItemVariants = VariantProps< + typeof toggleGroupItemVariants +> diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 4b006d3605..f0e59e7468 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -2051,6 +2051,10 @@ "Set Group Nodes to Always": "Set Group Nodes to Always" }, "widgets": { + "boolean": { + "true": "true", + "false": "false" + }, "selectModel": "Select model", "uploadSelect": { "placeholder": "Select...", diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts index 7e7a876484..de91c436ed 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.test.ts @@ -1,17 +1,30 @@ import { mount } from '@vue/test-utils' import PrimeVue from 'primevue/config' import ToggleSwitch from 'primevue/toggleswitch' -import type { ToggleSwitchProps } from 'primevue/toggleswitch' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' +import ToggleGroup from '@/components/ui/toggle-group/ToggleGroup.vue' +import ToggleGroupItem from '@/components/ui/toggle-group/ToggleGroupItem.vue' import type { SimplifiedWidget } from '@/types/simplifiedWidget' import WidgetToggleSwitch from './WidgetToggleSwitch.vue' +vi.mock('vue-i18n', () => ({ + useI18n: () => ({ + t: (key: string) => { + const translations: Record = { + 'widgets.boolean.true': 'true', + 'widgets.boolean.false': 'false' + } + return translations[key] || key + } + }) +})) + describe('WidgetToggleSwitch Value Binding', () => { const createMockWidget = ( value: boolean = false, - options: Partial = {}, + options: Record = {}, callback?: (value: boolean) => void ): SimplifiedWidget => ({ name: 'test_toggle', @@ -34,7 +47,7 @@ describe('WidgetToggleSwitch Value Binding', () => { }, global: { plugins: [PrimeVue], - components: { ToggleSwitch } + components: { ToggleSwitch, ToggleGroup, ToggleGroupItem } } }) } @@ -149,4 +162,82 @@ describe('WidgetToggleSwitch Value Binding', () => { expect(emitted![3]).toContain(false) }) }) + + describe('Label Display', () => { + it('uses ToggleGroup when labels are provided', () => { + const widget = createMockWidget(false, { on: 'Enabled', off: 'Disabled' }) + const wrapper = mountComponent(widget, false) + + expect(wrapper.findComponent({ name: 'ToggleGroup' }).exists()).toBe(true) + expect(wrapper.findComponent({ name: 'ToggleSwitch' }).exists()).toBe( + false + ) + }) + + it('uses ToggleSwitch when no labels are provided', () => { + const widget = createMockWidget(false, {}) + const wrapper = mountComponent(widget, false) + + expect(wrapper.findComponent({ name: 'ToggleSwitch' }).exists()).toBe( + true + ) + expect(wrapper.findComponent({ name: 'ToggleGroup' }).exists()).toBe( + false + ) + }) + + it('displays both label_on and label_off in ToggleGroup', () => { + const widget = createMockWidget(false, { on: 'Enabled', off: 'Disabled' }) + const wrapper = mountComponent(widget, false) + + expect(wrapper.text()).toContain('Enabled') + expect(wrapper.text()).toContain('Disabled') + }) + + it('displays correct active state for false', () => { + const widget = createMockWidget(false, { on: 'Enabled', off: 'Disabled' }) + const wrapper = mountComponent(widget, false) + + const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' }) + expect(toggleGroup.props('modelValue')).toBe('off') + }) + + it('displays correct active state for true', () => { + const widget = createMockWidget(true, { on: 'Enabled', off: 'Disabled' }) + const wrapper = mountComponent(widget, true) + + const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' }) + expect(toggleGroup.props('modelValue')).toBe('on') + }) + + it('updates active state when toggled', async () => { + const widget = createMockWidget(false, { + on: 'Markdown', + off: 'Plaintext' + }) + const wrapper = mountComponent(widget, false) + + const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' }) + expect(toggleGroup.props('modelValue')).toBe('off') + + await wrapper.setProps({ modelValue: true }) + + expect(toggleGroup.props('modelValue')).toBe('on') + }) + + it('emits update:modelValue when ToggleGroup item is clicked', async () => { + const widget = createMockWidget(false, { + on: 'Markdown', + off: 'Plaintext' + }) + const wrapper = mountComponent(widget, false) + + const toggleGroup = wrapper.findComponent({ name: 'ToggleGroup' }) + await toggleGroup.vm.$emit('update:modelValue', 'on') + + const emitted = wrapper.emitted('update:modelValue') + expect(emitted).toBeDefined() + expect(emitted![0]).toContain(true) + }) + }) }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue index b902b6655e..58b9cc75be 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetToggleSwitch.vue @@ -1,6 +1,27 @@ - + + + + + + + {{ labelOff }} + + + {{ labelOn }} + + + + import ToggleSwitch from 'primevue/toggleswitch' import { computed } from 'vue' +import { useI18n } from 'vue-i18n' +import ToggleGroup from '@/components/ui/toggle-group/ToggleGroup.vue' +import ToggleGroupItem from '@/components/ui/toggle-group/ToggleGroupItem.vue' import type { SimplifiedWidget } from '@/types/simplifiedWidget' import { STANDARD_EXCLUDED_PROPS, @@ -21,13 +45,50 @@ import { import WidgetLayoutField from './layout/WidgetLayoutField.vue' +interface BooleanWidgetOptions { + on?: string + off?: string +} + const { widget } = defineProps<{ - widget: SimplifiedWidget + widget: SimplifiedWidget }>() const modelValue = defineModel() +const { t } = useI18n() + const filteredProps = computed(() => filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS) ) + +const hasLabels = computed(() => { + return !!(widget.options?.on || widget.options?.off) +}) + +const labelOn = computed(() => widget.options?.on ?? t('widgets.boolean.true')) +const labelOff = computed( + () => widget.options?.off ?? t('widgets.boolean.false') +) + +const toggleGroupValue = computed(() => { + return modelValue.value ? 'on' : 'off' +}) + +function handleToggleGroupChange(value: unknown) { + if (value === 'on') { + modelValue.value = true + } else if (value === 'off') { + modelValue.value = false + } +} + +// Override WidgetLayoutField styling when using ToggleGroup +const widgetWithStyle = computed(() => ({ + ...widget, + borderStyle: hasLabels.value + ? 'focus-within:ring-0 bg-transparent rounded-none focus-within:outline-none' + : undefined, + labelStyle: hasLabels.value ? 'mb-[-0.5rem]' : undefined +})) diff --git a/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue b/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue index 696e37a45a..50dff3851c 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue @@ -8,7 +8,9 @@ defineProps<{ widget: Pick< SimplifiedWidget, 'name' | 'label' | 'borderStyle' - > + > & { + labelStyle?: string + } }>() const hideLayoutField = inject('hideLayoutField', false) @@ -18,7 +20,10 @@ const hideLayoutField = inject('hideLayoutField', false) - + {{ widget.label || widget.name }}