mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-01 19:51:54 +00:00
feat: add Reka UI ToggleGroup for BOOLEAN widget label_on/label_off display (#8680)
## Summary Surfaces the `label_on` and `label_off` (aka `on`/`off`) props for BOOLEAN widgets in the Vue implementation using a new Reka UI ToggleGroup component. **Before:** Labels extended outside node boundaries, causing overflow issues. **After:** Labels truncate with ellipsis and share space equally within the widget. [Screencast from 2026-02-13 16-27-49.webm](https://github.com/user-attachments/assets/912bab39-50a0-4d4e-8046-4b982e145bd9) ## Changes ### New ToggleGroup Component (`src/components/ui/toggle-group/`) - `ToggleGroup.vue` - Wrapper around Reka UI `ToggleGroupRoot` with variant support - `ToggleGroupItem.vue` - Styled toggle items with size variants (sm/default/lg) - `toggleGroup.variants.ts` - CVA variants adapted to ComfyUI design tokens - `ToggleGroup.stories.ts` - Storybook stories (Default, Disabled, Outline, Sizes, LongLabels) ### WidgetToggleSwitch Updates - Conditionally renders `ToggleGroup` when `options.on` or `options.off` are provided - Keeps PrimeVue `ToggleSwitch` for implicit boolean states (no labels) - Items use `flex-1 min-w-0 truncate` to share space and handle overflow ### i18n - Added `widgets.boolean.true` and `widgets.boolean.false` translation keys for fallback labels ## Implementation Details Follows the established pattern for adding Reka UI components in this codebase: - Uses Reka UI primitives (`ToggleGroupRoot`, `ToggleGroupItem`) - Adapts shadcn-vue structure with ComfyUI design tokens - Reactive provide/inject with typed `InjectionKey` for variant context - Styling matches existing `FormSelectButton` appearance Fixes COM-12709 --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,23 +1,32 @@
|
||||
<template>
|
||||
<WidgetLayoutField :widget>
|
||||
<div
|
||||
<!-- Use ToggleGroup when explicit labels are provided -->
|
||||
<ToggleGroup
|
||||
v-if="hasLabels"
|
||||
type="single"
|
||||
:model-value="modelValue ? 'on' : 'off'"
|
||||
:disabled="Boolean(widget.options?.read_only)"
|
||||
:class="
|
||||
cn('flex w-fit items-center gap-2', !hideLayoutField && 'ml-auto')
|
||||
cn(
|
||||
WidgetInputBaseClass,
|
||||
'w-full min-w-0 p-1 flex items-center justify-center gap-1'
|
||||
)
|
||||
"
|
||||
@update:model-value="(v) => handleOptionChange(v as string)"
|
||||
>
|
||||
<ToggleGroupItem value="off" size="sm">
|
||||
{{ widget.options?.off ?? t('widgets.boolean.false') }}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="on" size="sm">
|
||||
{{ widget.options?.on ?? t('widgets.boolean.true') }}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
|
||||
<!-- Use ToggleSwitch for implicit boolean states -->
|
||||
<div
|
||||
v-else
|
||||
:class="cn('flex w-fit items-center gap-2', hideLayoutField || 'ml-auto')"
|
||||
>
|
||||
<span
|
||||
v-if="stateLabel"
|
||||
:class="
|
||||
cn(
|
||||
'text-sm transition-colors',
|
||||
modelValue
|
||||
? 'text-node-component-slot-text'
|
||||
: 'text-node-component-slot-text/50'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ stateLabel }}
|
||||
</span>
|
||||
<ToggleSwitch
|
||||
v-model="modelValue"
|
||||
v-bind="filteredProps"
|
||||
@@ -30,7 +39,9 @@
|
||||
<script setup lang="ts">
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { useHideLayoutField } from '@/types/widgetTypes'
|
||||
@@ -40,6 +51,7 @@ import {
|
||||
filterWidgetProps
|
||||
} from '@/utils/widgetPropFilter'
|
||||
|
||||
import { WidgetInputBaseClass } from './layout'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const { widget } = defineProps<{
|
||||
@@ -49,14 +61,19 @@ const { widget } = defineProps<{
|
||||
const modelValue = defineModel<boolean>()
|
||||
|
||||
const hideLayoutField = useHideLayoutField()
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS)
|
||||
)
|
||||
|
||||
const stateLabel = computed(() => {
|
||||
const options = widget.options
|
||||
if (!options?.on && !options?.off) return null
|
||||
return modelValue.value ? (options.on ?? 'true') : (options.off ?? 'false')
|
||||
const hasLabels = computed(() => {
|
||||
return widget.options?.on != null || widget.options?.off != null
|
||||
})
|
||||
|
||||
function handleOptionChange(value: string | undefined) {
|
||||
if (value) {
|
||||
modelValue.value = value === 'on'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user