app
Adom Library
Public Made by Adomby adom
adom-lbr — the EDA library translator. Bring a component in from any supported EDA tool and convert it to any other (KiCad ⇄ Altium ⇄ EAGLE/Fusion) through one canonical adom-lbr JSON — symbol + footprint + 3D + pin↔pad map. Altium is written natively (no Altium needed). Includes the adom-lbr Manager: browse wiki components and view symbol/footprint/3D + the per-EDA layer stackup.
← Commit history
2.15.2: durable recall; module 2.1.2
5 files changed
+114−31
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-lbr"-version = "2.15.0"+version = "2.15.2" edition = "2021" description = "adom-lbr generator / EDA translator: the canonical adom-lbr JSON is the hub, with native converters to Altium (.SchLib/.PcbLib/.IntLib), EAGLE/Fusion (.lbr), and KiCad. Altium output is written natively — no Altium needed at conversion time."
VERSION+1−1@@ -1 +1 @@-2.15.0+2.15.2
src/manage.html+26−14@@ -811,21 +811,26 @@ function gradAiDriven(){ if(!gradState().aiDriven){gradSave({aiDriven:true});app function panelOpen(){ const g=gradState(),n=g.launches||0,dism=(g.dismissed||{}),ai=!!g.aiDriven; const allowed=settings?settings.show_say_strip!==false:true;- return gradRecall||(allowed&&!dism.say&&!ai&&n<5);+ if(gradRecall)return true; // brought back in THIS session+ if(!allowed)return false; // switched off in settings+ if(dism.say)return false; // the user closed it, and that is durable+ if(g.recalled)return true; // the user brought it back BEFORE, which also has to be+ // durable: graduation must not re-fold a panel someone+ // deliberately reopened, or the ? button looks broken+ // one reload later.+ return !ai&&n<5; // otherwise the policy decides } // The module owns the panel's open/closed rendering, its X, and the header toggle's // aria-pressed and label. What stays here is GRADUATION, which is this app's policy: after // enough green launches, or once the user drives by AI, the panel folds on its own. So this // function no longer touches the panel's DOM, it asks the handle. let npanel=null;-// Programmatic open/close must NOT look like a user decision. applyGraduation() drives the-// module, the module calls onToggle, and onToggle records intent: without this guard,-// graduation opening the panel would set gradRecall and pin it open forever, and graduation-// closing it would mark it user-dismissed. A feedback loop, not a toggle.-let npApplying=false; function applyGraduation(){ const open=panelOpen();- if(npanel){ npApplying=true; try{ open?npanel.open():npanel.close(); } finally { npApplying=false; } }+ // remember:false, because graduation deciding to fold the panel is not the user choosing+ // to dismiss it, and writing it to the stored preference would make a policy decision+ // permanent. The module tells onToggle which of us moved it (newbie-panel 2.1.0).+ if(npanel) open?npanel.open({remember:false}):npanel.close({remember:false}); // aria-pressed comes from the module; the VISIBLE difference is the app's own affordance, // because a toggle that looks identical in both states is a lie (input-feedback skill). document.getElementById('btnHelp').classList.toggle('on',open);@@ -857,14 +862,21 @@ async function mountNewbie(){ toggleButton:'#btnHelp', demo:{label:'Play the demo', meta:'13 steps, about 2 minutes', onPlay:()=>{tourPlaying=true;tourShow(0);}}, phrases:phrases,- onToggle:open=>{- // The module's X and the header toggle drive the same state; graduation has to hear- // about a USER close or the panel comes back on the next reload. Programmatic changes- // are skipped (see npApplying), because they are graduation talking to itself.+ onToggle:(open,source)=>{+ // aria-pressed, aria-label and the tooltip are the module's; the visible on-state is+ // this app's own affordance, and it tracks BOTH sources. document.getElementById('btnHelp').classList.toggle('on',open);- if(npApplying)return;- if(!open){ const g=gradState(); gradSave({dismissed:Object.assign(g.dismissed||{},{say:true})}); gradRecall=false; }- else { gradRecall=true; }+ // Only a HUMAN closing the panel counts as dismissing it. 'api' means graduation is+ // talking to itself, and recording that as intent pins the panel open (or shut)+ // forever. The module reports the source so this no longer needs a flag app-side.+ if(source!=='user')return;+ // A user decision is DURABLE, both ways, and it is written where panelOpen() reads it+ // so the module's stored preference and this app's policy cannot disagree after a+ // reload. gradRecall alone was session-only: reopening the panel worked, then+ // graduation folded it again on the next load and the control looked broken.+ const g=gradState();+ if(!open){ gradSave({dismissed:Object.assign(g.dismissed||{},{say:true}),recalled:false}); gradRecall=false; }+ else { gradSave({dismissed:Object.assign(g.dismissed||{},{say:false}),recalled:true}); gradRecall=true; } } }); applyGraduation();
src/newbie-panel.js+44−15@@ -154,6 +154,7 @@ function tipFor(node, text) { node.setAttribute('aria-label', text); node.removeAttribute('title'); // two tooltips on one control is a bug+ node.__anpSetTip = function (t) { text = t; if (tip && tip.classList.contains('on')) tip.textContent = t; }; node.addEventListener('mouseenter', function (e) { clearTimeout(tipT); tipT = setTimeout(function () {@@ -307,21 +308,32 @@ /* the header ? is a real TOGGLE, both ways, with aria-pressed and a label that swaps */ var toggleBtn = typeof cfg.toggleButton === 'string' ? document.querySelector(cfg.toggleButton) : cfg.toggleButton;- function setOpen(open, remember) {+ /* setOpen(open, remember, source)+ * source is 'user' when a person clicked the X or the header toggle, and 'api' when the+ * app called open()/close() itself. It is passed to onToggle, because an app with a+ * graduation policy calls open()/close() from inside its own onToggle handler: without a+ * source the callback cannot tell its own action from a person's, so graduating the panel+ * open pins it open forever and graduating it closed marks it user-dismissed. It looks+ * like "the panel sometimes ignores graduation". (lbr, 2026-08-25) */+ function setOpen(open, remember, source) { panel.hidden = !open; wrap.style.display = open ? '' : 'none'; /* no dead margin when collapsed */ if (toggleBtn) {+ var label = open ? 'Hide the sample prompts' : 'Show the sample prompts'; toggleBtn.setAttribute('aria-pressed', open ? 'true' : 'false');- toggleBtn.setAttribute('aria-label', open ? 'Hide the sample prompts' : 'Show the sample prompts');+ toggleBtn.setAttribute('aria-label', label);+ /* the TOOLTIP has to swap with the state too, or a person with the panel already in+ * front of them hovers the control and is told it will show them the panel */+ if (toggleBtn.__anpSetTip) toggleBtn.__anpSetTip(label); } if (remember !== false) { try { localStorage.setItem(key, open ? '1' : '0'); } catch (e) {} }- if (typeof cfg.onToggle === 'function') cfg.onToggle(open);+ if (typeof cfg.onToggle === 'function') cfg.onToggle(open, source || 'api'); }- x.addEventListener('click', function () { setOpen(false); });+ x.addEventListener('click', function () { setOpen(false, true, 'user'); }); if (toggleBtn) { if (!toggleBtn.innerHTML.trim()) toggleBtn.innerHTML = ICON.help; tipFor(toggleBtn, 'Show the sample prompts');- toggleBtn.addEventListener('click', function () { setOpen(panel.hidden); });+ toggleBtn.addEventListener('click', function () { setOpen(panel.hidden, true, 'user'); }); } var saved = null; try { saved = localStorage.getItem(key); } catch (e) {}@@ -345,8 +357,11 @@ } return {- panel: panel, open: function () { setOpen(true); }, close: function () { setOpen(false); },- toggle: function () { setOpen(panel.hidden); }, isOpen: function () { return !panel.hidden; },+ panel: panel,+ open: function (o) { setOpen(true, !(o && o.remember === false), 'api'); },+ close: function (o) { setOpen(false, !(o && o.remember === false), 'api'); },+ toggle: function () { setOpen(panel.hidden, true, 'api'); },+ isOpen: function () { return !panel.hidden; }, overBudget: over, toast: toast, selfCheck: function () { return selfCheck(panel); } }; }@@ -361,6 +376,10 @@ 'card padding': ['padding', '18px 20px 20px'], 'column gap': ['columnGap', '24px'] };+ /* selfCheck(panel) - pass the panel explicitly when the page mounts more than one, or+ * mounts into a shadow root. The no-arg form takes the FIRST .anp in the light DOM, which+ * is right for the single-panel case and silently wrong for the others. Returns+ * {ok:false, error} rather than throwing when there is no panel. (chip-fetcher, 2026-08-25) */ function selfCheck(panel) { panel = panel || document.querySelector('.anp'); if (!panel) return { ok: false, error: 'no .anp panel on the page' };@@ -374,13 +393,23 @@ } Object.keys(CANON).forEach(function (k) { var want = CANON[k][1];- if (want.indexOf('snap:') === 0) want = snap(parseFloat(want.slice(5)));- var got = cs[CANON[k][0]];- /* compare numerically with a sub-pixel tolerance: Chrome prints 0.666667px where the- * arithmetic gives 0.6666666666, and a string compare fails a panel that is correct */- var pass = (parseFloat(want) && parseFloat(got))- ? Math.abs(parseFloat(got) - parseFloat(want)) < 0.01- : got === want;+ var got = cs[CANON[k][0]], declared = CANON[k][1];+ /* Border widths need BOTH candidates accepted. Chrome reports the USED width, snapped+ * to whole device pixels, on some surfaces (ah's webview and pup on a dpr-1.5 laptop+ * both return 0.666667px for a declared 1px) and reports the DECLARED width on others+ * (an emulated dpr returns 1px at every ratio). Accepting only one of the two fails a+ * correct panel in the other environment, and at dpr 1.5 specifically, which is the+ * machine this fleet is reviewed on. Found by nb, 2026-08-25. */+ var pass;+ if (declared.indexOf('snap:') === 0) {+ var d = parseFloat(declared.slice(5)), g = parseFloat(got);+ pass = Math.abs(g - d) < 0.01 || Math.abs(g - parseFloat(snap(d))) < 0.01;+ want = d + 'px or ' + snap(d);+ } else {+ pass = (parseFloat(want) && parseFloat(got))+ ? Math.abs(parseFloat(got) - parseFloat(want)) < 0.01+ : got === want;+ } if (!pass) ok = false; rows.push({ what: k, want: want, got: got, pass: pass }); });@@ -399,6 +428,6 @@ return { ok: ok, rows: rows }; } - root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, version: '1.0.0',+ root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, version: '2.1.2', TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET }; })(typeof window !== 'undefined' ? window : this);
tools/page-smoke.js+42@@ -88,6 +88,48 @@ const EXPECT = { console.log('[smoke] FAIL javascript errors on load (including ones the page swallows):'); [...new Set(real)].slice(0, 6).forEach(e => console.log(' ' + e.slice(0, 160))); }+ // GRADUATION IS NOT DISMISSAL. The panel folding on its own after enough launches must+ // not be recorded as the user dismissing it, or the fold becomes permanent. The module+ // reports who moved the panel (newbie-panel 2.1.0 onToggle(open, source)); this asserts+ // this app reads it correctly, because the failure is silent and only shows up as a panel+ // that never comes back.+ try {+ const w = dom.window;+ const gradKey = Object.keys(w.localStorage).find(k => /graduation/.test(k));+ const readGrad = () => gradKey ? w.localStorage.getItem(gradKey) : '';+ // `let npanel` at script top level is NOT a window property, so w.npanel is undefined+ // and an `if (w.npanel)` guard skips this check in silence, which is the same silent+ // pass this whole file exists to abolish. Reach it through eval, in script scope.+ const hasHandle = w.eval("typeof npanel !== 'undefined' && !!npanel");+ if (!hasHandle) {+ bad++;+ console.log('[smoke] FAIL no panel handle in script scope: cannot verify graduation vs dismissal');+ } else {+ w.eval("npanel.close({remember:false})");+ const afterApi = readGrad();+ w.eval("npanel.open({remember:false})");+ const xBtn = d.querySelector('.anp-x');+ if (xBtn) xBtn.dispatchEvent(new w.MouseEvent('click', { bubbles: true }));+ const afterUser = readGrad();+ const apiClean = !/dismissed/.test(afterApi || '');+ const userRecorded = /dismissed/.test(afterUser || '');+ if (!apiClean) { bad++; console.log('[smoke] FAIL a programmatic close was recorded as a user dismissal'); }+ if (!userRecorded) { bad++; console.log('[smoke] FAIL a user close was NOT recorded, so the panel returns after dismissal'); }+ if (apiClean && userRecorded) console.log('[smoke] ok graduation close vs user dismissal are distinguished');+ // ...and a user REOPEN has to survive a reload. The setup matters: with a FRESH+ // profile the policy would keep the panel open anyway, so the check passes for the+ // wrong reason and cannot fail. (It did exactly that on its first run, and only the+ // negative control exposed it.) Graduate the user first, so the policy actively wants+ // the panel folded and only a durable recall can keep it open.+ w.eval("gradSave({launches:5})");+ const tgl = d.querySelector('#btnHelp');+ if (tgl) tgl.dispatchEvent(new w.MouseEvent('click', { bubbles: true }));+ const survives = w.eval("(function(){gradRecall=false;return panelOpen();})()");+ if (!survives) { bad++; console.log('[smoke] FAIL a user reopen does not survive a reload: graduation re-folds it'); }+ else console.log('[smoke] ok a user reopen survives a reload');+ }+ } catch (e) { console.log('[smoke] note: could not exercise the toggle: ' + e.message); }+ // The module proves its own conformance. jsdom has no layout engine, so the geometric // rows are meaningless here and only the structural verdict is worth reading; the real // selfCheck has to be run in ah's webview. Reported, never used as the pass condition.