Files
ComfyUI_frontend/src/components/ui/toggle-group/ToggleGroup.stories.ts
Christian Byrne 8744d3dd54 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>
2026-02-19 23:21:31 -08:00

183 lines
5.3 KiB
TypeScript

import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import ToggleGroup from './ToggleGroup.vue'
import ToggleGroupItem from './ToggleGroupItem.vue'
const meta = {
title: 'Components/ToggleGroup',
component: ToggleGroup,
tags: ['autodocs'],
argTypes: {
type: {
control: 'select',
options: ['single', 'multiple'],
description: 'Single or multiple selection'
},
variant: {
control: 'select',
options: ['default', 'outline'],
description: 'Visual style variant'
},
disabled: {
control: 'boolean',
description: 'When true, disables the toggle group'
},
'onUpdate:modelValue': { action: 'update:modelValue' }
}
} satisfies Meta<typeof ToggleGroup>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
render: (args) => ({
components: { ToggleGroup, ToggleGroupItem },
setup() {
const value = ref('center')
return { value, args }
},
template: `
<ToggleGroup
v-model="value"
type="single"
:variant="args.variant"
:disabled="args.disabled"
class="border border-border-default rounded-lg p-1"
>
<ToggleGroupItem value="left">Left</ToggleGroupItem>
<ToggleGroupItem value="center">Center</ToggleGroupItem>
<ToggleGroupItem value="right">Right</ToggleGroupItem>
</ToggleGroup>
<div class="mt-4 text-sm text-muted-foreground">
Selected: {{ value || 'None' }}
</div>
`
}),
args: {
variant: 'default',
disabled: false
}
}
export const Disabled: Story = {
render: (args) => ({
components: { ToggleGroup, ToggleGroupItem },
setup() {
const value = ref('on')
return { value, args }
},
template: `
<ToggleGroup v-model="value" type="single" disabled class="border border-border-default rounded-lg p-1">
<ToggleGroupItem value="off">Off</ToggleGroupItem>
<ToggleGroupItem value="on">On</ToggleGroupItem>
</ToggleGroup>
`
}),
args: {}
}
export const OutlineVariant: Story = {
render: (args) => ({
components: { ToggleGroup, ToggleGroupItem },
setup() {
const value = ref('medium')
return { value, args }
},
template: `
<ToggleGroup v-model="value" type="single" variant="outline" class="gap-2">
<ToggleGroupItem value="small" size="sm">Small</ToggleGroupItem>
<ToggleGroupItem value="medium">Medium</ToggleGroupItem>
<ToggleGroupItem value="large" size="lg">Large</ToggleGroupItem>
</ToggleGroup>
`
}),
args: {}
}
export const BooleanToggle: Story = {
render: (args) => ({
components: { ToggleGroup, ToggleGroupItem },
setup() {
const value = ref('off')
return { value, args }
},
template: `
<div class="space-y-4">
<p class="text-sm text-muted-foreground">Boolean toggle with custom labels:</p>
<ToggleGroup
v-model="value"
type="single"
class="w-48 border border-border-default rounded-lg p-1"
>
<ToggleGroupItem value="off" size="sm">Outside</ToggleGroupItem>
<ToggleGroupItem value="on" size="sm">Inside</ToggleGroupItem>
</ToggleGroup>
<div class="text-sm">Value: {{ value === 'on' ? true : false }}</div>
</div>
`
}),
args: {}
}
export const LongLabels: Story = {
render: (args) => ({
components: { ToggleGroup, ToggleGroupItem },
setup() {
const value = ref('option1')
return { value, args }
},
template: `
<div class="w-64">
<p class="text-sm text-muted-foreground mb-2">Labels truncate with ellipsis:</p>
<ToggleGroup
v-model="value"
type="single"
class="border border-border-default rounded-lg p-1"
>
<ToggleGroupItem value="option1" size="sm">Very Long Label One</ToggleGroupItem>
<ToggleGroupItem value="option2" size="sm">Another Long Label</ToggleGroupItem>
</ToggleGroup>
</div>
`
}),
args: {}
}
export const Sizes: Story = {
render: () => ({
components: { ToggleGroup, ToggleGroupItem },
setup() {
const sm = ref('a')
const md = ref('a')
const lg = ref('a')
return { sm, md, lg }
},
template: `
<div class="space-y-4">
<div>
<p class="text-sm text-muted-foreground mb-2">Small:</p>
<ToggleGroup v-model="sm" type="single" class="border border-border-default rounded p-1">
<ToggleGroupItem value="a" size="sm">A</ToggleGroupItem>
<ToggleGroupItem value="b" size="sm">B</ToggleGroupItem>
</ToggleGroup>
</div>
<div>
<p class="text-sm text-muted-foreground mb-2">Default:</p>
<ToggleGroup v-model="md" type="single" class="border border-border-default rounded-lg p-1">
<ToggleGroupItem value="a">A</ToggleGroupItem>
<ToggleGroupItem value="b">B</ToggleGroupItem>
</ToggleGroup>
</div>
<div>
<p class="text-sm text-muted-foreground mb-2">Large:</p>
<ToggleGroup v-model="lg" type="single" class="border border-border-default rounded-lg p-1">
<ToggleGroupItem value="a" size="lg">A</ToggleGroupItem>
<ToggleGroupItem value="b" size="lg">B</ToggleGroupItem>
</ToggleGroup>
</div>
</div>
`
})
}