Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
ecdbde7d0This file is pinned to the selected release. Download the original to open it in your design tool.
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<title>akcli calc — engineering bench</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%23d08a3d'/%3E%3Ctext x='8' y='12' font-family='monospace' font-size='11' font-weight='bold' text-anchor='middle' fill='%231a1408'%3E%3D%3C/text%3E%3C/svg%3E">
<style>
/* ============================ theme ================================== */
:root{
--mono: ui-monospace,"SF Mono","JetBrains Mono","Cascadia Code","Fira Code",Menlo,Consolas,monospace;
--disp: "Avenir Next Condensed","Arial Narrow","Helvetica Neue",Arial,sans-serif;
}
:root[data-theme="dark"]{
--bg:#14161a; --bg2:#181b20; --panel:#1d2127; --panel2:#171a1f; --inset:#101317;
--border:#2b3038; --border2:#3a414c;
--text:#ced3da; --muted:#8b929d; --faint:#5f6671;
--brand:#d08a3d; --brand2:#e8ab61; --oncopper:#1a1408;
--data:#66c8de; --data2:#8adcee;
--ok:#58c48e; --err:#e5665f; --warn:#d9a13c;
--sel:rgba(208,138,61,.13); --focus:rgba(102,200,222,.45);
--grid:rgba(102,200,222,.045); --grid2:rgba(102,200,222,.028);
--shadow:0 6px 24px rgba(0,0,0,.35);
--dg-cu:#d9903f; --dg-cu-dark:#a86a24; --dg-board:#2f6b3f; --dg-board-dark:#245233;
--dg-dim:#7fd4e8; --dg-wire:#8fd8ea; --dg-plane:#878d94; --dg-body:#3a3f44;
--dg-hot:#ff5544; --dg-flame:#ffb14e; --dg-gold:#ffd75e; --dg-ink:#1c2024;
}
:root[data-theme="light"]{
--bg:#e9e5db; --bg2:#efece3; --panel:#faf8f2; --panel2:#f0ede4; --inset:#fdfcf9;
--border:#d3ccbb; --border2:#bcb4a0;
--text:#2b2e33; --muted:#6c7076; --faint:#9a9c9e;
--brand:#a96617; --brand2:#8a5310; --oncopper:#fff8ec;
--data:#0f6f88; --data2:#0a5568;
--ok:#1e8a57; --err:#bf3934; --warn:#8f6408;
--sel:rgba(169,102,23,.10); --focus:rgba(15,111,136,.35);
--grid:rgba(15,111,136,.055); --grid2:rgba(15,111,136,.03);
--shadow:0 4px 18px rgba(60,50,30,.12);
--dg-cu:#c47c28; --dg-cu-dark:#965c1a; --dg-board:#3c7a4c; --dg-board-dark:#2c5c39;
--dg-dim:#0e6e86; --dg-wire:#0d6a82; --dg-plane:#8f959c; --dg-body:#d9d4c6;
--dg-hot:#d43c2c; --dg-flame:#c07b18; --dg-gold:#a8861c; --dg-ink:#1c2024;
}
/* ============================ base =================================== */
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0; font:13.5px/1.5 var(--mono); background:var(--bg); color:var(--text);
display:grid; grid-template-columns:280px 1fr; grid-template-rows:auto 1fr auto;
grid-template-areas:"appbar appbar" "side main" "status status";
height:100vh; overflow:hidden;
}
/* ---- shared bench chrome (identical on hub/calc/live) ---- */
#appbar{grid-area:appbar; display:flex; align-items:center; gap:12px;
padding:7px 14px; background:var(--panel2); border-bottom:1px solid var(--border)}
#mark{font-family:var(--disp); font-size:15px; font-weight:600; letter-spacing:.1em;
text-transform:uppercase; color:var(--brand2); text-decoration:none; white-space:nowrap}
#mark:hover{color:var(--brand)}
.tabs{display:flex; border:1px solid var(--border); border-radius:8px; overflow:hidden;
background:var(--panel)}
.tabs a{display:flex; align-items:center; padding:3px 13px; color:var(--muted);
text-decoration:none; font-family:var(--disp); font-size:11.5px; font-weight:600;
letter-spacing:.12em; text-transform:uppercase; border-right:1px solid var(--border)}
.tabs a:last-child{border-right:none}
.tabs a.on{background:var(--sel); color:var(--brand2)}
.tabs a:hover:not(.on){color:var(--text)}
#pagename{font-size:11px; color:var(--faint); letter-spacing:.04em}
.spacer{flex:1}
::selection{background:var(--sel)}
button{font:inherit; color:inherit; cursor:pointer}
:focus-visible{outline:2px solid var(--focus); outline-offset:1px; border-radius:4px}
@media (prefers-reduced-motion:reduce){ *,*::before,*::after{animation:none !important; transition:none !important} }
/* ============================ sidebar ================================ */
#side{grid-area:side; background:var(--panel2); border-right:1px solid var(--border);
display:flex; flex-direction:column; min-height:0}
#searchwrap{padding:12px 12px 10px; position:relative}
#filter{width:100%; padding:7px 30px 7px 10px; background:var(--inset);
border:1px solid var(--border); color:var(--text); border-radius:6px; font:inherit}
#filter::placeholder{color:var(--faint)}
#searchwrap .kbd{position:absolute; right:20px; top:19px; pointer-events:none}
#calclist{flex:1; overflow-y:auto; padding:0 8px 10px; min-height:0; scrollbar-width:thin}
.group-h{display:flex; align-items:center; gap:6px; width:100%; margin:10px 0 2px;
padding:2px 8px; background:none; border:none; text-align:left;
font-family:var(--disp); font-size:11px; font-weight:600; letter-spacing:.14em;
text-transform:uppercase; color:var(--brand); border-radius:5px}
.group-h:hover{background:var(--sel)}
.group-h .n{margin-left:auto; color:var(--faint); font-family:var(--mono); font-size:10px}
.group-h .tw{transition:transform .15s; font-size:9px; color:var(--faint)}
.group-h.closed .tw{transform:rotate(-90deg)}
.calc-btn{display:flex; align-items:baseline; gap:7px; width:100%; text-align:left;
background:none; border:none; color:var(--text); padding:4px 8px 4px 16px;
border-left:2px solid transparent; border-radius:0 5px 5px 0; font:inherit; font-size:13px}
.calc-btn:hover{background:var(--sel)}
.calc-btn.active{background:var(--sel); border-left-color:var(--brand); color:var(--brand2)}
.calc-btn.kbd-focus{outline:2px solid var(--focus); outline-offset:-2px}
.calc-btn .pin{color:var(--brand); font-size:10px}
.calc-btn mark{background:none; color:var(--data); font-weight:700}
#sidefoot{border-top:1px solid var(--border); padding:8px 16px; font-size:10.5px; color:var(--faint)}
/* ============================ main =================================== */
#main{grid-area:main; overflow-y:auto; min-height:0; position:relative;
background:
linear-gradient(var(--grid2) 1px, transparent 1px) 0 0/100% 96px,
linear-gradient(90deg, var(--grid2) 1px, transparent 1px) 0 0/96px 100%,
linear-gradient(var(--grid) 1px, transparent 1px) 0 0/100% 24px,
linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/24px 100%,
var(--bg2)}
#page,#home{padding:18px 26px 30px; max-width:1480px}
#topbar{display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin-bottom:4px}
#title{margin:0; font-family:var(--disp); font-size:24px; font-weight:600;
letter-spacing:.03em; color:var(--text)}
#title .cname{color:var(--brand2)}
.iconbtn{background:var(--panel); border:1px solid var(--border); color:var(--muted);
border-radius:7px; padding:4px 10px; font-size:12px; text-decoration:none}
.iconbtn:hover{border-color:var(--border2); color:var(--text)}
.iconbtn.on{color:var(--brand2); border-color:var(--brand)}
#ref{font-size:11.5px; color:var(--muted); margin:2px 0 4px; max-width:980px}
#ref b{color:var(--data); font-weight:normal}
#notes{font-size:11.5px; color:var(--warn); margin:0 0 12px; max-width:980px}
#content{display:grid; grid-template-columns:minmax(380px,470px) minmax(380px,1fr);
gap:20px; align-items:start}
@media (max-width:1020px){ #content{grid-template-columns:1fr} body{grid-template-columns:240px 1fr} }
.card{background:var(--panel); border:1px solid var(--border); border-radius:10px;
padding:16px; box-shadow:var(--shadow); animation:rise .25s ease-out both}
@keyframes rise{from{opacity:0; transform:translateY(6px)}}
.card h3{margin:0 0 10px; font-family:var(--disp); font-size:12px; font-weight:600;
letter-spacing:.16em; text-transform:uppercase; color:var(--muted)}
#rightcol{display:flex; flex-direction:column; gap:20px; position:sticky; top:18px}
#diagcard{display:flex; flex-direction:column; align-items:center}
#diagram svg{max-width:100%; height:auto}
#diagram svg text{font-family:var(--mono)}
.svgcap{font-size:10.5px; color:var(--faint); margin-top:6px; text-align:center}
/* ---- form ---- */
.prow{display:flex; align-items:center; gap:8px; margin-bottom:2px}
.prow label{width:128px; color:var(--data); text-align:right; font-size:12.5px;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:none}
.prow input,.prow select{flex:1; min-width:0; padding:6px 9px; background:var(--inset);
border:1px solid var(--border); color:var(--text); border-radius:6px; font:inherit}
.prow input::placeholder{color:var(--faint); opacity:.8}
.prow input.bad{border-color:var(--err)}
.prow input.flash-req{animation:reqflash .8s}
@keyframes reqflash{0%,60%{border-color:var(--err); box-shadow:0 0 0 2px rgba(229,102,95,.25)}}
.prow .unit{width:48px; color:var(--muted); font-size:12px; flex:none}
.prow .req{color:var(--err)}
.pmeta{display:flex; margin:0 56px 7px 136px; gap:8px; font-size:10.5px; min-height:15px}
.pmeta .help{color:var(--faint); flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.pmeta .parsed{color:var(--data); white-space:nowrap}
.pmeta .parsed.bad{color:var(--err)}
#formfoot{display:flex; align-items:center; gap:10px; margin-top:10px}
#runbtn{padding:7px 22px; background:var(--brand); border:1px solid var(--brand);
color:var(--oncopper); border-radius:6px; font-weight:700; letter-spacing:.03em;
white-space:nowrap}
#runbtn:hover{background:var(--brand2); border-color:var(--brand2)}
#resetbtn{padding:7px 12px}
.hint{font-size:10.5px; color:var(--faint)}
#error{color:var(--err); margin-top:10px; white-space:pre-wrap; font-size:12.5px}
/* ---- results ---- */
#rescard{margin-top:20px}
.reshead{display:flex; align-items:center; gap:6px; margin-bottom:10px}
.reshead h3{margin:0; flex:1}
#restime{font-size:10.5px; color:var(--faint)}
#results table{border-collapse:collapse; width:100%}
#results td,#results th{border:1px solid var(--border); padding:5px 10px; text-align:left; font:inherit}
#results th{background:var(--panel2); color:var(--muted); font-weight:normal; font-size:11px;
font-family:var(--disp); letter-spacing:.12em; text-transform:uppercase}
#results .rk{color:var(--data)}
#results .val{white-space:nowrap; color:var(--text)}
#results .val .num{cursor:copy; border-bottom:1px dotted var(--border2)}
#results .val .num:hover{color:var(--data2); border-bottom-color:var(--data)}
#results .note{color:var(--muted); font-size:11.5px}
#results tr{animation:rowin .2s ease-out both}
@keyframes rowin{from{opacity:0}}
.delta{font-size:10px; margin-left:6px; color:var(--warn)}
.chip{display:inline-block; border-radius:5px; padding:0 7px; font-size:11.5px; font-weight:700}
.chip.ok{color:var(--ok); background:color-mix(in srgb, var(--ok) 14%, transparent)}
.chip.err{color:var(--err); background:color-mix(in srgb, var(--err) 14%, transparent)}
.lchip{display:inline-block; border:1px solid var(--border2); border-radius:4px;
padding:0 6px; margin:1px 3px 1px 0; font-size:11.5px}
.lchip .sw{display:inline-block; width:9px; height:9px; border-radius:2px; margin-right:5px;
vertical-align:baseline; border:1px solid rgba(0,0,0,.35)}
.subtable{margin:4px 0}
.subtable td,.subtable th{font-size:11.5px !important; padding:3px 8px !important}
.subtable th{cursor:pointer; user-select:none}
.subtable th:hover{color:var(--data) !important}
.subtable th .arr{font-size:9px; color:var(--brand2)}
/* ---- home launcher ---- */
#home h2{font-family:var(--disp); font-size:30px; font-weight:600; letter-spacing:.06em;
text-transform:uppercase; margin:6px 0 2px; color:var(--text)}
#home h2 em{font-style:normal; color:var(--brand2)}
#home .lead{color:var(--muted); font-size:12.5px; margin:0 0 20px}
.hgroup{margin-bottom:18px}
.hgroup h4{font-family:var(--disp); font-size:12px; font-weight:600; letter-spacing:.16em;
text-transform:uppercase; color:var(--brand); margin:0 0 8px}
.hgrid{display:grid; grid-template-columns:repeat(auto-fill,minmax(215px,1fr)); gap:8px}
.tile{background:var(--panel); border:1px solid var(--border); border-radius:8px;
padding:8px 12px; text-align:left; display:block}
.tile:hover{border-color:var(--brand); box-shadow:var(--shadow)}
.tile .n{color:var(--brand2); font-weight:700; font-size:12.5px}
.tile .d{color:var(--muted); font-size:11px; margin-top:1px; display:block;
overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
/* ---- history ---- */
#histcard{margin-top:20px}
#histlist{display:flex; flex-direction:column; gap:2px; max-height:300px; overflow-y:auto}
.hrow{display:flex; gap:10px; align-items:baseline; padding:4px 8px; border-radius:6px;
background:none; border:none; text-align:left; font-size:12px; width:100%}
.hrow:hover{background:var(--sel)}
.hrow .ht{color:var(--faint); font-size:10.5px; flex:none; width:52px}
.hrow .hc{color:var(--brand2); flex:none}
.hrow .hp{color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.hrow .hr{color:var(--data); margin-left:auto; white-space:nowrap; font-size:11.5px}
/* ============================ status bar ============================= */
#status{grid-area:status; display:flex; align-items:center; gap:14px; padding:5px 14px;
background:var(--panel2); border-top:1px solid var(--border);
font-size:11px; color:var(--muted); min-height:28px}
#stdot{width:8px; height:8px; border-radius:50%; background:var(--ok); animation:pulse 2.4s infinite}
#stdot.off{background:var(--err); animation:none}
@keyframes pulse{50%{opacity:.35}}
#clicmd{flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; cursor:copy;
color:var(--faint)}
#clicmd:hover{color:var(--text)}
#clicmd b{color:var(--data); font-weight:normal}
.kbd{display:inline-block; border:1px solid var(--border2); border-bottom-width:2px;
border-radius:4px; padding:0 5px; font-size:10px; color:var(--muted); background:var(--panel)}
/* ============================ overlays =============================== */
.overlay{position:fixed; inset:0; background:rgba(8,10,12,.55); display:flex;
justify-content:center; align-items:flex-start; padding-top:12vh; z-index:50;
backdrop-filter:blur(2px)}
.overlay[hidden]{display:none}
#palette{width:min(600px,90vw); background:var(--panel); border:1px solid var(--border2);
border-radius:12px; box-shadow:var(--shadow); overflow:hidden;
animation:pop .14s ease-out}
@keyframes pop{from{opacity:0; transform:scale(.98) translateY(-6px)}}
#palinput{width:100%; padding:13px 16px; background:none; border:none; color:var(--text);
font:15px var(--mono); border-bottom:1px solid var(--border)}
#palinput:focus{outline:none}
#pallist{max-height:46vh; overflow-y:auto; padding:6px}
.palrow{display:flex; gap:10px; align-items:baseline; padding:7px 12px; border-radius:7px;
cursor:pointer}
.palrow.sel{background:var(--sel)}
.palrow .pn{color:var(--brand2); font-weight:700; flex:none}
.palrow .pt{color:var(--muted); font-size:12px; overflow:hidden; text-overflow:ellipsis;
white-space:nowrap}
.palrow .pg{margin-left:auto; color:var(--faint); font-size:10.5px; flex:none;
font-family:var(--disp); letter-spacing:.12em; text-transform:uppercase}
.palrow mark{background:none; color:var(--data); font-weight:700}
#helpbox{width:min(560px,90vw); background:var(--panel); border:1px solid var(--border2);
border-radius:12px; box-shadow:var(--shadow); padding:20px 24px;
animation:pop .14s ease-out}
#helpbox h3{margin:0 0 12px; font-family:var(--disp); letter-spacing:.14em;
text-transform:uppercase; color:var(--brand2); font-size:14px}
#helpbox table{width:100%; border-collapse:collapse; font-size:12.5px}
#helpbox td{padding:4px 6px; border-bottom:1px solid var(--border)}
#helpbox td:first-child{width:130px; color:var(--muted)}
/* ============================ toast ================================== */
#toast{position:fixed; bottom:42px; left:50%; transform:translateX(-50%);
background:var(--panel); color:var(--text); border:1px solid var(--brand);
border-radius:8px; padding:7px 18px; font-size:12.5px; box-shadow:var(--shadow);
z-index:60; opacity:0; pointer-events:none; transition:opacity .18s}
#toast.show{opacity:1}
/* ============================ print ================================== */
@media print{
body{display:block; height:auto; overflow:visible; background:#fff; color:#000}
#side,#status,.overlay,#toast,#formfoot,#reshead .iconbtn,#histcard,.iconbtn{display:none !important}
#main{overflow:visible; background:#fff}
.card{box-shadow:none; border-color:#999; break-inside:avoid}
#content{grid-template-columns:1fr}
}
</style>
</head>
<body>
<header id="appbar">
<a id="mark" href="/" title="hub (h)">⌂ akcli</a>
<nav class="tabs" aria-label="pages">
<a href="/" title="hub (h)">⌂</a>
<a href="/calc" class="on" id="tabcalc" title="calculator bench">= calc</a>
<a href="/live" title="live schematic timeline">◎ live</a>
</nav>
<span id="pagename">standards-cited calculators — every result carries its reference</span>
<span class="spacer"></span>
<a class="iconbtn" id="livelink" href="/live" hidden
title="open the live schematic timeline">▶ live</a>
<button class="iconbtn" id="themebtn" title="toggle dark / light">◐</button>
<button class="iconbtn" id="helpbtn" title="keyboard shortcuts (?)">?</button>
</header>
<nav id="side" aria-label="calculators">
<div id="searchwrap">
<input id="filter" placeholder="filter calculators…" aria-label="filter calculators"
autocomplete="off" spellcheck="false">
<span class="kbd">/</span>
</div>
<div id="calclist" role="list"></div>
<div id="sidefoot" style="white-space:nowrap; overflow:hidden"><span id="sfcount">…</span> · <span class="kbd">⌘K</span> · <span class="kbd">/</span> · <span class="kbd">?</span></div>
</nav>
<main id="main">
<div id="home"></div>
<div id="page" hidden>
<div id="topbar">
<h2 id="title"></h2>
<button class="iconbtn" id="pinbtn" title="pin to top of sidebar">☆ pin</button>
<button class="iconbtn" id="linkbtn" title="copy a URL that reproduces this computation">⧉ copy link</button>
<button class="iconbtn" id="printbtn" title="print / save PDF">⎙ print</button>
</div>
<div id="ref"></div>
<div id="notes"></div>
<div id="content">
<div>
<div class="card" id="formcard">
<h3>Parameters</h3>
<div id="form"></div>
<div id="formfoot">
<button id="runbtn">Calculate ⏎</button>
<button id="resetbtn" class="iconbtn" title="reset all fields to defaults">reset</button>
<span class="hint">notation: 4k7 · 100n · 35u · 2M2 · 1e-7 · 4R7</span>
</div>
<div id="error" role="alert"></div>
</div>
<div class="card" id="rescard" hidden>
<div class="reshead">
<h3>Results</h3>
<span id="restime"></span>
<button class="iconbtn" id="cpmd" title="copy as a markdown table">md</button>
<button class="iconbtn" id="cpjson" title="copy the raw JSON envelope">json</button>
<button class="iconbtn" id="cpcli" title="copy the equivalent akcli command">cli</button>
<button class="iconbtn" id="opsbtn" title="download a place_component op-list for akcli plan/draw" hidden>⤓ op-list</button>
</div>
<div id="results"></div>
</div>
<div class="card" id="histcard" hidden>
<div class="reshead">
<h3 style="flex:1">Session log</h3>
<button class="iconbtn" id="histclear" title="clear log">clear</button>
</div>
<div id="histlist"></div>
</div>
</div>
<div id="rightcol">
<div class="card" id="diagcard">
<div id="diagram"></div>
<div class="svgcap" id="diagcap"></div>
</div>
</div>
</div>
</div>
</main>
<footer id="status">
<span id="stdot" title="server connection"></span>
<span id="stinfo">connecting…</span>
<span id="clicmd" title="click to copy the CLI equivalent"></span>
<span><span class="kbd">⌘K</span> <span class="kbd">/</span> <span class="kbd">h</span> <span class="kbd">l</span> <span class="kbd">?</span></span>
</footer>
<div class="overlay" id="palov" hidden>
<div id="palette" role="dialog" aria-label="command palette">
<input id="palinput" placeholder="jump to calculator…" autocomplete="off" spellcheck="false">
<div id="pallist"></div>
</div>
</div>
<div class="overlay" id="helpov" hidden>
<div id="helpbox" role="dialog" aria-modal="true" aria-label="keyboard shortcuts" tabindex="-1">
<h3>Keyboard</h3>
<table>
<tr><td><span class="kbd">⌘K</span> / <span class="kbd">^K</span></td><td>command palette</td></tr>
<tr><td><span class="kbd">/</span></td><td>focus the sidebar filter</td></tr>
<tr><td><span class="kbd">↑</span> <span class="kbd">↓</span> <span class="kbd">⏎</span></td><td>navigate the filtered list / palette</td></tr>
<tr><td><span class="kbd">⏎</span> in a field</td><td>calculate now (auto-compute runs as you type)</td></tr>
<tr><td><span class="kbd">h</span></td><td>back to the hub</td></tr>
<tr><td><span class="kbd">l</span></td><td>live schematic timeline</td></tr>
<tr><td><span class="kbd">Esc</span></td><td>close overlays</td></tr>
<tr><td><span class="kbd">?</span></td><td>this help</td></tr>
</table>
<p class="hint" style="margin-top:12px">Engineering notation is accepted everywhere: 4k7, 100n, 35u, 2M2, 1e-7, 4R7.
Click any result value to copy the exact number. Share the URL to reproduce a computation.</p>
</div>
</div>
<div id="toast" role="status" aria-live="polite"></div>
<script>
"use strict";
/* @pure-begin — mirrors of calc/si.py + helpers, kept DOM-free for tests */
const PREFIXABLE = new Set(["Ω","V","A","W","F","H","Hz","s","m"]);
const STEPS = [[1e9,"G"],[1e6,"M"],[1e3,"k"],[1,""],[1e-3,"m"],[1e-6,"µ"],[1e-9,"n"],[1e-12,"p"]];
const MULT = {p:1e-12,n:1e-9,u:1e-6,"µ":1e-6,m:1e-3,k:1e3,K:1e3,M:1e6,G:1e9,R:1,r:1};
const ENG_RX = /^([0-9]*\.?[0-9]+)\s*([pnuµmkKMGRr])([0-9]*)$/;
/* parse "4k7" | "100n" | "1e-7" -> number; "" -> null; junk -> NaN */
function parseEng(s){
s = String(s).trim();
if (s === "") return null;
const f = Number(s);
if (Number.isFinite(f)) return f;
const m = ENG_RX.exec(s);
if (!m) return NaN;
return parseFloat(m[1] + (m[3] ? "." + m[3] : "")) * MULT[m[2]];
}
/* CLI-parity formatting: SI prefix only for bare base units (si.fmt_eng),
%.6g for everything else (cli._cmd_calc). */
function fmtEng(v, unit){
if (typeof v !== "number" || !Number.isFinite(v))
return String(v) + (unit ? " " + unit : "");
if (!unit) return Number(v.toPrecision(6)).toString();
if (!PREFIXABLE.has(unit)) return Number(v.toPrecision(6)) + " " + unit;
if (v === 0) return "0 " + unit;
const a = Math.abs(v);
for (const [s,p] of STEPS)
if (a >= s) return Number((v/s).toPrecision(4)) + " " + p + unit;
return Number(v.toPrecision(4)) + " " + unit;
}
/* the typed-back engineering form of a number: 3.5e-5 -> "35u" (parses again) */
function engInput(v){
if (typeof v !== "number" || !Number.isFinite(v) || v === 0) return String(v);
const a = Math.abs(v);
for (const [s,p] of STEPS)
if (a >= s) return Number((v/s).toPrecision(6)) + (p === "µ" ? "u" : p);
return String(v);
}
/* secondary representations for the hover tooltip */
function altForms(v, unit){
const out = [];
if (unit === "m") out.push(Number((v*1e3).toPrecision(6)) + " mm",
Number((v/25.4e-6).toPrecision(6)) + " mil");
if (unit === "m²") out.push(Number((v*1e6).toPrecision(6)) + " mm²");
if (Number.isFinite(v) && v !== 0 && (Math.abs(v) >= 1e5 || Math.abs(v) < 1e-3))
out.push(v.toExponential(4) + (unit ? " " + unit : ""));
return out;
}
/* subsequence fuzzy match: returns {score, idx:[positions]} or null */
function fuzzy(q, s){
q = q.toLowerCase(); s = s.toLowerCase();
let score = 0, i = 0, last = -2; const idx = [];
for (const ch of q){
const j = s.indexOf(ch, i);
if (j < 0) return null;
score += (j === last + 1 ? 3 : 1) + (j === 0 ? 2 : 0);
idx.push(j); last = j; i = j + 1;
}
return {score: score - s.length * 0.01, idx};
}
function esc(s){
return String(s).replace(/&/g,"&").replace(/</g,"<")
.replace(/>/g,">").replace(/"/g,""");
}
/* highlight matched character positions in already-escape-safe plain text */
function hilite(text, idx){
let out = "", set = new Set(idx);
for (let i = 0; i < text.length; i++)
out += set.has(i) ? "<mark>" + esc(text[i]) + "</mark>" : esc(text[i]);
return out;
}
/* the CLI line that reproduces a computation */
function cliLine(name, inputs){
const parts = ["akcli calc " + name];
for (const [k,v] of Object.entries(inputs)) if (v !== "") parts.push(k + "=" + v);
return parts.join(" ");
}
/* markdown export — mirrors cli._calc_md */
function toMarkdown(doc){
const lines = ["### " + doc.title, "", "| result | value | note |", "|---|---|---|"];
for (const [k, cell] of Object.entries(doc.results)){
let shown;
const v = cell.value, unit = cell.unit || "";
if (typeof v === "number") shown = fmtEng(v, unit);
else if (Array.isArray(v))
shown = v.length && typeof v[0] === "object"
? v.length + " entries (use --json)" : v.join("; ");
else shown = (String(v) + " " + unit).trim();
lines.push("| " + k + " | " + shown + " | " + (cell.note || "") + " |");
}
lines.push("", "*Reference: " + doc.reference + "*");
return lines.join("\n");
}
/* @pure-end */
/* ============================ state ================================== */
const $ = id => document.getElementById(id);
let REG = null, META = {count: 0, version: ""}, current = null;
let runSeq = 0, runCtrl = null, debTimer = null, lastDoc = null;
const prevNums = {}; // calc -> {resultKey: number} for delta chips
let kbdIdx = -1; // sidebar keyboard cursor
function store(k, d){ try { return JSON.parse(localStorage.getItem(k)) ?? d; } catch(e){ return d; } }
function save(k, v){ try { localStorage.setItem(k, JSON.stringify(v)); } catch(e){} }
function allCalcs(){ return Object.values(REG.groups).flat(); }
function byName(n){ return allCalcs().find(c => c.name === n); }
/* ============================ theme ================================== */
function applyTheme(t){
document.documentElement.dataset.theme = t;
save("akcli-theme", t);
}
applyTheme(store("akcli-theme", null) ||
(matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"));
$("themebtn").onclick = () =>
applyTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
/* ============================ toast ================================== */
let toastT = null;
function toast(msg){
const t = $("toast"); t.textContent = msg; t.classList.add("show");
clearTimeout(toastT); toastT = setTimeout(() => t.classList.remove("show"), 1400);
}
function copyText(text, msg){
navigator.clipboard.writeText(text).then(() => toast(msg || "copied"),
() => toast("copy failed"));
}
/* ============================ boot =================================== */
async function boot(){
try {
const r = await fetch("/api/list");
REG = await r.json();
META = REG.meta || META;
setStatus(true);
} catch (e) { setStatus(false); setTimeout(boot, 3000); return; }
$("sfcount").textContent = allCalcs().length + " calculators";
$("stinfo").textContent = allCalcs().length + " calculators · akcli " + (META.version || "");
$("livelink").hidden = !META.watching;
if (META.watching) $("livelink").textContent = "▶ live: " + META.watching;
renderSide(); renderHome();
routeFromHash(true);
addEventListener("hashchange", () => routeFromHash(false));
}
function setStatus(ok){
$("stdot").classList.toggle("off", !ok);
if (!ok) $("stinfo").textContent = "server unreachable — restart `akcli view calc`";
}
/* hash routing: #calc=<name>&k=v… selects, fills, runs (when valid).
Own updates go through replaceState (below), which never fires
hashchange — so this only reacts to user/browser navigation. */
function routeFromHash(isBoot){
const h = new URLSearchParams(location.hash.replace(/^#/, ""));
const c = byName(h.get("calc"));
if (!c) { showHome(); return; }
select(c, {noHash: true});
document.querySelectorAll("#form [data-param]").forEach(el => {
const v = h.get(el.dataset.param);
if (v !== null) { el.value = v; refreshParse(el); }
});
updateCliMirror();
if (formValid()) run();
}
function setHash(qs){
history.replaceState(null, "", "#" + qs);
}
/* ============================ sidebar ================================ */
function pins(){ return store("akcli-pins", []); }
function pinToggle(name){
const p = pins(), i = p.indexOf(name);
if (i >= 0) p.splice(i, 1); else p.push(name);
save("akcli-pins", p);
renderSide(); updatePinBtn();
}
function noteRecent(name){
const r = store("akcli-recent", []).filter(n => n !== name);
r.unshift(name); save("akcli-recent", r.slice(0, 8));
}
function collapsed(){ return store("akcli-collapsed", []); }
function groupToggle(g){
const c = collapsed(), i = c.indexOf(g);
if (i >= 0) c.splice(i, 1); else c.push(g);
save("akcli-collapsed", c); renderSide();
}
function calcBtn(c, matchIdx){
const b = document.createElement("button");
b.className = "calc-btn" + (current && current.name === c.name ? " active" : "");
b.dataset.name = c.name;
b.setAttribute("role", "listitem");
b.innerHTML = (pins().includes(c.name) ? '<span class="pin">★</span>' : "") +
(matchIdx ? hilite(c.name, matchIdx) : esc(c.name));
b.title = c.title;
b.onclick = () => select(c);
return b;
}
function renderSide(){
const q = $("filter").value.trim();
const box = $("calclist");
box.innerHTML = ""; kbdIdx = -1;
if (!q){
const pinned = pins().map(byName).filter(Boolean);
const recent = store("akcli-recent", []).map(byName)
.filter(c => c && !pins().includes(c.name)).slice(0, 5);
for (const [label, list] of [["★ pinned", pinned], ["recent", recent]]){
if (!list.length) continue;
const h = document.createElement("div");
h.className = "group-h"; h.textContent = label;
box.appendChild(h);
list.forEach(c => box.appendChild(calcBtn(c)));
}
}
for (const [group, calcs] of Object.entries(REG.groups)){
let hits = calcs.map(c => {
if (!q) return {c, idx: null};
const m = fuzzy(q, c.name) || fuzzy(q, c.title);
return m ? {c, idx: fuzzy(q, c.name) ? m.idx : null, score: m.score} : null;
}).filter(Boolean);
if (!hits.length) continue;
if (q) hits.sort((a,b) => (b.score||0) - (a.score||0));
const isClosed = !q && collapsed().includes(group);
const h = document.createElement("button");
h.className = "group-h" + (isClosed ? " closed" : "");
h.innerHTML = '<span class="tw">▼</span>' + esc(group) +
'<span class="n">' + hits.length + "</span>";
h.onclick = () => groupToggle(group);
box.appendChild(h);
if (!isClosed) hits.forEach(({c, idx}) => box.appendChild(calcBtn(c, idx)));
}
}
/* keyboard cursor over the visible calc buttons */
function sideButtons(){ return [...document.querySelectorAll("#calclist .calc-btn")]; }
function moveKbd(delta){
const btns = sideButtons();
if (!btns.length) return;
kbdIdx = Math.max(0, Math.min(btns.length - 1, kbdIdx + delta));
btns.forEach((b,i) => b.classList.toggle("kbd-focus", i === kbdIdx));
btns[kbdIdx].scrollIntoView({block: "nearest"});
}
$("filter").addEventListener("input", renderSide);
$("filter").addEventListener("keydown", e => {
if (e.key === "ArrowDown"){ e.preventDefault(); moveKbd(1); }
else if (e.key === "ArrowUp"){ e.preventDefault(); moveKbd(-1); }
else if (e.key === "Enter"){
const btns = sideButtons();
(btns[kbdIdx] || btns[0])?.click();
} else if (e.key === "Escape"){ $("filter").value = ""; renderSide(); $("filter").blur(); }
});
/* ============================ home =================================== */
function renderHome(){
const el = $("home");
let html = '<h2>akcli <em>calc bench</em></h2>' +
'<p class="lead">' + allCalcs().length + ' standards-cited calculators — every result carries its ' +
'formal reference. Pick one, or hit <span class="kbd">⌘K</span>.</p>';
for (const [group, calcs] of Object.entries(REG.groups)){
html += '<div class="hgroup"><h4>' + esc(group) + '</h4><div class="hgrid">';
for (const c of calcs)
html += '<button class="tile" data-name="' + esc(c.name) + '">' +
'<span class="n">' + esc(c.name) + '</span>' +
'<span class="d">' + esc(c.title) + '</span></button>';
html += "</div></div>";
}
el.innerHTML = html;
el.querySelectorAll(".tile").forEach(t => t.onclick = () => select(byName(t.dataset.name)));
}
function showHome(){
current = null;
$("home").hidden = false; $("page").hidden = true;
setHash(""); renderSide(); updateCliMirror();
}
/* clicking the CALC tab while already here returns to the launcher */
$("tabcalc").onclick = e => { e.preventDefault(); showHome(); };
/* overlay focus management: remember the opener, restore focus on close */
let lastFocus = null;
function openHelp(){ lastFocus = document.activeElement; $("helpov").hidden = false; $("helpbox").focus(); }
function closeHelp(){
if ($("helpov").hidden) return;
$("helpov").hidden = true;
if (lastFocus && document.contains(lastFocus)) lastFocus.focus();
lastFocus = null;
}
$("helpbtn").onclick = () => $("helpov").hidden ? openHelp() : closeHelp();
/* ============================ form =================================== */
function select(c, opts = {}){
if (!c) return;
current = c; lastDoc = null;
$("home").hidden = true; $("page").hidden = false;
$("title").innerHTML = '<span class="cname">' + esc(c.name) + "</span> — " + esc(c.title);
$("ref").innerHTML = "Reference: <b>" + esc(c.reference) + "</b>";
$("notes").textContent = c.notes || "";
$("error").textContent = "";
$("rescard").hidden = true;
$("results").innerHTML = "";
$("opsbtn").hidden = !c.mappable;
const f = $("form");
f.innerHTML = "";
for (const p of c.params){
const row = document.createElement("div"); row.className = "prow";
const lab = document.createElement("label");
lab.innerHTML = esc(p.name) + (p.required ? ' <span class="req">*</span>' : "");
lab.title = p.help;
row.appendChild(lab);
let inp;
if (p.choices.length){
inp = document.createElement("select");
if (p.required && !p.choices.includes(p.default)){
const o = document.createElement("option");
o.value = ""; o.textContent = "—"; inp.appendChild(o);
}
for (const ch of p.choices){
const o = document.createElement("option");
o.value = ch; o.textContent = ch;
if (ch === p.default) o.selected = true;
inp.appendChild(o);
}
inp.onchange = () => { updateCliMirror(); if (formValid()) run(); };
} else {
inp = document.createElement("input");
inp.autocomplete = "off"; inp.spellcheck = false;
inp.placeholder = p.default !== null && p.default !== ""
? (typeof p.default === "number" && !p.text
? engInput(p.default) : String(p.default))
: (p.required ? "required" : "");
inp.onkeydown = e => { if (e.key === "Enter"){ e.preventDefault(); run(); } };
inp.oninput = () => {
refreshParse(inp); updateCliMirror();
clearTimeout(debTimer);
debTimer = setTimeout(() => { if (formValid()) run(); }, 320);
};
}
inp.dataset.param = p.name;
inp.dataset.text = p.text ? "1" : "";
inp.setAttribute("aria-label", p.name);
row.appendChild(inp);
const u = document.createElement("span");
u.className = "unit"; u.textContent = p.unit || "";
row.appendChild(u);
f.appendChild(row);
const meta = document.createElement("div"); meta.className = "pmeta";
meta.innerHTML = '<span class="help" title="' + esc(p.help) + '">' + esc(p.help) +
'</span><span class="parsed" data-parsed-for="' + esc(p.name) + '"></span>';
f.appendChild(meta);
}
drawDiagram(c.name, {});
renderSide(); updatePinBtn(); renderHistory(); updateCliMirror();
if (!opts.noHash) setHash(new URLSearchParams({calc: c.name}).toString());
const first = f.querySelector("input,select");
if (first && !opts.noFocus) first.focus();
}
/* live engineering-notation feedback under each numeric field */
function refreshParse(inp){
const tag = document.querySelector('[data-parsed-for="' + CSS.escape(inp.dataset.param) + '"]');
if (!tag) return;
if (inp.dataset.text || inp.tagName === "SELECT") return;
const p = current.params.find(x => x.name === inp.dataset.param);
const v = parseEng(inp.value);
inp.classList.toggle("bad", Number.isNaN(v));
if (v === null){ tag.textContent = ""; tag.className = "parsed"; return; }
if (Number.isNaN(v)){ tag.textContent = "unparseable"; tag.className = "parsed bad"; return; }
tag.className = "parsed";
const pretty = fmtEng(v, p ? p.unit : "");
tag.textContent = String(v) === inp.value.trim() && !p?.unit ? "" : "= " + pretty;
}
function formValid(){
if (!current) return false;
for (const el of document.querySelectorAll("#form [data-param]")){
const p = current.params.find(x => x.name === el.dataset.param);
if (!p) continue;
if (el.value === ""){ if (p.required) return false; continue; }
if (el.tagName !== "SELECT" && !p.text && Number.isNaN(parseEng(el.value))) return false;
}
return true;
}
function collectInputs(){
const inputs = {};
document.querySelectorAll("#form [data-param]").forEach(el => {
if (el.value !== "") inputs[el.dataset.param] = el.value;
});
return inputs;
}
$("resetbtn").onclick = () => {
document.querySelectorAll("#form [data-param]").forEach(el => {
if (el.tagName === "SELECT"){
const p = current.params.find(x => x.name === el.dataset.param);
el.value = p && p.choices.includes(p.default) ? p.default : el.options[0].value;
} else { el.value = ""; refreshParse(el); el.classList.remove("bad"); }
});
$("error").textContent = ""; updateCliMirror();
};
$("runbtn").onclick = () => run();
$("printbtn").onclick = () => print();
$("pinbtn").onclick = () => current && pinToggle(current.name);
$("linkbtn").onclick = () => {
if (!current) return;
const h = new URLSearchParams({calc: current.name, ...collectInputs()});
copyText(location.origin + location.pathname + "#" + h.toString(), "link copied");
};
function updatePinBtn(){
if (!current) return;
$("pinbtn").textContent = pins().includes(current.name) ? "★ pinned" : "☆ pin";
$("pinbtn").classList.toggle("on", pins().includes(current.name));
}
function updateCliMirror(){
const el = $("clicmd");
if (!current){ el.innerHTML = ""; el.onclick = null; return; }
const line = cliLine(current.name, collectInputs());
el.innerHTML = "$ <b>" + esc(line) + "</b>";
el.onclick = () => copyText(line, "CLI command copied");
}
/* ============================ run ==================================== */
async function run(){
if (!current) return;
clearTimeout(debTimer);
/* client-side required check with a visible nudge */
for (const p of current.params){
if (!p.required) continue;
const el = document.querySelector('#form [data-param="' + CSS.escape(p.name) + '"]');
if (el && el.value === ""){
el.classList.remove("flash-req"); void el.offsetWidth; el.classList.add("flash-req");
el.focus();
$("error").textContent = "missing required parameter “" + p.name + "”";
return;
}
}
const inputs = collectInputs();
const params = new URLSearchParams({name: current.name, ...inputs});
noteRecent(current.name);
setHash(new URLSearchParams({calc: current.name, ...inputs}).toString());
const seq = ++runSeq;
if (runCtrl) runCtrl.abort();
runCtrl = new AbortController();
const t0 = performance.now();
let doc;
try {
const r = await fetch("/api/run?" + params.toString(), {signal: runCtrl.signal});
doc = await r.json();
setStatus(true);
} catch (e) {
if (e.name !== "AbortError") setStatus(false);
return;
}
if (seq !== runSeq) return; // a newer run superseded us
const ms = performance.now() - t0;
if (doc.error){
$("error").textContent = "ERROR: " + doc.error;
markErrorField(doc.error);
return;
}
$("error").textContent = "";
document.querySelectorAll("#form .bad").forEach(el => el.classList.remove("bad"));
lastDoc = doc;
renderResults(doc, ms);
pushHistory(current.name, inputs, doc);
drawDiagram(current.name, {inputs: doc.inputs, results: doc.results});
}
/* highlight the offending field when the server names it */
function markErrorField(msg){
if (!current) return;
for (const p of current.params){
if (msg.includes("'" + p.name + "'") || msg.startsWith(p.name + ":")){
const el = document.querySelector('#form [data-param="' + CSS.escape(p.name) + '"]');
if (el){ el.classList.add("bad"); el.focus(); }
return;
}
}
}
/* ============================ results ================================ */
const BAND_COLORS = {black:"#111",brown:"#6b3d1f",red:"#c0392b",orange:"#e67e22",
yellow:"#f1c40f",green:"#27ae60",blue:"#2980b9",violet:"#8e44ad",grey:"#95a5a6",
white:"#ecf0f1",gold:"#c9a227",silver:"#bdc3c7",pink:"#e8a0bf"};
let subtables = {}; // id -> {cols, rows, sortCol, dir}
function renderResults(doc, ms){
$("rescard").hidden = false;
$("restime").textContent = fmtEng(ms/1000, "s") + " · " +
new Date().toLocaleTimeString([], {hour12: false});
subtables = {};
const prev = prevNums[doc.calc] || {};
const nums = {};
let html = "<table><tr><th>result</th><th>value</th><th>note</th></tr>";
let row = 0;
for (const [k, cell] of Object.entries(doc.results)){
let val;
const v = cell.value;
if (Array.isArray(v)) val = renderList(k, v);
else if (typeof v === "boolean")
val = v ? '<span class="chip ok">✓ yes</span>' : '<span class="chip err">✗ NO</span>';
else if (typeof v === "number"){
nums[k] = v;
const alts = [String(v), ...altForms(v, cell.unit)];
val = '<span class="num" data-copy="' + esc(String(v)) + '" title="' +
esc(alts.join("\n") + "\nclick to copy") + '">' +
esc(fmtEng(v, cell.unit)) + "</span>" + deltaChip(prev[k], v);
} else val = esc((String(v) + " " + (cell.unit||"")).trim());
html += '<tr style="animation-delay:' + (row++ * 22) + 'ms">' +
'<td class="rk">' + esc(k) + '</td><td class="val">' + val +
'</td><td class="note">' + esc(cell.note || "") + "</td></tr>";
}
html += "</table>";
$("results").innerHTML = html;
prevNums[doc.calc] = nums;
$("results").querySelectorAll(".num").forEach(el =>
el.onclick = () => copyText(el.dataset.copy, "value copied"));
$("results").querySelectorAll(".subtable th").forEach(th =>
th.onclick = () => sortSub(th.dataset.tid, th.dataset.col));
}
function deltaChip(prev, v){
if (typeof prev !== "number" || prev === v || prev === 0 || !Number.isFinite(prev)) return "";
const pct = (v - prev) / Math.abs(prev) * 100;
if (Math.abs(pct) < 0.005) return "";
const up = pct > 0;
const body = Math.abs(pct) >= 1000 && v / prev > 0
? "×" + (v / prev).toPrecision(3) // huge swings read as a ratio
: (up ? "+" : "−") + Math.abs(pct).toPrecision(3) + "%";
return '<span class="delta" title="vs previous run">' + (up ? "▲" : "▼") + body + "</span>";
}
function renderList(key, list){
if (!list.length) return "—";
if (typeof list[0] !== "object"){
return list.map(x => {
const sw = BAND_COLORS[x] ? '<span class="sw" style="background:' + BAND_COLORS[x] + '"></span>' : "";
return '<span class="lchip">' + sw + esc(x) + "</span>";
}).join("");
}
const tid = "st-" + key.replace(/\W/g, "_");
subtables[tid] = {cols: Object.keys(list[0]), rows: list, sortCol: null, dir: 1};
return subHtml(tid);
}
function subHtml(tid){
const t = subtables[tid];
let h = '<table class="subtable" id="' + tid + '"><tr>' + t.cols.map(c =>
'<th data-tid="' + tid + '" data-col="' + esc(c) + '" title="sort">' + esc(c) +
(t.sortCol === c ? ' <span class="arr">' + (t.dir > 0 ? "▲" : "▼") + "</span>" : "") +
"</th>").join("") + "</tr>";
for (const rw of t.rows)
h += "<tr>" + t.cols.map(c => "<td>" +
esc(typeof rw[c] === "number" ? Number(rw[c].toPrecision(6)) : rw[c]) +
"</td>").join("") + "</tr>";
return h + "</table>";
}
function sortSub(tid, col){
const t = subtables[tid];
if (!t) return;
t.dir = t.sortCol === col ? -t.dir : 1;
t.sortCol = col;
t.rows = [...t.rows].sort((a,b) => {
const x = a[col], y = b[col];
return (typeof x === "number" && typeof y === "number"
? x - y : String(x).localeCompare(String(y))) * t.dir;
});
const node = document.getElementById(tid);
const tmp = document.createElement("div"); tmp.innerHTML = subHtml(tid);
node.replaceWith(tmp.firstChild);
document.getElementById(tid).querySelectorAll("th").forEach(th =>
th.onclick = () => sortSub(th.dataset.tid, th.dataset.col));
}
/* copy / export toolbar */
$("cpmd").onclick = () => lastDoc && copyText(toMarkdown(lastDoc), "markdown copied");
$("cpjson").onclick = () => lastDoc && copyText(JSON.stringify(lastDoc, null, 1), "JSON copied");
$("cpcli").onclick = () => lastDoc && copyText(cliLine(lastDoc.calc,
Object.fromEntries(Object.entries(lastDoc.inputs).map(([k,v]) => [k, String(v)]))),
"CLI command copied");
$("opsbtn").onclick = async () => {
if (!current) return;
const params = new URLSearchParams({name: current.name, ...collectInputs()});
try {
const r = await fetch("/api/ops?" + params.toString());
const doc = await r.json();
if (doc.error){ toast(doc.error); return; }
const blob = new Blob([JSON.stringify(doc, null, 1) + "\n"], {type: "application/json"});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = current.name + "-ops.json";
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
toast("op-list downloaded — edit coordinates, then `akcli plan`");
} catch (e) { toast("op-list export failed"); }
};
/* ============================ history ================================ */
function pushHistory(name, inputs, doc){
const h = store("akcli-history", []);
const headKey = Object.keys(doc.results).find(k => typeof doc.results[k].value === "number");
const head = headKey
? headKey + " = " + fmtEng(doc.results[headKey].value, doc.results[headKey].unit) : "";
const entry = {c: name, p: inputs, t: Date.now(), r: head};
if (h.length && h[0].c === name && JSON.stringify(h[0].p) === JSON.stringify(inputs)){
h[0] = entry; // same computation re-run: refresh in place
} else h.unshift(entry);
save("akcli-history", h.slice(0, 50));
renderHistory();
}
function renderHistory(){
const h = store("akcli-history", []);
$("histcard").hidden = !h.length;
const box = $("histlist");
box.innerHTML = "";
for (const e of h.slice(0, 14)){
const b = document.createElement("button");
b.className = "hrow";
const ts = new Date(e.t).toLocaleTimeString([], {hour12: false});
b.innerHTML = '<span class="ht">' + ts + '</span><span class="hc">' + esc(e.c) +
'</span><span class="hp">' +
esc(Object.entries(e.p).map(([k,v]) => k + "=" + v).join(" ")) +
'</span><span class="hr">' + esc(e.r || "") + "</span>";
b.onclick = () => restoreHistory(e);
box.appendChild(b);
}
}
function restoreHistory(e){
const c = byName(e.c);
if (!c) return;
select(c, {noFocus: true});
document.querySelectorAll("#form [data-param]").forEach(el => {
const v = e.p[el.dataset.param];
if (el.tagName === "SELECT"){
const p = c.params.find(x => x.name === el.dataset.param);
el.value = v ?? (p && p.choices.includes(p.default) ? p.default : el.options[0].value);
} else { el.value = v ?? ""; refreshParse(el); }
});
updateCliMirror();
run();
}
$("histclear").onclick = () => { save("akcli-history", []); renderHistory(); };
/* ============================ palette ================================ */
let palSel = 0, palRows = [];
function openPalette(){
lastFocus = document.activeElement;
$("palov").hidden = false;
$("palinput").value = ""; palSel = 0;
renderPalette();
$("palinput").focus();
}
function closePalette(){
if ($("palov").hidden) return;
$("palov").hidden = true;
if (lastFocus && document.contains(lastFocus)) lastFocus.focus();
lastFocus = null;
}
function renderPalette(){
const q = $("palinput").value.trim();
const recent = store("akcli-recent", []);
let scored = allCalcs().map(c => {
if (!q){
const ri = recent.indexOf(c.name);
return {c, score: ri >= 0 ? 100 - ri : 0, idx: null};
}
const mn = fuzzy(q, c.name), mt = fuzzy(q, c.title + " " + c.group);
if (!mn && !mt) return null;
return {c, score: Math.max(mn ? mn.score + 4 : -1, mt ? mt.score : -1),
idx: mn ? mn.idx : null};
}).filter(Boolean);
scored.sort((a,b) => b.score - a.score);
palRows = scored.slice(0, 12);
palSel = Math.min(palSel, Math.max(0, palRows.length - 1));
$("pallist").innerHTML = palRows.map((r, i) =>
'<div class="palrow' + (i === palSel ? " sel" : "") + '" data-i="' + i + '">' +
'<span class="pn">' + (r.idx ? hilite(r.c.name, r.idx) : esc(r.c.name)) + "</span>" +
'<span class="pt">' + esc(r.c.title) + "</span>" +
'<span class="pg">' + esc(r.c.group) + "</span></div>").join("") ||
'<div class="palrow"><span class="pt">no match</span></div>';
$("pallist").querySelectorAll(".palrow[data-i]").forEach(el => {
el.onclick = () => { closePalette(); select(palRows[+el.dataset.i].c); };
el.onmousemove = () => {
if (palSel !== +el.dataset.i){ palSel = +el.dataset.i; paintPalSel(); }
};
});
}
function paintPalSel(){
$("pallist").querySelectorAll(".palrow[data-i]").forEach(el =>
el.classList.toggle("sel", +el.dataset.i === palSel));
const sel = $("pallist").querySelector(".palrow.sel");
if (sel) sel.scrollIntoView({block: "nearest"});
}
$("palinput").addEventListener("input", () => { palSel = 0; renderPalette(); });
$("palinput").addEventListener("keydown", e => {
if (e.key === "ArrowDown"){ e.preventDefault(); palSel = Math.min(palSel+1, palRows.length-1); paintPalSel(); }
else if (e.key === "ArrowUp"){ e.preventDefault(); palSel = Math.max(palSel-1, 0); paintPalSel(); }
else if (e.key === "Enter" && palRows[palSel]){ closePalette(); select(palRows[palSel].c); }
else if (e.key === "Escape") closePalette();
});
$("palov").addEventListener("mousedown", e => { if (e.target === $("palov")) closePalette(); });
$("helpov").addEventListener("mousedown", e => { if (e.target === $("helpov")) closeHelp(); });
/* ============================ global keys ============================ */
document.addEventListener("keydown", e => {
const typing = /^(INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName);
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k"){
e.preventDefault(); $("palov").hidden ? openPalette() : closePalette(); return;
}
if (e.key === "Escape"){ closePalette(); closeHelp(); return; }
if (typing) return;
if (e.key === "/"){ e.preventDefault(); $("filter").focus(); $("filter").select(); }
else if (e.key === "?"){ $("helpov").hidden ? openHelp() : closeHelp(); }
else if (e.key === "h" || e.key === "H"){ location.href = "/"; }
else if (e.key === "l" || e.key === "L"){ location.href = "/live"; }
});
/* ============================ SVG diagrams =========================== */
/* Physical-style illustrations; colors ride the theme via CSS variables. */
const C = { cu:"var(--dg-cu)", cuDark:"var(--dg-cu-dark)", board:"var(--dg-board)",
boardDark:"var(--dg-board-dark)", dim:"var(--dg-dim)", wire:"var(--dg-wire)",
body:"var(--dg-body)", plane:"var(--dg-plane)", hot:"var(--dg-hot)",
flame:"var(--dg-flame)", gold:"var(--dg-gold)", ink:"var(--dg-ink)",
panel:"var(--panel)", ok:"var(--ok)" };
const DEFS = `<defs><marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="${C.dim}"/></marker></defs>`;
function dim(x1,y1,x2,y2,label,lx,ly){
return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${C.dim}" stroke-width="1.3" marker-start="url(#ar)" marker-end="url(#ar)"/>
<text x="${lx}" y="${ly}" fill="${C.dim}" font-size="13" text-anchor="middle">${label}</text>`;
}
function svg(inner, w=420, h=250){ return `<svg viewBox="0 0 ${w} ${h}" width="${w}" role="img">${DEFS}${inner}</svg>`; }
function rbox(x,y,w,h,label){ return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="none" stroke="${C.wire}" stroke-width="1.6"/><text x="${x+w/2}" y="${y+h/2+4}" fill="${C.wire}" font-size="12" text-anchor="middle">${label}</text>`; }
function wire(pts){ return `<polyline points="${pts}" fill="none" stroke="${C.wire}" stroke-width="1.6"/>`; }
function gnd(x,y){ return `<line x1="${x}" y1="${y}" x2="${x}" y2="${y+8}" stroke="${C.wire}" stroke-width="1.6"/><line x1="${x-10}" y1="${y+8}" x2="${x+10}" y2="${y+8}" stroke="${C.wire}" stroke-width="2"/><line x1="${x-6}" y1="${y+12}" x2="${x+6}" y2="${y+12}" stroke="${C.wire}" stroke-width="2"/><line x1="${x-2}" y1="${y+16}" x2="${x+2}" y2="${y+16}" stroke="${C.wire}" stroke-width="2"/>`; }
function label(x,y,t,anchor="middle",col=C.wire,size=13){ return `<text x="${x}" y="${y}" fill="${col}" font-size="${size}" text-anchor="${anchor}">${t}</text>`; }
/* the one annotatable caption per diagram — ANNOT rewrites it with live values */
function alabel(x,y,t,size=12){ return `<text id="an1" x="${x}" y="${y}" fill="${C.dim}" font-size="${size}" text-anchor="middle">${t}</text>`; }
const traceXsec = svg(`
<rect x="40" y="120" width="340" height="60" fill="${C.board}" stroke="${C.boardDark}"/>
<polygon points="150,120 270,120 262,96 158,96" fill="${C.cu}" stroke="${C.cuDark}"/>
${dim(150,86,270,86,"width W",210,78)}
${dim(292,96,292,120,"H (Cu)",332,112)}
${label(210,205,"substrate (FR-4)",'middle',C.dim,12)}
${alabel(210,40,"I → heat: I = k·ΔT^0.44·A^0.725 (IPC-2221)")}`);
const DIAGRAMS = {
trackwidth: traceXsec, trackcurrent: traceXsec, tracktemp: traceXsec,
clearance: svg(`
<rect x="30" y="90" width="360" height="90" fill="${C.board}" stroke="${C.boardDark}"/>
<rect x="70" y="110" width="110" height="50" rx="6" fill="${C.cu}" stroke="${C.cuDark}"/>
<rect x="240" y="110" width="110" height="50" rx="6" fill="${C.cu}" stroke="${C.cuDark}"/>
${label(125,140,"V1","middle",C.ink,14)} ${label(295,140,"V2","middle",C.ink,14)}
${dim(180,135,240,135,"",0,0)} ${label(210,124,"clearance",'middle',C.dim)}
${alabel(210,205,"IPC-2221B Table 6-1 · columns B1–B4, A5–A7")}`),
via: svg(`
<rect x="30" y="70" width="200" height="120" fill="${C.board}" stroke="${C.boardDark}"/>
<rect x="100" y="70" width="12" height="120" fill="${C.cu}"/>
<rect x="148" y="70" width="12" height="120" fill="${C.cu}"/>
<rect x="88" y="62" width="84" height="9" fill="${C.cu}"/>
<rect x="88" y="189" width="84" height="9" fill="${C.cu}"/>
${dim(112,150,148,150,"d",130,144)}
${dim(250,62,250,198,"L",262,134)}
<circle cx="330" cy="110" r="42" fill="${C.cu}"/>
<circle cx="330" cy="110" r="28" fill="${C.panel}" stroke="${C.cuDark}"/>
${dim(330,66,330,82,"T",348,76)}
${label(330,180,"top view",'middle',C.dim,12)}
${alabel(210,230,"barrel: R=ρL/A · L≈5.08h(ln(4h/d)+1) nH",11)}`),
microstrip: svg(`
<rect x="40" y="170" width="340" height="16" fill="${C.plane}"/>
<rect x="40" y="110" width="340" height="60" fill="${C.board}" stroke="${C.boardDark}"/>
<rect x="170" y="96" width="80" height="14" fill="${C.cu}" stroke="${C.cuDark}"/>
${dim(170,86,250,86,"w",210,78)}
${dim(300,110,300,170,"h",315,144)}
${label(90,145,"εr",'middle',C.dim)}
${alabel(210,220,"ground plane · Hammerstad–Jensen 1980")}`),
stripline: svg(`
<rect x="40" y="60" width="340" height="14" fill="${C.plane}"/>
<rect x="40" y="186" width="340" height="14" fill="${C.plane}"/>
<rect x="40" y="74" width="340" height="112" fill="${C.board}" stroke="${C.boardDark}"/>
<rect x="175" y="123" width="70" height="12" fill="${C.cu}" stroke="${C.cuDark}"/>
${dim(175,112,245,112,"w",210,105)}
${dim(310,74,310,186,"b",325,134)}
${label(90,135,"εr",'middle',C.dim)}
${alabel(210,232,"Cohn 1954 · exact (elliptic integrals)")}`),
coax: svg(`
<circle cx="210" cy="125" r="90" fill="${C.plane}"/>
<circle cx="210" cy="125" r="76" fill="${C.board}"/>
<circle cx="210" cy="125" r="22" fill="${C.cu}" stroke="${C.cuDark}"/>
${dim(210,103,210,50,"",0,0)} ${label(232,72,"D",'middle',C.dim)}
${dim(188,125,232,125,"d",210,118)}
${alabel(210,235,"Z0 = (η0/2π√εr)·ln(D/d)")}`),
twinlead: svg(`
<circle cx="140" cy="120" r="34" fill="${C.cu}" stroke="${C.cuDark}"/>
<circle cx="280" cy="120" r="34" fill="${C.cu}" stroke="${C.cuDark}"/>
${dim(140,170,280,170,"spacing D",210,192)}
${dim(106,90,174,90,"d",140,80)}
${alabel(210,230,"Z0 = (η0/π√εr)·acosh(D/d)")}`),
awg: svg(`
<circle cx="210" cy="120" r="70" fill="${C.cu}" stroke="${C.cuDark}" stroke-width="2"/>
${label(210,126,"Cu","middle",C.ink,16)}
${dim(140,120,280,120,"",0,0)} ${label(210,102,"d = 0.127·92^((36−n)/39) mm",'middle',C.dim,12)}
${label(210,225,"ASTM B258-18",'middle',C.dim,12)}`),
fusing: svg(`
<rect x="40" y="120" width="340" height="50" fill="${C.board}" stroke="${C.boardDark}"/>
<rect x="60" y="108" width="120" height="14" fill="${C.cu}"/>
<rect x="240" y="108" width="120" height="14" fill="${C.cu}"/>
<path d="M180,115 Q195,95 210,115 Q225,135 240,115" stroke="${C.hot}" stroke-width="4" fill="none"/>
<path d="M195,88 q4,-10 0,-18 M210,92 q4,-10 0,-18 M225,88 q4,-10 0,-18" stroke="${C.flame}" stroke-width="2" fill="none"/>
${label(210,205,"adiabatic fusing: Onderdonk 1928 · steady: Preece 1884",'middle',C.dim,12)}`),
wavelength: svg(`
<path d="M40,120 C70,50 100,50 130,120 S190,190 220,120 S280,50 310,120 S355,175 380,132" stroke="${C.wire}" stroke-width="2" fill="none"/>
${dim(40,190,220,190,"λ",130,212)}
${label(210,50,"v = c/√εr",'middle',C.dim)}`),
attenuator: svg(`
${wire("30,100 140,100")} ${rbox(140,84,60,32,"R series")} ${wire("200,100 390,100")}
${wire("110,100 110,140")} ${rbox(92,140,36,44,"R sh")} ${gnd(110,184)}
${wire("310,100 310,140")} ${rbox(292,140,36,44,"R sh")} ${gnd(310,184)}
${label(30,90,"in","start")} ${label(390,90,"out","end")}
${label(210,230,"PI topology (TEE/bridged-tee mirror it)",'middle',C.dim,12)}`),
ohm: svg(`
${wire("100,70 320,70 320,110")} ${rbox(302,110,36,50,"R")} ${wire("320,160 320,190 100,190 100,145")}
<line x1="85" y1="130" x2="115" y2="130" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="93" y1="145" x2="107" y2="145" stroke="${C.wire}" stroke-width="2"/>
${wire("100,70 100,130")}
${label(70,110,"V","middle")} ${label(360,138,"I→","start",C.dim)}
${label(210,230,"V = I·R · P = V·I",'middle',C.dim,12)}`),
vdivider: svg(`
${label(210,40,"Vin","middle")} ${wire("210,48 210,70")} ${rbox(192,70,36,52,"R top")}
${wire("210,122 210,150")} <circle cx="210" cy="150" r="3.5" fill="${C.wire}"/>
${wire("210,150 320,150")} ${label(330,155,"Vout","start")}
${wire("210,150 210,170")} ${rbox(192,170,36,52,"R bot")} ${wire("210,222 210,232")} ${gnd(210,232)}
${alabel(110,150,"Vout = Vin·Rb/(Rt+Rb)",11)}`),
led: svg(`
${label(110,40,"Vs","middle")} ${wire("110,48 110,70")} ${rbox(92,70,36,52,"R")}
${wire("110,122 110,150")}
<polygon points="98,150 122,150 110,172" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
<line x1="98" y1="172" x2="122" y2="172" stroke="${C.wire}" stroke-width="1.8"/>
<path d="M124,158 l14,-10 M124,168 l14,-10" stroke="${C.gold}" stroke-width="1.6"/>
${wire("110,172 110,196")} ${gnd(110,196)}
${alabel(260,120,"R = (Vs − n·Vf)/If")}`),
rc: svg(`
${label(60,105,"Vin","middle")} ${wire("75,100 140,100")} ${rbox(140,84,60,32,"R")}
${wire("200,100 300,100")} <circle cx="260" cy="100" r="3.5" fill="${C.wire}"/>
${label(315,105,"Vout","start")}
${wire("260,100 260,140")}
<line x1="242" y1="140" x2="278" y2="140" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="242" y1="152" x2="278" y2="152" stroke="${C.wire}" stroke-width="2.5"/>
${label(292,152,"C","start")}
${wire("260,152 260,176")} ${gnd(260,176)}
${alabel(210,230,"τ = RC · fc = 1/(2πRC)")}`),
lc: svg(`
${wire("120,80 300,80")} ${wire("120,180 300,180")}
${wire("160,80 160,110")} <path d="M160,110 a8,8 0 0 1 0,16 a8,8 0 0 1 0,16 a8,8 0 0 1 0,16" fill="none" stroke="${C.wire}" stroke-width="1.8"/> ${wire("160,158 160,180")} ${label(138,134,"L","middle")}
${wire("260,80 260,122")}
<line x1="242" y1="122" x2="278" y2="122" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="242" y1="136" x2="278" y2="136" stroke="${C.wire}" stroke-width="2.5"/>
${wire("260,136 260,180")} ${label(292,134,"C","start")}
${alabel(210,225,"f0 = 1/(2π√LC) · Z = √(L/C)")}`),
regulator: svg(`
<rect x="120" y="60" width="150" height="80" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(150,105,"Vin","middle")} ${label(243,105,"Vout","middle")} ${label(196,128,"ADJ/FB","middle")}
${wire("40,90 120,90")} <circle cx="60" cy="90" r="4" fill="none" stroke="${C.wire}"/>
${wire("270,90 380,90")} <circle cx="360" cy="90" r="4" fill="none" stroke="${C.wire}"/>
${wire("320,90 320,110")} ${rbox(302,110,36,44,"R1")} ${wire("320,154 320,170")}
<circle cx="320" cy="170" r="3.5" fill="${C.wire}"/>
${wire("320,170 195,170 195,140")}
${wire("320,170 320,180")} ${rbox(302,180,36,44,"R2")} ${wire("320,224 320,232")} ${gnd(320,232)}
${label(90,200,"Vout = Vref(1+R2/R1)+Iadj·R2","start",C.dim,11)}`),
buck: svg(`
${label(40,95,"Vin","middle")} ${wire("55,90 110,90")} ${rbox(110,74,52,32,"SW")}
${wire("162,90 210,90")} <circle cx="210" cy="90" r="3.5" fill="${C.wire}"/>
<path d="M210,90 h10 a9,9 0 0 1 18,0 a9,9 0 0 1 18,0 a9,9 0 0 1 18,0 h10" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(247,72,"L","middle")}
${wire("284,90 370,90")} ${label(385,95,"Vout","start")}
<polygon points="198,128 222,128 210,148" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
<line x1="198" y1="148" x2="222" y2="148" stroke="${C.wire}" stroke-width="1.8"/>
${wire("210,90 210,128")} ${wire("210,148 210,166")} ${gnd(210,166)}
${wire("330,90 330,120")}
<line x1="312" y1="120" x2="348" y2="120" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="312" y1="132" x2="348" y2="132" stroke="${C.wire}" stroke-width="2.5"/>
${wire("330,132 330,156")} ${gnd(330,156)} ${label(360,130,"C","start")}
${label(210,225,"TI SLVA477B power stage",'middle',C.dim,12)}`),
boost: svg(`
${label(40,95,"Vin","middle")}
<path d="M55,90 h20 a9,9 0 0 1 18,0 a9,9 0 0 1 18,0 a9,9 0 0 1 18,0 h20" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(103,72,"L","middle")}
${wire("149,90 200,90")} <circle cx="200" cy="90" r="3.5" fill="${C.wire}"/>
<polygon points="230,78 230,102 252,90" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
<line x1="252" y1="78" x2="252" y2="102" stroke="${C.wire}" stroke-width="1.8"/>
${wire("200,90 230,90")} ${wire("252,90 370,90")} ${label(385,95,"Vout","start")}
${rbox(182,120,36,44,"SW")} ${wire("200,90 200,120")} ${wire("200,164 200,178")} ${gnd(200,178)}
${wire("320,90 320,120")}
<line x1="302" y1="120" x2="338" y2="120" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="302" y1="132" x2="338" y2="132" stroke="${C.wire}" stroke-width="2.5"/>
${wire("320,132 320,156")} ${gnd(320,156)}
${label(210,225,"TI SLVA372C power stage",'middle',C.dim,12)}`),
battery: svg(`
<line x1="120" y1="90" x2="120" y2="160" stroke="${C.wire}" stroke-width="3"/>
<line x1="136" y1="105" x2="136" y2="145" stroke="${C.wire}" stroke-width="2"/>
<line x1="152" y1="90" x2="152" y2="160" stroke="${C.wire}" stroke-width="3"/>
<line x1="168" y1="105" x2="168" y2="145" stroke="${C.wire}" stroke-width="2"/>
${wire("168,125 240,125")} ${rbox(240,100,70,50,"load")}
${wire("310,125 350,125 350,200 120,200 120,160")}
${label(210,60,"t ≈ capacity·derating / I_avg",'middle',C.dim,12)}`),
"ne555-astable": svg(`
<rect x="150" y="60" width="110" height="130" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(205,130,"NE555","middle")}
${label(120,40,"VCC","middle")} ${wire("120,48 120,60 90,60")}
${wire("120,60 120,70")} ${rbox(102,70,36,36,"RA")}
${wire("120,106 120,120 150,120")} ${label(143,116,"DIS","end",C.wire,10)}
${wire("120,120 120,128")} ${rbox(102,128,36,36,"RB")}
${wire("120,164 120,178 150,178")} ${label(143,174,"THR","end",C.wire,10)}
${wire("120,178 120,196")}
<line x1="102" y1="196" x2="138" y2="196" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="102" y1="208" x2="138" y2="208" stroke="${C.wire}" stroke-width="2.5"/>
${label(88,206,"C","end")} ${wire("120,208 120,224")} ${gnd(120,224)}
${wire("260,110 330,110")} ${label(340,114,"OUT","start")}
${label(205,240,"f = 1.44/((RA+2RB)·C)",'middle',C.dim,12)}`),
"ne555-mono": svg(`
<rect x="150" y="60" width="110" height="130" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(205,130,"NE555","middle")}
${label(120,40,"VCC","middle")} ${wire("120,48 120,70")} ${rbox(102,70,36,44,"R")}
${wire("120,114 120,140 150,140")} ${label(143,136,"DIS/THR","end",C.wire,9)}
${wire("120,140 120,158")}
<line x1="102" y1="158" x2="138" y2="158" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="102" y1="170" x2="138" y2="170" stroke="${C.wire}" stroke-width="2.5"/>
${label(88,168,"C","end")} ${wire("120,170 120,190")} ${gnd(120,190)}
${wire("260,110 330,110")} ${label(340,114,"OUT","start")}
${wire("90,220 150,220")} ${label(80,224,"TRIG","end",C.wire,10)} ${wire("150,220 150,190")}
${label(205,240,"t = 1.1·R·C",'middle',C.dim,12)}`),
"opamp-gain": svg(`
<polygon points="170,70 170,170 280,120" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(184,100,"−","middle",C.wire,16)} ${label(184,150,"+","middle",C.wire,16)}
${wire("120,95 170,95")} ${wire("120,145 170,145")} ${label(108,150,"in","end")}
${wire("280,120 360,120")} ${label(372,124,"out","start")}
${wire("140,95 140,50 205,50")} ${rbox(205,34,56,32,"Rf")} ${wire("261,50 320,50 320,120")}
${wire("120,95 92,95")} ${rbox(36,79,56,32,"Rg/Rin")} ${wire("36,95 20,95 20,190 64,190")} ${gnd(64,190)}
${label(210,230,"G = 1 + Rf/Rg (non-inv) · −Rf/Rin (inv)",'middle',C.dim,12)}`),
"i2c-pullup": svg(`
${label(210,36,"VDD","middle")} ${wire("120,44 300,44")} ${wire("210,36 210,44")}
${wire("140,44 140,60")} ${rbox(122,60,36,44,"Rp")} ${wire("140,104 140,130")}
${wire("280,44 280,60")} ${rbox(262,60,36,44,"Rp")} ${wire("280,104 280,160")}
${wire("40,130 380,130")} ${label(392,134,"SDA","start")}
${wire("40,160 380,160")} ${label(392,164,"SCL","start")}
<circle cx="140" cy="130" r="3.5" fill="${C.wire}"/><circle cx="280" cy="160" r="3.5" fill="${C.wire}"/>
${rbox(60,186,84,40,"master")} ${rbox(250,186,84,40,"device")}
${wire("90,186 90,130")} ${wire("120,186 120,160")} ${wire("290,186 290,130")} ${wire("312,186 312,160")}
${alabel(210,246,"NXP UM10204 §7.1: Rp window from VOL and t_r",11)}`),
"crystal-caps": svg(`
${wire("100,90 170,90")} ${wire("250,90 320,90")}
${label(88,94,"OSC_IN","end",C.wire,11)} ${label(332,94,"OSC_OUT","start",C.wire,11)}
<line x1="170" y1="70" x2="170" y2="110" stroke="${C.wire}" stroke-width="2.5"/>
<rect x="182" y="66" width="56" height="48" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
<line x1="250" y1="70" x2="250" y2="110" stroke="${C.wire}" stroke-width="2.5"/>
${label(210,94,"XTAL","middle",C.wire,11)}
${wire("140,90 140,130")}
<line x1="122" y1="130" x2="158" y2="130" stroke="${C.wire}" stroke-width="2.5"/><line x1="122" y1="142" x2="158" y2="142" stroke="${C.wire}" stroke-width="2.5"/>
${label(108,142,"C1","end")} ${wire("140,142 140,166")} ${gnd(140,166)}
${wire("290,90 290,130")}
<line x1="272" y1="130" x2="308" y2="130" stroke="${C.wire}" stroke-width="2.5"/><line x1="272" y1="142" x2="308" y2="142" stroke="${C.wire}" stroke-width="2.5"/>
${label(322,142,"C2","start")} ${wire("290,142 290,166")} ${gnd(290,166)}
${label(210,225,"C1 = C2 = 2·(CL − Cstray) — ST AN2867",'middle',C.dim,12)}`),
thermal: svg(`
<rect x="150" y="150" width="120" height="26" fill="${C.body}" stroke="${C.plane}"/>
<rect x="175" y="128" width="70" height="22" fill="#7a4034" stroke="#a35"/>
<rect x="196" y="112" width="28" height="16" fill="${C.hot}"/>
<rect x="120" y="176" width="180" height="14" fill="${C.plane}"/>
${Array.from({length:7},(_,i)=>`<rect x="${128+i*25}" y="190" width="9" height="34" fill="${C.plane}"/>`).join("")}
${label(210,104,"junction (Tj)","middle",C.dim,11)}
${dim(320,120,320,150,"θJC",352,132)} ${dim(320,152,320,176,"θCS",352,166)} ${dim(320,178,320,224,"θSA",352,202)}
${label(210,246,"Tj = Ta + P·Σθ — JESD51",'middle',C.dim,12)}`),
rescolor: svg(`
${wire("40,120 110,120")} ${wire("310,120 380,120")}
<rect x="110" y="96" width="200" height="48" rx="22" fill="#d8c49a" stroke="#a08a5c"/>
<rect id="band0" x="136" y="96" width="16" height="48" fill="#666"/>
<rect id="band1" x="164" y="96" width="16" height="48" fill="#666"/>
<rect id="band2" x="192" y="96" width="16" height="48" fill="#666"/>
<rect id="band3" x="220" y="96" width="16" height="48" fill="#666"/>
<rect id="band4" x="266" y="96" width="16" height="48" fill="#666"/>
${label(210,200,"IEC 60062:2016 color bands",'middle',C.dim,12)}`),
smdcode: svg(`
<rect x="120" y="90" width="180" height="80" rx="4" fill="#111" stroke="#333"/>
<rect x="104" y="90" width="16" height="80" fill="#c9c9c9"/>
<rect x="300" y="90" width="16" height="80" fill="#c9c9c9"/>
<text id="smdtext" x="210" y="140" fill="#eee" font-size="30" text-anchor="middle" font-family="monospace">472</text>
${label(210,210,"3/4-digit · R-decimal · EIA-96 (IEC 60062)",'middle',C.dim,12)}`),
galvanic: svg(`
<rect x="90" y="90" width="110" height="90" fill="#b87333" stroke="#8a5626"/>
<rect x="200" y="90" width="110" height="90" fill="#c0c0c0" stroke="#909090"/>
<text x="145" y="140" fill="${C.ink}" font-size="13" text-anchor="middle">metal 1</text>
<text x="255" y="140" fill="${C.ink}" font-size="13" text-anchor="middle">metal 2</text>
<path d="M195,60 q10,18 0,26 q-10,-8 0,-26" fill="#6ec6ff88" stroke="${C.dim}"/>
${label(200,50,"electrolyte",'middle',C.dim,11)}
${dim(120,215,280,215,"ΔV anodic index",200,238)}
${label(200,205,"MIL-STD-889C",'middle',C.dim,11)}`),
reactance: svg(`
<line x1="120" y1="90" x2="120" y2="130" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="134" y1="90" x2="134" y2="130" stroke="${C.wire}" stroke-width="2.5"/>
${wire("90,110 120,110")} ${wire("134,110 164,110")} ${label(127,155,"Xc = 1/(2πfC)","middle",C.dim,12)}
<path d="M250,110 a9,9 0 0 1 18,0 a9,9 0 0 1 18,0 a9,9 0 0 1 18,0" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${wire("225,110 250,110")} ${wire("304,110 330,110")} ${label(277,155,"Xl = 2πfL","middle",C.dim,12)}`),
diffpair: svg(`
<rect x="40" y="170" width="340" height="16" fill="${C.plane}"/>
<rect x="40" y="110" width="340" height="60" fill="${C.board}" stroke="${C.boardDark}"/>
<rect x="120" y="96" width="70" height="14" fill="${C.cu}" stroke="${C.cuDark}"/>
<rect x="230" y="96" width="70" height="14" fill="${C.cu}" stroke="${C.cuDark}"/>
${dim(120,86,190,86,"w",155,78)} ${dim(190,120,230,120,"s",210,138)}
${dim(340,110,340,170,"h",356,144)}
${alabel(210,220,"Zdiff = 2·Z0·(1−0.48·e^(−0.96·s/h)) — IPC-2141A",11)}`),
lmatch: svg(`
${label(45,105,"Rs","middle")} ${wire("60,100 130,100")}
<path d="M130,100 h8 a8,8 0 0 1 16,0 a8,8 0 0 1 16,0 a8,8 0 0 1 16,0 h8" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(162,80,"L (series, low-R side)","middle",C.wire,10)}
${wire("194,100 360,100")} ${label(378,105,"Rl","middle")}
${wire("280,100 280,132")}
<line x1="262" y1="132" x2="298" y2="132" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="262" y1="144" x2="298" y2="144" stroke="${C.wire}" stroke-width="2.5"/>
${label(312,144,"C","start")} ${wire("280,144 280,168")} ${gnd(280,168)}
${label(210,225,"Q = √(Rhi/Rlo − 1) — Pozar §5.1",'middle',C.dim,12)}`),
pimatch: svg(`
${label(40,105,"Rs","middle")} ${wire("55,100 150,100")}
<path d="M150,100 h8 a8,8 0 0 1 16,0 a8,8 0 0 1 16,0 a8,8 0 0 1 16,0 h8" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(182,80,"L","middle")}
${wire("214,100 370,100")} ${label(385,105,"Rl","middle")}
${[110,300].map(x=>`${wire(`${x},100 ${x},132`)}
<line x1="${x-18}" y1="132" x2="${x+18}" y2="132" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="${x-18}" y1="144" x2="${x+18}" y2="144" stroke="${C.wire}" stroke-width="2.5"/>
${wire(`${x},144 ${x},168`)} ${gnd(x,168)}`).join("")}
${label(110,196,"C1","middle")} ${label(300,196,"C2","middle")}
${label(210,235,"two L-sections via virtual R — Bowick ch.4",'middle',C.dim,11)}`),
"sallen-key": svg(`
${wire("20,110 60,110")} ${rbox(60,94,44,32,"R1")} ${wire("104,110 140,110")}
<circle cx="140" cy="110" r="3" fill="${C.wire}"/> ${rbox(150,94,44,32,"R2")} ${wire("194,110 230,110")}
<polygon points="230,85 230,140 300,112" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(243,108,"+","middle",C.wire,14)} ${label(243,130,"−","middle",C.wire,14)}
${wire("300,112 370,112")} ${label(382,116,"out","start")}
${wire("140,110 140,60 340,60 340,112")}
<text x="188" y="50" fill="${C.wire}" font-size="11">C1 (to out)</text>
${wire("212,110 212,146")}
<line x1="194" y1="146" x2="230" y2="146" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="194" y1="156" x2="230" y2="156" stroke="${C.wire}" stroke-width="2.5"/>
${label(244,156,"C2","start",C.wire,11)} ${wire("212,156 212,176")} ${gnd(212,176)}
${label(210,235,"fc = 1/(2πRC), Q = 1/(3−K) — SLOA024B",'middle',C.dim,11)}`),
shunt: svg(`
${wire("30,100 150,100")} ${rbox(150,86,80,28,"R shunt")} ${wire("230,100 390,100")}
${label(40,90,"I →","start",C.dim)}
${wire("165,114 165,150 185,150")} ${wire("215,114 215,150 195,150")}
<polygon points="185,132 185,168 225,150" fill="none" stroke="${C.wire}" stroke-width="1.6" transform="rotate(90 205 150)"/>
${label(205,205,"sense amp (Kelvin sense)","middle",C.dim,11)}
${label(210,235,"R = Vfs/Imax · P = I²R (≥2× rating) — SBOA170",'middle',C.dim,11)}`),
"gate-drive": svg(`
${rbox(60,84,70,52,"driver")} ${wire("130,110 170,110")} ${rbox(170,94,50,32,"Rg")}
${wire("220,110 260,110")}
<line x1="260" y1="86" x2="260" y2="134" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="272" y1="80" x2="272" y2="140" stroke="${C.wire}" stroke-width="2.5"/>
${wire("272,86 310,86 310,60")} ${wire("272,134 310,134 310,160")} ${gnd(310,160)}
${label(330,60,"D","start")} ${label(240,80,"G","middle")}
${label(210,225,"Ipk ≈ Qg/tsw · P = Qg·Vdrv·f — SLUA618A",'middle',C.dim,12)}`),
"rs485-bias": svg(`
${label(60,40,"VCC","middle")} ${wire("60,48 60,66")} ${rbox(42,66,36,40,"Rup")}
${wire("60,106 60,126")}
${wire("30,126 390,126")} ${label(400,130,"A","start")}
${wire("30,166 390,166")} ${label(400,170,"B","start")}
${rbox(150,132,30,28,"120")} ${wire("165,126 165,132")} ${wire("165,160 165,166")}
${rbox(330,132,30,28,"120")} ${wire("345,126 345,132")} ${wire("345,160 345,166")}
${wire("60,166 60,186")} ${rbox(42,186,36,40,"Rdn")} ${wire("60,226 60,238")} ${gnd(60,238)}
${label(210,225,"idle V_AB ≥ 200 mV — TIA-485-A / SLLA070D",'middle',C.dim,11)}`),
"can-termination": svg(`
${wire("40,100 380,100")} ${label(392,104,"CANH","start",C.wire,11)}
${wire("40,180 380,180")} ${label(392,184,"CANL","start",C.wire,11)}
${wire("210,100 210,110")} ${rbox(192,110,36,26,"60")}
${wire("210,136 210,144")} <circle cx="210" cy="140" r="3" fill="${C.wire}"/>
${rbox(192,144,36,26,"60")} ${wire("210,170 210,180")}
${wire("210,140 280,140 280,152")}
<line x1="262" y1="152" x2="298" y2="152" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="262" y1="162" x2="298" y2="162" stroke="${C.wire}" stroke-width="2.5"/>
${label(312,162,"C","start")} ${wire("280,162 280,182")} ${gnd(280,182)}
${label(210,235,"split termination — ISO 11898-2 §10",'middle',C.dim,12)}`),
ldo: svg(`
<rect x="140" y="70" width="140" height="70" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(210,108,"LDO","middle")} ${wire("40,90 140,90")} ${wire("280,90 380,90")}
${label(55,80,"Vin","middle")} ${label(360,80,"Vout","middle")}
<path d="M180,60 q4,-12 0,-22 M210,62 q4,-12 0,-22 M240,60 q4,-12 0,-22" stroke="${C.flame}" stroke-width="2" fill="none"/>
${label(210,175,"P = (Vin−Vout)·Iout + Vin·Iq","middle",C.dim,12)}
${label(210,200,"Tj = Ta + P·θJA — JESD51",'middle',C.dim,12)}`),
hysteresis: svg(`
<polygon points="180,70 180,150 260,110" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${label(193,100,"+","middle",C.wire,14)} ${label(193,135,"−","middle",C.wire,14)}
${wire("120,95 180,95")} ${wire("120,140 180,140")} ${label(105,144,"sig","end",C.wire,11)}
${wire("260,110 360,110")} ${label(372,114,"out","start")}
${wire("120,95 120,50 60,50")} ${rbox(24,34,36,32,"R1")} ${label(12,54,"VCC","end",C.wire,10)}
${wire("120,95 120,170 92,170")} ${rbox(56,154,36,32,"R2")} ${wire("56,170 40,170 40,192 84,192")} ${gnd(84,192)}
${wire("120,95 148,95 148,40 320,40 320,110")} ${rbox(220,26,44,28,"Rh")}
${label(210,235,"VT± from the 3-R node — TI SLVA954",'middle',C.dim,12)}`),
tvs: svg(`
${wire("40,90 380,90")} ${label(50,78,"line","start",C.dim,11)}
${wire("210,90 210,116")}
<polygon points="196,116 224,116 210,142" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
<path d="M196,142 h28 M192,142 l-6,8 M224,142 l6,-8" stroke="${C.wire}" stroke-width="1.8" fill="none"/>
${wire("210,150 210,172")} ${gnd(210,172)}
${label(258,132,"TVS","start")}
<path d="M60,60 l14,-22 h-8 l16,-20" stroke="${C.gold}" stroke-width="2.4" fill="none"/>
${label(210,225,"VRWM ≥ Vline · Vclamp < abs-max — IEC 61000-4-5 surge",'middle',C.dim,11)}`),
flyback: svg(`
${label(40,95,"Vin","middle")} ${wire("55,90 120,90")}
<path d="M120,90 v10 a8,8 0 0 1 0,14 a8,8 0 0 1 0,14 a8,8 0 0 1 0,14 v10" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
<line x1="146" y1="92" x2="146" y2="160" stroke="${C.wire}" stroke-width="2"/>
<line x1="152" y1="92" x2="152" y2="160" stroke="${C.wire}" stroke-width="2"/>
<path d="M178,100 v6 a8,8 0 0 1 0,14 a8,8 0 0 1 0,14 a8,8 0 0 1 0,14 v6" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
${wire("120,90 120,100")} ${wire("120,152 120,168")} ${rbox(102,168,36,36,"SW")} ${wire("120,204 120,216")} ${gnd(120,216)}
${wire("178,100 178,88 240,88")}
<polygon points="240,76 240,100 262,88" fill="none" stroke="${C.wire}" stroke-width="1.6"/>
<line x1="262" y1="76" x2="262" y2="100" stroke="${C.wire}" stroke-width="1.6"/>
${wire("262,88 370,88")} ${label(382,92,"Vout","start")}
${wire("178,154 178,170 320,170")} ${wire("320,88 320,120")}
<line x1="302" y1="120" x2="338" y2="120" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="302" y1="132" x2="338" y2="132" stroke="${C.wire}" stroke-width="2.5"/>
${wire("320,132 320,170")}
${label(149,60,"Np : Ns","middle",C.dim,11)}
${label(210,240,"D = VOR/(VOR+Vin) · n = VOR/(Vout+Vd) — Erickson ch.6",'middle',C.dim,10)}`),
adc: svg(`
<polyline points="50,190 90,190 90,160 130,160 130,130 170,130 170,100 210,100 210,70 250,70" fill="none" stroke="${C.wire}" stroke-width="2"/>
<path d="M50,188 C100,140 180,80 250,68" stroke="${C.dim}" stroke-width="1.4" fill="none" stroke-dasharray="4 3"/>
${dim(268,70,268,100,"LSB",300,88)}
${label(160,225,"SNR = 6.02·N + 1.76 dB",'middle',C.dim,12)}`),
"fuse-derating": svg(`
${wire("40,110 130,110")}
<rect x="130" y="96" width="160" height="28" rx="14" fill="none" stroke="${C.wire}" stroke-width="1.8"/>
<path d="M130,110 C170,92 250,128 290,110" stroke="${C.wire}" stroke-width="1.6" fill="none"/>
${wire("290,110 380,110")}
${label(210,165,"In ≥ Iload/(0.75·k_temp)","middle",C.dim,12)}
${label(210,190,"R10 ladder: 1 1.25 1.6 2 2.5 3.15 4 5 6.3 8 — IEC 60127",'middle',C.dim,11)}`),
"inrush-ntc": svg(`
${label(45,105,"AC","middle")} ${wire("62,100 120,100")}
${rbox(120,84,70,32,"NTC")}
<line x1="120" y1="128" x2="196" y2="80" stroke="${C.dim}" stroke-width="1.4"/>
${wire("190,100 250,100")}
<line x1="250" y1="82" x2="250" y2="118" stroke="${C.wire}" stroke-width="2.5"/>
<line x1="262" y1="82" x2="262" y2="118" stroke="${C.wire}" stroke-width="2.5"/>
${label(256,140,"C bulk","middle",C.wire,11)}
${wire("262,100 330,100")} ${rbox(330,84,44,32,"load")}
${label(210,200,"R_cold ≥ Vpk/I_max · E = ½CV² — EPCOS guide",'middle',C.dim,11)}`),
rcombo: svg(`
${wire("30,110 90,110")} ${rbox(90,94,60,32,"R1")} ${wire("150,110 190,110")}
<circle cx="190" cy="110" r="3.5" fill="${C.wire}"/>
${wire("190,110 190,70 230,70")} ${rbox(230,54,60,32,"R2")} ${wire("290,70 330,70 330,110")}
${wire("190,110 190,150 230,150")} ${rbox(230,134,60,32,"R3")} ${wire("290,150 330,150 330,110")}
${wire("330,110 390,110")}
${label(210,215,"R1 + (R2 ∥ R3) … up to 4 parts, IEC 60063 pool",'middle',C.dim,12)}`),
};
DIAGRAMS["hysteresis-design"] = DIAGRAMS.hysteresis;
DIAGRAMS["vdivider-design"] = DIAGRAMS.vdivider;
DIAGRAMS["regulator-design"] = DIAGRAMS.regulator;
DIAGRAMS["rc-charge"] = DIAGRAMS.rc;
DIAGRAMS.eseries = DIAGRAMS.rescolor;
const FALLBACK = svg(`
<rect x="140" y="55" width="140" height="150" rx="10" fill="${C.body}" stroke="${C.wire}"/>
<rect x="155" y="70" width="110" height="34" rx="4" fill="#111" stroke="#444"/>
<text x="258" y="94" fill="${C.ok}" font-size="18" text-anchor="end" font-family="monospace">42</text>
${[0,1,2].map(r=>[0,1,2].map(c=>`<rect x="${158+c*38}" y="${116+r*28}" width="30" height="20" rx="4" fill="#3d444a" stroke="#555"/>`).join("")).join("")}`);
/* live diagram captions: computed values replace the formula line */
const rcell = (r, k) => r[k] ? fmtEng(r[k].value, r[k].unit) : "?";
const ANNOT = {
trackwidth: (i,r) => `W ext = ${rcell(r,"external_width")} · int = ${rcell(r,"internal_width")} — IPC-2221`,
trackcurrent: (i,r) => `Imax = ${rcell(r,"i_max")} (${i.layer || "external"}) — IPC-2221`,
tracktemp: (i,r) => `ΔT = ${rcell(r,"dtemp")} — IPC-2221`,
microstrip: (i,r) => `Z0 = ${rcell(r,"z0")} · εeff = ${rcell(r,"eeff")} — Hammerstad–Jensen`,
stripline: (i,r) => `Z0 = ${rcell(r,"z0")} — Cohn 1954`,
coax: (i,r) => `Z0 = ${rcell(r,"z0")}`,
twinlead: (i,r) => `Z0 = ${rcell(r,"z0")}`,
diffpair: (i,r) => `Zdiff = ${rcell(r,"z_diff")} · Zodd = ${rcell(r,"z_odd")} — IPC-2141A`,
vdivider: (i,r) => `Vout = ${rcell(r,"vout")} · Rth = ${rcell(r,"thevenin_r")}`,
led: (i,r) => `R = ${rcell(r,"r_ideal")} → std ${rcell(r,"r_standard")}`,
rc: (i,r) => `τ = ${rcell(r,"tau")} · fc = ${rcell(r,"fc")}`,
lc: (i,r) => `f0 = ${rcell(r,"f0")} · Z = ${rcell(r,"z")}`,
via: (i,r) => `R = ${rcell(r,"resistance")} · L = ${rcell(r,"inductance")} · Imax = ${rcell(r,"i_max")}`,
clearance: (i,r) => `B2 external ≥ ${rcell(r,"b2")} — IPC-2221B T6-1`,
"i2c-pullup": (i,r) => `Rp ${rcell(r,"r_min")} … suggested ${rcell(r,"suggested")} … ${rcell(r,"r_max")}`,
};
function drawDiagram(name, ctx){
const box = $("diagram");
box.innerHTML = DIAGRAMS[name] || FALLBACK;
$("diagcap").textContent = DIAGRAMS[name] ? "" : "no dedicated illustration yet";
if (name === "rescolor" && ctx.results && ctx.results.bands_list){
(ctx.results.bands_list.value || []).forEach((b,i) => {
const el = document.getElementById("band" + i);
if (el) el.setAttribute("fill", BAND_COLORS[b] || "#666");
});
}
if (name === "smdcode" && ctx.inputs && ctx.inputs.code){
const t = document.getElementById("smdtext");
if (t) t.textContent = String(ctx.inputs.code).toUpperCase();
}
const an = ANNOT[name];
if (an && ctx.results){
const el = document.getElementById("an1");
if (el){
try { const t = an(ctx.inputs || {}, ctx.results); if (t) el.textContent = t; }
catch (e) { /* diagram keeps its formula caption */ }
}
}
}
boot();
</script>
</body>
</html>