app
KiCad Library Manager
Public Made by Adomby adom
Organize your KiCad symbol / footprint / 3D libraries fast — three priority views, inline editing, live previews, and one-click sync.
← Commit history
Publish 1.1.0
4 files changed
+35−14
package.json+2−1@@ -4,8 +4,9 @@ "slug": "kicad-library-manager", "title": "KiCad Library Manager", "brief": "Organize your KiCad symbol / footprint / 3D libraries fast — three priority views, inline editing, live previews, and one-click sync.",- "version": "1.0.1",+ "version": "1.1.0", "visibility": "public",+ "changelog": "## 1.1.0\n- Footprint previews: pads render above silkscreen with pad numbers on top\n- 3D model preview (STEP→GLB) for footprints and a dedicated 3D Models view\n- Live JLCPCB stock + price on hover over an LCSC part number\n- Pending changes: deletes flag red, adds green, edits outline the changed field orange; header chip shows new / deleted / changed counts\n- Sync is a repeatable one-shot (resets after) with per-library sync; deleted libraries get unregistered from the lib-tables\n- Tabs renamed to Symbols / Footprints / 3D Models", "discovery_triggers": [ "kicad library manager", "organize my kicad libraries",
page.json+3−2@@ -4,7 +4,7 @@ "slug": "kicad-library-manager", "title": "KiCad Library Manager", "brief": "Organize your KiCad symbol / footprint / 3D libraries fast — three priority views, inline editing, live previews, and one-click sync.",- "version": "1.0.1",+ "version": "1.1.0", "description": "A fast workbench for your personal KiCad libraries. Probes your machine via adom-desktop for your sym-lib-table / fp-lib-table, then gives you three views — Symbol priority (inline-editable ref / footprint / datasheet / LCSC PN / description, with footprint autocomplete and live JLCPCB stock+price on hover), Footprint priority (3D model + the symbols using each footprint), and 3D-model priority. Add parts from the Adom wiki or blank, duplicate symbols, flag deletes (red) and adds (green) with an undo/redo tree, preview symbols + footprints + 3D models with kicad-cli rendering, and sync changes back to disk with reversible backups.", "dependencies": {}, "tags": [@@ -61,5 +61,6 @@ "preview symbol footprint 3d model", "lcsc stock and price", "klm"- ]+ ],+ "changelog": "## 1.1.0\n- Footprint previews: pads render above silkscreen with pad numbers on top\n- 3D model preview (STEP→GLB) for footprints and a dedicated 3D Models view\n- Live JLCPCB stock + price on hover over an LCSC part number\n- Pending changes: deletes flag red, adds green, edits outline the changed field orange; header chip shows new / deleted / changed counts\n- Sync is a repeatable one-shot (resets after) with per-library sync; deleted libraries get unregistered from the lib-tables\n- Tabs renamed to Symbols / Footprints / 3D Models" }
public/index.html+13−7@@ -124,6 +124,11 @@ .fp-tag.best{color:var(--accent-bright);border:1px solid var(--accent-border);background:var(--accent-dim)} .fp-tag.ext{color:var(--text-dim);border:1px solid var(--border)} input.cell.fpdef{background:rgba(140,107,247,.09);border-color:rgba(140,107,247,.22)}+ /* orange outline on a field changed since the last sync */+ input.cell.edited{border-color:#e8923c;box-shadow:0 0 0 2px rgba(232,146,60,.35)}+ input.cell.edited:focus{border-color:#e8923c;box-shadow:0 0 0 3px rgba(232,146,60,.45)}+ .ds-icon.edited{border-color:#e8923c;box-shadow:0 0 0 2px rgba(232,146,60,.35)}+ .desc-cell.edited{outline:2px solid rgba(232,146,60,.55);outline-offset:2px;border-radius:6px} input.cell.fpdef:focus{border-color:rgba(140,107,247,.5);box-shadow:0 0 0 2px rgba(140,107,247,.14)} input.cell.short{max-width:150px} td.col-ref{width:70px}@@ -243,7 +248,7 @@ <button class="btn warn" id="revertBtn" title="Revert all libraries to the initial probed state" disabled>⟲ Revert to backup</button> <span class="pending-chip" id="pendingChip" style="display:none"></span> <button class="btn primary" id="syncBtn" title="Write ALL your edits to disk">⇅ Sync all to disk</button>- <button class="btn" id="probeBtn" title="Scan your computer for KiCad libraries">◎ Probe libraries</button>+ <button class="btn" id="probeBtn" title="Scan your computer for KiCad libraries">⌖ Probe libraries</button> </header> <div class="sync-banner" id="syncBanner"></div>@@ -487,13 +492,14 @@ function renderSymbol(tbody){ for(const s of rows){ const tr=document.createElement('tr'); tr.className='item'+(curSel().has(s.id)?' sel':'')+pendClass(s); tr.dataset.id=s.id; const fpDefault = s.fpNick && (STATE.libs.defaultFpLibs||[]).some(n=>n.toLowerCase()===s.fpNick.toLowerCase());+ const ec=f=>(s.editedFields&&s.editedFields[f])?' edited':''; tr.innerHTML = selBox(s.id)+- `<td><div class="namewrap"><input class="cell name" data-id="${esc(s.id)}" data-field="name" value="${esc(s.name)}" spellcheck="false"><button class="dup-btn" data-previewsym="${esc(s.id)}" title="Preview symbol + footprint">${MAG_SVG}</button><button class="dup-btn" data-dup="${esc(s.id)}" title="Duplicate this symbol">⧉</button></div></td>`+- `<td class="col-ref"><input class="cell ref" data-id="${esc(s.id)}" data-field="ref" value="${esc(s.ref)}" spellcheck="false"></td>`+- `<td><input class="cell${fpDefault?' fpdef':''}" data-id="${esc(s.id)}" data-field="footprint" value="${esc(s.footprint)}" spellcheck="false" placeholder="lib:footprint" title="${fpDefault?'Linked footprint — KiCad default library':'Linked footprint (lib:name)'}"></td>`+- `<td class="col-ds"><span class="ds-icon ${isUrl(s.datasheet)?'on':''}" data-dsedit="${esc(s.id)}" title="${s.datasheet?'Datasheet: '+esc(s.datasheet)+' — click to edit':'No datasheet — click to add'}">${DS_SVG}</span></td>`+- `<td><input class="cell short" data-id="${esc(s.id)}" data-field="mpn" value="${esc(s.mpn)}" spellcheck="false" placeholder="—"></td>`+- `<td><div class="desc-cell"><span class="desc-text" title="${esc(s.description)}">${s.description?esc(s.description):'<span class=muted>—</span>'}</span><button class="desc-exp" data-desc="${esc(s.id)}" title="View / edit full description">⤢</button></div></td>`;+ `<td><div class="namewrap"><input class="cell name${ec('name')}" data-id="${esc(s.id)}" data-field="name" value="${esc(s.name)}" spellcheck="false"><button class="dup-btn" data-previewsym="${esc(s.id)}" title="Preview symbol + footprint">${MAG_SVG}</button><button class="dup-btn" data-dup="${esc(s.id)}" title="Duplicate this symbol">⧉</button></div></td>`++ `<td class="col-ref"><input class="cell ref${ec('ref')}" data-id="${esc(s.id)}" data-field="ref" value="${esc(s.ref)}" spellcheck="false"></td>`++ `<td><input class="cell${fpDefault?' fpdef':''}${ec('footprint')}" data-id="${esc(s.id)}" data-field="footprint" value="${esc(s.footprint)}" spellcheck="false" placeholder="lib:footprint" title="${fpDefault?'Linked footprint — KiCad default library':'Linked footprint (lib:name)'}"></td>`++ `<td class="col-ds"><span class="ds-icon ${isUrl(s.datasheet)?'on':''}${ec('datasheet')}" data-dsedit="${esc(s.id)}" title="${s.datasheet?'Datasheet: '+esc(s.datasheet)+' — click to edit':'No datasheet — click to add'}">${DS_SVG}</span></td>`++ `<td><input class="cell short${ec('mpn')}" data-id="${esc(s.id)}" data-field="mpn" value="${esc(s.mpn)}" spellcheck="false" placeholder="—"></td>`++ `<td><div class="desc-cell${ec('description')}"><span class="desc-text" title="${esc(s.description)}">${s.description?esc(s.description):'<span class=muted>—</span>'}</span><button class="desc-exp" data-desc="${esc(s.id)}" title="View / edit full description">⤢</button></div></td>`; tbody.appendChild(tr); } }
server.js+17−4@@ -171,6 +171,19 @@ function mpnOf(props) { for (const [k, v] of Object.entries(props)) { const t = (v || '').trim(); if (!t) continue; if (/lcsc|jlc/i.test(k) || /^C\d{3,}$/.test(t)) return { field: k, value: t }; } return { field: null, value: '' }; }+// Backfill per-field "changed" flags for symbols edited before field-tracking existed: diff each+// pending-edit symbol against its backup counterpart (matched by LCSC PN, else name).+const VIS_FIELDS = ['name', 'ref', 'footprint', 'datasheet', 'mpn', 'description'];+function backfillEditedFields() {+ if (!DB.backup) return;+ const bAll = DB.backup.symbolLibs.flatMap(l => l.symbols);+ for (const lib of DB.libs.symbolLibs) for (const s of lib.symbols) {+ if (!s.pendingEdit || s.editedFields || s.pendingAdd) continue;+ const b = (s.mpn && bAll.find(x => (x.mpn || '').toUpperCase() === s.mpn.toUpperCase())) || bAll.find(x => x.name === s.name);+ const ef = {}; if (b) for (const f of VIS_FIELDS) if ((s[f] || '') !== (b[f] || '')) ef[f] = true;+ s.editedFields = ef;+ }+} // Re-derive the LCSC/JLC PN for every loaded symbol from its raw block (used after the rule changes). function reDeriveMpn() { for (const lib of DB.libs.symbolLibs) for (const s of lib.symbols) {@@ -455,7 +468,7 @@ function editSymbolField(id, field, value) { s[field] = value.trim(); if (field === 'footprint') { s.fpNick = value.includes(':') ? value.split(':')[0] : ''; s.fpName = value.includes(':') ? value.split(':').slice(1).join(':') : value.trim(); } }- if (!s.pendingAdd) s.pendingEdit = true; // mark as changed (unless it's a brand-new symbol)+ if (!s.pendingAdd) { s.pendingEdit = true; s.editedFields = s.editedFields || {}; s.editedFields[field] = true; } // track which fields changed recomputeUsage(); commit('edit ' + field + ' · ' + s.name); save(); return s;@@ -751,7 +764,7 @@ async function restoreTrash() { // pending-add (green) flags on everything that was just written. function pruneAfterSync() { DB.libs.symbolLibs = DB.libs.symbolLibs.filter(l => !l.pendingDelete);- for (const l of DB.libs.symbolLibs) { l.symbols = l.symbols.filter(s => !s.pendingDelete); for (const s of l.symbols) { delete s.pendingAdd; delete s.pendingEdit; } }+ for (const l of DB.libs.symbolLibs) { l.symbols = l.symbols.filter(s => !s.pendingDelete); for (const s of l.symbols) { delete s.pendingAdd; delete s.pendingEdit; delete s.editedFields; } } DB.libs.footprintLibs = DB.libs.footprintLibs.filter(l => !l.pendingDelete); for (const l of DB.libs.footprintLibs) { l.footprints = l.footprints.filter(f => !f.pendingDelete); for (const f of l.footprints) f.added = false; } DB.libs.models3d = DB.libs.models3d.filter(m => !m.pendingDelete); for (const m of DB.libs.models3d) m.added = false;@@ -866,7 +879,7 @@ async function syncOneLibrary(mode, nick) { log.push('deleted symbol library ' + nick); } else { log.push(await writeSymLib(lib));- lib.symbols = lib.symbols.filter(s => !s.pendingDelete); for (const s of lib.symbols) delete s.pendingAdd;+ lib.symbols = lib.symbols.filter(s => !s.pendingDelete); for (const s of lib.symbols) { delete s.pendingAdd; delete s.pendingEdit; delete s.editedFields; } } } else if (mode === 'footprint') { const lib = findFpLib(nick); if (!lib) throw new Error('footprint library not found: ' + nick);@@ -1261,7 +1274,7 @@ const server = http.createServer(async (req, res) => { // rebuild usage + history root on boot if we have persisted libs if (DB.libs && (DB.libs.symbolLibs.length || DB.libs.footprintLibs.length || DB.libs.models3d.length)) {- reDeriveMpn(); recomputeUsage(); historyInit(DB.probe && DB.probe.probedAt ? 'restored' : 'initial');+ reDeriveMpn(); recomputeUsage(); backfillEditedFields(); historyInit(DB.probe && DB.probe.probedAt ? 'restored' : 'initial'); } server.listen(PORT, () => { console.log(`kicad-library-manager listening on http://localhost:${PORT}`);