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.
← Commit history
Publish 1.1.4
3 files changed
+40−4
package.json+1−1@@ -2,7 +2,7 @@ "slug": "component-widgets", "title": "Component Widgets", "type": "app",- "version": "1.1.3",+ "version": "1.1.4", "description": "Reusable, dependency-free web-component widgets to drop into any component or molecule page: <adom-pinout> (datasheet-style pin diagram + synced table), <adom-heatsink-via> (exposed-pad thermal via array, only when the part has an EP), <adom-molecule> (ganged part set + nets), and <adom-chip-thumbnail> (rendered part image). Each is one custom element, self-contained (Shadow DOM, no deps), theme-aware (light + dark), fed by inline JSON or el.data, and all on one lbr-hover cross-highlight bus so a pin hovered in one lights the same pin everywhere. Symbol, footprint, and 3D stay with adom-lbr; the datasheet renders via the native viewer. Installs the widget JS, iframe-embeddable viewers, example data, and an AI skill to ~/.claude/skills/component-widgets. Live reference page renders them all as a readme.html.", "license": "MIT", "dependencies": {},
page.json+2−2@@ -3,8 +3,8 @@ "slug": "component-widgets", "type": "app", "title": "Component Widgets",- "brief": "One home for the reusable, readme-instantiable component/molecule widgets (pinout, heat-sink via, chip thumbnail, molecule) \u2014 self-contained web components on the shared adom-lbr hover bus.",- "version": "1.1.1",+ "brief": "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.",+ "version": "1.1.4", "visibility": "private", "author": { "name": "Ray",
skills/component-widgets/widgets/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 ↓</button></span></div> <div class="body"> <div class="diagram"> <div class="readout" data-readout>hover a pin →</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}