;(function () {
const { useState: useS, useEffect: useE, createContext: createCtx, useContext: useCtx } = React;
const DEFAULTS = {
theme: 'dark', glow: true, annotations: true, legend: true, grid: 'normal',
speed: '25', layout: '3x4', rhythmStrip: true, mute: false, volume: 80,
printScope: 'visible', printGrid: true, heart: true,
};
const SettingsCtx = createCtx(DEFAULTS);
window.SettingsCtx = SettingsCtx;
function loadSettings() {
try { return { ...DEFAULTS, ...(JSON.parse(localStorage.getItem('pqrst.settings') || '{}')) }; } catch (e) { return { ...DEFAULTS }; }
}
function applyRoot(s) {
const r = document.documentElement;
r.setAttribute('data-theme', s.theme);
r.setAttribute('data-glow', s.glow ? 'on' : 'off');
r.setAttribute('data-anno', s.annotations ? 'on' : 'off');
r.setAttribute('data-legend', s.legend ? 'on' : 'off');
r.setAttribute('data-grid', s.grid);
r.setAttribute('data-print-grid', s.printGrid ? 'on' : 'off');
}
window.pqrstPrint = function (scopeKey) {
const r = document.documentElement, b = document.body;
const prevTheme = r.getAttribute('data-theme');
b.setAttribute('data-print', scopeKey || 'all');
r.setAttribute('data-theme', 'light');
r.setAttribute('data-printing', '1');
const done = () => {
b.removeAttribute('data-print');
r.removeAttribute('data-printing');
r.setAttribute('data-theme', prevTheme || 'dark');
window.removeEventListener('afterprint', done);
};
window.addEventListener('afterprint', done);
setTimeout(() => window.print(), 120);
setTimeout(done, 60000);
};
function Switch({ on, onChange, label, hint }) {
return (
<button className={'sw' + (on ? ' on' : '')} role="switch" aria-checked={on} onClick={() => onChange(!on)}>
<span className="sw-txt"><span className="sw-lbl">{label}</span>{hint && <span className="sw-hint">{hint}</span>}</span>
<span className="sw-track"><span className="sw-knob"></span></span>
</button>
);
}
function Row({ label, children }) {
return <div className="s-row"><span className="s-lbl">{label}</span>{children}</div>;
}
function SettingsPanel({ s, set, open, onClose }) {
const u = (k) => (v) => set({ ...s, [k]: v });
useE(() => {
if (!open) return;
const k = e => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', k);
return () => window.removeEventListener('keydown', k);
}, [open]);
return (
<>
<div className={'s-backdrop' + (open ? ' open' : '')} onClick={onClose}></div>
<aside className={'s-panel' + (open ? ' open' : '')} aria-hidden={!open} aria-label="Settings">
<div className="s-head">
<span>Settings</span>
<button className="icon-btn" onClick={onClose} aria-label="Close">×</button>
</div>
<div className="s-body">
<section>
<h3>Display</h3>
<Row label="Theme"><Seg value={s.theme} onChange={u('theme')} options={[['dark', '☾ Dark'], ['light', '☀ Light']]} /></Row>
<Row label="Grid"><Seg value={s.grid} onChange={u('grid')} options={[['off', 'Off'], ['faint', 'Faint'], ['normal', 'Normal'], ['bold', 'Bold']]} /></Row>
<Switch on={s.glow} onChange={u('glow')} label="Waveform glow" hint="Phosphor bloom on the trace" />
<Switch on={s.annotations} onChange={u('annotations')} label="Interval brackets" hint="P · PR · QRS · QT on the trace" />
<Switch on={s.legend} onChange={u('legend')} label="Interval legend" hint="Stats row below single-lead strips" />
<Switch on={s.heart} onChange={u('heart')} label="Conduction window" hint="Heart diagram lit in sync with the trace" />
</section>
<section>
<h3>Sweep</h3>
<Row label="Paper speed"><Seg value={s.speed} onChange={u('speed')} options={[['25', '25 mm/s'], ['50', '50 mm/s']]} /></Row>
</section>
<section>
<h3>12-Lead</h3>
<Row label="Layout"><Seg value={s.layout} onChange={u('layout')} options={[['3x4', '3 × 4'], ['6x2', '6 × 2']]} /></Row>
<Switch on={s.rhythmStrip} onChange={u('rhythmStrip')} label="Lead II rhythm strip" hint="Full-width strip under the grid" />
</section>
<section>
<h3>Sound</h3>
<Switch on={s.mute} onChange={u('mute')} label="Mute all strips" hint="Overrides per-strip sound" />
<Row label="Volume">
<input type="range" min="0" max="100" value={s.volume} onChange={e => set({ ...s, volume: +e.target.value })} disabled={s.mute} />
<span className="val">{s.volume}%</span>
</Row>
</section>
<section>
<h3>Print</h3>
<Switch on={s.printGrid} onChange={u('printGrid')} label="Print ECG grid" hint="Off saves ink; trace only" />
<p className="s-note">Prints on white paper stock with intervals, HR, paper speed and timestamp per strip. Use the printer icon on a card to print only that strip.</p>
<button className="s-primary" onClick={() => { onClose(); window.pqrstPrint('all'); }}>
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.4"><path d="M4 6V2.5h8V6M3 6h10a1 1 0 0 1 1 1v4h-2.5M2 11V7a1 1 0 0 1 1-1M4.5 9.5h7v4h-7z" /></svg>
Print all strips
</button>
</section>
<section>
<button className="s-ghost" onClick={() => set({ ...DEFAULTS })}>Reset to defaults</button>
</section>
</div>
</aside>
</>
);
}
function GearButton({ onClick, open }) {
return (
<button className={'theme-toggle gear' + (open ? ' on' : '')} onClick={onClick} title="Settings" aria-expanded={open}>
<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="1.3"><circle cx="8" cy="8" r="2.2" /><path d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.4 3.4l1.4 1.4M11.2 11.2l1.4 1.4M3.4 12.6l1.4-1.4M11.2 4.8l1.4-1.4" /></svg>
<span>Settings</span>
</button>
);
}
function ThemeSwitch({ theme, onChange }) {
const dark = theme === 'dark';
return (
<button className={'day-night' + (dark ? ' dark' : ' light')} role="switch" aria-checked={!dark} onClick={() => onChange(dark ? 'light' : 'dark')} title={dark ? 'Switch to day' : 'Switch to night'}>
<span className="dn-sun">
<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="1.4"><circle cx="8" cy="8" r="3" /><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3 3l1.4 1.4M11.6 11.6L13 13M3 13l1.4-1.4M11.6 4.4L13 3" /></svg>
</span>
<span className="dn-moon">
<svg viewBox="0 0 16 16" width="11" height="11" fill="currentColor"><path d="M10.5 2a6 6 0 1 0 3.5 10.9A6.5 6.5 0 0 1 10.5 2z" /></svg>
</span>
<span className="dn-knob"></span>
</button>
);
}
function App() {
const [s, setS] = useS(loadSettings);
const [open, setOpen] = useS(false);
useE(() => { applyRoot(s); try { localStorage.setItem('pqrst.settings', JSON.stringify(s)); } catch (e) {} }, [s]);
const headRoot = React.useRef(null);
useE(() => {
if (!headRoot.current) headRoot.current = ReactDOM.createRoot(document.getElementById('theme-mount'));
headRoot.current.render(<><ThemeSwitch theme={s.theme} onChange={t => setS(p => ({ ...p, theme: t }))} /><GearButton open={open} onClick={() => setOpen(o => !o)} /></>);
}, [open, s.theme]);
return (
<SettingsCtx.Provider value={s}>
<Demo />
<SettingsPanel s={s} set={setS} open={open} onClose={() => setOpen(false)} />
</SettingsCtx.Provider>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
})();