mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-14 17:37:46 +00:00
feat: extract SeedControlButton component (#9744)
<img width="1048" height="482" alt="스크린샷 2026-03-12 오전 9 11 56" src="https://github.com/user-attachments/assets/68009980-097c-4736-b7c4-eb8f9a6f05be" /> ## Summary - Extract inline value control button from `WidgetWithControl` into reusable `SeedControlButton` component - Support `badge` (pill) and `button` (square) variants per Figma design system spec - Use native `<button>` element for proper a11y (works with Reka UI's `PopoverTrigger as-child`) ## Test plan - [x] Verify seed control button renders correctly on KSampler node's seed widget - [x] Verify popover opens on click and mode selection works - [x] Verify all 4 modes display correct icon/text (shuffle, pencil-off, +1, -1) 🤖 Generated with [Claude Code](https://claude.com/claude-code) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9744-feat-extract-SeedControlButton-component-3206d73d365081a3823cc19e48d205c1) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import { CONTROL_OPTIONS } from '@/types/simplifiedWidget'
|
||||
|
||||
import ValueControlButton from './ValueControlButton.vue'
|
||||
|
||||
const meta: Meta<typeof ValueControlButton> = {
|
||||
title: 'Components/InputHelpers/ValueControlButton',
|
||||
component: ValueControlButton,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
argTypes: {
|
||||
mode: { control: 'select', options: [...CONTROL_OPTIONS] },
|
||||
variant: { control: 'select', options: ['badge', 'button'] }
|
||||
},
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template:
|
||||
'<div class="flex items-center justify-center rounded-lg bg-node-component-surface p-8"><story /></div>'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Randomize: Story = {
|
||||
args: { mode: 'randomize', variant: 'badge' }
|
||||
}
|
||||
|
||||
export const Fixed: Story = {
|
||||
args: { mode: 'fixed', variant: 'badge' }
|
||||
}
|
||||
|
||||
export const Increment: Story = {
|
||||
args: { mode: 'increment', variant: 'badge' }
|
||||
}
|
||||
|
||||
export const Decrement: Story = {
|
||||
args: { mode: 'decrement', variant: 'badge' }
|
||||
}
|
||||
|
||||
export const AllModes: Story = {
|
||||
render: () => ({
|
||||
components: { ValueControlButton },
|
||||
template: `
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<p class="mb-2 text-sm text-muted-foreground">Badge</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<ValueControlButton mode="randomize" variant="badge" />
|
||||
<ValueControlButton mode="fixed" variant="badge" />
|
||||
<ValueControlButton mode="increment" variant="badge" />
|
||||
<ValueControlButton mode="decrement" variant="badge" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm text-muted-foreground">Button</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<ValueControlButton mode="randomize" variant="button" />
|
||||
<ValueControlButton mode="fixed" variant="button" />
|
||||
<ValueControlButton mode="increment" variant="button" />
|
||||
<ValueControlButton mode="decrement" variant="button" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { ControlOptions } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { mode, variant = 'badge' } = defineProps<{
|
||||
mode: ControlOptions
|
||||
variant?: 'badge' | 'button'
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const iconMap: Record<ControlOptions, string | null> = {
|
||||
fixed: 'icon-[lucide--pencil-off]',
|
||||
randomize: 'icon-[lucide--shuffle]',
|
||||
increment: null,
|
||||
decrement: null
|
||||
}
|
||||
|
||||
const textMap: Record<ControlOptions, string | null> = {
|
||||
increment: '+1',
|
||||
decrement: '-1',
|
||||
fixed: null,
|
||||
randomize: null
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="t('widgets.valueControl.' + mode)"
|
||||
:class="
|
||||
cn(
|
||||
'flex shrink-0 cursor-pointer items-center justify-center border-none focus-visible:ring-2 focus-visible:ring-primary-background focus-visible:ring-offset-1 focus-visible:outline-none',
|
||||
variant === 'badge' ? 'h-4.5 w-8 rounded-full' : 'size-6 rounded-sm',
|
||||
mode !== 'fixed'
|
||||
? 'bg-primary-background/30 hover:bg-primary-background-hover/30'
|
||||
: 'bg-transparent'
|
||||
)
|
||||
"
|
||||
>
|
||||
<i
|
||||
v-if="iconMap[mode]"
|
||||
aria-hidden="true"
|
||||
:class="
|
||||
cn(
|
||||
iconMap[mode] ?? '',
|
||||
'text-xs',
|
||||
mode === 'fixed' ? 'text-muted-foreground' : 'text-primary-background'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<span
|
||||
v-else-if="textMap[mode]"
|
||||
class="text-xs font-normal text-primary-background"
|
||||
>
|
||||
{{ textMap[mode] }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -1,14 +1,15 @@
|
||||
<script setup lang="ts" generic="T extends WidgetValue">
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue'
|
||||
import { defineAsyncComponent, ref, watch } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import Popover from '@/components/ui/Popover.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type {
|
||||
SimplifiedControlWidget,
|
||||
WidgetValue
|
||||
} from '@/types/simplifiedWidget'
|
||||
|
||||
import ValueControlButton from './ValueControlButton.vue'
|
||||
|
||||
const ValueControlPopover = defineAsyncComponent(
|
||||
() => import('./ValueControlPopover.vue')
|
||||
)
|
||||
@@ -22,19 +23,6 @@ const modelValue = defineModel<T>()
|
||||
|
||||
const controlModel = ref(props.widget.controlWidget.value)
|
||||
|
||||
const controlButtonIcon = computed(() => {
|
||||
switch (controlModel.value) {
|
||||
case 'increment':
|
||||
return 'pi pi-plus'
|
||||
case 'decrement':
|
||||
return 'pi pi-minus'
|
||||
case 'fixed':
|
||||
return 'icon-[lucide--pencil-off]'
|
||||
default:
|
||||
return 'icon-[lucide--shuffle]'
|
||||
}
|
||||
})
|
||||
|
||||
watch(controlModel, props.widget.controlWidget.update)
|
||||
</script>
|
||||
<template>
|
||||
@@ -42,15 +30,7 @@ watch(controlModel, props.widget.controlWidget.update)
|
||||
<component :is="component" v-bind="$attrs" v-model="modelValue" :widget>
|
||||
<Popover>
|
||||
<template #button>
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
class="h-4 w-7 self-center rounded-xl bg-primary-background/30 p-0 hover:bg-primary-background-hover/30"
|
||||
>
|
||||
<i
|
||||
:class="`${controlButtonIcon} w-full text-xs text-primary-background`"
|
||||
/>
|
||||
</Button>
|
||||
<ValueControlButton :mode="controlModel" class="self-center" />
|
||||
</template>
|
||||
<ValueControlPopover v-model="controlModel" />
|
||||
</Popover>
|
||||
|
||||
Reference in New Issue
Block a user