import React, { useState } from 'react'; import { Move, AlignLeft, AlignCenter, AlignRight, ChevronDown, ChevronRight, Palette } from 'lucide-react'; import { AlignmentTools } from './AlignmentTools'; import { FontPicker } from './FontPicker'; import { DesignMD } from '../../types'; /* ─── Types ─── */ export interface FieldPosition { x: number; y: number; w: number; h: number; } export interface FieldTextStyle { fontSize?: number; fontWeight?: number; fontFamily?: string; color?: string; textAlign?: 'left' | 'center' | 'right'; opacity?: number; useBrandStyle?: boolean; textRole?: 'title' | 'subtitle' | 'paragraph'; } /** Resolve brand typography values for a given role */ export function resolveBrandRole(designMD: DesignMD, role: 'title' | 'subtitle' | 'paragraph') { switch (role) { case 'title': return { fontFamily: designMD.titleFont || designMD.baseFont, fontSize: designMD.titleSize || 48, fontWeight: 700, color: designMD.titleColor || designMD.textColor, }; case 'subtitle': return { fontFamily: designMD.subtitleFont || designMD.baseFont, fontSize: designMD.subtitleSize || 32, fontWeight: 600, color: designMD.subtitleColor || designMD.textColor, }; case 'paragraph': return { fontFamily: designMD.paragraphFont || designMD.baseFont, fontSize: designMD.paragraphSize || 18, fontWeight: 400, color: designMD.paragraphColor || designMD.textColor, }; } } interface FieldInspectorProps { /** Current position (0-100 %) */ position: FieldPosition; onPositionChange: (pos: Partial) => void; /** Text style (only rendered when provided) */ textStyle?: FieldTextStyle; onTextStyleChange?: (style: Partial) => void; /** Field metadata */ fieldType: 'text' | 'media' | 'logo' | 'brand-variable'; fieldLabel: string; /** Brand context for FontPicker and color palette */ brandFont?: string; brandColors?: string[]; /** Resolved brand design for typography roles */ resolvedDesignMD?: DesignMD; } /* ─── Collapsible Section ─── */ const Section: React.FC<{ title: string; icon?: React.ReactNode; defaultOpen?: boolean; children: React.ReactNode }> = ({ title, icon, defaultOpen = true, children, }) => { const [open, setOpen] = useState(defaultOpen); return (
{open && children}
); }; /** * FieldInspector — Shared property inspector for positioned canvas fields. * * Used by: * - Template Builder (SceneConfigurator) for ExpressField * - Potentially Studio (ElementPropertiesPanel) in the future * * Reuses existing shared components: AlignmentTools, FontPicker. */ export const FieldInspector: React.FC = ({ position, onPositionChange, textStyle, onTextStyleChange, fieldType, fieldLabel, brandFont, brandColors = [], resolvedDesignMD, }) => { const useBrand = textStyle?.useBrandStyle !== false; const currentRole = textStyle?.textRole || 'paragraph'; return (
{/* Header */}
{fieldLabel} {fieldType}
{/* ── Position & Size Grid ── */}
}>
{([ { key: 'x' as const, label: 'X' }, { key: 'y' as const, label: 'Y' }, { key: 'w' as const, label: 'W' }, { key: 'h' as const, label: 'H' }, ]).map(p => (
{ const val = Math.max(0, Math.min(100, parseInt(e.target.value) || 0)); onPositionChange({ [p.key]: val }); }} title={`${p.label} (${Math.round(position[p.key])}%)`} className="w-full bg-neutral-800 border border-neutral-700 rounded px-1.5 py-1 text-[10px] text-white text-center font-mono focus:border-violet-500/50 focus:outline-none" />
))}
{/* Alignment Tools */}
onPositionChange(updates as Partial)} />
{/* ── Text Styling (only for text fields) ── */} {textStyle && onTextStyleChange && (
{/* ── Typographic Role Pills ── */}
{(['title', 'subtitle', 'paragraph'] as const).map(role => { const labels = { title: 'Título', subtitle: 'Subtítulo', paragraph: 'Párrafo' }; const isActive = currentRole === role; return ( ); })}
{/* ── Text Align (always visible) ── */}
{([ { value: 'left' as const, icon: }, { value: 'center' as const, icon: }, { value: 'right' as const, icon: }, ]).map(a => ( ))}
{/* ── Brand toggle ── */}
{/* ── Brand mode preview ── */} {useBrand && resolvedDesignMD && (
{(() => { const vals = resolveBrandRole(resolvedDesignMD, currentRole); const fontName = (vals.fontFamily || 'Inter').split(',')[0].replace(/"/g, ''); return (
{fontName} · {vals.fontSize}px · {vals.fontWeight} ·
); })()}
)} {/* ── Advanced controls (only when brand is OFF) ── */} {!useBrand && (
{/* Font Size + Weight row */}
onTextStyleChange({ fontSize: parseInt(e.target.value) || 24 })} title={`Tamaño: ${textStyle.fontSize || 24}px`} className="w-full bg-neutral-800 border border-neutral-700 rounded px-1.5 py-1 text-[10px] text-white text-center font-mono focus:border-violet-500/50 focus:outline-none" />
{/* Font Picker */}
onTextStyleChange({ fontFamily: font })} brandFont={brandFont} />
{/* Color */}
onTextStyleChange({ color: e.target.value })} title="Color del texto" className="w-7 h-7 rounded cursor-pointer bg-transparent border border-neutral-700 p-0" /> {textStyle.color || '#ffffff'} {/* Brand color quick-picks */} {brandColors.length > 0 && (
{brandColors.map((c, i) => (
)}
{/* Opacity */}
{Math.round((textStyle.opacity ?? 1) * 100)}%
onTextStyleChange({ opacity: Number(e.target.value) / 100 })} title="Opacidad del texto" className="w-full h-1 bg-neutral-800 rounded-lg appearance-none cursor-pointer accent-violet-500" />
)}
)}
); };