/** * Property inspector panel (direct-manipulation primitive). * * Click an object on any canvas → this panel shows its editable properties; * every change calls back immediately so the canvas re-renders live (WYSIWYG). * Field types: text, number, slider, select, toggle, readonly. */ import type { ReactNode } from 'react'; import Box from '@mui/material/Box'; import MenuItem from '@mui/material/MenuItem'; import Slider from '@mui/material/Slider'; import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { InfoDot } from '../help/InfoTip'; import { tokens } from '../../theme'; export type InspectorField = | { type: 'text'; key: string; label: string; value: string; help?: string } | { type: 'number'; key: string; label: string; value: number; min?: number; max?: number; step?: number; help?: string } | { type: 'slider'; key: string; label: string; value: number; min: number; max: number; step?: number; unit?: string; help?: string } | { type: 'select'; key: string; label: string; value: string; options: Array<{ value: string; label: string }>; help?: string } | { type: 'toggle'; key: string; label: string; value: boolean; help?: string } | { type: 'readonly'; key: string; label: string; value: ReactNode; help?: string }; interface InspectorPanelProps { title: string; subtitle?: string; accent?: string; fields: InspectorField[]; onChange: (key: string, value: string | number | boolean) => void; footer?: ReactNode; emptyHint?: string; } function FieldLabel({ label, help }: { label: string; help?: string }) { return ( {label} {help ? : null} ); } export default function InspectorPanel({ title, subtitle, accent, fields, onChange, footer, emptyHint }: InspectorPanelProps) { return ( {title} {subtitle ? ( {subtitle} ) : null} {fields.length === 0 && emptyHint ? ( {emptyHint} ) : null} {fields.map((f) => { switch (f.type) { case 'text': return ( onChange(f.key, e.target.value)} /> ); case 'number': return ( { const v = Number(e.target.value); if (!Number.isNaN(v)) onChange(f.key, v); }} /> ); case 'slider': return ( {f.value} {f.unit ?? ''} onChange(f.key, v as number)} sx={{ py: 0.5 }} /> ); case 'select': return ( onChange(f.key, e.target.value)}> {f.options.map((o) => ( {o.label} ))} ); case 'toggle': return ( onChange(f.key, e.target.checked)} /> ); case 'readonly': return ( {f.value} ); default: return null; } })} {footer} ); }