Phase 6
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import type { Widget, Interface } from './lib/types';
|
||||
|
||||
interface Props {
|
||||
selected: Widget | null;
|
||||
iface: Interface;
|
||||
onChange: (updated: Widget) => void;
|
||||
onIfaceChange: (updated: Interface) => void;
|
||||
}
|
||||
|
||||
function Field({ label, children }: { label: string; children: any }) {
|
||||
return (
|
||||
<div class="prop-field">
|
||||
<label class="prop-label">{label}</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TextInput({ value, onCommit }: { value: string; onCommit: (v: string) => void }) {
|
||||
const [local, setLocal] = useState(value);
|
||||
return (
|
||||
<input
|
||||
class="prop-input"
|
||||
value={local}
|
||||
onInput={(e) => setLocal((e.target as HTMLInputElement).value)}
|
||||
onChange={(e) => onCommit((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PropertiesPane({ selected, iface, onChange, onIfaceChange }: Props) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return (
|
||||
<aside class={`panel props-panel${collapsed ? ' collapsed' : ''}`} style="border-left:1px solid #2d3748;border-right:none;">
|
||||
<div class="panel-header">
|
||||
{!collapsed && <span class="panel-title">Properties</span>}
|
||||
<button class="icon-btn" onClick={() => setCollapsed(c => !c)} title={collapsed ? 'Expand' : 'Collapse'}>
|
||||
{collapsed ? '◀' : '▶'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div class="props-body">
|
||||
{/* Interface-level properties (always shown) */}
|
||||
<div class="props-section">
|
||||
<div class="props-section-title">Canvas</div>
|
||||
<Field label="Name">
|
||||
<TextInput
|
||||
value={iface.name}
|
||||
onCommit={(v) => onIfaceChange({ ...iface, name: v })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Width">
|
||||
<input
|
||||
class="prop-input prop-input-num"
|
||||
type="number"
|
||||
value={iface.w}
|
||||
min={100}
|
||||
onChange={(e) => onIfaceChange({ ...iface, w: parseInt((e.target as HTMLInputElement).value, 10) || iface.w })}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Height">
|
||||
<input
|
||||
class="prop-input prop-input-num"
|
||||
type="number"
|
||||
value={iface.h}
|
||||
min={100}
|
||||
onChange={(e) => onIfaceChange({ ...iface, h: parseInt((e.target as HTMLInputElement).value, 10) || iface.h })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div class="props-section">
|
||||
<div class="props-section-title">{selected.type}</div>
|
||||
<Field label="X">
|
||||
<input class="prop-input prop-input-num" type="number" value={selected.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}
|
||||
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}
|
||||
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}
|
||||
onChange={(e) => setProp('h', parseFloat((e.target as HTMLInputElement).value) || 10)} />
|
||||
</Field>
|
||||
|
||||
{/* Signal */}
|
||||
{selected.signals.length > 0 && (
|
||||
<Field label="Signal">
|
||||
<span class="prop-sig">{selected.signals[0].ds}:{selected.signals[0].name}</span>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{/* Common options */}
|
||||
<Field label="Label">
|
||||
<TextInput
|
||||
value={selected.options['label'] ?? ''}
|
||||
onCommit={(v) => setOpt('label', v)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{/* Type-specific options */}
|
||||
{(selected.type === 'gauge' || selected.type === 'barh' || selected.type === 'barv') && (
|
||||
<div>
|
||||
<Field label="Min">
|
||||
<TextInput value={selected.options['min'] ?? '0'} onCommit={(v) => setOpt('min', v)} />
|
||||
</Field>
|
||||
<Field label="Max">
|
||||
<TextInput value={selected.options['max'] ?? '100'} onCommit={(v) => setOpt('max', v)} />
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected.type === 'led' && (
|
||||
<Field label="Condition">
|
||||
<TextInput value={selected.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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{selected.type === 'textlabel' && (
|
||||
<div>
|
||||
<Field label="Font size">
|
||||
<TextInput value={selected.options['fontSize'] ?? '14'} onCommit={(v) => setOpt('fontSize', v)} />
|
||||
</Field>
|
||||
<Field label="Color">
|
||||
<TextInput value={selected.options['color'] ?? '#e2e8f0'} onCommit={(v) => setOpt('color', v)} />
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selected.type === 'image' && (
|
||||
<Field label="URL">
|
||||
<TextInput value={selected.options['url'] ?? ''} onCommit={(v) => setOpt('url', v)} />
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{selected.type === 'link' && (
|
||||
<div>
|
||||
<Field label="Target">
|
||||
<TextInput value={selected.options['target'] ?? ''} onCommit={(v) => setOpt('target', v)} />
|
||||
</Field>
|
||||
<Field label="Label">
|
||||
<TextInput value={selected.options['label'] ?? ''} onCommit={(v) => setOpt('label', v)} />
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!selected && (
|
||||
<p class="hint" style="padding:0.75rem;">Select a widget to edit its properties.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user