dsfsdf
All checks were successful
Deploy / deploy (push) Successful in 44s

This commit is contained in:
2026-05-17 00:47:09 +02:00
parent 8647bff20f
commit f5bb37f96e
2 changed files with 174 additions and 101 deletions

View File

@@ -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);

View File

@@ -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) {