Implemented epics read/write
This commit is contained in:
+175
-53
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user