Files
2026-11-06 23:35:05 +08:00

382 lines
18 KiB
CSS

@layer theme {
/* generic .dropdown-menu popup: nothing in luci-base emits it — coverage vocabulary, since
* stock luci-theme-bootstrap styles it and a third-party app may emit it */
.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); color: var(--fs-accent); }
/* the outer div IS the button, class = ui.js's SELECTED mode; unguarded this erased that
* variant's fill while its ink survived — measured page-on-page at 1.06-1.13:1. */
.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 (theme/55-buttons.css gives the rest theirs); painted by specificity, not source order */
.cbi-page-actions .cbi-dropdown.cbi-button-apply,
.cbi-page-actions .cbi-dropdown > .cbi-button {
background: var(--fs-accent);
border: 0;
color: var(--fs-on-accent);
}
.cbi-page-actions .cbi-dropdown > .cbi-button:hover { filter: var(--fs-hover-lift); }
/* ---- cbi-dropdown widget: the JS select used across all forms ---- */
/* machinery absorbed from base, driven by ui.js attributes; the state machine below is
* UNFLAGGED, stating openly what upstream's !important flags resolved — docs/css.md
* "Dropdown: the display state machine, unflagged" has the specificity table. */
.cbi-dropdown {
display: inline-flex;
cursor: pointer;
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: hidden;
display: flex;
width: 100%;
}
/* clears the button's own side inset so the label starts on its text, unflagged (0,3,1) over the rule above's (0,1,1) */
.cbi-dropdown.btn > ul:not(.dropdown),
.cbi-dropdown.cbi-button > ul:not(.dropdown) {
margin-block: 0;
margin-inline: 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, `.more` is none until the showers flip it — one declaration per state or the audit's same-selector shadow check fires */
.cbi-dropdown > .open,
.cbi-dropdown > .more {
flex-grow: 0;
flex-shrink: 0;
flex-direction: column;
justify-content: center;
text-align: center;
}
/* currentColor: the variant's own ink, 4.6-5.4:1 on every button fill measured; --fs-border is 1.45:1 */
.cbi-dropdown.btn > .open,
.cbi-dropdown.cbi-button > .open {
border-inline-start: 1px solid currentColor;
}
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[placeholder] {
display: none;
}
.cbi-dropdown > ul > li {
display: none;
font-size: var(--fs-type-field);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* ellipsis did nothing until min-width: 0 — a flex item's automatic minimum floors it at its
* own content width — docs/css.md "Dropdown: ellipsis needs `min-width: 0` first". */
min-width: 0;
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; }
/* a button dropdown's label is a button label, not a form field's 16px value; block, not the
* state machine's flex, since text-overflow only draws its "…" on a block container */
.cbi-dropdown:is(.btn, .cbi-button) > ul:not(.dropdown) > li[display] { display: block; font-size: inherit; }
/* 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: var(--fs-hairline);
}
.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-block: 0;
margin-inline: 0 var(--fs-space-1);
}
/* ---- the display state machine, on specificity (docs/css.md has the per-selector table) ---- */
/* a closed optional dropdown with an empty value renders EMPTY unflagged too: the placeholder
* hider out-specifies the generic shower. :not([open]) keeps this out of the open list. */
.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][more] > .more,
.cbi-dropdown[multiple][empty] > .more,
.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: a CHILD with an inline width overflows, wrapping the row cannot reach it —
* form.js's RichListValue assumes an unbounded list (issue #15). */
.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: var(--fs-hairline);
border-radius: var(--fs-radius);
color: var(--fs-text);
min-height: var(--fs-ctl-h);
/* fixed height: without it a closed dropdown grows to its tallest child (Save & Apply's
* 40px button, measured 38 -> 42px on the gallery); --fs-ctl-h, since the min-height floor always won */
height: var(--fs-ctl-h);
font-size: var(--fs-type);
transition: var(--fs-field-transition);
/* absorbed from base/30-forms.css; NOT the dynlist's numbers (240/440) — one value vs a column of them */
min-width: 13.125rem;
max-width: 25rem;
width: auto;
}
.cbi-dropdown.btn, .cbi-dropdown.cbi-button { 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 row on a BUTTON dropdown, excluded by
* :not(.btn):not(.cbi-button) below — one menu, one row, but two selectors. */
.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);
/* a menu row is not a button label: the open list sits inside the button and would inherit its 600 and 1.1 line (a plain dropdown's rows: 400, 1.5) */
font-weight: var(--fs-weight-normal); line-height: var(--fs-leading);
/* a grid step, not calc(--fs-type * --fs-leading) (19.5px) — a half-pixel box edge is what the scale exists to stop */
min-height: var(--fs-space-5);
}
/* a MULTI-select wraps items in <form><label>…</label>, which theme/60-inputs.css's `.cbi-value-field *` would hand mono — inherit keeps it the row's sans, a LABEL not 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; }
/* same ink base/30-forms.css gives `placeholder`; repeats the row rule's two selectors rather
* than a shorter form, which LOSES to the row's own color and rendered in the value's ink. */
.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[placeholder],
.cbi-dropdown > ul.dropdown > li[placeholder] { color: var(--fs-placeholder); }
/* the multiselect's inline filter/create inputs, moved from base with the generic input box (theme/60-inputs.css) */
.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%; }
/* `.open` is the only hit target that opens the list (a click elsewhere on a button dropdown
* RUNS the button). `font-size: 0` collapsed the stock line-height to 28x0, putting the
* chevron outside its own box — a real box fixes that. */
.cbi-dropdown > .open {
display: flex;
font-size: 0; line-height: 0; border: 0;
position: relative; padding: 0;
align-self: stretch;
min-width: var(--fs-btn-size); min-height: var(--fs-ctl-h-sm);
align-items: center;
cursor: pointer;
}
/* `.more` was one base rule with `.open`, split when the theme took `.open` for the chevron */
.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" */
}
/* --fs-dim reads as a smudge on the accent-filled Save & Apply; currentColor uses the variant's own ink */
.cbi-dropdown.btn > .open::after,
.cbi-dropdown.cbi-button > .open::after { border-color: currentColor; }
/* open list -> design popover */
.cbi-dropdown[open] { z-index: var(--fs-z-dropdown); }
.cbi-dropdown[open] > ul.dropdown {
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: var(--fs-hairline);
border-radius: var(--fs-radius-lg);
box-shadow: var(--fs-shadow-pop);
padding: var(--fs-space-1-5);
margin-top: var(--fs-space-1-5);
/* anchored to its widget: ui.js's touch branch assumes the viewport as containing block; !important beats the inline style */
inset-inline: 0 auto !important;
min-width: 100%;
/* desktop only: min(92vw,420px) ignores trigger's left offset, grew past viewport under 767px (113 findings) */
width: max-content;
max-width: min(92vw, 26.25rem);
}
.cbi-dropdown[open] > ul.dropdown > li {
border-radius: var(--fs-radius-sm);
border-inline-start: 0;
position: relative;
/* an OPEN row wraps; only the closed preview ellipsizes — the menu is capped at
* min(92vw, 420px), so a verbose option would otherwise decide the width of every menu */
white-space: normal; overflow-wrap: anywhere;
/* no border-bottom: it curls at the corners of a rounded box; the ::after inset strip below is the divider instead */
border-bottom: 0;
}
/* The row's own wrap above never reaches a `.zonebadge` (base/95-luci.css): `white-space: nowrap`
* there, right for a one-line pill in a table cell, wrong here where a multi-homed interface
* (RichListValue) nests two `.ifacebadge` chips in one badge with no space between them — an
* unbreakable 254-258px at 320px (`R` past the 320px viewport), overflowing
* regardless of the popup's own width. Scoped to the open list only: every other `.zonebadge`
* keeps `nowrap`. */
.cbi-dropdown[open] > ul.dropdown > li .zonebadge {
white-space: normal; overflow-wrap: anywhere;
}
.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 — docs/css.md
* "Dropdown: the open list's rail and the selected-row's contrast". */
.cbi-dropdown[open] > ul.dropdown > li[selected] {
background: var(--fs-panel2); color: var(--fs-accent); box-shadow: var(--fs-row-rail);
}
/* theme's ring, not the UA's outline:auto; inset, since the menu clips overflow, rail rides
* INSIDE it so a focused+selected row keeps both */
.cbi-dropdown[open] > ul.dropdown > li:focus-visible {
outline: none;
box-shadow: var(--fs-row-rail), inset 0 0 0 2px var(--fs-accent-soft);
}
/* MULTI-select shows "chosen" via its CHECKBOX, drops the rail; the same declarations as
* `select option:checked` in theme/60-inputs.css, which has no checkbox (kept identical by hand) */
.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: var(--fs-hairline); margin-top: var(--fs-space-1); }
/* dropdown add-item (e.g. Community Lists) — the .cbi-dropdown styles itself, 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:not(.btn):not(.cbi-button),
.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 a multi-dropdown row no longer fits, so each row cards, same shape as the
* data-table stack (theme/30-tables.css); a @container, not the tables' own measurement, since
* un-collapsing a row to measure it CHANGES what its widgets read. 992, not 960 — docs/css.md
* "Dropdown: 992px, not 960" has the padding subtraction and the regression it fixed. */
@container fs-content (max-width: 62rem) {
.table.cbi-section-table,
.table.cbi-section-table .thead,
.table.cbi-section-table .tbody,
.table.cbi-section-table .td { display: block; width: auto; }
/* a card repeats every label, so the header goes in BOTH markups (form.js's .thead children,
* the Lua CBI's bare titles/descr rows) — except the opt-in Table Filters row (25.12 tags it
* -titles too): no card repeats it, and hidden its 4 gallery controls rendered 0x0 */
.table.cbi-section-table .thead > :not(.cbi-section-table-filter),
.table.cbi-section-table .tr.cbi-section-table-titles:not(.cbi-section-table-filter),
.table.cbi-section-table .tr.cbi-section-table-descr { display: none; }
/* stretch (not flex-start) keeps a pair's cells the same height when one wraps */
.table.cbi-section-table .tr {
display: flex; flex-wrap: wrap; width: auto;
border: var(--fs-hairline);
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);
}
/* margin: 0 load-bearing: the Lua CBI gives a table CELL the .cbi-value-field class a form
* field carries, base indents it 20px — without the reset two cells no longer fit a line */
.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;
/* overflow-wrap: anywhere for opaque tokens (a real IPv6 GUA has no break point) — one
* ran 156px past a 360px viewport before this */
overflow-wrap: anywhere; white-space: normal;
}
/* the filter cells are .th, so the .td rule above never reaches them: one field per card
* line, and no row divider (.cbi-section .table .th's border-bottom) between fields */
.table.cbi-section-table .tr.cbi-section-table-filter > .th {
flex: 1 1 100%; min-width: 0; border: 0;
padding-block: var(--fs-space-1-5); padding-inline: 0 var(--fs-space-2-5);
}
/* a cell that only READS a value is short, two fit a line; a real widget keeps full width (a dropdown at half a phone's width is unusable) */
.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%; }
/* a copy of the data table's rule (theme/30-tables.css), needed under a CLASS there and
* under this @container here, and CSS cannot OR the two guards. Keep them identical by hand. */
.table.cbi-section-table .td[data-title]::before {
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);
}
/* the row's buttons are not a label/value pair: full width, sharing the line. Copy in theme/30-tables.css. */
.table.cbi-section-table .td.cbi-section-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);
}
.table.cbi-section-table .td.cbi-section-actions::before { content: none; }
/* LuCI groups the row's buttons in ONE inner div, so the td's own wrap never fires — the
* div is a single flex item. No grow: sized to its label like the ComboButton below.
* Copy in theme/30-tables.css; same trap on Startup. */
.table.cbi-section-table .td.cbi-section-actions > :not(.cbi-dropdown) {
flex: 0 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2);
}
/* width: 0 — the label's nowrap min-content (288px for a Russian item at 360px) sizes the table-row-group
* past the card (td right 442 vs 315); basis `content` still sizes the item to its label (it was 266px, a whole card, with `1 1 auto`). Copy in theme/30-tables.css. */
.table.cbi-section-table .td.cbi-section-actions > .cbi-dropdown {
flex: 0 1 content; width: 0; min-width: 0;
}
.table.cbi-section-table .td .zone-forwards { width: 100%; }
}
/* the table card (theme/30-tables.css) clips with overflow: hidden, but a .cbi-dropdown's open
* list is absolutely positioned — drop the clip, restore corners via separated borders instead */
.table:has(.cbi-dropdown),
.table:has(.cbi-dynlist) {
overflow: visible;
border-collapse: separate;
border-spacing: 0;
}
}