mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
545 lines
30 KiB
CSS
545 lines
30 KiB
CSS
@layer theme {
|
|
/* Generic .dropdown-menu popup — ui.js's split "Save & Apply" menu, plus whatever a third-party
|
|
* luci-app-* emits. NOT the theme's nav menu (that is a plain nested <ul>; see 10-chrome.css).
|
|
* Hover text is ACCENT to match the open cbi-dropdown list: this is a form control's menu. */
|
|
.dropdown-menu { background-color: var(--fs-panel); border-color: var(--fs-border); }
|
|
.dropdown-menu a { color: var(--fs-text); text-shadow: none; }
|
|
.dropdown-menu a:hover,
|
|
.dropdown-menu a:focus-visible { background: var(--fs-panel2); background-image: none; color: var(--fs-accent); }
|
|
|
|
/* Split "Save & Apply": a plain dropdown in the action bar carries no frame of its own.
|
|
*
|
|
* The guard is the same `:not(.btn):not(.cbi-button)` the widget rules below use, and it is
|
|
* load-bearing: the split control's outer div IS the button, and the class on it is the one
|
|
* ui.js picked for the SELECTED mode — `cbi-button-apply` for "Save & Apply", but
|
|
* `cbi-button-negative` the moment the user picks "Apply unchecked" (luci.js's ComboButton
|
|
* `classes` map). Unguarded, this rule (0,2,0) erased that variant's fill while its ink
|
|
* (--fs-on-danger) survived from the variant rule that sets no background — a button painted
|
|
* page-on-page: 1.13:1 in dark and 1.06:1 in light, measured on the footer. Only the mode
|
|
* repainted below was ever visible. */
|
|
.cbi-page-actions .cbi-dropdown:not(.btn):not(.cbi-button) {
|
|
border: 0; border-radius: var(--fs-radius); background: transparent;
|
|
}
|
|
/* Apply is the one variant with no fill of its own — `.cbi-button-action` and the rest carry
|
|
* theirs from theme/55-buttons.css, and `.cbi-button-apply` is filled only where it is THE
|
|
* primary action, which is this bar. Painted by SPECIFICITY — it used to be a rule written
|
|
* afterwards, i.e. a fix that depended on source order. */
|
|
.cbi-page-actions .cbi-dropdown.cbi-button-apply,
|
|
.cbi-page-actions .cbi-dropdown > .cbi-button {
|
|
background: var(--fs-accent);
|
|
background-image: none;
|
|
border: 0;
|
|
color: var(--fs-on-accent);
|
|
}
|
|
.cbi-page-actions .cbi-dropdown.cbi-button-apply:hover,
|
|
.cbi-page-actions .cbi-dropdown > .cbi-button:hover { filter: var(--fs-hover-lift); }
|
|
|
|
/* ---- cbi-dropdown widget: the JS select used across all forms ----
|
|
* Restyled to the design dropdown (panel2, rounded, chevron, focus ring) + themed open list.
|
|
* A native <select> is styled separately.
|
|
*
|
|
* The widget's MACHINERY is here too — absorbed from base/80-dropdown.css whole. ui.js drives
|
|
* it purely through attributes ([open], [multiple], [empty], [more], [display], [selected]);
|
|
* the rules below turn those into a working combobox. The display state machine is UNFLAGGED:
|
|
* upstream resolved the states with !important, but flag-vs-flag falls back to
|
|
* specificity-then-order anyway, so the machine below states the SAME resolution openly (see
|
|
* the state-machine block). The `ul` margins were flagged too, on the stated grounds that ui.js
|
|
* writes `margin` INLINE on an open list. IT DOES NOT — grepped on both releases: ui.js touches
|
|
* only `top`/`left`/`right`/`maxHeight`/`bottom` on that `ul`, and only under `'ontouchstart' in
|
|
* window` (see 90-responsive.css, where the flags that fight THOSE live). The flags came in with
|
|
* the absorbed widget from luci-theme-bootstrap, which has no @layer and needed one to beat its
|
|
* own `ul { margin: 0 0 10px 25px }`; here the layer split does that for free. They were not
|
|
* idle, though — they were beating THIS FILE: `margin: 0 !important` on (0,1,1) out-ranked the
|
|
* open popover's own `margin-top` at (0,2,1) below, so the list sat 1px off the control instead
|
|
* of the designed 6px, and the two `.btn` rules then needed flags of their own purely to escape
|
|
* the first one. Measured on the gallery: dropping all three moves exactly one value (that gap,
|
|
* 1px -> 7px) and nothing else. */
|
|
.cbi-dropdown {
|
|
display: inline-flex;
|
|
cursor: pointer;
|
|
height: auto;
|
|
position: relative;
|
|
padding: 0;
|
|
}
|
|
.cbi-dropdown img {
|
|
/* constrain a vector icon of any intrinsic size to the text line */
|
|
max-height: 1.6em;
|
|
width: auto;
|
|
}
|
|
.cbi-dropdown > ul {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
overflow-x: hidden;
|
|
overflow-y: hidden;
|
|
display: flex;
|
|
width: 100%;
|
|
}
|
|
/* clears the button's own side inset so the label starts on its text, not on its border.
|
|
* Unflagged: (0,3,1) over the rule above's (0,1,1) — it only ever needed a flag to beat that
|
|
* rule's flag. */
|
|
.cbi-dropdown.btn > ul:not(.dropdown),
|
|
.cbi-dropdown.cbi-button > ul:not(.dropdown) {
|
|
margin: 0 0 0 var(--fs-space-3);
|
|
}
|
|
.cbi-dropdown.btn.spinning > ul:not(.dropdown),
|
|
.cbi-dropdown.cbi-button.spinning > ul:not(.dropdown) {
|
|
margin: 0;
|
|
}
|
|
.cbi-dropdown > ul.preview {
|
|
display: none;
|
|
}
|
|
/* no `display` here: `.open` states its own below, and `.more` is display: none until the
|
|
* [multiple][more]/[multiple][empty] showers flip it — one declaration per state, or the
|
|
* audit's same-selector shadow check fires on this very pair */
|
|
.cbi-dropdown > .open,
|
|
.cbi-dropdown > .more {
|
|
flex-grow: 0;
|
|
flex-shrink: 0;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
text-align: center;
|
|
}
|
|
/* margin only: base also gave the button's chevron a .5em padding and a border-left divider,
|
|
* but both were DEAD on arrival — the theme's `.open` rule below zeroes padding and border in
|
|
* a later layer. A dead declaration is deleted, not relocated. */
|
|
.cbi-dropdown.btn > .open,
|
|
.cbi-dropdown.cbi-button > .open {
|
|
margin-inline-start: .5em;
|
|
}
|
|
/* display only: the placeholder's ink is the theme's own `li[placeholder]` --fs-faint below
|
|
* (base's --fs-dim had already lost to it by layer — moving it would recolour the row) */
|
|
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[placeholder] {
|
|
display: none;
|
|
}
|
|
.cbi-dropdown > ul > li {
|
|
display: none;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
flex-shrink: 1;
|
|
flex-grow: 1;
|
|
align-items: center;
|
|
align-self: center;
|
|
color: inherit;
|
|
}
|
|
.cbi-dropdown > ul > li .hide-open { display: initial; }
|
|
.cbi-dropdown > ul > li .hide-close { display: none; }
|
|
/* the chip divider on a BUTTON dropdown's preview; the form dropdown zeroes it below */
|
|
.cbi-dropdown > ul > li[display]:not([display="0"]) {
|
|
border-inline-start: 1px solid var(--fs-border);
|
|
}
|
|
.cbi-dropdown[empty] > ul {
|
|
max-width: 1px;
|
|
}
|
|
.cbi-dropdown > ul > li > form {
|
|
display: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
pointer-events: none;
|
|
}
|
|
.cbi-dropdown > ul > li img {
|
|
vertical-align: middle;
|
|
margin-inline-end: .25em;
|
|
}
|
|
.cbi-dropdown > ul > li > form > input[type="checkbox"] {
|
|
margin: 0 var(--fs-space-1) 0 0;
|
|
}
|
|
|
|
/* ---- the display state machine, on specificity ----
|
|
* Every shower OUT-SPECIFIES the hider it overrides: li[display] (0,2,2) > the li hider
|
|
* (0,1,2); [open] > ul.dropdown > li (0,3,2) likewise; the placeholder rules (0,5,2 and
|
|
* 0,6,2) > the placeholder hider (0,4,2); the form rule (0,4,3) > its hider (0,1,3).
|
|
* [empty]'s block (0,2,2) loses to [open]'s flex (0,3,2) deliberately — an open empty list
|
|
* still lays out as a popup — which is exactly the resolution the old flags produced among
|
|
* themselves (importance never separated them; specificity always did). */
|
|
.cbi-dropdown > ul > li[display],
|
|
.cbi-dropdown[open] > ul.preview,
|
|
.cbi-dropdown[open] > ul.dropdown > li,
|
|
.cbi-dropdown[multiple] > ul > li > label,
|
|
.cbi-dropdown[multiple][open] > ul.dropdown > li,
|
|
.cbi-dropdown[multiple][more] > .more,
|
|
.cbi-dropdown[multiple][empty] > .more {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
}
|
|
/* The ONE state the old flags actually carried: a closed optional single dropdown with an
|
|
* empty value. ui.js hands the [display] attribute to the placeholder row itself
|
|
* (`sel[0] ?? items[0]`), and the placeholder hider (0,4,2) out-specifies the generic
|
|
* li[display] shower (0,2,2) — unflagged, the closed control rendered EMPTY (this is the
|
|
* "popup collapses" the old base comment measured). :not([open]) keeps this out of the open
|
|
* list, where the [optional][open] rule below owns the row — the states cannot overlap, so
|
|
* nothing here rests on source order. */
|
|
.cbi-dropdown:not([open]):not(.btn):not(.cbi-button) > ul > li[placeholder][display] {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
}
|
|
.cbi-dropdown[empty] > ul > li,
|
|
.cbi-dropdown[optional][open] > ul.dropdown > li[placeholder] {
|
|
display: block;
|
|
}
|
|
.cbi-dropdown[multiple][open] > ul.dropdown > li > form {
|
|
display: flex;
|
|
}
|
|
.cbi-dropdown[open] > ul.dropdown > li .hide-open { display: none; }
|
|
/* `min-width` because the thing that overflows an open row is a CHILD with an inline width, so
|
|
* wrapping the row (theme/65 below) cannot reach it: form.js's RichListValue writes the
|
|
* description block as `<div class="hide-close" style="min-width:25vw">`, which assumes the list
|
|
* grows to fit — true on a theme that leaves ul.dropdown unbounded, false here (capped at
|
|
* min(92vw, 420px), 90-responsive.css). The inline floor is a VIEWPORT fraction, so the wider the
|
|
* screen the worse it gets: measured on a live router at 2559px, 25vw = 640px inside a 406px row
|
|
* — scrollWidth 664 vs clientWidth 406, and the row's own `overflow: hidden` cut "…for routing
|
|
* with/withou" mid-word, losing the whole "Requires hardware NAT support" sentence (issue #15,
|
|
* reported again after 0.10.1, whose fix only addressed the row). At 1280px the same option fits,
|
|
* which is why it does not reproduce on a laptop. An author !important is the only thing that
|
|
* outranks an inline declaration; releasing the floor lets the text wrap inside its row. */
|
|
.cbi-dropdown[open] > ul.dropdown > li .hide-close { display: initial; min-width: 0 !important; }
|
|
.cbi-dropdown[open] > ul.dropdown > li:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
.cbi-dropdown[open] > ul.dropdown > li[unselectable] {
|
|
opacity: .7;
|
|
}
|
|
|
|
.cbi-dropdown:not(.btn):not(.cbi-button) {
|
|
background: var(--fs-panel2);
|
|
border: 1px solid var(--fs-border);
|
|
border-radius: var(--fs-radius);
|
|
color: var(--fs-text);
|
|
min-height: var(--fs-ctl-h);
|
|
/* absorbed from base's generic field box with the width below. The fixed height is
|
|
* load-bearing next to the min-height: without it a closed dropdown grows to its
|
|
* tallest child (the Save & Apply split control carries a 40px button — measured,
|
|
* 38 -> 42px on the gallery). */
|
|
height: var(--fs-control-h);
|
|
font-size: var(--fs-type);
|
|
transition: border-color var(--fs-dur), box-shadow var(--fs-dur);
|
|
/* Width absorbed from base/30-forms.css, where it was grouped with .cbi-dynlist's — so the
|
|
* theme sized the dynlist and base still sized the dropdown, two halves of the same
|
|
* decision in two layers. Same numbers, and they are NOT the dynlist's (240/440): a
|
|
* dropdown holds one value, a dynlist a column of them. */
|
|
min-width: 210px;
|
|
max-width: 400px;
|
|
width: auto;
|
|
/* The widget's own box. In base these two carried `!important` — not to beat an inline
|
|
* style, but to beat base's OWN generic form-field rule, which sets `display: inline-block`
|
|
* and `padding: 4px` on this very selector at a higher specificity (0,3,0 vs 0,1,0). A later
|
|
* LAYER beats it for free, which is exactly what the layer split is for. */
|
|
display: inline-flex;
|
|
padding: 0;
|
|
}
|
|
/* …and the button dropdown (the Save & Apply split control), where the flag was beating the
|
|
* theme's own `.cbi-button` padding. Specificity does it: 0,2,0 over 0,1,0. */
|
|
.cbi-dropdown.btn, .cbi-dropdown.cbi-button { display: inline-flex; padding: 0; }
|
|
.cbi-dropdown:not(.btn):not(.cbi-button):hover { border-color: var(--fs-dim); }
|
|
.cbi-dropdown:focus, .cbi-dropdown[open] {
|
|
border-color: var(--fs-accent);
|
|
box-shadow: var(--fs-focus-ring);
|
|
outline: none;
|
|
}
|
|
/* `.cbi-dropdown > ul.dropdown > li` is the same menu row on a BUTTON dropdown (the Save & Apply
|
|
* split control), which the `:not(.btn):not(.cbi-button)` guard excludes — so that menu kept
|
|
* base's .25em rows while every other dropdown got these. One menu, one row. */
|
|
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li,
|
|
.cbi-dropdown > ul.dropdown > li {
|
|
padding: var(--fs-space-2) var(--fs-space-3); color: var(--fs-text);
|
|
font-family: var(--fs-font-sans);
|
|
/* one menu row: a grid step, which is what a line of text at this scale rounds to. NOT
|
|
* `calc(--fs-type * --fs-leading)` — that is 19.5px, and a half-pixel on a BOX edge is
|
|
* the thing the scale exists to stop. Absorbed with the machinery above. */
|
|
min-height: var(--fs-space-5);
|
|
}
|
|
/* …and the row's CONTENTS, which is where that font was being lost. A MULTI-select wraps every
|
|
* item in `<form><label><input type=checkbox> text</label></form>` (ui.js builds the checkbox
|
|
* that way), and `.cbi-value-field *` in theme/60-inputs.css hands mono to every descendant of a
|
|
* field — it matches that form and that label DIRECTLY, while the rule above only reaches the
|
|
* `<li>`, whose font the label then overrides rather than inherits. So the same widget drew two
|
|
* fonts depending on one option: a single-select list, whose text sits in the `<li>` itself,
|
|
* came out sans; every multi-select list came out mono, in the open menu AND in the closed
|
|
* control that lists what is selected. Measured on the router with ui.Dropdown: `<li>` Manrope,
|
|
* its `<form>`/`<label>` JetBrains Mono.
|
|
*
|
|
* It is a LABEL, not a value — the same distinction `<strong>` gets in theme/45-misc.css — and
|
|
* mono spacing is what makes a wordy option list look stretched (`3G Band 1`, `LTE B1`, reported
|
|
* against luci-app-modeminfo's Bands page). `inherit`, not the token again: the row above is the
|
|
* one place the menu's font is chosen. The filter/create `<input>` inside the list is left
|
|
* alone: what the user TYPES there is a value. */
|
|
.cbi-dropdown > ul > li > form,
|
|
.cbi-dropdown > ul > li label { font-family: inherit; }
|
|
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[display]:not([display="0"]) { border-inline-start: 0; }
|
|
.cbi-dropdown > ul > li[placeholder] { color: var(--fs-faint); }
|
|
/* the multiselect's inline filter/create inputs — moved from base with the generic `input`
|
|
* box in theme/60-inputs.css: in base these beat it in-layer, left behind they would lose
|
|
* to it by layer and render as free-standing 210px fields inside the open list */
|
|
.cbi-dropdown > ul > li input[type="text"] { height: var(--fs-space-5); }
|
|
.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { width: 100%; }
|
|
|
|
/* Custom chevron in the toggle (stock text/svg arrow hidden). `.open` is the only hit target that
|
|
* opens the list — on a button dropdown (.cbi-dropdown.btn, e.g. Diagnostics) a click anywhere
|
|
* else RUNS the button. `font-size: 0` collapsed the stock `line-height: 2em`, so the span
|
|
* measured 28x0 and the chevron (an absolute ::after) sat outside its own box: aiming at it hit
|
|
* the button. Hence a real box, chevron centred in it. */
|
|
.cbi-dropdown > .open {
|
|
display: flex;
|
|
font-size: 0; line-height: 0; border: 0;
|
|
position: relative; padding: 0;
|
|
align-self: stretch;
|
|
min-width: 34px; min-height: var(--fs-ctl-h-sm);
|
|
align-items: center;
|
|
cursor: pointer;
|
|
}
|
|
/* `.more` is the "…" a multi-value dropdown shows when its chips overflow. base wrote it in ONE
|
|
* rule with `.open`; the theme took `.open` for the chevron above and left its twin behind, so
|
|
* the two halves of the same toggle strip sat in different layers. Same geometry, absorbed —
|
|
* and hidden by default here (the [multiple][more]/[multiple][empty] showers reveal it),
|
|
* with base's muted ink. */
|
|
.cbi-dropdown > .more { line-height: 2em; padding: 0 .3em; color: var(--fs-dim); display: none; }
|
|
.cbi-dropdown > .open > * { display: none; }
|
|
.cbi-dropdown > .open::after {
|
|
content: ""; position: absolute; inset: 0; margin: auto;
|
|
width: 7px; height: 7px;
|
|
border-right: 2px solid var(--fs-dim); border-bottom: 2px solid var(--fs-dim);
|
|
transform: translateY(-2px) rotate(45deg); /* optical centre of a "v" */
|
|
}
|
|
/* A BUTTON dropdown (Save & Apply, Diagnostics) draws that chevron on the button's own FILL, where
|
|
* --fs-dim is a grey nobody measured against it — on the accent-filled Save & Apply it read as a
|
|
* smudge, not an arrow. currentColor takes whatever ink the variant already set, so there is no
|
|
* second list of variants to keep in sync (the same reason base draws this widget's divider with
|
|
* a colourless `border-left`). The form-control dropdown keeps the muted chevron on purpose:
|
|
* there the arrow is an affordance beside the value, not the content. */
|
|
.cbi-dropdown.btn > .open::after,
|
|
.cbi-dropdown.cbi-button > .open::after { border-color: currentColor; }
|
|
|
|
/* open list -> design popover (this was once stated four times over; these are the values
|
|
* that were winning). */
|
|
.cbi-dropdown[open] { z-index: var(--fs-z-dropdown); }
|
|
.cbi-dropdown[open] > ul.dropdown {
|
|
/* what makes it a popup at all — out of flow, stacked from the --fs-z-* scale;
|
|
* absorbed with the machinery */
|
|
display: block;
|
|
position: absolute;
|
|
z-index: var(--fs-z-dropdown);
|
|
transition: max-height var(--fs-dur-move);
|
|
overflow-y: auto;
|
|
background: var(--fs-panel);
|
|
border: 1px solid var(--fs-border);
|
|
border-radius: var(--fs-radius-lg);
|
|
box-shadow: var(--fs-shadow-pop);
|
|
padding: var(--fs-space-1-5);
|
|
/* the gap that detaches the popover from its control. It was DEAD until the `ul` margin
|
|
* flags above came out (they out-ranked it from a lower specificity), so the list sat 1px
|
|
* off the control — the one value that moved when they went. */
|
|
margin-top: var(--fs-space-1-5);
|
|
}
|
|
.cbi-dropdown[open] > ul.dropdown > li {
|
|
border-radius: var(--fs-radius-sm); padding: var(--fs-space-2) var(--fs-space-3);
|
|
border-inline-start: 0; color: var(--fs-text);
|
|
position: relative;
|
|
/* An OPEN row wraps; only the closed preview ellipsizes. `.cbi-dropdown > ul > li` (0,1,2)
|
|
* sets nowrap + overflow:hidden + ellipsis, which is right for the collapsed control — one
|
|
* line showing the current value — and wrong for the menu: the list is capped at
|
|
* min(92vw, 420px) (90-responsive.css), so an option that carries a description ran out of
|
|
* box and was cut mid-word. Measured on Network → Firewall, "Hardware flow offloading":
|
|
* "Requires hardware NAT support…" ended at the box edge with no ellipsis, because the cut
|
|
* fell inside a nested element (issue #15). Raising the cap instead would let one verbose
|
|
* option decide the width of every menu on the page. */
|
|
white-space: normal; overflow-wrap: anywhere;
|
|
/* no border-bottom divider: on a rounded box it curls up at the corners. The straight
|
|
* inset strip below (::after) is the divider, so the hover pill stays rounded. */
|
|
border-bottom: 0;
|
|
}
|
|
.cbi-dropdown[open] > ul.dropdown > li:not(:last-child)::after {
|
|
content: ""; position: absolute; inset-inline: 11px; bottom: 0;
|
|
height: 1px; background: var(--fs-border);
|
|
}
|
|
.cbi-dropdown[open] > ul.dropdown > li:hover,
|
|
.cbi-dropdown[open] > ul.dropdown > li.focus { background: var(--fs-panel2); color: var(--fs-accent); }
|
|
/* After hover on purpose: hovering the selected row must still read as selected.
|
|
* The surface is OPAQUE (--fs-panel2) and the accent is carried by the inset rail, not by a
|
|
* tint of itself: accent text on --fs-accent-soft (a translucent tint of the accent) measured
|
|
* 4.21:1 in dark, under the 4.5:1 AA floor — the tint drags the background toward the text and
|
|
* eats its own contrast. axe never saw it because no gallery case rendered an OPEN dropdown
|
|
* with a chosen value; the widget shipped that way on every dark router. */
|
|
.cbi-dropdown[open] > ul.dropdown > li[selected] {
|
|
/* @mirror selected-row/paint */
|
|
background: var(--fs-panel2); color: var(--fs-accent); box-shadow: inset 2px 0 0 var(--fs-accent);
|
|
/* @endmirror */
|
|
}
|
|
/* THE FOCUSED ROW WEARS THE THEME'S RING, not the browser's.
|
|
*
|
|
* ui.js focuses a row as it opens the menu (the first selected one, or the first row), and the
|
|
* UA answers a programmatic focus with `outline: auto` — Chrome's 1px system-blue ring, drawn
|
|
* OUTSIDE the row and following its 8px radius, so opening a dropdown that already has a value
|
|
* flashed a loud blue capsule around the top row on every single-select and multi-select on the
|
|
* router. Every other control in this theme wears --fs-focus-ring; this row wore whatever the
|
|
* browser shipped.
|
|
*
|
|
* Inset rather than the token's outer 3px: the menu clips its own overflow, so an outer ring is
|
|
* cut on the first and last rows. The rail is restated INSIDE this ring so a focused row that is
|
|
* also selected keeps its rail — a shorthand cannot merge with the one below.
|
|
*
|
|
* Both selectors carry `[selected]`-level weight on purpose: the selected-row paint below is
|
|
* (0,4,1) and a bare `li:focus-visible` ties with it, which would leave the focus ring to source
|
|
* order. `docs/conventions.md`: win on specificity, never on order. */
|
|
.cbi-dropdown[open] > ul.dropdown > li[selected]:focus-visible,
|
|
.cbi-dropdown[open] > ul.dropdown > li:focus-visible {
|
|
outline: none;
|
|
box-shadow: inset 2px 0 0 var(--fs-accent), inset 0 0 0 2px var(--fs-accent-soft);
|
|
}
|
|
|
|
/* A MULTI-select row says "chosen" with its CHECKBOX, so it does not also need the rail.
|
|
*
|
|
* The rail is `inset 2px 0 0` on a row with an 8px radius, so it is drawn as an ARC, not a bar —
|
|
* fine as the single-select cue it was designed for, one row at a time, and wrong down a column
|
|
* of them: three chosen bands rendered three blue crescents stacked against the menu's edge and
|
|
* read as a broken frame (reported against luci-app-modeminfo's Bands page). The checkbox is a
|
|
* non-colour cue in its own right, which is what WCAG 2.2 SC 1.4.1 asks of "selected" — a
|
|
* single-select row has none, and keeps the rail.
|
|
*
|
|
* Written as an override rather than by narrowing the paint below, because those three
|
|
* declarations are pinned byte-identical to `select option:checked` in theme/60-inputs.css
|
|
* (tools/mirror.mjs) — a native `<select multiple>` option has no checkbox, so the rail is still
|
|
* the only cue there and the two must not diverge. `:not(:focus-visible)` keeps this off the
|
|
* focused row, so the ring above owns that state alone instead of tying with it. */
|
|
.cbi-dropdown[multiple][open] > ul.dropdown > li[selected]:not(:focus-visible) { box-shadow: none; }
|
|
.cbi-dropdown[multiple][open] > ul.dropdown > li[selected]:focus-visible {
|
|
box-shadow: inset 0 0 0 2px var(--fs-accent-soft);
|
|
}
|
|
|
|
.cbi-dropdown[open] > ul.dropdown > li.create-item { border-top: 1px solid var(--fs-border); margin-top: var(--fs-space-1); }
|
|
/* The dropdown's "add item" field is a text input and takes the same radius as every other
|
|
* one (theme/60-inputs.css). It said 8px here, and won or lost on source order. */
|
|
.cbi-dropdown .create-item-input { background: var(--fs-panel2); border: 1px solid var(--fs-border); border-radius: var(--fs-radius); color: var(--fs-text); }
|
|
/* dropdown add-item (e.g. Community Lists) — the .cbi-dropdown styles itself, so the
|
|
* wrapper stays transparent: no double frame. */
|
|
.cbi-dynlist > .add-item:has(> .cbi-dropdown) {
|
|
background: none; border: 0; overflow: visible; box-shadow: none;
|
|
}
|
|
|
|
.cbi-section-table .td .cbi-dropdown,
|
|
.cbi-section-table .td .cbi-input-select,
|
|
.cbi-section-table .td select,
|
|
.cbi-section-table .td input[type="text"] {
|
|
min-width: 0;
|
|
max-width: none;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Below ~960px of available width a multi-dropdown row no longer fits — card each config row.
|
|
* Same shape as the fs-dt stack (theme/30-tables.css): label ABOVE its cell, a read-only cell
|
|
* half the row, a widget cell the whole width — so an OpenVPN instance (6 columns) folds into
|
|
* 3 short lines instead of 6 right-flushed label:value ones.
|
|
*
|
|
* A @container, NOT the measurement the data tables use, and it must stay one: these rows are
|
|
* full of widgets that bake in a width from the layout they were rendered in, so un-collapsing
|
|
* one to take a reading CHANGES what is read — on the router the firewall zone table then
|
|
* claimed 1747px where it really needs 1190px, and overflowed its section by 557px. Do NOT
|
|
* "finish the job" by measuring it. */
|
|
@container fs-content (max-width: 960px) {
|
|
.table.cbi-section-table,
|
|
.table.cbi-section-table .tbody,
|
|
.table.cbi-section-table .td { display: block; width: auto; }
|
|
/* Hide the header in BOTH markups — a card repeats every label, so the header is a duplicate
|
|
* that only ran off the right edge. form.js wraps titles+descr in a `.thead`; the Lua CBI
|
|
* half the third-party apps still render through (luci-app-openvpn) emits a bare
|
|
* `tr.cbi-section-table-titles` with no .thead, which is why hiding only `.thead` left the
|
|
* OpenVPN header on screen. `.tr.table-titles` is deliberately NOT listed: it comes from
|
|
* L.ui.Table, which never renders a .cbi-section-table — that header is hidden by the fs-dt
|
|
* stack in theme/30-tables.css. */
|
|
.table.cbi-section-table .thead,
|
|
.table.cbi-section-table .tr.cbi-section-table-titles,
|
|
.table.cbi-section-table .tr.cbi-section-table-descr { display: none; }
|
|
/* no align-items: flex-start — stretch keeps a pair's cells (and their separators) the
|
|
* same height when one wraps; see the data card's .tr in theme/30-tables.css. */
|
|
.table.cbi-section-table .tr {
|
|
display: flex; flex-wrap: wrap; width: auto;
|
|
border: 1px solid var(--fs-border);
|
|
border-radius: var(--fs-radius-lg);
|
|
background: var(--fs-panel2);
|
|
padding: var(--fs-space-2) var(--fs-space-3-5) var(--fs-space-3);
|
|
margin-bottom: var(--fs-space-3);
|
|
}
|
|
.table.cbi-section-table .tr:hover { background: var(--fs-panel2); }
|
|
/* `.table.cbi-section-table`, not `.cbi-section-table`: the desktop cell padding is written as
|
|
* `.table.cbi-section-table .td` (theme/30-tables.css) and outranks a class-lighter selector,
|
|
* so a plainer one here silently kept the desktop padding.
|
|
*
|
|
* margin: 0 is load-bearing. The Lua CBI gives a table CELL the same `.cbi-value-field` class
|
|
* a FORM field carries, and base indents that class by 20px. A stacked cell then measured
|
|
* 50% + 20px, two no longer fit on a line, and each pair-cell wrapped to its own — which is
|
|
* why the OpenVPN row stayed one column deep. */
|
|
.table.cbi-section-table .td {
|
|
flex: 1 1 100%; min-width: 0; margin: 0;
|
|
padding-block: var(--fs-space-1-5); padding-inline: 0 var(--fs-space-2-5); border: 0; text-align: start;
|
|
/* same as the data card's cells (theme/30-tables.css): a carded cell holds opaque
|
|
* tokens (a real-length IPv6 GUA is 39 chars with no break point — colons are not
|
|
* break opportunities), and without `anywhere` one ran 156px past a 360px viewport
|
|
* on the Interfaces page. overflow-wrap inherits, so the inner spans get it too;
|
|
* white-space beats the `.nowrap` class LuCI puts on some cells. */
|
|
overflow-wrap: anywhere; white-space: normal;
|
|
}
|
|
/* A cell that only READS a value (dummy text, a flag, one button) is short, so two fit on a
|
|
* line; anything with a real widget keeps the full width (the 100% basis above) — a dropdown
|
|
* at half a phone's width is unusable. Both spellings of each widget: `CBI.DummyValue` from
|
|
* form.js, `dvalue` from the Lua CBI's typename. */
|
|
.table.cbi-section-table .td[data-widget="CBI.DummyValue"],
|
|
.table.cbi-section-table .td[data-widget="CBI.FlagValue"],
|
|
.table.cbi-section-table .td[data-widget="CBI.Button"],
|
|
.table.cbi-section-table .td[data-widget="dvalue"],
|
|
.table.cbi-section-table .td[data-widget="fvalue"],
|
|
.table.cbi-section-table .td[data-widget="button"] { flex-basis: 50%; }
|
|
/* PINNED to the data table's copy in theme/30-tables.css: the same card, needed under a CLASS
|
|
* there (.fs-stacked, measured) and under this @container here, and CSS cannot OR the two
|
|
* guards. tools/mirror.mjs holds the copies byte-identical — a duplicate detector goes QUIET
|
|
* once they differ, exactly when it should shout. */
|
|
.table.cbi-section-table .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);
|
|
text-transform: uppercase; color: var(--fs-eyebrow-color);
|
|
/* @endmirror */
|
|
}
|
|
/* The row's buttons are not a label/value pair: full width, each button stretching to
|
|
* share the line (Edit | Delete). PINNED — see above. (These six declarations were the
|
|
* data table's six in a DIFFERENT ORDER, so a grep could not tell they were one rule.) */
|
|
.table.cbi-section-table .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 */
|
|
}
|
|
.table.cbi-section-table .td.cbi-section-actions::before { content: none; }
|
|
/* flex-wrap: LuCI groups the row's buttons in ONE inner <div>, so the td's own wrap
|
|
* (above) never fires — the div is a single flex item. Four buttons are ~330px; on a
|
|
* 320px phone the carded interfaces row overflowed its section by 17px. PINNED to the
|
|
* data table's copy (theme/30-tables.css), which hit the same trap on Startup. */
|
|
.table.cbi-section-table .td.cbi-section-actions > * {
|
|
/* @mirror table-card/actions-inner */
|
|
flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
|
|
/* @endmirror */
|
|
}
|
|
.table.cbi-section-table .td .cbi-dropdown,
|
|
.table.cbi-section-table .td select,
|
|
.table.cbi-section-table .td input[type="text"] { width: 100%; min-width: 0; }
|
|
.table.cbi-section-table .td .zone-forwards { width: 100%; }
|
|
}
|
|
|
|
/* ---- widget popups must escape the table card ----
|
|
* The standalone-table card (theme/30-tables.css) clips row backgrounds to its rounded corners
|
|
* with `overflow: hidden`. But a form table (Diagnostics: three command widgets in one row) also
|
|
* holds .cbi-dropdown widgets whose open list is absolutely positioned — the clip cropped it at
|
|
* the table's edge and dragged the table's box around with it. So drop the clip where a widget
|
|
* lives; the `.cbi-dropdown[open]` z-index above lifts the list over the following (unpositioned)
|
|
* cards.
|
|
*
|
|
* Dropping it squares the frame off, though: a real <table> inherits `border-collapse: collapse`,
|
|
* which IGNORES border-radius — the card's corners came from the clip, not the border. The
|
|
* separated model restores them, and at zero spacing it lays out identically. */
|
|
.table:has(.cbi-dropdown),
|
|
.table:has(.cbi-dynlist) {
|
|
overflow: visible;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
}
|