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
141
frontend/app.js
141
frontend/app.js
@ -11,6 +11,7 @@ const MIN_SCALE = 0.2;
|
||||
const MAX_SCALE = 5;
|
||||
const FIT_MARGIN = 56;
|
||||
const FOCUS_MARGIN = 96;
|
||||
const THEME_KEY = "schemeta:theme:v1";
|
||||
|
||||
const state = {
|
||||
model: null,
|
||||
@ -31,7 +32,8 @@ const state = {
|
||||
showLabels: true,
|
||||
isolateNet: false,
|
||||
isolateComponent: false,
|
||||
renderMode: "schematic_stub",
|
||||
renderMode: "explicit",
|
||||
theme: "dark",
|
||||
userAdjustedView: false,
|
||||
spacePan: false,
|
||||
schemaText: "",
|
||||
@ -141,6 +143,7 @@ const el = {
|
||||
copyReproBtn: document.getElementById("copyReproBtn"),
|
||||
autoLayoutBtn: document.getElementById("autoLayoutBtn"),
|
||||
autoTidyBtn: document.getElementById("autoTidyBtn"),
|
||||
themeToggleBtn: document.getElementById("themeToggleBtn"),
|
||||
shortcutsBtn: document.getElementById("shortcutsBtn"),
|
||||
undoBtn: document.getElementById("undoBtn"),
|
||||
redoBtn: document.getElementById("redoBtn"),
|
||||
@ -158,7 +161,8 @@ const el = {
|
||||
commandModal: document.getElementById("commandModal"),
|
||||
closeCommandBtn: document.getElementById("closeCommandBtn"),
|
||||
commandInput: document.getElementById("commandInput"),
|
||||
commandList: document.getElementById("commandList")
|
||||
commandList: document.getElementById("commandList"),
|
||||
selectedNoteHint: null
|
||||
};
|
||||
|
||||
function toGrid(v) {
|
||||
@ -348,6 +352,27 @@ function setStatus(text, ok = true) {
|
||||
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) {
|
||||
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)`;
|
||||
@ -368,6 +393,8 @@ function compileOptions(extra = {}) {
|
||||
return {
|
||||
render_mode: state.renderMode,
|
||||
show_labels: state.showLabels,
|
||||
show_annotations: false,
|
||||
show_legend: false,
|
||||
generic_symbols: true,
|
||||
...extra
|
||||
};
|
||||
@ -427,6 +454,58 @@ function refreshJsonEditor() {
|
||||
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() {
|
||||
if (!state.model) {
|
||||
return;
|
||||
@ -451,6 +530,7 @@ function updateTransform() {
|
||||
applyLabelDensityByZoom(svg);
|
||||
resolveLabelCollisions(svg);
|
||||
}
|
||||
updateSelectedNoteHint();
|
||||
}
|
||||
|
||||
function layoutBounds(layout, margin = FIT_MARGIN) {
|
||||
@ -746,8 +826,8 @@ function applyLabelDensityByZoom(svg) {
|
||||
const pinLabels = svg.querySelectorAll("[data-pin-label]");
|
||||
const valueLabels = svg.querySelectorAll("[data-value-label]");
|
||||
const refLabels = svg.querySelectorAll("[data-ref-label]");
|
||||
const dense = state.scale < 0.85;
|
||||
const veryDense = state.scale < 0.65;
|
||||
const dense = state.scale < 1.05;
|
||||
const veryDense = state.scale < 0.8;
|
||||
|
||||
pinLabels.forEach((n) => {
|
||||
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 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)}">
|
||||
<input class="pinCol pinName" type="text" value="${escHtml(pin.name)}" placeholder="name" />
|
||||
<input class="pinCol pinNumber" type="text" value="${escHtml(pin.number)}" placeholder="number" />
|
||||
<select class="pinCol pinSide">${sideOptions}</select>
|
||||
<input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
|
||||
<select class="pinCol pinType">${typeOptions}</select>
|
||||
<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 class="symbolPinMain">
|
||||
<input class="pinCol pinName" type="text" value="${escHtml(pin.name)}" placeholder="name" />
|
||||
<input class="pinCol pinNumber" type="text" value="${escHtml(pin.number)}" placeholder="num" />
|
||||
<select class="pinCol pinSide">${sideOptions}</select>
|
||||
<input class="pinCol pinOffset" type="number" min="0" step="1" value="${Number(pin.offset ?? 0)}" />
|
||||
<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="down" title="Move pin down">Down</button>
|
||||
<button type="button" data-remove-symbol-pin="${escHtml(pin.name)}">Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@ -1301,7 +1385,7 @@ function renderSelected() {
|
||||
el.lockedInput.checked = Boolean(inst.placement.locked);
|
||||
el.instRefInput.value = inst.ref;
|
||||
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.deleteComponentBtn.disabled = false;
|
||||
el.isolateSelectedComponentBtn.disabled = false;
|
||||
@ -1693,7 +1777,11 @@ function bindSvgInteractions() {
|
||||
}
|
||||
|
||||
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 = {};
|
||||
for (const r of dragRefs) {
|
||||
const ii = state.model.instances.find((x) => x.ref === r);
|
||||
@ -1772,6 +1860,7 @@ function bindSvgInteractions() {
|
||||
function renderCanvas() {
|
||||
if (!state.compile?.svg) {
|
||||
el.canvasInner.innerHTML = "";
|
||||
updateSelectedNoteHint();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1784,6 +1873,7 @@ function renderCanvas() {
|
||||
applyLabelDensityByZoom(svg);
|
||||
resolveLabelCollisions(svg);
|
||||
}
|
||||
updateSelectedNoteHint();
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
@ -1868,7 +1958,7 @@ function queueCompile(keepView = true, source = "edit") {
|
||||
}
|
||||
state.compileDebounceId = setTimeout(() => {
|
||||
state.compileDebounceId = null;
|
||||
compileModel(state.model, { keepView, source });
|
||||
compileModel(state.model, { keepView, source, preservePlacement: true });
|
||||
}, 150);
|
||||
}
|
||||
|
||||
@ -2403,7 +2493,9 @@ async function runLayoutAction(path) {
|
||||
try {
|
||||
const out = await apiPost(path, {
|
||||
payload: state.model,
|
||||
options: compileOptions()
|
||||
options: compileOptions({
|
||||
respect_locks: true
|
||||
})
|
||||
});
|
||||
|
||||
state.model = applyCompileLayoutToModel(out.model, out.compile);
|
||||
@ -2439,8 +2531,8 @@ async function resetToSample(opts = {}) {
|
||||
state.isolateNet = false;
|
||||
state.isolateComponent = false;
|
||||
state.userAdjustedView = false;
|
||||
state.renderMode = "schematic_stub";
|
||||
el.renderModeSelect.value = "schematic_stub";
|
||||
state.renderMode = "explicit";
|
||||
el.renderModeSelect.value = "explicit";
|
||||
state.showLabels = true;
|
||||
el.showLabelsInput.checked = true;
|
||||
el.instanceFilter.value = "";
|
||||
@ -2642,6 +2734,7 @@ function setupEvents() {
|
||||
properties: {
|
||||
...(inst.properties ?? {}),
|
||||
value: el.instValueInput.value,
|
||||
note: el.instNotesInput.value,
|
||||
notes: el.instNotesInput.value
|
||||
}
|
||||
});
|
||||
@ -3156,7 +3249,7 @@ function setupEvents() {
|
||||
el.renderModeSelect.addEventListener("change", async () => {
|
||||
state.renderMode = el.renderModeSelect.value;
|
||||
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.locked = true;
|
||||
}
|
||||
compileModel(state.model, { source: "rotate", keepView: true });
|
||||
compileModel(state.model, { source: "rotate", keepView: true, preservePlacement: true });
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
@ -3646,6 +3739,10 @@ function setupEvents() {
|
||||
await runLayoutAction("/layout/tidy");
|
||||
});
|
||||
|
||||
el.themeToggleBtn?.addEventListener("click", () => {
|
||||
applyTheme(state.theme === "dark" ? "light" : "dark");
|
||||
});
|
||||
|
||||
el.undoBtn.addEventListener("click", async () => {
|
||||
await performUndo();
|
||||
});
|
||||
@ -3705,6 +3802,10 @@ function setupEvents() {
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
el.selectedNoteHint = document.createElement("div");
|
||||
el.selectedNoteHint.className = "selectedNoteHint hidden";
|
||||
el.canvasViewport.appendChild(el.selectedNoteHint);
|
||||
applyTheme(selectThemePreference());
|
||||
setupEvents();
|
||||
loadInspectorSectionState();
|
||||
updateTransform();
|
||||
|
||||
@ -20,14 +20,15 @@
|
||||
<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="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="undoBtn" title="Undo (Ctrl/Cmd+Z)">Undo</button>
|
||||
<button id="redoBtn" title="Redo (Ctrl/Cmd+Shift+Z)">Redo</button>
|
||||
<label class="inlineSelect">
|
||||
Mode
|
||||
<select id="renderModeSelect">
|
||||
<option value="schematic_stub">Schematic Stub</option>
|
||||
<option value="explicit">Explicit Wires</option>
|
||||
<option value="schematic_stub">Schematic Stub</option>
|
||||
</select>
|
||||
</label>
|
||||
<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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@ -38,6 +65,13 @@ body {
|
||||
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 {
|
||||
font-family: "IBM Plex Sans", "Manrope", "Segoe UI", sans-serif;
|
||||
color: var(--ink);
|
||||
@ -85,7 +119,7 @@ button {
|
||||
|
||||
button:hover {
|
||||
border-color: var(--line-strong);
|
||||
background: #f3f8ff;
|
||||
background: color-mix(in oklab, var(--accent-soft) 58%, var(--panel-strong));
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@ -122,7 +156,7 @@ button.activeChip {
|
||||
gap: 14px;
|
||||
padding: 10px 12px;
|
||||
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);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@ -174,7 +208,7 @@ button.activeChip {
|
||||
.pane {
|
||||
border: 1px solid var(--line);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -189,7 +223,7 @@ button.activeChip {
|
||||
|
||||
.pane.center {
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #fcfeff, #f6faff);
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 90%, #ffffff), var(--panel));
|
||||
}
|
||||
|
||||
.sectionHead {
|
||||
@ -217,7 +251,7 @@ select {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px;
|
||||
background: #fff;
|
||||
background: var(--panel-strong);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@ -241,7 +275,7 @@ textarea {
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
background: color-mix(in oklab, var(--panel-strong) 84%, transparent);
|
||||
}
|
||||
|
||||
.list li {
|
||||
@ -252,7 +286,7 @@ textarea {
|
||||
}
|
||||
|
||||
.list li:hover {
|
||||
background: #f2f7ff;
|
||||
background: color-mix(in oklab, var(--accent-soft) 70%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.list li:last-child {
|
||||
@ -290,7 +324,7 @@ textarea {
|
||||
.netLegend {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fbfdff;
|
||||
background: var(--panel-strong);
|
||||
padding: 8px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@ -334,7 +368,7 @@ textarea {
|
||||
.card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fbfdff;
|
||||
background: var(--panel-strong);
|
||||
padding: 8px;
|
||||
color: var(--ink-muted);
|
||||
font-size: 0.85rem;
|
||||
@ -346,7 +380,7 @@ textarea {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 9px;
|
||||
background: #f8fbff;
|
||||
background: color-mix(in oklab, var(--panel-strong) 86%, var(--panel));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@ -356,7 +390,7 @@ textarea {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-top: 8px;
|
||||
background: #ffffffcc;
|
||||
background: color-mix(in oklab, var(--panel-strong) 88%, transparent);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@ -370,12 +404,12 @@ textarea {
|
||||
}
|
||||
|
||||
.editorSection > summary:hover {
|
||||
background: #f3f8ff;
|
||||
background: color-mix(in oklab, var(--accent-soft) 60%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.editorSection[open] > summary {
|
||||
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 {
|
||||
@ -402,7 +436,7 @@ textarea {
|
||||
.miniList {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #fff;
|
||||
background: var(--panel-strong);
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
}
|
||||
@ -423,8 +457,27 @@ textarea {
|
||||
|
||||
.symbolPinRow {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1fr 0.9fr 0.9fr 0.8fr 1fr auto auto auto;
|
||||
align-items: start;
|
||||
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 {
|
||||
@ -457,7 +510,7 @@ textarea {
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
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 {
|
||||
@ -477,9 +530,9 @@ textarea {
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
background:
|
||||
linear-gradient(0deg, #d9e4f0 1px, transparent 1px),
|
||||
linear-gradient(90deg, #d9e4f0 1px, transparent 1px),
|
||||
linear-gradient(180deg, var(--canvas), #edf4fc);
|
||||
linear-gradient(0deg, color-mix(in oklab, var(--line) 72%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(90deg, color-mix(in oklab, var(--line) 72%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(180deg, var(--canvas), color-mix(in oklab, var(--canvas) 82%, #1c2f49));
|
||||
background-size: 20px 20px, 20px 20px, auto;
|
||||
}
|
||||
|
||||
@ -514,11 +567,26 @@ textarea {
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 8px;
|
||||
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);
|
||||
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 {
|
||||
display: none;
|
||||
}
|
||||
@ -547,11 +615,11 @@ textarea {
|
||||
padding: 7px;
|
||||
margin-bottom: 6px;
|
||||
cursor: pointer;
|
||||
background: #fff;
|
||||
background: var(--panel-strong);
|
||||
}
|
||||
|
||||
.issueRow:hover {
|
||||
background: #f4f8ff;
|
||||
background: color-mix(in oklab, var(--accent-soft) 66%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.issueErr {
|
||||
@ -604,7 +672,7 @@ textarea {
|
||||
height: min(88vh, 900px);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg);
|
||||
background: #fff;
|
||||
background: var(--panel-strong);
|
||||
box-shadow: 0 24px 60px rgba(16, 24, 40, 0.24);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
@ -658,7 +726,7 @@ textarea {
|
||||
padding: 8px 10px;
|
||||
font-size: 0.86rem;
|
||||
color: var(--ink-muted);
|
||||
background: #f8fbff;
|
||||
background: color-mix(in oklab, var(--panel-strong) 88%, var(--panel));
|
||||
}
|
||||
|
||||
kbd {
|
||||
@ -667,7 +735,7 @@ kbd {
|
||||
border: 1px solid var(--line-strong);
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
background: var(--panel-strong);
|
||||
padding: 2px 6px;
|
||||
color: var(--ink);
|
||||
font-size: 0.74rem;
|
||||
@ -679,7 +747,7 @@ kbd {
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: auto;
|
||||
max-height: 46vh;
|
||||
background: #fff;
|
||||
background: var(--panel-strong);
|
||||
}
|
||||
|
||||
.commandRow {
|
||||
@ -687,7 +755,7 @@ kbd {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
background: #fff;
|
||||
background: var(--panel-strong);
|
||||
text-align: left;
|
||||
box-shadow: none;
|
||||
padding: 8px 10px;
|
||||
@ -701,7 +769,7 @@ kbd {
|
||||
|
||||
.commandRow:hover,
|
||||
.commandRow.active {
|
||||
background: #edf4ff;
|
||||
background: color-mix(in oklab, var(--accent-soft) 70%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.flash {
|
||||
@ -801,6 +869,10 @@ kbd {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.symbolPinMain {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.jsonActions {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
@ -441,6 +441,7 @@ function renderAnnotationPanel(entries, maxWidth = 380) {
|
||||
export function renderSvgFromLayout(model, layout, options = {}) {
|
||||
const showLabels = options.show_labels !== false;
|
||||
const showAnnotations = options.show_annotations !== false;
|
||||
const showLegend = options.show_legend !== false;
|
||||
const pinNets = pinNetMap(model);
|
||||
const netClassByName = new Map((model.nets ?? []).map((n) => [n.name, n.class]));
|
||||
const allPinPoints = [];
|
||||
@ -635,7 +636,7 @@ export function renderSvgFromLayout(model, layout, options = {}) {
|
||||
...(annotationPanel.height > 0
|
||||
? [{ 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
|
||||
];
|
||||
|
||||
@ -761,7 +762,7 @@ export function renderSvgFromLayout(model, layout, options = {}) {
|
||||
<g data-layer="net-labels">${labelLayer}</g>
|
||||
<g data-layer="bus-groups">${busLabels}</g>
|
||||
${annotationPanel.svg}
|
||||
${renderLegend(legendY)}
|
||||
${showLegend ? renderLegend(legendY) : ""}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user