pcb-retrace/docs/schema.html

410 lines
22 KiB
HTML
Raw Normal View History

<!--
~ Copyright (c) 2025-2026 Taras Greben
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial-pcb-retrace
~ See LICENSE file for details.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag("consent","default",{ad_storage:"granted",ad_user_data:"granted",ad_personalization:"granted",analytics_storage:"granted"});gtag("consent","default",{ad_storage:"denied",ad_user_data:"denied",ad_personalization:"denied",analytics_storage:"denied",region:"AT,BE,BG,HR,CY,CZ,DK,EE,FI,FR,DE,GR,HU,IE,IT,LV,LT,LU,MT,NL,PL,PT,RO,SK,SI,ES,SE,IS,LI,NO,GB,CH,UA".split(",")});</script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-X1MKL6C4B0"></script>
<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag("js",new Date());gtag("config","G-X1MKL6C4B0");</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schematic ReTrace</title>
<script src="https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.8.26/dist/zip.min.js"></script>
<script src="db-core.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap');
:root {
--bg:#0d1117; --surface:#161b22; --surface2:#1c2230; --border:#30363d;
--accent:#00e5a0; --accent2:#00a8ff; --accent3:#ff6b35; --warn:#f0c040;
--text:#e6edf3; --text2:#8b949e;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg); color: var(--text);
font-family: 'Rajdhani', sans-serif; font-size: 14px;
height: 100dvh; display: flex; flex-direction: column; overflow: hidden;
}
/* ── Header ── */
#header {
background: var(--surface); border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 12px; padding: 6px 14px; flex-shrink: 0;
}
#header .logo { font-family: 'Share Tech Mono', monospace; font-size: 18px; color: var(--accent); letter-spacing: 2px; white-space: nowrap; }
#header .logo span { color: var(--accent2); }
.sep { width: 1px; height: 24px; background: var(--border); flex-shrink: 0; }
.spacer { flex: 1; }
/* ── Buttons ── */
.btn {
background: var(--surface2); border: 1px solid var(--border); color: var(--text);
padding: 4px 12px; border-radius: 4px; cursor: pointer;
font-family: 'Rajdhani', sans-serif; font-size: 13px; font-weight: 600;
transition: all .15s; white-space: nowrap;
display: inline-flex; align-items: center; justify-content: center;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.accent { border-color: var(--accent); color: var(--accent); }
.btn.accent:hover { background: var(--accent); color: var(--bg); }
.btn.warn { border-color: var(--warn); color: var(--warn); }
.btn-group { display: flex; gap: 6px; align-items: center; }
/* ── Status ── */
#status-msg { font-family: 'Space Mono', monospace; font-size: 11px; color: var(--text2); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#status-msg.ok { color: var(--accent); }
#status-msg.err { color: var(--accent3); }
#status-msg.warn { color: var(--warn); }
/* ── Main layout ── */
#main { display: flex; flex: 1; overflow: hidden; }
/* ── Left panel ── */
#left-panel { width: 220px; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden; }
.panel-title { padding: 8px 12px; font-size: 11px; font-weight: 700; letter-spacing: 2px; color: var(--text2); border-bottom: 1px solid var(--border); text-transform: uppercase; font-family: 'Space Mono', monospace; }
#net-list { flex: 1; min-height: 0; overflow-y: auto; padding: 4px 0; }
#net-list::-webkit-scrollbar { width: 4px; }
#net-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.net-item { padding: 5px 12px; cursor: pointer; display: flex; align-items: center; gap: 6px; font-family: 'Space Mono', monospace; font-size: 11px; transition: background .1s; }
.net-item:hover { background: var(--surface2); }
.net-item.selected { background: rgba(0,229,160,.08); color: var(--accent); }
.net-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.net-item .nc { margin-left: auto; color: var(--text2); font-size: 10px; }
.net-item.wip .net-dot { background: #f0c040; }
.net-item:not(.wip) .net-dot { background: var(--accent); }
#comp-list-section { border-top: 1px solid var(--border); flex: 1; min-height: 0; display: flex; flex-direction: column; }
#comp-list { overflow-y: auto; flex: 1; min-height: 0; padding: 4px 0; }
#comp-list::-webkit-scrollbar { width: 4px; }
#comp-list::-webkit-scrollbar-thumb { background: var(--border); }
.comp-item { padding: 4px 12px; cursor: pointer; font-family: 'Space Mono', monospace; font-size: 11px; display: flex; align-items: center; gap: 6px; transition: background .1s; }
.comp-item:hover { background: var(--surface2); }
.comp-item.selected { background: rgba(0,168,255,.08); color: var(--accent2); }
.cdot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }
/* ── Canvas area ── */
#canvas-wrap { flex: 1; overflow: hidden; position: relative; background: var(--bg); }
#sch-canvas { display: block; width: 100%; height: 100%; }
#coord-readout { position: absolute; top: 8px; right: 12px; font-family: 'Space Mono', monospace; font-size: 10px; color: var(--text2); background: rgba(13,17,23,.7); padding: 3px 8px; border-radius: 4px; border: 1px solid var(--border); pointer-events: none; }
#zoom-ind { position: absolute; bottom: 60px; right: 12px; font-family: 'Space Mono', monospace; font-size: 10px; color: var(--text2); background: rgba(13,17,23,.7); padding: 3px 8px; border-radius: 4px; border: 1px solid var(--border); pointer-events: none; }
#canvas-toolbar { position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; display: flex; gap: 4px; padding: 6px 10px; z-index: 50; }
#canvas-toolbar .btn { padding: 3px 10px; font-size: 12px; }
#hint-bar { position: absolute; top: 8px; left: 50%; transform: translateX(-50%); font-family: 'Space Mono', monospace; font-size: 10px; color: var(--text2); background: rgba(13,17,23,.85); padding: 3px 10px; border-radius: 4px; border: 1px solid var(--border); pointer-events: none; opacity: 0; transition: opacity .25s; }
#hint-bar.show { opacity: 1; }
/* ── Right panel (properties) ── */
#right-panel { width: 220px; background: var(--surface); border-left: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
#prop-panel { padding: 10px 12px; flex: 1; overflow-y: auto; font-family: 'Space Mono', monospace; font-size: 11px; }
#prop-panel::-webkit-scrollbar { width: 4px; }
#prop-panel::-webkit-scrollbar-thumb { background: var(--border); }
.prop-row { margin-bottom: 8px; }
.prop-label { color: var(--text2); margin-bottom: 2px; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; }
.prop-val { color: var(--text); }
.prop-input { width: 100%; background: var(--bg); border: 1px solid var(--border); color: var(--text); padding: 4px 6px; border-radius: 3px; font-family: 'Space Mono', monospace; font-size: 11px; margin-top: 2px; }
.prop-input:focus { outline: none; border-color: var(--accent2); }
/* ── Modals ── */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 1000; align-items: center; justify-content: center; }
.modal-overlay.active { display: flex; }
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 20px; width: 480px; max-width: 95vw; max-height: 80vh; display: flex; flex-direction: column; gap: 14px; }
.modal h2 { font-family: 'Share Tech Mono', monospace; color: var(--accent); font-size: 16px; letter-spacing: 2px; border-bottom: 1px solid var(--border); padding-bottom: 10px; }
.modal textarea { background: var(--bg); border: 1px solid var(--border); color: var(--text); padding: 8px; border-radius: 4px; font-family: 'Space Mono', monospace; font-size: 11px; resize: vertical; min-height: 160px; flex: 1; }
.modal textarea:focus { outline: none; border-color: var(--accent2); }
.modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
.modal p { font-size: 12px; color: var(--text2); line-height: 1.6; }
.modal .fmt { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 8px; font-family: 'Space Mono', monospace; font-size: 10px; color: var(--accent2); white-space: pre; overflow: auto; }
#export-content { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 10px; font-family: 'Space Mono', monospace; font-size: 10px; color: var(--accent); white-space: pre; overflow: auto; max-height: 300px; flex: 1; }
#db-list { max-height: 200px; overflow-y: auto; border: 1px solid var(--border); border-radius: 4px; }
.db-item { padding: 8px 12px; cursor: pointer; font-family: 'Space Mono', monospace; font-size: 11px; border-bottom: 1px solid var(--border); transition: background .1s; display: flex; justify-content: space-between; align-items: center; }
.db-item:last-child { border-bottom: none; }
.db-item:hover { background: var(--surface2); color: var(--accent); }
.db-item.selected { background: rgba(0,229,160,.08); color: var(--accent); }
.db-item .dm { color: var(--text2); font-size: 10px; }
/* ── Toast ── */
#toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%) translateY(60px); background: var(--surface); border: 1px solid var(--accent); color: var(--accent); font-family: 'Space Mono', monospace; font-size: 12px; padding: 8px 20px; border-radius: 6px; z-index: 2000; transition: transform .3s; pointer-events: none; }
#toast.show { border-color: var(--accent); color: var(--accent); transform: translateX(-50%) translateY(0); }
#toast.err { border-color: var(--accent3); color: var(--accent3); transform: translateX(-50%) translateY(0); }
#toast.warn { border-color: var(--warn); color: var(--warn); transform: translateX(-50%) translateY(0); }
/* ── Context menu ── */
#ctx-menu { user-select: none; }
#ctx-menu div + div { border-top: 1px solid var(--border); }
/* ── Empty state ── */
#empty-state { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 16px; pointer-events: none; }
#empty-state .et { font-family: 'Share Tech Mono', monospace; font-size: 24px; color: var(--border); letter-spacing: 4px; }
#empty-state .es { font-family: 'Space Mono', monospace; font-size: 12px; color: var(--text2); text-align: center; line-height: 1.8; max-width: 360px; }
#empty-state .eh { font-family: 'Rajdhani', sans-serif; font-size: 14px; color: var(--accent); pointer-events: all; cursor: pointer; border: 1px solid var(--accent); padding: 6px 20px; border-radius: 4px; transition: all .15s; }
#empty-state .eh:hover { background: var(--accent); color: var(--bg); }
.responsive-text::after { content: attr(data-long); white-space: nowrap; }
/* --- MOBILE LAYOUT --- */
.mobile-only { display: none !important; }
@media (max-width: 800px) {
.mobile-only { display: inline-flex !important; }
.hide-mobile { display: none !important; }
.responsive-text::after { content: attr(data-short); }
#header .btn { padding: 4px 8px; }
#header .sep { display: none; }
#header { overflow-x: auto; padding-bottom: 2px; }
#header::-webkit-scrollbar { height: 4px; }
#header::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
#main { position: relative; }
#left-panel {
position: absolute; left: -240px; top: 0; bottom: 0; z-index: 100;
transition: left 0.25s ease; box-shadow: 4px 0 15px rgba(0,0,0,0.6);
}
#left-panel.open { left: 0; }
#right-panel {
position: absolute; right: -240px; top: 0; bottom: 0; z-index: 100;
transition: right 0.25s ease; box-shadow: -4px 0 15px rgba(0,0,0,0.6);
}
#right-panel.open { right: 0; }
#canvas-toolbar { width: 90%; max-width: 380px; justify-content: center; flex-wrap: wrap; bottom: 10px; }
}
/* ── Copyright footer (standalone mode only) ── */
#copyright-footer {
display: none;
flex: 0 0 auto;
align-self: flex-start;
width: 100%;
text-align: center;
padding: 4px 14px;
font-family: 'Space Mono', monospace;
font-size: 10px;
color: var(--text2);
background: var(--surface);
border-top: 1px solid var(--border);
z-index: 20;
}
#copyright-footer a { color: var(--text2); text-decoration: none; }
#copyright-footer a:hover { color: var(--accent); }
/* Standalone detection: show footer whenever btn-load-db is not hidden.
Works regardless of what mechanism hides the button (url param, postMessage, etc.) */
body:not(:has(#btn-load-db[style*="display: none"])) #copyright-footer { display: block; }
@media (max-width: 800px) and (max-height: 500px) {
#copyright-footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 200;
}
/* Hide labels and separators in the left panel to save height */
.panel-title { display: none; }
#comp-list-section { border-top: none; }
/* Change overall app layout to row so header goes to the left edge */
body { flex-direction: row; }
/* Transform top header into a vertical sidebar */
#header {
flex-direction: column; overflow-y: auto; overflow-x: hidden;
width: auto; padding: 8px 4px;
border-bottom: none; border-right: 1px solid var(--border);
}
#header .btn-group { flex-direction: column; }
#status-msg {
display: block; text-align: center; line-height: 1.6; padding-bottom: 4px;
max-width: 40px;
white-space: normal;
margin: 0 auto;
}
#status-msg.responsive-text::after { white-space: normal; }
/* Move canvas toolbar to the RIGHT, hide zoom buttons to save height */
#canvas-toolbar {
width: auto; height: auto; flex-direction: column; flex-wrap: nowrap;
top: 50%; bottom: auto; left: auto; right: 10px; transform: translateY(-50%);
background: transparent; border: none; padding: 0; gap: 6px;
}
#canvas-toolbar .sep { display: none; }
#canvas-toolbar .btn { box-shadow: 0 2px 8px rgba(0,0,0,0.6); }
#btn-zoom-in, #btn-zoom-out { display: none; }
}
</style>
</head>
<body>
<div id="header">
<div class="logo hide-mobile">SCHEMA<span>RETRACE</span></div>
<div class="sep hide-mobile"></div>
<div class="btn-group">
<button class="btn accent" id="btn-load-db" title="Switch to PCB ReTrace"><span class="hide-mobile"> Switch to PCB ReTrace</span></button>
<button class="btn" id="btn-import-file" title="Import">📂<span class="hide-mobile"> Import</span></button>
<button class="btn" id="btn-lib-mgr" title="Libraries">📚<span class="hide-mobile"> Libraries</span></button>
</div>
<div class="sep"></div>
<div class="btn-group">
<button class="btn" id="btn-auto-place" title="Auto Layout">🔀<span class="hide-mobile"> Auto Layout</span></button>
<button class="btn" id="btn-zoom-fit" title="Fit View"><span class="hide-mobile"> Fit</span></button>
</div>
<div class="sep"></div>
<div class="btn-group">
<label class="hide-mobile" style="font-size:12px;color:var(--text2);white-space:nowrap">Export:</label>
<button class="btn warn" id="btn-export-kicad" title="Export KiCad">
<img src="kicad.svg" alt="KiCad" style="width:14px; height:14px; border-radius:2px;">
<span class="hide-mobile" style="margin-left:5px;">KiCad .kicad_sch</span>
</button>
<!--<button class="btn warn" id="btn-export-spice">SPICE .cir</button>-->
</div>
<div class="spacer"></div>
<span id="status-msg">No netlist loaded</span>
</div>
<div id="main">
<div id="left-panel">
<div class="panel-title">Nets</div>
<div id="net-list"></div>
<div id="comp-list-section">
<div class="panel-title">Components</div>
<div id="comp-list"></div>
</div>
</div>
<div id="canvas-wrap">
<canvas id="sch-canvas"></canvas>
<div id="coord-readout">X: 0 Y: 0</div>
<div id="zoom-ind">100%</div>
<div id="hint-bar"></div>
<div id="canvas-toolbar">
<button class="btn mobile-only" id="btn-toggle-left"></button>
<button class="btn" id="btn-zoom-in">+</button>
<button class="btn" id="btn-zoom-out"></button>
<button class="btn" id="btn-zoom-fit2"></button>
<div class="sep hide-mobile" style="height:20px"></div>
<button class="btn" id="btn-undo"></button>
<button class="btn mobile-only" id="btn-toggle-right">🛈</button>
</div>
<div id="empty-state">
<div class="et">NO NETLIST</div>
<div class="es">Import a KiCad or PCB ReTrace netlist file<br>or open from PCB ReTrace Studio</div>
<div class="eh" id="es-load-btn">📂 Import Netlist</div>
</div>
</div>
<div id="right-panel">
<div class="panel-title">Properties</div>
<div id="prop-panel">
<div style="color:var(--text2);font-size:10px;text-align:center;margin-top:20px">
Select a component<br>or wire segment
</div>
</div>
</div>
</div>
<div id="toast"></div>
<footer id="copyright-footer">
Copyright &copy; 2026 Taras Greben &mdash; <a href="https://pcb.etaras.com" target="_blank">pcb.eTaras.com</a>
</footer>
<!-- Import modal -->
<div class="modal-overlay" id="modal-import">
<div class="modal">
<h2>📂 IMPORT NETLIST</h2>
<p>Paste or open a file. Accepted: PCB ReTrace JSON, KiCad XML netlist, KiCad S-expr .net.</p>
<div class="fmt">{ "nets": [ { "name": "NET1",
"nodes": [{"ref":"R1","pin":"1"},{"ref":"C1","pin":"1"}] } ],
"components": [ {"ref":"R1","value":"10k","type":"R"} ] }</div>
<textarea id="import-textarea" placeholder="Paste netlist here…"></textarea>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn" id="btn-pick-file">📁 Open File</button>
<input type="file" id="file-input" accept=".json,.net,.cir,.txt" style="display:none">
<span id="file-name" style="font-family:'Space Mono',monospace;font-size:10px;color:var(--text2)">No file</span>
</div>
<div class="modal-actions">
<button class="btn" id="btn-import-cancel">Cancel</button>
<button class="btn accent" id="btn-import-ok">Import</button>
</div>
</div>
</div>
<!-- Replace Symbol modal -->
<div class="modal-overlay" id="modal-replace-symbol">
<div class="modal" style="width: 500px;">
<h2>✨ REPLACE SYMBOL</h2>
<div style="display:flex; flex-direction:column; gap: 4px; margin-bottom: 8px;">
<input type="text" id="sym-search" placeholder="Search symbols..." class="prop-input" style="width:100%;">
<div style="font-size:9px; color:var(--text2);">Advanced filters: <code>name:dio</code> <code>lib:device</code> <code>ref:D</code> <code>desc:zener</code></div>
</div>
<div id="sym-list" style="max-height: 300px; overflow-y: auto; border: 1px solid var(--border); border-radius: 4px;">
<!-- list populated via JS -->
</div>
<div class="modal-actions" style="margin-top: 14px;">
<button class="btn" id="btn-replace-cancel">Cancel</button>
</div>
</div>
</div>
<!-- Library Manager Modal -->
<div class="modal-overlay" id="modal-library-manager">
<div class="modal" style="width: 500px;">
<h2>📚 KICAD LIBRARY MANAGER</h2>
<div id="lib-default-ui" style="display:flex; flex-direction:column; gap:8px;">
<p>Import KiCad symbols from a local file (.zip, .kicad_sym) or a remote URL.</p>
<label style="font-size:10px; color:var(--text2); text-transform:uppercase;">Remote URL</label>
<div style="display:flex; gap:8px;">
<input type="text" id="lib-remote-url" value="https://pcb.etaras.com/assets/kicad-symbols-master.zip" placeholder="https://.../library.zip" class="prop-input" style="flex:1;">
<button class="btn accent" id="btn-lib-import-url">⬇ Fetch</button>
</div>
<div style="text-align:center; color:var(--text2); margin: 4px 0;">— OR —</div>
<label style="font-size:10px; color:var(--text2); text-transform:uppercase;">Local File</label>
<div style="display:flex; gap:8px; align-items:center;">
<button class="btn" id="btn-lib-pick-file">📁 Select File</button>
<input type="file" id="lib-file-input" accept=".zip,.kicad_sym" style="display:none">
<span id="lib-file-name" style="font-family:'Space Mono',monospace;font-size:10px;color:var(--text2)">No file</span>
</div>
</div>
<div id="lib-zip-explorer" style="display:none; flex-direction:column; gap:8px;">
<div style="font-size:11px; color:var(--text2);">Select libraries or symbols to import:</div>
<div style="display:flex; gap:6px;">
<button class="btn" id="btn-zip-sel-all">Select All</button>
<button class="btn" id="btn-zip-sel-none">Select None</button>
</div>
<div id="zip-tree" style="max-height: 240px; overflow-y: auto; border: 1px solid var(--border); padding: 6px; background: var(--bg); border-radius:4px; font-family:'Space Mono', monospace; font-size:11px;"></div>
<div style="display:flex; gap: 8px; margin-top:4px;">
<button class="btn accent" id="btn-zip-import">Import Selected</button>
<button class="btn" id="btn-zip-cancel">Cancel</button>
</div>
</div>
<div id="lib-import-log" style="margin-top:12px; max-height:100px; overflow-y:auto; font-size:10px; font-family:'Space Mono',monospace; color:var(--accent2); background:var(--bg); padding:6px; border:1px solid var(--border); border-radius:4px;"></div>
<div class="modal-actions" style="margin-top: 14px;">
<button class="btn" id="btn-lib-close">Close</button>
</div>
</div>
</div>
<!-- Export modal -->
<div class="modal-overlay" id="modal-export">
<div class="modal" style="width:560px">
<h2 id="export-title">EXPORT</h2>
<pre id="export-content"></pre>
<div class="modal-actions">
<button class="btn" id="btn-export-close">Close</button>
<button class="btn accent" id="btn-export-copy">📋 Copy</button>
<button class="btn warn" id="btn-export-download">⬇ Download</button>
</div>
</div>
</div>
<script type="module" src="schema/app.js"></script>
</body>
</html>