← Commit history

pinout section: load the registered widget instead of an inlined copy

Ray ·18510d9c63 ·1mo ago ·parent 261a426
This file carried its own ~20KB copy of pinout-widget.js, which is why the corner-overhang fix shipped in adom-widget on Aug 24 never reached the page and the end pins of every side were still crossing outside the body a week later. It now loads https://wiki.adom.inc/widget-script/adom-pinout.js, the registry route, so the next widget fix arrives here without an edit. 24913 bytes down to 1500. Data moved out to render/pinout.json and passed as src. Kept as a section rather than a README markdown tag on purpose: the tag form would move the pinout inside the readme block, and the pinout was deliberately made its own section. Verified against the registered script: body spans 76..324, pin run 95.5..304.5, 48 lines, no overhang on any side.
1 file changed +20−567
sections/pinout.html+20−567
@@ -4,574 +4,27 @@ :root{color-scheme:dark} body{margin:0;padding:4px 0 10px;background:transparent;   font-family:'Satoshi',system-ui,-apple-system,'Segoe UI',sans-serif;color:#e6edf3}-</style></head><body>-<adom-pinout><script type="application/json">{- "mpn": "STM32F103C8T6",- "package": "LQFP-48",- "pins": [-  {-   "number": 1,-   "name": "VBAT",-   "type": "power"-  },-  {-   "number": 2,-   "name": "PC13-TAMPER-RTC",-   "type": "io"-  },-  {-   "number": 3,-   "name": "PC14-OSC32_IN",-   "type": "io"-  },-  {-   "number": 4,-   "name": "PC15-OSC32_OUT",-   "type": "io"-  },-  {-   "number": 5,-   "name": "PD0-OSC_IN",-   "type": "io"-  },-  {-   "number": 6,-   "name": "PD1-OSC_OUT",-   "type": "io"-  },-  {-   "number": 7,-   "name": "NRST",-   "type": "io"-  },-  {-   "number": 8,-   "name": "VSSA",-   "type": "power"-  },-  {-   "number": 9,-   "name": "VDDA",-   "type": "power"-  },-  {-   "number": 10,-   "name": "PA0-WKUP",-   "type": "io"-  },-  {-   "number": 11,-   "name": "PA1",-   "type": "io"-  },-  {-   "number": 12,-   "name": "PA2",-   "type": "io"-  },-  {-   "number": 13,-   "name": "PA3",-   "type": "io"-  },-  {-   "number": 14,-   "name": "PA4",-   "type": "io"-  },-  {-   "number": 15,-   "name": "PA5",-   "type": "io"-  },-  {-   "number": 16,-   "name": "PA6",-   "type": "io"-  },-  {-   "number": 17,-   "name": "PA7",-   "type": "io"-  },-  {-   "number": 18,-   "name": "PB0",-   "type": "io"-  },-  {-   "number": 19,-   "name": "PB1",-   "type": "io"-  },-  {-   "number": 20,-   "name": "PB2",-   "type": "io"-  },-  {-   "number": 21,-   "name": "PB10",-   "type": "io"-  },-  {-   "number": 22,-   "name": "PB11",-   "type": "io"-  },-  {-   "number": 23,-   "name": "VSS_1",-   "type": "power"-  },-  {-   "number": 24,-   "name": "VDD_1",-   "type": "power"-  },-  {-   "number": 25,-   "name": "PB12",-   "type": "io"-  },-  {-   "number": 26,-   "name": "PB13",-   "type": "io"-  },-  {-   "number": 27,-   "name": "PB14",-   "type": "io"-  },-  {-   "number": 28,-   "name": "PB15",-   "type": "io"-  },-  {-   "number": 29,-   "name": "PA8",-   "type": "io"-  },-  {-   "number": 30,-   "name": "PA9",-   "type": "io"-  },-  {-   "number": 31,-   "name": "PA10",-   "type": "io"-  },-  {-   "number": 32,-   "name": "PA11",-   "type": "io"-  },-  {-   "number": 33,-   "name": "PA12",-   "type": "io"-  },-  {-   "number": 34,-   "name": "PA13",-   "type": "io"-  },-  {-   "number": 35,-   "name": "VSS_2",-   "type": "power"-  },-  {-   "number": 36,-   "name": "VDD_2",-   "type": "power"-  },-  {-   "number": 37,-   "name": "PA14",-   "type": "io"-  },-  {-   "number": 38,-   "name": "PA15",-   "type": "io"-  },-  {-   "number": 39,-   "name": "PB3",-   "type": "io"-  },-  {-   "number": 40,-   "name": "PB4",-   "type": "io"-  },-  {-   "number": 41,-   "name": "PB5",-   "type": "io"-  },-  {-   "number": 42,-   "name": "PB6",-   "type": "io"-  },-  {-   "number": 43,-   "name": "PB7",-   "type": "io"-  },-  {-   "number": 44,-   "name": "BOOT0",-   "type": "io"-  },-  {-   "number": 45,-   "name": "PB8",-   "type": "io"-  },-  {-   "number": 46,-   "name": "PB9",-   "type": "io"-  },-  {-   "number": 47,-   "name": "VSS_3",-   "type": "power"-  },-  {-   "number": 48,-   "name": "VDD_3",-   "type": "power"-  }- ]-}</script></adom-pinout>-<script>/*!- * <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 {-      // The body is drawn inset from the outermost pins, so a run distributed-      // across the whole inner..outer span puts the FIRST and LAST pin of each-      // side OUTSIDE the body: at 12 per side they land 4.3px past each corner-      // and cross the end pin of the neighbouring side (1 into 48, 12 into 13,-      // 24 into 25, 36 into 37). Real quad packages have a corner keep-out-      // anyway, which is why the explicit-xy path never showed this. Inset the-      // run to match the body. Ported from adom-widget 28e23c3.-      const N = edge.length, inner = pad, outer = S - pad, at = (a, b, t) => a + (b - a) * t;-      const CORNER = 26, runLo = inner + CORNER, runHi = outer - CORNER;-      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(runLo, runHi, t), side: 'left' };-          if (s === 1) return { ...p, x: at(runLo, runHi, t), y: outer, side: 'bottom' };-          if (s === 2) return { ...p, x: outer, y: at(runHi, runLo, t), side: 'right' };-          return { ...p, x: at(runHi, runLo, 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(runLo, runHi, (i + 0.5) / half), side: 'left' }-          : { ...p, x: outer, y: at(runHi, runLo, (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">-          <div class="hd">${heading ? `<span class="mpn">${esc(heading)}</span>` : ''}${d.package ? `<span class="pkg">${esc(d.package)}</span>` : ''}<span class="count">${pins.length} pins</span><span class="tools"><button class="tbtn" data-act="copy" title="Copy the pin map as JSON">copy</button><button class="tbtn" data-act="dl" title="Download the pin map as JSON">JSON&nbsp;↓</button></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); }--    /** Public: the canonical pin map as a plain object — the structured handle for code/AI.-     *  Types are resolved (inferred filled in) so consumers get a complete map. No fabricated fields. */-    toJSON() {-      const d = this._data || {};-      const pins = (Array.isArray(d.pins) ? d.pins : [])-        .map(p => ({ number: p.number, name: p.name, type: (p.type || inferType(p.name)).toLowerCase(), ...(p.xy ? { xy: p.xy } : {}) }))-        .sort((a, b) => (Number(a.number) - Number(b.number)) || String(a.number).localeCompare(String(b.number)));-      const out = {};-      if (d.mpn) out.mpn = d.mpn;-      if (d.package) out.package = d.package;-      out.pins = pins;-      return out;-    }--    _exportJson() { return JSON.stringify(this.toJSON(), null, 2); }-    _exportName() { const d = this._data || {}; return ((d.mpn || 'pinout') + (d.package ? '-' + d.package : '')).replace(/[^\w.-]+/g, '_') + '.pins.json'; }-    _download() {-      // data:, not blob:. This renders inside the readme's sandboxed frame,-      // whose CSP carries no blob:, so createObjectURL was refused and the-      // button did nothing at all without saying so.-      const a = document.createElement('a');-      a.href = 'data:application/json;charset=utf-8,' + encodeURIComponent(this._exportJson());-      a.download = this._exportName(); document.body.appendChild(a); a.click(); a.remove();-    }-    async _copy(btn) {-      const txt = this._exportJson(), done = () => { if (!btn) return; const o = btn.textContent; btn.textContent = 'copied'; btn.classList.add('ok'); setTimeout(() => { btn.textContent = o; btn.classList.remove('ok'); }, 1100); };-      try { await navigator.clipboard.writeText(txt); done(); return; } catch (e) { /* sandboxed / denied — fall back */ }-      try { const ta = document.createElement('textarea'); ta.value = txt; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); done(); }-      catch (e2) { this._download(); }   // last resort: hand them the file-    }--    // 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;-      const dl = root.querySelector('.tbtn[data-act="dl"]'); if (dl) dl.onclick = () => this._download();-      const cp = root.querySelector('.tbtn[data-act="copy"]'); if (cp) cp.onclick = () => this._copy(cp);-      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); });-      // Match the pin table's height to the chip diagram column so it fills the-      // space and scrolls, instead of a fixed short box next to a taller chip.-      const dia = root.querySelector('.diagram'), tw = root.querySelector('.tablewrap');-      if (dia && tw) {-        const fit = () => { const h = dia.getBoundingClientRect().height; if (h > 40) tw.style.maxHeight = Math.round(h) + 'px'; };-        requestAnimationFrame(fit); setTimeout(fit, 60);-        try { if (this._ro) this._ro.disconnect(); this._ro = new ResizeObserver(fit); this._ro.observe(dia); } catch (e) { /* */ }-      }-    }--    _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;container-type:inline-size}-      .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)}-      .hd .tools{display:flex;gap:6px;align-self:center}-      .tbtn{font:600 10.5px ui-monospace,monospace;letter-spacing:.03em;color:var(--dim);background:var(--panel);-        border:1px solid var(--line);border-radius:6px;padding:3px 8px;cursor:pointer;transition:color .12s,border-color .12s,background .12s;white-space:nowrap}-      .tbtn:hover{color:var(--text);border-color:var(--line2)}-      .tbtn.ok{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)}-      .body{display:grid;grid-template-columns:minmax(300px,1fr) minmax(220px,1fr);gap:20px;align-items:start}-      /* stack diagram over table when the WIDGET (not the viewport) is narrow —-         e.g. side-by-side in a cross-link row — so the table never clips */-      @container (max-width:580px){.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;scrollbar-width:thin;scrollbar-color:var(--line2) transparent}-      .tablewrap::-webkit-scrollbar{width:9px;height:9px}-      .tablewrap::-webkit-scrollbar-track{background:transparent}-      .tablewrap::-webkit-scrollbar-thumb{background:var(--line2);border-radius:9px;border:2px solid transparent;background-clip:padding-box}-      .tablewrap::-webkit-scrollbar-thumb:hover{background:var(--dim2);border:2px solid transparent;background-clip:padding-box}-      .tablewrap::-webkit-scrollbar-button{display:none;width:0;height:0}-      .tablewrap::-webkit-scrollbar-corner{background:transparent}-      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);-})();-</script>+</style>+<!-- The REGISTERED widget, not a copy of it.++     This file used to inline ~20KB of pinout-widget.js. That meant fixes shipped+     in adom-widget never arrived here: the corner-overhang fix (28e23c3,+     2026-08-24) sat unapplied on this page for a week while the registry served+     the corrected script the whole time. Loading /widget-script/adom-pinout.js+     means the next fix lands without anyone editing this page.++     The frame's CSP is `script-src 'unsafe-inline' https:` and `connect-src+     'self'`, so both the script and the same-origin data fetch resolve.++     The data src is the /api/pages/.../files/ path, NOT the /render/ shortcut the+     registry README suggests: /adom/stm32f103c8t6/render/pinout.json 404s on a+     component page, while the same shape works on an app page+     (/ray/widget-order-lab/render/pinout-small.json is 200). -->+<script src="https://wiki.adom.inc/widget-script/adom-pinout.js"></script>+</head><body>+<adom-pinout src="/api/pages/adom/stm32f103c8t6/files/render/pinout.json"></adom-pinout> <script> function fit(){parent.postMessage({type:'readme-height',height:document.documentElement.scrollHeight},'*')} addEventListener('load',fit);new ResizeObserver(fit).observe(document.documentElement); </script>-</body></html>\ No newline at end of file+</body></html>