This commit is contained in:
@@ -197,6 +197,11 @@ button, input, select {
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.component-card:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.component-card__preview {
|
||||
@@ -400,12 +405,19 @@ button, input, select {
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
z-index: 3;
|
||||
opacity: 0.32;
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
}
|
||||
|
||||
.rack-insert-zone.is-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.rack-insert-zone.is-preview,
|
||||
.rack-insert-zone:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.rack-insert-zone__ear {
|
||||
width: 28px;
|
||||
height: calc(100% - 4px);
|
||||
@@ -482,11 +494,11 @@ button, input, select {
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(34, 38, 44, 0.55);
|
||||
background: linear-gradient(180deg, rgba(237, 241, 246, 0.96), rgba(205, 213, 222, 0.96));
|
||||
border: 1px solid rgba(32, 36, 42, 0.8);
|
||||
background: linear-gradient(180deg, rgba(235, 239, 244, 0.98), rgba(191, 200, 211, 0.98));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 10px 18px rgba(5, 6, 7, 0.28);
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.94),
|
||||
0 8px 14px rgba(5, 6, 7, 0.24);
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
}
|
||||
@@ -509,8 +521,10 @@ button, input, select {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px 8px 12px;
|
||||
background: linear-gradient(90deg, rgba(250, 251, 252, 0.86), rgba(250, 251, 252, 0.08) 38%, rgba(250, 251, 252, 0.72));
|
||||
padding: 6px 10px 6px 10px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0) 42%),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0.06) 34%, rgba(255, 255, 255, 0.18));
|
||||
}
|
||||
|
||||
.rack-item__face {
|
||||
@@ -527,6 +541,7 @@ button, input, select {
|
||||
|
||||
.rack-item__header strong {
|
||||
color: #17191c;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.rack-item__drag-hint {
|
||||
@@ -537,6 +552,7 @@ button, input, select {
|
||||
color: rgba(245, 247, 250, 0.95);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.04em;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rack-item__remove {
|
||||
@@ -557,6 +573,10 @@ button, input, select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rack-item__remove:hover {
|
||||
background: rgba(134, 24, 24, 0.88);
|
||||
}
|
||||
|
||||
.subpanel {
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
@@ -646,7 +666,7 @@ button, input, select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 28px;
|
||||
padding: 6px 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -699,7 +719,7 @@ button, input, select {
|
||||
|
||||
.device-brand {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
top: 6px;
|
||||
left: 36px;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.04em;
|
||||
@@ -710,7 +730,7 @@ button, input, select {
|
||||
.device-silkscreen {
|
||||
position: absolute;
|
||||
left: 36px;
|
||||
bottom: 8px;
|
||||
bottom: 6px;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
@@ -765,7 +785,8 @@ button, input, select {
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
right: 70px;
|
||||
top: 22px;
|
||||
top: 48%;
|
||||
transform: translateY(-50%);
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -792,7 +813,8 @@ button, input, select {
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
right: 40px;
|
||||
top: 20px;
|
||||
top: 48%;
|
||||
transform: translateY(-50%);
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -807,10 +829,11 @@ button, input, select {
|
||||
|
||||
.device-uplink {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
top: 50%;
|
||||
right: 40px;
|
||||
width: 22px;
|
||||
height: 16px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(110, 117, 127, 0.25);
|
||||
background: linear-gradient(180deg, #5b646f, #262b31);
|
||||
@@ -818,9 +841,10 @@ button, input, select {
|
||||
|
||||
.device-sockets {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
top: 50%;
|
||||
left: 40px;
|
||||
right: 56px;
|
||||
transform: translateY(-50%);
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
}
|
||||
@@ -838,9 +862,10 @@ button, input, select {
|
||||
.device-display {
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 16px;
|
||||
top: 50%;
|
||||
width: 68px;
|
||||
height: 30px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(46, 180, 156, 0.9), rgba(18, 97, 90, 0.92)),
|
||||
@@ -852,10 +877,11 @@ button, input, select {
|
||||
|
||||
.device-vents {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
top: 50%;
|
||||
right: 42px;
|
||||
width: 96px;
|
||||
height: 22px;
|
||||
transform: translateY(-50%);
|
||||
background:
|
||||
repeating-linear-gradient(90deg, rgba(102, 108, 118, 0.36) 0 3px, transparent 3px 7px);
|
||||
opacity: 0.8;
|
||||
@@ -879,8 +905,9 @@ button, input, select {
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
right: 40px;
|
||||
top: 18px;
|
||||
top: 50%;
|
||||
height: 18px;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 4px 4px 12px 12px;
|
||||
background: linear-gradient(180deg, #d1d7df, #8f98a4);
|
||||
box-shadow: 0 5px 12px rgba(78, 82, 90, 0.18);
|
||||
|
||||
@@ -9,6 +9,8 @@ const state = {
|
||||
dragPlacementId: null,
|
||||
pointerDrag: null,
|
||||
selectedComponentId: null,
|
||||
libraryDragComponentId: null,
|
||||
rackDropPreviewY: null,
|
||||
};
|
||||
|
||||
const ui = {};
|
||||
@@ -64,6 +66,7 @@ function bindEvents() {
|
||||
ui.exportBom.addEventListener("click", copyBomCsv);
|
||||
document.addEventListener("pointermove", handlePointerMove);
|
||||
document.addEventListener("pointerup", handlePointerUp);
|
||||
document.addEventListener("dragend", handleLibraryDragEnd);
|
||||
}
|
||||
|
||||
async function loadBootstrap() {
|
||||
@@ -152,7 +155,7 @@ function renderLibrary() {
|
||||
ui.componentLibrary.innerHTML = filtered
|
||||
.map(
|
||||
(component) => `
|
||||
<article class="component-card component-card--${escapeHtml(component.category)}">
|
||||
<article class="component-card component-card--${escapeHtml(component.category)}" draggable="true" data-component-drag-id="${escapeHtml(component.id)}">
|
||||
<div class="component-card__preview">
|
||||
${renderComponentFace(component, "library")}
|
||||
</div>
|
||||
@@ -169,7 +172,7 @@ function renderLibrary() {
|
||||
<span>${formatCurrency(component.priceNet, component.currency)}</span>
|
||||
</div>
|
||||
<button type="button" data-action="select-component" data-component-id="${escapeHtml(component.id)}">
|
||||
${state.selectedComponentId === component.id ? "Ausgewaehlt" : "Zum Einstecken waehlen"}
|
||||
${state.selectedComponentId === component.id ? "Bereit zum Ziehen" : "Ins Rack ziehen"}
|
||||
</button>
|
||||
</article>
|
||||
`
|
||||
@@ -182,40 +185,21 @@ function renderLibrary() {
|
||||
renderAll();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addComponentToRack(componentId) {
|
||||
const component = state.components.find((entry) => entry.id === componentId);
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!component || !rack) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = findFirstFreePosition(component.heightU);
|
||||
if (position === null) {
|
||||
alert("Keine freie Position fuer diese Komponente im aktuellen Rack.");
|
||||
return;
|
||||
}
|
||||
|
||||
state.placedItems.push({
|
||||
placementId: `p${state.nextPlacementId++}`,
|
||||
componentId: component.id,
|
||||
y: position,
|
||||
x: getDefaultComponentX(component),
|
||||
ui.componentLibrary.querySelectorAll("[data-component-drag-id]").forEach((card) => {
|
||||
card.addEventListener("dragstart", (event) => beginLibraryDrag(event, card.dataset.componentDragId));
|
||||
});
|
||||
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function renderSelectionInfo() {
|
||||
const component = getComponent(state.selectedComponentId);
|
||||
const component = getComponent(state.libraryDragComponentId || state.selectedComponentId);
|
||||
if (!component) {
|
||||
ui.selectedComponentInfo.textContent = "Kein Element ausgewaehlt";
|
||||
ui.selectedComponentInfo.classList.remove("is-active");
|
||||
return;
|
||||
}
|
||||
|
||||
ui.selectedComponentInfo.textContent = `${component.name} · ${component.heightU}U · freie HE-Zone anklicken`;
|
||||
ui.selectedComponentInfo.textContent = `${component.name} · ${component.heightU}U · aus der Bibliothek ins Rack ziehen`;
|
||||
ui.selectedComponentInfo.classList.add("is-active");
|
||||
}
|
||||
|
||||
@@ -251,6 +235,10 @@ function renderRack() {
|
||||
|
||||
const layer = document.getElementById("rack-items-layer");
|
||||
layer.style.height = "100%";
|
||||
const bay = ui.rackGrid.querySelector(".rack-grid__bay");
|
||||
bay.addEventListener("dragover", handleRackDragOver);
|
||||
bay.addEventListener("drop", handleRackDrop);
|
||||
bay.addEventListener("dragleave", handleRackDragLeave);
|
||||
|
||||
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||
@@ -282,7 +270,7 @@ function renderRack() {
|
||||
<strong>${escapeHtml(component.name)}</strong>
|
||||
<div class="rack-item__meta">${component.heightU}U · ${component.depthMm} mm · ${formatCurrency(component.priceNet, component.currency)}</div>
|
||||
</div>
|
||||
<span class="chip">${formatRackPosition(item.y)}</span>
|
||||
<span class="chip">${formatRackPosition(item.y, component)}</span>
|
||||
</div>
|
||||
<div class="rack-item__drag-hint">Ziehen zum verschieben</div>
|
||||
<button type="button" class="rack-item__remove" data-action="remove" aria-label="Komponente entfernen">×</button>
|
||||
@@ -300,19 +288,20 @@ function renderRack() {
|
||||
}
|
||||
|
||||
function insertSelectedComponentAt(y) {
|
||||
if (!state.selectedComponentId) {
|
||||
const componentId = state.libraryDragComponentId || state.selectedComponentId;
|
||||
if (!componentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const component = getComponent(state.selectedComponentId);
|
||||
const component = getComponent(componentId);
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!component || !rack) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRect = {
|
||||
left: getDefaultComponentX(component),
|
||||
top: y,
|
||||
left: 0,
|
||||
top: snapToUnit(y),
|
||||
width: getComponentWidthPercent(component),
|
||||
height: getComponentHeightPx(component),
|
||||
};
|
||||
@@ -340,9 +329,11 @@ function insertSelectedComponentAt(y) {
|
||||
state.placedItems.push({
|
||||
placementId: `p${state.nextPlacementId++}`,
|
||||
componentId: component.id,
|
||||
y,
|
||||
x: getDefaultComponentX(component),
|
||||
y: snapToUnit(y),
|
||||
x: 0,
|
||||
});
|
||||
state.libraryDragComponentId = null;
|
||||
state.rackDropPreviewY = null;
|
||||
state.selectedComponentId = null;
|
||||
renderAll();
|
||||
}
|
||||
@@ -442,7 +433,7 @@ function renderCableSelectors() {
|
||||
if (!component) {
|
||||
return "";
|
||||
}
|
||||
return `<option value="${escapeHtml(item.placementId)}">${escapeHtml(component.name)} @ ${formatRackPosition(item.y)}</option>`;
|
||||
return `<option value="${escapeHtml(item.placementId)}">${escapeHtml(component.name)} @ ${formatRackPosition(item.y, component)}</option>`;
|
||||
})
|
||||
)
|
||||
.join("");
|
||||
@@ -628,43 +619,6 @@ async function copyBomCsv() {
|
||||
}
|
||||
}
|
||||
|
||||
function findFirstFreePosition(heightU) {
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!rack) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const unitHeight = getUnitHeightPx();
|
||||
const testHeight = heightU * unitHeight;
|
||||
for (let y = 0; y <= getRackHeightPx(rack) - testHeight; y += unitHeight) {
|
||||
const blocked = state.placedItems.some((item) => {
|
||||
const otherComponent = getComponent(item.componentId);
|
||||
if (!otherComponent) {
|
||||
return false;
|
||||
}
|
||||
return rectanglesOverlap(
|
||||
{
|
||||
left: getDefaultComponentX(component),
|
||||
top: y,
|
||||
width: getComponentWidthPercent(component),
|
||||
height: testHeight,
|
||||
},
|
||||
{
|
||||
left: item.x,
|
||||
top: item.y,
|
||||
width: getComponentWidthPercent(otherComponent),
|
||||
height: getComponentHeightPx(otherComponent),
|
||||
}
|
||||
);
|
||||
});
|
||||
if (!blocked) {
|
||||
return y;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCurrentRackTemplate() {
|
||||
return state.bootstrap?.rackTemplates.find((template) => template.id === state.rackTemplateId) ?? null;
|
||||
}
|
||||
@@ -701,9 +655,7 @@ function beginPointerDrag(event, item, component) {
|
||||
const rect = event.currentTarget.getBoundingClientRect();
|
||||
state.pointerDrag = {
|
||||
placementId: item.placementId,
|
||||
offsetX: event.clientX - rect.left,
|
||||
offsetY: event.clientY - rect.top,
|
||||
widthPercent: getComponentWidthPercent(component),
|
||||
heightPx: getComponentHeightPx(component),
|
||||
};
|
||||
event.currentTarget.setPointerCapture?.(event.pointerId);
|
||||
@@ -727,12 +679,10 @@ function handlePointerMove(event) {
|
||||
}
|
||||
|
||||
const layerRect = layer.getBoundingClientRect();
|
||||
const widthPx = (state.pointerDrag.widthPercent / 100) * layerRect.width;
|
||||
const xPx = clamp(event.clientX - layerRect.left - state.pointerDrag.offsetX, 0, layerRect.width - widthPx);
|
||||
const yPx = clamp(event.clientY - layerRect.top - state.pointerDrag.offsetY, 0, getRackHeightPx(rack) - state.pointerDrag.heightPx);
|
||||
|
||||
item.x = (xPx / layerRect.width) * 100;
|
||||
item.y = yPx;
|
||||
item.x = 0;
|
||||
item.y = snapToUnit(yPx);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
@@ -857,7 +807,11 @@ function renderSocketStrip(component) {
|
||||
}
|
||||
|
||||
function renderInsertionZones(rack) {
|
||||
const component = getComponent(state.selectedComponentId);
|
||||
if (!state.libraryDragComponentId) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const component = getComponent(state.libraryDragComponentId);
|
||||
if (!component) {
|
||||
return "";
|
||||
}
|
||||
@@ -872,27 +826,28 @@ function renderInsertionZones(rack) {
|
||||
}
|
||||
return rectanglesOverlap(
|
||||
{
|
||||
left: getDefaultComponentX(component),
|
||||
left: 0,
|
||||
top: y,
|
||||
width: getComponentWidthPercent(component),
|
||||
width: 100,
|
||||
height: getComponentHeightPx(component),
|
||||
},
|
||||
{
|
||||
left: item.x,
|
||||
left: 0,
|
||||
top: item.y,
|
||||
width: getComponentWidthPercent(other),
|
||||
width: 100,
|
||||
height: getComponentHeightPx(other),
|
||||
}
|
||||
);
|
||||
});
|
||||
const isPreview = state.rackDropPreviewY !== null && Math.abs(state.rackDropPreviewY - y) < 1;
|
||||
|
||||
zones.push(`
|
||||
<button
|
||||
type="button"
|
||||
class="rack-insert-zone${occupied ? " is-disabled" : ""}"
|
||||
class="rack-insert-zone${occupied ? " is-disabled" : ""}${isPreview ? " is-preview" : ""}"
|
||||
data-action="insert-component"
|
||||
data-insert-y="${y}"
|
||||
style="top:${y}px;height:${getComponentHeightPx(component)}px;left:${getDefaultComponentX(component)}%;width:${getComponentWidthPercent(component)}%;"
|
||||
style="top:${y}px;height:${getComponentHeightPx(component)}px;left:0;width:100%;"
|
||||
${occupied ? "disabled" : ""}
|
||||
>
|
||||
<span class="rack-insert-zone__ear rack-insert-zone__ear--left"></span>
|
||||
@@ -921,11 +876,11 @@ function getComponentHeightPx(component) {
|
||||
}
|
||||
|
||||
function getComponentWidthPercent(component) {
|
||||
return component.rackStandard === "10_inch" ? 58 : 86;
|
||||
return 100;
|
||||
}
|
||||
|
||||
function getDefaultComponentX(component) {
|
||||
return (100 - getComponentWidthPercent(component)) / 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getPlacementRect(item, component) {
|
||||
@@ -948,6 +903,17 @@ function formatRackPosition(y) {
|
||||
return `${(y / getUnitHeightPx() + 1).toFixed(1)}U`;
|
||||
}
|
||||
|
||||
function formatRackPosition(y, component) {
|
||||
const rack = getCurrentRackTemplate();
|
||||
if (!rack) {
|
||||
return "1U";
|
||||
}
|
||||
const rowIndex = Math.round(y / getUnitHeightPx());
|
||||
const componentHeightU = component?.heightU ?? 1;
|
||||
const u = rack.totalU - rowIndex - componentHeightU + 1;
|
||||
return `${Math.max(1, u)}U`;
|
||||
}
|
||||
|
||||
function getPlacementOverlaps() {
|
||||
const overlaps = [];
|
||||
for (let index = 0; index < state.placedItems.length; index += 1) {
|
||||
@@ -992,6 +958,86 @@ function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function snapToUnit(value) {
|
||||
const unitHeight = getUnitHeightPx();
|
||||
return Math.round(value / unitHeight) * unitHeight;
|
||||
}
|
||||
|
||||
function beginLibraryDrag(event, componentId) {
|
||||
state.libraryDragComponentId = componentId;
|
||||
state.selectedComponentId = componentId;
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData("text/plain", componentId);
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
renderAll();
|
||||
});
|
||||
}
|
||||
|
||||
function handleRackDragOver(event) {
|
||||
if (!state.libraryDragComponentId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const rack = getCurrentRackTemplate();
|
||||
const component = getComponent(state.libraryDragComponentId);
|
||||
const layer = document.getElementById("rack-items-layer");
|
||||
if (!rack || !component || !layer) {
|
||||
return;
|
||||
}
|
||||
const rect = layer.getBoundingClientRect();
|
||||
const y = clamp(event.clientY - rect.top - getComponentHeightPx(component) / 2, 0, getRackHeightPx(rack) - getComponentHeightPx(component));
|
||||
const snappedY = snapToUnit(y);
|
||||
if (state.rackDropPreviewY !== snappedY) {
|
||||
state.rackDropPreviewY = snappedY;
|
||||
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||
if (insertionLayer) {
|
||||
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||
ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => {
|
||||
element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY)));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleRackDrop(event) {
|
||||
if (!state.libraryDragComponentId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (state.rackDropPreviewY !== null) {
|
||||
insertSelectedComponentAt(state.rackDropPreviewY);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRackDragLeave(event) {
|
||||
if (!state.libraryDragComponentId) {
|
||||
return;
|
||||
}
|
||||
if (event.currentTarget.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
state.rackDropPreviewY = null;
|
||||
const rack = getCurrentRackTemplate();
|
||||
const insertionLayer = document.getElementById("rack-insertion-layer");
|
||||
if (rack && insertionLayer) {
|
||||
insertionLayer.innerHTML = renderInsertionZones(rack);
|
||||
ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => {
|
||||
element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleLibraryDragEnd() {
|
||||
if (!state.libraryDragComponentId && state.rackDropPreviewY === null) {
|
||||
return;
|
||||
}
|
||||
state.libraryDragComponentId = null;
|
||||
state.rackDropPreviewY = null;
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function applyRackColorTheme(element, color) {
|
||||
const rgb = hexToRgb(color);
|
||||
if (!rgb) {
|
||||
|
||||
Reference in New Issue
Block a user