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
Widgets: safe to render inline in a wiki README (adom/wiki#229)
Frame-only code (parent postMessage, page overflow/margin, adom-3d's Blob/storage shims and head style) now runs only in the sandboxed widget frame route (/OWNER/SLUG/widget/NAME), not when the wiki page itself is framed in Hydrogen. Inline, adom-3d embeds the wiki's same-origin /viewer/3d route instead of running Babylon in the page. pinout, footprint-variants, heatsink-via and molecule follow a host page's pinned color-scheme (the wiki is dark-only). Viewer bundle imports are root-relative so staging works. No behavior change in the frame route.
9 files changed
+63−10
widgets/adom-table.js+4@@ -35,6 +35,10 @@ var host = (function () { var framed = false; try { framed = global.parent && global.parent !== global; } catch (_) { framed = true; }+ // Inline in a wiki README the document is the wiki page, which is itself framed in a Hydrogen webview:+ // that parent is not this widget's host. The sandboxed widget frame is /OWNER/SLUG/widget/NAME.+ var wikiInline = !/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname) && !!document.querySelector('.adom-widget[data-widget]');+ if (wikiInline) framed = false; // An opaque-origin frame (the readme sandbox) throws on storage access. var opaque = false; try { global.localStorage.getItem('_aw'); } catch (_) { opaque = true; }
widgets/chip-thumbnail-widget.js+3−1@@ -5,7 +5,7 @@ // BEGIN bundled opt-in runtime (generated from component-lod-preview.js) // Opt-in LOD inspection, built on the canonical Adom Babylon viewer. // No module, engine or model is loaded until the user enables this preview.-const VIEWER='https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a';+const VIEWER='/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a'; async function loadAssetBytes(info,resolveAsset,signal) { if(info.transport?.kind!=='wiki-sandbox-frame@1'){ const r=await fetch(resolveAsset(info.path),{credentials:'include',signal});@@ -128,6 +128,8 @@ async function mountLodPreview({host,manifest,resolveAsset,onState,signal}) { // 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;+ // Only the wiki's sandboxed widget frame reports to its parent; inline, the parent may be a Hydrogen webview.+ if(!/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname))return; if(height!==this._lastHeight){this._lastHeight=height;parent.postMessage({type:'readme-height',height},'*');} parent.postMessage({type:'adom-chip-thumbnail-state',state:this.selfCheck()},'*'); });
widgets/component-lod-preview.js+1−1@@ -1,6 +1,6 @@ // Opt-in LOD inspection, built on the canonical Adom Babylon viewer. // No module, engine or model is loaded until the user enables this preview.-const VIEWER='https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a';+const VIEWER='/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js?v=20260828a'; async function loadAssetBytes(info,resolveAsset,signal) { if(info.transport?.kind!=='wiki-sandbox-frame@1'){ const r=await fetch(resolveAsset(info.path),{credentials:'include',signal});
widgets/footprint-variants-widget.js+5−1@@ -102,6 +102,9 @@ static parseMod = parseMod; // exposed for tests / structured consumers async connectedCallback() {+ // A host page that pins its color-scheme (the wiki is dark-only) decides the theme; inline, prefers-color-scheme+ // would follow the reader's OS instead (a frame inherits the embedding page's scheme).+ if (!this.hasAttribute('theme')) { const cs = getComputedStyle(document.documentElement).colorScheme.trim(); if (/^(only )?(dark|light)( only)?$/.test(cs)) this.setAttribute('theme', cs.replace(/only|\s/g, '')); } if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' }); this._mods = Object.create(null); // suffix -> parsed mod (or Promise)@@ -299,7 +302,8 @@ _emitBus(n) { const m = { type: 'lbr-hover', pin: (n === '' || n == null) ? null : String(n), mpn: (this._data && this._data.part) || '', src: this._self }; try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }- try { if (window.parent && window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }+ // parent relay only from the wiki's sandboxed widget frame; inline, BroadcastChannel already reaches every widget+ try { if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ } } _css() {
widgets/heatsink-via-widget.js+5−1@@ -42,6 +42,9 @@ get data() { return this._data; } async connectedCallback() {+ // A host page that pins its color-scheme (the wiki is dark-only) decides the theme; inline, prefers-color-scheme+ // would follow the reader's OS instead (a frame inherits the embedding page's scheme).+ if (!this.hasAttribute('theme')) { const cs = getComputedStyle(document.documentElement).colorScheme.trim(); if (/^(only )?(dark|light)( only)?$/.test(cs)) this.setAttribute('theme', cs.replace(/only|\s/g, '')); } if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' }); this._busInit();@@ -65,7 +68,8 @@ _post(n) { const m = { type: 'lbr-hover', pin: n == null ? null : String(n), mpn: (this._data && this._data.mpn) || '', src: this._self }; try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }- try { if (window.parent && window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }+ // parent relay only from the wiki's sandboxed widget frame; inline, BroadcastChannel already reaches every widget+ try { if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ } } _render() {
widgets/model-3d-widget.js+34−3@@ -14,9 +14,16 @@ * two). Unload, scrolling it well offscreen or leaving the page disposes the * viewer and releases the WebGL context; Load 3D model brings it back. The model is fetched as bytes and handed to loadModelBytes, and the * GLB's embedded textures are given out as data: URLs, because the frame's img-src forbids blob:.+ *+ * Inline in a wiki README (registry "render": "inline") the document is the wiki page itself, so none of the+ * frame's shims run: the widget embeds the wiki's own same-origin 3D viewer route instead+ * (/viewer/3d/OWNER/TYPE/SLUG?model=PATH), which carries the reader's session and needs no page CSP change.+ * Inline, src must be a file of a wiki page: /OWNER/SLUG/render/PATH.glb. */ (() => {- const VIEWER = 'https://wiki.adom.inc/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js';+ const VIEWER = '/static/vendor/adom-3d-viewer-babylon9/adom-3d-viewer-babylon9.esm.js';+ // the wiki's sandboxed widget frame (/OWNER/SLUG/widget/NAME); anywhere else this script runs inline in a page+ const FRAME = /^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname); const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); // The glTF loader turns embedded images into blob: URLs, which this frame's CSP blocks. Remember each Blob's@@ -54,10 +61,10 @@ if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' }); // in a wiki frame: no page margin, and nothing the viewer adds (its tip bubble) may give the frame a scrollbar- if (window.self !== window.top) { document.documentElement.style.overflow = 'hidden'; document.body.style.margin = '0'; }+ if (FRAME) { document.documentElement.style.overflow = 'hidden'; document.body.style.margin = '0'; } // the viewer shows the same Shift+Alt hint twice on first load (its pivot chip and its help tip; in a frame the // "seen" flag cannot persist, so both return every time): keep the chip only as the live "keys held" indicator- if (!document.getElementById('adom-3d-hint')) { const st = document.createElement('style'); st.id = 'adom-3d-hint';+ if (FRAME && !document.getElementById('adom-3d-hint')) { const st = document.createElement('style'); st.id = 'adom-3d-hint'; st.textContent = '.viewer-container:not(.pivot-mode) .pivot-chip { display: none !important; }'; document.head.appendChild(st); } this._src = this.getAttribute('src'); this._poster = this.getAttribute('poster');@@ -89,6 +96,7 @@ .foot button { font-size: 12px; padding: 5px 10px; } .err { color: #f85149; } ::slotted([slot=v]) { position: absolute; inset: 0; }+ .stage iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; } </style> <div class="card">${h ? `<div class="h">${esc(h)}</div>` : ''}<div class="stage">${stage}</div> <div class="foot"><span>${esc(c || '')}</span><span class="sp"></span>${foot || ''}</div></div>`;@@ -103,6 +111,7 @@ async _load() { const go = this.shadowRoot.getElementById('go'); go.disabled = true; go.textContent = 'Loading...'; this._loading = true;+ if (!FRAME) return this._loadInline(); try { shimStorage(); shimBlobUrls(); const res = await fetch(new URL(this._src, location.href).href);@@ -120,6 +129,28 @@ console.warn(e); this._unload(); this._idle(String(e && e.message || e).slice(0, 160)); } finally { this._loading = false; } }+ // Inline: the wiki's same-origin viewer route renders the model (its page type comes from the page API).+ async _loadInline() {+ try {+ const u = new URL(this._src, location.href);+ const m = u.origin === location.origin && decodeURIComponent(u.pathname).match(/^\/([^/]+)\/([^/]+)\/render\/(.+\.glb)$/i);+ if (!m) throw new Error('src must be a .glb on a wiki page: /OWNER/SLUG/render/PATH.glb');+ const res = await fetch(`/api/pages/${encodeURIComponent(m[1])}/${encodeURIComponent(m[2])}`, { credentials: 'same-origin', headers: { Accept: 'application/json' } });+ if (!res.ok) throw new Error('could not find the page ' + m[1] + '/' + m[2] + ' (' + res.status + ')');+ const type = ((await res.json()).page || {}).type;+ if (!type) throw new Error('the page ' + m[1] + '/' + m[2] + ' has no type');+ // the viewer route's ?model= check addresses the repo by the internal type name (hardware pages are component repos)+ const view = `/viewer/3d/${encodeURIComponent(m[1])}/${encodeURIComponent(type === 'hardware' ? 'component' : type)}/${encodeURIComponent(m[2])}?model=${encodeURIComponent(m[3])}`;+ this._frame(`<iframe src="${esc(view)}" title="${esc(this.getAttribute('heading') || '3D model')}" allow="fullscreen"></iframe>`,+ '<button id="off" type="button">Unload</button>');+ this._viewer = { destroy() {} };+ this.shadowRoot.getElementById('off').addEventListener('click', () => this._unload());+ this.shadowRoot.querySelector('iframe').addEventListener('load', () =>+ this.dispatchEvent(new CustomEvent('modelready', { detail: { view }, bubbles: true, composed: true })), { once: true });+ } catch (e) {+ console.warn(e); this._unload(); this._idle(String(e && e.message || e).slice(0, 160));+ } finally { this._loading = false; }+ } _unload(why) { if (this._viewer) { try { this._viewer.destroy(); } catch (e) {} this._viewer = null; } if (this._host) { this._host.remove(); this._host = null; }
widgets/molecule-widget.js+4−1@@ -28,6 +28,9 @@ set data(d) { this._data = d; if (this.shadowRoot) this._render(); } get data() { return this._data; } async connectedCallback() {+ // A host page that pins its color-scheme (the wiki is dark-only) decides the theme; inline, prefers-color-scheme+ // would follow the reader's OS instead (a frame inherits the embedding page's scheme).+ if (!this.hasAttribute('theme')) { const cs = getComputedStyle(document.documentElement).colorScheme.trim(); if (/^(only )?(dark|light)( only)?$/.test(cs)) this.setAttribute('theme', cs.replace(/only|\s/g, '')); } if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' }); this._self = 'm' + Math.random().toString(36).slice(2);@@ -84,7 +87,7 @@ p.onmouseleave = clear; }); }- _post(ref) { const m = { type: 'lbr-hover', ref: ref, mpn: (this._data && this._data.name) || '', src: this._self }; try { this._bus && this._bus.postMessage(m); } catch (_) { /* */ } try { if (window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /* */ } }+ _post(ref) { const m = { type: 'lbr-hover', ref: ref, mpn: (this._data && this._data.name) || '', src: this._self }; try { this._bus && this._bus.postMessage(m); } catch (_) { /* */ } try { if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) window.parent.postMessage(m, '*'); } catch (_) { /* */ } } _css() { return `
widgets/pinout-widget.js+5−1@@ -97,6 +97,9 @@ get data() { return this._data; } async connectedCallback() {+ // A host page that pins its color-scheme (the wiki is dark-only) decides the theme; inline, prefers-color-scheme+ // would follow the reader's OS instead (a frame inherits the embedding page's scheme).+ if (!this.hasAttribute('theme')) { const cs = getComputedStyle(document.documentElement).colorScheme.trim(); if (/^(only )?(dark|light)( only)?$/.test(cs)) this.setAttribute('theme', cs.replace(/only|\s/g, '')); } if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' }); this._busInit();@@ -265,7 +268,8 @@ _emitBus(n) { const m = { type: 'lbr-hover', pin: (n === '' || n == null) ? null : String(n), mpn: (this._data && this._data.mpn) || '', src: this._self }; try { this._bus && this._bus.postMessage(m); } catch (_) { /*noop*/ }- try { if (window.parent && window.parent !== window) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ }+ // parent relay only from the wiki's sandboxed widget frame; inline, BroadcastChannel already reaches every widget+ try { if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) window.parent.postMessage(m, '*'); } catch (_) { /*noop*/ } } _wire(pins) {
widgets/prompt-widget.js+2−1@@ -22,7 +22,8 @@ connectedCallback() { if (this._init) return; this._init = true; this.attachShadow({ mode: 'open' });- if (window.self !== window.top) { document.documentElement.style.overflow = 'hidden'; document.body.style.margin = '0'; }+ // only in the wiki's sandboxed widget frame (/OWNER/SLUG/widget/NAME); inline, the document is the wiki page+ if (/^\/[^/]+\/[^/]+\/widget\/[^/]+\/?$/.test(location.pathname)) { document.documentElement.style.overflow = 'hidden'; document.body.style.margin = '0'; } this._text = this.getAttribute('text') || ''; this._render(this._text ? null : 'Loading prompt...'); if (!this._text && this.getAttribute('src')) this._fetch();