← Commit history
README.md+54−1
@@ -1,3 +1,56 @@ # Component Widgets -One home for the reusable, readme-instantiable component/molecule widgets (pinout, heat-sink via, chip thumbnail, molecule) — self-contained web components on the shared adom-lbr hover bus.+One home for the reusable widgets you can drop into any component or molecule **readme HTML**. Each is a self-contained web component — no build, no dependencies — instantiated with one tag.++**Shared contract:** feed each widget three ways — `src="…json"`, an inline `<script type="application/json">`, or `el.data = {…}` from JS. Shadow-DOM isolated, light + dark. They all speak the **adom-lbr cross-highlight bus** (`BroadcastChannel('lbr-hover')` + parent relay), so any widgets on a page light up together on hover — pinout, footprint, 3D, all keyed on pin number.++Assets (load once): [`pinout-widget.js`](files/pinout-widget.js) · [`heatsink-via-widget.js`](files/heatsink-via-widget.js) · [`chip-thumbnail-widget.js`](files/chip-thumbnail-widget.js) · [`molecule-widget.js`](files/molecule-widget.js)++## The widgets++### `<adom-pinout>` — pinout diagram+Datasheet-style chip diagram: pins on the sides, colored by type, exposed pad in the center.+```html+<adom-pinout src="stm32.json"></adom-pinout>+<!-- { mpn, package, pins:[{number,name,type?,xy?}] } · degrades rich xy → inferred quad/dual -->+```++### `<adom-heatsink-via>` — exposed-pad thermal vias · **new**+The recommended thermal-via array under the exposed pad — **only renders when the part has an exposed pad**, otherwise an empty state.+```html+<adom-heatsink-via><script type="application/json">+{ "mpn":"MCP23017", "package":"QFN-28-1EP…EP4.25x4.25mm" }   <!-- EP from name / pads / explicit ep:{} -->+</script></adom-heatsink-via>+```++### `<adom-chip-thumbnail>` — rendered part image · **new**+```html+<adom-chip-thumbnail src="thumb.png" mpn="74HC00" package="SOIC-14"></adom-chip-thumbnail>+```++### `<adom-molecule>` — ganged molecule · **new**+A molecule as its ganged part set + how they connect. Hover a net to light its parts; hover a part to light its nets.+```html+<adom-molecule><script type="application/json">+{ "name":"…", "components":[{ref,mpn,value?,package?,type?}], "nets":[{name,pins:[{ref,pin}]}] }+</script></adom-molecule>+```++### Symbol · Footprint · 3D — **adom-lbr**+Provided by **adom-lbr**'s viewers (canonical adom-lbr JSON, footprint layer stackup KiCad+Altium, first-class pin table, per-lead 3D highlight). Embed the adom-lbr slot/viewer — don't reimplement. They already share the `lbr-hover` bus.++### Datasheet — native viewer+Push the part's PDF; the native viewer renders it inline with pop-out. No widget to build.++---++## Live (dogfood)++<div>+<script src="files/pinout-widget.js"></script>+<script src="files/heatsink-via-widget.js"></script>+<adom-pinout theme="dark"><script type="application/json">{"mpn":"NE555","package":"DIP-8","pins":[{"number":1,"name":"GND"},{"number":2,"name":"TRIG"},{"number":3,"name":"OUT"},{"number":4,"name":"RESET"},{"number":5,"name":"CTRL"},{"number":6,"name":"THRES"},{"number":7,"name":"DISCH"},{"number":8,"name":"VCC"}]}</script></adom-pinout>+<adom-heatsink-via theme="dark"><script type="application/json">{"mpn":"MCP23017","package":"QFN-28-1EP_6x6mm_P0.65mm_EP4.25x4.25mm","ep":{"w_mm":4.25,"h_mm":4.25,"number":29}}</script></adom-heatsink-via>+</div>++*If the block above renders as two live widgets, readme-instantiation works end-to-end. If not, the readme sandbox's CSP needs to allow page-asset scripts (a wiki-side toggle).*
files/chip-thumbnail-widget.jsadded+38
@@ -0,0 +1,38 @@+/*!+ * <adom-chip-thumbnail> — a small rendered image of the actual part.+ *   <adom-chip-thumbnail src="thumb.png" mpn="74HC00" package="SOIC-14"></adom-chip-thumbnail>+ *   el.data = { src, mpn, package }+ * `src` is a rendered PNG (in production: glb2png / the chip-thumbnailer off the part's GLB).+ * Self-contained, theme-aware.+ */+(() => {+  const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));+  const CSS = `+    :host{display:inline-block;--bg:#0d1420;--line:#26324a;--text:#dbe2ec;--dim:#8b97ac;--accent:#00e6dc}+    @media (prefers-color-scheme:light){:host{--bg:#fff;--line:#dbe2ee;--text:#1a2230;--dim:#5a6678;--accent:#0d8f88}}+    :host([theme="dark"]){--bg:#0d1420;--line:#26324a;--text:#dbe2ec;--dim:#8b97ac;--accent:#00e6dc}+    :host([theme="light"]){--bg:#fff;--line:#dbe2ee;--text:#1a2230;--dim:#5a6678;--accent:#0d8f88}+    *{box-sizing:border-box}+    .card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:12px;width:200px;text-align:center;color:var(--text);font:14px -apple-system,'Segoe UI',Roboto,sans-serif}+    img{width:100%;height:auto;border-radius:8px;display:block}+    .ph{aspect-ratio:1;display:flex;align-items:center;justify-content:center;color:var(--dim);border:1px dashed var(--line);border-radius:8px;font:12px ui-monospace,monospace}+    figcaption{margin-top:9px;font:600 13px ui-monospace,'SF Mono',Menlo,monospace}+    figcaption span{color:var(--accent);font-weight:400;font-size:11px}`;+  class AdomChipThumbnail extends HTMLElement {+    static get observedAttributes() { return ['src', 'mpn', 'package']; }+    set data(d) { this._data = d; if (this.shadowRoot) this._render(); }+    connectedCallback() { if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' }); this._render(); }+    attributeChangedCallback() { if (this.shadowRoot) this._render(); }+    _render() {+      const d = this._data || {};+      const src = d.src || this.getAttribute('src') || '';+      const mpn = d.mpn || this.getAttribute('mpn') || '';+      const pkg = d.package || this.getAttribute('package') || '';+      this.shadowRoot.innerHTML = `<style>${CSS}</style><figure class="card" style="margin:0">+        ${src ? `<img src="${esc(src)}" alt="${esc(mpn)}" onerror="this.outerHTML='<div class=ph>no render</div>'">` : `<div class="ph">no render</div>`}+        ${(mpn || pkg) ? `<figcaption>${esc(mpn)}${pkg ? ` · <span>${esc(pkg)}</span>` : ''}</figcaption>` : ''}+      </figure>`;+    }+  }+  if (!customElements.get('adom-chip-thumbnail')) customElements.define('adom-chip-thumbnail', AdomChipThumbnail);+})();
files/heatsink-via-widget.jsadded+152
@@ -0,0 +1,152 @@+/*!+ * <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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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() {+      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*/ }+      try { if (window.parent && window.parent !== window) 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);+})();
files/molecule-widget.jsadded+113
@@ -0,0 +1,113 @@+/*!+ * <adom-molecule> — the ganged-component widget for a molecule (a reusable sub-circuit).+ * Shows the molecule's parts as a ganged set + how they connect (nets). Hover a net to+ * light every part on it; hover a part to light its nets — the "ganged" relationship.+ *+ *   <adom-molecule src="molecule.json"></adom-molecule>+ *   el.data = {+ *     name, mpn?,+ *     components: [ { ref, mpn, value?, package?, type? } ],   // type: mcu|cap|res|xtal|ic|conn|diode|other+ *     nets:       [ { name, pins: [ {ref, pin} ] } ]+ *   }+ * Self-contained, theme-aware, shares the adom-lbr lbr-hover bus (posts a part's ref).+ */+(() => {+  const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));+  const TC = { mcu: '#b57cff', ic: '#4c9dff', cap: '#00d3c8', res: '#ffb454', xtal: '#4c9dff', diode: '#ff5c5c', conn: '#e0913a', other: '#8b97ac' };+  const TLAB = { mcu: 'MCU', ic: 'IC', cap: 'cap', res: 'res', xtal: 'xtal', diode: 'diode', conn: 'conn', other: '' };+  function inferType(c) {+    if (c.type) return c.type;+    const r = String(c.ref || '').toUpperCase();+    if (r[0] === 'U') return /STM|MCU|ESP|ATMEGA|RP2/i.test(c.mpn || '') ? 'mcu' : 'ic';+    if (r[0] === 'C') return 'cap'; if (r[0] === 'R') return 'res'; if (r[0] === 'Y' || r[0] === 'X') return 'xtal';+    if (r[0] === 'D') return 'diode'; if (r[0] === 'J' || r[0] === 'P') return 'conn';+    return 'other';+  }++  class AdomMolecule extends HTMLElement {+    set data(d) { this._data = d; if (this.shadowRoot) this._render(); }+    get data() { return this._data; }+    async connectedCallback() {+      if (this._init) return; this._init = true;+      this.attachShadow({ mode: 'open' });+      this._self = 'm' + Math.random().toString(36).slice(2);+      this._bus = ('BroadcastChannel' in window) ? new BroadcastChannel('lbr-hover') : null;+      if (!this._data) {+        const inl = this.querySelector('script[type="application/json"]');+        if (inl) { try { this._data = JSON.parse(inl.textContent); } catch (e) { /* */ } }+        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();+    }+    disconnectedCallback() { try { this._bus && this._bus.close(); } catch (_) { /* */ } }++    _render() {+      const d = this._data, root = this.shadowRoot;+      if (this._err || !d || !Array.isArray(d.components)) {+        root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="empty">${esc(this._err || 'no molecule data')}</div></div>`;+        return;+      }+      const comps = d.components.map(c => ({ ...c, type: inferType(c) }));+      const nets = Array.isArray(d.nets) ? d.nets : [];+      const netOf = new Map(); // ref -> [net names]+      nets.forEach(n => (n.pins || []).forEach(p => { const a = netOf.get(p.ref) || []; if (!a.includes(n.name)) a.push(n.name); netOf.set(p.ref, a); }));+      root.innerHTML = `<style>${this._css()}</style>+        <div class="card">+          <div class="hd"><span class="mpn">${esc(d.name || d.mpn || 'molecule')}</span><span class="count">${comps.length} parts · ${nets.length} nets</span></div>+          <div class="readout" data-readout>hover a part or net&nbsp;→</div>+          <div class="gang">${comps.map(c => `+            <div class="part" data-ref="${esc(c.ref)}" style="--c:${TC[c.type] || TC.other}">+              <span class="ref">${esc(c.ref)}</span>+              <span class="val">${esc(c.value || c.mpn || '')}</span>+              <span class="meta">${esc(c.package || '')}${TLAB[c.type] ? ' · ' + TLAB[c.type] : ''}</span>+            </div>`).join('')}</div>+          <div class="nets">${nets.map(n => `<div class="net" data-net="${esc(n.name)}" data-refs="${esc((n.pins || []).map(p => p.ref).join(','))}">+            <span class="nn">${esc(n.name)}</span><span class="nc">${(n.pins || []).length} pins</span>+            <span class="nr">${esc([...new Set((n.pins || []).map(p => p.ref))].join(' '))}</span></div>`).join('')}</div>+        </div>`;+      this._wire(netOf);+    }++    _wire(netOf) {+      const root = this.shadowRoot, ro = root.querySelector('[data-readout]');+      const parts = [...root.querySelectorAll('.part')], netEls = [...root.querySelectorAll('.net')];+      const clear = () => { parts.forEach(p => p.classList.remove('hot', 'dim')); netEls.forEach(n => n.classList.remove('hot', 'dim')); ro.innerHTML = 'hover a part or net&nbsp;→'; this._post(null); };+      const litParts = refs => { const set = new Set(refs); parts.forEach(p => { p.classList.toggle('hot', set.has(p.dataset.ref)); p.classList.toggle('dim', !set.has(p.dataset.ref)); }); };+      const litNets = names => { const set = new Set(names); netEls.forEach(n => { n.classList.toggle('hot', set.has(n.dataset.net)); n.classList.toggle('dim', !set.has(n.dataset.net)); }); };+      netEls.forEach(n => {+        n.onmouseenter = () => { const refs = (n.dataset.refs || '').split(',').filter(Boolean); litParts(refs); n.classList.add('hot'); netEls.forEach(x => x !== n && x.classList.add('dim')); ro.innerHTML = `<b>${esc(n.dataset.net)}</b> · ${refs.length} pins on ${new Set(refs).size} parts`; };+        n.onmouseleave = clear;+      });+      parts.forEach(p => {+        p.onmouseenter = () => { const names = netOf.get(p.dataset.ref) || []; litNets(names); p.classList.add('hot'); parts.forEach(x => x !== p && x.classList.add('dim')); ro.innerHTML = `<b>${esc(p.dataset.ref)}</b> · on ${names.length ? names.join(', ') : 'no nets'}`; this._post(p.dataset.ref); };+        p.onmouseleave = clear;+      });+    }+    _post(ref) { const m = { type: 'lbr-hover', ref: ref, mpn: (this._data && this._data.name) || '', src: this._self }; try { this._bus && this._bus.postMessage(m); } catch (_) { /* */ } try { if (window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /* */ } }++    _css() {+      return `+      :host{display:block;--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc}+      @media (prefers-color-scheme:light){:host{--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88}}+      :host([theme="dark"]){--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc}+      :host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88}+      *{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:8px}+      .hd .mpn{font:700 16px ui-monospace,'SF Mono',Menlo,monospace} .hd .count{margin-left:auto;font:12px ui-monospace,monospace;color:var(--dim2)}+      .readout{height:22px;display:flex;align-items:center;font:600 12.5px ui-monospace,monospace;color:var(--dim);margin-bottom:10px}+      .readout b{color:var(--accent)}+      .gang{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}+      .part{border:1px solid var(--line2);border-left:3px solid var(--c);border-radius:9px;padding:7px 11px;min-width:88px;background:var(--panel);transition:opacity .1s}+      .part .ref{font:700 12px ui-monospace,monospace;color:var(--c)} .part .val{display:block;font:600 12.5px ui-monospace,monospace;margin-top:1px} .part .meta{display:block;font-size:10.5px;color:var(--dim2);margin-top:1px}+      .part.hot{box-shadow:0 0 0 1px var(--c);background:color-mix(in srgb,var(--c) 10%,var(--panel))} .part.dim{opacity:.32}+      .nets{border-top:1px solid var(--line);padding-top:8px}+      .net{display:flex;align-items:center;gap:10px;padding:4px 8px;border-radius:6px;font:12px ui-monospace,monospace;cursor:default}+      .net .nn{color:var(--text);font-weight:700;min-width:70px} .net .nc{color:var(--dim2);font-size:11px} .net .nr{color:var(--dim);margin-left:auto;font-size:11px;text-align:right}+      .net.hot{background:color-mix(in srgb,var(--accent) 12%,transparent)} .net.hot .nn{color:var(--accent)} .net.dim{opacity:.34}+      .empty{color:var(--dim2);padding:26px;text-align:center;font-family:ui-monospace,monospace}`;+    }+  }+  if (!customElements.get('adom-molecule')) customElements.define('adom-molecule', AdomMolecule);+})();
files/pinout-stm32f103.jsonadded+1
@@ -0,0 +1 @@+{"mpn": "STM32F103C8T6", "package": "LQFP-48", "pins": [{"number": 1, "name": "VBAT", "xy": [-2.75, 4.25], "type": "power"}, {"number": 2, "name": "PC13-TAMPER-RTC", "xy": [-2.25, 4.25], "type": "io"}, {"number": 3, "name": "PC14-OSC32_IN", "xy": [-1.75, 4.25], "type": "clock"}, {"number": 4, "name": "PC15-OSC32_OUT", "xy": [-1.25, 4.25], "type": "clock"}, {"number": 5, "name": "PD0-OSC_IN", "xy": [-0.75, 4.25], "type": "clock"}, {"number": 6, "name": "PD1-OSC_OUT", "xy": [-0.25, 4.25], "type": "clock"}, {"number": 7, "name": "NRST", "xy": [0.25, 4.25], "type": "ctrl"}, {"number": 8, "name": "VSSA", "xy": [0.75, 4.25], "type": "power"}, {"number": 9, "name": "VDDA", "xy": [1.25, 4.25], "type": "power"}, {"number": 10, "name": "PA0-WKUP", "xy": [1.75, 4.25], "type": "io"}, {"number": 11, "name": "PA1", "xy": [2.25, 4.25], "type": "io"}, {"number": 12, "name": "PA2", "xy": [2.75, 4.25], "type": "io"}, {"number": 13, "name": "PA3", "xy": [4.25, 2.75], "type": "io"}, {"number": 14, "name": "PA4", "xy": [4.25, 2.25], "type": "io"}, {"number": 15, "name": "PA5", "xy": [4.25, 1.75], "type": "io"}, {"number": 16, "name": "PA6", "xy": [4.25, 1.25], "type": "io"}, {"number": 17, "name": "PA7", "xy": [4.25, 0.75], "type": "io"}, {"number": 18, "name": "PB0", "xy": [4.25, 0.25], "type": "io"}, {"number": 19, "name": "PB1", "xy": [4.25, -0.25], "type": "io"}, {"number": 20, "name": "PB2", "xy": [4.25, -0.75], "type": "io"}, {"number": 21, "name": "PB10", "xy": [4.25, -1.25], "type": "io"}, {"number": 22, "name": "PB11", "xy": [4.25, -1.75], "type": "io"}, {"number": 23, "name": "VSS_1", "xy": [4.25, -2.25], "type": "power"}, {"number": 24, "name": "VDD_1", "xy": [4.25, -2.75], "type": "power"}, {"number": 25, "name": "PB12", "xy": [2.75, -4.25], "type": "io"}, {"number": 26, "name": "PB13", "xy": [2.25, -4.25], "type": "io"}, {"number": 27, "name": "PB14", "xy": [1.75, -4.25], "type": "io"}, {"number": 28, "name": "PB15", "xy": [1.25, -4.25], "type": "io"}, {"number": 29, "name": "PA8", "xy": [0.75, -4.25], "type": "io"}, {"number": 30, "name": "PA9", "xy": [0.25, -4.25], "type": "io"}, {"number": 31, "name": "PA10", "xy": [-0.25, -4.25], "type": "io"}, {"number": 32, "name": "PA11", "xy": [-0.75, -4.25], "type": "io"}, {"number": 33, "name": "PA12", "xy": [-1.25, -4.25], "type": "io"}, {"number": 34, "name": "PA13", "xy": [-1.75, -4.25], "type": "io"}, {"number": 35, "name": "VSS_2", "xy": [-2.25, -4.25], "type": "power"}, {"number": 36, "name": "VDD_2", "xy": [-2.75, -4.25], "type": "power"}, {"number": 37, "name": "PA14", "xy": [-4.25, -2.75], "type": "io"}, {"number": 38, "name": "PA15", "xy": [-4.25, -2.25], "type": "io"}, {"number": 39, "name": "PB3", "xy": [-4.25, -1.75], "type": "io"}, {"number": 40, "name": "PB4", "xy": [-4.25, -1.25], "type": "io"}, {"number": 41, "name": "PB5", "xy": [-4.25, -0.75], "type": "io"}, {"number": 42, "name": "PB6", "xy": [-4.25, -0.25], "type": "io"}, {"number": 43, "name": "PB7", "xy": [-4.25, 0.25], "type": "io"}, {"number": 44, "name": "BOOT0", "xy": [-4.25, 0.75], "type": "ctrl"}, {"number": 45, "name": "PB8", "xy": [-4.25, 1.25], "type": "io"}, {"number": 46, "name": "PB9", "xy": [-4.25, 1.75], "type": "io"}, {"number": 47, "name": "VSS_3", "xy": [-4.25, 2.25], "type": "power"}, {"number": 48, "name": "VDD_3", "xy": [-4.25, 2.75], "type": "power"}]}\ No newline at end of file
files/pinout-widget.jsadded+256
@@ -0,0 +1,256 @@+/*!+ * <adom-pinout> — a reusable, dependency-free pinout widget.+ *+ * Drop-in:+ *   <script src="pinout-widget.js"></script>+ *   <adom-pinout src="part.json"></adom-pinout>+ *   <adom-pinout><script type="application/json">{ ...pin data... }</script></adom-pinout>+ *   // or: document.querySelector('adom-pinout').data = { ... }+ *+ * Data contract (everything except pins[].name is optional):+ *   {+ *     "mpn": "STM32F103C8T6",           // label shown in the chip body+ *     "package": "LQFP-48",             // label + hints the inferred layout (quad vs dual)+ *     "pins": [+ *       { "number": 1, "name": "VBAT",  // number+name are the essentials+ *         "type": "power",              // power|io|clock|ctrl|other  (auto-inferred from name if omitted)+ *         "xy": [-2.75, 4.25] }         // mm pad centre (footprint frame, +y up) — best placement.+ *                                       // omit xy → placed by inferred quad/dual layout.+ *     ]+ *   }+ *+ * Attributes: src (fetch JSON), layout ("auto"|"quad"|"dual"), heading (override title).+ * Self-contained (Shadow DOM), themes to the host via prefers-color-scheme.+ */+(() => {+  const TYPE_COLORS = {+    power: '#ff5c5c', io: '#00d3c8', clock: '#4c9dff', ctrl: '#b57cff', control: '#b57cff', other: '#8b97ac',+  };+  const TYPE_LABEL = { power: 'power', io: 'I/O', clock: 'clock', ctrl: 'control', control: 'control', other: 'other' };++  function inferType(name) {+    const n = String(name || '').toUpperCase();+    if (n === 'NC' || n === '') return 'other';+    if (/^(VDD|VSS|VCC|GND|VBAT|VDDA|VSSA|AVDD|AVSS|VREF|VEE|VBUS|V\+|V-|AGND|DGND)/.test(n)) return 'power';+    if (/(OSC|XTAL|CLKIN|CLKOUT|\bCLK\b|SCLK|MCLK)/.test(n)) return 'clock';+    if (/^(NRST|~?RESET|RST|BOOT|EN\b|~?CE|~?CS|MODE|TEST|WAKE|SHDN|~?SHDN|PDN)/.test(n)) return 'ctrl';+    return 'io';+  }++  // Exposed / thermal pad (QFN/DFN/SON/PowerPAD) — belongs in the chip centre, not on an edge.+  const isCenterPad = p => {+    const n = String(p.name || '').trim().toUpperCase();+    return /^(EP|EPAD|PAD|TAB|THERMAL|E[-_]?PAD|EXP|EXPPAD)$/.test(n) || /EXPOSED\s*PAD|THERMAL\s*PAD/.test(n);+  };++  // Return each pin as { ...pin, x, y, side } in a S×S box. Center pads get side:'center'.+  function layout(pins, pkg, forced) {+    const S = 400, pad = 60, mid = S / 2;+    const centers = pins.filter(isCenterPad);+    const edge = pins.filter(p => !isCenterPad(p));+    let laid;+    const hasXY = edge.length > 0 && edge.every(p => Array.isArray(p.xy) && p.xy.length === 2);+    if (hasXY && forced !== 'quad' && forced !== 'dual') {+      const xs = edge.map(p => p.xy[0]), ys = edge.map(p => p.xy[1]);+      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, sc = (S - 2 * pad) / span;+      const cx = (maxx + minx) / 2, cy = (maxy + miny) / 2, off = (v, c) => mid + (v - c) * sc;+      laid = edge.map(p => {+        const x = off(p.xy[0], cx), y = S - off(p.xy[1], cy); // flip y (mm +y up → screen down)+        const side = p.xy[1] === maxy ? 'top' : p.xy[1] === miny ? 'bottom' : p.xy[0] === maxx ? 'right' : 'left';+        return { ...p, x, y, side };+      });+    } else {+      const N = edge.length, inner = pad, outer = S - pad, at = (a, b, t) => a + (b - a) * t;+      const quad = forced === 'quad' || (forced !== 'dual' && /QF|BGA|LGA|LCC|QFN|QFP/i.test(pkg || ''));+      if (quad) {+        const per = Math.ceil(N / 4);+        laid = edge.map((p, i) => {+          const s = Math.floor(i / per), t = ((i % per) + 0.5) / per;+          if (s === 0) return { ...p, x: inner, y: at(inner, outer, t), side: 'left' };+          if (s === 1) return { ...p, x: at(inner, outer, t), y: outer, side: 'bottom' };+          if (s === 2) return { ...p, x: outer, y: at(outer, inner, t), side: 'right' };+          return { ...p, x: at(outer, inner, t), y: inner, side: 'top' };+        });+      } else {+        const half = Math.ceil(N / 2); // DIP-style: left top→bottom, right bottom→top+        laid = edge.map((p, i) => i < half+          ? { ...p, x: inner, y: at(inner, outer, (i + 0.5) / half), side: 'left' }+          : { ...p, x: outer, y: at(outer, inner, (i - half + 0.5) / (N - half)), side: 'right' });+      }+    }+    centers.forEach(p => laid.push({ ...p, x: mid, y: mid, side: 'center' }));+    return laid;+  }++  const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));++  class AdomPinout extends HTMLElement {+    set data(d) { this._data = d; if (this.shadowRoot) this._render(); }+    get data() { return this._data; }++    async connectedCallback() {+      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();+    }++    _render() {+      const d = this._data;+      const root = this.shadowRoot;+      if (this._err || !d || !Array.isArray(d.pins)) {+        root.innerHTML = `<style>${this._css()}</style><div class="card"><div class="empty">${esc(this._err || 'no pin data')}</div></div>`;+        return;+      }+      const pins = d.pins.map(p => ({ ...p, type: (p.type || inferType(p.name)).toLowerCase() }));+      const laid = layout(pins, d.package, this.getAttribute('layout'));+      const typesPresent = [...new Set(pins.map(p => p.type))];+      const heading = this.getAttribute('heading') || d.mpn || '';+      root.innerHTML = `<style>${this._css()}</style>+        <div class="card">+          ${heading ? `<div class="hd"><span class="mpn">${esc(heading)}</span>${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">${pins.length} pins</span></div>` : ''}+          <div class="body">+            <div class="diagram">+              <div class="readout" data-readout>hover a pin&nbsp;→</div>+              ${this._svg(laid, d)}+              <div class="legend">${typesPresent.map(t => `<span><i style="background:${TYPE_COLORS[t] || TYPE_COLORS.other}"></i>${TYPE_LABEL[t] || t}</span>`).join('')}</div>+            </div>+            <div class="tablewrap"><table>+              <thead><tr><th>#</th><th>Name</th><th>Type</th></tr></thead>+              <tbody>${pins.slice().sort((a, b) => a.number - b.number).map(p => `+                <tr data-pin="${esc(p.number)}"><td class="num">${esc(p.number)}</td><td class="nm">${esc(p.name)}</td>+                <td><span class="tp" style="color:${TYPE_COLORS[p.type] || TYPE_COLORS.other}">${TYPE_LABEL[p.type] || p.type}</span></td></tr>`).join('')}</tbody>+            </table></div>+          </div>+        </div>`;+      this._wire(pins);+    }++    _svg(laid, d) {+      const S = 400;+      const edge = laid.filter(p => p.side !== 'center');+      const cen = laid.filter(p => p.side === 'center');+      const xs = edge.map(p => p.x), ys = edge.map(p => p.y);+      const pminX = Math.min(...xs), pmaxX = Math.max(...xs), pminY = Math.min(...ys), pmaxY = Math.max(...ys);+      const sides = new Set(edge.map(p => p.side)), hasTB = sides.has('top') || sides.has('bottom');+      const bL = pminX + 16, bR = pmaxX - 16, bT = hasTB ? pminY + 16 : pminY - 12, bB = hasTB ? pmaxY - 16 : pmaxY + 12;+      const cx = (bL + bR) / 2, cyB = (bT + bB) / 2;+      let s = `<svg viewBox="0 0 ${S} ${S}" class="chip">`;+      s += `<rect x="${bL}" y="${bT}" width="${bR - bL}" height="${bB - bT}" rx="10" class="pkgbody"/>`;+      s += `<circle cx="${bL + 15}" cy="${bT + 15}" r="4" class="pin1"/>`;+      const lY = cen.length ? bT + 32 : cyB - 3;+      s += `<text x="${cx}" y="${lY}" text-anchor="middle" class="lbl">${esc(d.mpn || '')}</text>`;+      if (d.package) s += `<text x="${cx}" y="${lY + 13}" text-anchor="middle" class="lbl2">${esc(d.package)}</text>`;+      // center pad(s) — exposed/thermal pad, drawn in the body centre+      cen.forEach((p, i) => {+        const col = TYPE_COLORS[p.type] || TYPE_COLORS.other;+        const ex = cx + (i - (cen.length - 1) / 2) * 40, ey = cyB + (cen.length ? 22 : 0);+        s += `<g class="pin" data-pin="${esc(p.number)}">`+          + `<rect class="ep" x="${ex - 19}" y="${ey - 13}" width="38" height="26" rx="4" fill="${col}" fill-opacity="0.15" stroke="${col}"/>`+          + `<text class="ept" x="${ex}" y="${ey + 3}" text-anchor="middle">${esc(p.name)}·${esc(p.number)}</text></g>`;+      });+      for (const p of edge) {+        const col = TYPE_COLORS[p.type] || TYPE_COLORS.other;+        let ex = p.x, ey = p.y, lx = p.x, ly = p.y, anc = 'middle';+        if (p.side === 'left') { ex = bL; lx = p.x - 11; anc = 'end'; ly = p.y + 3; }+        else if (p.side === 'right') { ex = bR; lx = p.x + 11; anc = 'start'; ly = p.y + 3; }+        else if (p.side === 'top') { ey = bT; ly = p.y - 9; }+        else { ey = bB; ly = p.y + 14; }+        s += `<g class="pin" data-pin="${esc(p.number)}">`+          + `<line class="stub" x1="${p.x}" y1="${p.y}" x2="${ex}" y2="${ey}" stroke="${col}"/>`+          + `<circle class="dot" cx="${p.x}" cy="${p.y}" r="3.1" fill="${col}"/>`+          + `<text class="pn" x="${lx}" y="${ly}" text-anchor="${anc}">${esc(p.number)}</text></g>`;+      }+      return s + `</svg>`;+    }++    // Set the highlighted pin. `emit` fires a `pinhover` event (internal hover);+    // external callers use highlight() which does NOT emit (avoids feedback loops).+    _set(n, emit) {+      const root = this.shadowRoot; if (!root) return;+      const s = (n == null ? '' : String(n));+      root.querySelectorAll('tr[data-pin]').forEach(r => r.classList.toggle('hot', r.dataset.pin === s));+      root.querySelectorAll('svg .pin[data-pin]').forEach(g => g.classList.toggle('hot', g.dataset.pin === s));+      const p = (this._pinsRendered || []).find(x => String(x.number) === s);+      const ro = root.querySelector('[data-readout]');+      if (ro) ro.innerHTML = p+        ? `<b>PIN ${esc(p.number)}</b> · ${esc(p.name)}<span class="rt" style="color:${TYPE_COLORS[p.type] || TYPE_COLORS.other}">${TYPE_LABEL[p.type] || p.type}</span>`+        : 'hover a pin&nbsp;→';+      if (emit) { this.dispatchEvent(new CustomEvent('pinhover', { detail: p || null, bubbles: true })); this._emitBus(s); }+    }++    /** Public: drive the highlight from outside (e.g. a linked footprint/symbol widget). */+    highlight(n) { this._set(n, false); }++    // adom-lbr shared cross-highlight bus: BroadcastChannel('lbr-hover') + parent relay.+    _busInit() {+      this._self = 'w' + Math.random().toString(36).slice(2);+      this._bus = ('BroadcastChannel' in window) ? new BroadcastChannel('lbr-hover') : null;+      const on = e => { const d = e.data || {}; if (d.type === 'lbr-hover' && d.src !== this._self) this._set(d.pin == null ? '' : d.pin, false); };+      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); }+    _emitBus(n) {+      const m = { type: 'lbr-hover', pin: (n === '' || n == null) ? null : String(n), mpn: (this._data && this._data.mpn) || '', src: this._self };+      try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }+      try { if (window.parent && window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }+    }++    _wire(pins) {+      this._pinsRendered = pins;+      const root = this.shadowRoot;+      root.querySelectorAll('tr[data-pin]').forEach(r => { r.onmouseenter = () => this._set(r.dataset.pin, true); r.onmouseleave = () => this._set('', true); });+      root.querySelectorAll('svg .pin[data-pin]').forEach(g => { g.onmouseenter = () => this._set(g.dataset.pin, true); g.onmouseleave = () => this._set('', true); });+    }++    _css() {+      return `+      :host{display:block;+        --bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;+        --hot:rgba(0,230,220,.13)}+      @media (prefers-color-scheme:light){:host{--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--hot:rgba(13,143,136,.10)}}+      :host([theme="dark"]){--bg:#0d1420;--panel:#121a26;--line:#26324a;--line2:#33425f;--text:#dbe2ec;--dim:#8b97ac;--dim2:#66738c;--accent:#00e6dc;--hot:rgba(0,230,220,.13)}+      :host([theme="light"]){--bg:#f6f8fc;--panel:#fff;--line:#dbe2ee;--line2:#c3cede;--text:#1a2230;--dim:#5a6678;--dim2:#8592a6;--accent:#0d8f88;--hot:rgba(13,143,136,.10)}+      *{box-sizing:border-box}+      .card{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);+        font:14px/1.5 -apple-system,'Segoe UI',Roboto,Arial,sans-serif}+      .hd{display:flex;align-items:baseline;gap:10px;margin-bottom:12px}+      .hd .mpn{font:700 17px ui-monospace,'SF Mono',Menlo,monospace;letter-spacing:-.2px}+      .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:12px ui-monospace,monospace;color:var(--dim2)}+      .body{display:grid;grid-template-columns:minmax(300px,1fr) minmax(220px,1fr);gap:20px;align-items:start}+      @media(max-width:620px){.body{grid-template-columns:1fr}}+      .readout{height:24px;display:flex;align-items:center;gap:9px;font:600 13px ui-monospace,monospace;color:var(--dim);padding:0 4px}+      .readout b{color:var(--accent)} .readout .rt{margin-left:auto;font-size:11px;text-transform:uppercase;letter-spacing:.08em}+      .diagram{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px}+      svg.chip{width:100%;height:auto;display:block;max-height:440px}+      .pkgbody{fill:color-mix(in srgb,var(--text) 6%,var(--panel));stroke:var(--line2);stroke-width:1}+      .pin1{fill:var(--accent)}+      .lbl{fill:var(--dim);font:600 11px ui-monospace,monospace} .lbl2{fill:var(--dim2);font:9px ui-monospace,monospace}+      .pin .stub{stroke-width:2} .pin .pn{fill:var(--dim2);font:8px ui-monospace,monospace}+      .pin:hover .stub,.pin.hot .stub{stroke-width:3.6} .pin:hover .pn,.pin.hot .pn{fill:var(--text);font-weight:700}+      .pin .ep{stroke-width:1.2} .pin .ept{fill:var(--dim);font:8px ui-monospace,monospace}+      .pin:hover .ep,.pin.hot .ep{fill-opacity:.34;stroke-width:2} .pin:hover .ept,.pin.hot .ept{fill:var(--text);font-weight:700}+      .pin{cursor:default}+      .legend{display:flex;flex-wrap:wrap;gap:13px;margin:8px 2px 2px;font-size:11.5px;color:var(--dim)}+      .legend i{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:5px;vertical-align:middle}+      .tablewrap{max-height:440px;overflow:auto;border:1px solid var(--line);border-radius:10px}+      table{width:100%;border-collapse:collapse;font-size:12.5px}+      thead th{position:sticky;top:0;background:var(--panel);text-align:left;color:var(--dim2);font:600 10px ui-monospace,monospace;letter-spacing:.08em;text-transform:uppercase;padding:7px 9px;border-bottom:1px solid var(--line)}+      td{padding:4px 9px;border-bottom:1px solid color-mix(in srgb,var(--line) 55%,transparent);font-family:ui-monospace,monospace}+      td.num{color:var(--dim2)} td.nm{color:var(--text)}+      tr[data-pin]{cursor:default} tr.hot td{background:var(--hot)}+      .tp{font-size:10px;text-transform:uppercase;letter-spacing:.05em}+      .empty{color:var(--dim2);padding:30px;text-align:center;font-family:ui-monospace,monospace}`;+    }+  }+  if (!customElements.get('adom-pinout')) customElements.define('adom-pinout', AdomPinout);+})();
files/thumb-74hc00.pngadded
⋯ 1 unchanged line ⋯