Files
technitium-console/src/screens/logs/Logs.module.css
T
DenozordecandClaude Sonnet 5.5 f4513533c4 App shell as the canvas draws it, full lazy Cache tree, styles aligned with the parent projects
Shell: collapsible rail, DNS/Server groups with pins, always-on header with
breadcrumbs and server status, Ctrl/Cmd+K command palette.
Cache: left column is a lazily expanded tree of every zone, right column the
selected node's records; both columns are ReUI Frames.
Styles: dark variant follows data-theme (field and outline fills), 32 px selects,
36 px segmented strip, 40 px caption table heads, 56 px rows, 20/24/32 page padding.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-27 09:35:41 +07:00

324 lines
15 KiB
CSS

/* Logs. Two very different sub-screens: a two-column file viewer and a table with
fifteen filters. The table classes are the same as in Cache, Allowed and
Blocked. */
.wrap { composes: screen from '../../ui/SectionHeader.module.css'; }
.acts { display: flex; gap: var(--s-4); flex-wrap: wrap; justify-content: flex-end; align-items: center; }
/* ---- View Logs ---- */
/*
Master and detail, both at once, and at 390 too.
At 1440 it is `250px` for the list and the rest for the viewer. On narrowing it
**does not turn into navigation**: they stack and both stay present. And it is a
decision, not a convenience: a paginated collection would be worth turning into
list→detail, but here there are **three files**, so sending the user to another
screen to see one and back to see another costs more than it saves.
Each panel answers for itself: the list can fail without the viewer hearing about
it, and the other way round. That is why there is no "screen" state, there are
two.
The breakpoint is 720, the same one `SectionHeader`, `Segmented` and `PanelForm`
already use: below that, 250 px of list leave the viewer with less than half the
screen and a log line stops fitting anywhere.
*/
.split { display: grid; grid-template-columns: 250px minmax(0, 1fr); gap: var(--s-10); align-items: start; }
@media (max-width: 720px) {
.split { grid-template-columns: minmax(0, 1fr); }
}
/*
The window's height, handed down to the reader (see ViewLogs.tsx). The Shell's body
is a flex column for a screen marked `data-fills-window`; from there each step takes
the rest: the screen, the split under the header, the reader panel (stretched, while
the file list keeps its own height), its body, and `Raw`'s `fill` box at the end.
*/
.viewLogs { composes: wrap; flex: 1 1 auto; }
.viewLogs .split { flex: 1 1 auto; grid-template-rows: minmax(0, 1fr); }
.reader { align-self: stretch; display: flex; flex-direction: column; }
.readerBody { flex: 1 1 auto; display: flex; flex-direction: column; }
/* Stacked under the file list: 70 % of the window, see ViewLogs.tsx. */
@media (max-width: 720px) {
.viewLogs .split { grid-template-rows: auto; }
.readerBody { flex: none; height: 70dvh; }
}
/* The time of an entry, one step below its message: see LogText.tsx. */
.logStamp { color: var(--faint); }
/* A hundred lines of the file, laid out only near the screen: see LogText.tsx. */
.logChunk { display: block; content-visibility: auto; contain-intrinsic-block-size: auto 100lh; }
/* A run of zero bytes, laid out zero wide and on one line: see LogText.tsx. `clip`
and not `hidden`: Chromium leaves what `hidden` cuts out of a selection, and the
run is the file's own text, copied with the rest. */
.logNul { display: inline-block; inline-size: 0; overflow: clip; white-space: pre; vertical-align: top; }
.pbList { padding: var(--s-5); }
.logfiles { display: flex; flex-direction: column; gap: var(--s-1); }
.logfile { composes: row from '../../ui/list.module.css'; justify-content: space-between; }
.logfile .name { font-family: var(--font-mono); }
.sz { composes: annotationQuiet from '../../ui/text.module.css'; }
/* ---- Query Logs ---- */
.fs { composes: panel from '../../ui/Panel.module.css'; margin-bottom: var(--s-9); }
/* The box is a panel, so its title is a panel's (`ui/Panel`, `.title`): one step
above the field labels under it, not another row of small capitals. */
.fsTitle {
composes: title from '../../ui/Panel.module.css';
display: block; width: 100%; padding: var(--s-8) var(--s-10);
border-bottom: 1px solid var(--line);
}
/*
The filters are laid out as GROUPS SIDE BY SIDE, not as one row per group.
They used to be five full-width rows (Source, Mode, Query, Response, Paging), each
with its name in a 110 px column on the left and its fields in a single line on
the right. Every row used less than half of its width, and the five stacked made
the panel so tall that at 1440x980 the results started at y=842, with one row on
screen: the table, which is what the screen is for, was below the fold.
Now each group carries its name ABOVE its fields and the groups flow side by side,
wrapping as the width runs out: two bands at 1440 instead of five rows (Source,
Mode and Paging; then Query and Response), so the table starts at y=629. Nothing
is collapsed or hidden at this width: the same fifteen controls, the same labels and
options. Below 960 the whole panel folds instead (see `.fsToggle` below).
Two things moved: `Live Update` went to the screen's header (QueryLogs.tsx says
why), and Paging now follows Mode in the document, which is also the tab order.
The grouping is the one decided below (the MODE group keeps its four fields and
its edge together).
*/
.fsIn {
display: flex; flex-wrap: wrap; align-items: flex-start;
gap: var(--s-9) var(--s-10);
padding: var(--s-8) var(--s-10) var(--s-10);
}
.group { display: flex; flex-direction: column; gap: var(--s-4); min-width: 0; max-width: 100%; }
/* Every head is as tall as the `While Live Update` pill that appears in Mode's,
so the fields of the groups in one band line up and the pill appearing does
not move anything. */
.groupLabel {
display: flex; align-items: center; gap: var(--s-6) var(--s-8); flex-wrap: wrap;
min-height: var(--s-11);
font-size: var(--t-1); font-weight: 600; letter-spacing: 0.09em;
color: var(--mute);
}
.groupCtl { display: flex; align-items: flex-end; gap: var(--s-7); flex-wrap: wrap; }
.field { display: flex; flex-direction: column; gap: var(--s-3); width: var(--ql-field, auto); }
/* On a phone each filter takes the whole line instead of its desktop width: the
same column for all fifteen, and no field left half as wide as its neighbour. */
@media (max-width: 560px) {
.group, .groupCtl, .field { width: 100%; }
}
.field label { font-size: var(--t-2); color: var(--mute); font-weight: 550; }
.field input, .field select { width: 100%; }
/*
Folded, below 960 (FOLD_QUERY in QueryLogs.tsx says what and why).
The title becomes the toggle and the whole title row is its target, so the button
carries the padding the heading had. Closed, the row loses its rule: under it there
is either the list of what is set, which brings its own, or the panel's edge.
*/
.fsIn[hidden] { display: none; }
.fsTitleFold { padding: 0; }
.fsTitleClosed { border-bottom: 0; }
.fsToggle {
display: flex; align-items: center; gap: var(--s-5);
width: 100%; padding: var(--s-8) var(--s-10);
border: 0; background: none; color: inherit; font: inherit; text-align: left;
cursor: pointer;
}
/* The panel clips what overflows it, the focus ring included: drawn inside. */
.fsToggle:focus-visible { outline-offset: calc(-1 * var(--s-1)); }
.chevron { composes: chevron from '../../ui/Details.module.css'; color: var(--mute); }
[aria-expanded='true'] > .chevron { transform: rotate(90deg); }
/* What is set, by the fields' own labels: label quiet, value as typed. */
.fsSet {
display: flex; flex-wrap: wrap; gap: var(--s-3) var(--s-9);
margin: 0; padding: var(--s-7) var(--s-10) var(--s-8);
border-top: 1px solid var(--line);
font-size: var(--t-3); color: var(--ink);
}
.fsSetItem { min-width: 0; overflow-wrap: break-word; }
.fsSetLabel { color: var(--mute); }
/* The count is `ui/count.module.css`'s: see there for why. */
.count { composes: bar above from '../../ui/count.module.css'; }
.mono { composes: mono from '../../ui/text.module.css'; }
.nowrap { white-space: nowrap; }
/*
The Query Logs table: one line per row.
Every cell keeps to one line. Domain, Answer and the client address are cut with
an ellipsis and carry the whole value in their text and `title` (`Clip` in
QueryLogs.tsx); the rest are short by nature. Before, those three broke anywhere,
the timestamp took two lines, and a row with a long answer reached 200 px: at
1440x980 one row fitted on the first screen.
Answer is the elastic column: `max-width: 0` takes it out of the table's minimum
width and `width: 100%` hands it whatever the other columns leave, so a long list
of addresses never pushes the row menu out of the panel. Its minimum keeps a
readable piece of it where the table no longer fits and scrolls inside its panel.
Domain is cut at a fixed width instead, wide enough for 89 of 100 names on a page
of the showcase traffic. Written as `table.` to outweigh ui/Table's
`.table thead th` and `.table td`, which weigh the same as one class.
*/
table.queryTable thead th { white-space: normal; padding-left: var(--s-3); padding-right: var(--s-3); }
table.queryTable td { white-space: nowrap; padding-left: var(--s-3); padding-right: var(--s-3); }
table.queryTable td:first-child { padding-left: var(--s-7); }
table.queryTable td.answer { width: 100%; max-width: 0; min-width: 16ch; }
.clipDomain, .clipAnswer, .clipClient {
display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.clipDomain { max-width: 30ch; }
.clipClient { max-width: 24ch; }
/* Narrower windows: Domain gives up width first, and the timestamp breaks
between date and time (`Stamp`), so the table still fits at 1366 and needs
less of a scroll inside its panel below that. */
@media (max-width: 1439px) {
.clipDomain { max-width: 22ch; }
table.queryTable td.answer { min-width: 12ch; }
}
@media (max-width: 1365px) {
table.queryTable td.stamp { white-space: normal; }
}
/* When the rows stack (`stack` in ui/Table: the table no longer fits its panel,
which at 1024 and 768 it does not), each cell is as wide as its value and the
widths above stand down: the timestamp on one line, Domain and Answer cut only
where the row ends, and each value on its label's line. */
table.queryTable[data-stacked] td:first-child { padding-left: 0; }
table.queryTable[data-stacked] td.stamp { white-space: nowrap; }
/* Answer: its label and its one cut line side by side, the line taking the rest
of the row (a flex item shrinks; an inline box beside a label would not). */
table.queryTable[data-stacked] td.answer { display: flex; align-items: baseline; width: auto; max-width: 100%; min-width: 0; }
table.queryTable[data-stacked] .clipAnswer { min-width: 0; }
table.queryTable[data-stacked] .clipDomain,
table.queryTable[data-stacked] .clipClient { display: inline-block; max-width: 100%; vertical-align: bottom; }
table.queryTable[data-stacked] .rtt { display: inline; }
/*
Upstream's row colour code (logs.js:452-508), with the SAME mapping, drawn as a pill.
`rowClass` still decides one category per row from the RCODE and, within it, the
response type, exactly as upstream does. What changed is where it is painted.
Upstream lays it over the whole row; here it went as a 30 % background over every
cell, and on a page of mostly Cached rows the table read as one block of colour,
so the exception (a Blocked row) hardly stood out.
Now the row is neutral and the category sets ONE custom property, `--ql-tone`,
which the pill around the Response Type reads: the same 30 % over the panel the
whole row used to carry, so every measurement of `dev/row-colours.mjs` still
holds as it is (ΔE00 between pairs as a background, ink at 6.5:1 over the
lightest one).
The colour is the ROW's, not the cell's: an `NxDomain` row whose type is
`Recursive` gets the NX Domain colour on a pill that reads `Recursive`, exactly
as the whole row did before. That is why the legend still says "row colours".
**A pill and not an edge**: the edge belongs to the amber, and it means "this is
yours to change". And the colour is still reinforcement, never the only channel:
`RCODE` and `Response Type` are written in their cells, and the legend names the
seven conditions. One pair (`Server Failure`/`Cached` at 8.5) is below the
collision threshold, see `tokens.css`.
The custom property is called `--ql-tone` so that it cannot shadow a token: no
name in `tokens.css` starts with `--ql-`.
*/
.rServerFailure { --ql-tone: var(--ch-fail); }
.rBlocked { --ql-tone: var(--ch-block); }
.rNxDomain { --ql-tone: var(--ch-nx); }
.rRefused { --ql-tone: var(--ch-refuse); }
.rAuthoritative { --ql-tone: var(--ch-auth); }
.rRecursive { --ql-tone: var(--ch-rec); }
.rCached { --ql-tone: var(--ch-cache); }
/* A row with no category gets a pill with no fill, so the response type keeps
the same place on every row. */
.tone {
display: inline-block;
padding: var(--s-1) var(--s-5);
border-radius: var(--r-1);
background: color-mix(in srgb, var(--ql-tone, transparent) 30%, transparent);
white-space: nowrap;
}
.rtt { font-size: var(--t-2); color: var(--faint-on-tint); padding-left: var(--s-5); }
/*
The legend of the seven, and it is ALWAYS complete.
The design rule is explicit: every label that comes from the server carries its
legend entry. Here there were seven colours and none: the user saw coloured rows
and nothing explaining them.
Complete, and not "the ones on this page": a legend that changes with the data
shows six colours on one page and four on the next, and stops being a vocabulary
to become a summary of what is in front of you.
*/
.legend {
display: flex; flex-wrap: wrap; align-items: center;
gap: var(--s-5) var(--s-9);
padding: var(--s-7) var(--s-9);
margin-bottom: var(--s-7);
border: 1px solid var(--line); border-radius: var(--r-3); background: var(--pan);
}
.legendTitle {
font-size: var(--t-1); font-weight: 600; letter-spacing: 0.09em;
color: var(--mute);
}
.legendItem { display: inline-flex; align-items: center; gap: var(--s-5); font-size: var(--t-2); }
/* Each entry is the pill itself, the same one the Response Type cell carries,
followed by the condition that paints it. */
.legendItem .tone { font-weight: 550; color: var(--ink); }
.legendWhen { color: var(--faint); }
/* Folded (below 960): the seven pills stay, their conditions wait behind the
title, which is the toggle. */
.legendFolded .legendWhen { display: none; }
.legendToggle {
composes: hitArea from '../../ui/interaction.module.css';
display: inline-flex; align-items: center; gap: var(--s-4);
padding: 0; border: 0; background: none; font-family: inherit; cursor: pointer;
}
/* Centred on purpose: it is a bar filter, not a settings row. But the minimum
height is the same as `ui/Check.module.css`'s. */
/* `.check` went in the redesign: it was a hand copy of `ui/Check` with eight px of gap
instead of nine, and `uniformity.js` counted it as a separate signature of the
same checkbox. Query Logs uses the primitive now. */
/*
The group a MODE switches off: the edge, once for all four.
It is the same master-switch signal as `Settings` with the polarity inverted:
there the control is off because its master is OFF and the pill says `Needs …`;
here it is off because `Live Update` is ON and it says `While …`. The mechanics do
not change because what matters is the same: **the user can undo it themselves**,
and that is what the amber means.
It composes from `Form.module.css` so that it is THE SAME edge and not another one
like it: an inner shadow and not a border, because a border eats two pixels of the
box and shifts the content, as measured in `Settings`, where it gave away a third
grid signature that existed only on that screen.
It goes on the GROUP and not on each field because all four switch off together:
four pills saying the same thing are noise, and the `Settings` rule was already
one pill per group.
*/
/* The edge needs room to its left, and it takes it from the gap BEFORE the group
(the negative margin cancels the padding): a group that grew when the mode
switched on would push its neighbours to another band. */
/* The switch in the header: centred on the buttons beside it. */
.live { display: inline-flex; align-items: center; padding-left: var(--s-5); }
.byMode {
composes: dependent from '../../ui/Form.module.css';
padding-left: var(--s-8);
margin-left: calc(-1 * var(--s-8));
}