← Commit history

pinout: JSON export (copy + download) + public toJSON() — structured pin map for AI/code (John's ask)

Ray ·df406b97b9 ·2mo ago ·parent 07d042a
2 files changed +74−2
files/pinout-widget.js+37−1
@@ -115,7 +115,7 @@       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="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>@@ -189,6 +189,35 @@     /** 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() {+      const blob = new Blob([this._exportJson()], { type: 'application/json' });+      const url = URL.createObjectURL(blob), a = document.createElement('a');+      a.href = url; a.download = this._exportName(); document.body.appendChild(a); a.click();+      a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000);+    }+    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);@@ -207,6 +236,8 @@     _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@@ -234,6 +265,11 @@       .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}       @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}
readme.html+37−1
@@ -364,7 +364,7 @@ h2 .anchor:hover{opacity:1;color:var(--teal)}       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="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>@@ -438,6 +438,35 @@ h2 .anchor:hover{opacity:1;color:var(--teal)}     /** 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() {+      const blob = new Blob([this._exportJson()], { type: 'application/json' });+      const url = URL.createObjectURL(blob), a = document.createElement('a');+      a.href = url; a.download = this._exportName(); document.body.appendChild(a); a.click();+      a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000);+    }+    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);@@ -456,6 +485,8 @@ h2 .anchor:hover{opacity:1;color:var(--teal)}     _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@@ -483,6 +514,11 @@ h2 .anchor:hover{opacity:1;color:var(--teal)}       .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}       @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}