<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fusible Studio</title>
<style>
:root {
--bg-canvas: #090d16;
--bg-panel: #0f172a;
--bg-panel-border: #1e293b;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--accent-green: #10b981;
--accent-red: #f43f5e;
--accent-blue: #38bdf8;
--accent-gold: #fbbf24;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg-canvas);
color: var(--text-main);
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
user-select: none;
}
header {
height: 52px;
background: var(--bg-panel);
border-bottom: 1px solid var(--bg-panel-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
z-index: 20;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
font-size: 15px;
letter-spacing: -0.02em;
}
.logo-badge {
background: linear-gradient(135deg, #f59e0b, #ef4444);
color: white;
font-size: 11px;
font-weight: 900;
padding: 2px 7px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.board-name {
color: var(--text-muted);
font-weight: 500;
font-size: 13px;
}
.fuse-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 9999px;
font-size: 12px;
font-weight: 600;
transition: all 0.2s ease;
}
.fuse-intact {
background: rgba(16, 185, 129, 0.15);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.3);
}
.fuse-blown {
background: rgba(244, 63, 94, 0.2);
color: #fb7185;
border: 1px solid rgba(244, 63, 94, 0.5);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(0.98); }
}
.tabs {
display: flex;
background: rgba(15, 23, 42, 0.6);
padding: 3px;
border-radius: 8px;
border: 1px solid var(--bg-panel-border);
}
.tab-btn {
background: transparent;
border: none;
color: var(--text-muted);
padding: 6px 14px;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease;
}
.tab-btn:hover { color: var(--text-main); }
.tab-btn.active {
background: #1e293b;
color: var(--text-main);
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.controls {
display: flex;
align-items: center;
gap: 8px;
}
.icon-btn {
background: #1e293b;
border: 1px solid var(--bg-panel-border);
color: var(--text-muted);
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
}
.icon-btn:hover { color: var(--text-main); background: #334155; }
main {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
#viewport-container {
flex: 1;
position: relative;
background-image:
radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1px),
radial-gradient(rgba(255, 255, 255, 0.04) 1px, transparent 1px);
background-size: 40px 40px, 10px 10px;
background-position: 0 0, 0 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
}
#viewport-container:active { cursor: grabbing; }
#canvas-content {
transform-origin: 0 0;
display: inline-block;
transition: transform 0.05s linear;
will-change: transform;
}
#canvas-content svg {
display: block;
filter: drop-shadow(0 15px 30px rgba(0,0,0,0.6));
}
#sidebar {
width: 320px;
background: var(--bg-panel);
border-left: 1px solid var(--bg-panel-border);
display: flex;
flex-direction: column;
z-index: 10;
}
.sidebar-section {
padding: 14px 16px;
border-bottom: 1px solid var(--bg-panel-border);
}
.section-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin-bottom: 10px;
font-weight: 700;
}
.layer-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
font-size: 13px;
}
.layer-label {
display: flex;
align-items: center;
gap: 8px;
}
.layer-color-dot {
width: 10px;
height: 10px;
border-radius: 2px;
}
.toggle-checkbox {
accent-color: var(--accent-blue);
cursor: pointer;
}
#diagnostics-panel {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.diag-card {
background: rgba(244, 63, 94, 0.08);
border: 1px solid rgba(244, 63, 94, 0.25);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
font-size: 12px;
}
.diag-title {
font-weight: 700;
color: #fb7185;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 6px;
}
.diag-body {
color: #cbd5e1;
line-height: 1.4;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11px;
white-space: pre-wrap;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 30px 10px;
color: var(--text-muted);
text-align: center;
font-size: 13px;
}
.hud-toast {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(8px);
border: 1px solid var(--bg-panel-border);
padding: 8px 14px;
border-radius: 8px;
font-size: 12px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 8px;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-green);
}
</style>
</head>
<body>
<header>
<div class="brand">
<span class="logo-badge">Fusible</span>
<span class="board-name" id="board-name">Board Preview</span>
<div id="fuse-status" class="fuse-badge fuse-intact">
<span id="fuse-icon">⚡</span>
<span id="fuse-text">Fuse Intact</span>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" data-view="pcb" onclick="switchView('pcb')">PCB Layout</button>
<button class="tab-btn" data-view="schematic" onclick="switchView('schematic')">Schematic</button>
</div>
<div class="controls">
<button class="icon-btn" onclick="zoomIn()">Zoom +</button>
<button class="icon-btn" onclick="zoomOut()">Zoom -</button>
<button class="icon-btn" onclick="resetZoom()">Reset</button>
</div>
</header>
<main>
<div id="viewport-container">
<div id="canvas-content"></div>
<div class="hud-toast">
<div class="status-dot" id="live-dot"></div>
<span id="live-status">Live reload connected</span>
</div>
</div>
<aside id="sidebar">
<div class="sidebar-section" id="layers-section">
<div class="section-title">PCB Layers</div>
<div class="layer-item">
<div class="layer-label">
<span class="layer-color-dot" style="background: #cf8a3c;"></span>
<span>Top Copper</span>
</div>
<input type="checkbox" class="toggle-checkbox" checked onchange="toggleLayer('top_copper', this.checked)">
</div>
<div class="layer-item">
<div class="layer-label">
<span class="layer-color-dot" style="background: #3b5284;"></span>
<span>Bottom Copper</span>
</div>
<input type="checkbox" class="toggle-checkbox" checked onchange="toggleLayer('bottom_copper', this.checked)">
</div>
<div class="layer-item">
<div class="layer-label">
<span class="layer-color-dot" style="background: #e6c35c;"></span>
<span>Pads (ENIG Gold)</span>
</div>
<input type="checkbox" class="toggle-checkbox" checked onchange="toggleLayer('pads', this.checked)">
</div>
<div class="layer-item">
<div class="layer-label">
<span class="layer-color-dot" style="background: #f7fafc;"></span>
<span>Silkscreen</span>
</div>
<input type="checkbox" class="toggle-checkbox" checked onchange="toggleLayer('silkscreen', this.checked)">
</div>
</div>
<div class="sidebar-section">
<div class="section-title">Diagnostics & DRC</div>
</div>
<div id="diagnostics-panel">
<div class="empty-state">
<span>No electrical violations detected.</span>
</div>
</div>
</aside>
</main>
<script>
let currentView = 'pcb';
let pcbSvg = '';
let schematicSvg = '';
let zoomLevel = 1.0;
let panX = 0;
let panY = 0;
let isDragging = false;
let startX = 0;
let startY = 0;
const viewport = document.getElementById('viewport-container');
const canvas = document.getElementById('canvas-content');
function applyTransform() {
canvas.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`;
}
function zoomIn() {
zoomLevel *= 1.25;
applyTransform();
}
function zoomOut() {
zoomLevel /= 1.25;
applyTransform();
}
function resetZoom() {
zoomLevel = 1.0;
panX = 0;
panY = 0;
applyTransform();
}
viewport.addEventListener('wheel', (e) => {
e.preventDefault();
const zoomFactor = e.deltaY < 0 ? 1.15 : 0.85;
zoomLevel *= zoomFactor;
applyTransform();
}, { passive: false });
viewport.addEventListener('mousedown', (e) => {
if (e.button === 0) {
isDragging = true;
startX = e.clientX - panX;
startY = e.clientY - panY;
}
});
window.addEventListener('mousemove', (e) => {
if (isDragging) {
panX = e.clientX - startX;
panY = e.clientY - startY;
applyTransform();
}
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
function switchView(view) {
currentView = view;
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
});
document.getElementById('layers-section').style.display = view === 'pcb' ? 'block' : 'none';
renderCurrentView();
}
function renderCurrentView() {
if (currentView === 'pcb') {
canvas.innerHTML = pcbSvg || '<div class="empty-state">No PCB Layout Available</div>';
} else {
canvas.innerHTML = schematicSvg || '<div class="empty-state">No Schematic Available</div>';
}
}
function toggleLayer(layerName, visible) {
const groups = canvas.querySelectorAll(`g[id="${layerName}"]`);
groups.forEach(g => {
g.style.display = visible ? '' : 'none';
});
}
function updateDiagnostics(fuseBlown, diagnostics) {
const statusEl = document.getElementById('fuse-status');
const textEl = document.getElementById('fuse-text');
const iconEl = document.getElementById('fuse-icon');
const diagPanel = document.getElementById('diagnostics-panel');
if (fuseBlown) {
statusEl.className = 'fuse-badge fuse-blown';
textEl.textContent = `Fuse Blown (${diagnostics.length})`;
iconEl.textContent = '💥';
if (diagnostics.length > 0) {
diagPanel.innerHTML = diagnostics.map(d => `
<div class="diag-card">
<div class="diag-title">⚠️ ${escapeHtml(d.rule || 'Violation')}</div>
<div class="diag-body">${escapeHtml(d.message || '')}</div>
</div>
`).join('');
}
} else {
statusEl.className = 'fuse-badge fuse-intact';
textEl.textContent = 'Fuse Intact';
iconEl.textContent = '⚡';
diagPanel.innerHTML = '<div class="empty-state"><span>No electrical violations detected.</span></div>';
}
}
function escapeHtml(str) {
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function connectSSE() {
const eventSource = new EventSource('/events');
const liveDot = document.getElementById('live-dot');
const liveStatus = document.getElementById('live-status');
eventSource.onopen = () => {
liveDot.style.background = 'var(--accent-green)';
liveStatus.textContent = 'Live reload connected';
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.board_name) {
document.getElementById('board-name').textContent = data.board_name;
}
if (data.pcb_svg) pcbSvg = data.pcb_svg;
if (data.schematic_svg) schematicSvg = data.schematic_svg;
updateDiagnostics(data.fuse_blown, data.diagnostics || []);
renderCurrentView();
} catch (err) {
console.error('Error processing live reload payload', err);
}
};
eventSource.onerror = () => {
liveDot.style.background = 'var(--accent-red)';
liveStatus.textContent = 'Reconnecting...';
};
}
connectSSE();
</script>
</body>
</html>