Merge branch 'errors-fold-drag'
Some checks failed
Publish Docker Image / build-and-push (push) Failing after 4s
Some checks failed
Publish Docker Image / build-and-push (push) Failing after 4s
Smart fold-around-errors with drag-to-reveal handle. Default ±25 context per error, draggable bars between gaps, click-to-reveal-25 fallback.
This commit is contained in:
@@ -1088,6 +1088,71 @@ main {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Foldable variant — generated by the smart "errors + 25 context" toggle in
|
||||||
|
* log.js. The bar is draggable: vertical pointer drag reveals or re-hides
|
||||||
|
* lines from the top of the hidden range. Visual cues:
|
||||||
|
* - ns-resize cursor announces the drag direction.
|
||||||
|
* - Faint horizontal hatch hints at compressed content underneath.
|
||||||
|
* - Hover and drag states intensify the hatch toward --accent so the
|
||||||
|
* bar reads as an interactive separator, not decorative chrome.
|
||||||
|
*/
|
||||||
|
.collapsed-lines-foldable {
|
||||||
|
cursor: ns-resize;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-lines-foldable .collapsed-lines-count {
|
||||||
|
background:
|
||||||
|
repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0,
|
||||||
|
transparent 3px,
|
||||||
|
color-mix(in srgb, var(--border) 60%, transparent) 3px,
|
||||||
|
color-mix(in srgb, var(--border) 60%, transparent) 4px
|
||||||
|
),
|
||||||
|
var(--surface);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
transition:
|
||||||
|
background-color 0.12s ease,
|
||||||
|
color 0.12s ease,
|
||||||
|
border-color 0.12s ease,
|
||||||
|
box-shadow 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-lines-foldable:hover .collapsed-lines-count,
|
||||||
|
.collapsed-lines-dragging .collapsed-lines-count {
|
||||||
|
background:
|
||||||
|
repeating-linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0,
|
||||||
|
transparent 3px,
|
||||||
|
color-mix(in srgb, var(--accent) 35%, transparent) 3px,
|
||||||
|
color-mix(in srgb, var(--accent) 35%, transparent) 4px
|
||||||
|
),
|
||||||
|
var(--accent-bg);
|
||||||
|
border-top-color: color-mix(in srgb, var(--accent) 60%, transparent);
|
||||||
|
border-bottom-color: color-mix(in srgb, var(--accent) 60%, transparent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-lines-foldable:hover .collapsed-lines-count i,
|
||||||
|
.collapsed-lines-dragging .collapsed-lines-count i {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-lines-dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed-lines-dragging .collapsed-lines-count {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 1px color-mix(in srgb, var(--accent) 70%, transparent),
|
||||||
|
0 6px 16px color-mix(in srgb, var(--accent) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.log-inner .level {
|
.log-inner .level {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -47,7 +47,20 @@ function scrollToHeight(top, smoothScrollLimit = 10000) {
|
|||||||
window.scrollTo({left: 0, top, behavior});
|
window.scrollTo({left: 0, top, behavior});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* error collapse toggle */
|
/*
|
||||||
|
* Error collapse toggle.
|
||||||
|
*
|
||||||
|
* Smart fold: every entry within ±ERROR_WINDOW_SIZE of an error stays
|
||||||
|
* visible; runs of non-error entries with no nearby errors collapse into
|
||||||
|
* a draggable fold bar. Vertical drag on the bar progressively reveals or
|
||||||
|
* re-hides lines; a click without drag reveals the next DEFAULT_CLICK_REVEAL
|
||||||
|
* lines.
|
||||||
|
*/
|
||||||
|
const ERROR_WINDOW_SIZE = 25;
|
||||||
|
const PIXELS_PER_LINE_DRAG = 6;
|
||||||
|
const DEFAULT_CLICK_REVEAL = 25;
|
||||||
|
const DRAG_PIXEL_THRESHOLD = 3;
|
||||||
|
|
||||||
const toggleErrorsButton = document.getElementById("error-toggle");
|
const toggleErrorsButton = document.getElementById("error-toggle");
|
||||||
if (toggleErrorsButton) {
|
if (toggleErrorsButton) {
|
||||||
toggleErrorsButton.addEventListener("click", toggleErrors);
|
toggleErrorsButton.addEventListener("click", toggleErrors);
|
||||||
@@ -64,76 +77,134 @@ function toggleErrors() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collapseAllErrors() {
|
function collapseAllErrors() {
|
||||||
let firstNoErrorLine = false;
|
const lines = Array.from(document.querySelectorAll('.log-inner > .entry'));
|
||||||
let lines = document.querySelectorAll('.log-inner > .entry');
|
const total = lines.length;
|
||||||
let totalLines = lines.length;
|
if (!total) return;
|
||||||
for (const [i, line] of lines.entries()) {
|
|
||||||
let lineNumber = line.querySelector(".line-number").innerHTML;
|
|
||||||
if (line.classList.contains("entry-no-error")) {
|
|
||||||
line.style.display = "none";
|
|
||||||
|
|
||||||
if (firstNoErrorLine === false) {
|
// Pass 1: mark entries within ±ERROR_WINDOW_SIZE of any error.
|
||||||
firstNoErrorLine = lineNumber;
|
const mustShow = new Array(total).fill(false);
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
if (lines[i].classList.contains("entry-error")) {
|
||||||
|
const lo = Math.max(0, i - ERROR_WINDOW_SIZE);
|
||||||
|
const hi = Math.min(total - 1, i + ERROR_WINDOW_SIZE);
|
||||||
|
for (let j = lo; j <= hi; j++) mustShow[j] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i + 1 === totalLines && firstNoErrorLine) {
|
// Pass 2: hide unmarked entries; emit a fold bar at the START of each run.
|
||||||
line.insertAdjacentElement("afterend", generateCollapsedLines(firstNoErrorLine, lineNumber));
|
let runEntries = [];
|
||||||
}
|
const flushRun = () => {
|
||||||
|
if (!runEntries.length) return;
|
||||||
|
const bar = createFoldBar(runEntries);
|
||||||
|
runEntries[0].insertAdjacentElement("beforebegin", bar);
|
||||||
|
runEntries = [];
|
||||||
|
};
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
if (!mustShow[i]) {
|
||||||
|
lines[i].style.display = "none";
|
||||||
|
runEntries.push(lines[i]);
|
||||||
} else {
|
} else {
|
||||||
if (firstNoErrorLine) {
|
flushRun();
|
||||||
line.insertAdjacentElement("beforebegin", generateCollapsedLines(firstNoErrorLine, lineNumber - 1));
|
|
||||||
firstNoErrorLine = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
flushRun();
|
||||||
}
|
}
|
||||||
|
|
||||||
function uncollapseAllErrors() {
|
function uncollapseAllErrors() {
|
||||||
document.querySelectorAll('.entry-no-error').forEach(line => line.style.removeProperty("display"));
|
document.querySelectorAll(".log-inner > .entry").forEach(line => line.style.removeProperty("display"));
|
||||||
document.querySelectorAll('.collapsed-lines').forEach(collapsed => collapsed.remove());
|
document.querySelectorAll(".collapsed-lines").forEach(bar => bar.remove());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCollapsedClick(e) {
|
function createFoldBar(entries) {
|
||||||
let collapsed = e.currentTarget;
|
const bar = document.createElement("div");
|
||||||
let positionElement = document.getElementById(`L${parseInt(collapsed.dataset.end) + 1}`);
|
bar.classList.add("collapsed-lines", "collapsed-lines-foldable");
|
||||||
let position;
|
bar.appendChild(document.createElement("div")); // line-number column slot
|
||||||
if (positionElement) {
|
|
||||||
position = positionElement.getBoundingClientRect().top - window.scrollY;
|
const count = document.createElement("div");
|
||||||
|
count.classList.add("collapsed-lines-count");
|
||||||
|
bar.appendChild(count);
|
||||||
|
|
||||||
|
bar._hiddenEntries = entries;
|
||||||
|
bar._revealedCount = 0;
|
||||||
|
renderFoldLabel(bar);
|
||||||
|
attachFoldDragHandler(bar);
|
||||||
|
return bar;
|
||||||
}
|
}
|
||||||
for (let i = parseInt(collapsed.dataset.start); i <= parseInt(collapsed.dataset.end); i++) {
|
|
||||||
document.getElementById(`L${i}`).parentElement.parentElement.style.removeProperty("display");
|
function renderFoldLabel(bar) {
|
||||||
|
const entries = bar._hiddenEntries;
|
||||||
|
const total = entries.length;
|
||||||
|
const revealed = bar._revealedCount;
|
||||||
|
const remaining = total - revealed;
|
||||||
|
const count = bar.querySelector(".collapsed-lines-count");
|
||||||
|
count.replaceChildren();
|
||||||
|
|
||||||
|
if (remaining <= 0) return;
|
||||||
|
|
||||||
|
const firstHiddenLine = parseInt(entries[revealed].querySelector(".line-number").innerHTML, 10);
|
||||||
|
const lastHiddenLine = parseInt(entries[total - 1].querySelector(".line-number").innerHTML, 10);
|
||||||
|
const word = remaining === 1 ? "line" : "lines";
|
||||||
|
|
||||||
|
const grip = document.createElement("i");
|
||||||
|
grip.className = "fa-solid fa-grip-lines";
|
||||||
|
count.appendChild(grip);
|
||||||
|
count.append(` ${remaining} ${word} hidden · ${firstHiddenLine}–${lastHiddenLine} · drag to reveal `);
|
||||||
|
count.appendChild(grip.cloneNode());
|
||||||
}
|
}
|
||||||
if (positionElement) {
|
|
||||||
window.scrollTo({
|
function applyFoldReveal(bar, n) {
|
||||||
left: 0,
|
const entries = bar._hiddenEntries;
|
||||||
top: positionElement.getBoundingClientRect().top - position - collapsed.offsetHeight,
|
n = Math.max(0, Math.min(entries.length, n));
|
||||||
behavior: "instant"
|
if (n === bar._revealedCount) return;
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
if (i < n) entries[i].style.removeProperty("display");
|
||||||
|
else entries[i].style.display = "none";
|
||||||
|
}
|
||||||
|
bar._revealedCount = n;
|
||||||
|
renderFoldLabel(bar);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachFoldDragHandler(bar) {
|
||||||
|
let dragging = false;
|
||||||
|
let dragStartY = 0;
|
||||||
|
let dragStartReveal = 0;
|
||||||
|
let didDrag = false;
|
||||||
|
|
||||||
|
bar.addEventListener("pointerdown", (e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
dragging = true;
|
||||||
|
didDrag = false;
|
||||||
|
dragStartY = e.clientY;
|
||||||
|
dragStartReveal = bar._revealedCount;
|
||||||
|
bar.setPointerCapture(e.pointerId);
|
||||||
|
bar.classList.add("collapsed-lines-dragging");
|
||||||
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bar.addEventListener("pointermove", (e) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
const dy = e.clientY - dragStartY;
|
||||||
|
if (Math.abs(dy) >= DRAG_PIXEL_THRESHOLD) didDrag = true;
|
||||||
|
const target = dragStartReveal + Math.round(dy / PIXELS_PER_LINE_DRAG);
|
||||||
|
applyFoldReveal(bar, target);
|
||||||
|
});
|
||||||
|
|
||||||
|
const finish = (e) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
dragging = false;
|
||||||
|
try { bar.releasePointerCapture(e.pointerId); } catch {}
|
||||||
|
bar.classList.remove("collapsed-lines-dragging");
|
||||||
|
|
||||||
|
if (!didDrag) {
|
||||||
|
// Plain click: reveal the next chunk of lines.
|
||||||
|
applyFoldReveal(bar, bar._revealedCount + DEFAULT_CLICK_REVEAL);
|
||||||
}
|
}
|
||||||
collapsed.remove();
|
if (bar._revealedCount >= bar._hiddenEntries.length) {
|
||||||
|
bar.remove();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
function generateCollapsedLines(start, end) {
|
bar.addEventListener("pointerup", finish);
|
||||||
let count = end - start + 1;
|
bar.addEventListener("pointercancel", finish);
|
||||||
let string = count === 1 ? "line" : "lines";
|
|
||||||
|
|
||||||
let collapsedRow = document.createElement("div");
|
|
||||||
collapsedRow.classList.add("collapsed-lines");
|
|
||||||
collapsedRow.dataset.start = start;
|
|
||||||
collapsedRow.dataset.end = end;
|
|
||||||
collapsedRow.appendChild(document.createElement("div"));
|
|
||||||
collapsedRow.addEventListener("click", handleCollapsedClick);
|
|
||||||
|
|
||||||
let collapsedLinesCount = document.createElement("div");
|
|
||||||
collapsedLinesCount.classList.add("collapsed-lines-count");
|
|
||||||
let icon = document.createElement("i");
|
|
||||||
icon.classList.add("fa-solid", "fa-angle-up");
|
|
||||||
collapsedLinesCount.appendChild(icon);
|
|
||||||
collapsedLinesCount.append(` ${count} ${string} `);
|
|
||||||
collapsedLinesCount.append(icon.cloneNode());
|
|
||||||
collapsedRow.appendChild(collapsedLinesCount);
|
|
||||||
|
|
||||||
return collapsedRow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* convert timestamps */
|
/* convert timestamps */
|
||||||
|
|||||||
Reference in New Issue
Block a user