pcb-retrace/docs/schema.html
2026-07-03 17:16:21 +03:00

420 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
~ 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 { gap: 6px; padding: 4px 8px; overflow-x: auto; padding-bottom: 2px; }
.btn-group { gap: 4px; }
#header .btn { padding: 4px 8px; }
#header .sep { display: none; }
#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-reroute" title="Re-route the Schematic">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
<span class="hide-mobile" style="margin-left:5px;">Re-route</span>
</button>
<button class="btn" id="btn-auto-place" title="Auto Layout">🔀<span class="hide-mobile" style="margin-left:5px;">Auto Layout</span></button>
<button class="btn" id="btn-zoom-fit" title="Fit View">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M6 3H3v3 M3 18v3h3 M18 21h3v-3 M21 6V3h-3"/></svg>
<span class="hide-mobile" style="margin-left:5px;">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" title="Fit View">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M6 3H3v3 M3 18v3h3 M18 21h3v-3 M21 6V3h-3"/></svg>
</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>