Polish legacy UI: dark theme, note hints, lock-safe drag, and cleaner symbol editor
Some checks are pending
CI / test (push) Waiting to run
Some checks are pending
CI / test (push) Waiting to run
This commit is contained in:
parent
46175efe1b
commit
9a21e111d0
127
frontend/app.js
127
frontend/app.js
@ -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)}">
|
||||||
|
<div class="symbolPinMain">
|
||||||
<input class="pinCol pinName" type="text" value="${escHtml(pin.name)}" placeholder="name" />
|
<input class="pinCol pinName" type="text" value="${escHtml(pin.name)}" placeholder="name" />
|
||||||
<input class="pinCol pinNumber" type="text" value="${escHtml(pin.number)}" placeholder="number" />
|
<input class="pinCol pinNumber" type="text" value="${escHtml(pin.number)}" placeholder="num" />
|
||||||
<select class="pinCol pinSide">${sideOptions}</select>
|
<select class="pinCol pinSide">${sideOptions}</select>
|
||||||
<input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
|
<input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
|
||||||
<select class="pinCol pinType">${typeOptions}</select>
|
<select class="pinCol pinType">${typeOptions}</select>
|
||||||
|
</div>
|
||||||
|
<div class="symbolPinActions">
|
||||||
<button type="button" data-move-symbol-pin="up" title="Move pin up">Up</button>
|
<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-move-symbol-pin="down" title="Move pin down">Down</button>
|
||||||
<button type="button" data-remove-symbol-pin="${escHtml(pin.name)}">Remove</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();
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user