mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 05:02:17 +00:00
feat(ui): Implement experimental UI layer with versioned architecture
- Reorganize components into v1/v2 versioned structure - Add common components for shared UI elements - Introduce composables for reusable logic - Restructure views into v1/v2 directories - Remove old component structure in favor of versioned approach - Update router and UI store for new architecture
This commit is contained in:
204
ComfyUI_vibe/src/components/v2/nodes/widgets/WidgetSlider.vue
Normal file
204
ComfyUI_vibe/src/components/v2/nodes/widgets/WidgetSlider.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { WidgetDefinition } from '@/types/node'
|
||||
|
||||
interface Props {
|
||||
widget: WidgetDefinition<number>
|
||||
modelValue: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
const localValue = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
localValue.value = newVal
|
||||
}
|
||||
)
|
||||
|
||||
const min = computed(() => props.widget.options?.min ?? 0)
|
||||
const max = computed(() => props.widget.options?.max ?? 100)
|
||||
const step = computed(() => props.widget.options?.step ?? 1)
|
||||
const precision = computed(() => props.widget.options?.precision ?? 0)
|
||||
const disabled = computed(() => props.widget.options?.disabled ?? false)
|
||||
|
||||
const percentage = computed(() => {
|
||||
const range = max.value - min.value
|
||||
if (range === 0) return 0
|
||||
return ((localValue.value - min.value) / range) * 100
|
||||
})
|
||||
|
||||
const displayValue = computed(() => {
|
||||
return localValue.value.toFixed(precision.value)
|
||||
})
|
||||
|
||||
function handleSliderInput(event: Event): void {
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = parseFloat(target.value)
|
||||
localValue.value = value
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
function handleNumberInput(event: Event): void {
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = parseFloat(target.value)
|
||||
if (!isNaN(value)) {
|
||||
const clampedValue = Math.min(Math.max(value, min.value), max.value)
|
||||
localValue.value = clampedValue
|
||||
emit('update:modelValue', clampedValue)
|
||||
}
|
||||
}
|
||||
|
||||
function handleNumberBlur(event: Event): void {
|
||||
const target = event.target as HTMLInputElement
|
||||
target.value = displayValue.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="widget-slider" @pointerdown.stop @mousedown.stop>
|
||||
<div class="slider-container">
|
||||
<input
|
||||
type="range"
|
||||
:value="localValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
class="custom-slider nodrag"
|
||||
:style="{ '--fill-percent': `${percentage}%` }"
|
||||
@input="handleSliderInput"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
:value="displayValue"
|
||||
:min="min"
|
||||
:max="max"
|
||||
:step="step"
|
||||
:disabled="disabled"
|
||||
class="number-input nodrag"
|
||||
@input="handleNumberInput"
|
||||
@blur="handleNumberBlur"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.widget-slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#3b82f6 0%,
|
||||
#3b82f6 var(--fill-percent),
|
||||
#3f3f46 var(--fill-percent),
|
||||
#3f3f46 100%
|
||||
);
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-slider:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.custom-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fafafa;
|
||||
border: 2px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
transition: background-color 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.custom-slider::-webkit-slider-thumb:hover {
|
||||
background: #3b82f6;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.custom-slider::-webkit-slider-thumb:active {
|
||||
cursor: grabbing;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.custom-slider::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #fafafa;
|
||||
border: 2px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
cursor: grab;
|
||||
transition: background-color 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
.custom-slider::-moz-range-thumb:hover {
|
||||
background: #3b82f6;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.custom-slider::-moz-range-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.custom-slider::-moz-range-track {
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.number-input {
|
||||
width: 56px;
|
||||
background: #27272a;
|
||||
border: 1px solid #3f3f46;
|
||||
border-radius: 6px;
|
||||
color: #fafafa;
|
||||
padding: 4px 6px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
.number-input::-webkit-outer-spin-button,
|
||||
.number-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.number-input:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.number-input:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user