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 + footp
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>adom-lbr Manager</title>
<script type="module" src="https://unpkg.com/@google/[email protected]/dist/model-viewer.min.js"></script>
<style>
:root{--bg:#0f1115;--panel:#171a21;--edge:#252a34;--ink:#e6e9ef;--mut:#8b93a3;--acc:#6ea8fe}
*{box-sizing:border-box}
body{margin:0;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;background:var(--bg);color:var(--ink);height:100vh;display:flex}
#list{width:300px;min-width:300px;border-right:1px solid var(--edge);background:var(--panel);display:flex;flex-direction:column}
#list h1{font-size:15px;margin:0;padding:14px 16px;border-bottom:1px solid var(--edge);letter-spacing:.02em}
#list h1 small{color:var(--mut);font-weight:400}
#refresh{float:right;background:#0d0f14;border:1px solid var(--edge);color:var(--ink);border-radius:6px;width:26px;height:24px;font-size:14px;cursor:pointer;line-height:1}
#refresh:hover{background:#243049;border-color:var(--acc)}
#crumb{padding:7px 16px 0;font-size:12px;color:var(--mut)}
#crumb a{color:var(--acc);text-decoration:none}
#crumb a:hover{text-decoration:underline}
#crumb .cactive{color:var(--ink);font-weight:600}
.row .tag{display:inline-block;font-size:10px;background:#243049;color:var(--acc);border-radius:4px;padding:1px 5px;margin-left:6px;vertical-align:1px}
.cathead{padding:7px 16px 5px;font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--acc);background:#12151c;border-bottom:1px solid var(--edge);font-weight:700;position:sticky;top:0;z-index:1}
.pubsearch{margin-top:6px;border-top:1px solid var(--edge)}
.pubsearch .pubrow{display:flex;gap:8px;margin:8px 12px}
.pubsearch input{flex:1;min-width:0;background:#0d0f14;border:1px solid var(--edge);color:var(--ink);border-radius:8px;padding:9px 10px;font-size:13px}
.pubsearch input:focus{outline:none;border-color:var(--acc)}
.pubsearch button{background:var(--acc);color:#08111f;border:none;border-radius:8px;padding:0 13px;font-size:19px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center}
.pubsearch button:hover{filter:brightness(1.1)}
.row .selbox{margin-right:9px;vertical-align:middle;cursor:pointer}
#toolbar{border-bottom:1px solid var(--edge);padding:8px 12px;display:flex;flex-direction:column;gap:7px}
#toolbar .selall{font-size:12px;color:var(--mut);cursor:pointer;user-select:none;display:flex;align-items:center;gap:6px}
#toolbar .tbtns{display:flex;gap:7px}
#toolbar button{flex:1;background:#1a2130;border:1px solid var(--edge);color:var(--ink);border-radius:7px;padding:7px 8px;font-size:12px;cursor:pointer}
#toolbar button:hover{border-color:var(--acc);background:#22304a}
#toolbar button.primary{background:var(--acc);color:#08111f;border-color:var(--acc);font-weight:600}
#toolbar button:disabled{opacity:.45;cursor:not-allowed}
.modal-overlay{position:fixed;inset:0;background:#000a;display:flex;align-items:center;justify-content:center;z-index:1000}
.modal{background:var(--panel);border:1px solid var(--edge);border-radius:14px;width:min(760px,92vw);max-height:88vh;overflow:auto;padding:22px 24px;box-shadow:0 20px 60px #000a}
.modal h2{margin:0 0 4px;font-size:18px}
.modal .msub{color:var(--mut);font-size:13px;margin-bottom:16px}
.modal .opts{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px}
.modal .opt{border:1px solid var(--edge);border-radius:9px;padding:9px 13px;cursor:pointer;font-size:13px;display:flex;gap:8px;align-items:center}
.modal .opt.sel{border-color:var(--acc);background:#1a2740}
.modal table{width:100%;margin:0 0 6px}
.modal .sure{margin:18px 0 10px;font-size:15px;font-weight:600}
.modal-actions{display:flex;gap:10px;justify-content:flex-end}
.modal-actions button{background:#1a2130;border:1px solid var(--edge);color:var(--ink);border-radius:8px;padding:9px 18px;font-size:14px;cursor:pointer}
.modal-actions button.primary{background:var(--acc);color:#08111f;border-color:var(--acc);font-weight:700}
.modal .lib{font:12px ui-monospace,monospace;color:#7ee0c0}
.modal .note{background:#12151c;border:1px solid var(--edge);border-radius:8px;padding:10px 12px;font-size:12px;color:var(--mut);margin:6px 0 4px}
#modal-dest{margin:0 0 16px}
#modal-dest label{display:block;font-size:11px;color:var(--mut);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em}
#modal-dest .destrow{display:flex;gap:8px}
#modal-dest select,#modal-dest input{background:#0d0f14;border:1px solid var(--edge);color:var(--ink);border-radius:8px;padding:8px 10px}
#modal-dest select{min-width:200px}
#modal-dest input{flex:1;font:12px ui-monospace,monospace}
#modal-dest input:focus,#modal-dest select:focus{outline:none;border-color:var(--acc)}
#refresh.spin{animation:sp .6s linear infinite}
@keyframes sp{to{transform:rotate(360deg)}}
#q{margin:10px;padding:8px 10px;background:#0d0f14;border:1px solid var(--edge);border-radius:8px;color:var(--ink)}
#rows{overflow:auto;flex:1}
.row{padding:10px 16px;border-bottom:1px solid var(--edge);cursor:pointer}
.row:hover{background:#1e222b}
.row.sel{background:#243049;border-left:3px solid var(--acc);padding-left:13px}
.row .mpn{font-weight:600}
.row .sub{color:var(--mut);font-size:12px}
#view{flex:1;overflow:auto;padding:18px}
#hint{color:var(--mut);text-align:center;margin-top:20vh}
.cols{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px}
.card{background:var(--panel);border:1px solid var(--edge);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
.card h3{margin:0;padding:9px 12px;font-size:12px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);border-bottom:1px solid var(--edge)}
.card .body{aspect-ratio:1;width:100%;display:flex;align-items:center;justify-content:center;padding:10px;background:#161b22;overflow:hidden;cursor:grab;touch-action:none;position:relative}
.card .body svg{max-width:100%;max-height:100%;will-change:transform}
.card .hint2{position:absolute;bottom:6px;right:8px;font-size:10px;color:#b9c0cc;background:#0d0f14aa;border-radius:5px;padding:1px 6px;pointer-events:none}
.lfilter{display:flex;flex-wrap:wrap;gap:6px;padding:8px 10px;border-bottom:1px solid var(--edge)}
.chip{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--ink);background:#0d0f14;border:1px solid var(--edge);border-radius:6px;padding:3px 7px;cursor:pointer;user-select:none}
.chip input{margin:0;cursor:pointer}
.chip .sw{width:9px;height:9px;border-radius:2px;display:inline-block}
.chip:has(input:not(:checked)){opacity:.4;text-decoration:line-through}
model-viewer{width:100%;aspect-ratio:1;height:auto;background:#161b22}
h2.title{margin:0 0 14px}
h2.title small{color:var(--mut);font-weight:400;font-size:14px}
table{width:100%;border-collapse:collapse;margin-top:16px;background:var(--panel);border:1px solid var(--edge);border-radius:12px;overflow:hidden}
th,td{text-align:left;padding:10px 14px;border-bottom:1px solid var(--edge);font-size:13px}
th{color:var(--mut);font-weight:600;text-transform:uppercase;font-size:11px;letter-spacing:.05em}
tr:last-child td{border-bottom:none}
.swatch{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:7px;vertical-align:-1px}
.badge{font:12px ui-monospace,monospace;background:#0d0f14;border:1px solid var(--edge);border-radius:6px;padding:2px 7px}
.stacktitle{margin:22px 0 6px;font-size:13px;text-transform:uppercase;letter-spacing:.05em;color:var(--mut)}
.err{color:#f87171;padding:16px}
.card .body [data-num]{cursor:pointer}
.pinhi .pinvis{stroke:#6ea8fe !important;vector-effect:non-scaling-stroke;stroke-width:3}
rect.pad.pinhi{stroke:#6ea8fe;vector-effect:non-scaling-stroke;stroke-width:3}
#tip{position:fixed;z-index:9999;background:#0d1117;border:1px solid var(--acc);color:var(--ink);font:12px ui-monospace,monospace;padding:3px 8px;border-radius:6px;pointer-events:none;display:none;white-space:nowrap}
#tip b{color:var(--acc)}
</style>
</head>
<body>
<aside id="list">
<h1>adom-lbr <small>Manager</small><button id="refresh" title="refresh list (fetch newly-pushed boards)">↻</button></h1>
<div id="crumb"></div>
<input id="q" placeholder="filter boards…">
<div id="toolbar" style="display:none">
<label class="selall"><input type="checkbox" id="selall"> <span id="selcount">select all</span></label>
<div class="tbtns">
<button id="btn-addlib" disabled>Add to Library</button>
<button id="btn-addproj">Add Project ↓</button>
</div>
</div>
<div id="rows"></div>
</aside>
<main id="view"><div id="hint">← pick a board, then a component, to open its viewer</div></main>
<div id="modal" class="modal-overlay" style="display:none"><div class="modal">
<h2 id="modal-title"></h2><div class="msub" id="modal-sub"></div>
<div class="opts" id="modal-opts"></div>
<div id="modal-dest"></div>
<div id="modal-preview"></div>
<div class="sure">Are you sure this is good?</div>
<div class="modal-actions"><button id="modal-cancel">Cancel</button><button class="primary" id="modal-go">Yes, add it</button></div>
</div></div>
<script>
const COLORS={Silkscreen:'#d8b4fe',Courtyard:'#4ade80','Fabrication / Assembly':'#f472b6',Copper:'#f59e0b',Other:'#94a3b8'};
// Board-centric navigation: Boards (tagged board/molecule) ▸ a board's component
// dependencies ▸ the symbol/footprint/3D viewer.
let all=[], bySlug={}, boardSlug=null, curDeps=[], me='', pubQuery='';
const isBoard=c=>{const t=(c.tags||[]).map(x=>(''+x).toLowerCase());return t.includes('board')||t.includes('molecule');};
const qval=()=>document.getElementById('q').value.toLowerCase();
const rowsEl=()=>document.getElementById('rows');
function makeRow(slug,title,sub,onclick,tag,sel){
const d=document.createElement('div');d.className='row';d.dataset.slug=slug;
const cb=sel?`<input type="checkbox" class="selbox"${selected.has(slug)?' checked':''}>`:'';
d.innerHTML=`<div class="mpn">${cb}${title}${tag?`<span class="tag">${tag}</span>`:''}</div><div class="sub">${sub}</div>`;
d.onclick=(e)=>{ if(e.target.classList.contains('selbox')){toggleSel(slug,e.target.checked);e.stopPropagation();return;} onclick(slug,d); };
return d;
}
// component-type → the user's existing KiCad symbol library (fallback JLC_Misc_BLT)
const TYPE_TO_SYMLIB={'Capacitors':'JLC_Capacitors_BLT','Resistors':'JLC_Resistors_BLT','MOSFETs':'JLC_FETs_BLT','Transistors':'JLC_FETs_BLT','Inductors':'JLC_Inductors_BLT','LEDs':'JLC_LEDs_BLT','Diodes':'JLC_Diodes_BLT','Connectors':'JLC_Connectors_BLT','Microcontrollers':'JLC_MCUs_BLT','Power ICs':'JLC_Power_BLT','Crystals & Oscillators':'JLC_Oscillators_BLT','ICs':'JLC_Misc_BLT','Other':'JLC_Misc_BLT'};
const selected=new Set();
function symLibFor(c){return TYPE_TO_SYMLIB[typeOf(c)]||'JLC_Misc_BLT';}
function toggleSel(slug,on){on?selected.add(slug):selected.delete(slug);updateToolbar();}
function updateToolbar(){
const n=selected.size;
const cnt=document.getElementById('selcount'); if(cnt)cnt.textContent=n?`${n} selected · clear`:'select all';
const b=document.getElementById('btn-addlib'); if(b){b.disabled=n===0;b.textContent=n?`Add ${n} to Library`:'Add to Library';}
const sa=document.getElementById('selall'); if(sa)sa.checked=curDeps.length>0&&curDeps.every(s=>selected.has(s));
}
async function loadList(fresh){
const btn=document.getElementById('refresh'); if(btn&&fresh)btn.classList.add('spin');
if(!fresh) rowsEl().innerHTML='<div class="row" style="color:#8b93a3;cursor:default">loading boards from wiki…</div>';
try{
const r=await fetch('api/components'+(fresh?'?refresh=1':'')).then(r=>r.json());
all=r.components||[]; me=r.me||''; bySlug={}; all.forEach(c=>bySlug[c.slug]=c);
render();
}catch(e){
rowsEl().innerHTML='<div class="row" style="color:#f87171;cursor:default">failed to load — retry</div>';
}finally{ if(btn)btn.classList.remove('spin'); }
}
function render(){ boardSlug?renderDeps():renderBoards(); }
function renderBoards(){
document.getElementById('toolbar').style.display='none';selected.clear();
document.getElementById('crumb').innerHTML='<span class="cactive">Projects</span>';
document.getElementById('q').placeholder='filter projects…';
const q=qval(),el=rowsEl();el.innerHTML='';
const boards=all.filter(isBoard);
const mineAll=boards.filter(c=>c.owner&&c.owner===me);
const pubAll=boards.filter(c=>!(c.owner&&c.owner===me));
// My projects — a normal list (filtered by the top box)
const mine=mineAll.filter(c=>!q||c.slug.includes(q)||(c.title||'').toLowerCase().includes(q));
const h=document.createElement('div');h.className='cathead';h.textContent=`My projects · ${mine.length}`;el.appendChild(h);
if(!mine.length){const d=document.createElement('div');d.className='row';d.style.cssText='color:#8b93a3;cursor:default;line-height:1.5';d.innerHTML='none yet — push a board tagged <b>board</b>/<b>molecule</b> to your wiki, then press ↻';el.appendChild(d);}
mine.forEach(c=>el.appendChild(makeRow(c.slug,c.title||c.slug,c.slug,openBoard,'board')));
// Public projects — type a query, press Enter or the button to load matches
const wrap=document.createElement('div');wrap.className='pubsearch';
wrap.innerHTML=`<div class="cathead">Public projects</div><div class="pubrow"><input id="pubq" placeholder="search public projects"><button id="pubgo" title="search">⏎</button></div><div id="pubresults"></div>`;
el.appendChild(wrap);
const pubq=document.getElementById('pubq');pubq.value=pubQuery;
const renderPub=()=>{
const pr=document.getElementById('pubresults');pr.innerHTML='';const pq=pubQuery.trim().toLowerCase();
if(!pq)return;
const m=pubAll.filter(c=>c.slug.includes(pq)||(c.title||'').toLowerCase().includes(pq)||(c.brief||'').toLowerCase().includes(pq));
if(!m.length){pr.innerHTML=`<div class="row" style="color:#8b93a3;cursor:default">no public projects match “${pubQuery.trim()}”</div>`;return;}
m.forEach(c=>pr.appendChild(makeRow(c.slug,c.title||c.slug,c.slug,openBoard,'board')));
};
const doPub=()=>{pubQuery=pubq.value;renderPub();};
pubq.onkeydown=e=>{if(e.key==='Enter')doPub();};
document.getElementById('pubgo').onclick=doPub;
renderPub();
}
async function openBoard(slug){
boardSlug=slug; curDeps=[]; selected.clear();
const title=(bySlug[slug]||{}).title||slug;
document.getElementById('crumb').innerHTML=`<a href="#" onclick="backToBoards();return false">← Boards</a> / <span class="cactive">${title}</span>`;
rowsEl().innerHTML='<div class="row" style="color:#8b93a3;cursor:default">loading dependencies…</div>';
try{ const r=await fetch('api/board/'+slug).then(r=>r.json()); curDeps=(r.deps||[]).map(d=>d.slug); }
catch(e){ curDeps=[]; }
renderDeps();
}
// Infer a component's type from its tags + brief + title (most parts carry no
// explicit category, so keywords in the brief are the signal).
const TYPE_RULES=[
[/microcontroller|\bmcu\b|\bmpu\b|\bsoc\b/,'Microcontrollers'],
[/regulator|\bldo\b|dc-?dc|buck|boost|power management|\bpmic\b/,'Power ICs'],
[/mosfet|\bfet\b/,'MOSFETs'],
[/transistor|\bbjt\b|\bnpn\b|\bpnp\b/,'Transistors'],
[/\bled\b/,'LEDs'],
[/diode|schottky|zener|\btvs\b|rectifier/,'Diodes'],
[/mlcc|capacitor|\bcap\b|tantalum/,'Capacitors'],
[/resistor|thick film|thin film/,'Resistors'],
[/inductor|chip coil|\bcoil\b|ferrite|choke|\bbead\b/,'Inductors'],
[/crystal|oscillator|resonator/,'Crystals & Oscillators'],
[/connector|header|receptacle|socket|\bjack\b|\busb\b/,'Connectors'],
[/switch|button|tactile/,'Switches'],
[/\bfuse\b/,'Fuses'],
[/amplifier|op-?amp|\bdriver\b|sensor|\badc\b|\bdac\b|transceiver|controller|\bgate\b|logic|\bic\b/,'ICs'],
];
const TYPE_ORDER=['Microcontrollers','ICs','Power ICs','MOSFETs','Transistors','Diodes','LEDs','Resistors','Capacitors','Inductors','Crystals & Oscillators','Connectors','Switches','Fuses','Other'];
function typeOf(c){
const hay=(((c.tags||[]).join(' '))+' '+(c.brief||'')+' '+(c.title||'')+' '+(c.slug||'')).toLowerCase();
for(const [re,label] of TYPE_RULES) if(re.test(hay)) return label;
return 'Other';
}
function renderDeps(){
document.getElementById('q').placeholder='filter components…';
document.getElementById('toolbar').style.display='flex';
const q=qval(),el=rowsEl();el.innerHTML='';
if(!curDeps.length){el.innerHTML='<div class="row" style="color:#8b93a3;cursor:default">this board lists no component dependencies</div>';updateToolbar();return;}
const items=curDeps.map(s=>({slug:s,c:bySlug[s]||{slug:s}}))
.filter(x=>!q||x.slug.includes(q)||((x.c.title||'').toLowerCase().includes(q))||((x.c.brief||'').toLowerCase().includes(q)));
const groups={};
items.forEach(x=>{const t=typeOf(x.c);(groups[t]=groups[t]||[]).push(x);});
const cats=Object.keys(groups).sort((a,b)=>{const ia=TYPE_ORDER.indexOf(a),ib=TYPE_ORDER.indexOf(b);return (ia<0?99:ia)-(ib<0?99:ib)||a.localeCompare(b);});
cats.forEach(cat=>{
const h=document.createElement('div');h.className='cathead';h.textContent=`${cat} · ${groups[cat].length}`;el.appendChild(h);
groups[cat].forEach(x=>{const on=bySlug[x.slug];el.appendChild(makeRow(x.slug,x.c.title||x.slug,on?x.slug:x.slug+' · not on wiki',openPart,null,true));});
});
updateToolbar();
}
function backToBoards(){boardSlug=null;curDeps=[];document.getElementById('q').value='';document.querySelector('#view #hint')||(document.getElementById('view').innerHTML='<div id="hint">← pick a board, then a component, to open its viewer</div>');render();}
async function openPart(slug,el){
document.querySelectorAll('.row').forEach(r=>r.classList.remove('sel'));
if(el)el.classList.add('sel');
const v=document.getElementById('view');
v.innerHTML='<div id="hint">loading '+slug+'…</div>';
const p=await fetch('api/part/'+slug).then(r=>r.json());
if(p.status!=='ok'){v.innerHTML='<div class="err">'+(p.error||'failed')+'</div>';return;}
const glb=p.glb?`<model-viewer src="glb/${slug}" camera-controls auto-rotate ar shadow-intensity="1"></model-viewer>`:'<div class="body" style="color:#888;background:#0d0f14">no 3D model</div>';
let rows=(p.layers||[]).map(l=>`<tr>
<td><span class="swatch" style="background:${COLORS[l.role]||'#888'}"></span>${l.role}<span style="color:#8b93a3"> ×${l.count}</span></td>
<td><span class="badge">${l.kicad}</span></td>
<td><span class="badge">${l.altium}</span></td></tr>`).join('');
v.innerHTML=`
<h2 class="title">${p.mpn} <small>· ${p.pins} pins · ${p.pads} pads</small></h2>
<div class="cols">
<div class="card"><h3>Schematic Symbol</h3><div class="body">${p.symbol_svg}<span class="hint2">drag · scroll to zoom</span></div></div>
<div class="card"><h3>Footprint</h3>
<div class="lfilter">${(p.layers||[]).map(l=>`<label class="chip"><input type="checkbox" checked data-key="${l.key}"><span class="sw" style="background:${COLORS[l.role]||'#888'}"></span>${l.role}</label>`).join('')}</div>
<div class="body" id="fpbody">${p.footprint_svg}<span class="hint2">drag · scroll to zoom</span></div></div>
<div class="card"><h3>3D Model</h3>${glb}</div>
</div>
<div class="stacktitle">Footprint layer stackup — what each element becomes per EDA tool</div>
<table><thead><tr><th>Element (neutral role)</th><th>KiCad layer</th><th>Altium layer</th></tr></thead>
<tbody>${rows}</tbody></table>`;
v.querySelectorAll('.card .body').forEach(makePannable);
// footprint layer filter — toggle visibility of each layer's SVG elements
v.querySelectorAll('.lfilter input').forEach(cb=>{
cb.onchange=()=>{
v.querySelectorAll('#fpbody svg .ly-'+cb.dataset.key).forEach(el=>{el.style.display=cb.checked?'':'none';});
};
});
setupHighlight(v);
}
// Hover a pin (symbol) or pad (footprint) → highlight it + show its number and
// name/description in a tooltip. Pins/pads carry data-num + data-name.
function setupHighlight(v){
let tip=document.getElementById('tip');
if(!tip){tip=document.createElement('div');tip.id='tip';document.body.appendChild(tip);}
const clear=box=>box.querySelectorAll('.pinhi').forEach(x=>x.classList.remove('pinhi'));
v.querySelectorAll('.card .body').forEach(box=>{
box.addEventListener('pointermove',e=>{
const hit=e.target.closest('[data-num]');
clear(box);
if(hit){
hit.classList.add('pinhi');
const num=hit.dataset.num, nm=hit.dataset.name;
const kind=hit.closest('#fpbody')?'Pad':'Pin';
tip.innerHTML=(nm&&nm!=='~')?`<b>${kind} ${num}</b> · ${nm}`:`<b>${kind} ${num}</b>`;
tip.style.display='block';
tip.style.left=(e.clientX+14)+'px';
tip.style.top=(e.clientY+14)+'px';
} else { tip.style.display='none'; }
});
box.addEventListener('pointerleave',()=>{clear(box);tip.style.display='none';});
});
}
// Drag-to-pan + scroll-to-zoom on an SVG viewer box (pointer capture so a drag
// that leaves the box keeps tracking). Re-assigning on* handlers avoids stacking.
function makePannable(box){
const svg=box.querySelector('svg');
if(!svg) return;
let tx=0,ty=0,scale=1,sx=0,sy=0,drag=false;
const apply=()=>{svg.style.transform=`translate(${tx}px,${ty}px) scale(${scale})`;};
box.onpointerdown=e=>{drag=true;sx=e.clientX-tx;sy=e.clientY-ty;box.setPointerCapture(e.pointerId);box.style.cursor='grabbing';};
box.onpointermove=e=>{if(drag){tx=e.clientX-sx;ty=e.clientY-sy;apply();}};
box.onpointerup=box.onpointercancel=()=>{drag=false;box.style.cursor='grab';};
box.onwheel=e=>{e.preventDefault();const f=e.deltaY<0?1.12:0.89;scale=Math.max(0.25,Math.min(12,scale*f));apply();};
box.ondblclick=()=>{tx=ty=0;scale=1;apply();};
}
// ---- selection controls + add-to-library / add-project modal ----
document.getElementById('selall').onchange=(e)=>{const on=e.target.checked;curDeps.forEach(s=>on?selected.add(s):selected.delete(s));renderDeps();};
function previewTable(plan){
return `<table><thead><tr><th>Component</th><th>Type</th><th>Symbol → library</th><th>Footprint → library</th></tr></thead><tbody>${
plan.map(p=>`<tr><td>${p.title}</td><td>${p.type}</td><td><span class="lib">${p.symLib}.kicad_sym</span></td><td><span class="lib">${p.fpLib}.pretty</span></td></tr>`).join('')}</tbody></table>`;
}
function buildPlan(slugs,projectLib,projName){
return slugs.map(s=>{const c=bySlug[s]||{slug:s,title:s};const t=typeOf(c);
return {slug:s,title:c.title||s,type:t,symLib:projectLib?projName:symLibFor(c),fpLib:projectLib?projName:'adom_lbr'};});
}
let modalGo=null;
const showModal=()=>document.getElementById('modal').style.display='flex';
const hideModal=()=>{document.getElementById('modal').style.display='none';modalGo=null;const b=document.getElementById('applybar');if(b)b.remove();};
document.getElementById('modal-cancel').onclick=hideModal;
document.getElementById('modal-go').onclick=()=>{if(modalGo)modalGo();};
function selOpt(sel,attr,cb){document.querySelectorAll('#modal-opts .opt').forEach(o=>o.onclick=()=>{document.querySelectorAll('#modal-opts .opt').forEach(x=>x.classList.remove('sel'));o.classList.add('sel');o.querySelector('input').checked=true;cb(o.dataset[attr]);});}
// recommended destination parents for delivered projects (editable in the box)
const DEST_FOLDERS=['/home/barrett-land/Documents/KiCAD/Projects','/home/barrett-land/Documents/Adom/Molecules/Other Molecules','/home/barrett-land/Documents/Adom/Electrical','/home/barrett-land/Downloads'];
function openAddLib(){
const slugs=[...selected]; if(!slugs.length)return;
const plan=buildPlan(slugs,false);
document.getElementById('modal-dest').innerHTML='';
document.getElementById('modal-title').textContent='Add to Library';
document.getElementById('modal-sub').textContent=`${slugs.length} component(s) → your existing KiCad libraries`;
document.getElementById('modal-opts').innerHTML=`<div class="opt sel" data-eda="kicad"><input type="radio" name="eda" checked> KiCad</div><div class="opt" data-eda="altium"><input type="radio" name="eda"> Altium (soon)</div>`;
const go=document.getElementById('modal-go');
selOpt('eda','eda',v=>{document.getElementById('modal-preview').innerHTML=v==='altium'?'<div class="note">Altium add-to-library isn\'t wired up yet — pick KiCad for now.</div>':previewTable(plan);go.disabled=v==='altium';});
document.getElementById('modal-preview').innerHTML=previewTable(plan);
go.disabled=false;go.textContent='Yes, add it';
modalGo=()=>doApply(plan,'kicad',false);
showModal();
}
function openAddProj(){
const slugs=curDeps.slice();
const projName=((bySlug[boardSlug]||{}).title||boardSlug).replace(/[^A-Za-z0-9_-]+/g,'_');
let mode='personal';
document.getElementById('modal-title').textContent='Add Project to my Computer';
document.getElementById('modal-sub').textContent=`schematic + ${slugs.length} component(s) — symbols & footprints resolved so it opens clean`;
document.getElementById('modal-opts').innerHTML=`<div class="opt sel" data-mode="personal"><input type="radio" name="mode" checked> Integrate into my libraries</div><div class="opt" data-mode="project"><input type="radio" name="mode"> Project-only library</div>`;
// editable destination + recommended-folder presets
document.getElementById('modal-dest').innerHTML=`<label>Deliver project to</label><div class="destrow"><select id="destsel">${DEST_FOLDERS.map(f=>`<option value="${f}">${f.replace('/home/barrett-land','~')}</option>`).join('')}</select><input id="destpath" spellcheck="false"></div>`;
const destpath=document.getElementById('destpath'), destsel=document.getElementById('destsel');
destpath.value=DEST_FOLDERS[0]+'/'+boardSlug;
destsel.onchange=()=>{destpath.value=destsel.value+'/'+boardSlug;};
const draw=()=>{
const plan=buildPlan(slugs,mode==='project',projName);
document.getElementById('modal-preview').innerHTML=`<div class="note">${mode==='project'?'A self-contained <b>'+projName+'</b> symbol + footprint library is created for this project — your personal libraries stay untouched.':'Symbols merge into your existing per-type libraries; footprints go to <b>adom_lbr.pretty</b>. The schematic library references get registered so nothing shows as broken.'}</div>`+previewTable(plan);
modalGo=()=>doApply(plan,'kicad',mode==='project',boardSlug,document.getElementById('destpath').value);
};
selOpt('mode','mode',v=>{mode=v;draw();});
document.getElementById('modal-go').disabled=false;document.getElementById('modal-go').textContent='Yes, add the project';
draw();showModal();
}
async function doApply(plan,eda,project,board,dest){
const go=document.getElementById('modal-go');go.disabled=true;go.textContent='Starting…';
const opts=document.getElementById('modal-opts');
const start=await fetch('api/apply',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({target:eda,project:!!project,board:board||null,dest:dest||'',items:plan.map(p=>({slug:p.slug,symLib:p.symLib,fpLib:p.fpLib}))})}).then(r=>r.json()).catch(()=>({status:'error',error:'request failed'}));
if(start.status!=='started'){alert(start.error||'failed');go.textContent='Yes, add it';go.disabled=false;return;}
// live status bar above the preview
let bar=document.getElementById('applybar');
if(!bar){bar=document.createElement('div');bar.id='applybar';bar.className='note';opts.parentNode.insertBefore(bar,opts.nextSibling);}
const poll=setInterval(async()=>{
let p; try{p=await fetch('api/apply-progress').then(r=>r.json());}catch(e){return;}
const pct=p.total?` · ${p.current}/${p.total}`:'';
go.textContent=(p.done?'✓ ':'')+(p.stage||'Working…').replace(/…$/,'').slice(0,22)+'…';
bar.innerHTML=`<b>${p.stage||'Working…'}</b>${pct}`;
if(p.done){
clearInterval(poll);
const r=p.result||{}, pf=(r.project&&r.project.files||[]);
go.textContent=`✓ Added ${r.count||0}`;
const folder=(r.project&&r.project.folder||'').replace('/home/barrett-land','~');
bar.innerHTML=`<b>✓ Done.</b> Merged ${r.count||0} symbol(s) into your libraries.`+
(pf.length?`<br>Delivered ${pf.length} project file(s) → <span class="lib">${folder}</span>`:'')+
((r.errors&&r.errors.length)?`<br><span style="color:#f87171">${r.errors.length} issue(s): ${r.errors.slice(0,2).join('; ')}</span>`:'');
setTimeout(hideModal,pf.length||(r.errors&&r.errors.length)?3600:1600);
}
},350);
}
document.getElementById('btn-addlib').onclick=openAddLib;
document.getElementById('btn-addproj').onclick=openAddProj;
document.getElementById('q').oninput=render;
document.getElementById('refresh').onclick=()=>loadList(true);
loadList();
</script>
</body>
</html>