@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-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 ---- */ /* 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; 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, 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][open] > ul.dropdown > li, .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: 210px; max-width: 400px; 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
, 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-start: 0 !important; inset-inline-end: 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, 420px); } .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] { /* @mirror selected-row/paint */ background: var(--fs-panel2); color: var(--fs-accent); box-shadow: var(--fs-row-rail); /* @endmirror */ } /* 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; pinned byte-identical to * `select option:checked` in theme/60-inputs.css (tools/mirror.mjs), which has no checkbox */ .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); } .cbi-dropdown .create-item-input { background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius); color: var(--fs-text); } /* 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: 992px) { .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); } .table.cbi-section-table .tr:hover { background: var(--fs-panel2); } /* 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%; } /* PINNED to the data table's copy (theme/30-tables.css, tools/mirror.mjs): needed under a * CLASS there and under this @container here, and CSS cannot OR the two guards. */ .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); color: var(--fs-eyebrow-color); /* @endmirror */ } /* the row's buttons are not a label/value pair: full width, sharing the line. PINNED — see above. */ .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; } /* 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. PINNED to the data table's copy, same trap on Startup. */ .table.cbi-section-table .td.cbi-section-actions > :not(.cbi-dropdown) { /* @mirror table-card/actions-inner */ flex: 1 1 auto; display: flex; flex-wrap: wrap; gap: var(--fs-space-2); /* @endmirror */ } /* 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`). PINNED. */ .table.cbi-section-table .td.cbi-section-actions > .cbi-dropdown { /* @mirror table-card/actions-dropdown */ flex: 0 1 content; width: 0; min-width: 0; /* @endmirror */ } .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; } }