import React, { useEffect, useRef, useState } from 'react'; import { SilkeAutocompleteField } from '../silke-autocomplete-field'; import { useSilkeCSSContext } from '../silke-css-number-field'; import { SilkeBox } from '../silke-box'; import { SilkeAutocompletePopup } from '../silke-autocomplete-field/silke-autocomplete-popup'; import { SilkeTextMicro, SilkeTextSmall } from '../silke-text'; import { SilkeButton } from '../silke-button'; import styles from './silke-variables-wrapper.scss'; import { useTextFieldContext } from '../silke-text-field/text-field-context'; import { getCSSNumberFormat, getRelevantVariables, getVariableDisplayText, getVariableIcon, getVariableUnit, getVariableValue, LocalVariable, } from '@vev/utils'; import { SilkeIcon } from '../silke-icon'; import { capitalize } from 'lodash'; import { SilkeVariablesWrapperForm } from './silke-variables-wrapper-form'; import { SilkeTooltip } from '../silke-tooltip'; export type SilkeVariablesWrapperProps = { component?: T; value: string; formats?: string[]; variableType?: string; fullWidthWrapper?: boolean; templateVariable?: boolean; onChange: (value: string) => void; // return key? onVariableAdd?: (variable: LocalVariable) => Promise; onVariableEdit?: (key: string) => void; } & React.ComponentProps; const DROPDOWN_WIDTH = 189; const SUPPORTED_TYPES = ['text', 'number', 'color', 'image', 'video', 'font']; export function SilkeVariablesWrapper({ component, formats, variableType, fullWidthWrapper, templateVariable, onVariableAdd, onVariableEdit, ...rest }: SilkeVariablesWrapperProps) { const [openVariables, setOpenVariables] = useState(false); const [showAddNewForm, setShowAddNewForm] = useState(false); const [hovering, setHovering] = useState(false); if (!formats) formats = []; const rootRef = useRef(null); const context = useSilkeCSSContext(); const textFieldContext = useTextFieldContext(); const [hoveredItem, setHoveredItem] = useState(null); const filteredVariables = React.useMemo(() => { return getRelevantVariables(context.variablesv2, formats, variableType); }, [context.variablesv2, formats, variableType]); const variableItems = React.useMemo(() => { return filteredVariables.map((v) => ({ label: v.name, value: v.key, subLabel: v.type === 'font' ? context.getFontName?.(v.value || '') || '' : `${v.value}${v.unit ? `${v.unit}` : ''}`, icon: getVariableIcon(v.type), })); }, [filteredVariables]); const isVariable = typeof rest.value === 'string' && (rest.value?.startsWith('var(--vev-') || rest.value?.startsWith('{{')); const currentVariable = React.useMemo( () => context.variablesv2.find((v) => typeof rest.value === 'string' && rest.value?.includes(v.key)), [context.variablesv2, rest.value], ); const supportedType = !variableType || SUPPORTED_TYPES.includes(variableType || ''); const displayValue = isVariable ? getVariableDisplayText(variableType, currentVariable, rest.value) : rest.value; const hasLabelTop = rest.label && rest.inline !== 'label-inside' && textFieldContext.inline !== 'label-inside'; const offsetPx = rest.size === 's' || textFieldContext.size === 's' ? 16 : 18; useEffect(() => { const el = rootRef.current; if (el) { let timeout = 0; const handleMouseOver = () => { clearTimeout(timeout); timeout = self.setTimeout(() => setHovering(true), 300); }; const handleMouseOut = () => { clearTimeout(timeout); setHovering(false); }; el.addEventListener('mouseover', handleMouseOver, { passive: true }); el.addEventListener('mouseleave', handleMouseOut, { passive: true }); return () => { el.removeEventListener('mouseover', handleMouseOver); el.removeEventListener('mouseleave', handleMouseOver); }; } }, []); const handleVariablesDotClicked = (e: React.MouseEvent) => { e.preventDefault(); // If active variable, remove link and set to native value if (isVariable && currentVariable) { const variable = context.variablesv2.find((v) => rest.value?.includes(v.key)); const wasGradient = variable?.type === 'color' && variable.value?.includes('gradient'); if (wasGradient) { rest.onChange('000'); } else { rest.onChange(variable ? `${variable.value}${variable.unit || ''}` : '0'); } return; } if (filteredVariables.length) { setOpenVariables(true); } else { setShowAddNewForm(true); } }; const onShowAddForm = () => { setOpenVariables(false); setShowAddNewForm(true); }; const onHideAddForm = () => { setShowAddNewForm(false); setOpenVariables(true); }; const handleVariableChange = (key: string) => { // Tracking (if implemented) const variable = filteredVariables.find((v) => v.key === key); context?.onVariableAttached?.(variable?.type || ''); // Change the value return variableType === 'text' || templateVariable ? rest.onChange(`{{${key}}}`) : rest.onChange(`var(--vev-${key})`); }; const handleVariableAdd = async ({ variableName }: { variableName: string }) => { const newVariable = { type: variableType, name: variableName ? variableName : capitalize(variableType), value: isVariable ? getVariableValue(rest.value, context.variablesv2) || '0' : rest.value, unit: '', }; if (variableType === 'number') { newVariable.value = isVariable ? getVariableValue(rest.value, context.variablesv2) || '0' : String(parseFloat(rest.value || '0')); newVariable.unit = isVariable ? getVariableUnit(rest.value, context.variablesv2) || '' : getCSSNumberFormat(rest.value); } const newKey = await onVariableAdd?.(newVariable); if (newKey) { handleVariableChange(newKey); } setShowAddNewForm(false); }; const disabled = 'disabled' in rest ? rest.disabled : false; if (disabled) return React.createElement(component, { ...rest, value: displayValue, formats }); const itemRender = (item: any) => ( setHoveredItem(item.value)} onMouseLeave={() => setHoveredItem(null)} fill > {variableType !== 'color' && } {item.label} {item.subLabel} {hoveredItem === item.value && onVariableEdit && ( { e.preventDefault; e.stopPropagation(); onVariableEdit?.(item.value); }} /> )} ); return (
{supportedType && (
)} {hovering && isVariable && currentVariable && ( {currentVariable?.name} )} {isVariable && currentVariable ? ( itemRender(item)} value={displayValue} selectedVariableKey={currentVariable?.key} dropDownMinWidth={DROPDOWN_WIDTH} dropDownMaxWidth={DROPDOWN_WIDTH} width={rest.width} onChange={handleVariableChange} hideListArrow disableAddFromList /> ) : (
{component && React.createElement(component, { ...rest, value: displayValue, formats })}
)} {openVariables && ( itemRender(item)} dropDownMinWidth={DROPDOWN_WIDTH} dropDownMaxWidth={DROPDOWN_WIDTH} items={variableItems} value={currentVariable?.name} onAdd={onShowAddForm} onChange={handleVariableChange} onRequestClose={() => setOpenVariables(false)} showAddIcon={!!onVariableAdd} searchPlaceholder="Search variables" disableAddFromList /> )} {showAddNewForm && ( )}
); } SilkeVariablesWrapper.displayName = 'SilkeVariablesWrapper';