app
adom-layout-viewer
Public Made by Adomby adom
Interactive PCB layout viewer: your EDA's own render plus live net/trace highlighting and per-pad stock, price and wiki lookup
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>adom-layout-viewer</title>
<style>
/* Canonical Adom design tokens (adom-ui-design). Legacy names kept as aliases.
Primary accent #00b8b0; #00e6dc is the bright hover variant. */
:root { color-scheme: dark;
--bg:#0d1117; --panel:#161b22; --elevated:#1c2128; --overlay:#21262d; --edge:#30363d;
--ink:#e6edf3; --mut:#8b949e; --acc:#00b8b0; --acc2:#00e6dc;
--green:#3fb950; --red:#f85149; --yellow:#d29922; --hl:#4ff0e6;
--line:var(--edge); --txt:var(--ink); --muted:var(--mut); --accent:var(--acc); }
* { box-sizing: border-box; }
/* Three fonts by role: body Satoshi, headers Familjen Grotesk, data JetBrains Mono. */
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--txt); font: 13px/1.45 'Satoshi', -apple-system, 'Segoe UI', Roboto, sans-serif; overflow: hidden; }
#svgbox { position: absolute; inset: 0; cursor: grab; }
#svgbox:active { cursor: grabbing; }
#svgbox svg { display: block; }
.empty { position: absolute; inset: 0; display: grid; place-items: center; color: var(--muted); }
/* overlay is invisible until hovered / net-highlighted; only pads+vias+traces take pointer events.
pointer-events:FILL (not all) so the hit area is the pad SHAPE only — `all` would include the
phantom 1mm default stroke and make the hit zone ~2.5mm wide. */
#svgbox svg .adom-overlay .adom-pad .pad-body, #svgbox svg .adom-overlay .adom-via .pad-body { pointer-events: fill; cursor: pointer; }
#svgbox svg .adom-overlay .ov-trace { pointer-events: stroke; cursor: pointer; }
/* fills are clickable (select the net) but never hovered — hover only targets pads/vias */
#svgbox svg .adom-overlay .ov-zone { pointer-events: fill; cursor: pointer; }
/* ── hover: gentle white outline on the single pad/via ── */
#svgbox svg .adom-pad.pad-hl .pad-body { stroke: #fff !important; stroke-width: .07; paint-order: stroke; }
#svgbox svg .adom-via.pad-hl .pad-body { stroke: #fff !important; stroke-width: .05; }
/* cross-probe: the part selected from the schematic view — bright accent outline
on all of its pads so the whole footprint reads as "this component". */
#svgbox svg .adom-pad.comp-xhl .pad-body { stroke: var(--acc2) !important; stroke-width: .14 !important; paint-order: stroke; }
/* ── click net: dim every base layer; show the selected net's copper EXACTLY as on the
PCB (solid traces w/ round joins, real pads w/ drill holes), just in focus. No
outlines, no brightness boost, no transition (that caused the shrink). ── */
#svgbox svg.dim .adom-flip > g[class*="adom-layer-"] { opacity: .16 !important; }
#svgbox svg .adom-overlay .ov-trace.hl { stroke: var(--hlc) !important; }
#svgbox svg .adom-overlay .ov-zone.hl { fill: var(--hlc) !important; fill-opacity: .3 !important; stroke: none !important; }
#svgbox svg .adom-overlay .adom-pad.hl .pad-body, #svgbox svg .adom-overlay .adom-via.hl .pad-body { fill: var(--hlc) !important; }
#svgbox svg .adom-overlay .adom-pad.hl .pad-hole, #svgbox svg .adom-overlay .adom-via.hl .pad-hole { fill: #161b22 !important; }
/* ── chrome ── */
.panel { position: absolute; background: rgba(22,27,34,.82); backdrop-filter: blur(8px); border: 1px solid var(--line); border-radius: 10px; }
/* single right-side panel: Layers section on top, Nets below */
#side { top: 12px; right: 12px; width: 212px; max-height: 94vh; padding: 10px 12px; display: flex; flex-direction: column; }
#side h1 { margin: 0 0 8px; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
.sec { margin-bottom: 10px; min-height: 0; display: flex; flex-direction: column; }
.sec-h { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); font-weight: 600; margin: 2px 0 5px; border-bottom: 1px solid var(--line); padding-bottom: 3px; }
#layers { max-height: 42vh; overflow-y: auto; }
#netlist { max-height: 30vh; overflow-y: auto; }
.lyr { display: flex; align-items: center; gap: 7px; padding: 2px 0; cursor: pointer; user-select: none; }
.lyr input { accent-color: var(--accent); }
#fitbtn { margin-top: 4px; width: 100%; background: #17231c; color: var(--txt); border: 1px solid var(--line); border-radius: 7px; padding: 5px; cursor: pointer; }
#fitbtn:hover { border-color: var(--accent); }
#sidebtns { display: flex; align-items: center; gap: 4px; margin-bottom: 8px; }
#sidebtns .lbl { color: var(--muted); font-size: 11px; margin-right: auto; }
.sidebtn { background: #17231c; color: var(--muted); border: 1px solid var(--line); border-radius: 6px; padding: 2px 8px; cursor: pointer; font-size: 11px; }
.sidebtn.on { background: var(--accent); color: #111; border-color: var(--accent); font-weight: 600; }
.lyr-name { cursor: pointer; }
.lyr-name:hover { color: var(--accent); text-decoration: underline; }
/* the copper layer currently raised to the front (click its name again to reset) */
.lyr.raised .lyr-name { color: var(--accent); font-weight: 600; }
.lyr.raised .lyr-name::after { content: " \2191"; }
.netrow { padding: 3px 7px; border-radius: 5px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
.netrow:hover { background: #1c2a22; color: var(--hl); }
.netrow.sel { background: var(--accent); color: #06232a; font-weight: 600; }
.netrow.sel:hover { color: #06232a; }
#stat { bottom: 12px; left: 12px; padding: 6px 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
#netchip { bottom: 12px; left: 50%; transform: translateX(-50%); padding: 6px 13px; display: none; }
#netchip b { color: var(--accent); text-transform: uppercase; font-size: 10px; letter-spacing: .06em; margin-right: 4px; }
.muted { color: var(--muted); }
/* ── component info card ── */
#card { position: fixed; z-index: 9999; display: none; min-width: 180px; max-width: 260px; padding: 10px 12px; pointer-events: none; }
.ci-h { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 6px; padding-bottom: 6px; border-bottom: 1px solid var(--line); }
.ci-ref { font-weight: 700; color: var(--accent); font-size: 14px; }
.ci-val { color: var(--txt); }
.ci-row { display: flex; justify-content: space-between; gap: 10px; padding: 1px 0; }
.ci-k { color: var(--muted); }
.ci-v { text-align: right; word-break: break-word; }
.ci-enrich { margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--line); }
.pills { display: flex; flex-wrap: wrap; gap: 5px; }
.pill { background: #17231c; border: 1px solid var(--line); border-radius: 999px; padding: 1px 8px; font-size: 11px; }
.pill.ok { color: #57d97e; border-color: #2c5a3a; }
.pill.no { color: #e08a8a; border-color: #5a2c2c; }
.pill.tier { color: #f2d060; }
.pill.pop { color: #cba6ff; }
.ci-wiki { display: inline-block; margin-top: 6px; color: #6bb3ff; text-decoration: none; }
#side h1, .sec-h { font-family: 'Familjen Grotesk', -apple-system, sans-serif; }
/* ── canonical Adom 44px three-zone header (brand · subject · actions) ── */
header.app { display: none; position: fixed; top: 0; left: 0; right: 0; height: 44px; z-index: 50;
grid-template-columns: 1fr auto 1fr; align-items: center; gap: 12px; padding: 0 14px;
background: var(--panel); border-bottom: 1px solid var(--edge); }
body.chrome header.app { display: grid; }
body.chrome #svgbox { top: 44px; }
body.chrome #side { top: 56px; }
header.app .brand { display: flex; align-items: center; gap: 8px; min-width: 0; position: relative; cursor: default; padding-right: 14px; border-right: 1px solid var(--edge); }
header.app .brand svg { width: 16px; height: 16px; color: var(--ink); flex: 0 0 16px; }
header.app .app-name { font-family: 'Familjen Grotesk', -apple-system, sans-serif; font-weight: 600; font-size: 13px; color: var(--acc); white-space: nowrap; }
header.app .app-sub { color: var(--mut); font-size: 12px; white-space: nowrap; }
header.app .app-sub::before { content: "·"; margin: 0 6px; color: var(--mut); }
header.app .subject { justify-self: center; display: flex; align-items: baseline; gap: 10px; min-width: 0; }
header.app .subject-name { font-weight: 600; font-size: 13px; max-width: 44vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
header.app .subject-meta { font-family: 'JetBrains Mono', ui-monospace, monospace; font-size: 11px; color: var(--mut); white-space: nowrap; }
header.app .actions { justify-self: end; display: flex; align-items: center; gap: 8px; }
header.app .actions button { background: var(--elevated); color: var(--ink); border: 1px solid var(--edge); border-radius: 7px; width: 30px; height: 26px; cursor: pointer; display: grid; place-items: center; }
header.app .actions button:hover { border-color: var(--acc); }
header.app .actions button svg { width: 15px; height: 15px; }
header.app .appinfo { position: absolute; top: calc(100% + 6px); left: 0; min-width: 220px; background: var(--overlay); border: 1px solid var(--edge);
border-radius: 10px; box-shadow: 0 12px 34px #000a; padding: 12px 14px; font-size: 12px; opacity: 0; visibility: hidden; transition: opacity .18s ease; pointer-events: none; z-index: 60; }
header.app .brand:hover .appinfo { opacity: 1; visibility: visible; pointer-events: auto; transition-delay: .6s; }
header.app .appinfo .ai-row { display: flex; justify-content: space-between; gap: 16px; padding: 2px 0; }
header.app .appinfo .ai-k { color: var(--mut); }
header.app .appinfo .ai-v { font-family: 'JetBrains Mono', ui-monospace, monospace; }
header.app .appinfo a { color: var(--acc); text-decoration: none; }
header.app .appinfo a:hover { text-decoration: underline; }
/* ── toasts: transient feedback, bottom-center; also the AI's live channel ── */
#toasts { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 3000; display: flex; flex-direction: column-reverse; gap: 8px; align-items: center; pointer-events: none; }
.toast { pointer-events: auto; min-width: 200px; max-width: min(420px,90vw); display: flex; align-items: center; gap: 10px; padding: 10px 13px; border-radius: 10px;
background: var(--overlay); border: 1px solid var(--edge); color: var(--ink); font-size: 13px; box-shadow: 0 10px 30px #000a; cursor: pointer; animation: toastin .18s ease; }
@keyframes toastin { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.toast.out { opacity: 0; transform: translateY(8px); transition: opacity .2s ease, transform .2s ease; }
.toast .ti { flex: 0 0 16px; width: 16px; height: 16px; }
.toast.success .ti { color: var(--green); } .toast.success { border-color: #2c5a3a; }
.toast.error .ti { color: var(--red); } .toast.error { border-color: #5a2c2c; }
.toast.info .ti { color: var(--acc); }
.toast .tmsg { flex: 1; min-width: 0; }
/* ── tooltips: body-appended fixed div, 600ms reveal, clamped both axes ── */
#tt { position: fixed; z-index: 99999; max-width: 260px; background: var(--overlay); color: var(--ink); border: 1px solid var(--edge);
border-radius: 7px; padding: 6px 9px; font-size: 12px; line-height: 1.4; box-shadow: 0 8px 22px #000b; pointer-events: none;
opacity: 0; transform: translateY(3px); transition: opacity .18s ease, transform .18s ease; text-transform: none; }
#tt.show { opacity: 1; transform: none; }
@media (hover: none), (pointer: coarse) { #tt { display: none; } }
</style>
</head>
<body>
<header class="app">
<div class="brand">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="1.5"/><path d="M7 8h4v4H7zM14 14h3M15.5 8v4"/><circle cx="17" cy="8" r="1.1"/></svg>
<span class="app-name">PCB Viewer</span><span class="app-sub">interactive layout</span>
<div class="appinfo">
<div class="ai-row"><span class="ai-k">version</span><span class="ai-v" id="ai-ver">-</span></div>
<div class="ai-row"><span class="ai-k">engine</span><span class="ai-v">KiCad · Altium · Fusion</span></div>
<div class="ai-row"><span class="ai-k">source</span><a href="https://wiki.adom.inc/adom/adom-layout-viewer" target="_blank" rel="noopener">wiki page ↗</a></div>
</div>
</div>
<div class="subject"><span class="subject-name" id="hdr-name"></span><span class="subject-meta" id="hdr-meta"></span></div>
<div class="actions">
<button id="hdr-fit" data-tip="Fit the whole board to the view"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg></button>
</div>
</header>
<div id="svgbox"><div class="empty">loading…</div></div>
<div id="side" class="panel">
<div class="sec"><div class="sec-h">Layers</div><div id="layers"></div></div>
<div class="sec"><div class="sec-h">Nets</div><div id="netlist"></div></div>
<button id="fitbtn" data-tip="Zoom to fit the entire board">Fit</button>
</div>
<div id="stat" class="panel"></div>
<div id="netchip" class="panel"></div>
<div id="card" class="panel"></div>
<div id="toasts"></div>
<script src="viewer.js"></script>
</body>
</html>