Implemented epics read/write

This commit is contained in:
Martino Ferrari
2026-04-27 12:42:10 +02:00
parent b76b7f0ba8
commit 1bda25454b
32 changed files with 1553 additions and 281 deletions
+175 -53
View File
@@ -4,7 +4,7 @@ import type { Widget, Interface } from './lib/types';
interface Props {
selected: Widget | null;
multiCount: number; // >0 when multiple widgets are selected
multiCount: number;
iface: Interface;
onChange: (updated: Widget) => void;
onIfaceChange: (updated: Interface) => void;
@@ -21,6 +21,10 @@ function Field({ label, children }: { label: string; children: any }) {
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)
if (local !== value && document.activeElement?.tagName !== 'INPUT') {
setLocal(value);
}
return (
<input
class="prop-input"
@@ -31,6 +35,13 @@ function TextInput({ value, onCommit }: { value: string; onCommit: (v: string) =
);
}
// Widgets that show units from signal metadata (can be overridden)
const UNIT_WIDGETS = new Set(['textview', 'gauge', 'barh', 'barv', 'setvalue']);
// Widgets with a signal-based label (can be overridden)
const LABEL_WIDGETS = new Set(['textview', 'gauge', 'barh', 'barv', 'setvalue', 'led', 'multiled']);
// Widgets with multiple signals
const MULTI_SIGNAL_WIDGETS = new Set(['plot', 'multiled']);
export default function PropertiesPane({ selected, multiCount, iface, onChange, onIfaceChange }: Props) {
const [collapsed, setCollapsed] = useState(false);
@@ -44,6 +55,14 @@ export default function PropertiesPane({ selected, multiCount, iface, onChange,
onChange({ ...selected, [key]: value } as Widget);
}
function removeSignal(idx: number) {
if (!selected) return;
const signals = selected.signals.filter((_, i) => i !== idx);
onChange({ ...selected, signals });
}
const w = selected;
return (
<aside class={`panel props-panel${collapsed ? ' collapsed' : ''}`} style="border-left:1px solid #2d3748;border-right:none;">
<div class="panel-header">
@@ -55,7 +74,7 @@ export default function PropertiesPane({ selected, multiCount, iface, onChange,
{!collapsed && (
<div class="props-body">
{/* Interface-level properties (always shown) */}
{/* Canvas-level properties */}
<div class="props-section">
<div class="props-section-title">Canvas</div>
<Field label="Name">
@@ -84,117 +103,220 @@ export default function PropertiesPane({ selected, multiCount, iface, onChange,
</Field>
</div>
{selected && (
{w && (
<div class="props-section">
<div class="props-section-title">{selected.type}</div>
<div class="props-section-title">{w.type}</div>
{/* Position / size */}
<Field label="X">
<input class="prop-input prop-input-num" type="number" value={selected.x}
<input class="prop-input prop-input-num" type="number" value={w.x}
onChange={(e) => setProp('x', parseFloat((e.target as HTMLInputElement).value) || 0)} />
</Field>
<Field label="Y">
<input class="prop-input prop-input-num" type="number" value={selected.y}
<input class="prop-input prop-input-num" type="number" value={w.y}
onChange={(e) => setProp('y', parseFloat((e.target as HTMLInputElement).value) || 0)} />
</Field>
<Field label="Width">
<input class="prop-input prop-input-num" type="number" value={selected.w} min={10}
<input class="prop-input prop-input-num" type="number" value={w.w} min={10}
onChange={(e) => setProp('w', parseFloat((e.target as HTMLInputElement).value) || 10)} />
</Field>
<Field label="Height">
<input class="prop-input prop-input-num" type="number" value={selected.h} min={10}
<input class="prop-input prop-input-num" type="number" value={w.h} min={10}
onChange={(e) => setProp('h', parseFloat((e.target as HTMLInputElement).value) || 10)} />
</Field>
{/* Signal */}
{selected.signals.length > 0 && (
{/* Signals */}
{MULTI_SIGNAL_WIDGETS.has(w.type) ? (
<Field label="Signals">
<div class="prop-signals-list">
{w.signals.length === 0 && <span class="hint">No signals</span>}
{w.signals.map((s, i) => (
<div key={i} class="prop-signal-row">
<span class="prop-sig">{s.ds}:{s.name}</span>
<button
class="icon-btn prop-sig-remove"
title="Remove signal"
onClick={() => removeSignal(i)}
></button>
</div>
))}
</div>
</Field>
) : w.signals.length > 0 ? (
<Field label="Signal">
<span class="prop-sig">{selected.signals[0].ds}:{selected.signals[0].name}</span>
<span class="prop-sig">{w.signals[0].ds}:{w.signals[0].name}</span>
</Field>
) : null}
{/* ── Common label/unit overrides (issue #8) ──────────────────── */}
{/* Label: button and textlabel use it as display text;
signal widgets use it as header label */}
{w.type !== 'image' && w.type !== 'link' && (
<Field label={LABEL_WIDGETS.has(w.type) ? 'Label override' : 'Label'}>
<TextInput
value={w.options['label'] ?? ''}
onCommit={(v) => setOpt('label', v)}
/>
{LABEL_WIDGETS.has(w.type) && (
<span class="prop-hint">Empty = signal name</span>
)}
</Field>
)}
{/* Common options */}
<Field label="Label">
<TextInput
value={selected.options['label'] ?? ''}
onCommit={(v) => setOpt('label', v)}
/>
</Field>
{/* Unit override for signal widgets */}
{UNIT_WIDGETS.has(w.type) && (
<Field label="Unit override">
<TextInput
value={w.options['unit'] ?? ''}
onCommit={(v) => setOpt('unit', v)}
/>
<span class="prop-hint">Empty = from metadata · "none" = hide</span>
</Field>
)}
{/* Type-specific options */}
{(selected.type === 'gauge' || selected.type === 'barh' || selected.type === 'barv') && (
{/* ── Type-specific options ──────────────────────────────────── */}
{(w.type === 'gauge' || w.type === 'barh' || w.type === 'barv') && (
<div>
<Field label="Min">
<TextInput value={selected.options['min'] ?? '0'} onCommit={(v) => setOpt('min', v)} />
<TextInput value={w.options['min'] ?? '0'} onCommit={(v) => setOpt('min', v)} />
</Field>
<Field label="Max">
<TextInput value={selected.options['max'] ?? '100'} onCommit={(v) => setOpt('max', v)} />
<TextInput value={w.options['max'] ?? '100'} onCommit={(v) => setOpt('max', v)} />
</Field>
</div>
)}
{selected.type === 'led' && (
{w.type === 'led' && (
<Field label="Condition">
<TextInput value={selected.options['condition'] ?? '>0'} onCommit={(v) => setOpt('condition', v)} />
<TextInput value={w.options['condition'] ?? '>0'} onCommit={(v) => setOpt('condition', v)} />
</Field>
)}
{selected.type === 'setvalue' && (
<Field label="Min">
<TextInput value={selected.options['min'] ?? ''} onCommit={(v) => setOpt('min', v)} />
</Field>
{w.type === 'setvalue' && (
<div>
<Field label="Min">
<TextInput value={w.options['min'] ?? ''} onCommit={(v) => setOpt('min', v)} />
</Field>
<Field label="Confirm dialog">
<select class="prop-select" value={w.options['confirm'] ?? 'false'}
onChange={(e) => setOpt('confirm', (e.target as HTMLSelectElement).value)}>
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</Field>
</div>
)}
{selected.type === 'plot' && (
<Field label="Plot type">
<select
class="prop-select"
value={selected.options['plotType'] ?? 'timeseries'}
onChange={(e) => setOpt('plotType', (e.target as HTMLSelectElement).value)}
>
<option value="timeseries">Time-series</option>
<option value="histogram">Histogram</option>
<option value="bar">Bar chart</option>
<option value="fft">FFT</option>
<option value="waterfall">Waterfall</option>
<option value="logic">Logic analyser</option>
</select>
</Field>
{/* Button options (issue #2) */}
{w.type === 'button' && (
<div>
<Field label="Send value">
<TextInput value={w.options['value'] ?? '1'} onCommit={(v) => setOpt('value', v)} />
</Field>
<Field label="Mode">
<select class="prop-select" value={w.options['mode'] ?? 'oneshot'}
onChange={(e) => setOpt('mode', (e.target as HTMLSelectElement).value)}>
<option value="oneshot">One-shot (set once)</option>
<option value="setReset">Set-reset (auto-revert)</option>
<option value="persistent">Persistent (toggle / monitor)</option>
</select>
</Field>
{(w.options['mode'] === 'setReset' || w.options['mode'] === 'persistent') && (
<Field label="Reset value">
<TextInput value={w.options['resetValue'] ?? '0'} onCommit={(v) => setOpt('resetValue', v)} />
</Field>
)}
{w.options['mode'] === 'setReset' && (
<Field label="Reset after (s)">
<TextInput value={w.options['resetTime'] ?? '1'} onCommit={(v) => setOpt('resetTime', v)} />
</Field>
)}
<Field label="Confirm dialog">
<select class="prop-select" value={w.options['confirm'] ?? 'false'}
onChange={(e) => setOpt('confirm', (e.target as HTMLSelectElement).value)}>
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</Field>
</div>
)}
{selected.type === 'textlabel' && (
{/* Plot options */}
{w.type === 'plot' && (
<div>
<Field label="Plot type">
<select
class="prop-select"
value={w.options['plotType'] ?? 'timeseries'}
onChange={(e) => setOpt('plotType', (e.target as HTMLSelectElement).value)}
>
<option value="timeseries">Time-series</option>
<option value="histogram">Histogram</option>
<option value="bar">Bar chart</option>
<option value="fft">FFT</option>
<option value="waterfall">Waterfall</option>
<option value="logic">Logic analyser</option>
</select>
</Field>
{(w.options['plotType'] ?? 'timeseries') === 'timeseries' && (
<Field label="Time window (s)">
<TextInput value={w.options['timeWindow'] ?? '60'} onCommit={(v) => setOpt('timeWindow', v)} />
</Field>
)}
<Field label="Y min">
<TextInput value={w.options['yMin'] ?? 'auto'} onCommit={(v) => setOpt('yMin', v)} />
</Field>
<Field label="Y max">
<TextInput value={w.options['yMax'] ?? 'auto'} onCommit={(v) => setOpt('yMax', v)} />
</Field>
<Field label="Legend">
<select class="prop-select" value={w.options['legend'] ?? 'bottom'}
onChange={(e) => setOpt('legend', (e.target as HTMLSelectElement).value)}>
<option value="bottom">Bottom</option>
<option value="top">Top</option>
<option value="none">None</option>
</select>
</Field>
</div>
)}
{w.type === 'textlabel' && (
<div>
<Field label="Font size">
<TextInput value={selected.options['fontSize'] ?? '14'} onCommit={(v) => setOpt('fontSize', v)} />
<TextInput value={w.options['fontSize'] ?? '14'} onCommit={(v) => setOpt('fontSize', v)} />
</Field>
<Field label="Color">
<TextInput value={selected.options['color'] ?? '#e2e8f0'} onCommit={(v) => setOpt('color', v)} />
<TextInput value={w.options['color'] ?? '#e2e8f0'} onCommit={(v) => setOpt('color', v)} />
</Field>
</div>
)}
{selected.type === 'image' && (
{w.type === 'image' && (
<Field label="URL">
<TextInput value={selected.options['url'] ?? ''} onCommit={(v) => setOpt('url', v)} />
<TextInput value={w.options['url'] ?? ''} onCommit={(v) => setOpt('url', v)} />
</Field>
)}
{selected.type === 'link' && (
{w.type === 'link' && (
<div>
<Field label="Target">
<TextInput value={selected.options['target'] ?? ''} onCommit={(v) => setOpt('target', v)} />
<TextInput value={w.options['target'] ?? ''} onCommit={(v) => setOpt('target', v)} />
</Field>
<Field label="Label">
<TextInput value={selected.options['label'] ?? ''} onCommit={(v) => setOpt('label', v)} />
<TextInput value={w.options['label'] ?? ''} onCommit={(v) => setOpt('label', v)} />
</Field>
</div>
)}
</div>
)}
{!selected && multiCount > 1 && (
{!w && multiCount > 1 && (
<p class="hint" style="padding:0.75rem;">{multiCount} widgets selected.<br/>Use align/distribute tools in the toolbar.</p>
)}
{!selected && multiCount === 0 && (
{!w && multiCount === 0 && (
<p class="hint" style="padding:0.75rem;">Select a widget to edit its properties.</p>
)}
</div>