diff --git a/public/assets/app.css b/public/assets/app.css index 81f9421..0e70e8c 100644 --- a/public/assets/app.css +++ b/public/assets/app.css @@ -293,84 +293,271 @@ button, input, select { .rack-grid { position: relative; - border-radius: 28px; - border: 1px solid rgba(105, 108, 115, 0.12); + border-radius: 32px; + border: 1px solid rgba(105, 108, 115, 0.14); background: - radial-gradient(circle at top left, rgba(255, 255, 255, 0.07), transparent 20%), - linear-gradient(180deg, #26272b, #202126); + linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(239, 232, 214, 0.88)); padding: 16px; overflow: hidden; box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.08), - 0 20px 40px rgba(20, 18, 16, 0.18); + inset 0 1px 0 rgba(255, 255, 255, 0.7), + 0 24px 40px rgba(76, 60, 38, 0.14); } -.rack-grid__bay { +.rack-shell { position: relative; width: min(100%, 1180px); margin: 0 auto; 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; + border-radius: 28px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(0, 0, 0, 0.16)), + linear-gradient(90deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0)); box-shadow: - 0 18px 34px rgba(7, 8, 10, 0.32), + 0 24px 34px rgba(15, 16, 18, 0.22), inset 0 0 0 1px rgba(255, 255, 255, 0.04); isolation: isolate; } -.rack-grid__guides { - position: relative; - width: 100%; - height: 100%; - background: transparent; +.rack-shell__frame { + position: absolute; + background: linear-gradient(180deg, var(--rack-frame-light), var(--rack-frame-dark)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.18), + inset 0 -10px 12px rgba(0, 0, 0, 0.18); + z-index: 3; } -.rack-slot { - position: relative; - height: calc(100% / var(--rack-total-u)); - border-top: 0; - background: transparent; +.rack-shell__frame--top, +.rack-shell__frame--bottom { + left: 9%; + right: 9%; + height: 8%; + border-radius: 14px; } -.rack-slot:first-child { - border-top: 0; +.rack-shell__frame--top { + top: 6%; } -.rack-slot.is-drop-target { +.rack-shell__frame--bottom { + bottom: 6%; +} + +.rack-shell__frame--left, +.rack-shell__frame--right { + top: 8%; + bottom: 8%; + width: 6.8%; + border-radius: 24px; +} + +.rack-shell__frame--left { + left: 0.5%; +} + +.rack-shell__frame--right { + right: 0.5%; +} + +.rack-shell__screw { + position: absolute; + width: 3.6%; + aspect-ratio: 1; + border-radius: 50%; background: - linear-gradient(180deg, rgba(236, 129, 69, 0.16), rgba(236, 129, 69, 0.08)); + radial-gradient(circle at 32% 30%, rgba(255, 255, 255, 0.38), transparent 20%), + radial-gradient(circle at center, #5f646d 0 22%, #2b2f36 23% 55%, #818792 56% 70%, #23272e 71%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.22), + 0 2px 4px rgba(0, 0, 0, 0.25); + z-index: 4; } -.rack-slot__label { +.rack-shell__screw::before, +.rack-shell__screw::after { + content: ""; position: absolute; - 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); + top: 50%; + left: 50%; + background: rgba(20, 22, 26, 0.8); + border-radius: 999px; + transform: translate(-50%, -50%); } -.rack-slot__label--right { - left: auto; - right: 5.7%; +.rack-shell__screw::before { + width: 58%; + height: 10%; } -.rack-items-layer { +.rack-shell__screw::after { + width: 10%; + height: 58%; +} + +.rack-shell__screw--tl { top: 7.4%; left: 10.6%; } +.rack-shell__screw--tr { top: 7.4%; right: 10.6%; } +.rack-shell__screw--bl { bottom: 7.4%; left: 10.6%; } +.rack-shell__screw--br { bottom: 7.4%; right: 10.6%; } + +.rack-core { position: absolute; - inset: 13.9% 17.4% 16.8% 17.4%; + inset: 15% 12.8% 14% 12.8%; + display: grid; + grid-template-columns: 10% 1fr 10%; + gap: 0; z-index: 2; } -.rack-insertion-layer { +.rack-rail { + position: relative; + display: grid; + grid-template-rows: repeat(var(--rack-total-u), 1fr); + background: linear-gradient(180deg, color-mix(in srgb, var(--rack-rail) 90%, #14161a), color-mix(in srgb, var(--rack-rail) 65%, #08090b)); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.08), + inset 0 0 0 6px rgba(0, 0, 0, 0.18); +} + +.rack-rail--left { + border-radius: 8px 0 0 8px; +} + +.rack-rail--right { + border-radius: 0 8px 8px 0; +} + +.rack-rail__unit { + position: relative; + min-height: 0; +} + +.rack-rail__label { position: absolute; - inset: 13.9% 17.4% 16.8% 17.4%; - pointer-events: auto; - z-index: 3; + top: 50%; + transform: translateY(-50%); + font-size: 0.78rem; + color: rgba(228, 232, 238, 0.84); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.45); +} + +.rack-rail__label--left { + left: -2.1rem; +} + +.rack-rail__label--right { + right: -1.6rem; +} + +.rack-rail__tick { + position: absolute; + top: 39%; + width: 14%; + height: 2px; + background: rgba(223, 228, 235, 0.74); + border-radius: 999px; +} + +.rack-rail__tick--left { + left: 14%; +} + +.rack-rail__tick--right { + right: 14%; +} + +.rack-rail__holes { + position: absolute; + top: 50%; + transform: translateY(-50%); + display: grid; + grid-template-rows: repeat(3, 1fr); + gap: 0.22rem; + width: 24%; + height: 74%; +} + +.rack-rail__holes--left { + right: 13%; +} + +.rack-rail__holes--right { + left: 13%; +} + +.rack-rail__holes span { + border-radius: 4px; + background: linear-gradient(180deg, rgba(7, 8, 11, 0.96), rgba(25, 27, 33, 0.96)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); +} + +.rack-bay { + position: relative; + overflow: hidden; + background: + radial-gradient(circle at center, rgba(96, 99, 108, 0.22), transparent 48%), + linear-gradient(180deg, #181a1f, #2a2d34 36%, #22252c 68%, #17191d 100%); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.04), + inset 0 32px 42px rgba(0, 0, 0, 0.34), + inset 0 -24px 34px rgba(0, 0, 0, 0.28); +} + +.rack-bay__ceiling, +.rack-bay__floor { + position: absolute; + left: 8%; + right: 8%; + height: 14%; + background: + repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.04) 0 2%, transparent 2% 4.5%), + linear-gradient(180deg, rgba(71, 74, 82, 0.24), rgba(8, 9, 12, 0.12)); + z-index: 0; +} + +.rack-bay__ceiling { + top: 0; + transform: perspective(500px) rotateX(62deg); + transform-origin: top; +} + +.rack-bay__floor { + bottom: 0; + transform: perspective(500px) rotateX(-62deg); + transform-origin: bottom; +} + +.rack-bay__wall { + position: absolute; + top: 10%; + bottom: 10%; + width: 9%; + background: linear-gradient(90deg, rgba(7, 8, 11, 0.65), rgba(60, 64, 72, 0.12)); + z-index: 0; +} + +.rack-bay__wall--left { + left: 0; +} + +.rack-bay__wall--right { + right: 0; + transform: scaleX(-1); +} + +.rack-bay__back { + position: absolute; + inset: 14% 7% 14% 7%; + border-radius: 4px; + background: + radial-gradient(circle at 25% 25%, rgba(255, 255, 255, 0.12) 0 1%, transparent 1.2%), + radial-gradient(circle at 75% 25%, rgba(255, 255, 255, 0.12) 0 1%, transparent 1.2%), + radial-gradient(circle at 25% 75%, rgba(255, 255, 255, 0.12) 0 1%, transparent 1.2%), + radial-gradient(circle at 75% 75%, rgba(255, 255, 255, 0.12) 0 1%, transparent 1.2%), + linear-gradient(180deg, rgba(83, 86, 94, 0.16), rgba(24, 26, 31, 0.08)); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.04), + inset 0 12px 20px rgba(255, 255, 255, 0.03); } .rack-grid__header-badge { @@ -395,6 +582,19 @@ button, input, select { right: 1.8%; } +.rack-items-layer { + position: absolute; + inset: 0; + z-index: 2; +} + +.rack-insertion-layer { + position: absolute; + inset: 0; + pointer-events: auto; + z-index: 3; +} + .rack-insert-zone { position: absolute; display: flex; @@ -939,7 +1139,7 @@ button, input, select { padding-right: 26px; } - .rack-grid__bay, + .rack-shell, .rack-items-layer { width: 100%; } diff --git a/public/assets/app.js b/public/assets/app.js index 38c745e..29672f3 100644 --- a/public/assets/app.js +++ b/public/assets/app.js @@ -213,29 +213,37 @@ function renderRack() { ui.rackGrid.style.setProperty("--rack-total-u", String(rack.totalU)); applyRackColorTheme(ui.rackGrid, state.rackColor); - const slots = []; - for (let u = rack.totalU; u >= 1; u -= 1) { - slots.push(` -
- ${u}U - ${u} -
- `); - } - ui.rackGrid.innerHTML = `
${rack.rackStandard === "19_inch" ? '19" Rack' : '10" Rack'}
${rack.totalU} HE
-
-
${slots.join("")}
-
-
+
+
+
+
+
+
+
+
+
+
+
${renderRailScale(rack, "left")}
+
+
+
+
+
+
+
+
+
+
${renderRailScale(rack, "right")}
+
`; const layer = document.getElementById("rack-items-layer"); layer.style.height = "100%"; - const bay = ui.rackGrid.querySelector(".rack-grid__bay"); + const bay = document.getElementById("rack-bay"); bay.addEventListener("dragover", handleRackDragOver); bay.addEventListener("drop", handleRackDrop); bay.addEventListener("dragleave", handleRackDragLeave); @@ -287,6 +295,22 @@ function renderRack() { }); } +function renderRailScale(rack, side) { + const rows = []; + for (let u = rack.totalU; u >= 1; u -= 1) { + rows.push(` +
+ ${u} + + + + +
+ `); + } + return rows.join(""); +} + function insertSelectedComponentAt(y) { const componentId = state.libraryDragComponentId || state.selectedComponentId; if (!componentId) { @@ -899,10 +923,6 @@ function getPlacementCenterY(item, component) { return item.y + getComponentHeightPx(component) / 2; } -function formatRackPosition(y) { - return `${(y / getUnitHeightPx() + 1).toFixed(1)}U`; -} - function formatRackPosition(y, component) { const rack = getCurrentRackTemplate(); if (!rack) {