import { h, Fragment } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import SignalPicker, { SignalOption } from './SignalPicker';
import { checkExpr } from './lib/expr';
import type { LogicGraph, LogicNode, LogicNodeKind, LogicWire, StateVar, Widget } from './lib/types';

interface DataSource { name: string; }
interface SignalInfo { name: string; }

// Split a "ds:name" reference on the FIRST ':' only (EPICS PV names contain ':').
function splitRef(ref: string): { ds: string; name: string } {
  const i = ref.indexOf(':');
  if (i < 0) return { ds: '', name: '' };
  return { ds: ref.slice(0, i), name: ref.slice(i + 1) };
}

interface Props {
  graph: LogicGraph;
  onChange: (graph: LogicGraph) => void;
  // The panel's widgets, offered as targets for the Widget-control action.
  widgets?: Widget[];
  // Panel-local state variables, offered as quick write targets / signals.
  statevars?: StateVar[];
  // Edit local state variables directly from the logic editor (the layout-side
  // SignalTree is hidden while the logic tab is open).
  onStateVarsChange?: (vars: StateVar[]) => void;
}

// Visual geometry of a node block. Expressed in rem (relative to the root font
// size) so the whole flow editor scales coherently on high-DPI screens / when
// the base font is enlarged for accessibility, instead of staying pixel-tiny.
const REM = (() => {
  if (typeof document === 'undefined') return 16;
  return parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
})();
const NODE_W = 11.5 * REM;  // node width
const PORT_TOP = 2 * REM;   // y of the first port row, relative to node top
const PORT_GAP = 1.375 * REM; // vertical spacing between stacked output ports
const PORT_R = 0.375 * REM; // port radius (half the 0.75rem dot)
// Ports sit inside the node's padding box, offset from the node border-box origin
// by the node borders (3px colored top accent, 1px sides). Anchors add the same
// offsets so wires meet the port centers, not their top edges.
const BORDER = 1;
const BORDER_TOP = 3;

interface PaletteEntry {
  kind: LogicNodeKind;
  label: string;
  params: Record<string, string>;
}

const PALETTE: PaletteEntry[] = [
  { kind: 'trigger.button',    label: 'Button',     params: { name: 'action' } },
  { kind: 'trigger.threshold', label: 'Threshold',  params: { signal: '', op: '>', value: '0' } },
  { kind: 'trigger.change',    label: 'On change',  params: { signal: '' } },
  { kind: 'trigger.timer',     label: 'Timer',      params: { interval: '1000' } },
  { kind: 'trigger.loop',      label: 'Panel loop', params: { interval: '1000' } },
  { kind: 'trigger.start',     label: 'On open',    params: {} },
  { kind: 'trigger.stop',      label: 'On close',   params: {} },
  { kind: 'gate.and',          label: 'AND gate',   params: {} },
  { kind: 'flow.if',           label: 'If / else',  params: { cond: '' } },
  { kind: 'flow.loop',         label: 'Loop',       params: { mode: 'count', count: '3', cond: '' } },
  { kind: 'action.write',      label: 'Write',      params: { target: '', expr: '' } },
  { kind: 'action.delay',      label: 'Delay',      params: { ms: '500' } },
  { kind: 'action.accumulate', label: 'Accumulate', params: { array: 'data', expr: '' } },
  { kind: 'action.export',     label: 'Export CSV', params: { columns: '[{"array":"data","label":""}]', align: 'common', filename: '' } },
  { kind: 'action.clear',      label: 'Clear array', params: { array: 'data' } },
  { kind: 'action.log',        label: 'Log',        params: { expr: '', label: '' } },
  { kind: 'action.widget',     label: 'Widget control', params: { widget: '', op: 'disable' } },
  { kind: 'action.dialog.info',     label: 'Info dialog',      params: { title: 'Info', message: '' } },
  { kind: 'action.dialog.error',    label: 'Error dialog',     params: { title: 'Error', message: '' } },
  { kind: 'action.dialog.setpoint', label: 'Set-point dialog', params: { title: 'Set value', message: '', fields: '[{"type":"input","label":"","target":"","default":""}]' } },
];

const KIND_LABEL: Record<LogicNodeKind, string> = {
  'trigger.button': 'Button',
  'trigger.threshold': 'Threshold',
  'trigger.change': 'On change',
  'trigger.timer': 'Timer',
  'trigger.loop': 'Panel loop',
  'trigger.start': 'On open',
  'trigger.stop': 'On close',
  'gate.and': 'AND gate',
  'flow.if': 'If / else',
  'flow.loop': 'Loop',
  'action.write': 'Write',
  'action.delay': 'Delay',
  'action.accumulate': 'Accumulate',
  'action.export': 'Export CSV',
  'action.clear': 'Clear array',
  'action.log': 'Log',
  'action.widget': 'Widget control',
  'action.dialog.info': 'Info dialog',
  'action.dialog.error': 'Error dialog',
  'action.dialog.setpoint': 'Set-point dialog',
};

const THRESHOLD_OPS = ['>', '<', '>=', '<=', '==', '!='] as const;

function isTrigger(kind: LogicNodeKind): boolean { return kind.startsWith('trigger.'); }
function category(kind: LogicNodeKind): 'trigger' | 'gate' | 'flow' | 'action' {
  if (kind.startsWith('trigger.')) return 'trigger';
  if (kind.startsWith('gate.')) return 'gate';
  if (kind.startsWith('flow.')) return 'flow';
  return 'action';
}
function hasInput(kind: LogicNodeKind): boolean { return !isTrigger(kind); }

interface Port { id: string; label: string; }
function outputs(kind: LogicNodeKind): Port[] {
  if (kind === 'flow.if') return [{ id: 'then', label: 'then' }, { id: 'else', label: 'else' }];
  if (kind === 'flow.loop') return [{ id: 'body', label: 'body' }, { id: 'done', label: 'done' }];
  return [{ id: 'out', label: '' }];
}
function nodeHeight(kind: LogicNodeKind): number {
  return PORT_TOP + outputs(kind).length * PORT_GAP;
}

function genId(): string { return 'n_' + Math.random().toString(36).slice(2, 9); }

// Port anchors in canvas coordinates.
function inAnchor(n: LogicNode) { return { x: n.x + BORDER, y: n.y + BORDER_TOP + PORT_TOP }; }
function outAnchor(n: LogicNode, port: string) {
  const ports = outputs(n.kind);
  const i = Math.max(0, ports.findIndex(p => p.id === port));
  return { x: n.x + NODE_W - BORDER, y: n.y + BORDER_TOP + PORT_TOP + i * PORT_GAP };
}

function wirePathStr(x1: number, y1: number, x2: number, y2: number): string {
  const dx = Math.max(40, Math.abs(x2 - x1) / 2);
  return `M ${x1} ${y1} C ${x1 + dx} ${y1}, ${x2 - dx} ${y2}, ${x2} ${y2}`;
}

export default function LogicEditor({ graph, onChange, widgets = [], statevars, onStateVarsChange }: Props) {
  const nodes = graph.nodes;
  const wires = graph.wires;

  const [selectedNode, setSelectedNode] = useState<string | null>(null);
  const [selectedWire, setSelectedWire] = useState<number | null>(null);

  const [dataSources, setDataSources] = useState<string[]>([]);
  const [dsSignals, setDsSignals] = useState<Record<string, string[]>>({});

  const canvasRef = useRef<HTMLDivElement>(null);
  const dragNode = useRef<{ id: string; dx: number; dy: number; pushed: boolean } | null>(null);
  const [pendingWire, setPendingWire] = useState<{ from: string; port: string; x: number; y: number } | null>(null);
  const pendingRef = useRef(pendingWire);
  pendingRef.current = pendingWire;

  // Undo/redo history + clipboard, scoped to the logic graph (separate from
  // EditMode's widget/layout history). `graphRef` always holds the latest graph
  // so the ref-based stacks read a stable value across renders.
  const graphRef = useRef(graph);
  graphRef.current = graph;
  const undoStack = useRef<LogicGraph[]>([]);
  const redoStack = useRef<LogicGraph[]>([]);
  const clipboard = useRef<{ nodes: LogicNode[]; wires: LogicWire[] } | null>(null);
  const [, setHistTick] = useState(0); // re-render so toolbar enabled-state updates
  const bump = () => setHistTick(t => t + 1);
  const canUndo = undoStack.current.length > 0;
  const canRedo = redoStack.current.length > 0;

  useEffect(() => {
    fetch('/api/v1/datasources')
      .then(r => r.ok ? r.json() : [])
      .then((ds: DataSource[]) => setDataSources(ds.map(d => d.name)))
      .catch(() => {});
  }, []);

  async function loadSignals(ds: string) {
    if (!ds || ds === 'local' || ds === 'sys' || dsSignals[ds]) return;
    try {
      const res = await fetch(`/api/v1/signals?ds=${encodeURIComponent(ds)}`);
      if (!res.ok) return;
      const sigs: SignalInfo[] = await res.json();
      setDsSignals(prev => ({ ...prev, [ds]: sigs.map(s => s.name) }));
    } catch {}
  }

  // 'sys' exposes the engine's built-in time signals ({sys:time}, {sys:dt}).
  const dsOptions = ['local', 'sys', ...dataSources];
  function signalOptions(ds: string): string[] {
    if (ds === 'local') return (statevars ?? []).map(v => v.name);
    if (ds === 'sys') return ['time', 'dt'];
    return dsSignals[ds] ?? [];
  }

  // Flatten every known ds:name into a single list for the fuzzy SignalPicker,
  // and a hook to lazily load all data sources' signals when a picker opens.
  function allSignalOptions(): SignalOption[] {
    const out: SignalOption[] = [];
    for (const ds of dsOptions) for (const name of signalOptions(ds)) out.push({ ds, name });
    return out;
  }
  function openAllSignals() { dsOptions.forEach(ds => loadSignals(ds)); }

  const selected = nodes.find(n => n.id === selectedNode) ?? null;

  useEffect(() => {
    if (selected?.kind === 'trigger.threshold' || selected?.kind === 'trigger.change') {
      loadSignals(splitRef(selected.params.signal ?? '').ds);
    }
    if (selected?.kind === 'action.write') {
      loadSignals(splitRef(selected.params.target ?? '').ds);
    }
    if (selected && selected.kind.startsWith('action.dialog.')) {
      for (const f of parseDialogFields(selected)) {
        if (f.type === 'input') loadSignals(splitRef(f.target ?? '').ds);
      }
    }
  }, [selectedNode, selected?.kind]);

  // ── History ────────────────────────────────────────────────────────────────
  // Push the current graph onto the undo stack (clearing redo). Call right
  // before a discrete edit; for continuous gestures (node drag) call once.
  function pushUndo() {
    undoStack.current = [...undoStack.current.slice(-49), graphRef.current];
    redoStack.current = [];
    bump();
  }
  function undo() {
    if (undoStack.current.length === 0) return;
    const prev = undoStack.current[undoStack.current.length - 1];
    redoStack.current = [graphRef.current, ...redoStack.current];
    undoStack.current = undoStack.current.slice(0, -1);
    setSelectedNode(null);
    setSelectedWire(null);
    onChange(prev);
    bump();
  }
  function redo() {
    if (redoStack.current.length === 0) return;
    const next = redoStack.current[0];
    undoStack.current = [...undoStack.current, graphRef.current];
    redoStack.current = redoStack.current.slice(1);
    setSelectedNode(null);
    setSelectedWire(null);
    onChange(next);
    bump();
  }

  // ── Graph mutators ─────────────────────────────────────────────────────────
  // Apply a new graph, recording an undo entry unless `record` is false (used
  // for the intermediate frames of a node drag, which record once on first move).
  function setGraph(next: LogicGraph, record = true) {
    if (record) pushUndo();
    onChange(next);
  }
  function setNodes(next: LogicNode[], record = true) { setGraph({ nodes: next, wires }, record); }
  function setWires(next: LogicWire[]) { setGraph({ nodes, wires: next }); }

  function addNode(entry: PaletteEntry, x?: number, y?: number) {
    const node: LogicNode = {
      id: genId(),
      kind: entry.kind,
      x: x ?? 40 + (nodes.length % 5) * 30,
      y: y ?? 40 + (nodes.length % 5) * 30,
      params: { ...entry.params },
    };
    setGraph({ nodes: [...nodes, node], wires });
    setSelectedNode(node.id);
    setSelectedWire(null);
  }

  function patchParams(id: string, patch: Record<string, string>) {
    setNodes(nodes.map(n => (n.id === id ? { ...n, params: { ...n.params, ...patch } } : n)));
  }
  function moveNode(id: string, x: number, y: number, record = false) {
    setNodes(nodes.map(n => (n.id === id ? { ...n, x, y } : n)), record);
  }
  function deleteNode(id: string) {
    setGraph({
      nodes: nodes.filter(n => n.id !== id),
      wires: wires.filter(w => w.from !== id && w.to !== id),
    });
    if (selectedNode === id) setSelectedNode(null);
  }
  function addWire(from: string, port: string, to: string) {
    if (from === to) return;
    if (wires.some(w => w.from === from && (w.fromPort ?? 'out') === port && w.to === to)) return;
    setWires([...wires, { from, to, ...(port !== 'out' ? { fromPort: port } : {}) }]);
  }
  function deleteWire(idx: number) {
    setWires(wires.filter((_, i) => i !== idx));
    if (selectedWire === idx) setSelectedWire(null);
  }

  // ── Clipboard ────────────────────────────────────────────────────────────
  // Copy the selected node (params deep-cloned). Single-selection only, but the
  // paste remap is written generically so it extends to multi-select.
  function copySelection() {
    const n = graphRef.current.nodes.find(x => x.id === selectedNode);
    if (!n) return;
    clipboard.current = { nodes: [{ ...n, params: { ...n.params } }], wires: [] };
  }
  function pasteClipboard() {
    const clip = clipboard.current;
    if (!clip || clip.nodes.length === 0) return;
    const idMap = new Map<string, string>();
    const cur = graphRef.current;
    const newNodes = clip.nodes.map(n => {
      const id = genId();
      idMap.set(n.id, id);
      return { ...n, id, x: n.x + 30, y: n.y + 30, params: { ...n.params } };
    });
    const newWires = clip.wires
      .filter(w => idMap.has(w.from) && idMap.has(w.to))
      .map(w => ({ ...w, from: idMap.get(w.from)!, to: idMap.get(w.to)! }));
    setGraph({ nodes: [...cur.nodes, ...newNodes], wires: [...cur.wires, ...newWires] });
    if (newNodes.length === 1) { setSelectedNode(newNodes[0].id); setSelectedWire(null); }
  }

  // ── Pointer geometry ───────────────────────────────────────────────────────
  function toCanvas(e: MouseEvent): { x: number; y: number } {
    const el = canvasRef.current!;
    const rect = el.getBoundingClientRect();
    return { x: e.clientX - rect.left + el.scrollLeft, y: e.clientY - rect.top + el.scrollTop };
  }

  // ── Node dragging ──────────────────────────────────────────────────────────
  function startNodeDrag(e: MouseEvent, node: LogicNode) {
    e.stopPropagation();
    const p = toCanvas(e);
    dragNode.current = { id: node.id, dx: p.x - node.x, dy: p.y - node.y, pushed: false };
    setSelectedNode(node.id);
    setSelectedWire(null);
    window.addEventListener('mousemove', onNodeDragMove);
    window.addEventListener('mouseup', onNodeDragUp);
  }
  function onNodeDragMove(e: MouseEvent) {
    const d = dragNode.current;
    if (!d) return;
    const p = toCanvas(e);
    // Record a single undo entry per drag (on the first move, so a plain
    // click-to-select doesn't create a spurious history step).
    const record = !d.pushed;
    d.pushed = true;
    moveNode(d.id, Math.max(0, p.x - d.dx), Math.max(0, p.y - d.dy), record);
  }
  function onNodeDragUp() {
    dragNode.current = null;
    window.removeEventListener('mousemove', onNodeDragMove);
    window.removeEventListener('mouseup', onNodeDragUp);
  }

  // ── Wire drawing ───────────────────────────────────────────────────────────
  function startWire(e: MouseEvent, node: LogicNode, port: string) {
    e.stopPropagation();
    const p = toCanvas(e);
    setPendingWire({ from: node.id, port, x: p.x, y: p.y });
    window.addEventListener('mousemove', onWireMove);
    window.addEventListener('mouseup', onWireUp);
  }
  function onWireMove(e: MouseEvent) {
    const cur = pendingRef.current;
    if (!cur) return;
    const p = toCanvas(e);
    setPendingWire({ ...cur, x: p.x, y: p.y });
  }
  // Tear down an in-progress wire drag. Called both on a plain release over
  // empty canvas (window mouseup) and after a connection is made — the latter
  // is essential because the target port's mouseup calls stopPropagation, so
  // the window listener never fires and would otherwise leave a phantom wire
  // following the cursor.
  function endWire() {
    pendingRef.current = null;
    window.removeEventListener('mousemove', onWireMove);
    window.removeEventListener('mouseup', onWireUp);
    setPendingWire(null);
  }
  function onWireUp() {
    endWire();
  }
  function finishWire(target: LogicNode) {
    const cur = pendingRef.current;
    if (cur && hasInput(target.kind)) addWire(cur.from, cur.port, target.id);
    endWire();
  }

  // ── Palette drag-and-drop ────────────────────────────────────────────────
  const DRAG_MIME = 'application/x-uopi-logic-node';
  function onCanvasDragOver(e: DragEvent) {
    if (Array.from(e.dataTransfer?.types ?? []).includes(DRAG_MIME)) {
      e.preventDefault();
      if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
    }
  }
  function onCanvasDrop(e: DragEvent) {
    const kind = e.dataTransfer?.getData(DRAG_MIME);
    if (!kind) return;
    e.preventDefault();
    const entry = PALETTE.find(p => p.kind === kind);
    if (!entry) return;
    const p = toCanvas(e);
    // Drop point becomes the node's top-left, offset so the cursor lands inside.
    addNode(entry, Math.max(0, p.x - NODE_W / 2), Math.max(0, p.y - PORT_TOP / 2));
  }

  // ── Keyboard shortcuts (delete / undo / redo / copy / paste) ────────────────
  useEffect(() => {
    function onKey(e: KeyboardEvent) {
      const t = e.target as Element;
      // While typing in a field, leave native text editing shortcuts alone.
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return;
      const mod = e.ctrlKey || e.metaKey;
      if (mod && e.key.toLowerCase() === 'z' && !e.shiftKey) { e.preventDefault(); undo(); return; }
      if (mod && (e.key.toLowerCase() === 'y' || (e.key.toLowerCase() === 'z' && e.shiftKey))) { e.preventDefault(); redo(); return; }
      if (mod && e.key.toLowerCase() === 'c') { e.preventDefault(); copySelection(); return; }
      if (mod && e.key.toLowerCase() === 'v') { e.preventDefault(); pasteClipboard(); return; }
      if (e.key !== 'Delete' && e.key !== 'Backspace') return;
      if (selectedWire !== null) deleteWire(selectedWire);
      else if (selectedNode) deleteNode(selectedNode);
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [selectedNode, selectedWire, nodes, wires]);

  const byId = new Map(nodes.map(n => [n.id, n]));

  // Array names already referenced by accumulate/export/clear nodes, offered as
  // autocomplete suggestions so the same array is reused across nodes.
  const arrayNames = Array.from(new Set(
    nodes.map(n => n.params.array).filter((a): a is string => !!a)
  ));

  // Append a {ds:name} reference into a node's expression field.
  function insertRef(id: string, field: string, ds: string, sig: string) {
    const cur = (byId.get(id)?.params[field]) ?? '';
    patchParams(id, { [field]: `${cur}{${ds}:${sig}}` });
  }

  return (
    <div class="flow-editor">
      <div class="flow-palette">
        <div class="flow-palette-toolbar">
          <button class="toolbar-btn" disabled={!canUndo} onClick={undo} title="Undo (Ctrl+Z)">↩</button>
          <button class="toolbar-btn" disabled={!canRedo} onClick={redo} title="Redo (Ctrl+Shift+Z)">↪</button>
        </div>
        <div class="flow-palette-title">Triggers</div>
        {PALETTE.filter(e => category(e.kind) === 'trigger').map(paletteBtn)}
        <div class="flow-palette-title">Logic</div>
        {PALETTE.filter(e => category(e.kind) === 'gate' || category(e.kind) === 'flow').map(paletteBtn)}
        <div class="flow-palette-title">Actions</div>
        {PALETTE.filter(e => category(e.kind) === 'action').map(paletteBtn)}
        <div class="flow-palette-hint hint">
          Triggers start a flow. Drag a node's right port to another node's left port to connect.
          In expressions, reference signals as <code>{'{ds:name}'}</code> and local vars by name.
        </div>
        {onStateVarsChange && (
          <LocalVars statevars={statevars ?? []} onChange={onStateVarsChange} />
        )}
        <datalist id="flow-array-names">
          {arrayNames.map(a => <option key={a} value={a} />)}
        </datalist>
      </div>

      <div class="flow-canvas" ref={canvasRef}
        onMouseDown={() => { setSelectedNode(null); setSelectedWire(null); }}
        onDragOver={onCanvasDragOver}
        onDrop={onCanvasDrop}>
        <div class="flow-canvas-inner">
          <svg class="flow-wires">
            {wires.map((w, idx) => {
              const a = byId.get(w.from);
              const b = byId.get(w.to);
              if (!a || !b) return null;
              const p1 = outAnchor(a, w.fromPort ?? 'out');
              const p2 = inAnchor(b);
              return (
                <path key={idx}
                  class={`flow-wire${selectedWire === idx ? ' flow-wire-selected' : ''}`}
                  d={wirePathStr(p1.x, p1.y, p2.x, p2.y)}
                  onClick={(e) => { e.stopPropagation(); setSelectedWire(idx); setSelectedNode(null); }} />
              );
            })}
            {pendingWire && (() => {
              const a = byId.get(pendingWire.from);
              if (!a) return null;
              const p1 = outAnchor(a, pendingWire.port);
              return <path class="flow-wire flow-wire-pending" d={wirePathStr(p1.x, p1.y, pendingWire.x, pendingWire.y)} />;
            })()}
          </svg>

          {nodes.map(node => (
            <div key={node.id}
              class={`flow-node flow-node-${category(node.kind)}${selectedNode === node.id ? ' flow-node-selected' : ''}`}
              style={`left:${node.x}px; top:${node.y}px; width:${NODE_W}px; min-height:${nodeHeight(node.kind)}px;`}
              onMouseDown={(e) => startNodeDrag(e, node)}
              onMouseUp={() => { if (pendingRef.current) finishWire(node); }}>
              <div class="flow-node-header">
                <span class="flow-node-title">{KIND_LABEL[node.kind]}</span>
                <button class="flow-node-del" title="Delete node"
                  onMouseDown={(e) => e.stopPropagation()}
                  onClick={(e) => { e.stopPropagation(); deleteNode(node.id); }}>✕</button>
              </div>
              <div class="flow-node-body hint">{nodeSummary(node)}</div>

              {hasInput(node.kind) && (
                <div class="flow-port flow-port-in" title="Input"
                  style={`top:${PORT_TOP - PORT_R}px; left:${-PORT_R}px;`}
                  onMouseDown={(e) => e.stopPropagation()}
                  onMouseUp={(e) => { e.stopPropagation(); finishWire(node); }} />
              )}
              {outputs(node.kind).map((port, i) => (
                <Fragment key={port.id}>
                  {port.label && (
                    <span class="flow-port-label" style={`top:${PORT_TOP + i * PORT_GAP - 0.5 * REM}px;`}>{port.label}</span>
                  )}
                  <div class={`flow-port flow-port-out flow-port-${port.id}`}
                    style={`top:${PORT_TOP + i * PORT_GAP - PORT_R}px; right:${-PORT_R}px;`}
                    title={`Output: ${port.id}`}
                    onMouseDown={(e) => startWire(e, node, port.id)} />
                </Fragment>
              ))}
            </div>
          ))}
        </div>
      </div>

      <div class="flow-inspector">
        {!selected && <div class="hint">Select a node to edit it, or add one from the palette.</div>}
        {selected && (
          <Fragment>
            <div class="wizard-section-title">{KIND_LABEL[selected.kind]}</div>

            {selected.kind === 'trigger.button' && (
              <div class="wizard-field">
                <label>Action name</label>
                <input class="prop-input" value={selected.params.name ?? ''}
                  placeholder="e.g. open_valve"
                  onInput={(e) => patchParams(selected.id, { name: (e.target as HTMLInputElement).value })} />
                <p class="hint">Fires when a button widget's Action is set to this name.</p>
              </div>
            )}

            {(selected.kind === 'trigger.threshold' || selected.kind === 'trigger.change') && (() => {
              return (
                <Fragment>
                  <div class="wizard-field">
                    <label>Signal</label>
                    <SignalPicker value={selected.params.signal ?? ''} options={allSignalOptions()}
                      onOpen={openAllSignals}
                      onChange={(ref) => patchParams(selected.id, { signal: ref })} />
                  </div>
                  {selected.kind === 'trigger.threshold' && (
                    <div class="wizard-field wizard-field-row">
                      <div class="wizard-field" style="flex:0 0 4.5rem;">
                        <label>Op</label>
                        <select class="prop-select" value={selected.params.op ?? '>'}
                          onChange={(e) => patchParams(selected.id, { op: (e.target as HTMLSelectElement).value })}>
                          {THRESHOLD_OPS.map(o => <option key={o} value={o}>{o}</option>)}
                        </select>
                      </div>
                      <div class="wizard-field" style="flex:1;">
                        <label>Value</label>
                        <input class="prop-input" value={selected.params.value ?? ''}
                          onInput={(e) => patchParams(selected.id, { value: (e.target as HTMLInputElement).value })} />
                      </div>
                    </div>
                  )}
                  {selected.kind === 'trigger.change' && <p class="hint">Fires whenever the signal's value changes.</p>}
                </Fragment>
              );
            })()}

            {(selected.kind === 'trigger.timer' || selected.kind === 'trigger.loop') && (
              <div class="wizard-field">
                <label>Interval (ms)</label>
                <input class="prop-input" type="number" value={selected.params.interval ?? '1000'}
                  onInput={(e) => patchParams(selected.id, { interval: (e.target as HTMLInputElement).value })} />
                {selected.kind === 'trigger.loop' && <p class="hint">Runs once on panel load, then on every interval.</p>}
              </div>
            )}

            {selected.kind === 'gate.and' && (
              <p class="hint">Wire two or more triggers into this gate. The flow continues only when all
                inputs are satisfied (e.g. a Button click while a Threshold is currently true).</p>
            )}

            {selected.kind === 'flow.if' && (
              <ExprField label="Condition" value={selected.params.cond ?? ''}
                onChange={(v) => patchParams(selected.id, { cond: v })}
                onInsert={(ds, sig) => insertRef(selected.id, 'cond', ds, sig)}
                allSignals={allSignalOptions()} onOpenSignals={openAllSignals}
                hint="True → 'then' port, false → 'else' port. e.g. {stub:level} > 5 && temp < 100. {sys:time}=epoch seconds, {sys:dt}=seconds since the trigger last fired." />
            )}

            {selected.kind === 'flow.loop' && (
              <Fragment>
                <div class="wizard-field">
                  <label>Mode</label>
                  <select class="prop-select" value={selected.params.mode ?? 'count'}
                    onChange={(e) => patchParams(selected.id, { mode: (e.target as HTMLSelectElement).value })}>
                    <option value="count">Repeat N times</option>
                    <option value="while">While condition</option>
                  </select>
                </div>
                {(selected.params.mode ?? 'count') === 'count' ? (
                  <div class="wizard-field">
                    <label>Repeat count</label>
                    <input class="prop-input" type="number" value={selected.params.count ?? '3'}
                      onInput={(e) => patchParams(selected.id, { count: (e.target as HTMLInputElement).value })} />
                  </div>
                ) : (
                  <ExprField label="While condition" value={selected.params.cond ?? ''}
                    onChange={(v) => patchParams(selected.id, { cond: v })}
                    onInsert={(ds, sig) => insertRef(selected.id, 'cond', ds, sig)}
                    allSignals={allSignalOptions()} onOpenSignals={openAllSignals}
                    hint="Loops the 'body' port while true (capped). e.g. {stub:level} < 90" />
                )}
                <p class="hint">Runs the <b>body</b> port repeatedly, then continues on <b>done</b>.</p>
              </Fragment>
            )}

            {selected.kind === 'action.write' && (
              <Fragment>
                <div class="wizard-field">
                  <label>Target signal / variable</label>
                  <SignalPicker value={selected.params.target ?? ''} options={allSignalOptions()}
                    onOpen={openAllSignals} allowFreeform
                    onChange={(ref) => patchParams(selected.id, { target: ref })} />
                  <p class="hint">Pick a signal, or type <code>local:name</code> to write a panel-local variable.</p>
                </div>
                <ExprField label="Value (expression)" value={selected.params.expr ?? ''}
                  onChange={(v) => patchParams(selected.id, { expr: v })}
                  onInsert={(ds2, sig) => insertRef(selected.id, 'expr', ds2, sig)}
                  allSignals={allSignalOptions()} onOpenSignals={openAllSignals}
                  hint="e.g. ({stub:a} - {stub:b}) / {sys:dt} for a rate — or a constant like 42. {sys:time}=epoch seconds, {sys:dt}=seconds since the trigger last fired." />
              </Fragment>
            )}

            {selected.kind === 'action.delay' && (
              <div class="wizard-field">
                <label>Delay (ms)</label>
                <input class="prop-input" type="number" value={selected.params.ms ?? '0'}
                  onInput={(e) => patchParams(selected.id, { ms: (e.target as HTMLInputElement).value })} />
              </div>
            )}

            {selected.kind === 'action.accumulate' && (
              <Fragment>
                <div class="wizard-field">
                  <label>Array name</label>
                  <input class="prop-input" value={selected.params.array ?? ''} list="flow-array-names"
                    placeholder="e.g. data"
                    onInput={(e) => patchParams(selected.id, { array: (e.target as HTMLInputElement).value })} />
                </div>
                <ExprField label="Value (expression)" value={selected.params.expr ?? ''}
                  onChange={(v) => patchParams(selected.id, { expr: v })}
                  onInsert={(ds2, sig) => insertRef(selected.id, 'expr', ds2, sig)}
                  allSignals={allSignalOptions()} onOpenSignals={openAllSignals}
                  hint="Appends this value (with a timestamp) to the array. e.g. {stub:level}, or {sys:time} for the clock." />
              </Fragment>
            )}

            {selected.kind === 'action.export' && (
              <ExportEditor
                columns={parseExportColumns(selected)}
                align={selected.params.align ?? 'common'}
                filename={selected.params.filename ?? ''}
                onColumns={(cols) => patchParams(selected.id, { columns: JSON.stringify(cols) })}
                onAlign={(a) => patchParams(selected.id, { align: a })}
                onFilename={(f) => patchParams(selected.id, { filename: f })} />
            )}

            {selected.kind === 'action.clear' && (
              <div class="wizard-field">
                <label>Array name</label>
                <input class="prop-input" value={selected.params.array ?? ''} list="flow-array-names"
                  placeholder="e.g. data"
                  onInput={(e) => patchParams(selected.id, { array: (e.target as HTMLInputElement).value })} />
                <p class="hint">Empties the named array (e.g. to start a fresh capture).</p>
              </div>
            )}

            {selected.kind === 'action.log' && (
              <Fragment>
                <div class="wizard-field">
                  <label>Label (optional)</label>
                  <input class="prop-input" value={selected.params.label ?? ''}
                    placeholder="prefix for the console line"
                    onInput={(e) => patchParams(selected.id, { label: (e.target as HTMLInputElement).value })} />
                </div>
                <ExprField label="Value (expression)" value={selected.params.expr ?? ''}
                  onChange={(v) => patchParams(selected.id, { expr: v })}
                  onInsert={(ds2, sig) => insertRef(selected.id, 'expr', ds2, sig)}
                  allSignals={allSignalOptions()} onOpenSignals={openAllSignals}
                  hint="Logs this value to the browser console — handy for debugging a flow." />
              </Fragment>
            )}

            {selected.kind === 'action.widget' && (() => {
              const op = selected.params.op ?? 'disable';
              const plotOnly = op === 'clearplot' || op === 'pauseplot' || op === 'resumeplot';
              const choices = plotOnly ? widgets.filter(w => w.type === 'plot') : widgets;
              return (
                <Fragment>
                  <div class="wizard-field">
                    <label>Action</label>
                    <select class="prop-input" value={op}
                      onChange={(e) => patchParams(selected.id, { op: (e.target as HTMLSelectElement).value })}>
                      <option value="enable">Enable</option>
                      <option value="disable">Disable</option>
                      <option value="show">Show</option>
                      <option value="hide">Hide</option>
                      <option value="clearplot">Clear plot</option>
                      <option value="pauseplot">Pause plot</option>
                      <option value="resumeplot">Resume plot</option>
                    </select>
                  </div>
                  <div class="wizard-field">
                    <label>Widget</label>
                    <select class="prop-input" value={selected.params.widget ?? ''}
                      onChange={(e) => patchParams(selected.id, { widget: (e.target as HTMLSelectElement).value })}>
                      <option value="">— select a widget —</option>
                      {choices.map(w => (
                        <option value={w.id}>{widgetLabel(w)}</option>
                      ))}
                    </select>
                    {plotOnly && <p class="hint">Only plot widgets are listed for this action.</p>}
                  </div>
                </Fragment>
              );
            })()}

            {(selected.kind === 'trigger.start' || selected.kind === 'trigger.stop') && (
              <p class="hint">{selected.kind === 'trigger.start'
                ? 'Fires once when the panel is opened — use it to initialise state or greet the operator.'
                : 'Fires once when the panel is closed — use it to write a safe value or log a closing message. Only immediate (no-delay) actions are guaranteed to run.'}</p>
            )}

            {(selected.kind === 'action.dialog.info' || selected.kind === 'action.dialog.error') && (
              <Fragment>
                <div class="wizard-field">
                  <label>Title</label>
                  <input class="prop-input" value={selected.params.title ?? ''}
                    placeholder={selected.kind === 'action.dialog.error' ? 'Error' : 'Info'}
                    onInput={(e) => patchParams(selected.id, { title: (e.target as HTMLInputElement).value })} />
                </div>
                <div class="wizard-field">
                  <label>Message</label>
                  <textarea class="prop-input" rows={4} value={selected.params.message ?? ''}
                    placeholder="Shown to the operator. Embed values as {ds:name}."
                    onInput={(e) => patchParams(selected.id, { message: (e.target as HTMLTextAreaElement).value })} />
                  <p class="hint">Waits for the operator to dismiss the dialog before continuing.
                    Embed live values with <code>{'{ds:name}'}</code> (e.g. Level is {'{stub:level}'}).</p>
                </div>
                <DialogFieldsEditor allowInput={false}
                  fields={parseDialogFields(selected)}
                  onChange={(f) => patchParams(selected.id, { fields: JSON.stringify(f) })}
                  allSignals={allSignalOptions()} onOpenSignals={openAllSignals} />
              </Fragment>
            )}

            {selected.kind === 'action.dialog.setpoint' && (
              <Fragment>
                <div class="wizard-field">
                  <label>Title</label>
                  <input class="prop-input" value={selected.params.title ?? ''}
                    placeholder="Set value"
                    onInput={(e) => patchParams(selected.id, { title: (e.target as HTMLInputElement).value })} />
                </div>
                <div class="wizard-field">
                  <label>Message</label>
                  <textarea class="prop-input" rows={3} value={selected.params.message ?? ''}
                    placeholder="Prompt shown above the fields. Embed values as {ds:name}."
                    onInput={(e) => patchParams(selected.id, { message: (e.target as HTMLTextAreaElement).value })} />
                </div>
                <DialogFieldsEditor allowInput={true}
                  fields={parseDialogFields(selected)}
                  onChange={(f) => patchParams(selected.id, { fields: JSON.stringify(f) })}
                  allSignals={allSignalOptions()} onOpenSignals={openAllSignals} />
                <p class="hint">Each <b>input</b> prompts the operator and writes the entered number to its
                  target on OK; each <b>display</b> shows a live value. Cancel aborts the flow.</p>
              </Fragment>
            )}

            <button class="panel-btn" style="margin-top:1rem;" onClick={() => deleteNode(selected.id)}>Delete node</button>
          </Fragment>
        )}
      </div>
    </div>
  );

  function paletteBtn(entry: PaletteEntry) {
    return (
      <button key={entry.kind} class={`flow-palette-btn flow-palette-${category(entry.kind)}`}
        title={`${entry.kind} — drag onto the canvas or click to add`}
        draggable
        onDragStart={(e) => {
          e.dataTransfer?.setData(DRAG_MIME, entry.kind);
          if (e.dataTransfer) e.dataTransfer.effectAllowed = 'copy';
        }}
        onClick={() => addNode(entry)}>{entry.label}</button>
    );
  }
}

// Expression input with a signal-insert helper and live validation.
function ExprField({ label, value, onChange, onInsert, allSignals, onOpenSignals, hint }: {
  label: string;
  value: string;
  onChange: (v: string) => void;
  onInsert: (ds: string, sig: string) => void;
  allSignals: SignalOption[];
  onOpenSignals: () => void;
  hint: string;
}) {
  const err = checkExpr(value);
  return (
    <div class="wizard-field">
      <label>{label}</label>
      <input class={`prop-input${err ? ' prop-input-error' : ''}`} value={value}
        placeholder="expression"
        onInput={(e) => onChange((e.target as HTMLInputElement).value)} />
      {err && <p class="wizard-error">{err}</p>}
      <div class="flow-expr-insert">
        <SignalPicker value="" options={allSignals} onOpen={onOpenSignals}
          placeholder="+ insert signal"
          onChange={(ref) => { const s = splitRef(ref); onInsert(s.ds, s.name); }} />
      </div>
      <p class="hint">{hint}</p>
    </div>
  );
}

// ── Export CSV: multi-column editor ──────────────────────────────────────────

interface ExportColumn { array: string; label: string; }

// The columns configured on an action.export node. Reads the JSON `columns`
// param; falls back to the legacy single `array` param (so old panels migrate
// transparently the first time the node is edited).
function parseExportColumns(n: LogicNode): ExportColumn[] {
  const raw = (n.params.columns ?? '').trim();
  if (raw) {
    try {
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed)) {
        return parsed.map((c: any) => ({ array: String(c?.array ?? ''), label: String(c?.label ?? '') }));
      }
    } catch {}
  }
  const a = (n.params.array ?? '').trim();
  return [{ array: a, label: '' }];
}

const ALIGN_OPTS: Array<{ value: string; label: string; hint: string }> = [
  { value: 'common', label: 'Common rows', hint: 'Only timestamps present in every array (intersection).' },
  { value: 'any', label: 'All rows', hint: 'Union of all timestamps; missing cells are left blank.' },
  { value: 'interpolate', label: 'Interpolate', hint: 'Union of all timestamps; missing cells linearly interpolated.' },
];

function ExportEditor({ columns, align, filename, onColumns, onAlign, onFilename }: {
  columns: ExportColumn[];
  align: string;
  filename: string;
  onColumns: (cols: ExportColumn[]) => void;
  onAlign: (a: string) => void;
  onFilename: (f: string) => void;
}) {
  function patch(i: number, patch: Partial<ExportColumn>) {
    onColumns(columns.map((c, j) => (j === i ? { ...c, ...patch } : c)));
  }
  const alignHint = ALIGN_OPTS.find(o => o.value === align)?.hint ?? '';
  return (
    <Fragment>
      <div class="wizard-field">
        <label>Columns (arrays)</label>
        {columns.map((c, i) => (
          <div key={i} class="flow-row-edit">
            <input class="prop-input" value={c.array} list="flow-array-names"
              placeholder="array name"
              onInput={(e) => patch(i, { array: (e.target as HTMLInputElement).value })} />
            <input class="prop-input" value={c.label}
              placeholder="column label (optional)"
              onInput={(e) => patch(i, { label: (e.target as HTMLInputElement).value })} />
            <button class="flow-node-del" title="Remove column"
              disabled={columns.length <= 1}
              onClick={() => onColumns(columns.filter((_, j) => j !== i))}>✕</button>
          </div>
        ))}
        <button class="panel-btn flow-row-add" onClick={() => onColumns([...columns, { array: '', label: '' }])}>+ Add column</button>
      </div>
      {columns.length > 1 && (
        <div class="wizard-field">
          <label>Alignment</label>
          <select class="prop-select" value={align} onChange={(e) => onAlign((e.target as HTMLSelectElement).value)}>
            {ALIGN_OPTS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
          <p class="hint">{alignHint}</p>
        </div>
      )}
      <div class="wizard-field">
        <label>File name</label>
        <input class="prop-input" value={filename}
          placeholder="defaults to the column labels"
          onInput={(e) => onFilename((e.target as HTMLInputElement).value)} />
        <p class="hint">Downloads a CSV with columns: timestamp_ms, iso, then one column per array.</p>
      </div>
    </Fragment>
  );
}

// ── Dialogs: multi-field editor ──────────────────────────────────────────────

interface DialogFieldSpec { type: 'input' | 'display'; label: string; target?: string; default?: string; expr?: string; }

// The fields configured on an action.dialog.* node. Reads the JSON `fields`
// param; for a legacy set-point node it synthesises a single input from the old
// target/default params so existing panels migrate transparently.
function parseDialogFields(n: LogicNode): DialogFieldSpec[] {
  const raw = (n.params.fields ?? '').trim();
  if (raw) {
    try {
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed)) {
        return parsed.map((f: any) => ({
          type: f?.type === 'display' ? 'display' : 'input',
          label: String(f?.label ?? ''),
          target: String(f?.target ?? ''),
          default: String(f?.default ?? ''),
          expr: String(f?.expr ?? ''),
        }));
      }
    } catch {}
  }
  if (n.kind === 'action.dialog.setpoint') {
    return [{ type: 'input', label: '', target: n.params.target ?? '', default: n.params.default ?? '', expr: '' }];
  }
  return [];
}

function DialogFieldsEditor({ allowInput, fields, onChange, allSignals, onOpenSignals }: {
  allowInput: boolean;
  fields: DialogFieldSpec[];
  onChange: (fields: DialogFieldSpec[]) => void;
  allSignals: SignalOption[];
  onOpenSignals: () => void;
}) {
  function patch(i: number, patch: Partial<DialogFieldSpec>) {
    onChange(fields.map((f, j) => (j === i ? { ...f, ...patch } : f)));
  }
  function add(type: 'input' | 'display') {
    onChange([...fields, { type, label: '', target: '', default: '', expr: '' }]);
  }
  return (
    <div class="wizard-field">
      <label>{allowInput ? 'Fields' : 'Displayed values'}</label>
      {fields.length === 0 && <p class="hint">None yet.</p>}
      {fields.map((f, i) => {
        return (
          <div key={i} class="flow-field-edit">
            <div class="flow-field-head">
              {allowInput ? (
                <select class="prop-select" value={f.type}
                  onChange={(e) => patch(i, { type: (e.target as HTMLSelectElement).value as 'input' | 'display' })}>
                  <option value="input">Input</option>
                  <option value="display">Display</option>
                </select>
              ) : <span class="flow-field-kind">Display</span>}
              <input class="prop-input" value={f.label} placeholder="label"
                onInput={(e) => patch(i, { label: (e.target as HTMLInputElement).value })} />
              <button class="flow-node-del" title="Remove field"
                onClick={() => onChange(fields.filter((_, j) => j !== i))}>✕</button>
            </div>
            {f.type === 'input' ? (
              <Fragment>
                <div class="flow-row-edit">
                  <SignalPicker value={f.target ?? ''} options={allSignals}
                    onOpen={onOpenSignals} allowFreeform placeholder="target signal…"
                    onChange={(ref) => patch(i, { target: ref })} />
                </div>
                <input class={`prop-input${checkExpr(f.default ?? '') ? ' prop-input-error' : ''}`}
                  value={f.default ?? ''} placeholder="default value (expression, optional)"
                  onInput={(e) => patch(i, { default: (e.target as HTMLInputElement).value })} />
              </Fragment>
            ) : (
              <input class={`prop-input${checkExpr(f.expr ?? '') ? ' prop-input-error' : ''}`}
                value={f.expr ?? ''} placeholder="value to show (expression, e.g. {stub:level})"
                onInput={(e) => patch(i, { expr: (e.target as HTMLInputElement).value })} />
            )}
          </div>
        );
      })}
      <div class="flow-field-add">
        {allowInput && <button class="panel-btn flow-row-add" onClick={() => add('input')}>+ Add input</button>}
        <button class="panel-btn flow-row-add" onClick={() => add('display')}>+ Add value to show</button>
      </div>
    </div>
  );
}

// A human-friendly label for a widget in the Widget-control picker: its type
// plus its first signal (or id) so panels with many widgets stay legible.
function widgetLabel(w: Widget): string {
  const sig = w.signals[0] ? `${w.signals[0].ds}:${w.signals[0].name}` : '';
  return `${w.type}${sig ? ' · ' + sig : ''} (${w.id})`;
}

// One-line summary shown in a node block's body.
function nodeSummary(n: LogicNode): string {
  switch (n.kind) {
    case 'trigger.button':    return n.params.name || '(unnamed)';
    case 'trigger.threshold': return `${n.params.signal || '?'} ${n.params.op || '>'} ${n.params.value || '0'}`;
    case 'trigger.change':    return `Δ ${n.params.signal || '?'}`;
    case 'trigger.timer':     return `every ${n.params.interval || '?'} ms`;
    case 'trigger.loop':      return `load + every ${n.params.interval || '?'} ms`;
    case 'trigger.start':     return 'on panel open';
    case 'trigger.stop':      return 'on panel close';
    case 'gate.and':          return 'all inputs';
    case 'flow.if':           return n.params.cond || '(no condition)';
    case 'flow.loop':         return (n.params.mode ?? 'count') === 'count'
                                ? `repeat ${n.params.count || '0'}×`
                                : `while ${n.params.cond || '?'}`;
    case 'action.write':      return `${n.params.target || '?'} = ${n.params.expr || ''}`;
    case 'action.delay':      return `wait ${n.params.ms || '0'} ms`;
    case 'action.accumulate': return `${n.params.array || '?'} ← ${n.params.expr || ''}`;
    case 'action.export': {
      const cols = parseExportColumns(n).filter(c => c.array);
      const names = cols.map(c => c.label || c.array).join(', ');
      return `export ${names || '?'} → csv`;
    }
    case 'action.clear':      return `clear ${n.params.array || '?'}`;
    case 'action.log':        return `log ${n.params.label ? n.params.label + ': ' : ''}${n.params.expr || ''}`;
    case 'action.widget':     return `${n.params.op || 'disable'} ${n.params.widget || '?'}`;
    case 'action.dialog.info':     return `info: ${n.params.title || n.params.message || '…'}`;
    case 'action.dialog.error':    return `error: ${n.params.title || n.params.message || '…'}`;
    case 'action.dialog.setpoint': {
      const fs = parseDialogFields(n);
      const ins = fs.filter(f => f.type === 'input').length;
      const shows = fs.filter(f => f.type === 'display').length;
      const parts: string[] = [];
      if (ins) parts.push(`ask ${ins}`);
      if (shows) parts.push(`show ${shows}`);
      return parts.length ? parts.join(', ') : 'ask…';
    }
    default:                  return '';
  }
}

// Inline editor for the panel's local state variables, shown in the logic
// palette so they can be created without leaving the (full-width) logic tab.
function LocalVars({ statevars, onChange }: {
  statevars: StateVar[];
  onChange: (vars: StateVar[]) => void;
}) {
  const [open, setOpen] = useState(false);
  const [name, setName] = useState('');
  const [type, setType] = useState<'number' | 'bool' | 'string'>('number');
  const [initial, setInitial] = useState('0');

  function add() {
    const n = name.trim();
    if (!n) return;
    onChange([...statevars.filter(v => v.name !== n), { name: n, type, initial }]);
    setName('');
    setInitial('0');
  }

  return (
    <div class="flow-localvars">
      <div class="flow-palette-title">Local vars</div>
      {statevars.length === 0 && <div class="hint flow-localvars-empty">None yet.</div>}
      {statevars.map(v => (
        <div key={v.name} class="flow-localvar-row">
          <span class="flow-localvar-name" title={`${v.type ?? 'number'} = ${v.initial}`}>{v.name}</span>
          <button class="flow-node-del" title="Remove"
            onClick={() => onChange(statevars.filter(x => x.name !== v.name))}>✕</button>
        </div>
      ))}
      {!open && (
        <button class="flow-palette-btn flow-localvar-add" onClick={() => setOpen(true)}>+ Add variable</button>
      )}
      {open && (
        <div class="flow-localvar-form">
          <input class="prop-input" value={name} placeholder="name"
            onInput={(e) => setName((e.target as HTMLInputElement).value)} />
          <select class="prop-select" value={type}
            onChange={(e) => setType((e.target as HTMLSelectElement).value as 'number' | 'bool' | 'string')}>
            <option value="number">number</option>
            <option value="bool">bool</option>
            <option value="string">string</option>
          </select>
          <input class="prop-input" value={initial} placeholder="initial"
            onInput={(e) => setInitial((e.target as HTMLInputElement).value)} />
          <div class="flow-localvar-actions">
            <button class="panel-btn" onClick={add}>Add</button>
            <button class="panel-btn" onClick={() => { setOpen(false); setName(''); }}>Cancel</button>
          </div>
        </div>
      )}
    </div>
  );
}
