import { useState, useRef, useEffect, useMemo } from 'react' import { Braces, X } from 'lucide-react' import { cn } from '@/lib/utils' import { useDocumentStore } from '@/stores/document-store' import { isVariableRef, resolveVariableRef, getDefaultTheme } from '@/variables/resolve-variables' import type { VariableDefinition } from '@/types/variables' 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 default function VariablePicker({ type, currentValue, onBind, onUnbind, className, }: VariablePickerProps) { const [open, setOpen] = useState(false) const popoverRef = useRef(null) const variables = useDocumentStore((s) => s.document.variables) const themes = useDocumentStore((s) => s.document.themes) const isBound = typeof currentValue === 'string' && isVariableRef(currentValue) const boundName = isBound ? (currentValue as string).slice(1) : null // Filter variables by matching type const matchingVars = useMemo(() => { if (!variables) return [] return Object.entries(variables) .filter(([, def]) => def.type === type) .sort(([a], [b]) => a.localeCompare(b)) }, [variables, type]) // Close on outside click 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) // Show first theme value 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 defined
) : ( matchingVars.map(([name, def]) => ( )) )}
)}
) }