mirror of
https://github.com/ParkerTenBroeck/hdl_sim.git
synced 2026-06-06 21:24:06 -04:00
added webUI
This commit is contained in:
parent
a266096f32
commit
0201990df8
16 changed files with 2590 additions and 341 deletions
468
relay/ui/app.js
Normal file
468
relay/ui/app.js
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
// ----- LocalStorage keys -----
|
||||
const LS_KEY_VHDL = "circuit_ui:circuit.vhdl";
|
||||
|
||||
// ----- State -----
|
||||
let ws = null;
|
||||
let isConnected = false;
|
||||
|
||||
let switches = 0 >>> 0; // u32
|
||||
let buttons = 0 >>> 0; // u32
|
||||
|
||||
let autoscroll = true;
|
||||
|
||||
// ----- DOM -----
|
||||
const wsPill = document.getElementById("wsPill");
|
||||
const wsUrlText = document.getElementById("wsUrlText");
|
||||
|
||||
const connectToggleBtn = document.getElementById("connectToggleBtn");
|
||||
const sendInputsBtn = document.getElementById("sendInputsBtn");
|
||||
|
||||
const vhdlEditor = document.getElementById("vhdlEditor");
|
||||
const lineGutter = document.getElementById("lineGutter");
|
||||
const loadExampleBtn = document.getElementById("loadExampleBtn");
|
||||
|
||||
const ledRow = document.getElementById("ledRow");
|
||||
const ledLabels = document.getElementById("ledLabels");
|
||||
const hexRow = document.getElementById("hexRow");
|
||||
|
||||
const switchGrid = document.getElementById("switchGrid");
|
||||
const buttonGrid = document.getElementById("buttonGrid");
|
||||
|
||||
const logView = document.getElementById("logView");
|
||||
const clearLogBtn = document.getElementById("clearLogBtn");
|
||||
const autoscrollBtn = document.getElementById("autoscrollBtn");
|
||||
|
||||
const allSwOffBtn = document.getElementById("allSwOffBtn");
|
||||
const allSwOnBtn = document.getElementById("allSwOnBtn");
|
||||
|
||||
// ----- Helpers -----
|
||||
function wsUrl() {
|
||||
const proto = (location.protocol === "https:") ? "wss" : "ws";
|
||||
return `${proto}://${location.host}/ws`;
|
||||
}
|
||||
|
||||
function setStatus(connected) {
|
||||
isConnected = connected;
|
||||
wsPill.textContent = connected ? "CONNECTED" : "DISCONNECTED";
|
||||
wsPill.style.borderColor = connected ? "rgba(34,197,94,.6)" : "rgba(239,68,68,.6)";
|
||||
wsPill.style.background = connected ? "rgba(34,197,94,.14)" : "rgba(239,68,68,.10)";
|
||||
|
||||
sendInputsBtn.disabled = !connected;
|
||||
|
||||
// single button label
|
||||
connectToggleBtn.textContent = connected ? "Disconnect" : "Connect";
|
||||
connectToggleBtn.classList.toggle("secondary", connected);
|
||||
}
|
||||
|
||||
function appendLog(stream, line) {
|
||||
const prefix = stream === "stderr" ? "[stderr]" : "[stdout]";
|
||||
logView.textContent += `${prefix} ${line}\n`;
|
||||
if (autoscroll) {
|
||||
logView.scrollTop = logView.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logView.textContent = "";
|
||||
}
|
||||
|
||||
function resetOutputsVisuals() {
|
||||
// reset LED/HEX visuals to 0 immediately
|
||||
setLeds(0);
|
||||
setHex(0);
|
||||
}
|
||||
|
||||
function u32BitGet(x, i) {
|
||||
return ((x >>> i) & 1) === 1;
|
||||
}
|
||||
|
||||
function u32BitSet(x, i, on) {
|
||||
if (on) return (x | (1 << i)) >>> 0;
|
||||
return (x & ~(1 << i)) >>> 0;
|
||||
}
|
||||
|
||||
function sendClientInput() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
const msg = {
|
||||
type: "client_input",
|
||||
switch: switches >>> 0,
|
||||
buttons: buttons >>> 0,
|
||||
};
|
||||
ws.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
function parseHexDigits(hexU32) {
|
||||
const d0 = (hexU32 >>> 0) & 0xFF;
|
||||
const d1 = (hexU32 >>> 8) & 0xFF;
|
||||
const d2 = (hexU32 >>> 16) & 0xFF;
|
||||
const d3 = (hexU32 >>> 24) & 0xFF;
|
||||
return [d0, d1, d2, d3];
|
||||
}
|
||||
|
||||
// ----- Line numbers -----
|
||||
function updateLineNumbers() {
|
||||
const text = vhdlEditor.value || "";
|
||||
// count lines: number of '\n' + 1 (even empty text -> 1 line)
|
||||
const lines = text.length ? (text.split("\n").length) : 1;
|
||||
|
||||
// Build as one string for performance
|
||||
let out = "";
|
||||
for (let i = 1; i <= lines; i++) out += i + "\n";
|
||||
lineGutter.textContent = out;
|
||||
|
||||
syncGutterScroll();
|
||||
}
|
||||
|
||||
function syncGutterScroll() {
|
||||
// keep gutter aligned to editor scroll
|
||||
lineGutter.scrollTop = vhdlEditor.scrollTop;
|
||||
}
|
||||
|
||||
// ----- LocalStorage save (debounced) -----
|
||||
let saveTimer = null;
|
||||
function saveEditorToLocalStorageDebounced() {
|
||||
if (saveTimer) clearTimeout(saveTimer);
|
||||
saveTimer = setTimeout(() => {
|
||||
try {
|
||||
localStorage.setItem(LS_KEY_VHDL, vhdlEditor.value ?? "");
|
||||
} catch (e) {
|
||||
// ignore storage failures (private mode, quota)
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function loadEditorFromLocalStorage() {
|
||||
try {
|
||||
const saved = localStorage.getItem(LS_KEY_VHDL);
|
||||
if (saved !== null) return saved;
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ----- UI Builders -----
|
||||
function buildLeds() {
|
||||
ledRow.innerHTML = "";
|
||||
ledLabels.innerHTML = "";
|
||||
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const el = document.createElement("div");
|
||||
el.className = "led";
|
||||
el.title = `LED[${i}]`;
|
||||
el.dataset.bit = String(i);
|
||||
ledRow.appendChild(el);
|
||||
|
||||
const lab = document.createElement("span");
|
||||
lab.textContent = String(i);
|
||||
ledLabels.appendChild(lab);
|
||||
}
|
||||
}
|
||||
|
||||
function setLeds(bitsU32) {
|
||||
for (const el of ledRow.children) {
|
||||
const i = Number(el.dataset.bit);
|
||||
el.classList.toggle("on", u32BitGet(bitsU32 >>> 0, i));
|
||||
}
|
||||
}
|
||||
|
||||
function makeSevenSeg(digitIndex) {
|
||||
const wrap = document.createElement("div");
|
||||
|
||||
const disp = document.createElement("div");
|
||||
disp.className = "sevenSeg";
|
||||
disp.dataset.digit = String(digitIndex);
|
||||
|
||||
const segs = ["a","b","c","d","e","f","g","dp"];
|
||||
for (const s of segs) {
|
||||
const seg = document.createElement("div");
|
||||
seg.className = `seg ${s}`;
|
||||
seg.dataset.seg = s;
|
||||
disp.appendChild(seg);
|
||||
}
|
||||
|
||||
const label = document.createElement("div");
|
||||
label.className = "digitLabel";
|
||||
label.textContent = `HEX[${digitIndex}]`;
|
||||
|
||||
wrap.appendChild(disp);
|
||||
wrap.appendChild(label);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function buildHex() {
|
||||
hexRow.innerHTML = "";
|
||||
for (let i = 0; i < 4; i++) {
|
||||
hexRow.appendChild(makeSevenSeg(i));
|
||||
}
|
||||
}
|
||||
|
||||
function setHex(hexU32) {
|
||||
const digits = parseHexDigits(hexU32 >>> 0);
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const byte = digits[i] & 0xFF;
|
||||
const disp = hexRow.querySelector(`.sevenSeg[data-digit="${i}"]`);
|
||||
if (!disp) continue;
|
||||
|
||||
const map = { a:0, b:1, c:2, d:3, e:4, f:5, g:6, dp:7 };
|
||||
|
||||
for (const segEl of disp.querySelectorAll(".seg")) {
|
||||
const name = segEl.dataset.seg;
|
||||
const bit = map[name];
|
||||
const on = ((byte >>> bit) & 1) === 1;
|
||||
segEl.classList.toggle("on", on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildSwitches() {
|
||||
switchGrid.innerHTML = "";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "ioCell";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = `SW[${i}]`;
|
||||
|
||||
const toggle = document.createElement("div");
|
||||
toggle.className = "toggle";
|
||||
toggle.dataset.bit = String(i);
|
||||
toggle.title = `Toggle switch ${i}`;
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
const bit = Number(toggle.dataset.bit);
|
||||
const now = !u32BitGet(switches, bit);
|
||||
switches = u32BitSet(switches, bit, now);
|
||||
toggle.classList.toggle("on", now);
|
||||
sendClientInput();
|
||||
});
|
||||
|
||||
cell.appendChild(toggle);
|
||||
cell.appendChild(label);
|
||||
switchGrid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function syncSwitchesUI() {
|
||||
for (const toggle of switchGrid.querySelectorAll(".toggle")) {
|
||||
const i = Number(toggle.dataset.bit);
|
||||
toggle.classList.toggle("on", u32BitGet(switches, i));
|
||||
}
|
||||
}
|
||||
|
||||
function buildButtons() {
|
||||
buttonGrid.innerHTML = "";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
const cell = document.createElement("div");
|
||||
cell.className = "ioCell";
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.textContent = `KEY[${i}]`;
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "momentary";
|
||||
btn.type = "button";
|
||||
btn.textContent = "press";
|
||||
btn.dataset.bit = String(i);
|
||||
btn.title = `Momentary button ${i}`;
|
||||
|
||||
const press = () => {
|
||||
const bit = Number(btn.dataset.bit);
|
||||
buttons = u32BitSet(buttons, bit, true);
|
||||
btn.classList.add("down");
|
||||
sendClientInput();
|
||||
};
|
||||
|
||||
const release = () => {
|
||||
const bit = Number(btn.dataset.bit);
|
||||
buttons = u32BitSet(buttons, bit, false);
|
||||
btn.classList.remove("down");
|
||||
sendClientInput();
|
||||
};
|
||||
|
||||
// Mouse
|
||||
btn.addEventListener("mousedown", (e) => { e.preventDefault(); press(); });
|
||||
btn.addEventListener("mouseup", (e) => { e.preventDefault(); release(); });
|
||||
btn.addEventListener("mouseleave",(e) => { e.preventDefault(); release(); });
|
||||
|
||||
// Touch
|
||||
btn.addEventListener("touchstart",(e) => { e.preventDefault(); press(); }, {passive:false});
|
||||
btn.addEventListener("touchend", (e) => { e.preventDefault(); release(); }, {passive:false});
|
||||
btn.addEventListener("touchcancel",(e)=> { e.preventDefault(); release(); }, {passive:false});
|
||||
|
||||
cell.appendChild(btn);
|
||||
cell.appendChild(label);
|
||||
buttonGrid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- WebSocket -----
|
||||
function connect() {
|
||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
||||
|
||||
// Requirement (3): reset logs + outputs when connect is pressed
|
||||
clearLogs();
|
||||
resetOutputsVisuals();
|
||||
|
||||
const url = wsUrl();
|
||||
wsUrlText.textContent = url;
|
||||
|
||||
ws = new WebSocket(url);
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
setStatus(true);
|
||||
appendLog("stdout", "WebSocket connected.");
|
||||
|
||||
// First message MUST be the file map
|
||||
const files = {
|
||||
"circuit.vhdl": vhdlEditor.value ?? ""
|
||||
};
|
||||
ws.send(JSON.stringify(files));
|
||||
|
||||
// Push initial input state
|
||||
sendClientInput();
|
||||
});
|
||||
|
||||
ws.addEventListener("message", (ev) => {
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(ev.data);
|
||||
} catch {
|
||||
appendLog("stderr", String(ev.data));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (parsed.log !== undefined) {
|
||||
appendLog(parsed.log.stream ?? "stdout", parsed.log.line ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.led !== undefined) {
|
||||
const v = (parsed.led ?? parsed.value ?? parsed[0] ?? parsed["0"] ?? 0) >>> 0;
|
||||
setLeds(v);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.hex !== undefined) {
|
||||
const v = (parsed.hex ?? parsed.value ?? parsed[0] ?? parsed["0"] ?? 0) >>> 0;
|
||||
setHex(v);
|
||||
return;
|
||||
}
|
||||
|
||||
appendLog("stderr", `Unknown msg: ${ev.data}`);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
appendLog("stderr", "WebSocket closed.");
|
||||
setStatus(false);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", () => {
|
||||
appendLog("stderr", "WebSocket error.");
|
||||
setStatus(false);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (ws) ws.close();
|
||||
}
|
||||
|
||||
function toggleConnect() {
|
||||
if (isConnected) disconnect();
|
||||
else connect();
|
||||
}
|
||||
|
||||
// ----- Wire up controls -----
|
||||
connectToggleBtn.addEventListener("click", toggleConnect);
|
||||
|
||||
sendInputsBtn.addEventListener("click", () => {
|
||||
appendLog("stdout", `Manual send: sw=${switches >>> 0} key=${buttons >>> 0}`);
|
||||
sendClientInput();
|
||||
});
|
||||
|
||||
clearLogBtn.addEventListener("click", () => { clearLogs(); });
|
||||
|
||||
autoscrollBtn.addEventListener("click", () => {
|
||||
autoscroll = !autoscroll;
|
||||
autoscrollBtn.textContent = `Autoscroll: ${autoscroll ? "on" : "off"}`;
|
||||
});
|
||||
|
||||
allSwOffBtn.addEventListener("click", () => {
|
||||
switches = 0 >>> 0;
|
||||
syncSwitchesUI();
|
||||
sendClientInput();
|
||||
});
|
||||
|
||||
allSwOnBtn.addEventListener("click", () => {
|
||||
switches = 0xFFFF_FFFF >>> 0;
|
||||
syncSwitchesUI();
|
||||
sendClientInput();
|
||||
});
|
||||
|
||||
loadExampleBtn.addEventListener("click", () => {
|
||||
vhdlEditor.value =
|
||||
`library ieee;
|
||||
use ieee.std_logic_1164.all;
|
||||
use ieee.numeric_std.all;
|
||||
|
||||
-- Do not modify the following entity block
|
||||
entity circuit is
|
||||
port (
|
||||
clk: in std_logic; -- 500 Hz, period 2 ms
|
||||
key: in std_logic_vector(31 downto 0); -- active low
|
||||
sw: in std_logic_vector(31 downto 0); -- active high
|
||||
led: out std_logic_vector(31 downto 0) := (others => '0'); -- active high
|
||||
hex: out std_logic_vector(31 downto 0) := (others => '0') -- active low
|
||||
);
|
||||
end circuit;
|
||||
|
||||
|
||||
architecture description of circuit is
|
||||
signal counter: unsigned(31 downto 0) := x"00000000";
|
||||
begin
|
||||
led <= std_logic_vector(counter);
|
||||
process(clk)
|
||||
begin
|
||||
counter <= counter+1;
|
||||
end process;
|
||||
end description;`;
|
||||
|
||||
saveEditorToLocalStorageDebounced();
|
||||
updateLineNumbers();
|
||||
});
|
||||
|
||||
// Editor events: save + line numbers + gutter sync
|
||||
vhdlEditor.addEventListener("input", () => {
|
||||
saveEditorToLocalStorageDebounced();
|
||||
updateLineNumbers();
|
||||
});
|
||||
|
||||
vhdlEditor.addEventListener("scroll", () => {
|
||||
syncGutterScroll();
|
||||
});
|
||||
|
||||
// ----- Init -----
|
||||
(function init() {
|
||||
wsUrlText.textContent = wsUrl();
|
||||
|
||||
buildLeds();
|
||||
buildHex();
|
||||
buildSwitches();
|
||||
buildButtons();
|
||||
|
||||
resetOutputsVisuals();
|
||||
setStatus(false);
|
||||
|
||||
// Load from localStorage if present
|
||||
const saved = loadEditorFromLocalStorage();
|
||||
if (saved !== null) {
|
||||
vhdlEditor.value = saved;
|
||||
} else {
|
||||
vhdlEditor.value =
|
||||
`-- circuit.vhdl
|
||||
-- Paste your circuit here. The UI will send it on Connect.`;
|
||||
}
|
||||
|
||||
updateLineNumbers();
|
||||
})();
|
||||
119
relay/ui/index.html
Normal file
119
relay/ui/index.html
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>VHDL Circuit UI</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<div class="dot"></div>
|
||||
<div>
|
||||
<div class="title">Circuit Web UI</div>
|
||||
<div class="subtitle">LEDs • HEX • Switches • Buttons • circuit.vhdl</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="status">
|
||||
<span class="pill" id="wsPill">DISCONNECTED</span>
|
||||
<span class="muted" id="wsUrlText"></span>
|
||||
</div>
|
||||
|
||||
<!-- single toggle button now -->
|
||||
<button id="connectToggleBtn">Connect</button>
|
||||
|
||||
<button id="sendInputsBtn" class="secondary" disabled>Send Inputs</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="grid">
|
||||
<!-- Left: Editor -->
|
||||
<section class="card editor">
|
||||
<div class="cardHeader">
|
||||
<div class="cardTitle">circuit.vhdl</div>
|
||||
<div class="cardActions">
|
||||
<button id="loadExampleBtn" class="secondary">Load example</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NEW: editor with line-number gutter -->
|
||||
<div class="editorWrap">
|
||||
<pre id="lineGutter" class="lineGutter" aria-hidden="true"></pre>
|
||||
<textarea id="vhdlEditor" spellcheck="false"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
Saved locally as you type. On <b>Connect</b>, the UI sends <code>{"circuit.vhdl": "..."}</code> as the first WebSocket message.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right: IO -->
|
||||
<section class="card io">
|
||||
<div class="cardHeader">
|
||||
<div class="cardTitle">Outputs</div>
|
||||
</div>
|
||||
|
||||
<div class="outputs">
|
||||
<div class="block">
|
||||
<div class="blockTitle">LEDs (32)</div>
|
||||
<div id="ledRow" class="ledRow"></div>
|
||||
<div class="bitLabelRow" id="ledLabels"></div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="blockTitle">HEX (4 digits, 7-seg + dp)</div>
|
||||
<div id="hexRow" class="hexRow"></div>
|
||||
<div class="hint small">
|
||||
Assumes each digit is 8 bits: <code>bit0=a</code>, <code>1=b</code>, <code>2=c</code>, <code>3=d</code>, <code>4=e</code>, <code>5=f</code>, <code>6=g</code>, <code>7=dp</code>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bottom-left: Inputs -->
|
||||
<section class="card inputs">
|
||||
<div class="cardHeader">
|
||||
<div class="cardTitle">Inputs</div>
|
||||
<div class="cardActions">
|
||||
<button id="allSwOffBtn" class="secondary">All switches off</button>
|
||||
<button id="allSwOnBtn" class="secondary">All switches on</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="twoCols">
|
||||
<div class="block">
|
||||
<div class="blockTitle">Switches (32, latched)</div>
|
||||
<div id="switchGrid" class="ioGrid"></div>
|
||||
</div>
|
||||
|
||||
<div class="block">
|
||||
<div class="blockTitle">Buttons (32, momentary)</div>
|
||||
<div id="buttonGrid" class="ioGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
Switches toggle a bit. Buttons set the bit while pressed (mouse/touch) and clear on release.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Bottom-right: Logs -->
|
||||
<section class="card logs">
|
||||
<div class="cardHeader">
|
||||
<div class="cardTitle">Logs</div>
|
||||
<div class="cardActions">
|
||||
<button id="clearLogBtn" class="secondary">Clear</button>
|
||||
<button id="autoscrollBtn" class="secondary">Autoscroll: on</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre id="logView" class="logView"></pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
396
relay/ui/styles.css
Normal file
396
relay/ui/styles.css
Normal file
|
|
@ -0,0 +1,396 @@
|
|||
:root{
|
||||
--bg: #0b0f17;
|
||||
--card: #121a2a;
|
||||
--card2:#0f1726;
|
||||
--text: #e6eefc;
|
||||
--muted:#9fb0d0;
|
||||
--accent:#58a6ff;
|
||||
--ok:#22c55e;
|
||||
--bad:#ef4444;
|
||||
--warn:#f59e0b;
|
||||
--line:#1f2a44;
|
||||
--shadow: 0 12px 30px rgba(0,0,0,.35);
|
||||
--radius: 14px;
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
|
||||
}
|
||||
|
||||
*{ box-sizing:border-box; }
|
||||
html,body{ height:100%; }
|
||||
body{
|
||||
margin:0;
|
||||
background: radial-gradient(1000px 600px at 20% -10%, rgba(88,166,255,.20), transparent 60%),
|
||||
radial-gradient(900px 500px at 90% 0%, rgba(34,197,94,.12), transparent 60%),
|
||||
var(--bg);
|
||||
color:var(--text);
|
||||
font-family: var(--sans);
|
||||
}
|
||||
|
||||
.topbar{
|
||||
position: sticky;
|
||||
top:0;
|
||||
z-index: 10;
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
padding:14px 16px;
|
||||
border-bottom:1px solid rgba(255,255,255,.06);
|
||||
background: rgba(11,15,23,.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.brand{
|
||||
display:flex;
|
||||
gap:12px;
|
||||
align-items:center;
|
||||
}
|
||||
.brand .dot{
|
||||
width:12px;height:12px;border-radius:50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 18px rgba(88,166,255,.7);
|
||||
}
|
||||
.title{ font-weight:700; letter-spacing:.2px; }
|
||||
.subtitle{ font-size:12px; color:var(--muted); margin-top:2px; }
|
||||
|
||||
.controls{
|
||||
display:flex;
|
||||
gap:10px;
|
||||
align-items:center;
|
||||
}
|
||||
.status{
|
||||
display:flex;
|
||||
gap:8px;
|
||||
align-items:center;
|
||||
margin-right:6px;
|
||||
}
|
||||
.pill{
|
||||
font-size:12px;
|
||||
padding:6px 10px;
|
||||
border-radius:999px;
|
||||
border:1px solid rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.04);
|
||||
}
|
||||
.muted{ color:var(--muted); font-size:12px; }
|
||||
|
||||
button{
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background: rgba(88,166,255,.14);
|
||||
color:var(--text);
|
||||
padding:8px 12px;
|
||||
border-radius:10px;
|
||||
cursor:pointer;
|
||||
transition: transform .05s ease, background .2s ease;
|
||||
}
|
||||
button:hover{ background: rgba(88,166,255,.20); }
|
||||
button:active{ transform: translateY(1px); }
|
||||
button.secondary{
|
||||
background: rgba(255,255,255,.06);
|
||||
}
|
||||
button.secondary:hover{ background: rgba(255,255,255,.10); }
|
||||
button:disabled{
|
||||
opacity:.55;
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
.grid{
|
||||
display:grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
grid-template-rows: 420px 1fr;
|
||||
gap:14px;
|
||||
padding:14px;
|
||||
}
|
||||
|
||||
.card{
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.05), rgba(255,255,255,.03));
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
overflow:hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.cardHeader{
|
||||
display:flex;
|
||||
justify-content:space-between;
|
||||
align-items:center;
|
||||
padding:12px 12px;
|
||||
border-bottom:1px solid rgba(255,255,255,.06);
|
||||
background: rgba(0,0,0,.15);
|
||||
}
|
||||
.cardTitle{ font-weight:700; }
|
||||
.cardActions{ display:flex; gap:8px; }
|
||||
|
||||
.editor{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
textarea{
|
||||
width:100%;
|
||||
height:100%;
|
||||
padding:12px;
|
||||
background: rgba(0,0,0,.22);
|
||||
color:var(--text);
|
||||
border:0;
|
||||
outline:none;
|
||||
resize:none;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.hint{
|
||||
padding:10px 12px;
|
||||
border-top:1px solid rgba(255,255,255,.06);
|
||||
color:var(--muted);
|
||||
font-size:12px;
|
||||
background: rgba(0,0,0,.12);
|
||||
}
|
||||
.hint.small{ font-size:11px; }
|
||||
|
||||
.io .outputs{
|
||||
padding:12px;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:14px;
|
||||
}
|
||||
.blockTitle{
|
||||
font-size:12px;
|
||||
color:var(--muted);
|
||||
margin-bottom:8px;
|
||||
}
|
||||
|
||||
.ledRow{
|
||||
display:grid;
|
||||
grid-template-columns: repeat(16, 1fr);
|
||||
gap:6px;
|
||||
}
|
||||
.led{
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 999px;
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background: rgba(255,255,255,.08);
|
||||
box-shadow: inset 0 0 0 2px rgba(0,0,0,.18);
|
||||
}
|
||||
.led.on{
|
||||
background: rgba(34,197,94,.70);
|
||||
border-color: rgba(34,197,94,.9);
|
||||
box-shadow: 0 0 14px rgba(34,197,94,.45);
|
||||
}
|
||||
.bitLabelRow{
|
||||
display:grid;
|
||||
grid-template-columns: repeat(16, 1fr);
|
||||
gap:6px;
|
||||
margin-top:6px;
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-family: var(--mono);
|
||||
opacity: .85;
|
||||
}
|
||||
.bitLabelRow span{
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.hexRow{
|
||||
display:flex;
|
||||
gap:14px;
|
||||
flex-wrap:wrap;
|
||||
align-items:center;
|
||||
}
|
||||
|
||||
/* 7-seg display */
|
||||
.sevenSeg{
|
||||
width: 90px;
|
||||
height: 140px;
|
||||
position: relative;
|
||||
background: rgba(0,0,0,.18);
|
||||
border:1px solid rgba(255,255,255,.10);
|
||||
border-radius: 14px;
|
||||
padding:10px;
|
||||
}
|
||||
.seg{
|
||||
position:absolute;
|
||||
background: rgba(255,255,255,.10);
|
||||
border-radius: 8px;
|
||||
filter: drop-shadow(0 0 0 rgba(0,0,0,0));
|
||||
}
|
||||
.seg.on{
|
||||
background: rgba(88,166,255,.75);
|
||||
filter: drop-shadow(0 0 8px rgba(88,166,255,.35));
|
||||
}
|
||||
|
||||
/* segment positions */
|
||||
.seg.a{ top:10px; left:18px; width:54px; height:12px; }
|
||||
.seg.d{ bottom:10px; left:18px; width:54px; height:12px; }
|
||||
.seg.g{ top:64px; left:18px; width:54px; height:12px; }
|
||||
|
||||
.seg.f{ top:18px; left:10px; width:12px; height:54px; }
|
||||
.seg.b{ top:18px; right:10px; width:12px; height:54px; }
|
||||
|
||||
.seg.e{ bottom:18px; left:10px; width:12px; height:54px; }
|
||||
.seg.c{ bottom:18px; right:10px; width:12px; height:54px; }
|
||||
|
||||
.seg.dp{
|
||||
width:12px; height:12px;
|
||||
bottom:10px; right:10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.digitLabel{
|
||||
margin-top:8px;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.inputs{
|
||||
grid-column: 1 / 2;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
min-height: 0;
|
||||
}
|
||||
.twoCols{
|
||||
padding:12px;
|
||||
display:grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap:12px;
|
||||
min-height: 0;
|
||||
}
|
||||
.ioGrid{
|
||||
display:grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap:8px;
|
||||
}
|
||||
.ioCell{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:6px;
|
||||
align-items:center;
|
||||
padding:8px 6px;
|
||||
background: rgba(0,0,0,.16);
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
border-radius:12px;
|
||||
}
|
||||
.ioCell label{
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.toggle{
|
||||
width: 38px;
|
||||
height: 22px;
|
||||
border-radius: 999px;
|
||||
position: relative;
|
||||
background: rgba(255,255,255,.10);
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
cursor:pointer;
|
||||
}
|
||||
.toggle::after{
|
||||
content:"";
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position:absolute;
|
||||
top:2px;
|
||||
left:2px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.45);
|
||||
transition: left .12s ease, background .12s ease;
|
||||
}
|
||||
.toggle.on{
|
||||
background: rgba(34,197,94,.24);
|
||||
border-color: rgba(34,197,94,.55);
|
||||
}
|
||||
.toggle.on::after{
|
||||
left: 20px;
|
||||
background: rgba(34,197,94,.75);
|
||||
}
|
||||
|
||||
.momentary{
|
||||
width: 40px;
|
||||
height: 28px;
|
||||
border-radius: 10px;
|
||||
border:1px solid rgba(255,255,255,.14);
|
||||
background: rgba(255,255,255,.08);
|
||||
cursor:pointer;
|
||||
}
|
||||
.momentary.down{
|
||||
background: rgba(245,158,11,.25);
|
||||
border-color: rgba(245,158,11,.55);
|
||||
box-shadow: 0 0 12px rgba(245,158,11,.18);
|
||||
}
|
||||
|
||||
.logs{
|
||||
grid-column: 2 / 3;
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
min-height: 0;
|
||||
}
|
||||
.logView{
|
||||
margin:0;
|
||||
padding:12px;
|
||||
height:100%;
|
||||
overflow:auto;
|
||||
background: rgba(0,0,0,.22);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 1100px){
|
||||
.grid{
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
.inputs, .logs{
|
||||
grid-column: auto;
|
||||
}
|
||||
.twoCols{
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Editor w/ line numbers */
|
||||
.editorWrap{
|
||||
display:flex;
|
||||
width:100%;
|
||||
height:100%;
|
||||
min-height: 0;
|
||||
background: rgba(0,0,0,.22);
|
||||
}
|
||||
|
||||
.lineGutter{
|
||||
margin:0;
|
||||
padding:12px 10px 12px 12px;
|
||||
width: 54px; /* gutter width */
|
||||
overflow:hidden;
|
||||
user-select:none;
|
||||
text-align:right;
|
||||
color: rgba(159,176,208,.85);
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
border-right: 1px solid rgba(255,255,255,.06);
|
||||
background: rgba(0,0,0,.10);
|
||||
}
|
||||
|
||||
#lineGutter .ln { display:block; }
|
||||
|
||||
textarea#vhdlEditor{
|
||||
overscroll-behavior-y: none;
|
||||
flex:1;
|
||||
height:100%;
|
||||
padding:12px;
|
||||
background: transparent; /* uses editorWrap background */
|
||||
color:var(--text);
|
||||
border:0;
|
||||
outline:none;
|
||||
resize:none;
|
||||
font-family: var(--mono);
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
overflow:auto;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue