diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js
index 29b90f0..ab54e4a 100644
--- a/server/src/wled_controller/static/app.js
+++ b/server/src/wled_controller/static/app.js
@@ -1092,6 +1092,15 @@ async function showCalibration(deviceId) {
initSpanDrag();
requestAnimationFrame(() => renderCalibrationCanvas());
+ // Re-render on container resize (e.g. window resize changes aspect-ratio container)
+ if (!window._calibrationResizeObserver) {
+ window._calibrationResizeObserver = new ResizeObserver(() => {
+ updateSpanBars();
+ renderCalibrationCanvas();
+ });
+ }
+ window._calibrationResizeObserver.observe(preview);
+
} catch (error) {
console.error('Failed to load calibration:', error);
showToast('Failed to load calibration', 'error');
@@ -1116,6 +1125,9 @@ function forceCloseCalibrationModal() {
if (deviceId) {
clearTestMode(deviceId);
}
+ if (window._calibrationResizeObserver) {
+ window._calibrationResizeObserver.disconnect();
+ }
const modal = document.getElementById('calibration-modal');
const error = document.getElementById('calibration-error');
modal.style.display = 'none';
@@ -1198,9 +1210,9 @@ function renderCalibrationCanvas() {
const containerRect = container.getBoundingClientRect();
if (containerRect.width === 0 || containerRect.height === 0) return;
- // Canvas extends beyond the container (matches CSS: left:-36px, top:-36px, +72px/+72px)
- const padX = 36;
- const padY = 36;
+ // Canvas extends beyond the container (matches CSS: left:-40px, top:-40px, +80px/+80px)
+ const padX = 40;
+ const padY = 40;
const dpr = window.devicePixelRatio || 1;
const canvasW = containerRect.width + padX * 2;
@@ -1259,8 +1271,8 @@ function renderCalibrationCanvas() {
right: { y1: oy + ch + (spans.right?.start || 0) * edgeLenV, y2: oy + ch + (spans.right?.end || 1) * edgeLenV, midX: ox + cW - cw / 2, horizontal: false },
};
- // Axis positions for labels (outside the 12px toggle zones)
- const toggleSize = 12;
+ // Axis positions for labels (outside the 16px toggle zones)
+ const toggleSize = 16;
const axisPos = {
top: oy - toggleSize - 3,
bottom: oy + cH + toggleSize + 3,
@@ -1334,8 +1346,9 @@ function renderCalibrationCanvas() {
}
}
- // Tick styling
- const tickLen = 5;
+ // Tick styling — min/max ticks extend to container border, others short
+ const tickLenLong = toggleSize + 3;
+ const tickLenShort = 4;
ctx.strokeStyle = tickStroke;
ctx.lineWidth = 1;
ctx.fillStyle = tickFill;
@@ -1345,6 +1358,7 @@ function renderCalibrationCanvas() {
const fraction = count > 1 ? i / (count - 1) : 0.5;
const displayFraction = seg.reverse ? (1 - fraction) : fraction;
const ledIndex = totalLeds > 0 ? (seg.led_start + i) % totalLeds : seg.led_start + i;
+ const tickLen = (i === 0 || i === count - 1) ? tickLenLong : tickLenShort;
if (geo.horizontal) {
const tx = geo.x1 + displayFraction * (geo.x2 - geo.x1);
@@ -1379,16 +1393,16 @@ function renderCalibrationCanvas() {
}
});
- // Draw direction chevron at midpoint, inside the screen area
+ // Draw direction chevron at full-edge midpoint (not affected by span)
const s = 7;
let mx, my, angle;
if (geo.horizontal) {
- mx = (geo.x1 + geo.x2) / 2;
+ mx = ox + cw + edgeLenH / 2;
my = arrowPos[seg.edge];
angle = seg.reverse ? Math.PI : 0;
} else {
mx = arrowPos[seg.edge];
- my = (geo.y1 + geo.y2) / 2;
+ my = oy + ch + edgeLenV / 2;
angle = seg.reverse ? -Math.PI / 2 : Math.PI / 2;
}
@@ -1414,6 +1428,7 @@ function renderCalibrationCanvas() {
function updateSpanBars() {
const spans = window.edgeSpans || {};
+ const container = document.querySelector('.calibration-preview');
['top', 'right', 'bottom', 'left'].forEach(edge => {
const bar = document.querySelector(`.edge-span-bar[data-edge="${edge}"]`);
if (!bar) return;
@@ -1430,6 +1445,24 @@ function updateSpanBars() {
bar.style.top = (span.start * totalHeight) + 'px';
bar.style.height = ((span.end - span.start) * totalHeight) + 'px';
}
+
+ // Also reposition toggle zone to match span region
+ if (!container) return;
+ const toggle = container.querySelector(`.toggle-${edge}`);
+ if (!toggle) return;
+ if (isHorizontal) {
+ const cornerW = 56;
+ const edgeW = container.clientWidth - 2 * cornerW;
+ toggle.style.left = (cornerW + span.start * edgeW) + 'px';
+ toggle.style.right = 'auto';
+ toggle.style.width = ((span.end - span.start) * edgeW) + 'px';
+ } else {
+ const cornerH = 36;
+ const edgeH = container.clientHeight - 2 * cornerH;
+ toggle.style.top = (cornerH + span.start * edgeH) + 'px';
+ toggle.style.bottom = 'auto';
+ toggle.style.height = ((span.end - span.start) * edgeH) + 'px';
+ }
});
}
@@ -1537,6 +1570,11 @@ function setStartPosition(position) {
updateCalibrationPreview();
}
+function toggleEdgeInputs() {
+ const preview = document.querySelector('.calibration-preview');
+ if (preview) preview.classList.toggle('inputs-dimmed');
+}
+
function toggleDirection() {
const select = document.getElementById('cal-layout');
select.value = select.value === 'clockwise' ? 'counterclockwise' : 'clockwise';
diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html
index 0ea4dbb..7b1a8c7 100644
--- a/server/src/wled_controller/static/index.html
+++ b/server/src/wled_controller/static/index.html
@@ -80,15 +80,17 @@
+
Click an edge to toggle test LEDs on/off
-
0 / 0
+
0 / 0
-
Click an edge to toggle test LEDs on/off
@@ -170,8 +171,8 @@
@@ -181,6 +182,7 @@
@@ -224,6 +226,7 @@
@@ -390,8 +395,10 @@
modal.style.display = 'flex';
lockBody();
- // Hide cancel button if this is required login (no existing session)
+ // Hide cancel button and close X if this is required login (no existing session)
cancelBtn.style.display = hideCancel ? 'none' : 'inline-block';
+ const closeXBtn = document.getElementById('modal-close-x-btn');
+ if (closeXBtn) closeXBtn.style.display = hideCancel ? 'none' : '';
setTimeout(() => input.focus(), 100);
}
diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css
index 28bc8c7..246366f 100644
--- a/server/src/wled_controller/static/style.css
+++ b/server/src/wled_controller/static/style.css
@@ -58,9 +58,8 @@ header {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 20px 0;
- border-bottom: 2px solid var(--border-color);
- margin-bottom: 30px;
+ padding: 20px 0 10px;
+ margin-bottom: 10px;
}
.header-title {
@@ -783,6 +782,9 @@ input:-webkit-autofill:focus {
.modal-header {
padding: 24px 24px 16px 24px;
border-bottom: 1px solid var(--border-color);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
}
.modal-header h2 {
@@ -791,6 +793,27 @@ input:-webkit-autofill:focus {
color: var(--text-color);
}
+.modal-close-btn {
+ background: none;
+ border: none;
+ color: #777;
+ font-size: 1.2rem;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ border-radius: 4px;
+ transition: color 0.2s, background 0.2s;
+ flex-shrink: 0;
+}
+
+.modal-close-btn:hover {
+ color: var(--text-color);
+ background: rgba(128, 128, 128, 0.15);
+}
+
.modal-body {
padding: 24px;
}
@@ -850,11 +873,13 @@ input:-webkit-autofill:focus {
padding: 16px 24px 24px 24px;
display: flex;
justify-content: flex-end;
- gap: 12px;
+ gap: 8px;
}
-.modal-footer .btn {
- min-width: 100px;
+.modal-footer .btn-icon {
+ min-width: 60px;
+ padding: 10px 20px;
+ font-size: 1.4rem;
}
/* Theme Toggle */
@@ -875,9 +900,8 @@ input:-webkit-autofill:focus {
/* Footer */
.app-footer {
- margin-top: 60px;
- padding: 30px 0;
- border-top: 1px solid var(--border-color);
+ margin-top: 20px;
+ padding: 15px 0;
text-align: center;
}
@@ -911,7 +935,7 @@ input:-webkit-autofill:focus {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
- margin: 20px auto;
+ margin: 40px auto 20px;
background: var(--card-bg);
border: 2px solid var(--border-color);
border-radius: 8px;
@@ -920,10 +944,10 @@ input:-webkit-autofill:focus {
#calibration-preview-canvas {
position: absolute;
- top: -36px;
- left: -36px;
- width: calc(100% + 72px);
- height: calc(100% + 72px);
+ top: -40px;
+ left: -40px;
+ width: calc(100% + 80px);
+ height: calc(100% + 80px);
pointer-events: none;
z-index: 3;
}
@@ -950,12 +974,23 @@ input:-webkit-autofill:focus {
font-weight: 600;
opacity: 0.9;
transition: color 0.2s;
+ cursor: pointer;
+ user-select: none;
+}
+
+.preview-screen-total:hover {
+ opacity: 1;
}
.preview-screen-total.mismatch {
color: #FFC107;
}
+.inputs-dimmed .edge-led-input {
+ opacity: 0.2;
+ pointer-events: none;
+}
+
.preview-screen-controls {
display: flex;
align-items: center;
@@ -1011,8 +1046,8 @@ input:-webkit-autofill:focus {
position: absolute;
cursor: pointer;
z-index: 1;
- background: rgba(128, 128, 128, 0.08);
- border: 1px solid rgba(128, 128, 128, 0.15);
+ background: rgba(128, 128, 128, 0.1);
+ border: 1px solid rgba(128, 128, 128, 0.35);
border-radius: 3px;
transition: background 0.2s, box-shadow 0.2s;
}
@@ -1022,31 +1057,31 @@ input:-webkit-autofill:focus {
}
.toggle-top {
- top: -12px;
+ top: -16px;
left: 56px;
right: 56px;
- height: 12px;
+ height: 16px;
}
.toggle-bottom {
- bottom: -12px;
+ bottom: -16px;
left: 56px;
right: 56px;
- height: 12px;
+ height: 16px;
}
.toggle-left {
- left: -12px;
+ left: -16px;
top: 36px;
bottom: 36px;
- width: 12px;
+ width: 16px;
}
.toggle-right {
- right: -12px;
+ right: -16px;
top: 36px;
bottom: 36px;
- width: 12px;
+ width: 16px;
}
.edge-top {
@@ -1127,9 +1162,8 @@ input:-webkit-autofill:focus {
position: absolute;
background: rgba(76, 175, 80, 0.3);
border: 1px solid rgba(76, 175, 80, 0.5);
- border-radius: 3px;
+ border-radius: 2px;
cursor: grab;
- z-index: 1;
transition: background 0.15s;
}
@@ -1144,22 +1178,22 @@ input:-webkit-autofill:focus {
/* Horizontal edges: bar spans left-right */
.edge-top .edge-span-bar,
.edge-bottom .edge-span-bar {
- top: 2px;
- bottom: 2px;
+ top: 0;
+ bottom: 0;
}
/* Vertical edges: bar spans top-bottom */
.edge-left .edge-span-bar,
.edge-right .edge-span-bar {
- left: 2px;
- right: 2px;
+ left: 0;
+ right: 0;
}
/* Resize handles — large transparent hit area with narrow visible strip */
.edge-span-handle {
position: absolute;
background: transparent;
- z-index: 2;
+ z-index: 3;
opacity: 0;
transition: opacity 0.15s;
}
@@ -1255,7 +1289,6 @@ input:-webkit-autofill:focus {
.preview-corner:hover {
color: rgba(76, 175, 80, 0.6);
- transform: scale(1.2);
}
.preview-corner.active:hover {
@@ -1321,12 +1354,8 @@ input:-webkit-autofill:focus {
margin: 20px;
}
- .modal-footer {
- flex-direction: column-reverse;
- }
-
.modal-footer .btn {
- width: 100%;
+ min-width: 80px;
}
}