/** * PA100Panel — ABOUTAUDIO PA100 amplifier HMI, rendered at its native * 480 x 272 and scaled to fill its parent. * * Drop-in: no dependencies beyond React 16.8+, no CSS file, no build config. * * import PA100Panel from './PA100Panel'; *
* * The parent controls the size. By default the panel takes 100% width and * holds a 480:272 box; pass `fit="contain"` to fit a parent with a fixed * height instead. * * Props * interactive bool swipe / tap enabled (true) * live bool animate meters and charts (true) * showBoot bool play the 5 s boot screen on mount (true) * page number 0 channels, 1 diag, 2 status, 3 setup (0) * sub number sub-view within the page (0) * fit string 'width' | 'contain' ('width') * maxScale number cap the upscale (2 = never past 2x) (0 = none) * meterStyle string 'led' | 'grad' | 'cap' ('led') * chartStyle string 'area' | 'line' | 'tech' ('area') * navStyle string 'dots' | 'rail' | 'ticks' ('dots') * condition string 'ok' | 'warn' | 'alarm' | 'overtemp' ('ok') * device object { ip, model, serial, firmware, mac } * seed number varies the simulated signal (4) * loadFonts bool inject the IBM Plex once (true) * className, style forwarded to the outer wrapper * * Wrapped in an IIFE and exposed as window.PA100Panel so it can be dropped * into a plain HTML page (via Babel standalone) alongside other scripts * without leaking its top-level consts into the global scope. */ (function () { const { useState, useRef, useEffect, useCallback, useMemo } = React; /* ---------------------------------------------------------------- constants */ const W = 480, H = 272, RAIL = 52, VIEW = W - RAIL, HEAD = 52, BODY = H - HEAD; const BH = 156; // meter track height const SEG = 6; // LED segment period: 4 px lit + 2 px gap const DB_FLOOR = -60; const C = { screen: '#0B0D0F', plate: '#141A1F', track: '#171E23', rung: '#242E35', gap: '#0E1317', sel: '#193039', btn: '#17303A', bar: '#242D34', ink: '#E6EDF3', ink2: '#D4DDE3', ink3: '#C3CCD3', label: '#AEB9C0', dim: '#93A0A8', dim2: '#8D979E', dim3: '#8A949B', off: '#48525A', cyan: '#35C4E8', green: '#3FCE7C', meterGreen: '#2FBF71', orange: '#EF7C0F', amber: '#FFB020', yellow: '#F2C94C', red: '#FF4D4D', hot: '#FF6B35', violet: '#7B6BF0', blue: '#5A7FC4', }; const MONO = "'IBM Plex Mono',ui-monospace,monospace"; const SANS = "'IBM Plex Sans Condensed',system-ui,sans-serif"; const ICONS = { ok: { c: C.green, d: 'M12 3.2a8.8 8.8 0 100 17.6 8.8 8.8 0 000-17.6M7.6 12.2l3 3 5.9-6.3' }, warn: { c: C.amber, d: 'M12 3.6L21.4 20H2.6zM12 9.6v4.9M12 17.3v.1' }, alarm: { c: C.red, d: 'M12 3.2a8.8 8.8 0 100 17.6 8.8 8.8 0 000-17.6M8.5 8.5l7 7M15.5 8.5l-7 7' }, overtemp: { c: C.hot, d: 'M10 14.4V5.6a2 2 0 014 0v8.8a4 4 0 11-4 0M12 8.6v5.2' }, mute: { c: C.yellow, d: 'M10.8 5L6.4 8.9H3.2v6.2h3.2l4.4 3.9zM15.4 9.6l5 4.8M20.4 9.6l-5 4.8' }, usb: { c: C.violet, d: 'M12 20.8V5.2M12 5.2L9.6 8.6h4.8zM12 14.6l4.8-4.6V7.8M16.4 5.6h3v3h-3zM12 17.4L7.4 12.8V10M6 9.4h3v3H6z' }, lan: { c: C.green, d: 'M9 3.4h6v4.2H9zM2.6 16.4h5.2v4.2H2.6zM16.2 16.4h5.2v4.2h-5.2zM12 7.6v3.4M5.2 16.4v-2.6h13.6v2.6M12 11v2.8' }, }; const CONDITIONS = ['ok', 'warn', 'alarm', 'overtemp']; const SERIES = [ { key: 'supply', tab: 'SUPPLY', unit: 'V', lo: 40, hi: 60, nom: 52, amp: 0.9, dec: 1, c: C.cyan, th: 47, ticks: [40, 45, 50, 55, 60] }, { key: 'temp', tab: 'TEMP', unit: '\u00B0C', lo: 20, hi: 100, nom: 46, amp: 4.5, dec: 0, c: C.hot, th: 75, ticks: [20, 40, 60, 80, 100] }, { key: 'power', tab: 'POWER', unit: 'W', lo: 0, hi: 1200, nom: 430, amp: 150, dec: 0, c: C.green, th: 1000, ticks: [0, 300, 600, 900, 1200] }, ]; // chart plot box inside a 396 x 148 svg const N = 120, PX = 50, PW = 346, PY = 12, PHh = 126; const DEVICE = { ip: '192.168.001.148', model: 'PA100', serial: 'A7K2M9QX', firmware: '2.4.1', mac: '4C:2E:9A:11:0B:73', }; const PARAMS = [ { n: 'INPUT GAIN', v: 6, min: -100, max: 12, st: 0.5, u: 'dB', dec: 1 }, { n: 'HIGH-PASS', v: 80, min: 20, max: 200, st: 5, u: 'Hz', dec: 0 }, { n: 'LIMITER', v: -3, min: -12, max: 0, st: 0.5, u: 'dB', dec: 1 }, { n: 'MUTE', en: ['OFF', 'ON'], ei: 0 }, { n: 'STANDBY', v: 30, min: 0, max: 120, st: 5, u: 'min', dec: 0 }, ]; const LOGO_MARK_A = 'M87.51 46.1L78.81 46.1L95.81 2L104.31 2L121.31 46.1L112.61 46.1L100.21 13Z'; const LOGO_MARK_B = 'M87.51 46.1L78.81 46.1L95.81 16.7L104.31 16.7L121.31 46.1L112.61 46.1L100.21 24.1Z'; const LOGO_WORD = [ 'M58.121 65.414C56.721 65.414 55.521 65.914 54.622 66.913C53.722 67.913 53.222 69.113 53.222 70.612C53.222 72.212 53.722 73.412 54.622 74.411C55.521 75.411 56.721 75.911 58.121 75.911C59.52 75.911 60.72 75.411 61.62 74.411C62.52 73.412 63.02 72.212 63.02 70.612C63.02 69.113 62.52 67.813 61.62 66.813C60.72 65.813 59.52 65.414 58.121 65.414M58.021 61.115C59.721 61.115 61.32 61.515 62.82 62.414C64.319 63.214 65.519 64.414 66.319 65.914C67.119 67.413 67.619 68.913 67.619 70.612C67.619 72.312 67.219 73.912 66.319 75.411C65.519 76.911 64.319 78.011 62.92 78.91C61.42 79.71 59.821 80.11 58.121 80.11C55.521 80.11 53.322 79.21 51.522 77.411C49.723 75.611 48.823 73.311 48.823 70.713C48.823 67.913 49.823 65.613 51.922 63.714C53.622 61.915 55.722 61.115 58.021 61.115', 'M71.217 61.614L75.816 61.614L75.816 70.213C75.816 71.912 75.916 73.112 76.116 73.712C76.316 74.411 76.716 74.911 77.216 75.211C77.716 75.611 78.315 75.711 79.115 75.711C79.915 75.711 80.515 75.511 81.015 75.211C81.514 74.911 81.914 74.311 82.215 73.611C82.414 73.112 82.514 72.011 82.514 70.312L82.514 61.514L87.013 61.514L87.013 69.113C87.013 72.212 86.813 74.411 86.313 75.511C85.714 76.911 84.814 78.01 83.614 78.711C82.414 79.51 80.915 79.81 79.115 79.81C77.115 79.81 75.516 79.41 74.316 78.51C73.116 77.61 72.217 76.411 71.717 74.811C71.317 73.712 71.217 71.712 71.217 68.913Z', 'M92.112 55.017L96.611 55.017L96.611 61.615L99.31 61.615L99.31 65.514L96.611 65.514L96.611 79.611L92.112 79.611L92.112 65.514L89.813 65.514L89.813 61.615L92.112 61.615Z', 'M108.809 79.511L104.01 79.511L113.408 55.216L118.106 55.216L127.404 79.511L122.605 79.511L115.706 61.715Z', 'M5.733 79.71L0.934 79.71L10.332 55.416L15.031 55.416L24.329 79.71L19.53 79.71L12.631 61.915Z', 'M130.503 61.614L135.102 61.614L135.102 70.213C135.102 71.912 135.201 73.112 135.401 73.712C135.602 74.411 136.001 74.911 136.501 75.211C137.001 75.611 137.601 75.711 138.4 75.711C139.2 75.711 139.801 75.511 140.301 75.211C140.8 74.911 141.2 74.311 141.5 73.611C141.7 73.112 141.8 72.011 141.8 70.312L141.8 61.514L146.299 61.514L146.299 69.113C146.299 72.212 146.099 74.411 145.599 75.511C144.999 76.911 144.1 78.01 142.899 78.711C141.7 79.51 140.2 79.81 138.4 79.81C136.401 79.81 134.802 79.41 133.602 78.51C132.402 77.61 131.503 76.411 131.003 74.811C130.603 73.712 130.503 71.712 130.503 68.913Z', 'M172.593 61.615L177.091 61.615L177.091 79.611L172.593 79.611ZM174.792 54.217C175.592 54.217 176.291 54.517 176.791 55.117C177.392 55.716 177.591 56.416 177.591 57.216C177.591 58.016 177.291 58.716 176.791 59.316C176.191 59.915 175.592 60.215 174.792 60.215C173.992 60.215 173.292 59.915 172.792 59.316C172.192 58.716 171.992 58.016 171.992 57.216C171.992 56.416 172.293 55.716 172.792 55.216C173.393 54.517 173.992 54.217 174.792 54.217', 'M189.689 65.414C188.289 65.414 187.089 65.914 186.189 66.913C185.289 67.913 184.789 69.113 184.789 70.612C184.789 72.212 185.289 73.412 186.189 74.411C187.089 75.411 188.289 75.911 189.689 75.911C191.088 75.911 192.287 75.411 193.188 74.411C194.086 73.412 194.586 72.212 194.586 70.612C194.586 69.113 194.086 67.813 193.188 66.813C192.287 65.813 191.088 65.414 189.689 65.414M189.587 61.115C191.289 61.115 192.887 61.515 194.386 62.414C195.886 63.214 197.085 64.414 197.885 65.914C198.685 67.413 199.185 68.913 199.185 70.612C199.185 72.312 198.786 73.912 197.885 75.411C197.085 76.911 195.886 78.011 194.487 78.91C192.988 79.71 191.388 80.11 189.689 80.11C187.089 80.11 184.89 79.21 183.09 77.411C181.291 75.611 180.39 73.311 180.39 70.713C180.39 67.913 181.39 65.613 183.49 63.714C185.189 61.915 187.29 61.115 189.587 61.115', 'M40 74.401C39.1 75.401 37.9 75.9 36.501 75.9C35.101 75.9 33.901 75.401 33.002 74.401C32.102 73.401 31.602 72.201 31.602 70.602C31.602 69.103 32.102 67.902 33.002 66.903C33.901 65.903 35.101 65.403 36.501 65.403C37.9 65.403 39.1 65.804 40 66.803C40.9 67.803 41.4 69.103 41.4 70.602C41.4 72.201 40.9 73.401 40 74.401M44.699 65.903C43.899 64.403 42.699 63.205 41.2 62.405C39.7 61.504 38.101 61.105 36.401 61.105C34.699 61.105 33.108 61.547 31.707 62.504L31.707 54.617L27.208 54.617L27.208 70.606C27.208 70.638 27.203 70.669 27.203 70.703C27.203 73.301 28.103 75.601 29.902 77.401C31.702 79.2 33.901 80.099 36.501 80.099C38.201 80.099 39.8 79.7 41.3 78.901C42.699 78 43.899 76.901 44.699 75.401C45.599 73.901 45.999 72.302 45.999 70.602C45.999 68.902 45.499 67.402 44.699 65.903', 'M154.272 70.603C154.272 69.103 154.772 67.803 155.671 66.803C156.571 65.804 157.772 65.404 159.171 65.404C160.57 65.404 161.771 65.904 162.669 66.904C163.57 67.903 164.07 69.103 164.07 70.603C164.07 72.202 163.57 73.402 162.669 74.402C161.771 75.401 160.57 75.901 159.171 75.901C157.772 75.901 156.571 75.401 155.671 74.402C154.772 73.402 154.272 72.202 154.272 70.603M149.673 70.603C149.673 72.302 150.074 73.902 150.972 75.401C151.772 76.901 152.972 78 154.373 78.901C155.872 79.701 157.47 80.1 159.171 80.1C161.771 80.1 163.969 79.201 165.77 77.401C167.568 75.601 168.468 73.302 168.468 70.703C168.468 70.669 168.464 70.639 168.464 70.607L168.464 54.617L163.964 54.617L163.964 62.503C162.563 61.546 160.973 61.105 159.271 61.105C157.571 61.105 155.972 61.504 154.472 62.405C152.972 63.205 151.772 64.404 150.972 65.904C150.173 67.403 149.673 68.903 149.673 70.603', ]; /* ------------------------------------------------------------------ helpers */ const rnd = (s) => { const x = Math.sin(s * 127.1 + 311.7) * 43758.5453; return x - Math.floor(x); }; const frac = (db) => (Math.max(DB_FLOOR, Math.min(0, db)) - DB_FLOOR) / -DB_FLOOR; const snap = (f) => Math.min(BH, Math.ceil((f * BH) / SEG) * SEG); const pad2 = (n) => (n < 10 ? '0' : '') + n; const fmtParam = (p) => (p.en ? p.en[p.ei] : p.v.toFixed(p.dec)); const initLevels = (seed) => { const a = []; for (let i = 0; i < 8; i++) a.push(-3 - 30 * Math.pow(rnd(seed * 7.3 + i * 2.1), 1.3)); return a; }; const initHist = (seed) => SERIES.map((s, si) => { const a = []; for (let i = 0; i < N; i++) { a.push(s.nom + s.amp * Math.sin(i * 0.09 + seed + si) + s.amp * 0.5 * Math.sin(i * 0.31 + seed * 3) + s.amp * 0.35 * (rnd(i + seed * 11 + si * 5) - 0.5) * 2); } return a; }); let fontsInjected = false; function useIBMPlex(enabled) { useEffect(() => { if (!enabled || fontsInjected || typeof document === 'undefined') return; if (document.querySelector('link[data-pa100-fonts]')) { fontsInjected = true; return; } const l = document.createElement('link'); l.rel = 'stylesheet'; l.setAttribute('data-pa100-fonts', ''); l.href = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans+Condensed:wght@400;500;600;700&display=swap'; document.head.appendChild(l); fontsInjected = true; }, [enabled]); } /** keyframes the component needs, injected once */ let kfInjected = false; function useKeyframes() { useEffect(() => { if (kfInjected || typeof document === 'undefined') return; if (document.querySelector('style[data-pa100-kf]')) { kfInjected = true; return; } const s = document.createElement('style'); s.setAttribute('data-pa100-kf', ''); s.textContent = '@keyframes pa100blink{0%,100%{opacity:1}50%{opacity:.25}}' + '@keyframes pa100in{from{opacity:0;transform:scale(.94)}to{opacity:1;transform:none}}' + '@keyframes pa100bar{from{width:0}to{width:100%}}'; document.head.appendChild(s); kfInjected = true; }, []); } /* -------------------------------------------------------------- scale shell */ function useFitScale(fit, maxScale) { const host = useRef(null); const [k, setK] = useState(1); useEffect(() => { const el = host.current; if (!el || typeof ResizeObserver === 'undefined') return; const measure = () => { const r = el.getBoundingClientRect(); if (!r.width) return; let s = fit === 'contain' && r.height ? Math.min(r.width / W, r.height / H) : r.width / W; if (maxScale) s = Math.min(s, maxScale); setK(s); }; measure(); const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, [fit, maxScale]); return [host, k]; } /* ------------------------------------------------------------------- pieces */ function Tabs({ items }) { return (
{items.map((t) => (
{t.n}
))}
); } function Header({ title, children }) { return (
{title}
{children}
); } function Bar({ bar, style, labelSize }) { const led = style === 'led'; return (
{bar.label}
{led && (
)}
{bar.val}
); } function Scale({ ticks }) { return (
{ticks.map((t) => (
{t.t}
))}
); } function Chart({ c, style }) { return (
{c.cur} {c.unit}
{style === 'tech' && ( )} {style === 'line' && ( )} {style === 'area' && } {style === 'tech' && } {c.yl.map((l) => (
{l.t}
))} {style === 'tech' && (
{c.cur}
)}
); } /* ---------------------------------------------------------------- component */ function PA100Panel({ interactive = true, live = true, showBoot = true, page: pageProp = 0, sub: subProp = 0, fit = 'width', maxScale = 0, meterStyle = 'led', chartStyle = 'area', navStyle = 'dots', condition = 'ok', device, seed = 4, loadFonts = true, className, style: styleProp, }) { useIBMPlex(loadFonts); useKeyframes(); const [host, k] = useFitScale(fit, maxScale); const dev = useMemo(() => ({ ...DEVICE, ...(device || {}) }), [device]); const [page, setPage] = useState(pageProp); const [sub, setSub] = useState(subProp); const [drag, setDrag] = useState({ dx: 0, dy: 0, anim: true }); const [meters, setMeters] = useState(() => { const lv = initLevels(seed); return { lv, pk: lv.slice(), hold: lv.map(() => 0) }; }); const [hist, setHist] = useState(() => initHist(seed)); const [up, setUp] = useState(51845); const [params, setParams] = useState(PARAMS); const [netType, setNetType] = useState('STATIC'); const [edit, setEdit] = useState(-1); const [boot, setBoot] = useState(showBoot); const [bootOut, setBootOut] = useState(false); const [cond, setCond] = useState(condition); const [tog, setTog] = useState({ usb: false, lan: true }); useEffect(() => { if (!interactive) setPage(pageProp); }, [pageProp, interactive]); useEffect(() => { if (!interactive) setSub(subProp); }, [subProp, interactive]); useEffect(() => { setCond(condition); }, [condition]); /* boot */ const bootTimers = useRef([]); const startBoot = useCallback(() => { bootTimers.current.forEach(clearTimeout); bootTimers.current = []; setBoot(true); setBootOut(false); setPage(0); setSub(0); bootTimers.current.push(setTimeout(() => setBootOut(true), 4550)); bootTimers.current.push(setTimeout(() => setBoot(false), 5000)); }, []); useEffect(() => { if (!showBoot) return undefined; startBoot(); return () => bootTimers.current.forEach(clearTimeout); }, [showBoot, startBoot]); /* telemetry */ useEffect(() => { if (!live) return undefined; const t1 = setInterval(() => { setMeters((s) => { const now = Date.now(), spike = now % 6500 < 420; const lv = s.lv.map((v, i) => { const tgt = i === 4 && spike ? -0.2 - 0.6 * rnd(now / 200) : -4 - 26 * rnd(now / 900 + i * 3.7); return v + (tgt - v) * (tgt > v ? 0.7 : 0.12); }); const pk = s.pk.map((p, i) => (lv[i] >= p ? lv[i] : s.hold[i] > 0 ? p : p - 0.45)); const hold = s.hold.map((h, i) => (lv[i] >= s.pk[i] ? 500 : Math.max(0, h - 90))); return { lv, pk, hold }; }); }, 90); const t2 = setInterval(() => { setUp((u) => u + 1); setHist((hs) => hs.map((a, si) => { const cfg = SERIES[si], last = a[a.length - 1]; const nv = last + (cfg.nom - last) * 0.08 + cfg.amp * (Math.random() - 0.5) * 0.6; return a.slice(1).concat([Math.max(cfg.lo, Math.min(cfg.hi, nv))]); })); }, 500); return () => { clearInterval(t1); clearInterval(t2); }; }, [live]); /* gestures */ const subCount = (p) => (p === 0 ? 2 : p === 1 ? 3 : p === 2 ? 2 : 2); const g = useRef(null); const onDown = (e) => { if (!interactive || edit >= 0 || boot) return; g.current = { x: e.clientX, y: e.clientY, ax: 0, k: k || 1, moved: false, id: e.pointerId, el: e.currentTarget }; }; const onMove = (e) => { const s = g.current; if (!s) return; const dx = (e.clientX - s.x) / s.k, dy = (e.clientY - s.y) / s.k; if (!s.moved && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) { s.moved = true; // capture only once it is a drag, so a stationary press stays a tap if (s.el.setPointerCapture) { try { s.el.setPointerCapture(s.id); } catch (_) { /* noop */ } } } if (!s.moved) return; if (!s.ax) s.ax = Math.abs(dy) > Math.abs(dx) ? 'y' : 'x'; if (s.ax === 'y') setDrag({ dx: 0, dy: Math.max(-120, Math.min(120, dy)), anim: false }); else setDrag({ dy: 0, dx: Math.max(-160, Math.min(160, dx)), anim: false }); }; const onUp = () => { const s = g.current; if (!s) return; g.current = null; let wrapped = false, np = page, ns = sub; if (s.ax === 'y' && Math.abs(drag.dy) > 26) { const t = page + (drag.dy < 0 ? 1 : -1); wrapped = t < 0 || t > 3; np = (t + 4) % 4; ns = 0; } if (s.ax === 'x' && Math.abs(drag.dx) > 30) { const cnt = subCount(np), t = sub + (drag.dx < 0 ? 1 : -1); wrapped = wrapped || t < 0 || t > cnt - 1; ns = (t + cnt) % cnt; } setPage(np); setSub(ns); setDrag({ dx: 0, dy: 0, anim: !wrapped }); if (wrapped) setTimeout(() => setDrag({ dx: 0, dy: 0, anim: true }), 30); }; const bump = (dir) => setParams((ps) => ps.map((p, i) => { if (i !== edit) return p; if (p.en) return { ...p, ei: (p.ei + dir + p.en.length) % p.en.length }; return { ...p, v: Math.max(p.min, Math.min(p.max, +(p.v + dir * p.st).toFixed(2))) }; })); const hold = useRef({}); const stopHold = useCallback(() => { const h = hold.current; clearTimeout(h.t); clearInterval(h.i); clearTimeout(h.a); hold.current = {}; }, []); const startHold = useCallback((dir) => { bump(dir); stopHold(); if (params[edit] && params[edit].en) return; hold.current.t = setTimeout(() => { hold.current.i = setInterval(() => bump(dir), 260); hold.current.a = setTimeout(() => { clearInterval(hold.current.i); hold.current.i = setInterval(() => bump(dir), 70); }, 1600); }, 420); }, [edit]); useEffect(() => () => stopHold(), [stopHold]); /* ------------------------------------------------------------ derived */ const muted = params[3].ei === 1; const railIcons = [ { key: cond, ...ICONS[cond], lit: true, tap: () => setCond(CONDITIONS[(CONDITIONS.indexOf(cond) + 1) % 4]) }, { key: 'mute', ...ICONS.mute, lit: muted, tap: null }, { key: 'usb', ...ICONS.usb, lit: !!tog.usb, tap: () => setTog((t) => ({ ...t, usb: !t.usb })) }, { key: 'lan', ...ICONS.lan, lit: !!tog.lan, tap: () => setTog((t) => ({ ...t, lan: !t.lan })) }, ]; const scaleTicks = ['0', '-20', '-40', '-60'].map((t) => ({ t, y: Math.round(frac(Number(t)) * BH) })); const mkBar = (i, label, wide) => { const db = meters.lv[i], f = frac(db), pf = frac(meters.pk[i]); const led = meterStyle === 'led'; return { label, wide, h: led ? snap(f) : Math.max(1, Math.round(f * BH)), peak: led ? Math.max(SEG, snap(pf)) : Math.max(1, Math.round((meterStyle === 'cap' ? f : pf) * BH)), fill: meterStyle === 'cap' ? 0.18 : 1, val: db <= -59.5 ? '-\u221E' : db.toFixed(1), vc: db >= -5 ? C.red : db >= -20 ? C.orange : '#9FAAB2', }; }; const inBars = [mkBar(0, 'L', true), mkBar(1, 'R', true)]; const outBars = [2, 3, 4, 5, 6, 7].map((n, i) => mkBar(n, String(i + 1), false)); const charts = SERIES.map((cfg, si) => { const a = hist[si]; const y = (v) => PY + (1 - (v - cfg.lo) / (cfg.hi - cfg.lo)) * PHh; let line = ''; for (let i = 0; i < N; i++) { line += (i ? 'L' : 'M') + (PX + (i / (N - 1)) * PW).toFixed(1) + ' ' + y(a[i]).toFixed(1); } let grid = ''; cfg.ticks.forEach((t) => { grid += `M${PX} ${y(t).toFixed(1)}H${PX + PW}`; }); for (let c2 = 0; c2 <= 6; c2++) { const x = PX + (c2 * PW) / 6; grid += `M${x} ${PY}V${PY + PHh}`; } const mn = Math.min(...a), mx = Math.max(...a); const lastY = y(a[N - 1]); const fy = Math.max(PY, Math.min(PY + PHh - 19, lastY - 9.5)); return { cc: cfg.c, unit: cfg.unit, cur: a[N - 1].toFixed(cfg.dec), line, area: `${line}L${PX + PW} ${PY + PHh}L${PX} ${PY + PHh}Z`, grid, thresh: `M${PX} ${y(cfg.th).toFixed(1)}H${PX + PW}`, band: `M${PX} ${y(mx).toFixed(1)}H${PX + PW}V${y(mn).toFixed(1)}H${PX}Z`, yl: cfg.ticks.map((t) => ({ t: String(t), y: (y(t) - 6).toFixed(1) + 'px' })), lw: chartStyle === 'area' ? 2.4 : chartStyle === 'tech' ? 1.2 : 1.6, dotX: (PX + PW).toFixed(1), dotY: lastY.toFixed(1), flagX: (PX + PW - 58).toFixed(1), flagY: fy.toFixed(1), flagTy: (fy + 3).toFixed(1), flagXpx: (PX + PW - 58).toFixed(1) + 'px', flagTypx: (fy + 3).toFixed(1) + 'px', }; }); const tNow = hist[1][N - 1], vNow = hist[0][N - 1]; const statCards = [ { label: 'TEMP', v: tNow.toFixed(0), u: '\u00B0C', c: tNow > 75 ? C.hot : C.ink, pct: Math.round(((tNow - 20) / 80) * 100), lo: '20', hi: '100' }, { label: 'SUPPLY', v: vNow.toFixed(1), u: 'V', c: vNow < 47 ? C.amber : C.ink, pct: Math.round(((vNow - 40) / 20) * 100), lo: '40', hi: '60' }, ]; const uptime = `${pad2(Math.floor(up / 3600))}:${pad2(Math.floor(up / 60) % 60)}:${pad2(up % 60)}`; const infoRows = [ { k: 'MODEL', v: dev.model }, { k: 'SERIAL', v: dev.serial }, { k: 'FIRMWARE', v: dev.firmware }, { k: 'UPTIME', v: uptime }, { k: 'MAC', v: dev.mac }, ]; const ep = edit >= 0 ? params[edit] : null; const netDim = netType === 'DHCP'; const dimC = '#5A6570'; const netRows = [ { k: 'MODE', v: netType, nc: C.ink3, vc: C.cyan, cur: interactive ? 'pointer' : 'default', onTap: () => interactive && setNetType((t) => (t === 'STATIC' ? 'DHCP' : 'STATIC')) }, { k: 'IP ADDR', v: '192.168.001.148', nc: netDim ? dimC : C.ink3, vc: netDim ? dimC : C.ink2, cur: 'default', onTap: null }, { k: 'NETMASK', v: '255.255.255.000', nc: netDim ? dimC : C.ink3, vc: netDim ? dimC : C.ink2, cur: 'default', onTap: null }, { k: 'GATEWAY', v: '192.168.001.001', nc: netDim ? dimC : C.ink3, vc: netDim ? dimC : C.ink2, cur: 'default', onTap: null }, ]; const navNames = ['CHANNELS', 'DIAG', 'STATUS', 'SETUP']; const slide = (n, on) => ({ transform: `translateX(${(-n * VIEW + (on ? drag.dx : 0)).toFixed(1)}px)`, transition: drag.anim ? 'transform .24s cubic-bezier(.3,.9,.3,1)' : 'none', }); const strip = { display: 'flex', ...slide(0, false) }; /* --------------------------------------------------------------- render */ return (
{/* scaled box is absolutely positioned so its 480x272 layout size cannot override the host's aspect ratio at small widths */}
{/* status rail */}
interactive && startBoot()} style={{ width: 40, height: 40, flex: 'none', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: interactive ? 'pointer' : 'default' }} >
{railIcons.map((ic) => (
interactive && ic.tap && ic.tap()} style={{ width: 44, height: 44, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: interactive && ic.tap ? 'pointer' : 'default' }} >
))}
{/* page stack */}
{/* ---- channels ---- */}
({ n, on: page === 0 && sub === i, c: C.cyan }))} />
{inBars.map((b) => )}
{outBars.map((b) => )}
{/* ---- diagnostics ---- */}
({ n: s.tab, on: page === 1 && sub === i, c: s.c }))} />
{charts.map((c2, i) => )}
{/* ---- status ---- */}
({ n, on: page === 2 && sub === i, c: C.cyan }))} />
IP ADDRESS
{dev.ip}
{statCards.map((s) => (
{s.label}
{s.v} {s.u}
{s.lo}{s.hi}
))}
{infoRows.map((r) => (
{r.k} {r.v}
))}
{/* ---- setup ---- */}
({ n, on: page === 3 && sub === i, c: C.cyan }))} />
{params.map((p, i) => (
interactive && setEdit(i)} style={{ height: 36, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 12px 0 11px', background: i === edit ? C.sel : C.plate, boxSizing: 'border-box', cursor: interactive ? 'pointer' : 'default', }} > {p.n} {fmtParam(p)} {p.en ? '' : p.u}
))}
{ep && (
setEdit(-1)} style={{ flex: 1, padding: '16px 0 14px 14px', display: 'flex', flexDirection: 'column', justifyContent: 'center', cursor: 'pointer' }} >
{ep.n}
{fmtParam(ep)} {ep.en ? '' : ep.u}
{ep.en ? ep.en.join(' / ') : `${ep.min} \u2026 ${ep.max} ${ep.u} / ${ep.st}`}
TAP TO CONFIRM
{[1, -1].map((dir) => (
{ e.stopPropagation(); startHold(dir); }} onPointerUp={stopHold} onPointerCancel={stopHold} onPointerLeave={stopHold} style={{ height: 74, background: C.btn, borderRadius: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', touchAction: 'none' }} > 0 ? 'M12 19V6M5.5 12.5L12 6l6.5 6.5' : 'M12 5v13M5.5 11.5L12 18l6.5-6.5'} />
))}
)}
{netRows.map((r) => (
{r.k} {r.v}
))}
{/* page indicator */} {navStyle === 'dots' && (
{navNames.map((n, i) => (
))}
)} {navStyle === 'rail' && (
{navNames.map((n, i) => (
{n}
))}
)} {navStyle === 'ticks' && (
{navNames.map((n, i) => (
))}
)} {/* boot */} {boot && (
{ if (!interactive) return; bootTimers.current.forEach(clearTimeout); setBoot(false); setBootOut(false); }} style={{ position: 'absolute', inset: 0, zIndex: 20, background: C.screen, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 26, opacity: bootOut ? 0 : 1, transition: 'opacity .45s linear', cursor: interactive ? 'pointer' : 'default', }} > {LOGO_WORD.map((d, i) => )}
{dev.model}
)}
); } window.PA100Panel = PA100Panel; })();