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
Show reviewed chip artwork caches with previews, source hashes, reuse links and AI status
7 files changed
+255−101
README.md+29@@ -71,3 +71,32 @@ viewers, already on the same hover bus. The **datasheet** renders via the native PDF viewer. Don't reimplement those. See `SKILL.md` for the full data contracts and the bus protocol.++## Cached chip artwork (manifest mode)++The registered Markdown tag also accepts an `adom/chip-artwork-cache@1` JSON+manifest from the component repository:++```markdown+::adom-chip-thumbnail{src="/OWNER/COMPONENT/render/artwork/MPN-thumbs.json"}+```++It shows cached shaded renders, vector outlines, editable source files, exact MPN+placement, canonical Z-up orientation, source hashes and review state. Legacy+PNG/image `src` usage still works. The element's `selfCheck()` returns status,+cache key, review state, selected asset and AI reuse hints.++Read the cache before generating anything. Reuse the reviewed pose and marking+placement. Detailed/optical tops may require the MPN on a clear side surface;+never silently re-etch the top. Cache keys cover geometry, rigid pose, mark+placement, projection and generator. Source/asset hash checks are explicitly+publication-time checks, not continuous verification. A fresh source comparison+is required before downstream reuse. Original manufacturer files and license+terms stay intact. A reviewed display does not qualify pin 1 or native seating.++The canonical widget source is `widgets/chip-thumbnail-widget.js`; keep the+bundled skill copy in sync. Use `adom/widgets` for the registry instructions; no+custom HTML README or per-component JavaScript fork is needed. Never broaden+page visibility to make a private manifest load. Test the actual widget frame+with the intended reader's permissions. Manifest asset URLs must remain in the+component's repository on the manifest origin.
SKILL.md+29@@ -99,3 +99,32 @@ them. The **datasheet** renders via the wiki's native PDF viewer (just push the Each widget has an iframe-embeddable `*-viewer.html` in `widgets/` (`?src=`/`?part=`/ postMessage, `?theme=`, `readme-height` auto-grow) for embedding where live iframes are supported.++## Cached chip artwork (manifest mode)++The registered Markdown tag also accepts an `adom/chip-artwork-cache@1` JSON+manifest from the component repository:++```markdown+::adom-chip-thumbnail{src="/OWNER/COMPONENT/render/artwork/MPN-thumbs.json"}+```++It shows cached shaded renders, vector outlines, editable source files, exact MPN+placement, canonical Z-up orientation, source hashes and review state. Legacy+PNG/image `src` usage still works. The element's `selfCheck()` returns status,+cache key, review state, selected asset and AI reuse hints.++Read the cache before generating anything. Reuse the reviewed pose and marking+placement. Detailed/optical tops may require the MPN on a clear side surface;+never silently re-etch the top. Cache keys cover geometry, rigid pose, mark+placement, projection and generator. Source/asset hash checks are explicitly+publication-time checks, not continuous verification. A fresh source comparison+is required before downstream reuse. Original manufacturer files and license+terms stay intact. A reviewed display does not qualify pin 1 or native seating.++The canonical widget source is `widgets/chip-thumbnail-widget.js`; keep the+bundled skill copy in sync. Use `adom/widgets` for the registry instructions; no+custom HTML README or per-component JavaScript fork is needed. Never broaden+page visibility to make a private manifest load. Test the actual widget frame+with the intended reader's permissions. Manifest asset URLs must remain in the+component's repository on the manifest origin.
files/chip-thumbnail-widget.js+54−33@@ -1,38 +1,59 @@-/*!- * <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.- */+/* <adom-chip-thumbnail>: legacy image or reviewed reusable chip-artwork cache. MIT. */ (() => {- const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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}`;+ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));+ const CSS = `:host{display:block;color:#dbe2ec;font:14px system-ui;--accent:#00b8b0}*{box-sizing:border-box}.cache{background:#0d1117;border:1px solid #30363d;border-radius:12px;padding:20px}h3{margin:0 0 8px;font-size:20px}.muted{color:#a1abb7;line-height:1.5}.badges{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.badge{padding:5px 9px;border:1px solid #394550;border-radius:6px;font-size:12px}.row{display:grid;grid-template-columns:minmax(220px,1fr) minmax(250px,1fr);gap:24px}.preview{background:#faf8f0;border-radius:9px;overflow:hidden;display:flex;align-items:center;justify-content:center;min-height:250px}.preview img{width:100%;max-height:370px;object-fit:contain;transition:transform .3s}.controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}button,a.download{font:inherit;border:1px solid #45505d;border-radius:6px;background:#17202a;color:#e5edf5;padding:8px 12px;cursor:pointer;text-decoration:none}button:hover,a:hover{border-color:var(--accent)}button[aria-pressed=true]{border-color:var(--accent);color:#60e1d6}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}code{overflow-wrap:anywhere;font-size:12px}dl{margin:0}dt{color:#9da8b4;font-size:12px;margin-top:12px}dd{margin:4px 0;line-height:1.5}details{margin-top:16px}summary{cursor:pointer;padding:8px 0}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#161b22;padding:12px;border-radius:6px;font-size:12px}.files{display:grid;gap:9px;margin:12px 0}.files a{color:#6edfd5}.error{color:#ffb7a5}.legacy{width:200px;text-align:center;margin:0}.legacy img{width:100%}.legacy figcaption{padding:8px} @media(max-width:580px){.row{grid-template-columns:1fr}.cache{padding:14px}}`; 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>`;+ static get observedAttributes(){return ['src','mpn','package'];}+ set data(value){this._data=value;if(this.shadowRoot)this._load();}+ get data(){return this._data;}+ connectedCallback(){if(!this.shadowRoot)this.attachShadow({mode:'open'});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>this.isConnected&&this._load(),{once:true});else this._load();}+ attributeChangedCallback(){if(this.shadowRoot)this._load();}+ disconnectedCallback(){this._request=(this._request||0)+1;}+ _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');});}+ async _load(){+ const token=this._request=(this._request||0)+1,src=this.getAttribute('src')||'';+ try{+ let d=this._data;+ if(!d){const inline=this.querySelector('script[type="application/json"]');if(inline)d=JSON.parse(inline.textContent);}+ if(!d && /\.json(?:[?#]|$)/i.test(src)){+ this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache muted">Loading cached chip artwork…</div>`;+ const r=await fetch(src,{credentials:'include'});if(!r.ok)throw new Error('Manifest request returned '+r.status);d=await r.json();+ }+ if(token!==this._request)return;+ if(d?.schema==='adom/chip-artwork-cache@1')this._cache(d,src);+ else if(d?.schema)throw new Error('Unsupported cache schema: '+d.schema);+ else this._legacy(d||{src,mpn:this.getAttribute('mpn'),package:this.getAttribute('package')});+ }catch(e){if(token===this._request){this._state={ok:false,error:String(e),hints:['Check the manifest URL, permissions and schema. No regeneration was attempted.']};this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache error">${esc(e.message)}</div>`;}}+ this._height(); }+ _url(path,src){+ const base=new URL(src||location.href,location.href),marker=base.pathname.indexOf('/render/');+ if(marker>=0){base.pathname=base.pathname.slice(0,marker+8);base.search='';base.hash='';}+ const u=new URL(path,base);if(!['https:','http:'].includes(u.protocol)||u.origin!==base.origin)throw new Error('Asset must use the manifest origin');+ if(marker>=0&&!u.pathname.startsWith(base.pathname))throw new Error('Asset path escapes the component repository');return u.href;+ }+ _legacy(d){+ const src=d.src||'',valid=/^(https?:|data:image\/|\/|\.\/|[^:]+$)/i.test(src);+ this.shadowRoot.innerHTML=`<style>${CSS}</style><figure class="cache legacy">${src&&valid?`<img alt="${esc(d.mpn)}" src="${esc(src)}">`:'<div class="muted">No render cached</div>'}<figcaption>${esc(d.mpn)} ${esc(d.package)}</figcaption></figure>`;+ const img=this.shadowRoot.querySelector('img');if(img){img.onerror=()=>{img.replaceWith(document.createTextNode('Render unavailable'));this._height();};img.onload=()=>this._height();}this._state={ok:true,mode:'legacy-image'};+ }+ _cache(d,src){+ if(!Array.isArray(d.assets)||d.assets.length>100)throw new Error('Invalid asset list');+ const assets=d.assets.map(a=>({...a,url:this._url(a.path,src)}));+ const previews=assets.filter(a=>['png','jpg','jpeg','svg'].includes(a.format)&&a.kind!=='review-evidence');+ if(!previews.length)throw new Error('No cached previews');+ const verified=d.verification?.status==='matched-at-publication',review=d.review||{};+ this._state={ok:true,mode:'cache',mpn:d.mpn,cacheKey:d.cacheKey,assets:assets.length,sourceVerification:verified?'matched-at-publication':'not-verified',review,hints:d.hints||[],manifest:src};+ this.shadowRoot.innerHTML=`<style>${CSS}</style><section class="cache"><h3>Chip-thumbnailer cache</h3><div class="muted">${esc(d.mpn)} · Reuse the prepared chip artwork and its reviewed placement.</div><div class="badges"><span class="badge">${esc(d.recipe?.upAxis||'Unknown')}-up display</span><span class="badge">${assets.length} cached files</span><span class="badge">${verified?'Hashes matched when published':'Source check not recorded'}</span></div><div class="row"><div><div class="preview"><img alt="${esc(d.mpn)} cached chip artwork"></div><div class="controls variants"></div><div class="controls"><button class="zoom">Inspect closer</button><button class="reset">Fit</button></div></div><div><dl><dt>Virtual MPN placement</dt><dd>${esc(d.marking?.placement)}</dd><dt>Artwork review</dt><dd>${esc(review.projectedArtwork||'Not recorded')}</dd><dt>Display orientation</dt><dd>${esc(review.orientation)}</dd><dt>Source freshness</dt><dd class="muted">${verified?'Source and asset hashes matched '+esc(d.verification.checkedAtIso)+'.':'Source hashes have not been checked.'} This records a point-in-time check; compare the manifest hashes before reuse.</dd></dl><details><summary>Reuse files and generation recipe</summary><div class="files"></div><pre>${esc((d.hints||[]).join('\n'))}</pre><code>Cache key: ${esc(d.cacheKey)}</code></details></div></div></section>`;+ const img=this.shadowRoot.querySelector('img'),controls=this.shadowRoot.querySelector('.variants');+ previews.forEach((a,i)=>{const b=document.createElement('button');b.textContent=a.label||a.style||a.kind;b.setAttribute('aria-pressed',String(i===0));b.onclick=()=>{img.src=a.url;img.style.transform='';controls.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));this._state.selected=a.path;};controls.append(b);});+ img.src=previews[0].url;img.onload=()=>this._height();img.onerror=()=>{this._state.ok=false;this._state.error='Preview failed to load';img.alt='Cached preview unavailable';this._height();};+ this.shadowRoot.querySelector('.zoom').onclick=()=>{img.style.transform='scale(1.55)';};this.shadowRoot.querySelector('.reset').onclick=()=>{img.style.transform='';};+ const files=this.shadowRoot.querySelector('.files');for(const a of assets){const link=document.createElement('a');link.href=a.url.replace(/https:\/\/wiki\.adom\.inc\/([^/]+)\/([^/]+)\/render\//,'https://wiki.adom.inc/api/pages/$1/$2/files/');link.download=a.path.split('/').pop();link.textContent=`${a.path.split('/').pop()} · ${(a.sizeBytes/1024).toFixed(1)} KB`;files.append(link);}+ if(src){const a=document.createElement('a');a.href=src;a.download='';a.textContent='Cache manifest and source hashes';files.append(a);}+ this.shadowRoot.querySelector('details').addEventListener('toggle',()=>this._height());+ }+ selfCheck(){const s=structuredClone(this._state||{ok:false,status:'loading'}),img=this.shadowRoot?.querySelector('img');s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);return s;} }- if (!customElements.get('adom-chip-thumbnail')) customElements.define('adom-chip-thumbnail', AdomChipThumbnail);+ if(!customElements.get('adom-chip-thumbnail'))customElements.define('adom-chip-thumbnail',AdomChipThumbnail); })();
package.json+6−2@@ -2,7 +2,7 @@ "slug": "component-widgets", "title": "Component Widgets", "type": "app",- "version": "1.1.5",+ "version": "1.1.6", "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": {},@@ -38,7 +38,11 @@ "readme-drop-in widget", "drop a pinout into a page", "reusable component widget",- "cross-highlight pins on a page"+ "cross-highlight pins on a page",+ "cached chip thumbnails",+ "reuse chip outline",+ "reviewed MPN placement",+ "chip artwork cache" ], "discovery_pitch": "Reach for component-widgets when a component or molecule page needs a live pinout, an exposed-pad thermal via pattern, a ganged molecule/sub-circuit view, or a part thumbnail: each is one self-contained <adom-*> tag fed by inline JSON, theme-aware, and cross-linked with adom-lbr's symbol/footprint/3D on one hover bus.", "sample_prompts": [
skills/component-widgets/SKILL.md+29@@ -114,3 +114,32 @@ them. The **datasheet** renders via the wiki's native PDF viewer (just push the Each widget has an iframe-embeddable `*-viewer.html` in `widgets/` (`?src=`/`?part=`/ postMessage, `?theme=`, `readme-height` auto-grow) for embedding where live iframes are supported.++## Cached chip artwork (manifest mode)++The registered Markdown tag also accepts an `adom/chip-artwork-cache@1` JSON+manifest from the component repository:++```markdown+::adom-chip-thumbnail{src="/OWNER/COMPONENT/render/artwork/MPN-thumbs.json"}+```++It shows cached shaded renders, vector outlines, editable source files, exact MPN+placement, canonical Z-up orientation, source hashes and review state. Legacy+PNG/image `src` usage still works. The element's `selfCheck()` returns status,+cache key, review state, selected asset and AI reuse hints.++Read the cache before generating anything. Reuse the reviewed pose and marking+placement. Detailed/optical tops may require the MPN on a clear side surface;+never silently re-etch the top. Cache keys cover geometry, rigid pose, mark+placement, projection and generator. Source/asset hash checks are explicitly+publication-time checks, not continuous verification. A fresh source comparison+is required before downstream reuse. Original manufacturer files and license+terms stay intact. A reviewed display does not qualify pin 1 or native seating.++The canonical widget source is `widgets/chip-thumbnail-widget.js`; keep the+bundled skill copy in sync. Use `adom/widgets` for the registry instructions; no+custom HTML README or per-component JavaScript fork is needed. Never broaden+page visibility to make a private manifest load. Test the actual widget frame+with the intended reader's permissions. Manifest asset URLs must remain in the+component's repository on the manifest origin.
skills/component-widgets/widgets/chip-thumbnail-widget.js+54−33@@ -1,38 +1,59 @@-/*!- * <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.- */+/* <adom-chip-thumbnail>: legacy image or reviewed reusable chip-artwork cache. MIT. */ (() => {- const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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}`;+ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));+ const CSS = `:host{display:block;color:#dbe2ec;font:14px system-ui;--accent:#00b8b0}*{box-sizing:border-box}.cache{background:#0d1117;border:1px solid #30363d;border-radius:12px;padding:20px}h3{margin:0 0 8px;font-size:20px}.muted{color:#a1abb7;line-height:1.5}.badges{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.badge{padding:5px 9px;border:1px solid #394550;border-radius:6px;font-size:12px}.row{display:grid;grid-template-columns:minmax(220px,1fr) minmax(250px,1fr);gap:24px}.preview{background:#faf8f0;border-radius:9px;overflow:hidden;display:flex;align-items:center;justify-content:center;min-height:250px}.preview img{width:100%;max-height:370px;object-fit:contain;transition:transform .3s}.controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}button,a.download{font:inherit;border:1px solid #45505d;border-radius:6px;background:#17202a;color:#e5edf5;padding:8px 12px;cursor:pointer;text-decoration:none}button:hover,a:hover{border-color:var(--accent)}button[aria-pressed=true]{border-color:var(--accent);color:#60e1d6}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}code{overflow-wrap:anywhere;font-size:12px}dl{margin:0}dt{color:#9da8b4;font-size:12px;margin-top:12px}dd{margin:4px 0;line-height:1.5}details{margin-top:16px}summary{cursor:pointer;padding:8px 0}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#161b22;padding:12px;border-radius:6px;font-size:12px}.files{display:grid;gap:9px;margin:12px 0}.files a{color:#6edfd5}.error{color:#ffb7a5}.legacy{width:200px;text-align:center;margin:0}.legacy img{width:100%}.legacy figcaption{padding:8px} @media(max-width:580px){.row{grid-template-columns:1fr}.cache{padding:14px}}`; 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>`;+ static get observedAttributes(){return ['src','mpn','package'];}+ set data(value){this._data=value;if(this.shadowRoot)this._load();}+ get data(){return this._data;}+ connectedCallback(){if(!this.shadowRoot)this.attachShadow({mode:'open'});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>this.isConnected&&this._load(),{once:true});else this._load();}+ attributeChangedCallback(){if(this.shadowRoot)this._load();}+ disconnectedCallback(){this._request=(this._request||0)+1;}+ _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');});}+ async _load(){+ const token=this._request=(this._request||0)+1,src=this.getAttribute('src')||'';+ try{+ let d=this._data;+ if(!d){const inline=this.querySelector('script[type="application/json"]');if(inline)d=JSON.parse(inline.textContent);}+ if(!d && /\.json(?:[?#]|$)/i.test(src)){+ this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache muted">Loading cached chip artwork…</div>`;+ const r=await fetch(src,{credentials:'include'});if(!r.ok)throw new Error('Manifest request returned '+r.status);d=await r.json();+ }+ if(token!==this._request)return;+ if(d?.schema==='adom/chip-artwork-cache@1')this._cache(d,src);+ else if(d?.schema)throw new Error('Unsupported cache schema: '+d.schema);+ else this._legacy(d||{src,mpn:this.getAttribute('mpn'),package:this.getAttribute('package')});+ }catch(e){if(token===this._request){this._state={ok:false,error:String(e),hints:['Check the manifest URL, permissions and schema. No regeneration was attempted.']};this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache error">${esc(e.message)}</div>`;}}+ this._height(); }+ _url(path,src){+ const base=new URL(src||location.href,location.href),marker=base.pathname.indexOf('/render/');+ if(marker>=0){base.pathname=base.pathname.slice(0,marker+8);base.search='';base.hash='';}+ const u=new URL(path,base);if(!['https:','http:'].includes(u.protocol)||u.origin!==base.origin)throw new Error('Asset must use the manifest origin');+ if(marker>=0&&!u.pathname.startsWith(base.pathname))throw new Error('Asset path escapes the component repository');return u.href;+ }+ _legacy(d){+ const src=d.src||'',valid=/^(https?:|data:image\/|\/|\.\/|[^:]+$)/i.test(src);+ this.shadowRoot.innerHTML=`<style>${CSS}</style><figure class="cache legacy">${src&&valid?`<img alt="${esc(d.mpn)}" src="${esc(src)}">`:'<div class="muted">No render cached</div>'}<figcaption>${esc(d.mpn)} ${esc(d.package)}</figcaption></figure>`;+ const img=this.shadowRoot.querySelector('img');if(img){img.onerror=()=>{img.replaceWith(document.createTextNode('Render unavailable'));this._height();};img.onload=()=>this._height();}this._state={ok:true,mode:'legacy-image'};+ }+ _cache(d,src){+ if(!Array.isArray(d.assets)||d.assets.length>100)throw new Error('Invalid asset list');+ const assets=d.assets.map(a=>({...a,url:this._url(a.path,src)}));+ const previews=assets.filter(a=>['png','jpg','jpeg','svg'].includes(a.format)&&a.kind!=='review-evidence');+ if(!previews.length)throw new Error('No cached previews');+ const verified=d.verification?.status==='matched-at-publication',review=d.review||{};+ this._state={ok:true,mode:'cache',mpn:d.mpn,cacheKey:d.cacheKey,assets:assets.length,sourceVerification:verified?'matched-at-publication':'not-verified',review,hints:d.hints||[],manifest:src};+ this.shadowRoot.innerHTML=`<style>${CSS}</style><section class="cache"><h3>Chip-thumbnailer cache</h3><div class="muted">${esc(d.mpn)} · Reuse the prepared chip artwork and its reviewed placement.</div><div class="badges"><span class="badge">${esc(d.recipe?.upAxis||'Unknown')}-up display</span><span class="badge">${assets.length} cached files</span><span class="badge">${verified?'Hashes matched when published':'Source check not recorded'}</span></div><div class="row"><div><div class="preview"><img alt="${esc(d.mpn)} cached chip artwork"></div><div class="controls variants"></div><div class="controls"><button class="zoom">Inspect closer</button><button class="reset">Fit</button></div></div><div><dl><dt>Virtual MPN placement</dt><dd>${esc(d.marking?.placement)}</dd><dt>Artwork review</dt><dd>${esc(review.projectedArtwork||'Not recorded')}</dd><dt>Display orientation</dt><dd>${esc(review.orientation)}</dd><dt>Source freshness</dt><dd class="muted">${verified?'Source and asset hashes matched '+esc(d.verification.checkedAtIso)+'.':'Source hashes have not been checked.'} This records a point-in-time check; compare the manifest hashes before reuse.</dd></dl><details><summary>Reuse files and generation recipe</summary><div class="files"></div><pre>${esc((d.hints||[]).join('\n'))}</pre><code>Cache key: ${esc(d.cacheKey)}</code></details></div></div></section>`;+ const img=this.shadowRoot.querySelector('img'),controls=this.shadowRoot.querySelector('.variants');+ previews.forEach((a,i)=>{const b=document.createElement('button');b.textContent=a.label||a.style||a.kind;b.setAttribute('aria-pressed',String(i===0));b.onclick=()=>{img.src=a.url;img.style.transform='';controls.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));this._state.selected=a.path;};controls.append(b);});+ img.src=previews[0].url;img.onload=()=>this._height();img.onerror=()=>{this._state.ok=false;this._state.error='Preview failed to load';img.alt='Cached preview unavailable';this._height();};+ this.shadowRoot.querySelector('.zoom').onclick=()=>{img.style.transform='scale(1.55)';};this.shadowRoot.querySelector('.reset').onclick=()=>{img.style.transform='';};+ const files=this.shadowRoot.querySelector('.files');for(const a of assets){const link=document.createElement('a');link.href=a.url.replace(/https:\/\/wiki\.adom\.inc\/([^/]+)\/([^/]+)\/render\//,'https://wiki.adom.inc/api/pages/$1/$2/files/');link.download=a.path.split('/').pop();link.textContent=`${a.path.split('/').pop()} · ${(a.sizeBytes/1024).toFixed(1)} KB`;files.append(link);}+ if(src){const a=document.createElement('a');a.href=src;a.download='';a.textContent='Cache manifest and source hashes';files.append(a);}+ this.shadowRoot.querySelector('details').addEventListener('toggle',()=>this._height());+ }+ selfCheck(){const s=structuredClone(this._state||{ok:false,status:'loading'}),img=this.shadowRoot?.querySelector('img');s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);return s;} }- if (!customElements.get('adom-chip-thumbnail')) customElements.define('adom-chip-thumbnail', AdomChipThumbnail);+ if(!customElements.get('adom-chip-thumbnail'))customElements.define('adom-chip-thumbnail',AdomChipThumbnail); })();
widgets/chip-thumbnail-widget.js+54−33@@ -1,38 +1,59 @@-/*!- * <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.- */+/* <adom-chip-thumbnail>: legacy image or reviewed reusable chip-artwork cache. MIT. */ (() => {- const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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}`;+ const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));+ const CSS = `:host{display:block;color:#dbe2ec;font:14px system-ui;--accent:#00b8b0}*{box-sizing:border-box}.cache{background:#0d1117;border:1px solid #30363d;border-radius:12px;padding:20px}h3{margin:0 0 8px;font-size:20px}.muted{color:#a1abb7;line-height:1.5}.badges{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.badge{padding:5px 9px;border:1px solid #394550;border-radius:6px;font-size:12px}.row{display:grid;grid-template-columns:minmax(220px,1fr) minmax(250px,1fr);gap:24px}.preview{background:#faf8f0;border-radius:9px;overflow:hidden;display:flex;align-items:center;justify-content:center;min-height:250px}.preview img{width:100%;max-height:370px;object-fit:contain;transition:transform .3s}.controls{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}button,a.download{font:inherit;border:1px solid #45505d;border-radius:6px;background:#17202a;color:#e5edf5;padding:8px 12px;cursor:pointer;text-decoration:none}button:hover,a:hover{border-color:var(--accent)}button[aria-pressed=true]{border-color:var(--accent);color:#60e1d6}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}code{overflow-wrap:anywhere;font-size:12px}dl{margin:0}dt{color:#9da8b4;font-size:12px;margin-top:12px}dd{margin:4px 0;line-height:1.5}details{margin-top:16px}summary{cursor:pointer;padding:8px 0}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#161b22;padding:12px;border-radius:6px;font-size:12px}.files{display:grid;gap:9px;margin:12px 0}.files a{color:#6edfd5}.error{color:#ffb7a5}.legacy{width:200px;text-align:center;margin:0}.legacy img{width:100%}.legacy figcaption{padding:8px} @media(max-width:580px){.row{grid-template-columns:1fr}.cache{padding:14px}}`; 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>`;+ static get observedAttributes(){return ['src','mpn','package'];}+ set data(value){this._data=value;if(this.shadowRoot)this._load();}+ get data(){return this._data;}+ connectedCallback(){if(!this.shadowRoot)this.attachShadow({mode:'open'});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>this.isConnected&&this._load(),{once:true});else this._load();}+ attributeChangedCallback(){if(this.shadowRoot)this._load();}+ disconnectedCallback(){this._request=(this._request||0)+1;}+ _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');});}+ async _load(){+ const token=this._request=(this._request||0)+1,src=this.getAttribute('src')||'';+ try{+ let d=this._data;+ if(!d){const inline=this.querySelector('script[type="application/json"]');if(inline)d=JSON.parse(inline.textContent);}+ if(!d && /\.json(?:[?#]|$)/i.test(src)){+ this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache muted">Loading cached chip artwork…</div>`;+ const r=await fetch(src,{credentials:'include'});if(!r.ok)throw new Error('Manifest request returned '+r.status);d=await r.json();+ }+ if(token!==this._request)return;+ if(d?.schema==='adom/chip-artwork-cache@1')this._cache(d,src);+ else if(d?.schema)throw new Error('Unsupported cache schema: '+d.schema);+ else this._legacy(d||{src,mpn:this.getAttribute('mpn'),package:this.getAttribute('package')});+ }catch(e){if(token===this._request){this._state={ok:false,error:String(e),hints:['Check the manifest URL, permissions and schema. No regeneration was attempted.']};this.shadowRoot.innerHTML=`<style>${CSS}</style><div class="cache error">${esc(e.message)}</div>`;}}+ this._height(); }+ _url(path,src){+ const base=new URL(src||location.href,location.href),marker=base.pathname.indexOf('/render/');+ if(marker>=0){base.pathname=base.pathname.slice(0,marker+8);base.search='';base.hash='';}+ const u=new URL(path,base);if(!['https:','http:'].includes(u.protocol)||u.origin!==base.origin)throw new Error('Asset must use the manifest origin');+ if(marker>=0&&!u.pathname.startsWith(base.pathname))throw new Error('Asset path escapes the component repository');return u.href;+ }+ _legacy(d){+ const src=d.src||'',valid=/^(https?:|data:image\/|\/|\.\/|[^:]+$)/i.test(src);+ this.shadowRoot.innerHTML=`<style>${CSS}</style><figure class="cache legacy">${src&&valid?`<img alt="${esc(d.mpn)}" src="${esc(src)}">`:'<div class="muted">No render cached</div>'}<figcaption>${esc(d.mpn)} ${esc(d.package)}</figcaption></figure>`;+ const img=this.shadowRoot.querySelector('img');if(img){img.onerror=()=>{img.replaceWith(document.createTextNode('Render unavailable'));this._height();};img.onload=()=>this._height();}this._state={ok:true,mode:'legacy-image'};+ }+ _cache(d,src){+ if(!Array.isArray(d.assets)||d.assets.length>100)throw new Error('Invalid asset list');+ const assets=d.assets.map(a=>({...a,url:this._url(a.path,src)}));+ const previews=assets.filter(a=>['png','jpg','jpeg','svg'].includes(a.format)&&a.kind!=='review-evidence');+ if(!previews.length)throw new Error('No cached previews');+ const verified=d.verification?.status==='matched-at-publication',review=d.review||{};+ this._state={ok:true,mode:'cache',mpn:d.mpn,cacheKey:d.cacheKey,assets:assets.length,sourceVerification:verified?'matched-at-publication':'not-verified',review,hints:d.hints||[],manifest:src};+ this.shadowRoot.innerHTML=`<style>${CSS}</style><section class="cache"><h3>Chip-thumbnailer cache</h3><div class="muted">${esc(d.mpn)} · Reuse the prepared chip artwork and its reviewed placement.</div><div class="badges"><span class="badge">${esc(d.recipe?.upAxis||'Unknown')}-up display</span><span class="badge">${assets.length} cached files</span><span class="badge">${verified?'Hashes matched when published':'Source check not recorded'}</span></div><div class="row"><div><div class="preview"><img alt="${esc(d.mpn)} cached chip artwork"></div><div class="controls variants"></div><div class="controls"><button class="zoom">Inspect closer</button><button class="reset">Fit</button></div></div><div><dl><dt>Virtual MPN placement</dt><dd>${esc(d.marking?.placement)}</dd><dt>Artwork review</dt><dd>${esc(review.projectedArtwork||'Not recorded')}</dd><dt>Display orientation</dt><dd>${esc(review.orientation)}</dd><dt>Source freshness</dt><dd class="muted">${verified?'Source and asset hashes matched '+esc(d.verification.checkedAtIso)+'.':'Source hashes have not been checked.'} This records a point-in-time check; compare the manifest hashes before reuse.</dd></dl><details><summary>Reuse files and generation recipe</summary><div class="files"></div><pre>${esc((d.hints||[]).join('\n'))}</pre><code>Cache key: ${esc(d.cacheKey)}</code></details></div></div></section>`;+ const img=this.shadowRoot.querySelector('img'),controls=this.shadowRoot.querySelector('.variants');+ previews.forEach((a,i)=>{const b=document.createElement('button');b.textContent=a.label||a.style||a.kind;b.setAttribute('aria-pressed',String(i===0));b.onclick=()=>{img.src=a.url;img.style.transform='';controls.querySelectorAll('button').forEach(x=>x.setAttribute('aria-pressed',String(x===b)));this._state.selected=a.path;};controls.append(b);});+ img.src=previews[0].url;img.onload=()=>this._height();img.onerror=()=>{this._state.ok=false;this._state.error='Preview failed to load';img.alt='Cached preview unavailable';this._height();};+ this.shadowRoot.querySelector('.zoom').onclick=()=>{img.style.transform='scale(1.55)';};this.shadowRoot.querySelector('.reset').onclick=()=>{img.style.transform='';};+ const files=this.shadowRoot.querySelector('.files');for(const a of assets){const link=document.createElement('a');link.href=a.url.replace(/https:\/\/wiki\.adom\.inc\/([^/]+)\/([^/]+)\/render\//,'https://wiki.adom.inc/api/pages/$1/$2/files/');link.download=a.path.split('/').pop();link.textContent=`${a.path.split('/').pop()} · ${(a.sizeBytes/1024).toFixed(1)} KB`;files.append(link);}+ if(src){const a=document.createElement('a');a.href=src;a.download='';a.textContent='Cache manifest and source hashes';files.append(a);}+ this.shadowRoot.querySelector('details').addEventListener('toggle',()=>this._height());+ }+ selfCheck(){const s=structuredClone(this._state||{ok:false,status:'loading'}),img=this.shadowRoot?.querySelector('img');s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);return s;} }- if (!customElements.get('adom-chip-thumbnail')) customElements.define('adom-chip-thumbnail', AdomChipThumbnail);+ if(!customElements.get('adom-chip-thumbnail'))customElements.define('adom-chip-thumbnail',AdomChipThumbnail); })();