Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4fa21b5
feat: add label support to boolean toggle widgets
csongorczezar Dec 27, 2025
b16202e
fix: render wrapper dynamically
csongorczezar Dec 27, 2025
5fb3fc5
fix: used generic typing
csongorczezar Dec 27, 2025
795962f
fix: added fallback labels
csongorczezar Dec 27, 2025
035a0f2
feat: add ToggleGroup support for labeled boolean widgets
csongorczezar Dec 30, 2025
dd435f8
fix: aligned colors and positions
csongorczezar Dec 30, 2025
0e25a2f
fix: adding i18n keys for the fallback strings
csongorczezar Dec 31, 2025
9614107
fix: disable vue/no-unused-properties for shadcn-vue forwarded props
csongorczezar Dec 31, 2025
81e4f3c
fix: removed unused imports and dependencies
csongorczezar Dec 31, 2025
a1d4e62
[automated] Apply ESLint and Prettier fixes
actions-user Dec 31, 2025
5d94466
fix: use cva from catalog instead of class-variance-authority
csongorczezar Dec 31, 2025
dbf4a4c
feat: added primary togglegroup buttton styles
csongorczezar Jan 1, 2026
c8f52cb
fix: removing unused types
csongorczezar Jan 1, 2026
9bb16f6
[automated] Apply ESLint and Prettier fixes
actions-user Jan 1, 2026
d94476a
feat: added secondary and inverted styles
csongorczezar Jan 1, 2026
03186ce
fix: default variant changed and truncation added
csongorczezar Jan 2, 2026
e67d1bd
fix: downgrade pinia to 2.2.2 to match main and fix test failures
csongorczezar Jan 3, 2026
b6c7770
add conflict metadata to node requirements system
csongorczezar Jan 6, 2026
fd60f4e
chore: trigger CI re-run after dependency cleanup
csongorczezar Jan 6, 2026
43fce00
Merge remote-tracking branch 'origin/main' into feat-widget-label-props
csongorczezar Jan 6, 2026
b6ef7b8
fix: resolve coding guideline violations
csongorczezar Jan 6, 2026
f13e35c
Merge remote-tracking branch 'origin/main' into feat-widget-label-props
csongorczezar Jan 6, 2026
e4f87a9
fix: label aligned with button group
csongorczezar Jan 7, 2026
c33b30d
chore: merge latest main to sync dependencies
csongorczezar Jan 7, 2026
9592e0b
fix: update pnpm-lock.yaml to match main and resolve knip issues
csongorczezar Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions src/components/ui/toggle-group/ToggleGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
/* eslint-disable vue/no-unused-properties */
import { reactiveOmit } from '@vueuse/core'
import type { ToggleGroupRootEmits, ToggleGroupRootProps } from 'reka-ui'
import { ToggleGroupRoot, useForwardPropsEmits } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { provide } from 'vue'

import { cn } from '@/utils/tailwindUtil'

import { toggleGroupVariants } from './toggleGroup.variants'
import type { ToggleGroupItemVariants } from './toggleGroup.variants'

const props = defineProps<
ToggleGroupRootProps & {
class?: HTMLAttributes['class']
variant?: ToggleGroupItemVariants['variant']
}
>()
const emits = defineEmits<ToggleGroupRootEmits>()

provide('toggleGroup', {
variant: props.variant
})

const delegatedProps = reactiveOmit(props, 'class')

const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>

<template>
<ToggleGroupRoot
v-slot="slotProps"
v-bind="forwarded"
:class="cn(toggleGroupVariants(), props.class)"
>
<slot v-bind="slotProps" />
</ToggleGroupRoot>
</template>
47 changes: 47 additions & 0 deletions src/components/ui/toggle-group/ToggleGroupItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<script setup lang="ts">
/* eslint-disable vue/no-unused-properties */
import { reactiveOmit } from '@vueuse/core'
import type { ToggleGroupItemProps } from 'reka-ui'
import { ToggleGroupItem, useForwardProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { inject } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import { toggleGroupItemVariants } from './toggleGroup.variants'
import type { ToggleGroupItemVariants } from './toggleGroup.variants'
const props = defineProps<
ToggleGroupItemProps & {
class?: HTMLAttributes['class']
variant?: ToggleGroupItemVariants['variant']
}
>()
const context = inject<{ variant?: ToggleGroupItemVariants['variant'] }>(
'toggleGroup'
)
const delegatedProps = reactiveOmit(props, 'class', 'variant')
const forwardedProps = useForwardProps(delegatedProps)
</script>

<template>
<ToggleGroupItem
v-slot="slotProps"
v-bind="forwardedProps"
:class="
cn(
toggleGroupItemVariants({
variant: context?.variant || variant
}),
props.class
)
"
>
<span class="truncate min-w-0">
<slot v-bind="slotProps" />
</span>
</ToggleGroupItem>
</template>
36 changes: 36 additions & 0 deletions src/components/ui/toggle-group/toggleGroup.variants.ts
Original file line number Diff line number Diff line change
@@ -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
>
4 changes: 4 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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...",
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, string> = {
'widgets.boolean.true': 'true',
'widgets.boolean.false': 'false'
}
return translations[key] || key
}
})
}))

describe('WidgetToggleSwitch Value Binding', () => {
const createMockWidget = (
value: boolean = false,
options: Partial<ToggleSwitchProps> = {},
options: Record<string, unknown> = {},
callback?: (value: boolean) => void
): SimplifiedWidget<boolean> => ({
name: 'test_toggle',
Expand All @@ -34,7 +47,7 @@ describe('WidgetToggleSwitch Value Binding', () => {
},
global: {
plugins: [PrimeVue],
components: { ToggleSwitch }
components: { ToggleSwitch, ToggleGroup, ToggleGroupItem }
}
})
}
Expand Down Expand Up @@ -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)
})
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
<template>
<WidgetLayoutField :widget>
<WidgetLayoutField :widget="widgetWithStyle">
<!-- Use ToggleGroup when explicit labels are provided -->
<!-- The variant attribute is not necessary here because the default is secondary -->
<!-- It was still added to show that a variant (3) can be explicitly set -->
<ToggleGroup
v-if="hasLabels"
type="single"
variant="secondary"
:model-value="toggleGroupValue"
class="flex justify-end w-full mb-[-0.5rem]"
@update:model-value="handleToggleGroupChange"
>
<ToggleGroupItem value="off" :aria-label="`${widget.name}: ${labelOff}`">
{{ labelOff }}
</ToggleGroupItem>
<ToggleGroupItem value="on" :aria-label="`${widget.name}: ${labelOn}`">
{{ labelOn }}
</ToggleGroupItem>
</ToggleGroup>

<!-- Use ToggleSwitch for implicit boolean states -->
<ToggleSwitch
v-else
v-model="modelValue"
v-bind="filteredProps"
class="ml-auto block"
Expand All @@ -12,7 +33,10 @@
<script setup lang="ts">
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,
Expand All @@ -21,13 +45,50 @@ import {

import WidgetLayoutField from './layout/WidgetLayoutField.vue'

interface BooleanWidgetOptions {
on?: string
off?: string
}

const { widget } = defineProps<{
widget: SimplifiedWidget<boolean>
widget: SimplifiedWidget<boolean, BooleanWidgetOptions>
}>()

const modelValue = defineModel<boolean>()

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
}))
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ defineProps<{
widget: Pick<
SimplifiedWidget<string | number | undefined>,
'name' | 'label' | 'borderStyle'
>
> & {
labelStyle?: string
}
}>()

const hideLayoutField = inject<boolean>('hideLayoutField', false)
Expand All @@ -18,7 +20,10 @@ const hideLayoutField = inject<boolean>('hideLayoutField', false)
<div
class="grid grid-cols-subgrid min-w-0 justify-between gap-1 text-node-component-slot-text"
>
<div v-if="!hideLayoutField" class="truncate content-center-safe">
<div
v-if="!hideLayoutField"
:class="cn('truncate content-center-safe', widget.labelStyle)"
>
<template v-if="widget.name">
{{ widget.label || widget.name }}
</template>
Expand Down