Compare commits

...

25 Commits

Author SHA1 Message Date
Csongor Czezar
9592e0b0c6 fix: update pnpm-lock.yaml to match main and resolve knip issues 2026-01-07 13:14:49 -08:00
Csongor Czezar
c33b30d4a3 chore: merge latest main to sync dependencies 2026-01-07 12:37:07 -08:00
Csongor Czezar
e4f87a96a4 fix: label aligned with button group 2026-01-06 16:36:05 -08:00
Csongor Czezar
f13e35c89b Merge remote-tracking branch 'origin/main' into feat-widget-label-props 2026-01-06 15:26:59 -08:00
Csongor Czezar
b6ef7b8034 fix: resolve coding guideline violations 2026-01-06 14:47:10 -08:00
Csongor Czezar
43fce00e54 Merge remote-tracking branch 'origin/main' into feat-widget-label-props 2026-01-06 13:55:15 -08:00
Csongor Czezar
fd60f4edb6 chore: trigger CI re-run after dependency cleanup 2026-01-06 13:36:08 -08:00
Csongor Czezar
b6c77706d6 add conflict metadata to node requirements system 2026-01-06 12:33:00 -08:00
Csongor Czezar
e67d1bde5d fix: downgrade pinia to 2.2.2 to match main and fix test failures 2026-01-06 12:09:25 -08:00
Csongor Czezar
03186ceaf5 fix: default variant changed and truncation added 2026-01-06 12:09:25 -08:00
Csongor Czezar
d94476a7df feat: added secondary and inverted styles 2026-01-06 12:09:24 -08:00
GitHub Action
9bb16f6d99 [automated] Apply ESLint and Prettier fixes 2026-01-06 12:09:23 -08:00
Csongor Czezar
c8f52cbcb4 fix: removing unused types 2026-01-06 12:09:23 -08:00
Csongor Czezar
dbf4a4c64c feat: added primary togglegroup buttton styles 2026-01-06 12:09:22 -08:00
Csongor Czezar
5d94466d40 fix: use cva from catalog instead of class-variance-authority 2026-01-06 12:09:21 -08:00
GitHub Action
a1d4e62b87 [automated] Apply ESLint and Prettier fixes 2026-01-06 12:09:21 -08:00
Csongor Czezar
81e4f3cb77 fix: removed unused imports and dependencies 2026-01-06 12:09:20 -08:00
Csongor Czezar
9614107e82 fix: disable vue/no-unused-properties for shadcn-vue forwarded props 2026-01-06 12:09:19 -08:00
Csongor Czezar
0e25a2f79b fix: adding i18n keys for the fallback strings 2026-01-06 12:09:18 -08:00
Csongor Czezar
dd435f86f5 fix: aligned colors and positions 2026-01-06 12:09:18 -08:00
Csongor Czezar
035a0f250c feat: add ToggleGroup support for labeled boolean widgets 2026-01-06 12:09:17 -08:00
Csongor Czezar
795962f3c3 fix: added fallback labels 2026-01-06 12:09:16 -08:00
Csongor Czezar
5fb3fc5646 fix: used generic typing 2026-01-06 12:09:15 -08:00
Csongor Czezar
b16202e3ea fix: render wrapper dynamically 2026-01-06 12:09:15 -08:00
Csongor Czezar
4fa21b5f0f feat: add label support to boolean toggle widgets 2026-01-06 12:09:14 -08:00
7 changed files with 291 additions and 8 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
>

View File

@@ -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...",

View File

@@ -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',
@@ -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)
})
})
})

View File

@@ -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"
@@ -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,
@@ -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>

View File

@@ -8,7 +8,9 @@ defineProps<{
widget: Pick<
SimplifiedWidget<string | number | undefined>,
'name' | 'label' | 'borderStyle'
>
> & {
labelStyle?: string
}
}>()
const hideLayoutField = inject<boolean>('hideLayoutField', false)
@@ -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>