← Commit history

Show reviewed chip artwork caches with previews, source hashes, reuse links and AI status

John Lauer ·039f976876 ·1mo ago ·parent 2489e3d
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 => ({ '&': '&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}`;+  const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 => ({ '&': '&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}`;+  const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 => ({ '&': '&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}`;+  const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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); })();