mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 23:20:07 +00:00
673 lines
18 KiB
TypeScript
673 lines
18 KiB
TypeScript
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
|
|
|
import type { FormItem as FormItemType } from '@/types/settingTypes'
|
|
|
|
import FormItem from './FormItem.vue'
|
|
|
|
const meta: Meta = {
|
|
title: 'Components/Common/FormItem',
|
|
component: FormItem as any,
|
|
parameters: {
|
|
layout: 'centered',
|
|
docs: {
|
|
description: {
|
|
component:
|
|
'FormItem is a generalized form component that dynamically renders different input types based on configuration. Supports text, number, boolean, combo, slider, knob, color, image, and custom renderer inputs with proper labeling and accessibility.'
|
|
}
|
|
}
|
|
},
|
|
argTypes: {
|
|
item: {
|
|
control: 'object',
|
|
description:
|
|
'FormItem configuration object defining the input type and properties'
|
|
},
|
|
formValue: {
|
|
control: 'text',
|
|
description: 'The current form value (v-model)',
|
|
defaultValue: ''
|
|
},
|
|
id: {
|
|
control: 'text',
|
|
description: 'Optional HTML id for the form input',
|
|
defaultValue: undefined
|
|
},
|
|
labelClass: {
|
|
control: 'text',
|
|
description: 'Additional CSS classes for the label',
|
|
defaultValue: undefined
|
|
}
|
|
},
|
|
tags: ['autodocs']
|
|
}
|
|
|
|
export default meta
|
|
type Story = StoryObj
|
|
|
|
export const TextInput: Story = {
|
|
render: (args: any) => ({
|
|
components: { FormItem },
|
|
setup() {
|
|
return { args }
|
|
},
|
|
data() {
|
|
return {
|
|
value: args.formValue || 'Default text value',
|
|
textItem: {
|
|
name: 'Workflow Name',
|
|
type: 'text',
|
|
tooltip: 'Enter a descriptive name for your workflow',
|
|
attrs: {
|
|
placeholder: 'e.g., SDXL Portrait Generation'
|
|
}
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: string) {
|
|
console.log('Text value updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Text input form item with tooltip:
|
|
</div>
|
|
<FormItem
|
|
:item="textItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="workflow-name"
|
|
/>
|
|
<div style="margin-top: 12px; font-size: 12px; color: #6b7280;">
|
|
Current value: "{{ value }}"
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
args: {
|
|
formValue: 'My Workflow'
|
|
},
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Text input FormItem with tooltip and placeholder. Hover over the info icon to see the tooltip.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const NumberInput: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: 7.5,
|
|
numberItem: {
|
|
name: 'CFG Scale',
|
|
type: 'number',
|
|
tooltip:
|
|
'Classifier-free guidance scale controls how closely the AI follows your prompt',
|
|
attrs: {
|
|
min: 1,
|
|
max: 30,
|
|
step: 0.5,
|
|
showButtons: true
|
|
}
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: number) {
|
|
console.log('CFG scale updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Number input with controls and constraints:
|
|
</div>
|
|
<FormItem
|
|
:item="numberItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="cfg-scale"
|
|
/>
|
|
<div style="margin-top: 12px; font-size: 12px; color: #6b7280;">
|
|
Current CFG scale: {{ value }}
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Number input FormItem with min/max constraints and increment buttons for CFG scale parameter.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const BooleanToggle: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: false,
|
|
booleanItem: {
|
|
name: 'Enable GPU Acceleration',
|
|
type: 'boolean',
|
|
tooltip: 'Use GPU for faster processing when available'
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: boolean) {
|
|
console.log('GPU acceleration toggled:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Boolean toggle switch form item:
|
|
</div>
|
|
<FormItem
|
|
:item="booleanItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="gpu-accel"
|
|
/>
|
|
<div style="margin-top: 12px; font-size: 12px; color: #6b7280;">
|
|
GPU acceleration: {{ value ? 'Enabled' : 'Disabled' }}
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Boolean FormItem using ToggleSwitch component for enable/disable settings.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const ComboSelect: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: 'euler_a',
|
|
comboItem: {
|
|
name: 'Sampling Method',
|
|
type: 'combo',
|
|
tooltip: 'Algorithm used for denoising during generation',
|
|
options: [
|
|
'euler_a',
|
|
'euler',
|
|
'heun',
|
|
'dpm_2',
|
|
'dpm_2_ancestral',
|
|
'lms',
|
|
'dpm_fast',
|
|
'dpm_adaptive',
|
|
'dpmpp_2s_ancestral',
|
|
'dpmpp_sde',
|
|
'dpmpp_2m'
|
|
]
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: string) {
|
|
console.log('Sampling method updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Combo select with sampling methods:
|
|
</div>
|
|
<FormItem
|
|
:item="comboItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="sampling-method"
|
|
/>
|
|
<div style="margin-top: 12px; font-size: 12px; color: #6b7280;">
|
|
Selected: {{ value }}
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Combo select FormItem with ComfyUI sampling methods showing dropdown selection.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const SliderInput: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: 0.7,
|
|
sliderItem: {
|
|
name: 'Denoise Strength',
|
|
type: 'slider',
|
|
tooltip:
|
|
'How much to denoise the input image (0 = no change, 1 = complete redraw)',
|
|
attrs: {
|
|
min: 0,
|
|
max: 1,
|
|
step: 0.01
|
|
}
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: number) {
|
|
console.log('Denoise strength updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Slider input with precise decimal control:
|
|
</div>
|
|
<FormItem
|
|
:item="sliderItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="denoise-strength"
|
|
/>
|
|
<div style="margin-top: 12px; font-size: 12px; color: #6b7280;">
|
|
Denoise: {{ (value * 100).toFixed(0) }}%
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Slider FormItem for denoise strength with percentage display and fine-grained control.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const KnobInput: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: 20,
|
|
knobItem: {
|
|
name: 'Sampling Steps',
|
|
type: 'knob',
|
|
tooltip:
|
|
'Number of denoising steps - more steps = higher quality but slower generation',
|
|
attrs: {
|
|
min: 1,
|
|
max: 150,
|
|
step: 1
|
|
}
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: number) {
|
|
console.log('Steps updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Knob input for sampling steps:
|
|
</div>
|
|
<FormItem
|
|
:item="knobItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="sampling-steps"
|
|
/>
|
|
<div style="margin-top: 12px; font-size: 12px; color: #6b7280;">
|
|
Steps: {{ value }} ({{ value < 10 ? 'Very Fast' : value < 30 ? 'Fast' : value < 50 ? 'Balanced' : 'High Quality' }})
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Knob FormItem for sampling steps with quality indicator based on step count.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const MultipleFormItems: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
widthValue: 512,
|
|
heightValue: 512,
|
|
stepsValue: 20,
|
|
cfgValue: 7.5,
|
|
samplerValue: 'euler_a',
|
|
hiresValue: false
|
|
}
|
|
},
|
|
computed: {
|
|
formItems() {
|
|
return [
|
|
{
|
|
name: 'Width',
|
|
type: 'number',
|
|
tooltip: 'Image width in pixels',
|
|
attrs: { min: 64, max: 2048, step: 64 }
|
|
},
|
|
{
|
|
name: 'Height',
|
|
type: 'number',
|
|
tooltip: 'Image height in pixels',
|
|
attrs: { min: 64, max: 2048, step: 64 }
|
|
},
|
|
{
|
|
name: 'Sampling Steps',
|
|
type: 'knob',
|
|
tooltip: 'Number of denoising steps',
|
|
attrs: { min: 1, max: 150, step: 1 }
|
|
},
|
|
{
|
|
name: 'CFG Scale',
|
|
type: 'slider',
|
|
tooltip: 'Classifier-free guidance scale',
|
|
attrs: { min: 1, max: 30, step: 0.5 }
|
|
},
|
|
{
|
|
name: 'Sampler',
|
|
type: 'combo',
|
|
tooltip: 'Sampling algorithm',
|
|
options: ['euler_a', 'euler', 'heun', 'dpm_2', 'dpmpp_2m']
|
|
},
|
|
{
|
|
name: 'High-res Fix',
|
|
type: 'boolean',
|
|
tooltip: 'Enable high-resolution generation'
|
|
}
|
|
] as FormItemType[]
|
|
},
|
|
allSettings() {
|
|
return {
|
|
width: this.widthValue,
|
|
height: this.heightValue,
|
|
steps: this.stepsValue,
|
|
cfg: this.cfgValue,
|
|
sampler: this.samplerValue,
|
|
enableHires: this.hiresValue
|
|
}
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 500px;">
|
|
<div style="margin-bottom: 16px;">
|
|
<h3 style="margin: 0 0 8px 0; color: #374151;">ComfyUI Generation Settings</h3>
|
|
<p style="margin: 0; font-size: 14px; color: #6b7280;">
|
|
Multiple form items demonstrating different input types in a realistic settings panel.
|
|
</p>
|
|
</div>
|
|
|
|
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px;">
|
|
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
<FormItem
|
|
:item="formItems[0]"
|
|
:formValue="widthValue"
|
|
@update:formValue="(value) => widthValue = value"
|
|
id="form-width"
|
|
/>
|
|
<FormItem
|
|
:item="formItems[1]"
|
|
:formValue="heightValue"
|
|
@update:formValue="(value) => heightValue = value"
|
|
id="form-height"
|
|
/>
|
|
<FormItem
|
|
:item="formItems[2]"
|
|
:formValue="stepsValue"
|
|
@update:formValue="(value) => stepsValue = value"
|
|
id="form-steps"
|
|
/>
|
|
<FormItem
|
|
:item="formItems[3]"
|
|
:formValue="cfgValue"
|
|
@update:formValue="(value) => cfgValue = value"
|
|
id="form-cfg"
|
|
/>
|
|
<FormItem
|
|
:item="formItems[4]"
|
|
:formValue="samplerValue"
|
|
@update:formValue="(value) => samplerValue = value"
|
|
id="form-sampler"
|
|
/>
|
|
<FormItem
|
|
:item="formItems[5]"
|
|
:formValue="hiresValue"
|
|
@update:formValue="(value) => hiresValue = value"
|
|
id="form-hires"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 16px; background: rgba(0,0,0,0.05); padding: 12px; border-radius: 4px;">
|
|
<div style="font-weight: 600; margin-bottom: 8px; font-size: 14px;">Current Settings:</div>
|
|
<div style="font-family: monospace; font-size: 12px; color: #4b5563;">
|
|
{{ JSON.stringify(allSettings, null, 2) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Multiple FormItems demonstrating all major input types in a realistic ComfyUI settings panel.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const WithCustomLabels: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: 'custom_model.safetensors',
|
|
customItem: {
|
|
name: 'Model File',
|
|
type: 'text',
|
|
tooltip: 'Select the checkpoint model file to use for generation',
|
|
attrs: {
|
|
placeholder: 'Select or enter model filename...'
|
|
}
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: string) {
|
|
console.log('Model file updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
FormItem with custom label styling and slots:
|
|
</div>
|
|
<FormItem
|
|
:item="customItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="model-file"
|
|
:labelClass="{ 'font-bold': true, 'text-blue-600': true }"
|
|
>
|
|
<template #name-prefix>
|
|
<i class="pi pi-download" style="margin-right: 6px; color: #3b82f6;"></i>
|
|
</template>
|
|
<template #name-suffix>
|
|
<span style="margin-left: 6px; font-size: 10px; color: #ef4444;">*</span>
|
|
</template>
|
|
</FormItem>
|
|
<div style="margin-top: 12px; font-size: 12px; color: #6b7280;">
|
|
Selected model: {{ value || 'None' }}
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'FormItem with custom label styling and prefix/suffix slots for enhanced UI elements.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const ColorPicker: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: '#3b82f6',
|
|
colorItem: {
|
|
name: 'Theme Accent Color',
|
|
type: 'color',
|
|
tooltip: 'Primary accent color for the interface theme'
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: string) {
|
|
console.log('Color updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Color picker form item:
|
|
</div>
|
|
<FormItem
|
|
:item="colorItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="theme-color"
|
|
/>
|
|
<div style="margin-top: 16px; display: flex; align-items: center; gap: 12px;">
|
|
<div style="font-size: 12px; color: #6b7280;">Preview:</div>
|
|
<div
|
|
:style="{
|
|
backgroundColor: value,
|
|
width: '40px',
|
|
height: '20px',
|
|
borderRadius: '4px',
|
|
border: '1px solid #e2e8f0'
|
|
}"
|
|
></div>
|
|
<span style="font-family: monospace; font-size: 12px;">{{ value }}</span>
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Color picker FormItem with live preview showing the selected color value.'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const ComboWithComplexOptions: Story = {
|
|
render: () => ({
|
|
components: { FormItem },
|
|
data() {
|
|
return {
|
|
value: 'medium',
|
|
comboItem: {
|
|
name: 'Quality Preset',
|
|
type: 'combo',
|
|
tooltip:
|
|
'Predefined quality settings that adjust multiple parameters',
|
|
options: [
|
|
{ text: 'Draft (Fast)', value: 'draft' },
|
|
{ text: 'Medium Quality', value: 'medium' },
|
|
{ text: 'High Quality', value: 'high' },
|
|
{ text: 'Ultra (Slow)', value: 'ultra' }
|
|
]
|
|
} as FormItemType
|
|
}
|
|
},
|
|
methods: {
|
|
updateValue(newValue: string) {
|
|
console.log('Quality preset updated:', newValue)
|
|
this.value = newValue
|
|
}
|
|
},
|
|
computed: {
|
|
presetDescription() {
|
|
const descriptions = {
|
|
draft: 'Fast generation with 10 steps, suitable for previews',
|
|
medium: 'Balanced quality with 20 steps, good for most use cases',
|
|
high: 'High quality with 40 steps, slower but better results',
|
|
ultra: 'Maximum quality with 80 steps, very slow but best results'
|
|
}
|
|
return (descriptions as any)[this.value] || 'Unknown preset'
|
|
}
|
|
},
|
|
template: `
|
|
<div style="padding: 20px; min-width: 400px;">
|
|
<div style="margin-bottom: 16px; font-size: 14px; color: #6b7280;">
|
|
Combo with complex option objects:
|
|
</div>
|
|
<FormItem
|
|
:item="comboItem"
|
|
:formValue="value"
|
|
@update:formValue="updateValue"
|
|
id="quality-preset"
|
|
/>
|
|
<div style="margin-top: 12px; padding: 8px; background: rgba(0,0,0,0.05); border-radius: 4px;">
|
|
<div style="font-size: 12px; font-weight: 600; color: #374151;">{{ presetDescription }}</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
}),
|
|
parameters: {
|
|
docs: {
|
|
description: {
|
|
story:
|
|
'Complex combo FormItem with object options showing text/value pairs and descriptions.'
|
|
}
|
|
}
|
|
}
|
|
}
|