diff --git a/package.json b/package.json
index ca01657..6818f0a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "esp-emu-peris",
- "version": "0.2.0",
+ "version": "0.3.1",
"private": true,
"type": "module",
"description": "Simulated peripherals for the esp-emulator browser (wasm) build",
@@ -9,8 +9,8 @@
"check": "tsc -p tsconfig.json --noEmit",
"serve": "python serve.py",
"sync": "python sync_www.py",
- "test": "node --test tests/unit/ && node tests/m1_smoke.mjs",
- "test:unit": "node --test tests/unit/",
+ "test": "node --test tests/unit/core.test.mjs tests/unit/devices.test.mjs && node tests/m1_smoke.mjs",
+ "test:unit": "node --test tests/unit/core.test.mjs tests/unit/devices.test.mjs",
"test:smoke": "node tests/m1_smoke.mjs",
"test:m0": "node tests/m0_probe.mjs"
},
diff --git a/peris/devices/button.js b/peris/devices/button.js
index 0259c81..5d666eb 100644
--- a/peris/devices/button.js
+++ b/peris/devices/button.js
@@ -6,7 +6,7 @@
*/
export class Device {
static typeId = 'button';
- static version = '1.0.0';
+ static version = '1.1.0';
static title = 'Push button';
static category = 'basic';
static description = 'Momentary button driving a GPIO input (active-low by default).';
@@ -14,7 +14,8 @@ export class Device {
static pins(cfg) { return [cfg.pin]; }
static configSchema = [
{ key: 'pin', label: 'GPIO pin', type: 'pin' },
- { key: 'activeLow', label: 'Active low (pull-up wiring)', type: 'checkbox' },
+ // chip:'pull' → the row shows V (pull-up) when activeLow, G otherwise
+ { key: 'activeLow', label: 'Active low (pull-up wiring)', type: 'checkbox', chip: 'pull', pullWhenChecked: 'high' },
{ key: 'name', label: 'Label', type: 'text' },
];
@@ -72,8 +73,28 @@ export class View {
this.btn = btn;
el.appendChild(btn);
}
+
+ /* Compact row icon: press-and-hold, same press/release commands. */
+ rowIcon(el) {
+ el.title = 'Push — press and hold';
+ const b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'icon-press';
+ b.innerHTML = '
';
+ const down = (e) => { e.preventDefault(); this.send('press'); b.classList.add('down'); };
+ const up = (e) => { if (e) e.preventDefault(); this.send('release'); b.classList.remove('down'); };
+ b.addEventListener('pointerdown', down);
+ b.addEventListener('pointerup', up);
+ b.addEventListener('pointerleave', up);
+ b.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') down(e); });
+ b.addEventListener('keyup', up);
+ el.appendChild(b);
+ this.iconBtn = b;
+ }
+
render(state) {
this.btn.classList.toggle('down', !!state.pressed);
this.btn.textContent = state.pressed ? 'PRESSED' : 'PRESS';
+ if (this.iconBtn) this.iconBtn.classList.toggle('down', !!state.pressed);
}
}
diff --git a/peris/devices/led.js b/peris/devices/led.js
index 81a71b7..45b852c 100644
--- a/peris/devices/led.js
+++ b/peris/devices/led.js
@@ -5,7 +5,7 @@
*/
export class Device {
static typeId = 'led';
- static version = '1.0.0';
+ static version = '1.1.1';
static title = 'LED';
static category = 'basic';
static description = 'Single-colour LED wired to a GPIO output.';
@@ -45,8 +45,12 @@ export class Device {
}
export class View {
- constructor(el, sendCmd) {
+ constructor(el, sendCmd, host) {
this.el = el;
+ this.send = sendCmd;
+ this.host = host; // host.updateConfig({color}) powers the row icon picker
+ this.colour = '#ff4d4d';
+ this.dot = null;
el.className = 'dev-body led-body';
const bulb = document.createElement('div');
bulb.className = 'led-bulb';
@@ -62,13 +66,46 @@ export class View {
el.appendChild(wrap);
el.appendChild(cap);
}
+
+ /* Compact row icon: a mini bulb that lights with the state; clicking it
+ * opens the colour picker and writes the choice back into the config. */
+ rowIcon(el) {
+ el.title = 'LED — click to change colour';
+ const dot = document.createElement('span');
+ dot.className = 'led-dot';
+ const picker = document.createElement('input');
+ picker.type = 'color';
+ picker.className = 'led-dot-picker';
+ picker.addEventListener('change', () => {
+ if (this.host) this.host.updateConfig({ color: picker.value });
+ else this.setColour(picker.value);
+ });
+ dot.appendChild(picker);
+ dot.addEventListener('click', () => { picker.value = this.colour; picker.click(); });
+ el.appendChild(dot);
+ this.dot = dot;
+ this.setColour(this.colour); // keep icon and bulb in lock-step
+ }
+
+ /* The ONE colour setter — applies to every LED visual (details bulb +
+ * row dot). Never style one of them anywhere else. */
+ setColour(color) {
+ this.colour = color || '#ff4d4d';
+ const bulb = this.halo ? this.halo.parentElement : null;
+ for (const t of [bulb, this.dot]) {
+ if (t) t.style.setProperty('--led-color', this.colour);
+ }
+ }
+
render(state) {
const bulb = this.halo.parentElement;
bulb.classList.toggle('lit', !!state.on);
bulb.classList.toggle('floating', state.driven === false);
this.cap.textContent = state.driven === false ? 'not driven by firmware' : (state.on ? 'ON' : 'OFF');
+ if (this.dot) this.dot.classList.toggle('lit', !!state.on);
}
+
setConfig(cfg) {
- this.halo.parentElement.style.setProperty('--led-color', cfg.color || '#ff4d4d');
+ this.setColour(cfg.color);
}
}
diff --git a/peris/devices/toggle-switch.js b/peris/devices/toggle-switch.js
index 5ec8222..3f9e7a7 100644
--- a/peris/devices/toggle-switch.js
+++ b/peris/devices/toggle-switch.js
@@ -4,7 +4,7 @@
*/
export class Device {
static typeId = 'toggle-switch';
- static version = '1.0.0';
+ static version = '1.1.0';
static title = 'Toggle switch';
static category = 'basic';
static description = 'Latching switch driving a GPIO input level.';
@@ -12,7 +12,8 @@ export class Device {
static pins(cfg) { return [cfg.pin]; }
static configSchema = [
{ key: 'pin', label: 'GPIO pin', type: 'pin' },
- { key: 'activeLow', label: 'Active low', type: 'checkbox' },
+ // chip:'pull' → the row shows V (pull-up) when activeLow, G otherwise
+ { key: 'activeLow', label: 'Active low', type: 'checkbox', chip: 'pull', pullWhenChecked: 'high' },
{ key: 'initial', label: 'Initial ON state', type: 'checkbox' },
{ key: 'name', label: 'Label', type: 'text' },
];
@@ -61,8 +62,22 @@ export class View {
this.text = wrap.querySelector('.sw-text');
el.appendChild(wrap);
}
+
+ /* Compact row icon: mini slide switch, click to toggle. */
+ rowIcon(el) {
+ el.title = 'Switch — click to toggle';
+ const b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'icon-toggle';
+ b.innerHTML = '
';
+ b.addEventListener('click', () => this.send('toggle'));
+ el.appendChild(b);
+ this.iconBtn = b;
+ }
+
render(state) {
this.input.checked = !!state.on;
this.text.textContent = state.on ? 'ON' : 'OFF';
+ if (this.iconBtn) this.iconBtn.classList.toggle('on', !!state.on);
}
}
diff --git a/peris/devices/touch-button.js b/peris/devices/touch-button.js
index c39ea63..fdb29f4 100644
--- a/peris/devices/touch-button.js
+++ b/peris/devices/touch-button.js
@@ -10,16 +10,20 @@ import { Device as ButtonDevice } from './button.js';
export class Device extends ButtonDevice {
static typeId = 'touch-button';
- static version = '1.0.0';
+ static version = '1.1.0';
static title = 'Touch button';
static category = 'basic';
static description = 'Touch-style pad driving a GPIO input (touch peripheral itself is not emulated — firmware must read it as GPIO).';
static defaultConfig = { name: 'Touch', pin: 10, activeLow: true };
static pins(cfg) { return [cfg.pin]; }
- static configSchema = ButtonDevice.configSchema;
+ static configSchema = ButtonDevice.configSchema; // includes the V/G pull chip
// idleLevel/pressedLevel inherited from button.js (verified levels).
}
+const TOUCH_SVG = `
`;
+
export class View {
constructor(el, sendCmd) {
this.send = sendCmd;
@@ -28,9 +32,7 @@ export class View {
pad.className = 'touch-pad';
pad.setAttribute('role', 'button');
pad.tabIndex = 0;
- pad.innerHTML = `
TOUCH`;
+ pad.innerHTML = `${TOUCH_SVG}
TOUCH`;
const down = (e) => { e.preventDefault(); sendCmd('press'); pad.classList.add('active'); };
const up = (e) => { if (e) e.preventDefault(); sendCmd('release'); pad.classList.remove('active'); };
pad.addEventListener('pointerdown', down);
@@ -41,7 +43,27 @@ export class View {
this.pad = pad;
el.appendChild(pad);
}
+
+ /* Compact row icon: press-and-hold fingertip. */
+ rowIcon(el) {
+ el.title = 'Touch — press and hold';
+ const b = document.createElement('button');
+ b.type = 'button';
+ b.className = 'icon-touch';
+ b.innerHTML = TOUCH_SVG;
+ const down = (e) => { e.preventDefault(); this.send('press'); b.classList.add('down'); };
+ const up = (e) => { if (e) e.preventDefault(); this.send('release'); b.classList.remove('down'); };
+ b.addEventListener('pointerdown', down);
+ b.addEventListener('pointerup', up);
+ b.addEventListener('pointerleave', up);
+ b.addEventListener('keydown', (e) => { if (e.key === ' ' || e.key === 'Enter') down(e); });
+ b.addEventListener('keyup', up);
+ el.appendChild(b);
+ this.iconBtn = b;
+ }
+
render(state) {
this.pad.classList.toggle('active', !!state.pressed);
+ if (this.iconBtn) this.iconBtn.classList.toggle('down', !!state.pressed);
}
}
diff --git a/peris/peris.css b/peris/peris.css
index f93410d..78dbb47 100644
--- a/peris/peris.css
+++ b/peris/peris.css
@@ -2,40 +2,56 @@
* esp-emu-peris panel styles. The stock page CSS stays inline in
* esp-emu-peris.html (kept verbatim from upstream for easy re-diff);
* everything peris-specific lives here.
+ *
+ * Layout: console | peripherals (one compact row per device, expandable
+ * details) | debug — all three visible at once. The peripherals panel has
+ * its own scrolling area so the toolbar stays in view.
*/
-/* ---- right panel tabs ---- */
-.panel-tabs {
- display: flex;
- border-bottom: 1px solid var(--border);
- position: sticky; top: 0;
- background: var(--surface);
- z-index: 2;
-}
-.ptab {
- flex: 1;
- background: none;
- border: 0;
- border-bottom: 2px solid transparent;
- color: var(--muted);
- padding: 10px 8px 8px;
- font-size: 11.5px;
- font-weight: 600;
+/* ---- right-side panel heads (peripherals + debug) ---- */
+.panel-head {
+ padding: 8px 14px 7px;
+ font-size: 10.5px;
text-transform: uppercase;
- letter-spacing: 0.8px;
- cursor: pointer;
+ letter-spacing: 0.9px;
+ color: var(--muted);
+ font-weight: 600;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface);
+ flex-shrink: 0;
+ user-select: none;
+}
+.main-area > .peris-panel {
+ width: 360px;
+ background: var(--surface);
+ border-left: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ flex-shrink: 0;
+}
+/* Higher specificity than the stock `.side-panel` width rules so both
+ right-hand panels always share the same width. */
+@media (max-width: 1400px) {
+ .main-area > .peris-panel, .main-area > .side-panel { width: 300px; }
+}
+@media (max-width: 1150px) {
+ .main-area > .peris-panel, .main-area > .side-panel { width: 260px; }
+}
+@media (max-width: 860px) {
+ .main-area > .peris-panel, .main-area > .side-panel {
+ width: 100%;
+ border-left: 0;
+ border-bottom: 1px solid var(--border);
+ }
}
-.ptab:hover { color: var(--text); }
-.ptab.active { color: var(--accent-text); border-bottom-color: var(--accent); }
-.pane.hidden { display: none; }
/* ---- peripherals panel ---- */
-#pane-peris { flex: 1; display: flex; flex-direction: column; }
#pane-peris.over { outline: 2px dashed var(--accent); outline-offset: -6px; }
.peris-toolbar {
- display: flex; gap: 6px; padding: 10px 14px;
+ display: flex; gap: 6px; padding: 10px 12px;
border-bottom: 1px solid var(--border);
- position: sticky; top: 41px; background: var(--surface); z-index: 1;
+ flex-shrink: 0;
}
.peris-toolbar select {
flex: 1; min-width: 0;
@@ -48,38 +64,170 @@
padding: 6px 14px; border-bottom: 1px solid var(--border);
line-height: 1.5;
}
-#peris-cards { flex: 1; padding: 12px 14px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }
+.peris-scroll {
+ flex: 1; min-height: 0;
+ overflow-y: auto;
+ scrollbar-width: thin;
+ scrollbar-color: var(--border-strong) transparent;
+}
+#peris-cards { padding: 10px 12px 14px; display: flex; flex-direction: column; gap: 6px; }
#peris-empty { padding: 22px 10px; text-align: center; color: var(--faint); font-size: 12px; line-height: 1.6; }
#peris-empty strong { color: var(--muted); }
-.peris-card {
+/* ---- device rows (photoshop-layers style) ---- */
+.peris-row {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius);
- padding: 10px 12px;
}
-.peris-card-head {
- display: flex; align-items: center; gap: 8px;
- margin-bottom: 8px;
+.peris-row.dragging { opacity: 0.5; border-style: dashed; }
+.peris-row-main {
+ display: flex; align-items: center; gap: 6px;
+ padding: 4px 4px 4px 8px;
+ min-height: 32px;
}
-.dev-title { font-weight: 600; font-size: 12.5px; }
-.dev-type { color: var(--faint); font-size: 10.5px; font-family: var(--mono); }
-.dev-remove { margin-left: auto; padding: 1px 7px; font-size: 13px; color: var(--muted); }
-.dev-remove:hover { color: var(--err); border-color: var(--err); }
-.dev-editor { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; margin-top: 10px; }
+/* interactive device icon (built by View.rowIcon; fallback = letter chip) */
+.dev-icon { width: 24px; height: 24px; flex-shrink: 0; display: grid; place-items: center; }
+.dev-icon-plain {
+ width: 18px; height: 18px; border-radius: 5px;
+ background: var(--surface-3); border: 1px solid var(--border-strong);
+ color: var(--muted); font-size: 10px; font-weight: 700;
+}
+
+/* name: click to rename in place */
+.dev-name-slot { min-width: 0; display: flex; }
+.dev-name {
+ background: none; border: 0; padding: 2px 5px; border-radius: 4px;
+ color: var(--text); font-size: 12px; font-weight: 600; text-align: left;
+ max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+ cursor: text;
+}
+.dev-name:hover { background: var(--surface-3); outline: 1px dashed var(--border-strong); }
+.dev-name-input {
+ background: var(--bg); border: 1px solid var(--accent); border-radius: 4px;
+ color: var(--text); font-size: 12px; font-weight: 600; padding: 1px 4px;
+ width: 110px; outline: none;
+}
+
+/* pin config chips: GPIO number box + V/G pull badge */
+.dev-chip-slot { display: flex; }
+.dev-chip {
+ font-family: var(--mono); font-size: 11px; font-weight: 600; line-height: 1;
+ min-width: 22px; height: 20px; padding: 0 4px; text-align: center;
+ background: var(--bg); color: var(--accent-text);
+ border: 1px solid var(--border-strong); border-radius: 4px; cursor: pointer;
+ display: inline-flex; align-items: center; justify-content: center;
+ transition: border-color 0.1s, color 0.1s;
+}
+.dev-chip:hover { border-color: var(--accent); }
+.dev-chip-pull.v { color: var(--ok); }
+.dev-chip-pull.g { color: var(--faint); }
+.dev-chip-select {
+ font-family: var(--mono); font-size: 11px; height: 20px; padding: 0 2px;
+ background: var(--bg); color: var(--text);
+ border: 1px solid var(--accent); border-radius: 4px; outline: none;
+ max-width: 130px;
+}
+
+/* right-aligned row controls: expand / remove / drag handle */
+.row-spacer { flex: 1; }
+.row-btn {
+ width: 22px; height: 22px; flex-shrink: 0;
+ background: none; border: 0; border-radius: 4px; color: var(--faint);
+ display: grid; place-items: center; cursor: pointer;
+ font-size: 13px; line-height: 1;
+ transition: background 0.1s, color 0.1s;
+}
+.row-btn:hover { background: var(--surface-3); color: var(--text); }
+.row-remove:hover { color: var(--err); }
+.row-expand svg { transition: transform 0.12s; }
+.peris-row.open .row-expand svg { transform: rotate(90deg); }
+.row-grip {
+ width: 18px; height: 22px; flex-shrink: 0;
+ display: grid; place-items: center;
+ color: var(--faint); cursor: grab; font-size: 11px;
+ border-radius: 4px; user-select: none;
+}
+.row-grip:hover { color: var(--muted); background: var(--surface-3); }
+
+/* expandable details (blender-style, one property per line) */
+.peris-details { display: none; border-top: 1px dashed var(--border); padding: 8px 10px 10px; }
+.peris-row.open .peris-details { display: block; }
+.dev-body-wrap { display: flex; justify-content: center; padding: 2px 0 6px; }
+
+.dev-editor {
+ display: grid; grid-template-columns: 1fr; gap: 5px;
+ margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border);
+}
.dev-field { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--muted); }
.dev-field span { flex: 1; white-space: nowrap; }
.dev-field input[type="text"], .dev-field input[type="number"], .dev-field select {
- width: 96px; flex-shrink: 0;
+ width: 118px; flex-shrink: 0;
background: var(--bg); border: 1px solid var(--border-strong);
border-radius: 4px; padding: 3px 6px; font-size: 11.5px; outline: none;
}
.dev-field input[type="color"] { width: 34px; height: 22px; padding: 0; border: 1px solid var(--border-strong); border-radius: 4px; background: var(--bg); }
.dev-field input[type="checkbox"] { accent-color: var(--accent); }
-.dev-field select { max-width: 96px; }
+.dev-field select { max-width: 118px; }
-/* device view bodies */
+/* ---- row icon widgets (built by each plugin's View.rowIcon) ---- */
+.led-dot {
+ --led-color: #ff4d4d;
+ display: block; width: 14px; height: 14px; border-radius: 50%;
+ border: 2px solid color-mix(in srgb, var(--led-color) 45%, #2a2a30);
+ background: radial-gradient(circle at 38% 32%, color-mix(in srgb, var(--led-color) 16%, #1a1a1e), #101014 72%);
+ position: relative; cursor: pointer;
+ transition: box-shadow 0.15s, border-color 0.15s;
+}
+.led-dot.lit {
+ background: radial-gradient(circle at 38% 32%, #ffffff33 0%, var(--led-color) 55%, color-mix(in srgb, var(--led-color) 55%, #20060a) 100%);
+ box-shadow: 0 0 10px 2px color-mix(in srgb, var(--led-color) 55%, transparent);
+ border-color: var(--led-color);
+}
+/* the native colour picker is opened straight from a transparent overlay */
+.led-dot-picker { position: absolute; inset: -4px; opacity: 0; width: calc(100% + 8px); height: calc(100% + 8px); cursor: pointer; border: 0; padding: 0; }
+
+.icon-press {
+ width: 22px; height: 22px; padding: 0; border-radius: 50%;
+ border: 1px solid var(--border-strong); background: var(--surface-3);
+ color: var(--muted); cursor: pointer; display: grid; place-items: center;
+ box-shadow: 0 2px 0 #0a0a0c;
+ transition: transform 0.05s, box-shadow 0.05s, color 0.1s, border-color 0.1s;
+}
+.icon-press.down, .icon-press:active {
+ transform: translateY(2px); box-shadow: 0 0 0 #0a0a0c;
+ color: var(--accent-text); border-color: var(--accent);
+}
+.icon-touch {
+ width: 22px; height: 22px; padding: 0; border-radius: 50%;
+ border: 1px solid var(--border-strong); background: var(--surface-3);
+ color: var(--muted); cursor: pointer; display: grid; place-items: center;
+ transition: color 0.1s, border-color 0.1s, box-shadow 0.1s;
+}
+.icon-touch svg { width: 13px; height: 13px; }
+.icon-touch.down, .icon-touch:active {
+ color: var(--accent-text); border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-soft);
+}
+
+.icon-toggle { background: none; border: 0; padding: 2px; cursor: pointer; display: block; }
+.mini-sw {
+ width: 26px; height: 14px; border-radius: 999px;
+ background: var(--bg); border: 1px solid var(--border-strong);
+ position: relative; display: block;
+ transition: background 0.15s, border-color 0.15s;
+}
+.mini-sw i {
+ position: absolute; top: 1.5px; left: 1.5px;
+ width: 9px; height: 9px; border-radius: 50%;
+ background: var(--surface-3); border: 1px solid var(--border-strong);
+ transition: left 0.15s, background 0.15s;
+}
+.icon-toggle.on .mini-sw { background: var(--accent-soft); border-color: var(--accent); }
+.icon-toggle.on .mini-sw i { left: 14px; background: var(--accent); border-color: var(--accent); }
+
+/* ---- device view bodies (inside expanded details) ---- */
.dev-body { min-height: 74px; display: flex; align-items: center; justify-content: center; }
.led-wrap { display: flex; align-items: center; justify-content: center; padding: 6px 0; }
.led-bulb {
diff --git a/src/core/types.ts b/src/core/types.ts
index 9e24960..f1241ea 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -34,7 +34,11 @@ export interface DeviceCtx {
log(text: string, level?: 'info' | 'warn' | 'error'): void;
}
-/** One field of the generic config editor. */
+/**
+ * One field of the generic config editor. A `pin` field is additionally
+ * shown as the compact GPIO-number chip in the device row; a checkbox field
+ * marked `chip: 'pull'` is shown as the V/G idle-pull badge (click to flip).
+ */
export interface ConfigField {
key: string;
label: string;
@@ -43,6 +47,10 @@ export interface ConfigField {
max?: number;
step?: number;
options?: Array<{ value: string | number; label: string }>;
+ /** Render this checkbox as the row's V/G pull badge. */
+ chip?: 'pull';
+ /** Idle pull level while the checkbox is checked ('high' → V, 'low' → G). Defaults to 'high'. */
+ pullWhenChecked?: 'high' | 'low';
}
/** A device entry in a wiring config: { id, type, ...settings }. */
@@ -83,13 +91,28 @@ export interface DeviceLike {
destroy?(): void;
}
+/**
+ * Page-side handle handed to a View so it can request config changes
+ * (e.g. the LED row icon's colour picker) without touching the panel.
+ */
+export interface ViewHost {
+ /** Merge a patch into this device's config, re-apply and persist. */
+ updateConfig(patch: Record
): void;
+}
+
/** Page-side view of a device plugin. */
export interface ViewLike {
render(state: Record): void;
setConfig?(cfg: DeviceConfig): void;
+ /**
+ * Optional: build the compact interactive icon shown in the device row
+ * (click actions: LED → colour picker, button → press, switch → toggle).
+ * The same render(state) calls drive it; `el` is a ~24px container.
+ */
+ rowIcon?(el: HTMLElement): void;
}
export interface ViewClass {
- new (el: HTMLElement, sendCmd: (cmd: string, params?: Record) => void): ViewLike;
+ new (el: HTMLElement, sendCmd: (cmd: string, params?: Record) => void, host?: ViewHost): ViewLike;
}
export interface DeviceModule {
diff --git a/src/core/version.ts b/src/core/version.ts
index 0398611..cd9daf9 100644
--- a/src/core/version.ts
+++ b/src/core/version.ts
@@ -2,4 +2,4 @@
* Project version (keep in sync with package.json and docs/CHANGELOG.md).
* Device plugins carry their own per-device version in Device.version.
*/
-export const PERIS_VERSION = '0.2.0';
+export const PERIS_VERSION = '0.3.1';
diff --git a/src/ui/app.ts b/src/ui/app.ts
index 475fa7f..ffb783a 100644
--- a/src/ui/app.ts
+++ b/src/ui/app.ts
@@ -168,10 +168,11 @@ class App {
this.applyMachineInfo({ ...this.machine, ...msg });
this.term.writeln('\x1b[33m[System] Emulator reset\x1b[0m');
this.term.writeln(`\x1b[32m[System] Firmware reloaded, PC=0x${msg.pc.toString(16)}\x1b[0m`);
- this.firmwareLoaded = true;
- this.debug.renderCores([msg.pc], this.machine.chip);
- this.perf.updateTiles(0);
- this.setStatus('ready', 'Ready');
+ this.firmwareLoaded = true;
+ this.debug.renderCores([msg.pc], this.machine.chip);
+ this.perf.updateTiles(0);
+ this.peris.refreshForChip(); // pin options follow the (possibly new) chip
+ this.setStatus('ready', 'Ready');
} else {
this.term.writeln('\x1b[33m[System] Emulator reset (no firmware)\x1b[0m');
this.firmwareLoaded = false;
diff --git a/src/ui/config-editor.ts b/src/ui/config-editor.ts
index 2ae87e8..e9c9912 100644
--- a/src/ui/config-editor.ts
+++ b/src/ui/config-editor.ts
@@ -1,7 +1,9 @@
/*
* Generic device config editor, driven by the device's configSchema.
- * Rendered inside each peripheral card; edits update the config object
- * through onChange and are then applied to the worker by the panel.
+ * Rendered in each peripheral row's expanded details; edits update the
+ * config object through onChange and are then applied to the worker by
+ * the panel. Also exports rowChips() and fillPinOptions() — the shared
+ * helpers behind the compact GPIO / V·G chips shown inline in the row.
*/
import type { ConfigField, DeviceConfig, DeviceMeta } from '../core/types.js';
@@ -13,6 +15,23 @@ export interface EditorDeps {
onChange(): void;
}
+/**
+ * Fill a