// part.js — part detail: interactive PBR viewer + full curated record (specs, pins, spice, downloads). import { mountViewer } from '/assets/viewer.js'; const esc = (s) => (s == null ? '' : String(s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' }[c]))); const id = new URLSearchParams(location.search).get('id'); function dl(name, text, mime = 'text/plain') { const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([text], { type: mime })); a.download = name; a.click(); } function spendCredit() { let c = parseInt(localStorage.getItem('ppcb_credits') || '5', 10); if (c <= 0) { alert('Out of download credits — contribute a curated part to earn 5 more.'); return false; } localStorage.setItem('ppcb_credits', --c); document.getElementById('cred-n').textContent = c; return true; } (async () => { const parts = await (await fetch('/assets/catalog.json')).json(); const p = parts.find((x) => x.id === id); const root = document.getElementById('root'); document.getElementById('cred-n').textContent = localStorage.getItem('ppcb_credits') || '5'; if (!p) { root.innerHTML = '
Part not found. Back to catalog
'; return; } document.title = `${p.mpn || p.id} — PromptPCB Parts`; const params = (p.params || []).map((x) => `${esc(x.name)}${esc(x.value)} ${esc(x.unit || '')}${x.note ? `${esc(x.note)}` : ''}`).join(''); const pins = (p.pins || []).map((x) => `
${esc(x.pin ?? '')}${esc(x.name || '')}${esc(x.type || '')}
`).join(''); const ucs = (p.use_cases || []).map((u) => `
  • ${esc(u)}
  • `).join(''); const tagList = Object.entries(p.tags || {}).filter(([, v]) => v != null && v !== '' && !Array.isArray(v)).map(([k, v]) => `${esc(k)} ${esc(v)}`).join(''); const kr = ((p.tags || {}).key_ratings || []).map((r) => `${esc(r)}`).join(''); const ser = p.series ? `
    ${Object.entries(p.series).filter(([k]) => k !== 'mesh').map(([k, v]) => `${esc(k)} ${esc(v)}`).join('')}
    ` : ''; root.innerHTML = `
    PBR · drag to orbit${esc(p.package || (p.series && p.series.package) || '')}${(p.pins || []).length} pins
    ${esc(p.class)}${p.type === 'series' ? ' · series' : ''}

    ${esc((p.role || p.summary || '').split('—')[0])}

    ${esc(p.mpn || p.id)} · ${esc(p.manufacturer || '')} · ${esc(p.lcsc || '')}

    ${esc(p.summary)}

    Datasheet ↗
    ✓ 3D model ✓ SPICE ✓ Datasheet ✓ Pin map (${(p.pins || []).length}) ✓ Verified
    ${p.when_to_use ? `

    When to use

    ${esc(p.when_to_use)}

    ` : ''} ${ucs ? `

    What it's used for

      ${ucs}
    ` : ''} ${params ? `

    Specifications

    ${params}
    ` : ''} ${ser} ${pins ? `

    Pins & pads · aligned to footprint

    ${pins}
    ` : ''} ${(tagList || kr) ? `

    Requirement tags

    ${kr}${tagList}
    ` : ''} ${p.spice ? `

    SPICE model

    ${esc(p.spice)}
    ` : ''}
    `; mountViewer(document.getElementById('cv'), p.model_3d); document.getElementById('dl-all').onclick = async () => { if (!spendCredit()) return; const obj = await (await fetch(p.model_3d)).text(); dl(`${p.id}.obj`, obj); dl(`${p.id}.cir`, `* ${p.mpn} SPICE model — PromptPCB Parts\n${p.spice || ''}\n`); dl(`${p.id}.json`, JSON.stringify(p, null, 2), 'application/json'); }; document.getElementById('lib').onclick = () => { const lib = JSON.parse(localStorage.getItem('ppcb_lib') || '[]'); if (!lib.includes(p.id)) { lib.push(p.id); localStorage.setItem('ppcb_lib', JSON.stringify(lib)); } document.getElementById('lib').textContent = '✓ In library'; }; })();