mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
Compare commits
1 Commits
pr-10303
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a6531101c |
@@ -46,6 +46,9 @@ const config: KnipConfig = {
|
|||||||
'.github/workflows/ci-oss-assets-validation.yaml',
|
'.github/workflows/ci-oss-assets-validation.yaml',
|
||||||
// Pending integration in stacked PR
|
// Pending integration in stacked PR
|
||||||
'src/components/sidebar/tabs/nodeLibrary/CustomNodesPanel.vue',
|
'src/components/sidebar/tabs/nodeLibrary/CustomNodesPanel.vue',
|
||||||
|
// Pending integration in stacked PR (#9647)
|
||||||
|
'src/components/ui/color-picker/ColorPickerSaturationValue.vue',
|
||||||
|
'src/components/ui/color-picker/ColorPickerSlider.vue',
|
||||||
// Agent review check config, not part of the build
|
// Agent review check config, not part of the build
|
||||||
'.agents/checks/eslint.strict.config.js'
|
'.agents/checks/eslint.strict.config.js'
|
||||||
],
|
],
|
||||||
|
|||||||
101
src/components/ui/color-picker/ColorPickerSaturationValue.vue
Normal file
101
src/components/ui/color-picker/ColorPickerSaturationValue.vue
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const { hue } = defineProps<{
|
||||||
|
hue: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const saturation = defineModel<number>('saturation', { required: true })
|
||||||
|
const value = defineModel<number>('value', { required: true })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const hueBackground = computed(() => `hsl(${hue}, 100%, 50%)`)
|
||||||
|
|
||||||
|
const handleStyle = computed(() => ({
|
||||||
|
left: `${saturation.value}%`,
|
||||||
|
top: `${100 - value.value}%`
|
||||||
|
}))
|
||||||
|
|
||||||
|
function clamp(v: number, min: number, max: number) {
|
||||||
|
return Math.max(min, Math.min(max, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromPointer(e: PointerEvent) {
|
||||||
|
const el = containerRef.value
|
||||||
|
if (!el) return
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||||
|
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height))
|
||||||
|
saturation.value = Math.round(x * 100)
|
||||||
|
value.value = Math.round((1 - y) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDown(e: PointerEvent) {
|
||||||
|
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
||||||
|
updateFromPointer(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(e: PointerEvent) {
|
||||||
|
if (!(e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) return
|
||||||
|
updateFromPointer(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
const step = e.shiftKey ? 10 : 1
|
||||||
|
let handled = true
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
saturation.value = clamp(saturation.value - step, 0, 100)
|
||||||
|
break
|
||||||
|
case 'ArrowRight':
|
||||||
|
saturation.value = clamp(saturation.value + step, 0, 100)
|
||||||
|
break
|
||||||
|
case 'ArrowUp':
|
||||||
|
value.value = clamp(value.value + step, 0, 100)
|
||||||
|
break
|
||||||
|
case 'ArrowDown':
|
||||||
|
value.value = clamp(value.value - step, 0, 100)
|
||||||
|
break
|
||||||
|
case 'Home':
|
||||||
|
saturation.value = 0
|
||||||
|
value.value = 0
|
||||||
|
break
|
||||||
|
case 'End':
|
||||||
|
saturation.value = 100
|
||||||
|
value.value = 100
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
handled = false
|
||||||
|
}
|
||||||
|
if (handled) e.preventDefault()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
role="group"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-label="t('colorPicker.saturationAndBrightness')"
|
||||||
|
class="relative aspect-square w-full cursor-crosshair rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-highlight"
|
||||||
|
:style="{ backgroundColor: hueBackground, touchAction: 'none' }"
|
||||||
|
@pointerdown="handlePointerDown"
|
||||||
|
@pointermove="handlePointerMove"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 rounded-sm bg-linear-to-r from-white to-transparent"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 rounded-sm bg-linear-to-b from-transparent to-black"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute size-3.5 -translate-1/2 rounded-full border-2 border-white shadow-[0_0_2px_rgba(0,0,0,0.6)]"
|
||||||
|
:style="handleStyle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
130
src/components/ui/color-picker/ColorPickerSlider.vue
Normal file
130
src/components/ui/color-picker/ColorPickerSlider.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { hsbToRgb, rgbToHex } from '@/utils/colorUtil'
|
||||||
|
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
hue = 0,
|
||||||
|
saturation = 100,
|
||||||
|
brightness = 100
|
||||||
|
} = defineProps<{
|
||||||
|
type: 'hue' | 'alpha'
|
||||||
|
hue?: number
|
||||||
|
saturation?: number
|
||||||
|
brightness?: number
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modelValue = defineModel<number>({ required: true })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const max = computed(() => (type === 'hue' ? 360 : 100))
|
||||||
|
|
||||||
|
const fraction = computed(() => modelValue.value / max.value)
|
||||||
|
|
||||||
|
const trackBackground = computed(() => {
|
||||||
|
if (type === 'hue') {
|
||||||
|
return 'linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)'
|
||||||
|
}
|
||||||
|
const rgb = hsbToRgb({ h: hue, s: saturation, b: brightness })
|
||||||
|
const hex = rgbToHex(rgb)
|
||||||
|
return `linear-gradient(to right, transparent, ${hex})`
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerStyle = computed(() => {
|
||||||
|
if (type === 'alpha') {
|
||||||
|
return {
|
||||||
|
backgroundImage:
|
||||||
|
'repeating-conic-gradient(#808080 0% 25%, transparent 0% 50%)',
|
||||||
|
backgroundSize: '8px 8px',
|
||||||
|
touchAction: 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
background: trackBackground.value,
|
||||||
|
touchAction: 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ariaLabel = computed(() =>
|
||||||
|
type === 'hue' ? t('colorPicker.hue') : t('colorPicker.alpha')
|
||||||
|
)
|
||||||
|
|
||||||
|
const ariaValueText = computed(() =>
|
||||||
|
type === 'hue' ? `${modelValue.value}°` : `${modelValue.value}%`
|
||||||
|
)
|
||||||
|
|
||||||
|
function clamp(v: number, min: number, maxVal: number) {
|
||||||
|
return Math.max(min, Math.min(maxVal, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFromPointer(e: PointerEvent) {
|
||||||
|
const el = e.currentTarget as HTMLElement
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
|
||||||
|
modelValue.value = Math.round(x * max.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDown(e: PointerEvent) {
|
||||||
|
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
|
||||||
|
updateFromPointer(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerMove(e: PointerEvent) {
|
||||||
|
if (!(e.currentTarget as HTMLElement).hasPointerCapture(e.pointerId)) return
|
||||||
|
updateFromPointer(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
const step = e.shiftKey ? 10 : 1
|
||||||
|
let handled = true
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'ArrowDown':
|
||||||
|
modelValue.value = clamp(modelValue.value - step, 0, max.value)
|
||||||
|
break
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'ArrowUp':
|
||||||
|
modelValue.value = clamp(modelValue.value + step, 0, max.value)
|
||||||
|
break
|
||||||
|
case 'Home':
|
||||||
|
modelValue.value = 0
|
||||||
|
break
|
||||||
|
case 'End':
|
||||||
|
modelValue.value = max.value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
handled = false
|
||||||
|
}
|
||||||
|
if (handled) e.preventDefault()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
role="slider"
|
||||||
|
tabindex="0"
|
||||||
|
:aria-label="ariaLabel"
|
||||||
|
:aria-valuenow="modelValue"
|
||||||
|
:aria-valuemin="0"
|
||||||
|
:aria-valuemax="max"
|
||||||
|
:aria-valuetext="ariaValueText"
|
||||||
|
class="relative flex h-4 cursor-pointer items-center rounded-full p-px outline-none focus-visible:ring-2 focus-visible:ring-highlight"
|
||||||
|
:style="containerStyle"
|
||||||
|
@pointerdown="handlePointerDown"
|
||||||
|
@pointermove="handlePointerMove"
|
||||||
|
@keydown="handleKeydown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="type === 'alpha'"
|
||||||
|
class="absolute inset-0 rounded-full"
|
||||||
|
:style="{ background: trackBackground }"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute aspect-square h-full -translate-x-1/2 rounded-full border-2 border-white shadow-[0_0_2px_rgba(0,0,0,0.6)]"
|
||||||
|
:style="{ left: `${fraction * 100}%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -468,6 +468,11 @@
|
|||||||
"issueReport": {
|
"issueReport": {
|
||||||
"helpFix": "Help Fix This"
|
"helpFix": "Help Fix This"
|
||||||
},
|
},
|
||||||
|
"colorPicker": {
|
||||||
|
"hue": "Hue",
|
||||||
|
"alpha": "Alpha",
|
||||||
|
"saturationAndBrightness": "Color saturation and brightness"
|
||||||
|
},
|
||||||
"color": {
|
"color": {
|
||||||
"noColor": "No Color",
|
"noColor": "No Color",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
|
|||||||
Reference in New Issue
Block a user