adasd
All checks were successful
Deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-05-15 23:37:09 +02:00
parent e7708831d0
commit 8647bff20f
3 changed files with 109 additions and 119 deletions

View File

@@ -289,76 +289,47 @@ button, input, select {
.rack-grid { .rack-grid {
position: relative; position: relative;
border-radius: 28px; border-radius: 28px;
border: 1px solid rgba(105, 108, 115, 0.22); border: 1px solid rgba(105, 108, 115, 0.12);
background: background:
linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(228, 229, 232, 0.86)), radial-gradient(circle at top left, rgba(255, 255, 255, 0.07), transparent 20%),
linear-gradient(90deg, rgba(144, 148, 156, 0.06), rgba(144, 148, 156, 0)); linear-gradient(180deg, #26272b, #202126);
padding: 28px 40px 28px 74px; padding: 16px;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.65), inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 -18px 24px rgba(134, 137, 145, 0.12), 0 20px 40px rgba(20, 18, 16, 0.18);
0 24px 40px rgba(92, 84, 73, 0.12);
}
.rack-grid {
outline: 18px solid var(--rack-frame-dark);
outline-offset: -18px;
}
.rack-grid::before,
.rack-grid::after {
content: "";
position: absolute;
top: 20px;
bottom: 20px;
width: 16px;
border-radius: 6px;
background:
radial-gradient(circle at center 8px, rgba(116, 120, 128, 0.48) 0 2px, transparent 2.5px) center top / 100% 18px repeat-y,
linear-gradient(180deg, var(--rack-frame-light), var(--rack-rail));
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.45),
inset 0 -10px 10px rgba(79, 82, 90, 0.16);
}
.rack-grid::before {
left: 26px;
}
.rack-grid::after {
right: 26px;
} }
.rack-grid__bay { .rack-grid__bay {
position: relative; position: relative;
width: var(--rack-inner-width); width: min(100%, 1180px);
margin: 0 auto; margin: 0 auto;
padding: 18px 0 14px; aspect-ratio: 3 / 2;
background: background-image:
linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent 13%, transparent 87%, rgba(0, 0, 0, 0.48)), linear-gradient(180deg, color-mix(in srgb, var(--rack-frame) 38%, transparent), color-mix(in srgb, var(--rack-frame) 38%, transparent)),
linear-gradient(135deg, rgba(255, 255, 255, 0.05), transparent 30%); url("images/rack-template.png");
border-radius: 8px; background-size: cover;
background-position: center;
background-repeat: no-repeat;
border-radius: 18px;
box-shadow: box-shadow:
inset 0 0 0 2px rgba(0, 0, 0, 0.35), 0 18px 34px rgba(7, 8, 10, 0.32),
inset 0 18px 32px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.04);
inset 0 -18px 32px rgba(0, 0, 0, 0.24); isolation: isolate;
} }
.rack-grid__guides { .rack-grid__guides {
position: relative; position: relative;
width: 100%; width: 100%;
background: linear-gradient(180deg, rgba(20, 22, 28, 0.92), rgba(7, 8, 11, 0.98)); height: 100%;
border-left: 4px solid rgba(0, 0, 0, 0.4); background: transparent;
border-right: 4px solid rgba(0, 0, 0, 0.4);
} }
.rack-slot { .rack-slot {
position: relative; position: relative;
height: var(--rack-unit-height); height: calc(100% / var(--rack-total-u));
border-top: 1px solid rgba(126, 131, 140, 0.14); border-top: 0;
background: background: transparent;
linear-gradient(180deg, rgba(255, 255, 255, 0.52), rgba(221, 224, 230, 0.42));
} }
.rack-slot:first-child { .rack-slot:first-child {
@@ -372,50 +343,51 @@ button, input, select {
.rack-slot__label { .rack-slot__label {
position: absolute; position: absolute;
left: -52px; left: 5.7%;
top: 8px; top: 39%;
font-size: 0.76rem; font-size: 0.78rem;
color: rgba(76, 78, 84, 0.82); color: rgba(239, 241, 245, 0.92);
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65);
} }
.rack-slot__label--right { .rack-slot__label--right {
left: auto; left: auto;
right: -42px; right: 5.7%;
} }
.rack-items-layer { .rack-items-layer {
position: absolute; position: absolute;
inset: 18px 0 14px; inset: 13.9% 17.4% 16.8% 17.4%;
width: var(--rack-inner-width); z-index: 2;
} }
.rack-insertion-layer { .rack-insertion-layer {
position: absolute; position: absolute;
inset: 18px 0 14px; inset: 13.9% 17.4% 16.8% 17.4%;
width: var(--rack-inner-width); pointer-events: auto;
pointer-events: none; z-index: 3;
} }
.rack-grid__header-badge { .rack-grid__header-badge {
position: absolute; position: absolute;
top: 16px; top: 1.7%;
z-index: 4; z-index: 6;
padding: 14px 18px; padding: 14px 18px;
border-radius: 14px; border-radius: 16px;
background: rgba(17, 19, 24, 0.84); background: rgba(17, 19, 24, 0.84);
color: #f6f7f9; color: #f6f7f9;
font-size: 1rem; font-size: 1.05rem;
box-shadow: box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12), inset 0 0 0 1px rgba(255, 255, 255, 0.12),
0 10px 20px rgba(20, 22, 28, 0.18); 0 10px 20px rgba(20, 22, 28, 0.18);
} }
.rack-grid__header-badge--left { .rack-grid__header-badge--left {
left: 16px; left: 1.8%;
} }
.rack-grid__header-badge--right { .rack-grid__header-badge--right {
right: 16px; right: 1.8%;
} }
.rack-insert-zone { .rack-insert-zone {
@@ -427,6 +399,7 @@ button, input, select {
background: transparent; background: transparent;
pointer-events: auto; pointer-events: auto;
cursor: pointer; cursor: pointer;
z-index: 3;
} }
.rack-insert-zone.is-disabled { .rack-insert-zone.is-disabled {
@@ -435,7 +408,7 @@ button, input, select {
.rack-insert-zone__ear { .rack-insert-zone__ear {
width: 28px; width: 28px;
height: calc(100% - 2px); height: calc(100% - 4px);
border-radius: 10px; border-radius: 10px;
background: rgba(83, 156, 243, 0.26); background: rgba(83, 156, 243, 0.26);
border: 1px solid rgba(116, 184, 255, 0.5); border: 1px solid rgba(116, 184, 255, 0.5);
@@ -477,6 +450,7 @@ button, input, select {
background: linear-gradient(180deg, rgba(35, 92, 154, 0.34), rgba(23, 52, 84, 0.42)); background: linear-gradient(180deg, rgba(35, 92, 154, 0.34), rgba(23, 52, 84, 0.42));
box-shadow: inset 0 0 0 1px rgba(120, 182, 247, 0.24); box-shadow: inset 0 0 0 1px rgba(120, 182, 247, 0.24);
font-size: 1rem; font-size: 1rem;
transition: background 120ms ease, box-shadow 120ms ease, transform 120ms ease;
} }
.rack-insert-zone__icon { .rack-insert-zone__icon {
@@ -489,6 +463,13 @@ button, input, select {
background-color: rgba(72, 151, 228, 0.34); background-color: rgba(72, 151, 228, 0.34);
} }
.rack-insert-zone:hover .rack-insert-zone__body {
transform: scaleY(1.02);
box-shadow:
inset 0 0 0 1px rgba(120, 182, 247, 0.34),
0 0 0 1px rgba(120, 182, 247, 0.18);
}
.rack-insert-zone.is-disabled .rack-insert-zone__body, .rack-insert-zone.is-disabled .rack-insert-zone__body,
.rack-insert-zone.is-disabled .rack-insert-zone__ear { .rack-insert-zone.is-disabled .rack-insert-zone__ear {
opacity: 0.28; opacity: 0.28;
@@ -500,12 +481,12 @@ button, input, select {
padding: 0; padding: 0;
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
border-radius: 10px; border-radius: 8px;
border: 1px solid rgba(122, 127, 137, 0.35); border: 1px solid rgba(34, 38, 44, 0.55);
background: linear-gradient(180deg, #fcfcfd, #e1e5eb); background: linear-gradient(180deg, rgba(237, 241, 246, 0.96), rgba(205, 213, 222, 0.96));
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.76), inset 0 1px 0 rgba(255, 255, 255, 0.9),
0 10px 18px rgba(80, 84, 93, 0.18); 0 10px 18px rgba(5, 6, 7, 0.28);
touch-action: none; touch-action: none;
cursor: grab; cursor: grab;
} }
@@ -548,16 +529,32 @@ button, input, select {
color: #17191c; color: #17191c;
} }
.rack-item__actions { .rack-item__drag-hint {
display: flex; align-self: start;
flex-wrap: wrap; padding: 5px 8px;
gap: 8px; border-radius: 999px;
background: rgba(19, 23, 28, 0.78);
color: rgba(245, 247, 250, 0.95);
font-size: 0.72rem;
letter-spacing: 0.04em;
} }
.rack-item__actions button { .rack-item__remove {
padding: 8px 10px; position: absolute;
font-size: 0.84rem; top: 8px;
background: rgba(181, 93, 45, 0.92); right: 8px;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 999px;
background: rgba(16, 18, 23, 0.78);
color: #fff;
font-size: 1rem;
line-height: 1;
cursor: pointer;
} }
.subpanel { .subpanel {

View File

@@ -226,6 +226,7 @@ function renderRack() {
} }
ui.rackGrid.style.setProperty("--rack-unit-height", `${RACK_UNIT_PX}px`); ui.rackGrid.style.setProperty("--rack-unit-height", `${RACK_UNIT_PX}px`);
ui.rackGrid.style.setProperty("--rack-total-u", String(rack.totalU));
applyRackColorTheme(ui.rackGrid, state.rackColor); applyRackColorTheme(ui.rackGrid, state.rackColor);
const slots = []; const slots = [];
@@ -238,26 +239,25 @@ function renderRack() {
`); `);
} }
const insertionZones = renderInsertionZones(rack);
ui.rackGrid.innerHTML = ` ui.rackGrid.innerHTML = `
<div class="rack-grid__header-badge rack-grid__header-badge--left">${rack.rackStandard === "19_inch" ? '19" Rack' : '10" Rack'}</div> <div class="rack-grid__header-badge rack-grid__header-badge--left">${rack.rackStandard === "19_inch" ? '19" Rack' : '10" Rack'}</div>
<div class="rack-grid__header-badge rack-grid__header-badge--right">${rack.totalU} HE</div> <div class="rack-grid__header-badge rack-grid__header-badge--right">${rack.totalU} HE</div>
<div class="rack-grid__bay"> <div class="rack-grid__bay">
<div class="rack-grid__guides">${slots.join("")}</div> <div class="rack-grid__guides">${slots.join("")}</div>
<div class="rack-insertion-layer">${insertionZones}</div> <div class="rack-insertion-layer" id="rack-insertion-layer"></div>
<div class="rack-items-layer" id="rack-items-layer"></div> <div class="rack-items-layer" id="rack-items-layer"></div>
</div> </div>
`; `;
const layer = document.getElementById("rack-items-layer");
layer.style.height = "100%";
const insertionLayer = document.getElementById("rack-insertion-layer");
insertionLayer.innerHTML = renderInsertionZones(rack);
ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => { ui.rackGrid.querySelectorAll("[data-action='insert-component']").forEach((element) => {
element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY))); element.addEventListener("click", () => insertSelectedComponentAt(Number(element.dataset.insertY)));
}); });
const layer = document.getElementById("rack-items-layer");
const rackHeight = rack.totalU * RACK_UNIT_PX;
layer.style.height = `${rackHeight}px`;
state.placedItems.forEach((item) => { state.placedItems.forEach((item) => {
const component = getComponent(item.componentId); const component = getComponent(item.componentId);
if (!component) { if (!component) {
@@ -284,21 +284,12 @@ function renderRack() {
</div> </div>
<span class="chip">${formatRackPosition(item.y)}</span> <span class="chip">${formatRackPosition(item.y)}</span>
</div> </div>
<div class="rack-item__actions"> <div class="rack-item__drag-hint">Ziehen zum verschieben</div>
<button type="button" data-action="move-left"></button> <button type="button" class="rack-item__remove" data-action="remove" aria-label="Komponente entfernen">×</button>
<button type="button" data-action="move-right">→</button>
<button type="button" data-action="move-up">+1U</button>
<button type="button" data-action="move-down">-1U</button>
<button type="button" data-action="remove">Entfernen</button>
</div>
</div> </div>
`; `;
element.addEventListener("pointerdown", (event) => beginPointerDrag(event, item, component)); element.addEventListener("pointerdown", (event) => beginPointerDrag(event, item, component));
element.querySelector("[data-action='move-left']").addEventListener("click", () => nudgePlacedItem(item.placementId, -2, 0));
element.querySelector("[data-action='move-right']").addEventListener("click", () => nudgePlacedItem(item.placementId, 2, 0));
element.querySelector("[data-action='move-up']").addEventListener("click", () => nudgePlacedItem(item.placementId, 0, -RACK_UNIT_PX));
element.querySelector("[data-action='move-down']").addEventListener("click", () => nudgePlacedItem(item.placementId, 0, RACK_UNIT_PX));
element.querySelector("[data-action='remove']").addEventListener("click", () => { element.querySelector("[data-action='remove']").addEventListener("click", () => {
state.placedItems = state.placedItems.filter((entry) => entry.placementId !== item.placementId); state.placedItems = state.placedItems.filter((entry) => entry.placementId !== item.placementId);
renderAll(); renderAll();
@@ -308,19 +299,6 @@ function renderRack() {
}); });
} }
function nudgePlacedItem(placementId, deltaX, deltaY) {
const rack = getCurrentRackTemplate();
const item = state.placedItems.find((entry) => entry.placementId === placementId);
const component = item ? getComponent(item.componentId) : null;
if (!rack || !item || !component) {
return;
}
item.x = clamp(item.x + deltaX, 0, 100 - getComponentWidthPercent(component));
item.y = clamp(item.y + deltaY, 0, getRackHeightPx(rack) - getComponentHeightPx(component));
renderAll();
}
function insertSelectedComponentAt(y) { function insertSelectedComponentAt(y) {
if (!state.selectedComponentId) { if (!state.selectedComponentId) {
return; return;
@@ -494,7 +472,7 @@ function renderCableEstimate() {
return; return;
} }
const verticalMm = Math.abs(getPlacementCenterY(from, fromComponent) - getPlacementCenterY(to, toComponent)) * (44.45 / RACK_UNIT_PX); const verticalMm = Math.abs(getPlacementCenterY(from, fromComponent) - getPlacementCenterY(to, toComponent)) * (44.45 / getUnitHeightPx());
const depthAllowance = Math.min(rack.usableDepthMm * 0.35, 280); const depthAllowance = Math.min(rack.usableDepthMm * 0.35, 280);
const sideAllowance = estimateSideAllowance(fromComponent, toComponent); const sideAllowance = estimateSideAllowance(fromComponent, toComponent);
const rawLength = verticalMm + depthAllowance + sideAllowance; const rawLength = verticalMm + depthAllowance + sideAllowance;
@@ -656,8 +634,9 @@ function findFirstFreePosition(heightU) {
return null; return null;
} }
const testHeight = heightU * RACK_UNIT_PX; const unitHeight = getUnitHeightPx();
for (let y = 0; y <= getRackHeightPx(rack) - testHeight; y += RACK_UNIT_PX) { const testHeight = heightU * unitHeight;
for (let y = 0; y <= getRackHeightPx(rack) - testHeight; y += unitHeight) {
const blocked = state.placedItems.some((item) => { const blocked = state.placedItems.some((item) => {
const otherComponent = getComponent(item.componentId); const otherComponent = getComponent(item.componentId);
if (!otherComponent) { if (!otherComponent) {
@@ -883,8 +862,9 @@ function renderInsertionZones(rack) {
return ""; return "";
} }
const unitHeight = getUnitHeightPx();
const zones = []; const zones = [];
for (let y = 0; y <= getRackHeightPx(rack) - getComponentHeightPx(component); y += RACK_UNIT_PX) { for (let y = 0; y <= getRackHeightPx(rack) - getComponentHeightPx(component); y += unitHeight) {
const occupied = state.placedItems.some((item) => { const occupied = state.placedItems.some((item) => {
const other = getComponent(item.componentId); const other = getComponent(item.componentId);
if (!other) { if (!other) {
@@ -929,11 +909,15 @@ function renderInsertionZones(rack) {
} }
function getRackHeightPx(rack) { function getRackHeightPx(rack) {
const layer = document.getElementById("rack-items-layer");
if (layer) {
return layer.clientHeight || rack.totalU * RACK_UNIT_PX;
}
return rack.totalU * RACK_UNIT_PX; return rack.totalU * RACK_UNIT_PX;
} }
function getComponentHeightPx(component) { function getComponentHeightPx(component) {
return component.heightU * RACK_UNIT_PX; return component.heightU * getUnitHeightPx();
} }
function getComponentWidthPercent(component) { function getComponentWidthPercent(component) {
@@ -961,7 +945,7 @@ function getPlacementCenterY(item, component) {
} }
function formatRackPosition(y) { function formatRackPosition(y) {
return `${(y / RACK_UNIT_PX + 1).toFixed(1)}U`; return `${(y / getUnitHeightPx() + 1).toFixed(1)}U`;
} }
function getPlacementOverlaps() { function getPlacementOverlaps() {
@@ -995,6 +979,15 @@ function getRackInnerWidthPx() {
return 620; return 620;
} }
function getUnitHeightPx() {
const rack = getCurrentRackTemplate();
const layer = document.getElementById("rack-items-layer");
if (rack && layer && layer.clientHeight) {
return layer.clientHeight / rack.totalU;
}
return RACK_UNIT_PX;
}
function clamp(value, min, max) { function clamp(value, min, max) {
return Math.max(min, Math.min(max, value)); return Math.max(min, Math.min(max, value));
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB