yssadasd
All checks were successful
Deploy / deploy (push) Successful in 16s

This commit is contained in:
2026-05-15 23:08:24 +02:00
parent 838f4d7eec
commit b2dcff9cb6
2 changed files with 329 additions and 26 deletions

View File

@@ -194,6 +194,12 @@ button, input, select {
gap: 10px;
}
.component-card__preview {
padding: 10px;
border-radius: 14px;
background: linear-gradient(180deg, rgba(44, 47, 53, 0.08), rgba(18, 19, 23, 0.02));
}
.component-card__header {
display: flex;
justify-content: space-between;
@@ -276,12 +282,16 @@ button, input, select {
.rack-grid {
position: relative;
border-radius: 28px;
border: 2px solid rgba(77, 50, 21, 0.4);
border: 2px solid rgba(42, 34, 24, 0.55);
background:
linear-gradient(90deg, rgba(98, 71, 45, 0.18), rgba(255, 255, 255, 0) 14%, rgba(255, 255, 255, 0) 86%, rgba(98, 71, 45, 0.18)),
linear-gradient(180deg, rgba(255, 255, 255, 0.42), rgba(218, 197, 174, 0.36));
linear-gradient(90deg, rgba(9, 11, 15, 0.88), rgba(20, 25, 31, 0.15) 14%, rgba(20, 25, 31, 0.15) 86%, rgba(9, 11, 15, 0.88)),
linear-gradient(180deg, #6e737b 0%, #3a3f48 12%, #15181d 22%, #0f1217 100%);
padding: 24px 38px 24px 74px;
overflow: hidden;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 0 0 2px rgba(255, 255, 255, 0.04),
0 24px 40px rgba(9, 11, 15, 0.22);
}
.rack-grid::before,
@@ -290,9 +300,14 @@ button, input, select {
position: absolute;
top: 18px;
bottom: 18px;
width: 12px;
border-radius: 999px;
background: rgba(92, 65, 37, 0.42);
width: 16px;
border-radius: 6px;
background:
radial-gradient(circle at center 8px, rgba(205, 214, 229, 0.75) 0 2px, transparent 2.5px) center top / 100% 18px repeat-y,
linear-gradient(180deg, #404550, #1a1d23);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
inset 0 0 12px rgba(0, 0, 0, 0.4);
}
.rack-grid::before {
@@ -306,7 +321,9 @@ button, input, select {
.rack-slot {
position: relative;
height: var(--rack-unit-height);
border-top: 1px dashed rgba(89, 60, 31, 0.25);
border-top: 1px solid rgba(202, 212, 222, 0.06);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0.06));
}
.rack-slot:first-child {
@@ -314,7 +331,8 @@ button, input, select {
}
.rack-slot.is-drop-target {
background: rgba(181, 93, 45, 0.11);
background:
linear-gradient(180deg, rgba(236, 129, 69, 0.16), rgba(236, 129, 69, 0.08));
}
.rack-slot__label {
@@ -322,7 +340,7 @@ button, input, select {
left: -52px;
top: 8px;
font-size: 0.76rem;
color: var(--muted);
color: rgba(219, 225, 235, 0.84);
}
.rack-items-layer {
@@ -334,11 +352,15 @@ button, input, select {
position: absolute;
left: 0;
right: 0;
padding: 12px 12px 12px 14px;
display: grid;
gap: 8px;
border-left: 8px solid var(--accent);
padding: 0;
user-select: none;
overflow: hidden;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(180deg, #272c34, #11151b);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
inset 0 -12px 18px rgba(0, 0, 0, 0.28);
}
.rack-item[data-standard="10_inch"] {
@@ -356,13 +378,32 @@ button, input, select {
align-items: start;
}
.rack-item__overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 8px 10px 8px 12px;
background: linear-gradient(90deg, rgba(11, 13, 16, 0.72), rgba(11, 13, 16, 0.08) 45%, rgba(11, 13, 16, 0.56));
}
.rack-item__face {
position: absolute;
inset: 0;
}
.rack-item__meta,
.list-output,
.notes {
color: var(--muted);
color: rgba(222, 227, 235, 0.76);
font-size: 0.9rem;
}
.rack-item__header strong {
color: #f3f5f8;
}
.rack-item__actions {
display: flex;
flex-wrap: wrap;
@@ -372,6 +413,7 @@ button, input, select {
.rack-item__actions button {
padding: 8px 10px;
font-size: 0.84rem;
background: rgba(181, 93, 45, 0.9);
}
.subpanel {
@@ -438,6 +480,181 @@ button, input, select {
border-radius: var(--radius-md);
}
.device-face {
position: relative;
width: 100%;
height: 100%;
padding: 8px 12px;
overflow: hidden;
}
.device-face--library {
min-height: 72px;
border-radius: 10px;
}
.device-face--rack {
min-height: 100%;
}
.device-face--switch,
.device-face--patch-panel,
.device-face--pdu,
.device-face--blank,
.device-face--generic {
background: linear-gradient(180deg, #4d535d 0%, #262c34 18%, #12161d 100%);
}
.device-face--ups {
background: linear-gradient(180deg, #636973 0%, #2a3038 16%, #0f1319 100%);
}
.device-face--shelf {
background: linear-gradient(180deg, #3e444c 0%, #171b21 100%);
}
.device-silkscreen {
position: absolute;
left: 12px;
bottom: 8px;
font-size: 0.7rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: rgba(222, 227, 235, 0.58);
}
.device-leds {
display: flex;
gap: 5px;
position: absolute;
top: 10px;
right: 12px;
}
.device-leds span {
width: 7px;
height: 7px;
border-radius: 50%;
background: radial-gradient(circle at 35% 35%, #b8ff8e, #367f26 72%);
box-shadow: 0 0 8px rgba(114, 255, 86, 0.4);
}
.device-ports,
.device-keystones,
.device-sockets {
display: grid;
gap: 4px;
}
.device-ports {
position: absolute;
left: 16px;
right: 54px;
top: 18px;
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.device-ports span,
.device-keystones span {
height: 11px;
border-radius: 2px;
background: linear-gradient(180deg, #181d23, #050709);
border: 1px solid rgba(183, 191, 207, 0.12);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.device-ports--8 {
grid-template-columns: repeat(8, minmax(0, 1fr));
}
.device-ports--12 {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.device-keystones {
position: absolute;
left: 16px;
right: 16px;
top: 18px;
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.device-face--patch-panel .device-keystones span {
height: 13px;
background: linear-gradient(180deg, #0b0d10, #1b2128);
}
.device-face--patch-panel .device-keystones span:nth-child(odd) {
border-color: rgba(239, 147, 78, 0.24);
}
.device-uplink {
position: absolute;
top: 16px;
right: 16px;
width: 22px;
height: 16px;
border-radius: 4px;
border: 1px solid rgba(188, 197, 214, 0.15);
background: linear-gradient(180deg, #11161c, #050709);
}
.device-sockets {
position: absolute;
top: 14px;
left: 16px;
right: 44px;
grid-template-columns: repeat(8, minmax(0, 1fr));
align-items: center;
}
.device-sockets span {
width: 15px;
height: 15px;
border-radius: 50%;
background:
radial-gradient(circle at center, transparent 0 3px, #0f1216 3px 5px, transparent 5px),
linear-gradient(180deg, #aeb8c6, #57606e);
border: 1px solid rgba(229, 233, 240, 0.18);
}
.device-display {
position: absolute;
left: 16px;
top: 14px;
width: 68px;
height: 30px;
border-radius: 8px;
background:
linear-gradient(180deg, rgba(46, 180, 156, 0.9), rgba(18, 97, 90, 0.92)),
linear-gradient(180deg, #0f151c, #080b0f);
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
0 0 16px rgba(46, 180, 156, 0.22);
}
.device-vents {
position: absolute;
top: 16px;
right: 18px;
width: 96px;
height: 22px;
background:
repeating-linear-gradient(90deg, rgba(222, 227, 235, 0.16) 0 3px, transparent 3px 7px);
opacity: 0.8;
}
.device-shelf-top {
position: absolute;
left: 18px;
right: 18px;
top: 14px;
height: 18px;
border-radius: 4px 4px 12px 12px;
background: linear-gradient(180deg, #69707a, #262b33);
box-shadow: 0 5px 12px rgba(0, 0, 0, 0.28);
}
@media (max-width: 1280px) {
.workspace {
grid-template-columns: 1fr;

View File

@@ -139,7 +139,10 @@ function renderLibrary() {
ui.componentLibrary.innerHTML = filtered
.map(
(component) => `
<article class="component-card">
<article class="component-card component-card--${escapeHtml(component.category)}">
<div class="component-card__preview">
${renderComponentFace(component, "library")}
</div>
<div class="component-card__header">
<div>
<strong>${escapeHtml(component.name)}</strong>
@@ -235,12 +238,16 @@ function renderRack() {
const top = (rack.totalU - (item.startU + component.heightU) + 1) * 38;
const height = component.heightU * 38 - 2;
const element = document.createElement("article");
element.className = "rack-item";
element.className = `rack-item rack-item--${component.category.replace(/_/g, "-")}`;
element.dataset.standard = component.rackStandard;
element.draggable = true;
element.style.top = `${top}px`;
element.style.height = `${height}px`;
element.innerHTML = `
<div class="rack-item__face rack-item__face--${escapeHtml(component.category)}">
${renderComponentFace(component, "rack")}
</div>
<div class="rack-item__overlay">
<div class="rack-item__header">
<div>
<strong>${escapeHtml(component.name)}</strong>
@@ -253,6 +260,7 @@ function renderRack() {
<button type="button" data-action="move-down">-1U</button>
<button type="button" data-action="remove">Entfernen</button>
</div>
</div>
`;
element.addEventListener("dragstart", () => {
@@ -634,6 +642,84 @@ function formatCurrency(value, currency) {
}).format(value);
}
function renderComponentFace(component, mode) {
const ports = renderPortStrip(component);
const leds = '<div class="device-leds"><span></span><span></span><span></span></div>';
const label = `<div class="device-silkscreen">${escapeHtml(component.manufacturer || component.category)}</div>`;
const modeClass = mode === "rack" ? "device-face--rack" : "device-face--library";
switch (component.category) {
case "switch":
return `
<div class="device-face device-face--switch ${modeClass}">
${label}
${leds}
${ports}
<div class="device-uplink"></div>
</div>
`;
case "patch_panel":
return `
<div class="device-face device-face--patch-panel ${modeClass}">
${label}
${renderPatchPanel(component)}
</div>
`;
case "pdu":
return `
<div class="device-face device-face--pdu ${modeClass}">
${label}
<div class="device-sockets">${renderSocketStrip(component)}</div>
${leds}
</div>
`;
case "ups":
return `
<div class="device-face device-face--ups ${modeClass}">
<div class="device-display"></div>
<div class="device-vents"></div>
${label}
</div>
`;
case "shelf":
return `
<div class="device-face device-face--shelf ${modeClass}">
<div class="device-shelf-top"></div>
${label}
</div>
`;
case "blank_panel":
return `
<div class="device-face device-face--blank ${modeClass}">
${label}
</div>
`;
default:
return `
<div class="device-face device-face--generic ${modeClass}">
${label}
</div>
`;
}
}
function renderPortStrip(component) {
const totalPorts = component.name.includes("24") ? 24 : component.name.includes("12") ? 12 : 8;
const ports = Array.from({ length: totalPorts }, (_, index) => `<span title="Port ${index + 1}"></span>`).join("");
return `<div class="device-ports device-ports--${Math.min(totalPorts, 24)}">${ports}</div>`;
}
function renderPatchPanel(component) {
const totalPorts = component.name.includes("24") ? 24 : 12;
const ports = Array.from({ length: totalPorts }, (_, index) => `<span title="Patch ${index + 1}"></span>`).join("");
return `<div class="device-keystones">${ports}</div>`;
}
function renderSocketStrip(component) {
const totalSockets = component.name.includes("8") ? 8 : 6;
return Array.from({ length: totalSockets }, () => "<span></span>").join("");
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")