import { useEffect, useState, type CSSProperties, type InputHTMLAttributes, type PointerEvent } from "react"; import { cn } from "./cn.js"; import { Dropdown } from "./Dropdown.js"; export interface RangeControlProps extends Omit, "type" | "value" | "onChange"> { label: string; value: number; min: number; max: number; formatValue?: (value: number) => string; onChange: (value: number) => void; } export function RangeControl({ label, value, min, max, step, formatValue = String, onChange, className, ...props }: RangeControlProps) { const safeMax = max === min ? min + 1 : max; const progress = Math.max(0, Math.min(100, ((value - min) / (safeMax - min)) * 100)); const style = { "--nodedc-range-progress": `${progress}%` } as CSSProperties; return ( ); } export interface ColorFieldProps extends Omit, "type" | "value" | "onChange"> { value: string; onChange: (value: string) => void; label?: string; } type Hsv = { h: number; s: number; v: number }; const palettePresets = ["#ff2f92", "#ff6caf", "#d746ff", "#7a3cff", "#4ea4ff", "#35d7c1", "#c3ff66", "#ffffff", "#404040", "#111116"]; function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } function parseHexColor(value: string): [number, number, number] | null { const match = /^#?([0-9a-f]{6})$/i.exec(value.trim()); if (!match) return null; const hex = match[1]; return [ Number.parseInt(hex.slice(0, 2), 16), Number.parseInt(hex.slice(2, 4), 16), Number.parseInt(hex.slice(4, 6), 16), ]; } function hexToHsv(value: string): Hsv | null { const rgb = parseHexColor(value); if (!rgb) return null; const [red, green, blue] = rgb.map((channel) => channel / 255) as [number, number, number]; const max = Math.max(red, green, blue); const min = Math.min(red, green, blue); const delta = max - min; let hue = 0; if (delta) { if (max === red) hue = 60 * (((green - blue) / delta) % 6); else if (max === green) hue = 60 * ((blue - red) / delta + 2); else hue = 60 * ((red - green) / delta + 4); } return { h: (hue + 360) % 360, s: max === 0 ? 0 : (delta / max) * 100, v: max * 100 }; } function hsvToHex({ h, s, v }: Hsv): string { const hue = ((h % 360) + 360) % 360; const saturation = clamp(s, 0, 100) / 100; const value = clamp(v, 0, 100) / 100; const chroma = value * saturation; const component = (hue / 60) % 2; const match = chroma * (1 - Math.abs(component - 1)); const base = hue < 60 ? [chroma, match, 0] : hue < 120 ? [match, chroma, 0] : hue < 180 ? [0, chroma, match] : hue < 240 ? [0, match, chroma] : hue < 300 ? [match, 0, chroma] : [chroma, 0, match]; const offset = value - chroma; return `#${base.map((channel) => Math.round((channel + offset) * 255).toString(16).padStart(2, "0")).join("")}`; } export function ColorField({ value, onChange, label = "Цвет", className, disabled = false, ...props }: ColorFieldProps) { const validColor = hexToHsv(value); const [hsv, setHsv] = useState(() => validColor ?? { h: 330, s: 82, v: 100 }); const style = { "--nodedc-color-value": validColor ? hsvToHex(validColor) : hsvToHex(hsv) } as CSSProperties; useEffect(() => { if (validColor) setHsv(validColor); }, [value, validColor?.h, validColor?.s, validColor?.v]); const commit = (next: Hsv) => { const normalized = { h: clamp(next.h, 0, 360), s: clamp(next.s, 0, 100), v: clamp(next.v, 0, 100) }; setHsv(normalized); onChange(hsvToHex(normalized)); }; const updateSpectrum = (event: PointerEvent) => { const rect = event.currentTarget.getBoundingClientRect(); commit({ ...hsv, s: ((event.clientX - rect.left) / rect.width) * 100, v: 100 - ((event.clientY - rect.top) / rect.height) * 100 }); }; return (
( )} >
{ if (disabled) return; event.currentTarget.setPointerCapture(event.pointerId); updateSpectrum(event); }} onPointerMove={(event) => { if (event.currentTarget.hasPointerCapture(event.pointerId)) updateSpectrum(event); }} onPointerUp={(event) => { if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); }} onKeyDown={(event) => { const delta = event.shiftKey ? 10 : 2; if (event.key === "ArrowLeft") { event.preventDefault(); commit({ ...hsv, s: hsv.s - delta }); } if (event.key === "ArrowRight") { event.preventDefault(); commit({ ...hsv, s: hsv.s + delta }); } if (event.key === "ArrowDown") { event.preventDefault(); commit({ ...hsv, v: hsv.v - delta }); } if (event.key === "ArrowUp") { event.preventDefault(); commit({ ...hsv, v: hsv.v + delta }); } }} >
{palettePresets.map((preset) => (
onChange(event.target.value)} {...props} />
); }