Log list: resizable columns with a sticky header

The list gets a thin sticky header (received, message time, severity,
host, app, message). Dragging the edge of a header resizes the column,
a double-click returns it to the automatic width; widths are clamped per
column and remembered per browser (logstream.cols.*), and Settings >
Interface has a "Reset column widths" button.

The columns are defined once on a #table wrapper from --col-* variables;
the header and every row use subgrid, so all rows line up (host and app
widths no longer vary from row to row). The wrapper clips with
overflow: clip so that the header can stick under the top bar. Phones
keep the two-line layout without header.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
cedricandClaude Opus 5.5 committed 2026-10-01 22:07:05 +02:00
1 parent fd8d133b9c
commit a12fac16c8
4 files changed
+133 -5

No files matched your search

+42 -4
View File
@@ -296,6 +296,39 @@ body.busy .progress::after {
.list { margin: 6px 20px 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
.list:empty { display: none; }
/* Columns: received, message time, severity, host, app, message. Widths come from
--col-* (set on #table when a column has been resized, see app.js), shared by
the header and every row through subgrid so that the columns line up. */
.table {
display: grid;
grid-template-columns: var(--col-rcv, max-content) var(--col-mt, max-content) var(--col-sev, 4.6rem)
var(--col-host, minmax(5rem, 9rem)) var(--col-app, minmax(4rem, 8rem)) minmax(0, 1fr);
column-gap: 12px;
margin: 6px 20px 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
overflow: clip; /* rounded corners without breaking the sticky header (hidden would) */
}
.table[hidden] { display: none; }
.table .list {
display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
margin: 0; border: 0; border-radius: 0; background: none; overflow: visible;
}
.table .row { grid-template-columns: subgrid; grid-column: 1 / -1; }
.list-head {
display: grid; grid-template-columns: subgrid; grid-column: 1 / -1;
position: sticky; top: var(--topbar-h, 0px); z-index: 5;
padding: 0 14px 0 11px; border-left: 3px solid transparent;
background: var(--panel-2); border-bottom: 1px solid var(--border);
font-size: 10.5px; font-weight: 650; letter-spacing: .04em; text-transform: uppercase; color: var(--muted);
}
.list-head > span { position: relative; min-width: 0; padding: 4px 0; }
/* Labels never widen a column (contain: inline-size), they are cut instead */
.list-head .lbl { display: block; contain: inline-size; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: inherit; }
.list-head .grip { position: absolute; top: 0; bottom: 0; right: -9px; width: 6px; z-index: 1; cursor: col-resize; touch-action: none; }
.list-head .grip::after { content: ""; position: absolute; left: 2px; top: 25%; bottom: 25%; border-left: 1px solid var(--border); }
.list-head .grip:hover::after, .list-head .grip.active::after { top: 0; bottom: 0; border-left: 2px solid var(--accent); left: 2px; }
body.col-resizing, body.col-resizing * { cursor: col-resize !important; user-select: none; -webkit-user-select: none; }
.cols-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; margin-top: 12px; }
.row {
display: grid;
grid-template-columns: max-content max-content 4.6rem minmax(5rem, 9rem) minmax(4rem, 8rem) 1fr;
@@ -313,7 +346,8 @@ body.busy .progress::after {
.row.new { animation: flash 1.2s ease-out; }
@keyframes flash { from { background: var(--accent-soft); } to { background: transparent; } }
.row time { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.row > * { min-width: 0; }
.row time { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; overflow: hidden; text-overflow: ellipsis; }
.row time.rcv { color: var(--text); }
.row time .ms { opacity: .6; }
.row .host, .row .app { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -322,7 +356,7 @@ body.busy .progress::after {
.row .msg { white-space: pre-wrap; word-break: break-word; min-width: 0; }
.sev {
justify-self: start;
justify-self: start; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em;
padding: 1px 6px; border-radius: 5px;
color: var(--sev); background: color-mix(in srgb, var(--sev) 14%, transparent);
@@ -593,8 +627,12 @@ input.switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2
#count { flex: 1 1 50%; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.histo { padding: 0 16px 6px; }
.h-leg { display: none; }
.list, .error-banner { margin-left: 16px; margin-right: 16px; }
.row {
.list, .error-banner, .table { margin-left: 16px; margin-right: 16px; }
/* Phones: no columns, two lines per log (layout below); the widths do not apply */
.table { display: block; }
.table .list { display: block; margin: 0; }
.list-head { display: none; }
.row, .table .row {
grid-template-columns: auto auto 1fr;
grid-template-areas: "rcv sev host" "msg msg msg";
gap: 3px 8px; padding: 8px 12px 8px 10px;