Add library browser, BOM generation, and UI improvements
- Add parts library browser with ODBC database search - Implement hierarchical BOM generation with PCB side detection - Add STEP export and PCB rendering functionality - Adjust page width to 80% up to 1536px max - Add library-only mode for standalone library browsing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,8 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
width: 80%;
|
||||
max-width: 1536px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
@@ -270,12 +271,21 @@
|
||||
|
||||
<h1>KiCad Manager</h1>
|
||||
|
||||
{% if library_only_mode %}
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('main')">Main</button>
|
||||
<button class="tab" onclick="switchTab('variants')">Variant Manager</button>
|
||||
<button class="tab active" onclick="switchTab('library')">Library</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="switchTab('debug')">Debug</button>
|
||||
<button class="tab" onclick="switchTab('publish')">Publish</button>
|
||||
<button class="tab" onclick="switchTab('library')">Library</button>
|
||||
<button class="tab" onclick="switchTab('variants')">Variants</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div id="mainTab" class="tab-content active">
|
||||
{% if not library_only_mode %}
|
||||
<div id="debugTab" class="tab-content active">
|
||||
|
||||
<div class="command-container">
|
||||
<h2>Invocation Command</h2>
|
||||
@@ -298,10 +308,16 @@
|
||||
<h2>Actions</h2>
|
||||
<button id="generatePdfBtn" class="btn">Generate All PDFs (Schematic + Board Layers)</button>
|
||||
<button id="generateGerbersBtn" class="btn" style="margin-left: 10px;">Generate Gerbers & Drill Files</button>
|
||||
<button id="exportStepBtn" class="btn" style="margin-left: 10px;">Export PCB to STEP</button>
|
||||
<button id="renderPcbBtn" class="btn" style="margin-left: 10px;">Render PCB Image</button>
|
||||
<button id="generateBomBtn" class="btn" style="margin-left: 10px;">Generate BOM (All Variants)</button>
|
||||
<button id="syncLibrariesBtn" class="btn" style="margin-left: 10px;">Sync Symbol Libraries</button>
|
||||
<button id="syncDbBtn" class="btn" style="margin-left: 10px;">Sync Parts to Database (R & C)</button>
|
||||
<div id="message" class="message"></div>
|
||||
<div id="gerberMessage" class="message"></div>
|
||||
<div id="stepMessage" class="message"></div>
|
||||
<div id="renderMessage" class="message"></div>
|
||||
<div id="bomMessage" class="message"></div>
|
||||
<div id="syncMessage" class="message"></div>
|
||||
<div id="dbSyncMessage" class="message"></div>
|
||||
</div>
|
||||
@@ -330,8 +346,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- End mainTab -->
|
||||
</div><!-- End debugTab -->
|
||||
|
||||
<!-- Publish Tab -->
|
||||
<div id="publishTab" class="tab-content">
|
||||
<div class="container">
|
||||
<h2>Build & Publish</h2>
|
||||
<p>Generate all manufacturing outputs in a single operation with progress feedback.</p>
|
||||
|
||||
<div class="actions">
|
||||
<h3>Coming Soon</h3>
|
||||
<p>This tab will allow you to:</p>
|
||||
<ul style="margin-left: 20px; line-height: 1.8;">
|
||||
<li>Generate PDFs (Schematic + Board)</li>
|
||||
<li>Export Gerbers & Drill Files</li>
|
||||
<li>Export STEP model</li>
|
||||
<li>Generate BOM</li>
|
||||
<li>Run design checks</li>
|
||||
</ul>
|
||||
<p style="margin-top: 20px;">All with a single button press and real-time progress tracking.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End publishTab -->
|
||||
{% endif %}
|
||||
|
||||
<!-- Library Tab -->
|
||||
<div id="libraryTab" class="tab-content {% if library_only_mode %}active{% endif %}">
|
||||
<div class="container">
|
||||
<h2>Parts Library Browser</h2>
|
||||
|
||||
<div style="margin: 20px 0;">
|
||||
<input type="text" id="librarySearchInput" placeholder="Search by IPN, MPN, Manufacturer, or Description..."
|
||||
style="width: 70%; padding: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
|
||||
<button id="librarySearchBtn" class="btn" style="margin-left: 10px;">Search</button>
|
||||
<button id="libraryClearBtn" class="btn secondary" style="margin-left: 5px;">Clear</button>
|
||||
</div>
|
||||
|
||||
<div id="libraryMessage" class="message"></div>
|
||||
|
||||
<div id="libraryResults" style="margin-top: 20px;">
|
||||
<p style="color: #6c757d;">Enter a search query or click Search to browse all parts</p>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- End libraryTab -->
|
||||
|
||||
{% if not library_only_mode %}
|
||||
<!-- Variant Manager Tab -->
|
||||
<div id="variantsTab" class="tab-content">
|
||||
<div class="container">
|
||||
@@ -381,8 +440,10 @@
|
||||
<button class="btn" onclick="closeEditPartsModal()">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
const libraryOnlyMode = {{ 'true' if library_only_mode else 'false' }};
|
||||
const socket = io();
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
@@ -414,6 +475,8 @@
|
||||
socket.disconnect();
|
||||
});
|
||||
|
||||
// Only initialize project-specific features if not in library-only mode
|
||||
if (!libraryOnlyMode) {
|
||||
// PDF Generation
|
||||
const generatePdfBtn = document.getElementById('generatePdfBtn');
|
||||
const messageEl = document.getElementById('message');
|
||||
@@ -486,6 +549,114 @@
|
||||
generateGerbersBtn.disabled = false;
|
||||
});
|
||||
|
||||
// STEP Export
|
||||
const exportStepBtn = document.getElementById('exportStepBtn');
|
||||
const stepMessageEl = document.getElementById('stepMessage');
|
||||
|
||||
function showStepMessage(text, type) {
|
||||
stepMessageEl.textContent = text;
|
||||
stepMessageEl.className = 'message ' + type;
|
||||
stepMessageEl.style.display = 'block';
|
||||
}
|
||||
|
||||
exportStepBtn.addEventListener('click', () => {
|
||||
exportStepBtn.disabled = true;
|
||||
showStepMessage('Exporting PCB to STEP...', 'info');
|
||||
socket.emit('export_step');
|
||||
});
|
||||
|
||||
socket.on('step_status', (data) => {
|
||||
showStepMessage(data.status, 'info');
|
||||
});
|
||||
|
||||
socket.on('step_complete', (data) => {
|
||||
showStepMessage('STEP file generated successfully! Downloading...', 'success');
|
||||
exportStepBtn.disabled = false;
|
||||
|
||||
// Trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = '/download/' + data.filename;
|
||||
link.download = data.filename;
|
||||
link.click();
|
||||
});
|
||||
|
||||
socket.on('step_error', (data) => {
|
||||
showStepMessage('Error: ' + data.error, 'error');
|
||||
exportStepBtn.disabled = false;
|
||||
});
|
||||
|
||||
// PCB Render
|
||||
const renderPcbBtn = document.getElementById('renderPcbBtn');
|
||||
const renderMessageEl = document.getElementById('renderMessage');
|
||||
|
||||
function showRenderMessage(text, type) {
|
||||
renderMessageEl.textContent = text;
|
||||
renderMessageEl.className = 'message ' + type;
|
||||
renderMessageEl.style.display = 'block';
|
||||
}
|
||||
|
||||
renderPcbBtn.addEventListener('click', () => {
|
||||
renderPcbBtn.disabled = true;
|
||||
showRenderMessage('Rendering PCB image...', 'info');
|
||||
socket.emit('render_pcb');
|
||||
});
|
||||
|
||||
socket.on('render_status', (data) => {
|
||||
showRenderMessage(data.status, 'info');
|
||||
});
|
||||
|
||||
socket.on('render_complete', (data) => {
|
||||
showRenderMessage('PCB image rendered successfully! Downloading...', 'success');
|
||||
renderPcbBtn.disabled = false;
|
||||
|
||||
// Trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = '/download/' + data.filename;
|
||||
link.download = data.filename;
|
||||
link.click();
|
||||
});
|
||||
|
||||
socket.on('render_error', (data) => {
|
||||
showRenderMessage('Error: ' + data.error, 'error');
|
||||
renderPcbBtn.disabled = false;
|
||||
});
|
||||
|
||||
// BOM Generation
|
||||
const generateBomBtn = document.getElementById('generateBomBtn');
|
||||
const bomMessageEl = document.getElementById('bomMessage');
|
||||
|
||||
function showBomMessage(text, type) {
|
||||
bomMessageEl.textContent = text;
|
||||
bomMessageEl.className = 'message ' + type;
|
||||
bomMessageEl.style.display = 'block';
|
||||
}
|
||||
|
||||
generateBomBtn.addEventListener('click', () => {
|
||||
generateBomBtn.disabled = true;
|
||||
showBomMessage('Generating BOMs...', 'info');
|
||||
socket.emit('generate_bom');
|
||||
});
|
||||
|
||||
socket.on('bom_status', (data) => {
|
||||
showBomMessage(data.status, 'info');
|
||||
});
|
||||
|
||||
socket.on('bom_complete', (data) => {
|
||||
showBomMessage('BOMs generated successfully! Downloading ZIP archive...', 'success');
|
||||
generateBomBtn.disabled = false;
|
||||
|
||||
// Trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = '/download/' + data.filename;
|
||||
link.download = data.filename;
|
||||
link.click();
|
||||
});
|
||||
|
||||
socket.on('bom_error', (data) => {
|
||||
showBomMessage('Error: ' + data.error, 'error');
|
||||
generateBomBtn.disabled = false;
|
||||
});
|
||||
|
||||
// Library Synchronization
|
||||
const syncLibrariesBtn = document.getElementById('syncLibrariesBtn');
|
||||
const syncMessageEl = document.getElementById('syncMessage');
|
||||
@@ -663,16 +834,98 @@
|
||||
});
|
||||
|
||||
// Show selected tab
|
||||
if (tabName === 'main') {
|
||||
document.getElementById('mainTab').classList.add('active');
|
||||
if (tabName === 'debug') {
|
||||
document.getElementById('debugTab').classList.add('active');
|
||||
document.querySelectorAll('.tab')[0].classList.add('active');
|
||||
} else if (tabName === 'publish') {
|
||||
document.getElementById('publishTab').classList.add('active');
|
||||
document.querySelectorAll('.tab')[1].classList.add('active');
|
||||
} else if (tabName === 'library') {
|
||||
document.getElementById('libraryTab').classList.add('active');
|
||||
document.querySelectorAll('.tab')[2].classList.add('active');
|
||||
} else if (tabName === 'variants') {
|
||||
document.getElementById('variantsTab').classList.add('active');
|
||||
document.querySelectorAll('.tab')[1].classList.add('active');
|
||||
document.querySelectorAll('.tab')[3].classList.add('active');
|
||||
loadVariants();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Library Browser
|
||||
// ========================================
|
||||
const librarySearchInput = document.getElementById('librarySearchInput');
|
||||
const librarySearchBtn = document.getElementById('librarySearchBtn');
|
||||
const libraryClearBtn = document.getElementById('libraryClearBtn');
|
||||
const libraryMessageEl = document.getElementById('libraryMessage');
|
||||
const libraryResultsEl = document.getElementById('libraryResults');
|
||||
|
||||
function showLibraryMessage(text, type) {
|
||||
libraryMessageEl.textContent = text;
|
||||
libraryMessageEl.className = 'message ' + type;
|
||||
libraryMessageEl.style.display = 'block';
|
||||
}
|
||||
|
||||
function searchParts() {
|
||||
const query = librarySearchInput.value.trim();
|
||||
showLibraryMessage('Searching...', 'info');
|
||||
socket.emit('search_parts', { query: query });
|
||||
}
|
||||
|
||||
librarySearchBtn.addEventListener('click', searchParts);
|
||||
|
||||
librarySearchInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
searchParts();
|
||||
}
|
||||
});
|
||||
|
||||
libraryClearBtn.addEventListener('click', () => {
|
||||
librarySearchInput.value = '';
|
||||
libraryResultsEl.innerHTML = '<p style="color: #6c757d;">Enter a search query or click Search to browse all parts</p>';
|
||||
libraryMessageEl.style.display = 'none';
|
||||
});
|
||||
|
||||
socket.on('library_search_results', (data) => {
|
||||
libraryMessageEl.style.display = 'none';
|
||||
|
||||
if (data.parts.length === 0) {
|
||||
libraryResultsEl.innerHTML = '<p style="color: #6c757d;">No parts found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<p style="margin-bottom: 10px;"><strong>Found ${data.count} part(s)</strong></p>
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
||||
<thead>
|
||||
<tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
|
||||
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">IPN</th>
|
||||
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">MPN</th>
|
||||
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">Manufacturer</th>
|
||||
<th style="padding: 10px; text-align: left; border: 1px solid #dee2e6;">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
for (const part of data.parts) {
|
||||
html += `
|
||||
<tr style="border-bottom: 1px solid #dee2e6;">
|
||||
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.ipn}</td>
|
||||
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.mpn}</td>
|
||||
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.manufacturer}</td>
|
||||
<td style="padding: 8px; border: 1px solid #dee2e6;">${part.description}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
html += '</tbody></table>';
|
||||
libraryResultsEl.innerHTML = html;
|
||||
});
|
||||
|
||||
socket.on('library_error', (data) => {
|
||||
showLibraryMessage('Error: ' + data.error, 'error');
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// Variant Manager
|
||||
// ========================================
|
||||
@@ -821,6 +1074,10 @@
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('variant_status', (data) => {
|
||||
showVariantMessage(data.status, 'info');
|
||||
});
|
||||
|
||||
socket.on('variant_updated', (data) => {
|
||||
showVariantMessage(data.message, 'success');
|
||||
loadVariants();
|
||||
@@ -877,6 +1134,7 @@
|
||||
event.target.style.display = 'none';
|
||||
}
|
||||
}
|
||||
} // End of if (!libraryOnlyMode)
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user