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
<adom-states>: a component's visible states (adom/component-states@1), one model and picture per state
2 files changed
+97
README.md+1@@ -23,6 +23,7 @@ adom-wiki pkg install adom/component-widgets | `<adom-moleculize>` | Board → workcell-molecule preview (reads a dropped `.kicad_pcb` client-side, no backend) + AI-thread generate | drop a `.kicad_pcb`, or `{ name, width_mm, height_mm }` | | `<adom-verified>` | "Verified by" trust mark + verification log; amber when a verified file changes | `verification.json` (adom/verification@1), `tools/verify_log.py` | | `<adom-models>` | Every 3D file on the page: STEP / GLB tabs, top + underside previews, what is unique, freshness | `models.json` (adom/models@1), `tools/models_json.py` |+| `<adom-states>` | A component's visible states (an LED off and lit): a toggle, a picture of each state, when it applies, its STEP and GLB | `states.json` (adom/component-states@1) | | `<adom-footprint-variants>` | The footprint's variant family (heatsink vias, solder balls, rounded pads) | `variants.json`, `tools/make-variants.py` | Each is one custom element: Shadow DOM, no dependencies, light + dark (or
widgets/states-widget.jsadded+96@@ -0,0 +1,96 @@+/*!+ * <adom-states> - the visible states of a component (an LED off and on), one model per state.+ *+ * Drop-in (wiki README):+ * ::adom-states{src="/OWNER/SLUG/render/states.json"}+ *+ * Data contract (adom/component-states@1), committed in the component page's repo as states.json:+ * { "schema": "adom/component-states@1", "mpn": "IN-S63BTG", "kind": "led", "default": "off",+ * "states": [+ * { "id": "off", "label": "Off", "summary": "...", "models": { "step": "IN-S63BTG-uncoloured.step", "glb": "states/IN-S63BTG-off.glb" },+ * "image": "states/IN-S63BTG-state-off.png" },+ * { "id": "on", "label": "On (green)", "summary": "...", "models": { "step": "IN-S63BTG.step", "glb": "states/IN-S63BTG-on.glb" },+ * "image": "states/IN-S63BTG-state-on.png",+ * "look": { "lit": "the materials the off model does not have (the lens)", "emissive_strength": 1.3, "glow": "#2bd17e" },+ * "when": { "forward_current_mA": [0.1, 25], "text": "forward current through A to K above about 0.1 mA" } } ],+ * "electrical": { "vf_V": 3.2, "if_max_mA": 25, "lambda_d_nm": 525 } }+ * Every state names its own models (a viewer swaps them; a simulator shows the state a test expects) and, for the+ * page, a rendered image. Self-contained (Shadow DOM), the same brand tokens as <adom-models>.+ */+(() => {+ const esc = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));+ const KIND = { led: 'LED', relay: 'Relay', switch: 'Switch', display: 'Display', lamp: 'Lamp' };++ class AdomStates extends HTMLElement {+ async connectedCallback() {+ if (this._init) return; this._init = true;+ this.attachShadow({ mode: 'open' });+ const src = this.getAttribute('src');+ this._paint('<div class="empty">loading component states…</div>');+ if (!src) return this._paint('<div class="empty">no src: point me at a states.json</div>');+ const abs = new URL(src, location.href).href;+ this._base = abs.slice(0, abs.lastIndexOf('/') + 1);+ const m = abs.match(/^(https?:\/\/[^/]+)\/([^/]+)\/([^/]+)\/render\//);+ this._api = m ? `${m[1]}/api/pages/${m[2]}/${m[3]}` : null;+ try { this._d = await (await fetch(abs)).json(); }+ catch (e) { return this._paint('<div class="empty">could not load ' + esc(src) + '</div>'); }+ if (this._d.schema !== 'adom/component-states@1') return this._paint('<div class="empty">not an adom/component-states@1 file</div>');+ this._cur = this.getAttribute('state') || this._d.default || this._d.states?.[0]?.id;+ this._render();+ this.shadowRoot.addEventListener('click', e => {+ const b = e.target.closest('button[data-s]'); if (!b) return;+ this._cur = b.dataset.s; this._render();+ this.dispatchEvent(new CustomEvent('statechange', { detail: { state: this._cur }, bubbles: true, composed: true }));+ });+ }+ _abs(p) { return p ? new URL(p, this._base).href : ''; }+ _dl(p) { return this._api ? `${this._api}/files/${p.split('/').map(encodeURIComponent).join('/')}` : this._abs(p); }+ _render() {+ const d = this._d, st = (d.states || []).find(s => s.id === this._cur) || d.states?.[0] || {};+ const glow = st.look?.glow || null;+ const tabs = (d.states || []).map(s => `<button data-s="${esc(s.id)}" class="${s.id === st.id ? 'on' : ''}" aria-pressed="${s.id === st.id}">${esc(s.label || s.id)}</button>`).join('');+ const img = st.image ? `<img src="${esc(this._abs(st.image))}" alt="${esc(d.mpn)} ${esc(st.label || st.id)}" loading="lazy">` : `<div class="nothumb">no image for this state</div>`;+ const files = Object.entries(st.models || {}).map(([fmt, p]) => `<a class="file" href="${esc(this._dl(p))}" download="${esc(p.split('/').pop())}"><span class="chip fmt ${esc(fmt)}">${esc(fmt.toUpperCase())}</span><code>${esc(p.split('/').pop())}</code></a>`).join('');+ const e = d.electrical || {};+ const elec = [e.vf_V != null && `Vf ${e.vf_V} V`, e.if_max_mA != null && `If max ${e.if_max_mA} mA`, e.lambda_d_nm != null && `λd ${e.lambda_d_nm} nm`].filter(Boolean).join(' · ');+ this._paint(`<div class="h">States<span class="part">${esc(d.mpn || '')} · ${esc(KIND[d.kind] || d.kind || 'component')}</span></div>+ <div class="sub">One 3D model per state, so a viewer or a simulation shows the part as it looks ${esc(d.kind === 'led' ? 'off and lit' : 'in each state')}.</div>+ <div class="tabs" role="group" aria-label="State">${tabs}</div>+ <div class="stage${glow ? ' lit' : ''}" style="${glow ? `--glow:${esc(glow)}` : ''}">${img}<span class="vlabel">${esc(st.label || st.id)}${st.id === d.default ? ' · default' : ''}</span></div>+ <div class="body">+ ${st.summary ? `<div class="sum">${esc(st.summary)}</div>` : ''}+ ${st.when?.text ? `<div class="when"><b>When:</b> ${esc(st.when.text)}</div>` : ''}+ ${files ? `<div class="files">${files}</div>` : ''}+ ${elec ? `<div class="meta">${esc(elec)}</div>` : ''}+ </div>`);+ }+ _paint(inner) { this.shadowRoot.innerHTML = `<style>${this._css()}</style><div class="wrap">${inner}</div>`; }+ _css() {+ return `+ :host{display:block;--bg:#161b22;--panel:#0d1117;--line:#30363d;--text:#e6edf3;--body:#c9d1d9;--dim:#8b949e;+ --accent:#00b8b1;--accent-bright:#00e6dc;--blue:#64abff;+ --prose:var(--font-body,Satoshi,-apple-system,'Segoe UI',sans-serif);--display:var(--font-display,'Familjen Grotesk',Satoshi,sans-serif);+ --mono:var(--font-mono,'JetBrains Mono',Consolas,ui-monospace,monospace)}+ .wrap{background:var(--bg);border:1px solid var(--line);border-radius:14px;padding:16px;color:var(--text);font:14px/1.5 var(--prose);max-width:560px}+ .empty{color:var(--dim);padding:8px}+ .h{font:700 21px/1.2 var(--display);letter-spacing:-.01em}.part{font:500 14px var(--prose);color:var(--dim);margin-left:8px}+ .sub{color:var(--dim);margin:2px 0 10px;font-size:13px}+ .tabs{display:flex;gap:6px;margin:0 0 10px}.tabs button{font:600 13px var(--prose);color:var(--body);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 14px;cursor:pointer}+ .tabs button.on{color:#05221f;background:var(--accent);border-color:var(--accent)}+ .stage{position:relative;background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden;aspect-ratio:1/1;max-height:320px;display:flex;align-items:center;justify-content:center}+ .stage img{width:100%;height:100%;object-fit:contain;display:block}+ .stage.lit{box-shadow:inset 0 0 60px -18px var(--glow)}+ .vlabel{position:absolute;left:8px;top:6px;font-size:11px;color:var(--dim);background:rgba(13,17,23,.7);padding:0 6px;border-radius:6px}+ .nothumb{color:var(--dim);font-size:12px}+ .body{display:flex;flex-direction:column;gap:6px;margin-top:10px}+ .sum{color:var(--body)}.when{color:var(--body);font-size:13px}.when b{color:var(--text)}+ .files{display:flex;flex-wrap:wrap;gap:6px 14px}.file{display:inline-flex;gap:6px;align-items:center;text-decoration:none;color:var(--body)}+ code{font-family:var(--mono);font-size:12px;color:var(--body)}+ .chip{font-size:11px;padding:1px 7px;border-radius:999px;border:1px solid var(--line);color:var(--body)}+ .chip.fmt.step{color:var(--accent);border-color:var(--accent)}.chip.fmt.glb{color:var(--blue);border-color:var(--blue)}+ .meta{font-size:12px;color:var(--dim)}+ a{color:var(--accent)}`;+ }+ }+ if (!customElements.get('adom-states')) customElements.define('adom-states', AdomStates);+})();