import { useEffect, useRef, useState } from "react"; import { adjustNumericToken, parseNumericToken } from "./propertyPanelHelpers"; import { useInspectorGestureTransaction } from "./useInspectorGestureTransaction"; function arrowDirection(key: string): 1 | -1 | null { if (key === "ArrowUp") return 1; if (key === "ArrowDown") return -1; return null; } export function CommitField({ value, disabled, liveCommit, align = "left", onPreview, onCommit, }: { value: string; disabled?: boolean; liveCommit?: boolean; align?: "left" | "right"; onPreview?: (nextValue: string) => void; onCommit: (nextValue: string) => void; }) { const [draft, setDraft] = useState(value); const valueRef = useRef(value); const draftRef = useRef(draft); const inputRef = useRef(null); const focusedRef = useRef(false); const dirtyRef = useRef(false); valueRef.current = value; draftRef.current = draft; const gestureActiveRef = useRef(false); const gestureSettleTimerRef = useRef | null>(null); const gestureTransaction = useInspectorGestureTransaction({ sourceValue: value, onPreview: (nextValue) => { setDraft(nextValue); onPreview?.(nextValue); }, onCommit, }); const gestureTransactionRef = useRef(gestureTransaction); gestureTransactionRef.current = gestureTransaction; const clearGestureSettleTimer = () => { if (!gestureSettleTimerRef.current) return; clearTimeout(gestureSettleTimerRef.current); gestureSettleTimerRef.current = null; }; const settleGesture = () => { clearGestureSettleTimer(); if (!gestureActiveRef.current) return false; gestureActiveRef.current = false; gestureTransaction.settle(); return true; }; const scheduleGestureSettle = () => { clearGestureSettleTimer(); gestureSettleTimerRef.current = setTimeout(() => { gestureSettleTimerRef.current = null; if (!gestureActiveRef.current) return; gestureActiveRef.current = false; gestureTransactionRef.current.settle(); }, 250); }; const cancelGesture = () => { clearGestureSettleTimer(); gestureActiveRef.current = false; gestureTransaction.cancel(); }; const commitDraft = (nextValue: string) => { setDraft(nextValue); onPreview?.(nextValue); if (nextValue !== valueRef.current) onCommit(nextValue); }; const cancelGestureFromKeyEvent = (event: React.KeyboardEvent) => { if (!gestureActiveRef.current) return false; event.preventDefault(); event.stopPropagation(); cancelGesture(); return true; }; const previewNumericKeyStep = (event: React.KeyboardEvent) => { const direction = arrowDirection(event.key); if (direction === null) return; const nextDraft = adjustNumericToken(draftRef.current, direction, event); if (!nextDraft) return; event.preventDefault(); dirtyRef.current = false; gestureActiveRef.current = true; gestureTransaction.preview(nextDraft); scheduleGestureSettle(); }; const handleKeyDown = (event: React.KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "z") { cancelGestureFromKeyEvent(event); return; } if (event.key === "Escape") { cancelGestureFromKeyEvent(event); return; } if (event.key === "Enter") { event.currentTarget.blur(); return; } previewNumericKeyStep(event); }; useEffect(() => { if (focusedRef.current && dirtyRef.current) return; setDraft(value); }, [value]); useEffect(() => { const el = inputRef.current; if (!el) return; const handler = (event: WheelEvent) => { if (disabled || document.activeElement !== el) return; const delta = event.deltaY === 0 ? event.deltaX : event.deltaY; if (delta === 0) return; const nextDraft = adjustNumericToken(draftRef.current, delta < 0 ? 1 : -1, event); if (!nextDraft) return; event.preventDefault(); event.stopPropagation(); dirtyRef.current = false; gestureActiveRef.current = true; gestureTransactionRef.current.preview(nextDraft); scheduleGestureSettle(); }; el.addEventListener("wheel", handler, { passive: false }); return () => { el.removeEventListener("wheel", handler); clearGestureSettleTimer(); }; }, [disabled]); return ( { focusedRef.current = true; }} onChange={(event) => { settleGesture(); dirtyRef.current = true; setDraft(event.target.value); if (liveCommit) onPreview?.(event.target.value); }} onBlur={() => { if (settleGesture()) { focusedRef.current = false; return; } const wasDirty = dirtyRef.current; focusedRef.current = false; dirtyRef.current = false; if (wasDirty && (!liveCommit || parseNumericToken(draft))) { commitDraft(draft); } else { setDraft(valueRef.current); if (wasDirty && liveCommit) onPreview?.(valueRef.current); } }} onKeyDown={handleKeyDown} title={parseNumericToken(value) ? "Scroll or use Arrow keys to adjust" : undefined} className={`min-w-0 w-full bg-transparent text-[11px] font-medium text-neutral-100 outline-none disabled:cursor-not-allowed disabled:text-neutral-600 ${ align === "right" ? "text-right" : "text-left" }`} /> ); }