diff --git a/public/assets/app.css b/public/assets/app.css index 3042dfa..b0d66d6 100644 --- a/public/assets/app.css +++ b/public/assets/app.css @@ -289,76 +289,47 @@ button, input, select { .rack-grid { position: relative; border-radius: 28px; - border: 1px solid rgba(105, 108, 115, 0.22); + border: 1px solid rgba(105, 108, 115, 0.12); background: - linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(228, 229, 232, 0.86)), - linear-gradient(90deg, rgba(144, 148, 156, 0.06), rgba(144, 148, 156, 0)); - padding: 28px 40px 28px 74px; + radial-gradient(circle at top left, rgba(255, 255, 255, 0.07), transparent 20%), + linear-gradient(180deg, #26272b, #202126); + padding: 16px; overflow: hidden; box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.65), - inset 0 -18px 24px rgba(134, 137, 145, 0.12), - 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; + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 20px 40px rgba(20, 18, 16, 0.18); } .rack-grid__bay { position: relative; - width: var(--rack-inner-width); + width: min(100%, 1180px); margin: 0 auto; - padding: 18px 0 14px; - background: - linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent 13%, transparent 87%, rgba(0, 0, 0, 0.48)), - linear-gradient(135deg, rgba(255, 255, 255, 0.05), transparent 30%); - border-radius: 8px; + aspect-ratio: 3 / 2; + background-image: + linear-gradient(180deg, color-mix(in srgb, var(--rack-frame) 38%, transparent), color-mix(in srgb, var(--rack-frame) 38%, transparent)), + url("images/rack-template.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + border-radius: 18px; box-shadow: - inset 0 0 0 2px rgba(0, 0, 0, 0.35), - inset 0 18px 32px rgba(0, 0, 0, 0.3), - inset 0 -18px 32px rgba(0, 0, 0, 0.24); + 0 18px 34px rgba(7, 8, 10, 0.32), + inset 0 0 0 1px rgba(255, 255, 255, 0.04); + isolation: isolate; } .rack-grid__guides { position: relative; width: 100%; - background: linear-gradient(180deg, rgba(20, 22, 28, 0.92), rgba(7, 8, 11, 0.98)); - border-left: 4px solid rgba(0, 0, 0, 0.4); - border-right: 4px solid rgba(0, 0, 0, 0.4); + height: 100%; + background: transparent; } .rack-slot { position: relative; - height: var(--rack-unit-height); - border-top: 1px solid rgba(126, 131, 140, 0.14); - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.52), rgba(221, 224, 230, 0.42)); + height: calc(100% / var(--rack-total-u)); + border-top: 0; + background: transparent; } .rack-slot:first-child { @@ -372,50 +343,51 @@ button, input, select { .rack-slot__label { position: absolute; - left: -52px; - top: 8px; - font-size: 0.76rem; - color: rgba(76, 78, 84, 0.82); + left: 5.7%; + top: 39%; + font-size: 0.78rem; + color: rgba(239, 241, 245, 0.92); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.65); } .rack-slot__label--right { left: auto; - right: -42px; + right: 5.7%; } .rack-items-layer { position: absolute; - inset: 18px 0 14px; - width: var(--rack-inner-width); + inset: 13.9% 17.4% 16.8% 17.4%; + z-index: 2; } .rack-insertion-layer { position: absolute; - inset: 18px 0 14px; - width: var(--rack-inner-width); - pointer-events: none; + inset: 13.9% 17.4% 16.8% 17.4%; + pointer-events: auto; + z-index: 3; } .rack-grid__header-badge { position: absolute; - top: 16px; - z-index: 4; + top: 1.7%; + z-index: 6; padding: 14px 18px; - border-radius: 14px; + border-radius: 16px; background: rgba(17, 19, 24, 0.84); color: #f6f7f9; - font-size: 1rem; + font-size: 1.05rem; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12), 0 10px 20px rgba(20, 22, 28, 0.18); } .rack-grid__header-badge--left { - left: 16px; + left: 1.8%; } .rack-grid__header-badge--right { - right: 16px; + right: 1.8%; } .rack-insert-zone { @@ -427,6 +399,7 @@ button, input, select { background: transparent; pointer-events: auto; cursor: pointer; + z-index: 3; } .rack-insert-zone.is-disabled { @@ -435,7 +408,7 @@ button, input, select { .rack-insert-zone__ear { width: 28px; - height: calc(100% - 2px); + height: calc(100% - 4px); border-radius: 10px; background: rgba(83, 156, 243, 0.26); 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)); box-shadow: inset 0 0 0 1px rgba(120, 182, 247, 0.24); font-size: 1rem; + transition: background 120ms ease, box-shadow 120ms ease, transform 120ms ease; } .rack-insert-zone__icon { @@ -489,6 +463,13 @@ button, input, select { 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__ear { opacity: 0.28; @@ -500,12 +481,12 @@ button, input, select { padding: 0; user-select: none; overflow: hidden; - border-radius: 10px; - border: 1px solid rgba(122, 127, 137, 0.35); - background: linear-gradient(180deg, #fcfcfd, #e1e5eb); + 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)); box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.76), - 0 10px 18px rgba(80, 84, 93, 0.18); + inset 0 1px 0 rgba(255, 255, 255, 0.9), + 0 10px 18px rgba(5, 6, 7, 0.28); touch-action: none; cursor: grab; } @@ -548,16 +529,32 @@ button, input, select { color: #17191c; } -.rack-item__actions { - display: flex; - flex-wrap: wrap; - gap: 8px; +.rack-item__drag-hint { + align-self: start; + padding: 5px 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 { - padding: 8px 10px; - font-size: 0.84rem; - background: rgba(181, 93, 45, 0.92); +.rack-item__remove { + position: absolute; + top: 8px; + 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 { diff --git a/public/assets/app.js b/public/assets/app.js index 1ccf487..fb0f6e8 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -226,6 +226,7 @@ function renderRack() { } 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); const slots = []; @@ -238,26 +239,25 @@ function renderRack() { `); } - const insertionZones = renderInsertionZones(rack); - ui.rackGrid.innerHTML = `
${rack.rackStandard === "19_inch" ? '19" Rack' : '10" Rack'}
${rack.totalU} HE
${slots.join("")}
-
${insertionZones}
+
`; + 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) => { 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) => { const component = getComponent(item.componentId); if (!component) { @@ -284,21 +284,12 @@ function renderRack() { ${formatRackPosition(item.y)} -
- - - - - -
+
Ziehen zum verschieben
+ `; 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", () => { state.placedItems = state.placedItems.filter((entry) => entry.placementId !== item.placementId); 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) { if (!state.selectedComponentId) { return; @@ -494,7 +472,7 @@ function renderCableEstimate() { 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 sideAllowance = estimateSideAllowance(fromComponent, toComponent); const rawLength = verticalMm + depthAllowance + sideAllowance; @@ -656,8 +634,9 @@ function findFirstFreePosition(heightU) { return null; } - const testHeight = heightU * RACK_UNIT_PX; - for (let y = 0; y <= getRackHeightPx(rack) - testHeight; y += RACK_UNIT_PX) { + 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) { @@ -883,8 +862,9 @@ function renderInsertionZones(rack) { return ""; } + const unitHeight = getUnitHeightPx(); 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 other = getComponent(item.componentId); if (!other) { @@ -929,11 +909,15 @@ function renderInsertionZones(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; } function getComponentHeightPx(component) { - return component.heightU * RACK_UNIT_PX; + return component.heightU * getUnitHeightPx(); } function getComponentWidthPercent(component) { @@ -961,7 +945,7 @@ function getPlacementCenterY(item, component) { } function formatRackPosition(y) { - return `${(y / RACK_UNIT_PX + 1).toFixed(1)}U`; + return `${(y / getUnitHeightPx() + 1).toFixed(1)}U`; } function getPlacementOverlaps() { @@ -995,6 +979,15 @@ function getRackInnerWidthPx() { 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) { return Math.max(min, Math.min(max, value)); } diff --git a/public/assets/images/rack-template.png b/public/assets/images/rack-template.png new file mode 100644 index 0000000..d8c91da Binary files /dev/null and b/public/assets/images/rack-template.png differ