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
Expose on-demand viewer resource diagnostics and prevent iframe height feedback; document page-wide memory audits
6 files changed
+98−11
SKILL.md+12@@ -201,3 +201,15 @@ For private wiki LODs, generate one lazy `*.frame.html` transport per level in t Opaque widgets also deny Web Storage. If access throws, use ephemeral document-local storage for canonical viewer preferences; never overwrite working user storage. Test against the real wiki CSP and private-file authorization separately from an unrestricted local harness. Preserve optional 3D behavior and small cached previews by default. When enabling an inline preview, reveal the expanded viewer inside its existing work surface before starting offscreen observation or model loading. Test with the enable button near the bottom of a short viewport. Otherwise an otherwise-correct offscreen cleanup policy can immediately cancel the user's requested preview. Preserve the editor pane and OS foreground.++## True on-demand viewers and page memory++Before an explicit click, an optional 3D control must create no renderer, WebGL context, canvas, model-transfer iframe, model decode or Babylon import. A hidden container may be empty; CSS hiding an initialized engine and iframe loading="lazy" do not meet this contract. Scrolling near a button must not activate its 3D preview. Cached PNG/WebP/SVG previews may load independently with lazy image decoding.++Audit the exact user-named page and browser tab. `selfCheck().resources` reports widget-local canvases, transfer frames, viewer module presence/requests and model requests without calling canvas.getContext (which could itself create a context). Inspect the parent page's hero, symbol, footprint and datasheet frames separately: a correct LOD widget cannot make eager sibling viewers inexpensive. Browser process memory and device free memory are separate from JS heap, GLB download size and manifest buffer estimates.++After activation, verify off, offscreen, hidden-tab, disconnection and cancelled-load cleanup: abort requests, dispose scene/engine/assets, release buffers/context and remove DOM. Disposing an engine does not unload an already imported JS module; use a disposable iframe for features that must also release the module realm. Do not claim total browser RAM release from a zero-buffer count. Keep a tab's URL and static content available when suspending heavy viewers; loading again requires an explicit click.++Fit iframe height from intrinsic widget content, coalesce callbacks and only emit changed heights. Never repeatedly add padding to documentElement.scrollHeight: the iframe viewport can become its own ever-growing input. Test within a parent that really applies readme-height, including repeated status updates and scrolling.++For every widget, record cold-load and scroll-only network/DOM/context evidence before clicking, then an activation and teardown cycle. Same-browser siblings can dominate the page's memory even when this widget reports zero. Source edits, shared package publication, registry script delivery, and already-open browser tabs are distinct deployment states.
files/chip-thumbnail-widget.js+23−3@@ -120,8 +120,18 @@ async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) { get data(){return this._data;} connectedCallback(){if(!this.shadowRoot)this.attachShadow({mode:'open'});if(!this._commandHandler){this._commandHandler=e=>{if(e.source!==parent||e.data?.type!=='adom-chip-thumbnail-command')return;const {action,index,open}=e.data,s=this.shadowRoot;let button;if(action==='selectVariant'&&Number.isInteger(index))button=s.querySelectorAll('.variants button')[index];else if(action==='zoom')button=s.querySelector('.zoom');else if(action==='fit')button=s.querySelector('.reset');else if(action==='showFiles'&&typeof open==='boolean'){const details=s.querySelector('details');if(details)details.open=open;}if(button)button.click();this._height();};window.addEventListener('message',this._commandHandler);}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>this.isConnected&&this._load(),{once:true});else this._load();} attributeChangedCallback(){if(this.shadowRoot)this._load();}- disconnectedCallback(){this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}- _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');});}+ disconnectedCallback(){cancelAnimationFrame(this._heightFrame);this._heightFrame=0;this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}+ _height(){+ if(this._heightFrame)return;+ this._heightFrame=requestAnimationFrame(()=>{+ this._heightFrame=0;if(!this.isConnected)return;+ // Measure content, never the iframe viewport: scrollHeight + 2 can grow+ // its parent's iframe repeatedly and trigger scrolling/layout feedback.+ const height=Math.ceil(this.getBoundingClientRect().height)+2;+ if(height!==this._lastHeight){this._lastHeight=height;parent.postMessage({type:'readme-height',height},'*');}+ parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');+ });+ } async _load(){ this._stopPreview(); const token=this._request=(this._request||0)+1,src=this.getAttribute('src')||'';@@ -211,7 +221,17 @@ async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) { 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');if(s.mode!=='lod')s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);return s;}+ selfCheck(){+ const s=structuredClone(this._state||{ok:false,status:'loading'}),img=this.shadowRoot?.querySelector('img');+ if(s.mode!=='lod')s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);+ const host=this.shadowRoot?.querySelector('.lod-live');+ s.resources={viewerModuleLoaded:!!window.Adom3DViewerBabylon9,previewActive:!!this._previewActive,+ viewerCanvases:host?.querySelectorAll('canvas').length||0,viewerFrames:host?.querySelectorAll('iframe').length||0,+ assetTransferFrames:document.querySelectorAll('iframe[title="Component LOD asset transfer"]').length,+ viewerModuleRequests:performance.getEntriesByType('resource').filter(r=>/adom-3d-viewer-babylon9/.test(r.name)).length,+ modelRequests:s.glbRequests||0,activation:'explicit-click',scope:'This widget document only; inspect sibling page viewers separately'};+ return s;+ } } if(!customElements.get('adom-chip-thumbnail'))customElements.define('adom-chip-thumbnail',AdomChipThumbnail); if(!customElements.get('adom-component-lod'))customElements.define('adom-component-lod',class extends AdomChipThumbnail {});
package.json+5−2@@ -2,7 +2,7 @@ "slug": "component-widgets", "title": "Component Widgets", "type": "app",- "version": "1.1.12",+ "version": "1.1.13", "description": "Reusable component-page widgets for pinouts, thermal vias, molecule relationships, reviewed chip outlines and image-first LOD caches with an optional zoom-driven 3D preview.", "license": "MIT", "dependencies": {},@@ -49,7 +49,10 @@ "high medium low super-low", "LOD preview screenshots", "image-first LOD widget",- "turn on 3D preview"+ "turn on 3D preview",+ "lazy load component viewers",+ "wiki page memory",+ "LOD preview memory audit" ], "discovery_pitch": "Show cached component artwork and LOD images without loading 3D. Enable the canonical Adom viewer only when the user asks to inspect zoom-driven detail.", "sample_prompts": [
skills/component-widgets/SKILL.md+12@@ -201,3 +201,15 @@ For private wiki LODs, generate one lazy `*.frame.html` transport per level in t Opaque widgets also deny Web Storage. If access throws, use ephemeral document-local storage for canonical viewer preferences; never overwrite working user storage. Test against the real wiki CSP and private-file authorization separately from an unrestricted local harness. Preserve optional 3D behavior and small cached previews by default. When enabling an inline preview, reveal the expanded viewer inside its existing work surface before starting offscreen observation or model loading. Test with the enable button near the bottom of a short viewport. Otherwise an otherwise-correct offscreen cleanup policy can immediately cancel the user's requested preview. Preserve the editor pane and OS foreground.++## True on-demand viewers and page memory++Before an explicit click, an optional 3D control must create no renderer, WebGL context, canvas, model-transfer iframe, model decode or Babylon import. A hidden container may be empty; CSS hiding an initialized engine and iframe loading="lazy" do not meet this contract. Scrolling near a button must not activate its 3D preview. Cached PNG/WebP/SVG previews may load independently with lazy image decoding.++Audit the exact user-named page and browser tab. `selfCheck().resources` reports widget-local canvases, transfer frames, viewer module presence/requests and model requests without calling canvas.getContext (which could itself create a context). Inspect the parent page's hero, symbol, footprint and datasheet frames separately: a correct LOD widget cannot make eager sibling viewers inexpensive. Browser process memory and device free memory are separate from JS heap, GLB download size and manifest buffer estimates.++After activation, verify off, offscreen, hidden-tab, disconnection and cancelled-load cleanup: abort requests, dispose scene/engine/assets, release buffers/context and remove DOM. Disposing an engine does not unload an already imported JS module; use a disposable iframe for features that must also release the module realm. Do not claim total browser RAM release from a zero-buffer count. Keep a tab's URL and static content available when suspending heavy viewers; loading again requires an explicit click.++Fit iframe height from intrinsic widget content, coalesce callbacks and only emit changed heights. Never repeatedly add padding to documentElement.scrollHeight: the iframe viewport can become its own ever-growing input. Test within a parent that really applies readme-height, including repeated status updates and scrolling.++For every widget, record cold-load and scroll-only network/DOM/context evidence before clicking, then an activation and teardown cycle. Same-browser siblings can dominate the page's memory even when this widget reports zero. Source edits, shared package publication, registry script delivery, and already-open browser tabs are distinct deployment states.
skills/component-widgets/widgets/chip-thumbnail-widget.js+23−3@@ -120,8 +120,18 @@ async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) { get data(){return this._data;} connectedCallback(){if(!this.shadowRoot)this.attachShadow({mode:'open'});if(!this._commandHandler){this._commandHandler=e=>{if(e.source!==parent||e.data?.type!=='adom-chip-thumbnail-command')return;const {action,index,open}=e.data,s=this.shadowRoot;let button;if(action==='selectVariant'&&Number.isInteger(index))button=s.querySelectorAll('.variants button')[index];else if(action==='zoom')button=s.querySelector('.zoom');else if(action==='fit')button=s.querySelector('.reset');else if(action==='showFiles'&&typeof open==='boolean'){const details=s.querySelector('details');if(details)details.open=open;}if(button)button.click();this._height();};window.addEventListener('message',this._commandHandler);}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>this.isConnected&&this._load(),{once:true});else this._load();} attributeChangedCallback(){if(this.shadowRoot)this._load();}- disconnectedCallback(){this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}- _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');});}+ disconnectedCallback(){cancelAnimationFrame(this._heightFrame);this._heightFrame=0;this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}+ _height(){+ if(this._heightFrame)return;+ this._heightFrame=requestAnimationFrame(()=>{+ this._heightFrame=0;if(!this.isConnected)return;+ // Measure content, never the iframe viewport: scrollHeight + 2 can grow+ // its parent's iframe repeatedly and trigger scrolling/layout feedback.+ const height=Math.ceil(this.getBoundingClientRect().height)+2;+ if(height!==this._lastHeight){this._lastHeight=height;parent.postMessage({type:'readme-height',height},'*');}+ parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');+ });+ } async _load(){ this._stopPreview(); const token=this._request=(this._request||0)+1,src=this.getAttribute('src')||'';@@ -211,7 +221,17 @@ async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) { 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');if(s.mode!=='lod')s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);return s;}+ selfCheck(){+ const s=structuredClone(this._state||{ok:false,status:'loading'}),img=this.shadowRoot?.querySelector('img');+ if(s.mode!=='lod')s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);+ const host=this.shadowRoot?.querySelector('.lod-live');+ s.resources={viewerModuleLoaded:!!window.Adom3DViewerBabylon9,previewActive:!!this._previewActive,+ viewerCanvases:host?.querySelectorAll('canvas').length||0,viewerFrames:host?.querySelectorAll('iframe').length||0,+ assetTransferFrames:document.querySelectorAll('iframe[title="Component LOD asset transfer"]').length,+ viewerModuleRequests:performance.getEntriesByType('resource').filter(r=>/adom-3d-viewer-babylon9/.test(r.name)).length,+ modelRequests:s.glbRequests||0,activation:'explicit-click',scope:'This widget document only; inspect sibling page viewers separately'};+ return s;+ } } if(!customElements.get('adom-chip-thumbnail'))customElements.define('adom-chip-thumbnail',AdomChipThumbnail); if(!customElements.get('adom-component-lod'))customElements.define('adom-component-lod',class extends AdomChipThumbnail {});
widgets/chip-thumbnail-widget.js+23−3@@ -120,8 +120,18 @@ async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) { get data(){return this._data;} connectedCallback(){if(!this.shadowRoot)this.attachShadow({mode:'open'});if(!this._commandHandler){this._commandHandler=e=>{if(e.source!==parent||e.data?.type!=='adom-chip-thumbnail-command')return;const {action,index,open}=e.data,s=this.shadowRoot;let button;if(action==='selectVariant'&&Number.isInteger(index))button=s.querySelectorAll('.variants button')[index];else if(action==='zoom')button=s.querySelector('.zoom');else if(action==='fit')button=s.querySelector('.reset');else if(action==='showFiles'&&typeof open==='boolean'){const details=s.querySelector('details');if(details)details.open=open;}if(button)button.click();this._height();};window.addEventListener('message',this._commandHandler);}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>this.isConnected&&this._load(),{once:true});else this._load();} attributeChangedCallback(){if(this.shadowRoot)this._load();}- disconnectedCallback(){this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}- _height(){requestAnimationFrame(()=>{parent.postMessage({type:'readme-height',height:Math.ceil(document.documentElement.scrollHeight)+2},'*');parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');});}+ disconnectedCallback(){cancelAnimationFrame(this._heightFrame);this._heightFrame=0;this._stopPreview();this._request=(this._request||0)+1;if(this._commandHandler){window.removeEventListener('message',this._commandHandler);this._commandHandler=null;}}+ _height(){+ if(this._heightFrame)return;+ this._heightFrame=requestAnimationFrame(()=>{+ this._heightFrame=0;if(!this.isConnected)return;+ // Measure content, never the iframe viewport: scrollHeight + 2 can grow+ // its parent's iframe repeatedly and trigger scrolling/layout feedback.+ const height=Math.ceil(this.getBoundingClientRect().height)+2;+ if(height!==this._lastHeight){this._lastHeight=height;parent.postMessage({type:'readme-height',height},'*');}+ parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*');+ });+ } async _load(){ this._stopPreview(); const token=this._request=(this._request||0)+1,src=this.getAttribute('src')||'';@@ -211,7 +221,17 @@ async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) { 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');if(s.mode!=='lod')s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);return s;}+ selfCheck(){+ const s=structuredClone(this._state||{ok:false,status:'loading'}),img=this.shadowRoot?.querySelector('img');+ if(s.mode!=='lod')s.previewLoaded=!!(img?.complete&&img.naturalWidth>0);+ const host=this.shadowRoot?.querySelector('.lod-live');+ s.resources={viewerModuleLoaded:!!window.Adom3DViewerBabylon9,previewActive:!!this._previewActive,+ viewerCanvases:host?.querySelectorAll('canvas').length||0,viewerFrames:host?.querySelectorAll('iframe').length||0,+ assetTransferFrames:document.querySelectorAll('iframe[title="Component LOD asset transfer"]').length,+ viewerModuleRequests:performance.getEntriesByType('resource').filter(r=>/adom-3d-viewer-babylon9/.test(r.name)).length,+ modelRequests:s.glbRequests||0,activation:'explicit-click',scope:'This widget document only; inspect sibling page viewers separately'};+ return s;+ } } if(!customElements.get('adom-chip-thumbnail'))customElements.define('adom-chip-thumbnail',AdomChipThumbnail); if(!customElements.get('adom-component-lod'))customElements.define('adom-component-lod',class extends AdomChipThumbnail {});