feat(web): two-pane "cockpit" redesign with a live board rail

Restructure the workshop flow into the designer's cockpit: a persistent shell
(header + 5-step stepper + sticky instrument rail) wrapping the phase routes via
a React-Router layout route, so the rail stays mounted across navigation.

- Design system: IBM Plex Mono + Newsreader; the full cockpit token set (light
  + dark) in index.css; a working light/dark theme toggle (store `theme` +
  useApplyTheme); a `switch` ui primitive.
- Shell: CockpitLayout, Stepper (forward-gated), PanelChrome helpers. Every
  phase page restyled to the editorial panels + the WORKSHOP-FLOW fixes
  (channels-after-bind, domain framing + L1 prefill, L2/L3 prefill at 3/3,
  in-place submission finale). Store gains `tried` + `channels.saidHi` (v4).
- Live rail (CockpitRail): real node heartbeat + agent activity log + ADD
  progress; sim telemetry (useTelemetry) for the waveform/accel/I2C behind a
  seam, marked SIM.
- LED-matrix PIXEL MIRROR (real): the rail shows exactly what the physical
  matrix displays — API GET /nodes/:team/matrix reads the board's framebuffer
  off the :9999 relay (readMatrixFrame + the `matrixget` relay command);
  useMatrixMirror polls it and unpacks the 104 bits.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-23 07:38:14 -07:00
co-authored by Claude Opus 4.8
parent 6c82b79b01
commit 1a39457940
28 changed files with 1240 additions and 434 deletions
+66
View File
@@ -0,0 +1,66 @@
import { useEffect, useRef } from 'react'
/**
* The live-acceleration waveform. Reads the magnitude history from a ref and
* redraws via requestAnimationFrame — independent of React renders, so it stays
* smooth. Line turns to the spike colour during an impact event.
*/
export function WaveformCanvas({
wave,
impact,
}: {
wave: React.MutableRefObject<number[]>
impact: boolean
}) {
const ref = useRef<HTMLCanvasElement>(null)
const impactRef = useRef(impact)
impactRef.current = impact
useEffect(() => {
const cv = ref.current
if (!cv) return
const ctx = cv.getContext('2d')
if (!ctx) return
const dpr = window.devicePixelRatio || 1
const resize = () => {
cv.width = cv.clientWidth * dpr
cv.height = cv.clientHeight * dpr
}
resize()
window.addEventListener('resize', resize)
let raf = 0
const draw = () => {
const w = cv.width
const h = cv.height
ctx.clearRect(0, 0, w, h)
// center baseline
ctx.strokeStyle = 'rgba(255,255,255,0.06)'
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(0, h / 2)
ctx.lineTo(w, h / 2)
ctx.stroke()
// waveform
const buf = wave.current
const n = buf.length
ctx.strokeStyle = impactRef.current ? '#ff8a5c' : '#7fa8ff'
ctx.lineWidth = 1.5 * dpr
ctx.beginPath()
for (let i = 0; i < n; i++) {
const x = (i / (n - 1)) * w
const y = h / 2 - buf[i] * (h * 0.42)
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)
}
ctx.stroke()
raf = requestAnimationFrame(draw)
}
raf = requestAnimationFrame(draw)
return () => {
cancelAnimationFrame(raf)
window.removeEventListener('resize', resize)
}
}, [wave])
return <canvas ref={ref} className="block h-[78px] w-full rounded-[10px] border border-rail-line bg-rail-inset" />
}