mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
759 lines
48 KiB
CSS
759 lines
48 KiB
CSS
@layer theme {
|
||
/* ---------- panels / cards (cbi) ---------- */
|
||
.cbi-section, .cbi-map > .cbi-section-node, fieldset.cbi-section, .panel {
|
||
background: var(--fs-panel);
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius-lg);
|
||
box-shadow: var(--fs-shadow);
|
||
}
|
||
.cbi-section { padding: var(--fs-card-pad); margin-bottom: var(--fs-card-gap); }
|
||
/* a bare <legend>; the ones inside a .cbi-section are covered below. The ink is the heading
|
||
* ramp's (45-misc.css) — legend is kept out of that group so this rule stays its one owner;
|
||
* no line-height here: base's `fieldset legend` sets 1 and every real legend sits in a
|
||
* fieldset, so stating leading here would silently repaint them all through the layer. */
|
||
legend { font-size: var(--fs-type-lg); font-weight: var(--fs-weight-bold); color: var(--fs-text); }
|
||
|
||
/* overview (luci-mod-status) DOM:
|
||
* .cbi-section > .cbi-title>h3 + div > table.table (.tr/.td) */
|
||
|
||
/* section title + collapse control */
|
||
.cbi-section > .cbi-title { margin-bottom: var(--fs-space-3); }
|
||
.cbi-section > .cbi-title h3,
|
||
.cbi-section > h3:first-child,
|
||
.cbi-section > legend {
|
||
font-size: var(--fs-type-lg); font-weight: var(--fs-weight-bold); color: var(--fs-text);
|
||
margin: 0; padding: 0; border: 0;
|
||
}
|
||
/* A bare section title (no .cbi-title wrapper) sat flush against the content below it —
|
||
* on System → System the "System Properties" h3 touched the tab strip. Give it the same
|
||
* 12px the .cbi-title wrapper already carries above. Excludes .cbi-title h3: that variant
|
||
* gets its gap from the wrapper's own margin-bottom, so flagging the inner h3 too would
|
||
* double it. */
|
||
.cbi-section > h3:first-child,
|
||
.cbi-section > legend { margin-bottom: var(--fs-space-3); }
|
||
|
||
/* A second heading inside the same card — the one that follows a table rather than opening the
|
||
* section — has no air at all: on Status -> Overview the stock includes stack `table + h3 +
|
||
* table` inside one .cbi-section, and the heading reads as another row of the table above it.
|
||
* The rule above cannot cover it, `:first-child` being what excludes the one that opens the
|
||
* card. The two values are the ladder's own: a heading is separated from what came before by
|
||
* more than from what it introduces. */
|
||
.cbi-section .table + h3 { margin-top: var(--fs-space-5); }
|
||
.cbi-section h3 + .table { margin-top: var(--fs-space-3); }
|
||
|
||
/* ---- tables in cards ----
|
||
* Discriminator: a key/value include (System, Memory) has NO id; a data table has one
|
||
* (status_leases) or the JS tag .fs-dt. Key/value is not itself a card — no frame, no clip;
|
||
* a data table keeps the `.table` frame below. Nested (`.table .table`, `.cbi-value .table`)
|
||
* and the config-diff dialog (`.cbi-modal .table`) go with key/value. */
|
||
.cbi-section .table:not([id]):not(.fs-dt),
|
||
.cbi-section-node .table:not([id]):not(.fs-dt),
|
||
.cbi-modal .table, .table .table, .cbi-value .table {
|
||
border: 0; border-radius: 0; overflow: visible; background: none;
|
||
}
|
||
.cbi-section .table { margin: 0; width: 100%; }
|
||
/* A data table straight in #view (no section: package-manager, Startup) misses the width above,
|
||
* so it sized to preferred width and overflowed the content column — which base already fixes
|
||
* for EVERY `.table` (`width: 100%`, base/40-tables.css), and has for as long as both rules have
|
||
* existed. Switched off against the 24 tables that match it across eight pages: no computed
|
||
* style moved, so the rule only restated a value one layer down. Removed. */
|
||
/* Config-table fields fill their cell — moved from base with the generic field box in
|
||
* theme/60-inputs.css (in base these beat its 210px in-layer; left behind they would lose
|
||
* to it by layer), together with the numeric-cell refinement below them. */
|
||
.table.cbi-section-table input[type="password"],
|
||
.table.cbi-section-table input[type="text"],
|
||
.table.cbi-section-table textarea,
|
||
.table.cbi-section-table select,
|
||
.table.cbi-section-table .cbi-select {
|
||
width: 100%;
|
||
min-width: auto;
|
||
}
|
||
.table.cbi-section-table .td.cbi-section-table-cell select,
|
||
.table.cbi-section-table .td.cbi-section-table-cell .cbi-select { width: inherit; }
|
||
/* The filter row (form.js renderHeaderRows) is the one place a field cannot claim the width it
|
||
* wants: it lives in a table HEADER cell, so the column is as wide as the DATA under it. The
|
||
* rule above makes it fill that cell; what does not fit is the TEXT, LuCI writing the
|
||
* placeholder as `Filter ` plus the column title, so a long translated title is sliced
|
||
* mid-glyph (issue #12). The flag column is the worse half: form.js writes `width: 30px` inline
|
||
* there, of which the theme's padding ate 24, leaving less than one glyph.
|
||
*
|
||
* A denser box and denser type buy back ~20px per column; the ellipsis changes the FAILURE, a
|
||
* placeholder too long for its column now ending instead of being cut through a letter. Nothing
|
||
* is lost to it — the column title says the same word directly above. No !important, so the
|
||
* inline 30px still stands: it is upstream's sizing for a one-character field. */
|
||
.table.cbi-section-table .cbi-section-filter {
|
||
padding: var(--fs-space-1) var(--fs-space-1-5);
|
||
font-size: var(--fs-type-xs);
|
||
text-overflow: ellipsis;
|
||
}
|
||
/* width:100% cannot shrink a table below min-content — one long unbreakable value holds the
|
||
* floor. Breaking values reflows it into the column instead of scrolling, and that break is the
|
||
* DEFAULT for every cell (base/40-tables.css), so a copy here would say nothing the cascade does
|
||
* not.
|
||
*
|
||
* A rule on this property is back a few lines down, and it is the INVERSE rather than a copy: a
|
||
* data table gets `break-word` while it is still a table, so its columns keep a floor and an
|
||
* overflow can be seen. Read that block before touching either. DHCP leases need finer control
|
||
* (only DUID/IPv6 may break) and get it from `white-space: nowrap`: pages/50-leases.css. */
|
||
.cbi-section .table .td,
|
||
.cbi-section .table .th {
|
||
padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-2-5) 0;
|
||
border: 0; border-bottom: var(--fs-hairline);
|
||
line-height: 1.45; vertical-align: middle;
|
||
text-align: start;
|
||
}
|
||
/* Row actions hug the far edge. Base says so too but loses to the `text-align: start` above (an
|
||
* earlier layer), so restate it. `.cbi-section-table-cell` is the discriminator:
|
||
* form.TableSection's action cells carry it (firewall zones, port forwards); the hand-built
|
||
* ones of Wireless (`.td.middle`) and DHCP leases do not, and stay left. */
|
||
.cbi-section .table .td.cbi-section-table-cell.cbi-section-actions { text-align: end; }
|
||
.cbi-section .table .tr:last-child .td { border-bottom: 0; }
|
||
/* Case is the app's, not the theme's: `text-transform: uppercase` stood here and printed LuCI's
|
||
* own `IPv4 address` as `IPV4 ADDRESS`, which reads as a typo in a column heading rather than as
|
||
* a style (issue #39). Everything else that made these read as headings — the smaller size, the
|
||
* weight, the faint ink, the tracking — stays. Four rules carry the same decision: the title row
|
||
* below, the card label (mirrored into theme/65-dropdown.css) and the alert's table in
|
||
* theme/35-alerts.css. */
|
||
.cbi-section .table .th {
|
||
color: var(--fs-eyebrow-color); font-weight: var(--fs-eyebrow-weight); font-size: var(--fs-type-xs);
|
||
letter-spacing: var(--fs-eyebrow-tracking);
|
||
}
|
||
|
||
/* --- DATA tables ([id] or JS-tagged .fs-dt): leases, wifi, processes, routes … ---
|
||
* Symmetric padding so cells clear the table's own contour (below).
|
||
*
|
||
* `:is([id], .fs-dt)` is one discriminator at ONE specificity: written as two selector lists the
|
||
* two names for "data table" land at different weights — `.cbi-section .table[id] .td` (0,4,0)
|
||
* against `table.fs-dt .td` (0,2,1), the latter losing to the key/value default (0,3,0) — so a
|
||
* table known only by the JS tag keeps the flush-left padding, with no mono face, no row hover
|
||
* and no header rule. Status -> Routing emits exactly that shape. */
|
||
.cbi-section .table:is([id], .fs-dt) .td,
|
||
.cbi-section .table:is([id], .fs-dt) .th,
|
||
table.fs-dt .td, table.fs-dt .th { padding: var(--fs-space-2-5) var(--fs-space-3); }
|
||
.cbi-section .table:is([id], .fs-dt) .td { font-family: var(--fs-font-mono); font-size: var(--fs-type); color: var(--fs-text); }
|
||
|
||
/* ---- the floor, and it belongs to the one tier a measurement reads ----
|
||
*
|
||
* `base/40-tables.css` breaks every value `anywhere`, the one break value that also lowers the
|
||
* cell's min-content contribution — to a single character. That is what keeps a table nobody
|
||
* measures inside its card, and it is also a LIE told to the engine: with no floor, auto table
|
||
* layout is free to starve a column and the browser answers "it fits", truthfully and uselessly.
|
||
*
|
||
* So the floor goes back exactly where it is READ: a data table, while it is still a table.
|
||
* `break-word` breaks the same values in the same places — the two differ only in whether the
|
||
* break counts towards min-content (css-text-3 §5.4) — so nothing about the rendering changes;
|
||
* what changes is that the column can no longer be starved below it, and an overflow becomes
|
||
* visible to `fit.overflows()`.
|
||
*
|
||
* `:not(.fs-stacked)` is the whole guard, and it is the state the fitter measures in (it strips
|
||
* the class first, fs-fit rule 1). Once carded, every cell has the row to itself.
|
||
*
|
||
* base/40-tables.css is deliberately NOT touched: `break-word` there was tried and recorded as a
|
||
* failure (a table stayed 355px wide in a 346px column, and .fs-main clips), a tier with no card
|
||
* and no scroll having nowhere to put the overflow. */
|
||
.table.fs-dt:not(.fs-stacked) .td { overflow-wrap: break-word; }
|
||
/* …and the ONE column the fitter decided may be shredded rather than card the whole table. Written
|
||
* by fs-select.js on the cells of a single column, stripped before every measurement. (0,5,0) — it
|
||
* beats the floor above on specificity, not on source order. */
|
||
.table.fs-dt:not(.fs-stacked) .td.fs-td-break { overflow-wrap: anywhere; }
|
||
|
||
/* ---- and the one flag, because the view writes its shredding inline ----
|
||
*
|
||
* `luci-mod-status`'s processes.js emits `E('span', {'style': 'word-break: break-word'}, …)`.
|
||
* `word-break: break-word` is the deprecated alias defined as "`word-break: normal` plus
|
||
* `overflow-wrap: anywhere`" (css-text-3 §5.1), so it lowers the cell's min-content to a single
|
||
* character from an INLINE declaration no cascade layer can outrank: measured at 720px, the
|
||
* Command column sat at 126px against a widest token of 353px and forcing the column's
|
||
* `overflow-wrap` any of three ways changed nothing, the floor being erased inside the span.
|
||
* Neutralised, the same table asks for 963px in 688px of room, which is the truth.
|
||
*
|
||
* This re-times the view's intent rather than overruling it: the column may still shred, but
|
||
* when the fitter marks it (`.fs-td-break`, one rung before carding) rather than always. Buttons
|
||
* and inputs are excluded — base gives them `break-all` deliberately, and a control label is not
|
||
* a value being read.
|
||
*
|
||
* The `!important` is the sanctioned kind: it fights an inline `style=`, the one adversary a
|
||
* layer cannot reach.
|
||
*
|
||
* A CARD IS NOT EXEMPT, though `:not(.fs-stacked)` stood here. The inline alias damages the
|
||
* MEASUREMENT, and a carded table is not measured — but it is not the only adversary. LuCI's
|
||
* luci-mod-dashboard ships `.Dashboard … .devices-info .tr .td:first-child { word-break:
|
||
* break-all }` in view/dashboard/css/custom.css with no layer around it, and `break-all` splits a
|
||
* token that would have fitted: measured at 390px, `t3e-syp.home.iot` over six lines and the
|
||
* caption `HOSTNAME` cut as `HOSTNA`/`ME`. A card's own breaking is unchanged — `.td` below keeps
|
||
* `overflow-wrap: anywhere`, so a token too long for the line still breaks. */
|
||
.table.fs-dt .td,
|
||
.table.fs-dt .td :not(button):not(.btn):not(input):not(select):not(textarea) {
|
||
word-break: normal !important;
|
||
}
|
||
|
||
/* Whether a table cards, breaks one column or scrolls is MEASURED (fs-select.js), at any width. */
|
||
|
||
/* ---- the header row stays while the rows go past ----
|
||
*
|
||
* Processes renders 114 rows and Connections a few hundred: past the first screenful the column
|
||
* headings are gone and every value is an unlabelled string of digits. The card view solves that
|
||
* by printing the heading above each value; a table that FITS had no equivalent.
|
||
*
|
||
* On the CELLS, not the row: `position: sticky` needs a box with a background, and in the
|
||
* separated border model a row's background does not travel with a sticky cell — the heading
|
||
* would float over the rows with the values showing through. So each `.th` sticks and carries
|
||
* the header's own fill.
|
||
*
|
||
* What it sticks TO depends on which box scrolls. In the vertical sidebar `.fs-main` is its own
|
||
* scroller and nothing overlaps its top edge, so the offset is 0. In the bar layouts the
|
||
* DOCUMENT scrolls under a sticky bar whose height is not `--fs-bar-h` — it grows when the brand
|
||
* wraps, when the menu takes a row or when the cluster does, all decided by measurement — so
|
||
* fs-chrome.js publishes `--fs-bar-live` and this reads it, with the designed height as the
|
||
* fallback for the frame before the chrome has been fitted.
|
||
*
|
||
* And a dialog is neither, which is why every one of these selectors names its root. The roots
|
||
* ride in `:where()` so they cost nothing: spelled out they weigh (1,9,0) against the (1,7,0)
|
||
* ceiling tools/css-metrics.mjs enforces, while the cell rule that has to win is (0,6,0). */
|
||
:where(:root[data-layout="sidebar"]:not([data-narrow]) #view, #modal_overlay)
|
||
.table.fs-dt:not(.fs-stacked) .tr.table-titles .th {
|
||
position: sticky; top: 0; z-index: 1;
|
||
background: var(--fs-panel2);
|
||
}
|
||
:where(:root[data-layout="top"] #view, :root[data-narrow] #view)
|
||
.table.fs-dt:not(.fs-stacked) .tr.table-titles .th {
|
||
position: sticky; top: var(--fs-bar-live, var(--fs-bar-h)); z-index: 1;
|
||
background: var(--fs-panel2);
|
||
}
|
||
|
||
/* ---- a measurement must not throw the reader ----
|
||
*
|
||
* `fitTables()` decides a data table's tier by taking its marks OFF and asking the engine
|
||
* (fs-fit rule 1), and the reading forces layout, so for that pass the document really is laid
|
||
* out with a table several screens taller than the one on screen.
|
||
*
|
||
* Scroll anchoring turns that into a visible bug: the browser keeps the node under the reader
|
||
* still by adjusting the offset, and re-picks that node whenever the offset changes for any
|
||
* other reason — which, while the user scrolls, is every frame. The pick can land inside a table
|
||
* in the un-stacked state the fitter is about to undo, and the next frame the engine faithfully
|
||
* corrects the offset by the difference. Measured on the Overview at 390x844 with the poll
|
||
* driving real re-renders: 18 movements nobody asked for in 1500 frames, the worst 1877px,
|
||
* always a pair.
|
||
*
|
||
* So the tables the fitter re-stamps are declared ineligible as anchors — and only they, because
|
||
* every other node staying eligible is what keeps the good half of anchoring working. NOT on the
|
||
* scroll container, which is the answer that suggests itself and is worse: with
|
||
* `html, body, #maincontent { overflow-anchor: none }` the same run still moved 12 times, up to
|
||
* 788px, having thrown that compensation away as well. */
|
||
.table.fs-dt { overflow-anchor: none; }
|
||
|
||
.cbi-section .table:is([id], .fs-dt) .tr:not(.table-titles):not(.placeholder):hover .td { background: var(--fs-panel2); }
|
||
.cbi-section .table:is([id], .fs-dt) .tr.table-titles .th { border-bottom-color: var(--fs-border); }
|
||
/* Contour around the whole data table. A real <table> needs border-collapse:separate or the
|
||
* radius is silently dropped. Scoped to #view too: Startup renders a .table.fs-dt in a bare
|
||
* #view wrapper with no section, and drew frameless under a section-scoped selector. */
|
||
.cbi-section .table[id], #view .table[id],
|
||
.cbi-section .table.fs-dt, .cbi-section table.fs-dt, table.fs-dt {
|
||
border-collapse: separate; border-spacing: 0;
|
||
border: var(--fs-hairline); border-radius: var(--fs-radius-lg);
|
||
/* EXPLICIT, not an omission: the generic `.table` rule below still says `overflow: hidden`
|
||
* — which clipped the last column of a wide table — and not repeating it here does not
|
||
* stop it applying, which is what kept the bug alive. The corner cells (below) round the
|
||
* frame instead, and long values wrap so the table shrinks to fit. */
|
||
overflow: visible;
|
||
}
|
||
/* The frame is overflow:visible, so nothing clips the square corner cells to its radius and
|
||
* they poked past it. Round the four OUTER cells — the frame's corners, not a per-row pill.
|
||
* `#view .table[id]` is listed because the frame rule draws the border there too. */
|
||
.cbi-section .table[id] .tr.table-titles .th:first-child,
|
||
#view .table[id] .tr.table-titles .th:first-child,
|
||
table.fs-dt .tr.table-titles .th:first-child { border-top-left-radius: var(--fs-radius-lg); }
|
||
.cbi-section .table[id] .tr.table-titles .th:last-child,
|
||
#view .table[id] .tr.table-titles .th:last-child,
|
||
table.fs-dt .tr.table-titles .th:last-child { border-top-right-radius: var(--fs-radius-lg); }
|
||
.cbi-section .table[id] .tr:last-child .td:first-child,
|
||
#view .table[id] .tr:last-child .td:first-child,
|
||
table.fs-dt .tr:last-child .td:first-child { border-bottom-left-radius: var(--fs-radius-lg); }
|
||
.cbi-section .table[id] .tr:last-child .td:last-child,
|
||
#view .table[id] .tr:last-child .td:last-child,
|
||
table.fs-dt .tr:last-child .td:last-child { border-bottom-right-radius: var(--fs-radius-lg); }
|
||
/* Empty table: L.ui.Table adds a single-cell .tr.placeholder. Spanning it across the table is
|
||
* base's job (base/40-tables.css); what is left here is the FRAMED table's own flavour — the
|
||
* plate reads as prose, left-aligned like the rows it stands in for, and carries the frame's
|
||
* bottom corners so the rounding does not cut across it. */
|
||
.cbi-section .table[id] .tr.placeholder .td,
|
||
#view .table[id] .tr.placeholder .td,
|
||
table.fs-dt .tr.placeholder .td {
|
||
/* the framed flavour pads its cells `--fs-space-2-5` a side (above), and that shorthand is in
|
||
* THIS layer, so it overrides base's placeholder padding and made the plate 4px taller than
|
||
* the row reserved for it — measured on Wireless as 67px inside 63px, printing below the
|
||
* frame's bottom border. Put base's number back for this one cell. */
|
||
padding-block: var(--fs-space-2);
|
||
text-align: start;
|
||
color: var(--fs-dim); font-family: var(--fs-font-sans);
|
||
border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg);
|
||
}
|
||
|
||
/* --- KEY/VALUE tables: System, Memory ---
|
||
* "no id" is not enough: form.TableSection/GridSection also render `.table.cbi-section-table`
|
||
* with no id, where a 40%/nowrap first column starves the data columns. So exclude it and .fs-dt
|
||
* explicitly.
|
||
*
|
||
* Also exclude the realtime graph legend: `[style*="table-layout"]` is the one no-id `.table`
|
||
* LuCI hands an inline `table-layout:fixed`, where a 40% first column would only starve the
|
||
* other five. A DDNS-style multi-column status table is not excluded and keeps the key/value
|
||
* first column. */
|
||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:first-child {
|
||
color: var(--fs-dim); font-weight: var(--fs-weight); width: 40%; white-space: nowrap;
|
||
}
|
||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:not(:first-child) {
|
||
font-family: var(--fs-font-mono); color: var(--fs-text); text-align: end;
|
||
}
|
||
|
||
/* --- the realtime graph legend (status/realtime/*) ---
|
||
* The one no-id `.table` LuCI renders with inline `table-layout:fixed;width:100%` and six cells
|
||
* per row. `fixed` splits the row into six equal columns, so on a phone each is ~1/6 of the
|
||
* content width and the labels overflow onto the value beside them. Stock luci-theme-bootstrap
|
||
* folds the six cells into label/value PAIRS on a narrow screen; do the same. `display:flex` on
|
||
* the row takes the cells out of the fixed table layout, so the inline `table-layout` no longer
|
||
* bites. Keyed on that inline attribute so only this legend is touched. */
|
||
@media (max-width: 767px) {
|
||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt)[style*="table-layout"] .tr {
|
||
display: flex; flex-wrap: wrap;
|
||
}
|
||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt)[style*="table-layout"] .td {
|
||
flex: 1 1 50%; min-width: 0; width: auto;
|
||
}
|
||
}
|
||
|
||
/* remove double card when a section nests a table-only include */
|
||
.cbi-section .cbi-section { border: 0; box-shadow: none; padding: 0; margin: 0; background: transparent; }
|
||
|
||
/* generic links/labels inside cards */
|
||
.cbi-section .ifacebox, .cbi-section .zonebadge { border-radius: var(--fs-radius); }
|
||
/* moved from base with the generic field box (same layer arithmetic as the cell rules above) */
|
||
.zonebadge input { width: 6em; }
|
||
/* The device/MAC badge inside the Network status box kept base's flat 4px, reading as square
|
||
* against the card's --fs-radius corners (issue #5.4). Scoped to the status box: the small
|
||
* firewall zone .ifacebadge chips want to stay tight. Follows the Rounding axis via the token. */
|
||
.network-status-table .ifacebox-body .ifacebadge { border-radius: var(--fs-radius); }
|
||
/* …and the head has to round WITH the box: it sits inside the box's 1px border and carries the
|
||
* zone colour as an inline background, so its own corners are what you see, and a 3px head
|
||
* inside a 10px box cuts straight through the rounding with the zone colour spilling past the
|
||
* corner (issue #7). Minus the 1px border it is inset by; max() because the Rounding axis goes
|
||
* down to 0, where a bare subtraction is negative — an invalid declaration, silently dropped. */
|
||
.cbi-section .ifacebox > .ifacebox-head {
|
||
--fs-head-radius: max(0px, calc(var(--fs-radius) - 1px));
|
||
border-radius: var(--fs-head-radius) var(--fs-head-radius) 0 0;
|
||
}
|
||
|
||
/* Inactive tab panes. Base's [data-tab-title] rule sets height:0/overflow:hidden, collapsing only
|
||
* the CONTENT box — padding, border and margin still draw empty bars, and a pane left in flow
|
||
* inflates scrollHeight (Interfaces scrolled ~585px past the footer, Network -> DNS another
|
||
* ~750px). Zeroing the box does both jobs, and hiding the pane with `display: none` on top of
|
||
* that buys nothing (Interfaces scrollHeight 1039 either way).
|
||
*
|
||
* It is `visibility`, not `display: none`, because a display:none pane has no width and a view
|
||
* is entitled to measure one: stock channel_analysis.js builds its channel axis from
|
||
* `graph.offsetWidth` once, in a rAF, so a pane that starts inactive reads 0 and the 5GHz graph
|
||
* stays a collapsed blob for the life of the page. `visibility: hidden` keeps the pane laid out
|
||
* at full width while still taking it out of the a11y tree and the tab order, and
|
||
* height:0 + overflow:hidden keep it out of the scroll.
|
||
*
|
||
* `position: relative` is what makes that overflow hold: an absolutely positioned descendant is
|
||
* clipped only by an overflow ancestor in its own containing-block chain, and a collapsed pane of
|
||
* ordinary markup has none — the theme supplies them itself, the toggle switch parking its real
|
||
* checkbox at `position: absolute` (theme/60-inputs.css). Measured: DHCP and DNS scrolled 1841px
|
||
* against 1110px of content on 24.10 and 1925 vs 1439 on 25.12, i.e. ~500-700px of empty page
|
||
* under the footer (issue #10).
|
||
*
|
||
* Hence any [data-tab-title], not just .cbi-section ones. */
|
||
[data-tab-title]:not([data-tab-active="true"]) {
|
||
visibility: hidden;
|
||
position: relative;
|
||
height: 0;
|
||
overflow: hidden;
|
||
padding: 0;
|
||
margin: 0;
|
||
border: 0;
|
||
}
|
||
/* 6%, not 12%. The list inside is bold --fs-danger on a tint OF --fs-danger, so the tint
|
||
* eats its own contrast: 12% measured 4.4:1 (axe-core). At 6% every palette clears 4.5
|
||
* (4.87 / 4.95 / 5.28 / 6.09) and the box still reads red. */
|
||
.cbi-section-error { border-color: var(--fs-danger); background: color-mix(in srgb, var(--fs-danger) 6%, var(--fs-panel)); color: var(--fs-text); }
|
||
.cbi-section-error ul li { color: var(--fs-danger); }
|
||
|
||
/* denser data tables (dhcp static leases, section-tables) */
|
||
.cbi-section-table .th, .cbi-section-table .td,
|
||
/* The `::before` is the GridSection name CELL on 24.10, where form.js renders none and base
|
||
* generates the column from `data-title` (base/95-luci.css). It is not a `.th`/`.td`, so every
|
||
* cell rule below has to name it too, or the column it draws lines up with nothing. Listing
|
||
* `.tr::before` unqualified is safe: base names exactly the rows that get one, and a
|
||
* pseudo-element whose content is `none` generates no box. */
|
||
.table.cbi-section-table .th, .table.cbi-section-table .td,
|
||
.table.cbi-section-table .tr::before { padding: var(--fs-space-2) var(--fs-space-3) var(--fs-space-2) 0; }
|
||
/* The zero padding-left above is right only INSIDE a card, where the .cbi-section supplies that
|
||
* gutter itself. Outside one it is wrong — Status -> Firewall renders `.table.cbi-section-table`
|
||
* in a bare <div>, so the table draws its own frame and the header text ends up glued to that
|
||
* frame's rounded corner (issue #7). A table carrying its own frame pads its own cells. */
|
||
.table.cbi-section-table:not(.cbi-section *):not(.cbi-section-node *) .th,
|
||
.table.cbi-section-table:not(.cbi-section *):not(.cbi-section-node *) .td { padding: var(--fs-space-2-5) var(--fs-space-3); }
|
||
.cbi-section-table .tr.cbi-section-table-titles .th,
|
||
.cbi-section-table .tr.table-titles .th { font-size: var(--fs-type-xs); letter-spacing: var(--fs-eyebrow-tracking); line-height: 1.25; }
|
||
|
||
/* Standalone data tables (Startup, Processes, Routes) -> rounded card. Inside a CBI section
|
||
* the section already IS the card, so the frame is stripped there (above): no double border. */
|
||
.table {
|
||
border: var(--fs-hairline);
|
||
border-radius: var(--fs-radius-lg);
|
||
overflow: hidden;
|
||
background: var(--fs-panel);
|
||
}
|
||
/* Separators go on the CELLS, not on the rows, and that is not a preference: the frame above needs
|
||
* `border-collapse: separate` (a collapsed table silently drops the radius), and in the separated
|
||
* model a row's border is not painted at all — on the .tr these rules paint nothing, and what
|
||
* draws the lines is base's per-cell `border-top`, which also runs along the table's top edge
|
||
* across the frame's rounded corners (issue #7). base no longer sets it, and the lines are
|
||
* declared here, where they paint. Remove one and the other is not a fallback.
|
||
*
|
||
* `:not(:last-child)` lives IN the body-row selector: a trailing bottom border draws a straight
|
||
* line the full width and pokes past the frame's rounded bottom corners. */
|
||
.table .tr.table-titles > .th,
|
||
.table .tr.cbi-section-table-titles > .th,
|
||
.table .tr.cbi-section-table-titles::before { border-bottom: var(--fs-hairline); }
|
||
.table .tr:not(.table-titles):not(.cbi-section-table-titles):not(:last-child) > .td,
|
||
.table .tr.cbi-section-table-row:not(:last-child)::before { border-bottom: var(--fs-hairline); }
|
||
/* descendant combinator (not >) so a real <table>'s browser-inserted <tbody> is covered
|
||
* too — one rule themes every data table, div- or table-based. */
|
||
.table .tr.table-titles, .table .tr.cbi-section-table-titles {
|
||
background: transparent; /* drop the base panel2 header bar — match the dashboard's faint-label + underline look */
|
||
}
|
||
.table .tr.table-titles .th, .table .tr.cbi-section-table-titles .th,
|
||
.table .tr.cbi-section-table-titles::before {
|
||
color: var(--fs-eyebrow-color); font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight);
|
||
letter-spacing: var(--fs-eyebrow-tracking);
|
||
background: transparent;
|
||
}
|
||
/* The row's NAME, which must read the same on both releases: 25.12 renders a real
|
||
* `.td.cbi-section-table-titles`, 24.10 base's `::before`. Both are a LABEL, not a value, so
|
||
* both take the UI face — the surrounding value cells are mono, and base makes this one bold, so
|
||
* with no 700 mono face shipped every rule name is drawn in synthetic bold mono, smearing the
|
||
* grid on the one string in the row that is a name. Specificity (0,3,0), not source order, is
|
||
* what beats the (0,1,0) mono rule. */
|
||
.table.cbi-section-table .td.cbi-section-table-titles,
|
||
.table.cbi-section-table .tr.cbi-section-table-row::before {
|
||
font-family: var(--fs-font-sans); font-weight: var(--fs-weight-bold); color: var(--fs-text);
|
||
}
|
||
.table .tr:not(.table-titles):not(.cbi-section-table-titles) {
|
||
transition: background var(--fs-dur);
|
||
}
|
||
/* Row hover ONLY on real data/config tables. A plain layout `.table` — Diagnostics is one
|
||
* <tr> of input+button spanning the whole card, with no header row, so fs-select never tags
|
||
* it .fs-dt — lit the ENTIRE block on hover (issue #5.3), and a data table's placeholder row
|
||
* ("no active leases") lit up too (issue #5.2, plus :not(.placeholder) above). Data tables
|
||
* hover per-cell above; config tables (.cbi-section-table, never .fs-dt) hover data rows here. */
|
||
/* :not(.placeholder) — a config table's "This section contains no values yet" row carries
|
||
* .cbi-section-table-row too, so it lit up under the pointer exactly as the data tables' did
|
||
* before the rule above excluded theirs (issue #15: reported on UPnP, but every empty
|
||
* GridSection had it — firewall's NAT rules, IP sets, static routes). A hover cue promises a
|
||
* row you can act on; there is no row. */
|
||
.table.cbi-section-table .tr.cbi-section-table-row:not(.placeholder):hover { background: var(--fs-panel2); }
|
||
/* keep section/modal tables flush (no forced hover-card look) */
|
||
.cbi-section .table > .tr, .cbi-modal .table > .tr { border-radius: 0; }
|
||
|
||
/* .fs-dt is set by fs-select.js on a #view .table that has a header row and is NOT a
|
||
* .cbi-section-table — replacing a live `:has(.tr.table-titles):not(.cbi-section-table)`
|
||
* the style engine re-evaluated on every mutation of these polled tables (processes,
|
||
* routes, leases). */
|
||
|
||
/* ---- the card stack: one copy, every data table, no @container threshold ----
|
||
*
|
||
* Three @container thresholds meant two copies of these declarations, CSS being unable to share
|
||
* a block across thresholds — 55 duplicated declarations, free to drift. All three really asked
|
||
* "does it OVERFLOW?", which the browser can compute: fs-select.js measures it and sets
|
||
* .fs-stacked, so every table finds its own width, a third-party app's included. Only the
|
||
* judgement a measurement cannot make survives, in fs-select.js: "too cramped to be a table".
|
||
*
|
||
* A config table (.cbi-section-table) is deliberately not here and keeps its @container
|
||
* (theme/65-dropdown.css): its rows hold widgets that bake in a width from the layout they were
|
||
* rendered in, so un-collapsing one to read it CHANGES what is read — the firewall zone table
|
||
* then claimed 1747px where it needs 1190px. Do not "finish the job" by measuring it.
|
||
*
|
||
* Shape = stock LuCI's phone layout: label ABOVE its value, cell = half the row. Label:value on
|
||
* one line with the value right-flushed costs a leases row 7 full-width lines and gives the
|
||
* value ~40% of the width; half-width pairs fold it into 4. Values stay left-aligned.
|
||
*
|
||
* ONE CONTRACT — do not split it again. The stack is MEASURED and fires at ANY width, while
|
||
* `display: block` and the `.hide-xs`/`.hide-sm` drops once lived only inside
|
||
* `@media (max-width: 767px)` (theme/90-responsive.css). The window between the two is real: the
|
||
* sidebar layout's content column is (viewport - 224 - 56), so from ~768 to 860px it is already
|
||
* below CRAMPED while that query is off. Measured at 790/820/850px, status_leases stacked with
|
||
* `display: table` still in force and wifi_assoclist_table rendered all five of its .hide-xs
|
||
* cells; at 760px both were correct. So they key off the CLASS. */
|
||
/* ---- a table nobody has answered for yet takes no room ----
|
||
*
|
||
* A poll tick REPLACES these tables, and a fresh one carries no marks: for the moment between
|
||
* landing and being stamped it is laid out as a full-width table, several screens taller than
|
||
* the card stack it is about to be. The engine re-anchors on that intermediate and throws the
|
||
* reader 612px out and back, twice per tick. fs-select.js answers in a MutationObserver
|
||
* microtask, i.e. before paint — but only if nothing forces layout in between, and an app that
|
||
* reads a width right after rendering does exactly that.
|
||
*
|
||
* So the table waits outside the layout until it has an answer. `display: none`, not
|
||
* `visibility: hidden`, because a hidden box still occupies its height.
|
||
*
|
||
* Guarded on the fitters being alive: `:root[data-fs-fit]` is written by fs-fit.js and by
|
||
* nothing else, so a document where that file never ran shows every table exactly as before this
|
||
* rule existed. A stylesheet that can hide content on its own is one that can hide it
|
||
* forever. */
|
||
:root[data-fs-fit] #view .table.fs-dt:not(.fs-fitted),
|
||
:root[data-fs-fit] #modal_overlay .table.fs-dt:not(.fs-fitted) { display: none; }
|
||
|
||
.table.fs-stacked { display: block; width: 100%; }
|
||
/* `.fs-drop-xs` is the same drop one rung earlier: fs-select.js writes it on a table that has
|
||
* overflowed, before it decides to card, because the view author has already said which columns
|
||
* are expendable and a table with two fewer columns often fits. Same declaration, so the two
|
||
* selectors share the block, and the class is what makes the drop a MEASURED decision rather
|
||
* than a viewport guess (theme/90-responsive.css keeps that copy for tables that never card). */
|
||
.table.fs-stacked .td.hide-sm, .table.fs-stacked .th.hide-sm,
|
||
.table.fs-stacked .td.hide-xs, .table.fs-stacked .th.hide-xs,
|
||
.table.fs-drop-xs .td.hide-sm, .table.fs-drop-xs .th.hide-sm,
|
||
.table.fs-drop-xs .td.hide-xs, .table.fs-drop-xs .th.hide-xs { display: none; }
|
||
|
||
.table.fs-stacked .tr.table-titles,
|
||
.table.fs-stacked .thead,
|
||
.table.fs-stacked .tr.cbi-section-table-titles,
|
||
.table.fs-stacked .tr.cbi-section-table-descr { display: none; }
|
||
/* align-items: stretch (the default), NOT flex-start: the cells carry a per-cell
|
||
* border-bottom, so when one cell of a pair wraps (a 2-line hostname beside a 1-line MAC)
|
||
* a flex-start neighbour stays short and its separator paints 52px higher — a broken line
|
||
* mid-card (measured). Stretched, both cells take the flex line's height and the
|
||
* separators meet; content still sits at the top (the cells are blocks). */
|
||
/* A `<tfoot>` with no `.tr` inside it IS the row — see the footer block below for which view
|
||
* writes which shape.
|
||
*
|
||
* TWO rules, not one list: a selector list is not forgiving, so a browser without `:has()`
|
||
* (Firefox before 121) throws away the WHOLE list, and `.table.fs-stacked .tr` — every card on
|
||
* every stacked table — loses `display: flex` with it. Split, the `:has()` half degrades alone.
|
||
* The duplicated body is under `css-dup`'s three-declaration floor, so it will not be flagged;
|
||
* the two must change together. */
|
||
.table.fs-stacked .tr { display: flex; flex-wrap: wrap; }
|
||
.table.fs-stacked tfoot:not(:has(> .tr)),
|
||
.table.fs-stacked .tfoot:not(:has(> .tr)) { display: flex; flex-wrap: wrap; }
|
||
.table.fs-stacked .td {
|
||
display: block; min-width: 0; width: auto;
|
||
padding-block: var(--fs-space-1-5); padding-inline: 0 var(--fs-space-2-5); border: 0; text-align: start;
|
||
overflow-wrap: anywhere; white-space: normal;
|
||
}
|
||
/* The column label, lifted above its value (LuCI writes it per cell as data-title).
|
||
*
|
||
* Pinned: the config table (theme/65-dropdown.css) needs these exact declarations under its
|
||
* @container guard, and CSS cannot share a block between a class and a container query — the two
|
||
* guards do not OR. tools/mirror.mjs keeps the copies byte-identical; edit one and the build
|
||
* fails until you edit the other, a duplicate detector going quiet once two copies differ. */
|
||
.table.fs-stacked .td[data-title]::before {
|
||
/* @mirror table-card/label */
|
||
content: attr(data-title); display: block; margin-bottom: var(--fs-space-1);
|
||
font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight); letter-spacing: var(--fs-eyebrow-tracking);
|
||
color: var(--fs-eyebrow-color);
|
||
/* @endmirror */
|
||
}
|
||
/* A footer cell is the fourth member here, and it has to carry `[data-title]` in its own selector
|
||
* to win: `.table.fs-stacked tfoot .td::before` is (0,3,1) against the caption rule's (0,4,0) and
|
||
* loses wherever it is written. Measured on the wifi footer with the titles stamped: `content`
|
||
* computed to "SSID" over `Total:`. With the attribute it is (0,4,1) and (0,5,0). */
|
||
.table.fs-stacked .td.cbi-section-actions::before,
|
||
.table.fs-stacked .td:not([data-title])::before,
|
||
.table.fs-stacked .td[data-title=""]::before,
|
||
.table.fs-stacked tfoot .td[data-title]::before,
|
||
.table.fs-stacked .tfoot .td[data-title]::before { content: none; }
|
||
/* The row's buttons are not a label/value pair — full width below the pairs.
|
||
* PINNED with the config table's copy — see above. */
|
||
.table.fs-stacked .td.cbi-section-actions {
|
||
/* @mirror table-card/actions */
|
||
display: flex; flex: 1 1 100%; justify-content: flex-start;
|
||
flex-wrap: wrap; gap: var(--fs-space-2); padding-top: var(--fs-space-2);
|
||
/* @endmirror */
|
||
}
|
||
/* LuCI groups a row's buttons in ONE inner <div> (startup.js wraps all five), so the td's own
|
||
* flex-wrap above never fires — the div is a single flex item, and .fs-main's overflow-x:clip
|
||
* CUT the tail button (Startup at 390px, measured: the div needs 350px in a 324px cell and
|
||
* "Stop" vanished). Same trap the config table hit on the interfaces row; PINNED to its copy
|
||
* in theme/65-dropdown.css. */
|
||
.table.fs-stacked .td.cbi-section-actions > * {
|
||
/* @mirror table-card/actions-inner */
|
||
flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
|
||
/* @endmirror */
|
||
}
|
||
/* Nothing in a half-width cell may force the row wider than the viewport: .fs-main is
|
||
* overflow-x:clip, so an over-wide child is CUT, not scrolled to. */
|
||
.table.fs-stacked .td > *,
|
||
.table.fs-stacked .td .ifacebadge { min-width: 0; max-width: 100%; }
|
||
|
||
/* ---- the footer, in both shapes upstream writes it ----
|
||
*
|
||
* A `<tfoot>` is still a table-footer-group inside a `display: block` table, so in a card it drew
|
||
* as a box beside the cards: measured at 390px on the station list, 186px wide against 354.
|
||
* `20_lan.js` builds `tfoot > tr > td` and needs only the footer out of the way, the `.tr` above
|
||
* already being a flex row; `30_wifi.js` appends the `<td>`s to the `<tfoot>` through E(), where
|
||
* no parser runs and no row is implied, so there the footer has to become the row — that arm is
|
||
* up with the `.tr` rule. `:has(> .tr)` tells them apart.
|
||
*
|
||
* An empty footer cell goes entirely: a card is a list of facts and an empty cell is not one. The
|
||
* captions are suppressed up in the `content: none` group, for the specificity reason stated
|
||
* there. */
|
||
.table.fs-stacked tfoot:has(> .tr),
|
||
.table.fs-stacked .tfoot:has(> .tr) { display: block; }
|
||
.table.fs-stacked tfoot .td:empty,
|
||
.table.fs-stacked .tfoot .td:empty { display: none; }
|
||
|
||
/* --- the DATA table's flavour (L.ui.Table, tagged .fs-dt) ---
|
||
* Clip rows to the frame's corners: zebra paints its background on the .tr and the frame is
|
||
* overflow:visible, so a striped row's square background pokes past the corners. Safe once
|
||
* stacked — a flex-wrap row fits the width, so there is no horizontal column left to clip, which
|
||
* is the only thing overflow:visible protected. `#view .table.fs-dt.fs-stacked` (1,3,0) outranks
|
||
* the frame's `#view .table[id]` (1,2,0) on specificity, not on order.
|
||
*
|
||
* `#modal_overlay` as well, because the FRAME already reaches a dialog and this clip did not: a
|
||
* data table inside a dialog's `.cbi-section` takes the frame from `.cbi-section .table[id]`
|
||
* above and has the same corners with the same zebra painted square across them. */
|
||
#view .table.fs-dt.fs-stacked,
|
||
#modal_overlay .table.fs-dt.fs-stacked { overflow: hidden; }
|
||
/* `:not(.placeholder)` — the plate is out of flow (base/40-tables.css) and its row exists only to
|
||
* reserve the height it needs. Padding is subtracted from that reservation (border-box), so the
|
||
* card's row inset left 41px of the 63px the plate is: measured on Wireless as 10px of text
|
||
* hanging below the frame once the table stacked. A row with no rows in it has nothing to inset. */
|
||
.table.fs-dt.fs-stacked .tr:not(.placeholder) { padding: var(--fs-space-2-5) var(--fs-space-4) var(--fs-space-3); }
|
||
/* ---- the pair width, and the adversary specificity cannot reach ----
|
||
*
|
||
* Half the row is what makes the card a card. It was `.fs-stacked .td` at (0,2,0), deliberately
|
||
* low so the unguarded `.table .td.col-N` below, at (0,3,0), could beat it on specificity rather
|
||
* than source order — which holds against every rule in a LAYER and none outside one.
|
||
* luci-mod-dashboard ships `@media (max-width: 640px) { .Dashboard … .tr .td { flex: 2 2 10% } }`
|
||
* unlayered, and that beats every layered rule at any specificity: measured at 390px, four cells
|
||
* of a station row at 81px on one line, `flex-basis` computing to 10% — the pairs never formed.
|
||
*
|
||
* Hence the flag, the sanctioned kind. Spent narrowly, and by SELECTOR because a flag would
|
||
* outrank these two families as well: `.cbi-section-actions` is a full-width line of buttons,
|
||
* `[class*="col-"]` is the view author's own weight. The third `:not()` is not one of those — it
|
||
* is how this rule loses to the meter below, both being flagged: without it (0,5,0) beats (0,4,0)
|
||
* and the meter silently keeps half a line. */
|
||
.table.fs-stacked .td:not(.cbi-section-actions):not([class*="col-"]):not(:has(.cbi-progressbar)) {
|
||
flex: 1 1 50% !important;
|
||
}
|
||
/* A meter takes the whole line: `.cbi-progressbar::after` prints the bar's title above its right
|
||
* edge (25-progressbar.css), which in a card is where the cell's own caption sits — measured at
|
||
* 390px, `SIGNAL STRENGTH` and `RSSI: 65% (-57dBm)` drawn over each other. Beside the bar instead
|
||
* would be paid out of the bar, too short at half a row to read as one. */
|
||
.table.fs-stacked .td:has(.cbi-progressbar) { flex: 1 1 100% !important; }
|
||
|
||
/* ---- column weights: one unguarded block, unguarded on purpose ----
|
||
* A weight (1-10) is a share of a CARDED row — the JS says which cell is a narrow badge and
|
||
* which the wide status text; without it every cell falls back to the flat 50% above.
|
||
*
|
||
* Do not copy them under a guard. `flex` applies only to a FLEX ITEM, and a `.td` becomes one
|
||
* exactly when its `.tr` turns into a flex container — by either mechanism (`.fs-stacked`,
|
||
* measured at any width, or the config table's @container, which can card on a 1200px desktop).
|
||
* Anywhere else they are inert, so one copy serves all three tiers. Basis N*30px, as in stock,
|
||
* so the weights compose the same way. */
|
||
.table .td.col-1, .table .th.col-1 { flex: 1 1 30px; }
|
||
.table .td.col-2, .table .th.col-2 { flex: 2 2 60px; }
|
||
.table .td.col-3, .table .th.col-3 { flex: 3 3 90px; }
|
||
.table .td.col-4, .table .th.col-4 { flex: 4 4 120px; }
|
||
.table .td.col-5, .table .th.col-5 { flex: 5 5 150px; }
|
||
.table .td.col-6, .table .th.col-6 { flex: 6 6 180px; }
|
||
.table .td.col-7, .table .th.col-7 { flex: 7 7 210px; }
|
||
.table .td.col-8, .table .th.col-8 { flex: 8 8 240px; }
|
||
.table .td.col-9, .table .th.col-9 { flex: 9 9 270px; }
|
||
.table .td.col-10, .table .th.col-10 { flex: 10 10 300px; }
|
||
|
||
/* general table cell padding — compact but readable (Startup/Processes/etc) */
|
||
.table .td { padding-top: var(--fs-space-2); padding-bottom: var(--fs-space-2); }
|
||
|
||
/* ---- the last resort: a foreign <table> the card stack cannot claim ----
|
||
*
|
||
* fs-select.js tags any `#view table` that HAS a header row as `.table.fs-dt`, so it is measured
|
||
* and cards like every other data table. What is left here is the table with no header at all: a
|
||
* log, a statistics matrix, a layout table. Those must not be carded even if they overflow — a
|
||
* card prints `data-title` above each value, and a matrix has no titles to print, so stacking one
|
||
* produces a column of numbers with nothing saying what they are. Comparison is the point of
|
||
* that shape; it scrolls instead.
|
||
*
|
||
* `display: block` plus `overflow-x: auto` on the table ITSELF, not on a wrapper: the theme
|
||
* cannot add markup around someone else's element, and the block box is what gives it a scroll
|
||
* container. The rows still lay out as a table, a `display: block` <table> wrapping its children
|
||
* in an anonymous table box.
|
||
*
|
||
* Unguarded by a media query, because the viewport was never the fact that mattered: a narrow
|
||
* panel or a split window overflows at any width, and a phone report is only where it was
|
||
* noticed first.
|
||
*
|
||
* A dialog is the worst place to leave it out, hence the `#modal_overlay` half of the selector:
|
||
* the overlay is the scroll container (base/60-modal.css) and the dialog is centred in it with
|
||
* `margin: auto`, so an over-wide child scrolls the dialog's own edge off the screen instead of
|
||
* scrolling itself. Measured at 390px with a four-column `nowrap` log table: in #view it scrolls
|
||
* inside itself (358px shown of 636px, page scroll 0); in a dialog it rendered 636px wide in
|
||
* 317px of room and gave the overlay 282px of horizontal scroll. */
|
||
/* `.fs-xscroll` is the same outcome reached the other way: fs-select.js writes it on a `.table`
|
||
* with no header row that has been measured to overflow — the shape a bare `<table>` selector
|
||
* cannot reach, a third-party app being free to render its log as `<div class="table">`. A class
|
||
* rather than a selector, and measured rather than declared, for a spec-level reason:
|
||
* `overflow-x: auto` forces `overflow-y` to compute to `auto` too (css-overflow-3 §3.1), so a
|
||
* table given this becomes a clip for every popup inside it, and ui.js then walks up from an open
|
||
* dropdown, finds the TABLE as the scroll parent and sizes the list against it. The fitter
|
||
* refuses the class to any table holding a widget, so this rule needs no `:has()` ladder. */
|
||
#view table:not(.table):not(.cbi-section-table):not(.fs-dt),
|
||
#modal_overlay table:not(.table):not(.cbi-section-table):not(.fs-dt),
|
||
#view .table.fs-xscroll, #modal_overlay .table.fs-xscroll {
|
||
display: block;
|
||
max-width: 100%;
|
||
overflow-x: auto;
|
||
/* the scroll gesture stops at this table instead of dragging the page sideways */
|
||
overscroll-behavior-x: contain;
|
||
/* the scrollbar IS the affordance here. A gradient "there is more to the right" shadow is the
|
||
* usual trick and does not work on these tables: it has to paint on the scroll container,
|
||
* behind rows that carry their own zebra fill, so it shows through every second row only. */
|
||
scrollbar-width: thin;
|
||
}
|
||
|
||
/* ---- a table of controls stacks: it is a form laid out as a table ----
|
||
*
|
||
* Written by fs-select.js on a header-less table that overflows and holds a control. Scrolling
|
||
* one is not an option — the popup inside would be clipped by the scroll container it just
|
||
* became — and doing nothing leaves it cut: at 320px of room the row needed 338 and the button
|
||
* was past `.fs-main`'s clip with no way to reach it.
|
||
*
|
||
* `display: block` on all three levels, the one place this theme wants it: the rows are a form,
|
||
* so there is nothing to compare across them and nothing to keep in columns. Each control takes
|
||
* the width of its line and the table cannot exceed its parent by construction. Nothing here
|
||
* creates a scroll container, so an open dropdown still escapes. */
|
||
.table.fs-rowstack,
|
||
.table.fs-rowstack .tr,
|
||
.table.fs-rowstack .td,
|
||
.table.fs-rowstack .th { display: block; width: auto; }
|
||
/* the cells were table-cells a moment ago and carried its side padding; as blocks they need the
|
||
* vertical rhythm instead, or the controls sit flush against each other */
|
||
.table.fs-rowstack .td { padding-inline: 0; padding-block: var(--fs-space-1); }
|
||
|
||
/* ---- what makes a scrolled table readable: the row keeps its name ----
|
||
*
|
||
* Scrolling a table sideways takes the identity column away first, leaving a screenful of values
|
||
* belonging to nothing. Pinning the first cell of every row is the one thing a card view gives
|
||
* for free and a scrolled table does not.
|
||
*
|
||
* The cells need their own background (a row's does not travel with a sticky cell in the
|
||
* separated model, as for the header above) and a hairline on the inline end, so the pinned
|
||
* column reads as an edge rather than as text sliding over text. `z-index: 2` keeps it over the
|
||
* sticky header's `1` where the two cross, at the corner cell. */
|
||
#view .table.fs-xscroll .tr > :first-child,
|
||
#modal_overlay .table.fs-xscroll .tr > :first-child,
|
||
#view .table.fs-xscroll tr > :first-child,
|
||
#modal_overlay .table.fs-xscroll tr > :first-child {
|
||
position: sticky; left: 0; z-index: 2;
|
||
background: var(--fs-panel);
|
||
border-inline-end: var(--fs-hairline);
|
||
}
|
||
/* a keyboard user reaches the scroll box by tabbing to it (fs-select.js gives it tabindex and a
|
||
* name); it has to show that it has focus — outline, not box-shadow, so forced-colors keeps it */
|
||
#view .table.fs-xscroll:focus-visible,
|
||
#modal_overlay .table.fs-xscroll:focus-visible {
|
||
outline: 2px solid var(--fs-accent);
|
||
outline-offset: 2px;
|
||
}
|
||
}
|