mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 00:10:40 +00:00
<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>
197 lines
4.5 KiB
TypeScript
197 lines
4.5 KiB
TypeScript
import type { ChartData, ChartOptions, ChartType } from 'chart.js'
|
|
import {
|
|
BarController,
|
|
BarElement,
|
|
CategoryScale,
|
|
Chart,
|
|
Filler,
|
|
Legend,
|
|
LinearScale,
|
|
LineController,
|
|
LineElement,
|
|
PointElement,
|
|
Tooltip
|
|
} from 'chart.js'
|
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
import type { Ref } from 'vue'
|
|
|
|
Chart.register(
|
|
BarController,
|
|
BarElement,
|
|
CategoryScale,
|
|
Filler,
|
|
Legend,
|
|
LinearScale,
|
|
LineController,
|
|
LineElement,
|
|
PointElement,
|
|
Tooltip
|
|
)
|
|
|
|
function getCssVar(name: string): string {
|
|
return getComputedStyle(document.documentElement)
|
|
.getPropertyValue(name)
|
|
.trim()
|
|
}
|
|
|
|
function getDefaultOptions(type: ChartType): ChartOptions {
|
|
const foreground = getCssVar('--color-base-foreground') || '#ffffff'
|
|
const muted = getCssVar('--color-muted-foreground') || '#8a8a8a'
|
|
|
|
return {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
align: 'start',
|
|
labels: {
|
|
color: foreground,
|
|
usePointStyle: true,
|
|
pointStyle: 'circle',
|
|
boxWidth: 8,
|
|
boxHeight: 8,
|
|
padding: 16,
|
|
font: { family: 'Inter', size: 11 },
|
|
generateLabels(chart) {
|
|
const datasets = chart.data.datasets
|
|
return datasets.map((dataset, i) => {
|
|
const color =
|
|
(dataset as { borderColor?: string }).borderColor ??
|
|
(dataset as { backgroundColor?: string }).backgroundColor ??
|
|
'#888'
|
|
return {
|
|
text: dataset.label ?? '',
|
|
fillStyle: color as string,
|
|
strokeStyle: color as string,
|
|
lineWidth: 0,
|
|
pointStyle: 'circle' as const,
|
|
hidden: !chart.isDatasetVisible(i),
|
|
datasetIndex: i
|
|
}
|
|
})
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
enabled: true
|
|
}
|
|
},
|
|
elements: {
|
|
point: {
|
|
radius: 0,
|
|
hoverRadius: 4
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: {
|
|
color: muted,
|
|
font: { family: 'Inter', size: 11 },
|
|
padding: 8
|
|
},
|
|
grid: {
|
|
display: true,
|
|
color: muted + '33',
|
|
drawTicks: false
|
|
},
|
|
border: { display: true, color: muted }
|
|
},
|
|
y: {
|
|
ticks: {
|
|
color: muted,
|
|
font: { family: 'Inter', size: 11 },
|
|
padding: 4
|
|
},
|
|
grid: {
|
|
display: false,
|
|
drawTicks: false
|
|
},
|
|
border: { display: true, color: muted }
|
|
}
|
|
},
|
|
...(type === 'bar' && {
|
|
datasets: {
|
|
bar: {
|
|
borderRadius: { topLeft: 4, topRight: 4 },
|
|
borderSkipped: false,
|
|
barPercentage: 0.6,
|
|
categoryPercentage: 0.8
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
export function useChart(
|
|
canvasRef: Ref<HTMLCanvasElement | null>,
|
|
type: Ref<ChartType>,
|
|
data: Ref<ChartData>,
|
|
options?: Ref<ChartOptions | undefined>
|
|
) {
|
|
const chartInstance = ref<Chart | null>(null)
|
|
|
|
function createChart() {
|
|
if (!canvasRef.value) return
|
|
|
|
chartInstance.value?.destroy()
|
|
|
|
const defaults = getDefaultOptions(type.value)
|
|
const merged = options?.value
|
|
? deepMerge(defaults, options.value)
|
|
: defaults
|
|
|
|
chartInstance.value = new Chart(canvasRef.value, {
|
|
type: type.value,
|
|
data: data.value,
|
|
options: merged
|
|
})
|
|
}
|
|
|
|
onMounted(createChart)
|
|
|
|
watch([type, data, options ?? ref(undefined)], () => {
|
|
if (chartInstance.value) {
|
|
chartInstance.value.data = data.value
|
|
chartInstance.value.options = options?.value
|
|
? deepMerge(getDefaultOptions(type.value), options.value)
|
|
: getDefaultOptions(type.value)
|
|
chartInstance.value.update()
|
|
}
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
chartInstance.value?.destroy()
|
|
chartInstance.value = null
|
|
})
|
|
|
|
return { chartInstance }
|
|
}
|
|
|
|
function deepMerge<T extends Record<string, unknown>>(
|
|
target: T,
|
|
source: Record<string, unknown>
|
|
): T {
|
|
const result = { ...target } as Record<string, unknown>
|
|
for (const key of Object.keys(source)) {
|
|
const srcVal = source[key]
|
|
const tgtVal = result[key]
|
|
if (
|
|
srcVal &&
|
|
typeof srcVal === 'object' &&
|
|
!Array.isArray(srcVal) &&
|
|
tgtVal &&
|
|
typeof tgtVal === 'object' &&
|
|
!Array.isArray(tgtVal)
|
|
) {
|
|
result[key] = deepMerge(
|
|
tgtVal as Record<string, unknown>,
|
|
srcVal as Record<string, unknown>
|
|
)
|
|
} else {
|
|
result[key] = srcVal
|
|
}
|
|
}
|
|
return result as T
|
|
}
|