diff --git a/components/admin/overview-image-generator.tsx b/components/admin/overview-image-generator.tsx index 1692cd6..0719902 100644 --- a/components/admin/overview-image-generator.tsx +++ b/components/admin/overview-image-generator.tsx @@ -88,6 +88,7 @@ import { } from "@/components/ui/popover"; import { ColorPicker, + ColorPickerEyeDropper, ColorPickerHue, ColorPickerSelection, } from "@/components/ui/color-picker"; @@ -571,7 +572,7 @@ function ColorField({ }) { const invalid = !normalizeHex(value); const handlePickerChange = useCallback( - (color: number[] | Record) => { + (color: unknown) => { if (!Array.isArray(color)) return; const hex = color .slice(0, 3) @@ -610,6 +611,7 @@ function ColorField({ > + diff --git a/components/ui/color-picker.tsx b/components/ui/color-picker.tsx index 94a84b6..8308c44 100644 --- a/components/ui/color-picker.tsx +++ b/components/ui/color-picker.tsx @@ -51,7 +51,7 @@ export const useColorPicker = () => { return context } -export type ColorPickerProps = HTMLAttributes & { +export type ColorPickerProps = Omit, "onChange"> & { value?: Parameters[0] defaultValue?: Parameters[0] onChange?: (value: Parameters[0]) => void @@ -64,41 +64,68 @@ export const ColorPicker = ({ className, ...props }: ColorPickerProps) => { - const selectedColor = Color(value) - const defaultColor = Color(defaultValue) + const selectedColor = Color(value ?? defaultValue) - const [hue, setHue] = useState(selectedColor.hue() || defaultColor.hue() || 0) - const [saturation, setSaturation] = useState( - selectedColor.saturationl() || defaultColor.saturationl() || 100, - ) - const [lightness, setLightness] = useState( - selectedColor.lightness() || defaultColor.lightness() || 50, - ) - const [alpha, setAlpha] = useState(selectedColor.alpha() * 100 || defaultColor.alpha() * 100) + const [hue, setHueState] = useState(selectedColor.hue() || 0) + const [saturation, setSaturationState] = useState(selectedColor.saturationl()) + const [lightness, setLightnessState] = useState(selectedColor.lightness()) + const [alpha, setAlphaState] = useState(selectedColor.alpha() * 100) const [mode, setMode] = useState("hex") + const channelsRef = useRef({ hue, saturation, lightness, alpha }) + + const updateColor = useCallback( + (patch: Partial) => { + const next = { ...channelsRef.current, ...patch } + channelsRef.current = next + setHueState(next.hue) + setSaturationState(next.saturation) + setLightnessState(next.lightness) + setAlphaState(next.alpha) + + if (onChange) { + const rgba = Color.hsl(next.hue, next.saturation, next.lightness) + .alpha(next.alpha / 100) + .rgb() + .array() + onChange([rgba[0], rgba[1], rgba[2], next.alpha / 100]) + } + }, + [onChange], + ) + + const setHue = useCallback((next: number) => updateColor({ hue: next }), [updateColor]) + const setSaturation = useCallback( + (next: number) => updateColor({ saturation: next }), + [updateColor], + ) + const setLightness = useCallback( + (next: number) => updateColor({ lightness: next }), + [updateColor], + ) + const setAlpha = useCallback( + (next: number) => updateColor({ alpha: next }), + [updateColor], + ) // Update color when controlled value changes useEffect(() => { if (value) { const color = Color(value).hsl() + const next = { + hue: color.hue() || 0, + saturation: color.saturationl(), + lightness: color.lightness(), + alpha: color.alpha() * 100, + } - setHue(color.hue() || 0) - setSaturation(color.saturationl()) - setLightness(color.lightness()) - setAlpha(color.alpha() * 100) + channelsRef.current = next + setHueState(next.hue) + setSaturationState(next.saturation) + setLightnessState(next.lightness) + setAlphaState(next.alpha) } }, [value]) - // Notify parent of changes - useEffect(() => { - if (onChange) { - const color = Color.hsl(hue, saturation, lightness).alpha(alpha / 100) - const rgba = color.rgb().array() - - onChange([rgba[0], rgba[1], rgba[2], alpha / 100]) - } - }, [hue, saturation, lightness, alpha, onChange]) - return ( export const ColorPickerSelection = memo(({ className, ...props }: ColorPickerSelectionProps) => { const containerRef = useRef(null) const [isDragging, setIsDragging] = useState(false) - const [positionX, setPositionX] = useState(0) - const [positionY, setPositionY] = useState(0) - const { hue, setSaturation, setLightness } = useColorPicker() + const { hue, saturation, lightness, setSaturation, setLightness } = useColorPicker() + const positionX = saturation / 100 + const topLightness = 100 - positionX * 50 + const positionY = Math.max(0, Math.min(1, 1 - lightness / topLightness)) const backgroundGradient = useMemo(() => { return `linear-gradient(0deg, rgba(0,0,0,1), rgba(0,0,0,0)), @@ -134,47 +162,38 @@ export const ColorPickerSelection = memo(({ className, ...props }: ColorPickerSe hsl(${hue}, 100%, 50%)` }, [hue]) - const handlePointerMove = useCallback( - (event: PointerEvent) => { - if (!(isDragging && containerRef.current)) { - return - } + const updateFromPointer = useCallback( + (event: { clientX: number; clientY: number }) => { + if (!containerRef.current) return const rect = containerRef.current.getBoundingClientRect() const x = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)) const y = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height)) - setPositionX(x) - setPositionY(y) setSaturation(x * 100) - const topLightness = x < 0.01 ? 100 : 50 + 50 * (1 - x) - const lightness = topLightness * (1 - y) + const nextTopLightness = 100 - x * 50 + const nextLightness = nextTopLightness * (1 - y) - setLightness(lightness) + setLightness(nextLightness) }, - [isDragging, setSaturation, setLightness], + [setSaturation, setLightness], ) - useEffect(() => { - const handlePointerUp = () => setIsDragging(false) - - if (isDragging) { - window.addEventListener("pointermove", handlePointerMove) - window.addEventListener("pointerup", handlePointerUp) - } - - return () => { - window.removeEventListener("pointermove", handlePointerMove) - window.removeEventListener("pointerup", handlePointerUp) - } - }, [isDragging, handlePointerMove]) - return (
{ - e.preventDefault() + onPointerDown={event => { + event.preventDefault() + event.currentTarget.setPointerCapture(event.pointerId) setIsDragging(true) - handlePointerMove(e.nativeEvent) + updateFromPointer(event) }} + onPointerMove={event => { + if (isDragging) updateFromPointer(event) + }} + onPointerUp={event => { + event.currentTarget.releasePointerCapture(event.pointerId) + setIsDragging(false) + }} + onPointerCancel={() => setIsDragging(false)} ref={containerRef} style={{ background: backgroundGradient, @@ -290,7 +309,12 @@ export const ColorPickerOutput = ({ className, ...props }: ColorPickerOutputProp const { mode, setMode } = useColorPicker() return ( - { + if (value) setMode(value) + }} + value={mode} + >