Polish legacy UI: dark theme, note hints, lock-safe drag, and cleaner symbol editor
Some checks are pending
CI / test (push) Waiting to run

This commit is contained in:
Rbanh 2026-02-20 02:06:07 -05:00
parent 46175efe1b
commit 9a21e111d0
4 changed files with 227 additions and 52 deletions

View File

@ -11,6 +11,7 @@ const MIN_SCALE = 0.2;
const MAX_SCALE = 5; const MAX_SCALE = 5;
const FIT_MARGIN = 56; const FIT_MARGIN = 56;
const FOCUS_MARGIN = 96; const FOCUS_MARGIN = 96;
const THEME_KEY = "schemeta:theme:v1";
const state = { const state = {
model: null, model: null,
@ -31,7 +32,8 @@ const state = {
showLabels: true, showLabels: true,
isolateNet: false, isolateNet: false,
isolateComponent: false, isolateComponent: false,
renderMode: "schematic_stub", renderMode: "explicit",
theme: "dark",
userAdjustedView: false, userAdjustedView: false,
spacePan: false, spacePan: false,
schemaText: "", schemaText: "",
@ -141,6 +143,7 @@ const el = {
copyReproBtn: document.getElementById("copyReproBtn"), copyReproBtn: document.getElementById("copyReproBtn"),
autoLayoutBtn: document.getElementById("autoLayoutBtn"), autoLayoutBtn: document.getElementById("autoLayoutBtn"),
autoTidyBtn: document.getElementById("autoTidyBtn"), autoTidyBtn: document.getElementById("autoTidyBtn"),
themeToggleBtn: document.getElementById("themeToggleBtn"),
shortcutsBtn: document.getElementById("shortcutsBtn"), shortcutsBtn: document.getElementById("shortcutsBtn"),
undoBtn: document.getElementById("undoBtn"), undoBtn: document.getElementById("undoBtn"),
redoBtn: document.getElementById("redoBtn"), redoBtn: document.getElementById("redoBtn"),
@ -158,7 +161,8 @@ const el = {
commandModal: document.getElementById("commandModal"), commandModal: document.getElementById("commandModal"),
closeCommandBtn: document.getElementById("closeCommandBtn"), closeCommandBtn: document.getElementById("closeCommandBtn"),
commandInput: document.getElementById("commandInput"), commandInput: document.getElementById("commandInput"),
commandList: document.getElementById("commandList") commandList: document.getElementById("commandList"),
selectedNoteHint: null
}; };
function toGrid(v) { function toGrid(v) {
@ -348,6 +352,27 @@ function setStatus(text, ok = true) {
el.compileStatus.className = ok ? "status-ok" : ""; el.compileStatus.className = ok ? "status-ok" : "";
} }
function selectThemePreference() {
const saved = localStorage.getItem(THEME_KEY);
if (saved === "dark" || saved === "light") {
return saved;
}
return "dark";
}
function applyTheme(theme, persist = true) {
const next = theme === "light" ? "light" : "dark";
state.theme = next;
document.body.classList.toggle("theme-dark", next === "dark");
if (el.themeToggleBtn) {
el.themeToggleBtn.textContent = next === "dark" ? "Light" : "Dark";
el.themeToggleBtn.setAttribute("aria-label", next === "dark" ? "Switch to light mode" : "Switch to dark mode");
}
if (persist) {
localStorage.setItem(THEME_KEY, next);
}
}
function formatCompileStatus(result) { function formatCompileStatus(result) {
const m = result?.layout_metrics ?? {}; const m = result?.layout_metrics ?? {};
return `Compiled (${result.errors.length}E, ${result.warnings.length}W | ${m.crossings ?? 0} crossings, ${m.overlap_edges ?? 0} overlaps, ${m.total_bends ?? 0} bends, ${m.label_tie_routes ?? 0} tie-nets, ${(m.detour_ratio ?? 1).toFixed(2)}x detour)`; return `Compiled (${result.errors.length}E, ${result.warnings.length}W | ${m.crossings ?? 0} crossings, ${m.overlap_edges ?? 0} overlaps, ${m.total_bends ?? 0} bends, ${m.label_tie_routes ?? 0} tie-nets, ${(m.detour_ratio ?? 1).toFixed(2)}x detour)`;
@ -368,6 +393,8 @@ function compileOptions(extra = {}) {
return { return {
render_mode: state.renderMode, render_mode: state.renderMode,
show_labels: state.showLabels, show_labels: state.showLabels,
show_annotations: false,
show_legend: false,
generic_symbols: true, generic_symbols: true,
...extra ...extra
}; };
@ -427,6 +454,58 @@ function refreshJsonEditor() {
el.jsonEditor.value = JSON.stringify(state.model, null, 2); el.jsonEditor.value = JSON.stringify(state.model, null, 2);
} }
function selectedComponentNote() {
if (!state.model || state.selectedNet || state.selectedPin || state.selectedRefs.length !== 1) {
return null;
}
const inst = instanceByRef(state.selectedRefs[0]);
if (!inst) {
return null;
}
const raw = String(inst.properties?.note ?? inst.properties?.notes ?? "").trim();
if (!raw) {
return null;
}
return raw.length > 240 ? `${raw.slice(0, 237)}...` : raw;
}
function updateSelectedNoteHint() {
if (!el.selectedNoteHint) {
return;
}
const note = selectedComponentNote();
if (!note || !state.compile?.layout || !state.model) {
el.selectedNoteHint.classList.add("hidden");
return;
}
const ref = state.selectedRefs[0];
const inst = instanceByRef(ref);
const sym = inst ? state.model.symbols?.[inst.symbol] : null;
const placed = (state.compile.layout.placed ?? []).find((p) => p.ref === ref);
if (!inst || !sym || !placed) {
el.selectedNoteHint.classList.add("hidden");
return;
}
el.selectedNoteHint.textContent = note;
el.selectedNoteHint.classList.remove("hidden");
el.selectedNoteHint.style.visibility = "hidden";
const viewportRect = el.canvasViewport.getBoundingClientRect();
const x = (placed.x + sym.body.width / 2) * state.scale + state.panX;
const y = (placed.y - 12) * state.scale + state.panY;
el.selectedNoteHint.style.left = `${Math.round(x)}px`;
el.selectedNoteHint.style.top = `${Math.round(y)}px`;
el.selectedNoteHint.style.transform = "translate(-50%, -100%)";
const hintRect = el.selectedNoteHint.getBoundingClientRect();
const minX = 14 + hintRect.width / 2;
const maxX = Math.max(minX, viewportRect.width - 14 - hintRect.width / 2);
const clampedX = Math.max(minX, Math.min(maxX, x));
el.selectedNoteHint.style.left = `${Math.round(clampedX)}px`;
el.selectedNoteHint.style.visibility = "";
}
function saveSnapshot() { function saveSnapshot() {
if (!state.model) { if (!state.model) {
return; return;
@ -451,6 +530,7 @@ function updateTransform() {
applyLabelDensityByZoom(svg); applyLabelDensityByZoom(svg);
resolveLabelCollisions(svg); resolveLabelCollisions(svg);
} }
updateSelectedNoteHint();
} }
function layoutBounds(layout, margin = FIT_MARGIN) { function layoutBounds(layout, margin = FIT_MARGIN) {
@ -746,8 +826,8 @@ function applyLabelDensityByZoom(svg) {
const pinLabels = svg.querySelectorAll("[data-pin-label]"); const pinLabels = svg.querySelectorAll("[data-pin-label]");
const valueLabels = svg.querySelectorAll("[data-value-label]"); const valueLabels = svg.querySelectorAll("[data-value-label]");
const refLabels = svg.querySelectorAll("[data-ref-label]"); const refLabels = svg.querySelectorAll("[data-ref-label]");
const dense = state.scale < 0.85; const dense = state.scale < 1.05;
const veryDense = state.scale < 0.65; const veryDense = state.scale < 0.8;
pinLabels.forEach((n) => { pinLabels.forEach((n) => {
n.style.display = dense ? "none" : ""; n.style.display = dense ? "none" : "";
@ -973,14 +1053,18 @@ function symbolPinRowHtml(pin) {
const sideOptions = PIN_SIDES.map((s) => `<option value="${s}" ${pin.side === s ? "selected" : ""}>${s}</option>`).join(""); const sideOptions = PIN_SIDES.map((s) => `<option value="${s}" ${pin.side === s ? "selected" : ""}>${s}</option>`).join("");
const typeOptions = PIN_TYPES.map((t) => `<option value="${t}" ${pin.type === t ? "selected" : ""}>${t}</option>`).join(""); const typeOptions = PIN_TYPES.map((t) => `<option value="${t}" ${pin.type === t ? "selected" : ""}>${t}</option>`).join("");
return `<div class="miniRow symbolPinRow" data-old-pin="${escHtml(pin.name)}"> return `<div class="miniRow symbolPinRow" data-old-pin="${escHtml(pin.name)}">
<input class="pinCol pinName" type="text" value="${escHtml(pin.name)}" placeholder="name" /> <div class="symbolPinMain">
<input class="pinCol pinNumber" type="text" value="${escHtml(pin.number)}" placeholder="number" /> <input class="pinCol pinName" type="text" value="${escHtml(pin.name)}" placeholder="name" />
<select class="pinCol pinSide">${sideOptions}</select> <input class="pinCol pinNumber" type="text" value="${escHtml(pin.number)}" placeholder="num" />
<input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" /> <select class="pinCol pinSide">${sideOptions}</select>
<select class="pinCol pinType">${typeOptions}</select> <input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
<button type="button" data-move-symbol-pin="up" title="Move pin up">Up</button> <select class="pinCol pinType">${typeOptions}</select>
<button type="button" data-move-symbol-pin="down" title="Move pin down">Down</button> </div>
<button type="button" data-remove-symbol-pin="${escHtml(pin.name)}">Remove</button> <div class="symbolPinActions">
<button type="button" data-move-symbol-pin="up" title="Move pin up">Up</button>
<button type="button" data-move-symbol-pin="down" title="Move pin down">Down</button>
<button type="button" data-remove-symbol-pin="${escHtml(pin.name)}">Remove</button>
</div>
</div>`; </div>`;
} }
@ -1301,7 +1385,7 @@ function renderSelected() {
el.lockedInput.checked = Boolean(inst.placement.locked); el.lockedInput.checked = Boolean(inst.placement.locked);
el.instRefInput.value = inst.ref; el.instRefInput.value = inst.ref;
el.instValueInput.value = String(inst.properties?.value ?? ""); el.instValueInput.value = String(inst.properties?.value ?? "");
el.instNotesInput.value = String(inst.properties?.notes ?? ""); el.instNotesInput.value = String(inst.properties?.note ?? inst.properties?.notes ?? "");
el.duplicateComponentBtn.disabled = false; el.duplicateComponentBtn.disabled = false;
el.deleteComponentBtn.disabled = false; el.deleteComponentBtn.disabled = false;
el.isolateSelectedComponentBtn.disabled = false; el.isolateSelectedComponentBtn.disabled = false;
@ -1693,7 +1777,11 @@ function bindSvgInteractions() {
} }
const pt = canvasToSvgPoint(evt.clientX, evt.clientY); const pt = canvasToSvgPoint(evt.clientX, evt.clientY);
const dragRefs = state.selectedRefs.length ? [...state.selectedRefs] : [ref]; const dragRefsRaw = state.selectedRefs.length ? [...state.selectedRefs] : [ref];
const dragRefs = dragRefsRaw.filter((r) => !instanceByRef(r)?.placement?.locked);
if (!dragRefs.length) {
return;
}
const baseByRef = {}; const baseByRef = {};
for (const r of dragRefs) { for (const r of dragRefs) {
const ii = state.model.instances.find((x) => x.ref === r); const ii = state.model.instances.find((x) => x.ref === r);
@ -1772,6 +1860,7 @@ function bindSvgInteractions() {
function renderCanvas() { function renderCanvas() {
if (!state.compile?.svg) { if (!state.compile?.svg) {
el.canvasInner.innerHTML = ""; el.canvasInner.innerHTML = "";
updateSelectedNoteHint();
return; return;
} }
@ -1784,6 +1873,7 @@ function renderCanvas() {
applyLabelDensityByZoom(svg); applyLabelDensityByZoom(svg);
resolveLabelCollisions(svg); resolveLabelCollisions(svg);
} }
updateSelectedNoteHint();
} }
function renderAll() { function renderAll() {
@ -1868,7 +1958,7 @@ function queueCompile(keepView = true, source = "edit") {
} }
state.compileDebounceId = setTimeout(() => { state.compileDebounceId = setTimeout(() => {
state.compileDebounceId = null; state.compileDebounceId = null;
compileModel(state.model, { keepView, source }); compileModel(state.model, { keepView, source, preservePlacement: true });
}, 150); }, 150);
} }
@ -2403,7 +2493,9 @@ async function runLayoutAction(path) {
try { try {
const out = await apiPost(path, { const out = await apiPost(path, {
payload: state.model, payload: state.model,
options: compileOptions() options: compileOptions({
respect_locks: true
})
}); });
state.model = applyCompileLayoutToModel(out.model, out.compile); state.model = applyCompileLayoutToModel(out.model, out.compile);
@ -2439,8 +2531,8 @@ async function resetToSample(opts = {}) {
state.isolateNet = false; state.isolateNet = false;
state.isolateComponent = false; state.isolateComponent = false;
state.userAdjustedView = false; state.userAdjustedView = false;
state.renderMode = "schematic_stub"; state.renderMode = "explicit";
el.renderModeSelect.value = "schematic_stub"; el.renderModeSelect.value = "explicit";
state.showLabels = true; state.showLabels = true;
el.showLabelsInput.checked = true; el.showLabelsInput.checked = true;
el.instanceFilter.value = ""; el.instanceFilter.value = "";
@ -2642,6 +2734,7 @@ function setupEvents() {
properties: { properties: {
...(inst.properties ?? {}), ...(inst.properties ?? {}),
value: el.instValueInput.value, value: el.instValueInput.value,
note: el.instNotesInput.value,
notes: el.instNotesInput.value notes: el.instNotesInput.value
} }
}); });
@ -3156,7 +3249,7 @@ function setupEvents() {
el.renderModeSelect.addEventListener("change", async () => { el.renderModeSelect.addEventListener("change", async () => {
state.renderMode = el.renderModeSelect.value; state.renderMode = el.renderModeSelect.value;
if (state.model) { if (state.model) {
await compileModel(state.model, { keepView: true }); await compileModel(state.model, { keepView: true, preservePlacement: true });
} }
}); });
@ -3374,7 +3467,7 @@ function setupEvents() {
inst.placement.rotation = ((Math.round(current / 90) * 90 + 90) % 360 + 360) % 360; inst.placement.rotation = ((Math.round(current / 90) * 90 + 90) % 360 + 360) % 360;
inst.placement.locked = true; inst.placement.locked = true;
} }
compileModel(state.model, { source: "rotate", keepView: true }); compileModel(state.model, { source: "rotate", keepView: true, preservePlacement: true });
evt.preventDefault(); evt.preventDefault();
return; return;
} }
@ -3646,6 +3739,10 @@ function setupEvents() {
await runLayoutAction("/layout/tidy"); await runLayoutAction("/layout/tidy");
}); });
el.themeToggleBtn?.addEventListener("click", () => {
applyTheme(state.theme === "dark" ? "light" : "dark");
});
el.undoBtn.addEventListener("click", async () => { el.undoBtn.addEventListener("click", async () => {
await performUndo(); await performUndo();
}); });
@ -3705,6 +3802,10 @@ function setupEvents() {
} }
(async function init() { (async function init() {
el.selectedNoteHint = document.createElement("div");
el.selectedNoteHint.className = "selectedNoteHint hidden";
el.canvasViewport.appendChild(el.selectedNoteHint);
applyTheme(selectThemePreference());
setupEvents(); setupEvents();
loadInspectorSectionState(); loadInspectorSectionState();
updateTransform(); updateTransform();

View File

@ -20,14 +20,15 @@
<button id="exportBtn" aria-label="Export Schemeta JSON file">Export JSON</button> <button id="exportBtn" aria-label="Export Schemeta JSON file">Export JSON</button>
<button id="autoLayoutBtn" aria-label="Run automatic layout">Auto Layout</button> <button id="autoLayoutBtn" aria-label="Run automatic layout">Auto Layout</button>
<button id="autoTidyBtn" aria-label="Run automatic tidy layout">Auto Tidy</button> <button id="autoTidyBtn" aria-label="Run automatic tidy layout">Auto Tidy</button>
<button id="themeToggleBtn" aria-label="Toggle dark mode">Dark</button>
<button id="shortcutsBtn" aria-label="Show keyboard shortcuts">Shortcuts</button> <button id="shortcutsBtn" aria-label="Show keyboard shortcuts">Shortcuts</button>
<button id="undoBtn" title="Undo (Ctrl/Cmd+Z)">Undo</button> <button id="undoBtn" title="Undo (Ctrl/Cmd+Z)">Undo</button>
<button id="redoBtn" title="Redo (Ctrl/Cmd+Shift+Z)">Redo</button> <button id="redoBtn" title="Redo (Ctrl/Cmd+Shift+Z)">Redo</button>
<label class="inlineSelect"> <label class="inlineSelect">
Mode Mode
<select id="renderModeSelect"> <select id="renderModeSelect">
<option value="schematic_stub">Schematic Stub</option>
<option value="explicit">Explicit Wires</option> <option value="explicit">Explicit Wires</option>
<option value="schematic_stub">Schematic Stub</option>
</select> </select>
</label> </label>
<input id="fileInput" type="file" accept="application/json,.json" hidden /> <input id="fileInput" type="file" accept="application/json,.json" hidden />

View File

@ -28,6 +28,33 @@
--shadow-2: 0 10px 24px rgba(16, 24, 40, 0.08); --shadow-2: 0 10px 24px rgba(16, 24, 40, 0.08);
} }
body.theme-dark {
--bg-0: #0b1220;
--bg-1: #0f172a;
--bg-2: #172237;
--panel: #0f1c2e;
--panel-strong: #13233a;
--canvas: #0d1728;
--ink: #e6edf7;
--ink-muted: #a7b6cd;
--ink-subtle: #7f91ad;
--line: #2a3c57;
--line-strong: #436086;
--accent: #60a5fa;
--accent-strong: #3b82f6;
--accent-soft: #162945;
--ok: #42ba86;
--warn: #f0b54f;
--err: #ef7367;
--power: #f59a4b;
--ground: #9ab0ca;
--clock: #f1835f;
--signal: #7aa4ff;
--analog: #4ec2ba;
--shadow-1: 0 1px 2px rgba(0, 0, 0, 0.35);
--shadow-2: 0 10px 24px rgba(0, 0, 0, 0.36);
}
* { * {
box-sizing: border-box; box-sizing: border-box;
} }
@ -38,6 +65,13 @@ body {
min-height: 100%; min-height: 100%;
} }
body.theme-dark {
background:
radial-gradient(circle at 0% 0%, #2f1f17 0 18%, transparent 30%),
radial-gradient(circle at 96% 5%, #122739 0 18%, transparent 30%),
linear-gradient(180deg, var(--bg-1), var(--bg-0));
}
body { body {
font-family: "IBM Plex Sans", "Manrope", "Segoe UI", sans-serif; font-family: "IBM Plex Sans", "Manrope", "Segoe UI", sans-serif;
color: var(--ink); color: var(--ink);
@ -85,7 +119,7 @@ button {
button:hover { button:hover {
border-color: var(--line-strong); border-color: var(--line-strong);
background: #f3f8ff; background: color-mix(in oklab, var(--accent-soft) 58%, var(--panel-strong));
} }
button:disabled { button:disabled {
@ -122,7 +156,7 @@ button.activeChip {
gap: 14px; gap: 14px;
padding: 10px 12px; padding: 10px 12px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, #f9fcff, #f2f8ff); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 88%, #ffffff), var(--panel));
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
position: sticky; position: sticky;
top: 0; top: 0;
@ -174,7 +208,7 @@ button.activeChip {
.pane { .pane {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: linear-gradient(180deg, var(--panel), #f6faff); background: linear-gradient(180deg, var(--panel), color-mix(in oklab, var(--panel) 88%, #081120));
box-shadow: var(--shadow-1); box-shadow: var(--shadow-1);
} }
@ -189,7 +223,7 @@ button.activeChip {
.pane.center { .pane.center {
overflow: hidden; overflow: hidden;
background: linear-gradient(180deg, #fcfeff, #f6faff); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 90%, #ffffff), var(--panel));
} }
.sectionHead { .sectionHead {
@ -217,7 +251,7 @@ select {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: 8px; padding: 8px;
background: #fff; background: var(--panel-strong);
color: var(--ink); color: var(--ink);
} }
@ -241,7 +275,7 @@ textarea {
overflow: auto; overflow: auto;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.72); background: color-mix(in oklab, var(--panel-strong) 84%, transparent);
} }
.list li { .list li {
@ -252,7 +286,7 @@ textarea {
} }
.list li:hover { .list li:hover {
background: #f2f7ff; background: color-mix(in oklab, var(--accent-soft) 70%, var(--panel-strong));
} }
.list li:last-child { .list li:last-child {
@ -290,7 +324,7 @@ textarea {
.netLegend { .netLegend {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: #fbfdff; background: var(--panel-strong);
padding: 8px; padding: 8px;
display: grid; display: grid;
gap: 6px; gap: 6px;
@ -334,7 +368,7 @@ textarea {
.card { .card {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: #fbfdff; background: var(--panel-strong);
padding: 8px; padding: 8px;
color: var(--ink-muted); color: var(--ink-muted);
font-size: 0.85rem; font-size: 0.85rem;
@ -346,7 +380,7 @@ textarea {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: 9px; padding: 9px;
background: #f8fbff; background: color-mix(in oklab, var(--panel-strong) 86%, var(--panel));
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
@ -356,7 +390,7 @@ textarea {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
margin-top: 8px; margin-top: 8px;
background: #ffffffcc; background: color-mix(in oklab, var(--panel-strong) 88%, transparent);
overflow: hidden; overflow: hidden;
} }
@ -370,12 +404,12 @@ textarea {
} }
.editorSection > summary:hover { .editorSection > summary:hover {
background: #f3f8ff; background: color-mix(in oklab, var(--accent-soft) 60%, var(--panel-strong));
} }
.editorSection[open] > summary { .editorSection[open] > summary {
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
background: #eef4ff; background: color-mix(in oklab, var(--accent-soft) 76%, var(--panel-strong));
} }
.editorSection > summary::-webkit-details-marker { .editorSection > summary::-webkit-details-marker {
@ -402,7 +436,7 @@ textarea {
.miniList { .miniList {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: #fff; background: var(--panel-strong);
max-height: 180px; max-height: 180px;
overflow: auto; overflow: auto;
} }
@ -423,8 +457,27 @@ textarea {
.symbolPinRow { .symbolPinRow {
display: grid; display: grid;
align-items: center; align-items: start;
grid-template-columns: 1fr 0.9fr 0.9fr 0.8fr 1fr auto auto auto; grid-template-columns: 1fr;
gap: 6px;
}
.symbolPinMain {
display: grid;
grid-template-columns: minmax(72px, 1fr) minmax(58px, 0.8fr) minmax(76px, 0.9fr) minmax(66px, 0.7fr) minmax(110px, 1fr);
gap: 6px;
}
.symbolPinActions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.symbolPinActions button {
min-width: 0;
padding: 4px 6px;
font-size: 0.75rem;
} }
.pinCol { .pinCol {
@ -457,7 +510,7 @@ textarea {
gap: 8px; gap: 8px;
padding: 8px; padding: 8px;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
background: linear-gradient(180deg, #f8fbff, #f2f7ff); background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 90%, #ffffff), var(--panel));
} }
#compileStatus { #compileStatus {
@ -477,9 +530,9 @@ textarea {
overflow: hidden; overflow: hidden;
cursor: grab; cursor: grab;
background: background:
linear-gradient(0deg, #d9e4f0 1px, transparent 1px), linear-gradient(0deg, color-mix(in oklab, var(--line) 72%, transparent) 1px, transparent 1px),
linear-gradient(90deg, #d9e4f0 1px, transparent 1px), linear-gradient(90deg, color-mix(in oklab, var(--line) 72%, transparent) 1px, transparent 1px),
linear-gradient(180deg, var(--canvas), #edf4fc); linear-gradient(180deg, var(--canvas), color-mix(in oklab, var(--canvas) 82%, #1c2f49));
background-size: 20px 20px, 20px 20px, auto; background-size: 20px 20px, 20px 20px, auto;
} }
@ -514,11 +567,26 @@ textarea {
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
padding: 6px 8px; padding: 6px 8px;
color: var(--ink); color: var(--ink);
background: rgba(255, 255, 255, 0.95); background: color-mix(in oklab, var(--panel-strong) 92%, transparent);
box-shadow: var(--shadow-2); box-shadow: var(--shadow-2);
font-size: 0.74rem; font-size: 0.74rem;
} }
.selectedNoteHint {
position: absolute;
z-index: 18;
pointer-events: none;
max-width: min(460px, 56vw);
border: 1px solid var(--line);
border-radius: 10px;
background: color-mix(in oklab, var(--panel-strong) 92%, transparent);
color: var(--ink);
box-shadow: var(--shadow-2);
font-size: 0.78rem;
line-height: 1.35;
padding: 8px 10px;
}
.hidden { .hidden {
display: none; display: none;
} }
@ -547,11 +615,11 @@ textarea {
padding: 7px; padding: 7px;
margin-bottom: 6px; margin-bottom: 6px;
cursor: pointer; cursor: pointer;
background: #fff; background: var(--panel-strong);
} }
.issueRow:hover { .issueRow:hover {
background: #f4f8ff; background: color-mix(in oklab, var(--accent-soft) 66%, var(--panel-strong));
} }
.issueErr { .issueErr {
@ -604,7 +672,7 @@ textarea {
height: min(88vh, 900px); height: min(88vh, 900px);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: #fff; background: var(--panel-strong);
box-shadow: 0 24px 60px rgba(16, 24, 40, 0.24); box-shadow: 0 24px 60px rgba(16, 24, 40, 0.24);
padding: 12px; padding: 12px;
display: flex; display: flex;
@ -658,7 +726,7 @@ textarea {
padding: 8px 10px; padding: 8px 10px;
font-size: 0.86rem; font-size: 0.86rem;
color: var(--ink-muted); color: var(--ink-muted);
background: #f8fbff; background: color-mix(in oklab, var(--panel-strong) 88%, var(--panel));
} }
kbd { kbd {
@ -667,7 +735,7 @@ kbd {
border: 1px solid var(--line-strong); border: 1px solid var(--line-strong);
border-bottom-width: 2px; border-bottom-width: 2px;
border-radius: 6px; border-radius: 6px;
background: #fff; background: var(--panel-strong);
padding: 2px 6px; padding: 2px 6px;
color: var(--ink); color: var(--ink);
font-size: 0.74rem; font-size: 0.74rem;
@ -679,7 +747,7 @@ kbd {
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
overflow: auto; overflow: auto;
max-height: 46vh; max-height: 46vh;
background: #fff; background: var(--panel-strong);
} }
.commandRow { .commandRow {
@ -687,7 +755,7 @@ kbd {
border: none; border: none;
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
border-radius: 0; border-radius: 0;
background: #fff; background: var(--panel-strong);
text-align: left; text-align: left;
box-shadow: none; box-shadow: none;
padding: 8px 10px; padding: 8px 10px;
@ -701,7 +769,7 @@ kbd {
.commandRow:hover, .commandRow:hover,
.commandRow.active { .commandRow.active {
background: #edf4ff; background: color-mix(in oklab, var(--accent-soft) 70%, var(--panel-strong));
} }
.flash { .flash {
@ -801,6 +869,10 @@ kbd {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.symbolPinMain {
grid-template-columns: 1fr 1fr;
}
.jsonActions { .jsonActions {
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;

View File

@ -441,6 +441,7 @@ function renderAnnotationPanel(entries, maxWidth = 380) {
export function renderSvgFromLayout(model, layout, options = {}) { export function renderSvgFromLayout(model, layout, options = {}) {
const showLabels = options.show_labels !== false; const showLabels = options.show_labels !== false;
const showAnnotations = options.show_annotations !== false; const showAnnotations = options.show_annotations !== false;
const showLegend = options.show_legend !== false;
const pinNets = pinNetMap(model); const pinNets = pinNetMap(model);
const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class])); const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class]));
const allPinPoints = []; const allPinPoints = [];
@ -635,7 +636,7 @@ export function renderSvgFromLayout(model, layout, options = {}) {
...(annotationPanel.height > 0 ...(annotationPanel.height > 0
? [{ x: annotationPanel.x - 2, y: annotationPanel.y - 2, width: annotationPanel.width + 4, height: annotationPanel.height + 4 }] ? [{ x: annotationPanel.x - 2, y: annotationPanel.y - 2, width: annotationPanel.width + 4, height: annotationPanel.height + 4 }]
: []), : []),
{ x: 6, y: legendY - 8, width: 126, height: 86 }, ...(showLegend ? [{ x: 6, y: legendY - 8, width: 126, height: 86 }] : []),
...componentRects ...componentRects
]; ];
@ -761,7 +762,7 @@ export function renderSvgFromLayout(model, layout, options = {}) {
<g data-layer="net-labels">${labelLayer}</g> <g data-layer="net-labels">${labelLayer}</g>
<g data-layer="bus-groups">${busLabels}</g> <g data-layer="bus-groups">${busLabels}</g>
${annotationPanel.svg} ${annotationPanel.svg}
${renderLegend(legendY)} ${showLegend ? renderLegend(legendY) : ""}
</svg>`; </svg>`;
} }