import { useEffect, useRef, useState } from 'react'; import { Input } from '@/components/ui/input'; /** * A colour, chosen without leaving the panel. * * ── Why not `` ── * * It was one, and the browser's own picker is a window-level popup positioned by the browser. In a * panel near the right edge it opened off the side of the window and was simply unreachable — nothing * in CSS can move it, because it is not in the document. * * ── What replaces it ── * * Swatches and a hex field, in a panel anchored to the RIGHT of its trigger so it grows inward. Text * over a photograph is white or black almost every time; the field is there for the times it is not. * * Deliberately not a gradient area and a hue strip. That is what the native control already does well, * and rebuilding it badly to fix a positioning problem would be a worse control than the one being * replaced. */ /** Enough to cover a photograph, and short enough to read as choices rather than as a palette. */ const PRESETS = [ '#ffffff', '#f5f5f5', '#cccccc', '#888888', '#333333', '#000000', '#f0c808', '#e05a3a', ]; export const ColorField = ({ value, onChange }: { value: string; onChange: (color: string) => void }) => { const [open, setOpen] = useState(false); const [draft, setDraft] = useState(value); const wrapper = useRef(null); // Follows the value when it changes elsewhere — a different block being selected, or a load. useEffect(() => setDraft(value), [value]); /* Close on a press anywhere else. `pointerdown` rather than `click`, so it closes on the way down and a press meant for something behind the panel does not land twice. Capture, because a control inside a panel below may stop propagation for its own reasons. */ useEffect(() => { if (!open) return; const onDown = (event: PointerEvent) => { if (!wrapper.current?.contains(event.target as Node)) setOpen(false); }; document.addEventListener('pointerdown', onDown, true); return () => document.removeEventListener('pointerdown', onDown, true); }, [open]); const commit = (next: string) => { const trimmed = next.trim(); const full = /^#?[0-9a-fA-F]{6}$/.test(trimmed) ? `#${trimmed.replace('#', '')}`.toLowerCase() : null; if (full) onChange(full); }; return (
{open && (
{PRESETS.map((preset) => (
setDraft(event.currentTarget.value)} onBlur={() => commit(draft)} onKeyDown={(event) => { if (event.key === 'Enter') { commit(draft); setOpen(false); } }} placeholder="#ffffff" className="mt-2 h-7 font-mono text-xs" />
)}
); };