/** * Per-type preview-value inputs for the Variables panel. Text-like inputs * draft locally and commit on blur/Enter so the preview doesn't reload per * keystroke; discrete inputs (checkbox, select, color swatch, range) commit * immediately. */ import { useState } from "react"; import type { CompositionVariable, ColorVariable, EnumVariable, NumberVariable, } from "@hyperframes/sdk"; export const VARIABLES_INPUT_CLASS = "w-full bg-neutral-900 border border-neutral-800 rounded px-2 py-1 text-[10px] text-neutral-200 focus:outline-none focus:border-neutral-700"; /** Text input that drafts locally and commits on blur/Enter. */ function DraftTextInput({ value, onCommit, type = "text", className = VARIABLES_INPUT_CLASS, maxLength, placeholder, min, max, step, }: { value: string; onCommit: (raw: string) => void; type?: "text" | "number"; className?: string; maxLength?: number; placeholder?: string; min?: number; max?: number; step?: number; }) { const [draft, setDraft] = useState(null); return ( setDraft(e.target.value)} onBlur={() => { if (draft !== null && draft !== value) onCommit(draft); setDraft(null); }} onKeyDown={(e) => e.key === "Enter" && e.currentTarget.blur()} className={className} /> ); } function EnumControl({ decl, current, onCommit, }: { decl: EnumVariable; current: unknown; onCommit: (value: unknown) => void; }) { return ( ); } function ColorControl({ current, onCommit, }: { decl: ColorVariable; current: unknown; onCommit: (value: unknown) => void; }) { const colorValue = typeof current === "string" ? current : "#000000"; // The native picker fires change continuously while dragging the gradient; // draft locally and commit once on close (blur) — each commit reloads the // whole preview iframe. const [draft, setDraft] = useState(null); return (
setDraft(e.target.value)} onBlur={() => { if (draft !== null && draft !== colorValue) onCommit(draft); setDraft(null); }} className="h-6 w-6 cursor-pointer rounded border border-neutral-700 bg-transparent" />
); } // fallow-ignore-next-line complexity function NumberControl({ decl, current, onCommit, }: { decl: NumberVariable; current: unknown; onCommit: (value: unknown) => void; }) { const numberValue = typeof current === "number" ? current : Number(current) || 0; const hasRange = decl.min !== undefined && decl.max !== undefined; // Drag ticks stay local; commit once on release — each commit reloads the // whole preview iframe, so per-tick commits would thrash it. const [dragValue, setDragValue] = useState(null); const commitDrag = () => { if (dragValue !== null && dragValue !== numberValue) onCommit(dragValue); setDragValue(null); }; const commitRaw = (raw: string) => { const n = Number(raw); onCommit(Number.isFinite(n) ? n : raw); }; return (
{hasRange && ( setDragValue(Number(e.target.value))} onPointerUp={commitDrag} onKeyUp={commitDrag} onBlur={commitDrag} className="flex-1" /> )} {decl.unit && {decl.unit}}
); } // Per-type dispatcher — one branch per variable type, same shape as BlockParamsPanel. // fallow-ignore-next-line complexity export function PreviewValueControl({ decl, value, onCommit, }: { decl: CompositionVariable; value: unknown; onCommit: (value: unknown) => void; }) { const current = value === undefined ? decl.default : value; switch (decl.type) { case "boolean": return ( onCommit(e.target.checked)} className="h-3.5 w-3.5 accent-neutral-400" /> ); case "enum": return ; case "color": return ; case "number": return ; default: { // string / font (family name) / image (URL) — plain text input for v1. const textValue = typeof current === "string" ? current : JSON.stringify(current); return ( ); } } }