initial personal site
This commit is contained in:
361
static/script.js
361
static/script.js
@ -1,361 +0,0 @@
|
||||
const container = document.getElementById('patch-container');
|
||||
const svgOverlay = document.getElementById('cable-overlay');
|
||||
|
||||
const portsState = {};
|
||||
|
||||
const LED_COLORS = [
|
||||
// Gruvbox dark bright colors
|
||||
'#fb4934', // red
|
||||
'#b8bb26', // green
|
||||
'#fabd2f', // yellow
|
||||
'#83a598', // blue
|
||||
'#d3869b', // purple
|
||||
'#8ec07c', // aqua
|
||||
'#fe8019', // orange
|
||||
];
|
||||
|
||||
const CABLE_COLORS = [
|
||||
// Gruvbox light dark colors
|
||||
'#9d0006', // red
|
||||
'#79740e', // green
|
||||
'#b57614', // yellow
|
||||
'#076678', // blue
|
||||
'#8f3f71', // purple
|
||||
'#427b58', // aqua
|
||||
'#af3a03', // orange
|
||||
];
|
||||
|
||||
let cables = [];
|
||||
|
||||
let dragSource = null;
|
||||
let tempCablePath = null;
|
||||
|
||||
const NUM_SEGMENTS = 64;
|
||||
const GRAVITY = 0.6;
|
||||
const DAMPING = 0.4;
|
||||
const CONSTRAINT_ITERATIONS = 15;
|
||||
|
||||
const SWAY_AMPLITUDE = 0.05;
|
||||
const SWAY_FREQUENCY = 0.002;
|
||||
|
||||
function getPortCenter(portEl) {
|
||||
const rect = portEl.getBoundingClientRect();
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2
|
||||
};
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* LED logic
|
||||
**********************************************/
|
||||
function initPortState(portId) {
|
||||
if (!portsState[portId]) {
|
||||
portsState[portId] = {
|
||||
connectionCount: 0,
|
||||
ledColor: null,
|
||||
ledIsLit: false,
|
||||
nextBlinkTime: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
function addConnection(portId) {
|
||||
initPortState(portId);
|
||||
const st = portsState[portId];
|
||||
st.connectionCount++;
|
||||
if (st.connectionCount === 1) {
|
||||
// choose a color if not set
|
||||
if (!st.ledColor) {
|
||||
st.ledColor = LED_COLORS[Math.floor(Math.random() * LED_COLORS.length)];
|
||||
}
|
||||
// schedule next blink
|
||||
st.nextBlinkTime = performance.now();
|
||||
}
|
||||
}
|
||||
function removeConnection(portId) {
|
||||
initPortState(portId);
|
||||
const st = portsState[portId];
|
||||
if (st.connectionCount > 0) {
|
||||
st.connectionCount--;
|
||||
}
|
||||
if (st.connectionCount <= 0) {
|
||||
st.connectionCount = 0;
|
||||
st.ledIsLit = false;
|
||||
updatePortLED(portId);
|
||||
}
|
||||
}
|
||||
/** Check if we should toggle LED, then update DOM. */
|
||||
function updateLEDs(now) {
|
||||
for (const portId in portsState) {
|
||||
const st = portsState[portId];
|
||||
if (st.connectionCount > 0) {
|
||||
if (now >= st.nextBlinkTime) {
|
||||
st.ledIsLit = !st.ledIsLit;
|
||||
const interval = 50 + Math.random() * 150; // 200..800 ms
|
||||
st.nextBlinkTime = now + interval;
|
||||
}
|
||||
}
|
||||
updatePortLED(portId);
|
||||
}
|
||||
}
|
||||
function updatePortLED(portId) {
|
||||
const st = portsState[portId];
|
||||
const portEl = document.getElementById(portId);
|
||||
if (!portEl) return;
|
||||
const ledEl = portEl.querySelector('.led');
|
||||
if (!ledEl) return;
|
||||
|
||||
if (st.connectionCount > 0 && st.ledColor) {
|
||||
ledEl.style.background = st.ledIsLit ? st.ledColor : '#222';
|
||||
} else {
|
||||
ledEl.style.background = '#222';
|
||||
}
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* Cable creation & removal
|
||||
**********************************************/
|
||||
function randomColor() {
|
||||
return CABLE_COLORS[Math.floor(Math.random() * CABLE_COLORS.length)];
|
||||
}
|
||||
function createCablePath(color) {
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute('stroke', color);
|
||||
path.setAttribute('fill', 'none');
|
||||
path.setAttribute('stroke-width', '3');
|
||||
path.classList.add('cable-path');
|
||||
// We want to click cables to remove them
|
||||
path.addEventListener('click', onCableClick);
|
||||
svgOverlay.appendChild(path);
|
||||
return path;
|
||||
}
|
||||
function createRope(sourceEl, targetEl, numSegments) {
|
||||
const s = getPortCenter(sourceEl);
|
||||
const t = getPortCenter(targetEl);
|
||||
const rope = [];
|
||||
for (let i = 0; i <= numSegments; i++) {
|
||||
const alpha = i / numSegments;
|
||||
rope.push({
|
||||
x: s.x + (t.x - s.x) * alpha,
|
||||
y: s.y + (t.y - s.y) * alpha,
|
||||
vx: 0, vy: 0
|
||||
});
|
||||
}
|
||||
return rope;
|
||||
}
|
||||
function connect(sourceId, targetId, color) {
|
||||
if (!sourceId || !targetId || sourceId === targetId) return;
|
||||
const sourceEl = document.getElementById(sourceId);
|
||||
const targetEl = document.getElementById(targetId);
|
||||
if (!sourceEl || !targetEl) return;
|
||||
|
||||
color = color || randomColor();
|
||||
const rope = createRope(sourceEl, targetEl, NUM_SEGMENTS);
|
||||
const dx = rope[rope.length - 1].x - rope[0].x;
|
||||
const dy = rope[rope.length - 1].y - rope[0].y;
|
||||
const totalDist = Math.sqrt(dx * dx + dy * dy);
|
||||
const restLength = totalDist / NUM_SEGMENTS;
|
||||
|
||||
const pathEl = createCablePath(color);
|
||||
const swayOffset = Math.random() * 1000;
|
||||
|
||||
cables.push({
|
||||
sourceId, targetId,
|
||||
pathEl, ropeSegments: rope,
|
||||
restLength, color, swayOffset
|
||||
});
|
||||
|
||||
addConnection(sourceId);
|
||||
addConnection(targetId);
|
||||
}
|
||||
function removeCable(cable) {
|
||||
svgOverlay.removeChild(cable.pathEl);
|
||||
cables = cables.filter(c => c !== cable);
|
||||
removeConnection(cable.sourceId);
|
||||
removeConnection(cable.targetId);
|
||||
}
|
||||
function onCableClick(e) {
|
||||
const pathEl = e.currentTarget;
|
||||
const cable = cables.find(c => c.pathEl === pathEl);
|
||||
if (cable) removeCable(cable);
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* Dragging logic (user can draw cables)
|
||||
**********************************************/
|
||||
function handlePortMouseDown(e) {
|
||||
e.preventDefault();
|
||||
dragSource = e.currentTarget;
|
||||
const color = randomColor();
|
||||
tempCablePath = createCablePath(color);
|
||||
tempCablePath.style.pointerEvents = 'none';
|
||||
}
|
||||
function handleMouseMove(e) {
|
||||
if (!dragSource || !tempCablePath) return;
|
||||
const s = getPortCenter(dragSource);
|
||||
const mx = e.clientX, my = e.clientY;
|
||||
const midX = (s.x + mx) / 2, midY = (s.y + my) / 2;
|
||||
const d = `
|
||||
M ${s.x},${s.y}
|
||||
C ${midX},${s.y}
|
||||
${midX},${my}
|
||||
${mx},${my}
|
||||
`;
|
||||
tempCablePath.setAttribute('d', d);
|
||||
}
|
||||
function handleMouseUp(e) {
|
||||
|
||||
const dropTargetEl = document.elementFromPoint(e.clientX, e.clientY);
|
||||
|
||||
const portEl = dropTargetEl && dropTargetEl.closest('.port');
|
||||
|
||||
if (portEl) {
|
||||
|
||||
const sourceId = dragSource.id;
|
||||
const targetId = portEl.id;
|
||||
if (sourceId !== targetId) {
|
||||
const color = tempCablePath.getAttribute('stroke');
|
||||
svgOverlay.removeChild(tempCablePath);
|
||||
tempCablePath = null;
|
||||
connect(sourceId, targetId, color);
|
||||
} else {
|
||||
|
||||
svgOverlay.removeChild(tempCablePath);
|
||||
tempCablePath = null;
|
||||
}
|
||||
} else {
|
||||
svgOverlay.removeChild(tempCablePath);
|
||||
tempCablePath = null;
|
||||
}
|
||||
dragSource = null;
|
||||
}
|
||||
|
||||
/***********************************************
|
||||
* NAS drive-lights random blinking
|
||||
***********************************************/
|
||||
let driveLights = [];
|
||||
function initDriveLights() {
|
||||
driveLights = document.querySelectorAll('.drive-light');
|
||||
driveLights.forEach(light => {
|
||||
|
||||
light.dataset.nextBlinkTime = '0';
|
||||
});
|
||||
}
|
||||
function updateDriveLights(timestamp) {
|
||||
driveLights.forEach(light => {
|
||||
let nextBlink = parseFloat(light.dataset.nextBlinkTime) || 0;
|
||||
if (timestamp >= nextBlink) {
|
||||
|
||||
light.classList.toggle('lit');
|
||||
|
||||
const interval = 300 + Math.random() * 500; // 300..800 ms
|
||||
light.dataset.nextBlinkTime = (timestamp + interval).toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**********************************************
|
||||
* Main animation loop
|
||||
**********************************************/
|
||||
function animate(timestamp) {
|
||||
|
||||
updateLEDs(timestamp);
|
||||
updateDriveLights(timestamp);
|
||||
|
||||
|
||||
const dt = 1.0;
|
||||
cables.forEach(cable => {
|
||||
const rope = cable.ropeSegments;
|
||||
const n = rope.length;
|
||||
if (n < 2) return;
|
||||
|
||||
const sourceEl = document.getElementById(cable.sourceId);
|
||||
const targetEl = document.getElementById(cable.targetId);
|
||||
if (!sourceEl || !targetEl) return;
|
||||
|
||||
const s = getPortCenter(sourceEl);
|
||||
const t = getPortCenter(targetEl);
|
||||
|
||||
// anchors
|
||||
rope[0].x = s.x; rope[0].y = s.y;
|
||||
rope[0].vx = 0; rope[0].vy = 0;
|
||||
rope[n - 1].x = t.x; rope[n - 1].y = t.y;
|
||||
rope[n - 1].vx = 0; rope[n - 1].vy = 0;
|
||||
|
||||
// 1) gravity + wind for interior segments
|
||||
for (let i = 1; i < n - 1; i++) {
|
||||
rope[i].vy += GRAVITY;
|
||||
const wave = Math.sin((timestamp + cable.swayOffset + i * 50) * SWAY_FREQUENCY);
|
||||
rope[i].vx += wave * SWAY_AMPLITUDE;
|
||||
|
||||
rope[i].vx *= DAMPING;
|
||||
rope[i].vy *= DAMPING;
|
||||
rope[i].x += rope[i].vx * dt;
|
||||
rope[i].y += rope[i].vy * dt;
|
||||
}
|
||||
|
||||
// 2) constraints
|
||||
for (let iter = 0; iter < CONSTRAINT_ITERATIONS; iter++) {
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
let p1 = rope[i], p2 = rope[i + 1];
|
||||
let dx = p2.x - p1.x, dy = p2.y - p1.y;
|
||||
let dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < 0.001) dist = 0.001;
|
||||
let diff = (dist - cable.restLength) / dist;
|
||||
if (i !== 0) {
|
||||
p1.x += 0.5 * dx * diff;
|
||||
p1.y += 0.5 * dy * diff;
|
||||
}
|
||||
if (i + 1 !== n - 1) {
|
||||
p2.x -= 0.5 * dx * diff;
|
||||
p2.y -= 0.5 * dy * diff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) recalc velocities
|
||||
for (let i = 1; i < n - 1; i++) {
|
||||
rope[i].vx = rope[i].x - (rope[i].x - rope[i].vx * dt);
|
||||
rope[i].vy = rope[i].y - (rope[i].y - rope[i].vy * dt);
|
||||
}
|
||||
|
||||
// 4) update path
|
||||
let pathD = `M ${rope[0].x},${rope[0].y}`;
|
||||
for (let i = 1; i < n; i++) {
|
||||
pathD += ` L ${rope[i].x},${rope[i].y}`;
|
||||
}
|
||||
cable.pathEl.setAttribute('d', pathD);
|
||||
});
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********************************************
|
||||
* Startup
|
||||
**********************************************/
|
||||
initDriveLights();
|
||||
document.querySelectorAll('.port').forEach(portEl => {
|
||||
portEl.addEventListener('mousedown', handlePortMouseDown);
|
||||
});
|
||||
container.addEventListener('mousemove', handleMouseMove);
|
||||
container.addEventListener('mouseup', handleMouseUp);
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
|
||||
|
||||
connect('r1s0_portA', 'r1s2_portA',randomColor());
|
||||
connect('r1s2_portB', 'r2s1_portA',randomColor());
|
||||
connect('r1s2_portC', 'r1s3_portA',randomColor());
|
||||
|
||||
connect('r2s1_portB', 'r2s2_portS',randomColor());
|
||||
connect('r2s2_portA', 'r2s3_portA',randomColor());
|
||||
connect('r2s2_portB', 'r3s1_portA',randomColor());
|
||||
connect('r2s2_portC', 'r3s2_portA',randomColor());
|
||||
connect('r2s2_portD', 'r3s3_portA',randomColor());
|
||||
connect('r2s1_portD', 'r2s1_portG',randomColor());
|
||||
connect('r2s1_portK', 'r3s2_portB',randomColor());
|
||||
|
||||
|
||||
|
||||
|
341
static/style.css
341
static/style.css
@ -3,321 +3,72 @@
|
||||
padding: 0
|
||||
}
|
||||
|
||||
/* Basic reset */
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #1d2021;
|
||||
overflow: hidden;
|
||||
/* No default scrollbars */
|
||||
font-family: "JetBrains Mono", serif;
|
||||
background: #e8e8ea;
|
||||
font-family: "Newsreader", serif;
|
||||
font-optical-sizing: auto;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 20px;
|
||||
color: #1A1C1A;
|
||||
}
|
||||
|
||||
.header {
|
||||
column-span:all;
|
||||
}
|
||||
|
||||
.holder {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
color: #ebdbb2;
|
||||
font-size: 5rem;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 36px;
|
||||
color: #ebdbb2;
|
||||
.regimg {
|
||||
max-width: 14em;
|
||||
max-height: 20em;
|
||||
height: auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 28px;
|
||||
color: #ebdbb2;
|
||||
.largeimage {
|
||||
width: auto;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
column-span: none;
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Main Container (holds racks + cables)
|
||||
*******************************************/
|
||||
#patch-container {
|
||||
position: relative;
|
||||
.content {
|
||||
/* height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* SVG overlay for cables, on top of racks */
|
||||
#cable-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
/* put cables on top */
|
||||
pointer-events: none;
|
||||
/* let clicks pass through by default */
|
||||
}
|
||||
|
||||
/* Each cable path can be clicked on => remove */
|
||||
.cable-path {
|
||||
pointer-events: auto;
|
||||
/* re-enable clicks for cable paths specifically */
|
||||
cursor: pointer;
|
||||
transition: stroke 0.2s;
|
||||
}
|
||||
|
||||
.cable-path:hover {
|
||||
stroke: #cc241d !important;
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Data center (the racks)
|
||||
*******************************************/
|
||||
#datacenter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
/* If you want horizontal scrolling if there are many racks, use:
|
||||
overflow-x: auto;
|
||||
*/
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
/* behind cable overlay */
|
||||
}
|
||||
|
||||
.rack {
|
||||
width: 16em;
|
||||
/* each rack’s width */
|
||||
/* fill all available vertical space */
|
||||
background: #282828;
|
||||
border: 2px solid #3c3836;
|
||||
margin: 10px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* stack servers top to bottom */
|
||||
}
|
||||
|
||||
.rack-title {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: #ebdbb2;
|
||||
margin: 8px 0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.server {
|
||||
background: #32302f;
|
||||
border: 1px solid #3c3836;
|
||||
margin: 6px;
|
||||
/* spacing within the rack */
|
||||
}
|
||||
|
||||
.server-under-construction {
|
||||
/* Diagonal stripes:
|
||||
repeating-linear-gradient(angle, color1 start, color1 end, color2 start, color2 end) */
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
#282828 0,
|
||||
#282828 12px,
|
||||
#32302f 12px,
|
||||
#32302f 24px
|
||||
);
|
||||
color:#000;
|
||||
border: 2px dashed #282828; /* Give it a dashed border for effect */
|
||||
}
|
||||
|
||||
.server-row {
|
||||
padding: 5px 8px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.text-row {
|
||||
font-size: 14px;
|
||||
color: #ebdbb2;
|
||||
}
|
||||
|
||||
.port-row {
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
/* If you want a spacer between servers, you can use a simple div with .spacer. */
|
||||
.spacer {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/*******************************************
|
||||
* Port + LED styling
|
||||
*******************************************/
|
||||
.port {
|
||||
position: relative;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background: #32302f;
|
||||
border: 2px solid #504945;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.port:hover {
|
||||
background: #7c6f64;
|
||||
border-color: #ebdbb2;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #b16286;
|
||||
}
|
||||
|
||||
/* visited link */
|
||||
a:visited {
|
||||
color: #8f3f71;
|
||||
}
|
||||
|
||||
/* mouse over link */
|
||||
a:hover {
|
||||
color: #d3869b;
|
||||
}
|
||||
|
||||
/* selected link */
|
||||
a:active {
|
||||
color: #b16286;
|
||||
}
|
||||
|
||||
.led {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
width: .4rem;
|
||||
height: .4rem;
|
||||
background: #1d2021;
|
||||
/* off state */
|
||||
border: 2px solid #504945;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 2px #000;
|
||||
}
|
||||
|
||||
.temp-row {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.temp-label {
|
||||
color: #eee;
|
||||
font-size: 11px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.temp-bar {
|
||||
flex: 1;
|
||||
height: 10px;
|
||||
background: linear-gradient(to right, #f80, #f00);
|
||||
box-shadow: 0 0 6px rgba(255,80,0,0.5);
|
||||
animation: flickerHeat 1.5s infinite ease-in-out alternate;
|
||||
}
|
||||
@keyframes flickerHeat {
|
||||
0% { opacity: 1; transform: scaleX(1); }
|
||||
100% { opacity: 0.7; transform: scaleX(1.05); }
|
||||
}
|
||||
|
||||
/***************************************************
|
||||
* POWER USAGE LIGHTS (Random Blink)
|
||||
***************************************************/
|
||||
.power-lights-row {
|
||||
gap: 4px;
|
||||
}
|
||||
.power-light {
|
||||
width: 10px; height: 10px;
|
||||
background: #222;
|
||||
border: 2px solid #555;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 2px #000;
|
||||
}
|
||||
.power-light.lit {
|
||||
background: #0f0; /* or something bright to indicate usage */
|
||||
}
|
||||
|
||||
/************************************************
|
||||
* FAN SPACER: Animated fans
|
||||
************************************************/
|
||||
.fans {
|
||||
background: #7c6f64;
|
||||
margin: 6px;
|
||||
/* spacing within the rack */
|
||||
}
|
||||
|
||||
.fan-row {
|
||||
display: flex;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
grid-template: repeat(4, 1fr) / repeat(2, 1fr);
|
||||
grid-auto-flow: column; */
|
||||
height: 100%;
|
||||
width: 40em;
|
||||
min-width: 35em;
|
||||
column-count: 2;
|
||||
column-gap: 1em;
|
||||
column-rule: thin solid black;
|
||||
column-fill: auto;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.fan {
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 50%;
|
||||
background-image: url('fan.svg');
|
||||
filter: invert(29%) sepia(3%) saturate(1342%) hue-rotate(338deg) brightness(93%) contrast(90%);
|
||||
animation: spin 1s linear infinite;
|
||||
transform-origin: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: all 1s linear;
|
||||
}
|
||||
|
||||
.fan:hover {
|
||||
animation: 5s;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************
|
||||
* NAS SPACER: Random blinking drive lights
|
||||
************************************************/
|
||||
.nas {
|
||||
background: #504945;
|
||||
border: 1px solid #7c6f64;
|
||||
margin: 6px;
|
||||
/* spacing within the rack */
|
||||
}
|
||||
|
||||
.drive-lights-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
padding: 5px 0;
|
||||
flex-wrap: wrap;
|
||||
/* if many lights, wrap to next line */
|
||||
}
|
||||
|
||||
.drive-light {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #1d2021;
|
||||
/* off by default */
|
||||
border: 2px solid #504945;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 0 2px #000;
|
||||
}
|
||||
|
||||
.drive-light.lit {
|
||||
background: #b8bb26;
|
||||
/* or any color you like */
|
||||
.item {
|
||||
padding: .2em;
|
||||
margin: .2em;
|
||||
}
|
Reference in New Issue
Block a user