app
Component Widgets
Public Made by Adomby adom
Reusable component widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and lightweight four-level LOD cache listings.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
/*!
* <adom-heatsink-via> — the exposed-pad thermal-via widget.
* Only renders content when the part actually HAS an exposed/thermal pad; otherwise
* it shows an empty state ("no exposed pad — heat-sink vias not applicable").
* Draws the EP copper + a standard thermal via array (annular ring + drill), with
* the via count / drill / pitch, so a page author can show the recommended
* heat-sinking pattern for that chip.
*
* <script src="heatsink-via-widget.js"></script>
* <adom-heatsink-via src="pads.json"></adom-heatsink-via>
* el.data = { mpn, package, pads:[{number,x_mm,y_mm}] } // detects the EP
* el.data = { mpn, package, ep:{w_mm,h_mm,number?}, via?:{drill_mm,pitch_mm} } // explicit
*
* Shares the adom-lbr cross-highlight bus: highlights the EP when its pin is hovered
* elsewhere, and posts on EP hover.
*/
(() => {
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const isCenterName = p => /^(EP|EPAD|PAD|TAB|THERMAL|E[-_]?PAD|EXP|EXPPAD)$/i.test(String(p.name || p.number || '').trim());
// Work out the exposed pad from whatever data we were given.
function resolveEP(d) {
if (d.ep && (d.ep.w_mm || d.ep.h_mm)) return { w: d.ep.w_mm || d.ep.h_mm, h: d.ep.h_mm || d.ep.w_mm, number: d.ep.number };
const m = /EP([\d.]+)x([\d.]+)/i.exec(d.package || '');
let epNum;
const pads = Array.isArray(d.pads) ? d.pads.filter(p => typeof p.x_mm === 'number') : [];
if (pads.length) {
const xs = pads.map(p => p.x_mm), ys = pads.map(p => p.y_mm);
const minx = Math.min(...xs), maxx = Math.max(...xs), miny = Math.min(...ys), maxy = Math.max(...ys);
const span = Math.max(maxx - minx, maxy - miny) || 1, em = 0.2 * span;
const cenP = pads.find(p => isCenterName(p) || Math.min(Math.abs(p.x_mm - minx), Math.abs(p.x_mm - maxx), Math.abs(p.y_mm - miny), Math.abs(p.y_mm - maxy)) > em);
if (cenP) epNum = cenP.number;
if (m) return { w: +m[1], h: +m[2], number: epNum, bodyW: maxx - minx, bodyH: maxy - miny };
if (cenP) { const bw = maxx - minx, bh = maxy - miny; return { w: bw * 0.55, h: bh * 0.55, number: epNum, bodyW: bw, bodyH: bh, estimated: true }; }
}
if (m) return { w: +m[1], h: +m[2] };
return null;
}
class AdomHeatsinkVia extends HTMLElement {
set data(d) { this._data = d; if (this.shadowRoot) this._render(); }
get data() { return this._data; }
async connectedCallback() {
// A host page that pins its color-scheme (the wiki is dark-only) decides the theme; inline, prefers-color-scheme
// would follow the reader's OS instead (a frame inherits the embedding page's scheme).
if (!this.hasAttribute('theme')) { const cs = getComputedStyle(document.documentElement).colorScheme.trim(); if (/^(only )?(dark|light)( only)?$/.test(cs)) this.setAttribute('theme', cs.replace(/only|\s/g, '')); }
if (this._init) return; this._init = true;
this.attachShadow({ mode: 'open' });
this._busInit();
if (!this._data) {
const inline = this.querySelector('script[type="application/json"]');
if (inline) { try { this._data = JSON.parse(inline.textContent); } catch (e) { /* ignore */ } }
const src = this.getAttribute('src');
if (!this._data && src) { try { this._data = await (await fetch(src)).json(); } catch (e) { this._err = 'could not load ' + src; } }
}
this._render();
}
_busInit() {
this._self = 'h' + Math.random().toString(36).slice(2);
this._bus = ('BroadcastChannel' in window) ? new BroadcastChannel('lbr-hover') : null;
const on = e => { const x = e.data || {}; if (x.type === 'lbr-hover' && x.src !== this._self) this._hi(x.pin); };
if (this._bus) this._bus.onmessage = on;
this._winmsg = on; window.addEventListener('message', on);
}
disconnectedCallback() { try { this._bus && this._bus.close(); } catch (_) { /*noop*/ } if (this._winmsg) window.removeEventListener('message', this._winmsg); }
_hi(n) { const g = this.shadowRoot && this.shadowRoot.querySelector('.epwrap'); if (g) g.classList.toggle('hot', n != null && this._epNum != null && String(n) === String(this._epNum)); }
_post(n) {
const m = { type: 'lbr-hover', pin: n == null ? null : String(n), mpn: (this._data && this._data.mpn) || '', src: this._self };
try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }
// parent relay only from the wiki's sandboxed widget frame; inline, BroadcastChannel already reaches every widget
try { if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }
}
_render() {
const d = this._data || {}, root = this.shadowRoot;
const ep = d.pads || d.ep || d.package ? resolveEP(d) : null;
if (this._err) return void (root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="empty">${esc(this._err)}</div></div>`);
if (!ep) {
this._epNum = null;
root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="hd"><span class="mpn">${esc(d.mpn || '')}</span>${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}</div>
<div class="empty"><b>No exposed pad</b> on this package — heat-sink vias aren't applicable.</div></div>`;
return;
}
this._epNum = ep.number;
const via = { drill: (d.via && d.via.drill_mm) || 0.3, pitch: (d.via && d.via.pitch_mm) || Math.max(0.8, Math.min(1.2, Math.min(ep.w, ep.h) / 4)) };
// via grid centred on the EP
const margin = Math.max(0.35, via.drill * 1.2);
const nx = Math.max(1, Math.floor((ep.w - 2 * margin) / via.pitch) + 1);
const ny = Math.max(1, Math.floor((ep.h - 2 * margin) / via.pitch) + 1);
const count = nx * ny;
root.innerHTML = `<style>${this._css()}</style>
<div class="card">
<div class="hd"><span class="mpn">${esc(d.mpn || '')}</span>${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">exposed pad</span></div>
<div class="body">
${this._svg(ep, via, nx, ny)}
<div class="info">
<div class="kv"><span>Exposed pad</span><b>${ep.w.toFixed(2)} × ${ep.h.toFixed(2)} mm${ep.estimated ? ' *' : ''}</b></div>
<div class="kv"><span>Thermal vias</span><b>${count} (${nx}×${ny})</b></div>
<div class="kv"><span>Drill · pitch</span><b>Ø${via.drill} · ${via.pitch.toFixed(2)} mm</b></div>
<p class="note">A grid of plated vias under the exposed pad carries heat from the die into the board's inner/bottom copper (usually a ground plane). Tent or plug them to stop solder wicking during reflow.${ep.estimated ? '<br><span class="star">* EP size estimated from the footprint; use the datasheet land pattern for production.</span>' : ''}</p>
</div>
</div>
</div>`;
const w = root.querySelector('.epwrap');
if (w) { w.onmouseenter = () => { this._hi(ep.number); this._post(ep.number); }; w.onmouseleave = () => { this._hi(null); this._post(null); }; }
}
_svg(ep, via, nx, ny) {
const S = 300, pad = 34;
const bodyW = ep.bodyW || ep.w * 1.5, bodyH = ep.bodyH || ep.h * 1.5;
const span = Math.max(bodyW, bodyH) || 1, sc = (S - 2 * pad) / span, c = S / 2;
const X = mm => c + mm * sc, Y = mm => c + mm * sc;
const epW = ep.w * sc, epH = ep.h * sc, bW = bodyW * sc, bH = bodyH * sc;
let s = `<svg viewBox="0 0 ${S} ${S}" class="hv">`;
s += `<rect class="courtyard" x="${c - bW / 2}" y="${c - bH / 2}" width="${bW}" height="${bH}" rx="6"/>`;
s += `<g class="epwrap"><rect class="ep" x="${c - epW / 2}" y="${c - epH / 2}" width="${epW}" height="${epH}" rx="2"/>`;
const x0 = -(nx - 1) * via.pitch / 2, y0 = -(ny - 1) * via.pitch / 2;
const vr = Math.max(3, via.drill * sc * 0.9), dr = Math.max(1.4, via.drill * sc * 0.5);
for (let i = 0; i < nx; i++) for (let j = 0; j < ny; j++) {
const vx = X(x0 + i * via.pitch), vy = Y(y0 + j * via.pitch);
s += `<circle class="viaring" cx="${vx}" cy="${vy}" r="${vr}"/><circle class="viadrill" cx="${vx}" cy="${vy}" r="${dr}"/>`;
}
s += `</g>`;
s += `<text x="${c}" y="${c - bH / 2 - 8}" text-anchor="middle" class="lbl">exposed pad · ${nx}×${ny} thermal vias</text>`;
return s + `</svg>`;
}
_css() {
return `
:host{display:block;--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;--copper:#e0913a;--drill:#0a0e14}
@media (prefers-color-scheme:light){:host{--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--copper:#c47a24;--drill:#fff}}
:host([theme="dark"]){--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;--copper:#e0913a;--drill:#0a0e14}
:host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--copper:#c47a24;--drill:#fff}
*{box-sizing:border-box}
.card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);font:14px -apple-system,'Segoe UI',Roboto,Arial,sans-serif}
.hd{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}
.hd .mpn{font:700 16px ui-monospace,'SF Mono',Menlo,monospace} .hd .pkg{font:600 11px ui-monospace,monospace;color:var(--accent);background:color-mix(in srgb,var(--accent) 12%,transparent);border:1px solid color-mix(in srgb,var(--accent) 30%,transparent);padding:2px 8px;border-radius:6px}
.hd .count{margin-left:auto;font:11px ui-monospace,monospace;color:var(--copper);text-transform:uppercase;letter-spacing:.08em}
.body{display:grid;grid-template-columns:minmax(220px,1fr) minmax(200px,1fr);gap:18px;align-items:center}
@media(max-width:560px){.body{grid-template-columns:1fr}}
svg.hv{width:100%;height:auto;max-height:300px;display:block;background:var(--panel);border:1px solid var(--line);border-radius:10px}
.courtyard{fill:none;stroke:var(--line2);stroke-width:1.2;stroke-dasharray:3 3}
.ep{fill:var(--copper);fill-opacity:.22;stroke:var(--copper);stroke-width:1.4}
.viaring{fill:var(--copper);fill-opacity:.9} .viadrill{fill:var(--drill)}
.epwrap{cursor:default} .epwrap.hot .ep{fill-opacity:.4;stroke-width:2.4;filter:drop-shadow(0 0 5px var(--copper))}
.epwrap.hot .viaring{fill-opacity:1}
.lbl{fill:var(--dim);font:9px ui-monospace,monospace}
.info .kv{display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);font-size:13px}
.info .kv span{color:var(--dim2)} .info .kv b{font-family:ui-monospace,monospace}
.info .note{font-size:12px;color:var(--dim);margin:10px 0 0;line-height:1.5} .info .star{color:var(--dim2);font-size:11px}
.empty{color:var(--dim);padding:22px;text-align:center;font-size:13.5px} .empty b{color:var(--text)}`;
}
}
if (!customElements.get('adom-heatsink-via')) customElements.define('adom-heatsink-via', AdomHeatsinkVia);
})();