import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; import type { Widget, Interface } from './lib/types'; import { containingTabPane, tabLabels } from './lib/containers'; interface Props { selected: Widget | null; multiCount: number; iface: Interface; onChange: (updated: Widget) => void; onRenameId: (oldId: string, newId: string) => void; onIfaceChange: (updated: Interface) => void; width?: number; } function Field({ label, children }: { label: string; children: any }) { return (
{children}
); } function TextInput({ value, onCommit }: { value: string; onCommit: (v: string) => void }) { const [local, setLocal] = useState(value || ''); // Sync when external value changes (e.g. different widget selected) useEffect(() => { setLocal(value || ''); }, [value]); return ( setLocal((e.target as HTMLInputElement).value)} onChange={(e) => onCommit((e.target as HTMLInputElement).value)} onKeyDown={(e) => { if (e.key === 'Enter') { onCommit((e.target as HTMLInputElement).value); (e.target as HTMLInputElement).blur(); } }} /> ); } // Like TextInput, but for the widget id: rejects invalid (empty/duplicate) // commits and reverts the field to the current id when `onRename` returns false. function IdInput({ value, onRename }: { value: string; onRename: (v: string) => boolean }) { const [local, setLocal] = useState(value || ''); useEffect(() => { setLocal(value || ''); }, [value]); function commit() { const v = local.trim(); if (v === value) { setLocal(value); return; } if (!onRename(v)) setLocal(value); // rejected — revert to the current id } return ( setLocal((e.target as HTMLInputElement).value)} onChange={commit} onKeyDown={(e) => { if (e.key === 'Enter') { commit(); (e.target as HTMLInputElement).blur(); } }} /> ); } // Widgets that show units from signal metadata (can be overridden) const UNIT_WIDGETS = new Set(['textview', 'gauge', 'barh', 'barv', 'setvalue']); // Widgets where the user can set a numeric format string const FORMAT_WIDGETS = new Set(['textview', 'gauge', 'barh', 'barv']); // Widgets with a signal-based label (can be overridden) const LABEL_WIDGETS = new Set(['textview', 'gauge', 'barh', 'barv', 'setvalue', 'led', 'multiled', 'toggle']); // Widgets with multiple signals const MULTI_SIGNAL_WIDGETS = new Set(['plot', 'multiled', 'table']); export default function PropertiesPane({ selected, multiCount, iface, onChange, onRenameId, onIfaceChange, width }: Props) { const [collapsed, setCollapsed] = useState(false); const [configSets, setConfigSets] = useState<{ id: string; name: string }[]>([]); // Config sets are only needed by the Config Selector widget; fetch lazily the // first time one is selected. useEffect(() => { if (selected?.type !== 'configselect' || configSets.length > 0) return; fetch('/api/v1/config/sets') .then(r => r.ok ? r.json() : []) .then((list: { id: string; name: string }[]) => setConfigSets(list ?? [])) .catch(() => {}); }, [selected?.type]); function setOpt(key: string, value: string) { if (!selected) return; onChange({ ...selected, options: { ...selected.options, [key]: value } }); } function setProp(key: keyof Widget, value: any) { if (!selected) return; onChange({ ...selected, [key]: value } as Widget); } // Apply a custom widget id. Rejects empty or duplicate ids (returns false so // the field reverts); on success the rename propagates to logic/plot refs. function renameId(newId: string): boolean { if (!selected) return false; if (!newId) return false; if (newId === selected.id) return false; if ((iface.widgets || []).some(x => x.id === newId)) return false; onRenameId(selected.id, newId); return true; } function removeSignal(idx: number) { if (!selected) return; const signals = selected.signals.filter((_, i) => i !== idx); onChange({ ...selected, signals }); } const w = selected; return ( ); }