import { useState, useRef, useEffect, useMemo } from 'react'; import { Braces, X } from 'lucide-react'; import { cn } from '../ui-primitives.js'; import { useDocument } from '../hooks/use-document.js'; import { isVariableRef, resolveVariableRef, getDefaultTheme } from '@zseven-w/pen-core'; import type { VariableDefinition } from '@zseven-w/pen-types'; export interface VariablePickerProps { /** Variable type to filter by */ type: 'color' | 'number' | 'string'; /** Current value — if it starts with '$', it's a variable reference */ currentValue?: string | number; /** Called when a variable is selected — value will be '$variableName' */ onBind: (ref: string) => void; /** Called when the variable binding is removed — should set the resolved concrete value */ onUnbind: (resolvedValue: string | number) => void; className?: string; } export function VariablePicker({ type, currentValue, onBind, onUnbind, className, }: VariablePickerProps) { const [open, setOpen] = useState(false); const popoverRef = useRef(null); const doc = useDocument(); const variables = doc.variables; const themes = doc.themes; const isBound = typeof currentValue === 'string' && isVariableRef(currentValue); const boundName = isBound ? (currentValue as string).slice(1) : null; const matchingVars = useMemo(() => { if (!variables) return []; return Object.entries(variables) .filter(([, def]) => def.type === type) .sort(([a], [b]) => a.localeCompare(b)); }, [variables, type]); useEffect(() => { if (!open) return; const handler = (e: MouseEvent) => { if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, [open]); const handleBind = (name: string) => { onBind(`$${name}`); setOpen(false); }; const handleUnbind = () => { if (!boundName || !variables?.[boundName]) return; const activeTheme = getDefaultTheme(themes); const resolved = resolveVariableRef(`$${boundName}`, variables, activeTheme); const fallback: string | number = type === 'color' ? '#000000' : type === 'number' ? 0 : ''; const val = resolved != null && typeof resolved !== 'boolean' ? resolved : fallback; onUnbind(val); setOpen(false); }; const getPreview = (def: VariableDefinition): string => { const val = def.value; if (!Array.isArray(val)) return String(val); return val[0]?.value != null ? String(val[0].value) : ''; }; if (matchingVars.length === 0 && !isBound) return null; return (
{isBound ? ( ) : ( )} {open && (
{isBound && ( <>
)} {matchingVars.length === 0 ? (
No {type} variables
) : ( matchingVars.map(([name, def]) => ( )) )}
)}
); }