feat(ui): port editor + add-port form in the sidebar inspector
m: 'Add port' should be a sidebar form, not a two-step canvas gesture.
- Port inspector gains a Type dropdown (read /api/cable-types via
state.cableTypes, PATCH /ports/:id with type_id). Edge picker + label
+ delete from prior shift are unchanged.
- New "Add port" form rendered from selection.kind === "port_new":
Type / Edge / Label, Create + Cancel buttons. Default label is the
next free index for the chosen type on this device ("HDMI 3" if two
HDMIs already live there). Recomputes when m changes the type, but
stops recomputing as soon as m hand-edits the label.
- +Port in the device inspector now flips selection to port_new,
rendering the form. Submit → POST → switch to the new port's editor.
No second canvas click required.
- Clicking a port row in the device inspector's port list selects that
port and opens its editor (same surface as canvas-click).
- "← <device name>" back-link in both port editor and add-port form
jumps back to the device inspector.
Removed: state.tool === "port" branch, armPortTool helper, placePortAt
function, .tool-port CSS, state.portToolDevice / portToolTypeID. The
canvas-armed +Port tool was the user-trip-wire perseus flagged; the
sidebar form replaces it entirely.
snapToDeviceEdge also removed — placePortAt was its only caller; the
edgeCentre + portEdge + relayoutEdge trio fully owns port placement
now.
Port rows in the device inspector get a hover background + pointer
cursor to read as clickable.
This commit is contained in:
@@ -56,14 +56,11 @@ const state = {
|
|||||||
/** @type {Bundle[]} */ bundles: [],
|
/** @type {Bundle[]} */ bundles: [],
|
||||||
/** @type {SetupTemplate[]} */ setupTemplates: [],
|
/** @type {SetupTemplate[]} */ setupTemplates: [],
|
||||||
activeTypeId: /** @type {number|null} */ (null),
|
activeTypeId: /** @type {number|null} */ (null),
|
||||||
/** "frame" | "device" | "io" | "req" | "port" | "cable" | null */
|
/** "frame" | "device" | "io" | "req" | "cable" | null */
|
||||||
tool: /** @type {string|null} */ (null),
|
tool: /** @type {string|null} */ (null),
|
||||||
/** Slice-7 transient state for the +Port tool. */
|
|
||||||
portToolDevice: /** @type {number|null} */ (null),
|
|
||||||
portToolTypeID: /** @type {number|null} */ (null),
|
|
||||||
/** Slice-7: when the user clicked a source port, this is its id. */
|
/** Slice-7: when the user clicked a source port, this is its id. */
|
||||||
cableDrawFromPortID: /** @type {number|null} */ (null),
|
cableDrawFromPortID: /** @type {number|null} */ (null),
|
||||||
/** @type {{kind: "frame"|"device"|"io"|"cable_type"|"requirement"|"cable"|"bundle"|"port", id: number} | null} */ selection: null,
|
/** @type {({kind: "frame"|"device"|"io"|"cable_type"|"requirement"|"cable"|"bundle"|"port", id: number} | {kind: "port_new", device_id: number}) | null} */ selection: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- API client ---------- //
|
// ---------- API client ---------- //
|
||||||
@@ -441,6 +438,7 @@ function renderInspector() {
|
|||||||
case "requirement": return renderInspectorRequirement(body, state.selection.id);
|
case "requirement": return renderInspectorRequirement(body, state.selection.id);
|
||||||
case "cable": return renderInspectorCable(body, state.selection.id);
|
case "cable": return renderInspectorCable(body, state.selection.id);
|
||||||
case "port": return renderInspectorPort(body, state.selection.id);
|
case "port": return renderInspectorPort(body, state.selection.id);
|
||||||
|
case "port_new": return renderInspectorPortNew(body, state.selection.device_id);
|
||||||
default: body.innerHTML = `<p class="muted">Nothing selected.</p>`;
|
default: body.innerHTML = `<p class="muted">Nothing selected.</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -727,13 +725,24 @@ function renderInspectorDevice(body, id) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// +Port — arms the port-placement gesture. Active cable type comes
|
// +Port — switch the inspector to the new-port form. m fills in
|
||||||
// from the legend selection; if none, defaults to the first cable_type.
|
// type + edge + label and clicks Create; no canvas click required.
|
||||||
body.querySelector("#dev-add-port").addEventListener("click", () => {
|
body.querySelector("#dev-add-port").addEventListener("click", () => {
|
||||||
if (!state.active) return;
|
if (!state.active) return;
|
||||||
const typeID = state.activeTypeId ?? state.cableTypes[0]?.id;
|
state.selection = { kind: "port_new", device_id: d.id };
|
||||||
if (!typeID) { alert("Pick a cable type in the legend first"); return; }
|
render();
|
||||||
armPortTool(d.id, typeID);
|
});
|
||||||
|
|
||||||
|
// Clicking a port row in the device's port list selects that port
|
||||||
|
// and opens its editor in the inspector pane.
|
||||||
|
body.querySelectorAll(".port-row[data-port-id]").forEach((row) => {
|
||||||
|
row.addEventListener("click", (e) => {
|
||||||
|
if (e.target instanceof HTMLElement && e.target.closest(".port-del")) return;
|
||||||
|
const pid = Number(row.getAttribute("data-port-id"));
|
||||||
|
if (!pid) return;
|
||||||
|
state.selection = { kind: "port", id: pid };
|
||||||
|
render();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Per-port delete.
|
// Per-port delete.
|
||||||
@@ -1003,27 +1012,26 @@ function renderInspectorIO(body, id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slice 7 follow-up: m can select a port to edit its edge / label / delete.
|
// Port editor — type / edge / label / delete. m can also navigate back
|
||||||
|
// to the device by clicking "back to device" or anywhere on the device.
|
||||||
function renderInspectorPort(body, id) {
|
function renderInspectorPort(body, id) {
|
||||||
const prt = state.ports.find((p) => p.id === id);
|
const prt = state.ports.find((p) => p.id === id);
|
||||||
if (!prt) { body.innerHTML = ""; return; }
|
if (!prt) { body.innerHTML = ""; return; }
|
||||||
const dev = state.devices.find((d) => d.id === prt.device_id);
|
const dev = state.devices.find((d) => d.id === prt.device_id);
|
||||||
if (!dev) { body.innerHTML = ""; return; }
|
if (!dev) { body.innerHTML = ""; return; }
|
||||||
const ct = state.cableTypes.find((t) => t.id === prt.type_id);
|
|
||||||
const ctColor = ct?.color || "#888";
|
|
||||||
const ctName = ct?.name || "?";
|
|
||||||
const currentEdge = portEdge(prt, dev);
|
const currentEdge = portEdge(prt, dev);
|
||||||
|
const typeOptions = state.cableTypes
|
||||||
|
.map((t) => `<option value="${t.id}">${escapeHtml(t.name)}</option>`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
body.innerHTML = `
|
body.innerHTML = `
|
||||||
<p class="section-title">Port</p>
|
<p class="section-title">Port</p>
|
||||||
<dl>
|
<p style="font-size:12px;margin:0 0 8px 0;">
|
||||||
<dt>device</dt><dd>${dev.name}</dd>
|
<a href="#" id="port-back-device" class="btn-link">← ${escapeHtml(dev.name)}</a>
|
||||||
<dt>type</dt>
|
</p>
|
||||||
<dd><span class="swatch" style="background:${ctColor}"></span>${ctName}</dd>
|
|
||||||
</dl>
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Label</span>
|
<span>Type</span>
|
||||||
<input class="inline-input" id="port-label" value="" />
|
<select id="port-type">${typeOptions}</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Edge</span>
|
<span>Edge</span>
|
||||||
@@ -1034,12 +1042,36 @@ function renderInspectorPort(body, id) {
|
|||||||
<option value="left">Left</option>
|
<option value="left">Left</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Label</span>
|
||||||
|
<input class="inline-input" id="port-label" value="" />
|
||||||
|
</label>
|
||||||
<div class="inspector-actions">
|
<div class="inspector-actions">
|
||||||
<button type="button" class="btn btn-danger btn-tiny" id="port-delete">Delete</button>
|
<button type="button" class="btn btn-danger btn-tiny" id="port-delete">Delete</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
body.querySelector("#port-label").value = prt.label ?? "";
|
body.querySelector("#port-type").value = String(prt.type_id);
|
||||||
body.querySelector("#port-edge").value = currentEdge;
|
body.querySelector("#port-edge").value = currentEdge;
|
||||||
|
body.querySelector("#port-label").value = prt.label ?? "";
|
||||||
|
|
||||||
|
body.querySelector("#port-back-device").addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.selection = { kind: "device", id: dev.id };
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
body.querySelector("#port-type").addEventListener("change", async (e) => {
|
||||||
|
if (!state.active) return;
|
||||||
|
const newTypeID = Number(/** @type {HTMLSelectElement} */ (e.target).value);
|
||||||
|
if (newTypeID === prt.type_id) return;
|
||||||
|
try {
|
||||||
|
const updated = await patchPort(state.active.id, prt.id, { type_id: newTypeID });
|
||||||
|
Object.assign(prt, updated);
|
||||||
|
renderCanvas();
|
||||||
|
} catch (ex) {
|
||||||
|
alert(`Type change failed: ${ex.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
bindDebouncedRename(body.querySelector("#port-label"), async (label) => {
|
bindDebouncedRename(body.querySelector("#port-label"), async (label) => {
|
||||||
if (!state.active) return;
|
if (!state.active) return;
|
||||||
@@ -1110,6 +1142,114 @@ function edgeCentre(dev, edge) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compute the next available default label for a new port of `typeID`
|
||||||
|
// on `deviceID`. e.g. if a TV already has "HDMI 1" and "HDMI 2", a new
|
||||||
|
// HDMI port gets "HDMI 3".
|
||||||
|
function nextDefaultPortLabel(deviceID, typeID) {
|
||||||
|
const ct = state.cableTypes.find((t) => t.id === typeID);
|
||||||
|
const prefix = ct?.name || "Port";
|
||||||
|
const sibs = state.ports.filter((p) => p.device_id === deviceID && p.type_id === typeID);
|
||||||
|
let max = 0;
|
||||||
|
for (const p of sibs) {
|
||||||
|
const m = (p.label || "").match(new RegExp("^" + escapeRegExp(prefix) + "\\s+(\\d+)$"));
|
||||||
|
if (m) {
|
||||||
|
const n = parseInt(m[1], 10);
|
||||||
|
if (n > max) max = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${prefix} ${Math.max(max + 1, sibs.length + 1)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(s) {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Add port" form. Submit → POST → switch inspector to the new port's
|
||||||
|
// editor. m can cancel back to the device inspector.
|
||||||
|
function renderInspectorPortNew(body, deviceID) {
|
||||||
|
const dev = state.devices.find((d) => d.id === deviceID);
|
||||||
|
if (!dev) { body.innerHTML = ""; return; }
|
||||||
|
if (state.cableTypes.length === 0) {
|
||||||
|
body.innerHTML = `
|
||||||
|
<p class="section-title">Add port</p>
|
||||||
|
<p class="muted">No cable types defined. Add one from the legend first.</p>
|
||||||
|
<div class="inspector-actions">
|
||||||
|
<button type="button" class="btn btn-tiny" id="port-new-cancel">Cancel</button>
|
||||||
|
</div>`;
|
||||||
|
body.querySelector("#port-new-cancel").addEventListener("click", () => {
|
||||||
|
state.selection = { kind: "device", id: dev.id };
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const defaultTypeID = state.activeTypeId ?? state.cableTypes[0].id;
|
||||||
|
const typeOptions = state.cableTypes
|
||||||
|
.map((t) => `<option value="${t.id}">${escapeHtml(t.name)}</option>`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
body.innerHTML = `
|
||||||
|
<p class="section-title">Add port</p>
|
||||||
|
<p style="font-size:12px;margin:0 0 8px 0;">
|
||||||
|
<a href="#" id="port-new-back" class="btn-link">← ${escapeHtml(dev.name)}</a>
|
||||||
|
</p>
|
||||||
|
<label class="field">
|
||||||
|
<span>Type</span>
|
||||||
|
<select id="port-new-type">${typeOptions}</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Edge</span>
|
||||||
|
<select id="port-new-edge">
|
||||||
|
<option value="top">Top</option>
|
||||||
|
<option value="right">Right</option>
|
||||||
|
<option value="bottom" selected>Bottom</option>
|
||||||
|
<option value="left">Left</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Label</span>
|
||||||
|
<input class="inline-input" id="port-new-label" value="" />
|
||||||
|
</label>
|
||||||
|
<div class="inspector-actions">
|
||||||
|
<button type="button" class="btn btn-primary btn-tiny" id="port-new-create">Create</button>
|
||||||
|
<button type="button" class="btn btn-tiny" id="port-new-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const typeSel = /** @type {HTMLSelectElement} */ (body.querySelector("#port-new-type"));
|
||||||
|
const edgeSel = /** @type {HTMLSelectElement} */ (body.querySelector("#port-new-edge"));
|
||||||
|
const labelInp = /** @type {HTMLInputElement} */ (body.querySelector("#port-new-label"));
|
||||||
|
typeSel.value = String(defaultTypeID);
|
||||||
|
labelInp.value = nextDefaultPortLabel(dev.id, defaultTypeID);
|
||||||
|
labelInp.placeholder = labelInp.value;
|
||||||
|
|
||||||
|
// Recompute default label whenever the type changes (only if m hasn't
|
||||||
|
// edited the field).
|
||||||
|
let labelUserEdited = false;
|
||||||
|
labelInp.addEventListener("input", () => { labelUserEdited = true; });
|
||||||
|
typeSel.addEventListener("change", () => {
|
||||||
|
if (labelUserEdited) return;
|
||||||
|
const tid = Number(typeSel.value);
|
||||||
|
const next = nextDefaultPortLabel(dev.id, tid);
|
||||||
|
labelInp.value = next;
|
||||||
|
labelInp.placeholder = next;
|
||||||
|
});
|
||||||
|
|
||||||
|
body.querySelector("#port-new-back").addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.selection = { kind: "device", id: dev.id };
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
body.querySelector("#port-new-cancel").addEventListener("click", () => {
|
||||||
|
state.selection = { kind: "device", id: dev.id };
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
body.querySelector("#port-new-create").addEventListener("click", async () => {
|
||||||
|
const tid = Number(typeSel.value);
|
||||||
|
const edge = edgeSel.value;
|
||||||
|
const label = labelInp.value.trim();
|
||||||
|
await createPortFromForm(dev.id, tid, edge, label);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderInspectorCableType(body, id) {
|
function renderInspectorCableType(body, id) {
|
||||||
const t = state.cableTypes.find((x) => x.id === id);
|
const t = state.cableTypes.find((x) => x.id === id);
|
||||||
if (!t) { body.innerHTML = ""; return; }
|
if (!t) { body.innerHTML = ""; return; }
|
||||||
@@ -1313,27 +1453,15 @@ function armTool(tool) {
|
|||||||
const wrap = $(".canvas-wrap");
|
const wrap = $(".canvas-wrap");
|
||||||
wrap.classList.toggle("tool-frame", tool === "frame");
|
wrap.classList.toggle("tool-frame", tool === "frame");
|
||||||
wrap.classList.toggle("tool-device", tool === "device");
|
wrap.classList.toggle("tool-device", tool === "device");
|
||||||
wrap.classList.toggle("tool-port", tool === "port");
|
|
||||||
wrap.classList.toggle("tool-cable", tool === "cable");
|
wrap.classList.toggle("tool-cable", tool === "cable");
|
||||||
for (const btn of document.querySelectorAll("[data-tool]")) {
|
for (const btn of document.querySelectorAll("[data-tool]")) {
|
||||||
btn.classList.toggle("armed", btn.getAttribute("data-tool") === tool);
|
btn.classList.toggle("armed", btn.getAttribute("data-tool") === tool);
|
||||||
}
|
}
|
||||||
if (tool !== "port") {
|
|
||||||
state.portToolDevice = null;
|
|
||||||
state.portToolTypeID = null;
|
|
||||||
}
|
|
||||||
if (tool !== "cable") {
|
if (tool !== "cable") {
|
||||||
state.cableDrawFromPortID = null;
|
state.cableDrawFromPortID = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Slice 7: device inspector arms +Port for a specific device + type. */
|
|
||||||
function armPortTool(deviceID, typeID) {
|
|
||||||
state.portToolDevice = deviceID;
|
|
||||||
state.portToolTypeID = typeID;
|
|
||||||
armTool("port");
|
|
||||||
}
|
|
||||||
|
|
||||||
function bindTools() {
|
function bindTools() {
|
||||||
for (const btn of document.querySelectorAll("[data-tool]")) {
|
for (const btn of document.querySelectorAll("[data-tool]")) {
|
||||||
btn.addEventListener("click", () => armTool(btn.getAttribute("data-tool")));
|
btn.addEventListener("click", () => armTool(btn.getAttribute("data-tool")));
|
||||||
@@ -1385,11 +1513,6 @@ function onCanvasPointerDown(e) {
|
|||||||
placeDeviceAt(p);
|
placeDeviceAt(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.tool === "port") {
|
|
||||||
e.preventDefault();
|
|
||||||
placePortAt(p);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state.tool === "io") {
|
if (state.tool === "io") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
placeIOMarkerAt(p);
|
placeIOMarkerAt(p);
|
||||||
@@ -1568,27 +1691,8 @@ function openNewDeviceModal(geom) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Snap (x, y) onto the closest edge of `device`. Returns the (x_off,
|
|
||||||
* y_off) relative to the device's top-left + a debug-friendly edge name. */
|
|
||||||
function snapToDeviceEdge(device, x, y) {
|
|
||||||
// Distance from the point to each of the four edges.
|
|
||||||
const dxLeft = Math.abs(x - device.x);
|
|
||||||
const dxRight = Math.abs((device.x + device.width) - x);
|
|
||||||
const dyTop = Math.abs(y - device.y);
|
|
||||||
const dyBottom = Math.abs((device.y + device.height) - y);
|
|
||||||
const min = Math.min(dxLeft, dxRight, dyTop, dyBottom);
|
|
||||||
// Clamp the perpendicular coordinate so the port sits *on* the rect.
|
|
||||||
const localX = Math.max(0, Math.min(device.width, x - device.x));
|
|
||||||
const localY = Math.max(0, Math.min(device.height, y - device.y));
|
|
||||||
if (min === dxLeft) return { xOff: 0, yOff: localY, edge: "left" };
|
|
||||||
if (min === dxRight) return { xOff: device.width, yOff: localY, edge: "right" };
|
|
||||||
if (min === dyTop) return { xOff: localX, yOff: 0, edge: "top" };
|
|
||||||
return { xOff: localX, yOff: device.height, edge: "bottom" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Which edge does a given port currently sit on? Snaps the port's
|
// Which edge does a given port currently sit on? Snaps the port's
|
||||||
// existing (x_offset, y_offset) to the nearest of the four edges using
|
// existing (x_offset, y_offset) to the nearest of the four edges.
|
||||||
// the same distance heuristic as snapToDeviceEdge.
|
|
||||||
function portEdge(port, device) {
|
function portEdge(port, device) {
|
||||||
const dL = port.x_offset;
|
const dL = port.x_offset;
|
||||||
const dR = device.width - port.x_offset;
|
const dR = device.width - port.x_offset;
|
||||||
@@ -1762,33 +1866,28 @@ async function finishCableDrawAtIO(ioMarker) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function placePortAt(p) {
|
// Create a port from the sidebar "Add port" form and switch the
|
||||||
|
// inspector to its editor. Used by renderInspectorPortNew on submit.
|
||||||
|
async function createPortFromForm(deviceID, typeID, edge, label) {
|
||||||
if (!state.active) return;
|
if (!state.active) return;
|
||||||
const did = state.portToolDevice;
|
const dev = state.devices.find((d) => d.id === deviceID);
|
||||||
const tid = state.portToolTypeID;
|
if (!dev) return;
|
||||||
if (did == null || tid == null) { armTool(null); return; }
|
const tmp = edgeCentre(dev, edge);
|
||||||
const dev = state.devices.find((d) => d.id === did);
|
|
||||||
if (!dev) { armTool(null); return; }
|
|
||||||
const snap = snapToDeviceEdge(dev, p.x, p.y);
|
|
||||||
try {
|
try {
|
||||||
const port = await createPort(state.active.id, did, {
|
const port = await createPort(state.active.id, deviceID, {
|
||||||
type_id: tid,
|
type_id: typeID,
|
||||||
x_offset: snap.xOff,
|
label: label || undefined,
|
||||||
y_offset: snap.yOff,
|
x_offset: tmp.xOff,
|
||||||
|
y_offset: tmp.yOff,
|
||||||
});
|
});
|
||||||
state.ports.push(port);
|
state.ports.push(port);
|
||||||
// Re-layout all ports on this edge so the new one + existing ones
|
// Re-space every port on this edge so the new one slots into the
|
||||||
// are evenly spaced — m's invariant: never let two ports stack.
|
// even-spacing grid.
|
||||||
await relayoutEdge(did, snap.edge);
|
await relayoutEdge(deviceID, edge);
|
||||||
// Select the freshly-placed port so the inspector switches to the
|
|
||||||
// port panel (edge dropdown / label / delete) and the .selected halo
|
|
||||||
// marks it.
|
|
||||||
state.selection = { kind: "port", id: port.id };
|
state.selection = { kind: "port", id: port.id };
|
||||||
armTool(null);
|
|
||||||
render();
|
render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(`Add port failed: ${e.message}`);
|
alert(`Add port failed: ${e.message}`);
|
||||||
armTool(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -215,8 +215,6 @@ body {
|
|||||||
.canvas-wrap.tool-device #canvas *,
|
.canvas-wrap.tool-device #canvas *,
|
||||||
.canvas-wrap.tool-io #canvas,
|
.canvas-wrap.tool-io #canvas,
|
||||||
.canvas-wrap.tool-io #canvas *,
|
.canvas-wrap.tool-io #canvas *,
|
||||||
.canvas-wrap.tool-port #canvas,
|
|
||||||
.canvas-wrap.tool-port #canvas *,
|
|
||||||
.canvas-wrap.tool-cable #canvas,
|
.canvas-wrap.tool-cable #canvas,
|
||||||
.canvas-wrap.tool-cable #canvas * { cursor: crosshair !important; }
|
.canvas-wrap.tool-cable #canvas * { cursor: crosshair !important; }
|
||||||
|
|
||||||
@@ -296,8 +294,11 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding: 2px 0;
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.port-row:hover { background: var(--surface-2); }
|
||||||
.port-row .swatch,
|
.port-row .swatch,
|
||||||
.swatch {
|
.swatch {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
Reference in New Issue
Block a user