mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-29 05:25:13 +08:00
631 lines
17 KiB
CSS
631 lines
17 KiB
CSS
@layer base {
|
|
/* Widgets with no generic equivalent: CBI section tables, interface/zone badges,
|
|
* the uci change dialog, spinners, the file browser, and a few rules luci-mod-*
|
|
* depends on. */
|
|
/* .hidden: theme/60-inputs.css (with the .cbi-* variants LuCI emits) */
|
|
form.inline { display: inline; margin-bottom: 0; }
|
|
|
|
#modemenu li:last-child span.divider { display: none }
|
|
|
|
/* #syslog: absorbed into pages/70-syslog.css — in base it LOST to the theme layer's generic
|
|
* form-field box (a layer beats specificity), so the log rendered as a 210px column. */
|
|
|
|
/* :not(.placeholder): the empty-table row gets no hover cue (theme/30-tables.css spares it),
|
|
* but this rule paints the CELL and a placeholder row carrying no `.cbi-section-table-row`
|
|
* class escapes that guard — measured on UPnP (issue #15). */
|
|
.cbi-section-table .tr:not(.placeholder):hover .td,
|
|
.cbi-section-table .tr:not(.placeholder):hover .th,
|
|
.cbi-section-table .tr:not(.placeholder):hover::before {
|
|
background-color: var(--fs-panel2);
|
|
}
|
|
|
|
.cbi-section-table .tr.cbi-section-table-descr .th {
|
|
font-weight: var(--fs-weight-normal);
|
|
}
|
|
|
|
.cbi-section-table-titles {
|
|
font-weight: var(--fs-weight-bold);
|
|
}
|
|
|
|
/* GridSection name column, discriminated by markup not release: docs/css.md "base/95-luci.css:
|
|
* the GridSection name column, read from the markup, not the release" (issue #9). Column
|
|
* geometry lives in theme/30-tables.css, next to the real cells it lines up with. */
|
|
.cbi-section-table-titles.named[data-title]::before,
|
|
.cbi-section-table-titles.named[data-title] ~ .cbi-section-table-descr.named::before {
|
|
content: attr(data-title) " ";
|
|
display: table-cell;
|
|
vertical-align: middle;
|
|
}
|
|
.cbi-section-table-row[data-title]:not(:has(> .cbi-section-table-titles))::before {
|
|
content: attr(data-title) " ";
|
|
display: table-cell;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.left { text-align: left !important; }
|
|
.right { text-align: right !important; }
|
|
.center { text-align: center !important; }
|
|
.top { vertical-align: top !important; }
|
|
.middle { vertical-align: middle !important; }
|
|
.bottom { vertical-align: bottom !important; }
|
|
|
|
table table td,
|
|
.cbi-value-field table td {
|
|
border: none;
|
|
}
|
|
|
|
/* cell-filling field widths (+ numeric-cell refinement): theme/30-tables.css —
|
|
* absorbed with the generic field box, which would out-layer them here */
|
|
|
|
.table.cbi-section-table .td.cbi-section-table-cell {
|
|
white-space: nowrap;
|
|
text-align: end;
|
|
}
|
|
|
|
.td.cbi-section-actions {
|
|
text-align: end;
|
|
vertical-align: middle;
|
|
width: 15%;
|
|
}
|
|
|
|
.td.cbi-section-actions > * {
|
|
display: flex;
|
|
}
|
|
|
|
.td.cbi-section-actions > :not(.cbi-dropdown) > *,
|
|
.td.cbi-section-actions > * > form > * {
|
|
flex: 1 1 4em;
|
|
margin: 0 var(--fs-space-1); /* WCAG 2.2 SC 2.5.8: 1px apart, targets sat inside each other */
|
|
}
|
|
|
|
.td.cbi-section-actions > * > form {
|
|
display: inline-flex;
|
|
margin: 0;
|
|
}
|
|
|
|
.table.valign-middle .td {
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.cbi-rowstyle-2,
|
|
.tr.table-titles,
|
|
.tr.cbi-section-table-titles {
|
|
background: var(--fs-panel2);
|
|
}
|
|
|
|
th[data-sortable-row] { cursor: pointer; }
|
|
th[data-sort-direction="asc"]::after { content: "\a0\25b2"; }
|
|
th[data-sort-direction="desc"]::after { content: "\a0\25bc"; }
|
|
|
|
/* Indent and the ::before glyph's own `left` are ONE relationship (both 1.25em, stated twice
|
|
* before) — now one local. */
|
|
.cbi-value-description {
|
|
--help-indent: 1.25em;
|
|
|
|
margin: var(--fs-space-1) 0 0 var(--help-indent);
|
|
position: relative;
|
|
}
|
|
|
|
.cbi-value-description:not(:empty)::before {
|
|
/* OURS, drawn as a stroke like --spinner-icon below and --fs-icon-refresh in 02-tokens.css
|
|
* (fill:none, round caps, 24 grid) — not the solid filled path luci-theme-bootstrap used. */
|
|
--help-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='9.4'/><path d='M8.9 9.4a3.2 3.2 0 1 1 3.9 3.6v1.4'/><path d='M12.8 17.6h-.02'/></svg>");
|
|
|
|
position: absolute;
|
|
inset-inline-start: calc(var(--help-indent) * -1);
|
|
content: "\a0";
|
|
display: inline-block;
|
|
width: 1em;
|
|
height: 1em;
|
|
margin-inline-end: var(--fs-space-1);
|
|
background: var(--fs-accent);
|
|
mask-image: var(--help-icon);
|
|
mask-size: cover;
|
|
-webkit-mask-image: var(--help-icon);
|
|
-webkit-mask-size: cover;
|
|
}
|
|
|
|
/* tint (6%-not-12% reasoning): theme/30-tables.css — absorbed */
|
|
.cbi-section-error {
|
|
border: 1px solid var(--fs-danger);
|
|
border-radius: var(--fs-radius);
|
|
padding: var(--fs-space-2);
|
|
margin-bottom: var(--fs-space-4);
|
|
}
|
|
|
|
.cbi-section-error ul { margin: 0 0 0 var(--fs-space-5); }
|
|
|
|
.cbi-section-error ul li {
|
|
font-weight: var(--fs-weight-bold); /* colour: theme/35-alerts.css — absorbed */
|
|
}
|
|
|
|
.ifacebox {
|
|
border: var(--fs-hairline);
|
|
margin: 0 var(--fs-space-3);
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
border-radius: var(--fs-radius-sm);
|
|
display: inline-flex;
|
|
flex-direction: column;
|
|
line-height: var(--fs-leading-tight); /* a plate of single-line labels, not prose */
|
|
min-width: 100px;
|
|
}
|
|
|
|
.ifacebox .ifacebox-head {
|
|
border-bottom: var(--fs-hairline);
|
|
/* inside the box's 1px border, so radius = the box's less that border */
|
|
border-radius: calc(var(--fs-radius-sm) - 1px) calc(var(--fs-radius-sm) - 1px) 0 0;
|
|
padding: var(--fs-space-0-5);
|
|
}
|
|
|
|
/* !important fights luci-mod-network's inline zone-colour style=: docs/css.md "base/95-luci.css:
|
|
* the dark-mode zone tint, and why it has no grey fallback". */
|
|
[data-darkmode="true"] .zonebadge[style],
|
|
[data-darkmode="true"] .ifacebox-head[style] {
|
|
background: linear-gradient(rgba(var(--zone-color-rgb), .4), rgba(var(--zone-color-rgb), .3)) !important;
|
|
}
|
|
|
|
/* generalised once: docs/css.md "base/95-luci.css: the overflow-wrap fix generalised once, not
|
|
* restated per page". */
|
|
.ifacebox .ifacebox-body {
|
|
padding: var(--fs-space-1);
|
|
white-space: normal;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
#cbi-network-interface .ifacebox img {
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
.ifacebadge {
|
|
display: inline-block;
|
|
flex-direction: row;
|
|
border: var(--fs-hairline);
|
|
padding: var(--fs-space-0-5);
|
|
color: var(--fs-text);
|
|
border-radius: var(--fs-radius-sm);
|
|
cursor: default;
|
|
line-height: var(--fs-leading-tight);
|
|
}
|
|
|
|
.ifacebadge img {
|
|
width: 32px;
|
|
height: 32px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.ifacebadge-active {
|
|
font-weight: var(--fs-weight-bold);
|
|
}
|
|
|
|
.network-status-table {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.network-status-table .ifacebox {
|
|
margin: var(--fs-space-2);
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.network-status-table .ifacebox-body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
text-align: start;
|
|
}
|
|
|
|
/* same fix as .ifacebox-head above, same doc section (RU caption "Подключение IPv4
|
|
* (внешняя сеть)" measured 41.3px past #view at 320px/Large, 0px in English). */
|
|
.network-status-table .ifacebox-head {
|
|
white-space: normal;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.network-status-table .ifacebox-body > * {
|
|
margin: var(--fs-space-1);
|
|
}
|
|
|
|
.network-status-table .ifacebox-body > span {
|
|
flex: 10 10 auto;
|
|
height: 100%;
|
|
}
|
|
|
|
.network-status-table .ifacebox-body > div {
|
|
margin: -.125em;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
#dsl_status_table .ifacebox-body span > strong {
|
|
display: inline-block;
|
|
min-width: 35%;
|
|
}
|
|
|
|
.ifacebadge.large,
|
|
.network-status-table .ifacebox-body .ifacebadge {
|
|
flex: 1;
|
|
padding: var(--fs-space-1);
|
|
min-width: 220px;
|
|
margin: var(--fs-space-1);
|
|
}
|
|
|
|
.network-status-table .ifacebox-body .ifacebadge {
|
|
display: flex;
|
|
}
|
|
|
|
.ifacebadge.large {
|
|
display: inline-flex;
|
|
}
|
|
|
|
.network-status-table .ifacebox-body .ifacebadge > span {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.ifacebadge > *,
|
|
.ifacebadge.large > * {
|
|
margin: 0 var(--fs-space-1);
|
|
}
|
|
|
|
.zonebadge {
|
|
padding: var(--fs-space-0-5);
|
|
border-radius: var(--fs-radius-sm);
|
|
display: inline-block;
|
|
white-space: nowrap;
|
|
color: var(--fs-text);
|
|
}
|
|
|
|
.zonebadge > em,
|
|
.zonebadge > strong {
|
|
margin: 0 var(--fs-space-1);
|
|
display: inline-block;
|
|
}
|
|
|
|
/* .zonebadge input width: theme/30-tables.css — absorbed with the generic box */
|
|
|
|
.zonebadge > .ifacebadge {
|
|
margin-inline-start: var(--fs-space-0-5);
|
|
}
|
|
|
|
.zonebadge-empty {
|
|
border: 1px dashed var(--fs-border); /* width/style only — colour + ink: theme/45-misc.css */
|
|
font-style: italic;
|
|
font-size: smaller;
|
|
}
|
|
|
|
div.cbi-value var,
|
|
.td.cbi-value-field var {
|
|
font-style: italic; /* colour: theme/60-inputs.css — absorbed */
|
|
}
|
|
|
|
div.cbi-value var[data-tooltip],
|
|
.td.cbi-value-field var[data-tooltip],
|
|
div.cbi-value var.cbi-tooltip-container,
|
|
.td.cbi-value-field var.cbi-tooltip-container {
|
|
cursor: help;
|
|
border-bottom: 1px dotted var(--fs-accent);
|
|
}
|
|
|
|
div.cbi-value var.cbi-tooltip-container,
|
|
.td.cbi-value-field var.cbi-tooltip-container .cbi-tooltip {
|
|
font-style: normal;
|
|
white-space: normal;
|
|
color: var(--fs-text);
|
|
}
|
|
|
|
#modal_overlay > .modal.uci-dialog,
|
|
#modal_overlay > .modal.cbi-modal {
|
|
max-width: 900px;
|
|
}
|
|
|
|
#modal_overlay > .modal.login > button.important {
|
|
/* Coverage vocabulary, not a live control: no luci-base call site passes ui.showModal a
|
|
* `login` class, and this theme's own login is a server-rendered form. Inherited from
|
|
* luci-theme-bootstrap so a foreign app emitting it still gets a themed result. */
|
|
font-size: var(--fs-type-lg);
|
|
margin-top: var(--fs-space-2);
|
|
width: 100%;
|
|
}
|
|
|
|
.uci-change-list {
|
|
line-height: var(--fs-leading); /* prose leading for a list of changed settings, not a hand 170% */
|
|
white-space: pre;
|
|
}
|
|
|
|
.uci-dialog div > del,
|
|
.uci-dialog div > ins,
|
|
.uci-dialog div > var {
|
|
margin-bottom: var(--fs-space-0-5);
|
|
border: var(--fs-hairline);
|
|
line-height: var(--fs-leading-tight); /* one row of single-line code takes the tight step */
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
padding: var(--fs-space-0-5);
|
|
position: relative;
|
|
background-color: var(--fs-panel2);
|
|
}
|
|
|
|
.uci-dialog div > ins {
|
|
background-color: var(--fs-good-fill);
|
|
border-color: var(--fs-good-line);
|
|
}
|
|
|
|
.uci-dialog div > del {
|
|
background-color: var(--fs-danger-fill);
|
|
border-color: var(--fs-danger-line);
|
|
}
|
|
|
|
.uci-dialog var > ins {
|
|
background-color: var(--fs-good-fill);
|
|
}
|
|
|
|
.uci-dialog var > del {
|
|
background-color: var(--fs-danger-fill);
|
|
}
|
|
|
|
.uci-change-list > var > * {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.uci-dialog del,
|
|
.uci-dialog ins,
|
|
.uci-dialog var {
|
|
text-decoration: none;
|
|
font-family: var(--fs-font-mono);
|
|
font-style: normal;
|
|
color: var(--fs-text);
|
|
display: block;
|
|
}
|
|
|
|
.uci-change-legend {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.uci-change-legend-label {
|
|
flex-basis: 150px;
|
|
margin: var(--fs-space-0-5);
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.uci-change-legend-label > ins,
|
|
.uci-change-legend-label > del,
|
|
.uci-change-legend-label > var {
|
|
margin-inline-end: var(--fs-space-1);
|
|
width: 16px;
|
|
height: 16px;
|
|
display: flex;
|
|
}
|
|
|
|
.uci-change-legend-label > * > * {
|
|
flex-basis: 100%;
|
|
padding: 1px;
|
|
}
|
|
|
|
#applyreboot-section {
|
|
line-height: 300%;
|
|
}
|
|
|
|
@keyframes flash {
|
|
0% { opacity: 1; }
|
|
50% { opacity: .5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
.flash {
|
|
animation: flash var(--fs-dur-fade);
|
|
}
|
|
|
|
#view > div.spinning:first-child {
|
|
display: table;
|
|
margin: 15vh auto;
|
|
}
|
|
|
|
/* Rotate in CSS, never a SMIL <animateTransform> inside the data-URI: that runs in the SVG's
|
|
* own document, out of reach of the page's `prefers-reduced-motion`. In-flow glyph, not
|
|
* absolutely positioned: docs/css.md "base/95-luci.css: the spinner glyph sits in the flow, on
|
|
* purpose" (issues #15, #22). */
|
|
.spinning::before {
|
|
--spinner-icon: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' stroke='%23000' stroke-width='3' fill='none'><circle cx='10' cy='10' r='8' stroke-opacity='.5'/><path d='M10 2c4 0 8 4 8 8'/></svg>");
|
|
|
|
display: inline-block;
|
|
flex: 0 0 auto;
|
|
vertical-align: middle;
|
|
margin-inline-end: var(--fs-space-1);
|
|
width: var(--fs-space-5); /* one reader, so the size lives here rather than behind a token */
|
|
height: var(--fs-space-5);
|
|
content: " ";
|
|
background: var(--on-color, #000);
|
|
mask: var(--spinner-icon) center/cover no-repeat;
|
|
-webkit-mask: var(--spinner-icon) center/cover no-repeat;
|
|
/* two deliberate exceptions to the motion scale (02-tokens.css): a LOOP, not a state change
|
|
* — 1s is a spinner period not one of the four durations, and `linear` avoids the stutter
|
|
* `ease` gives an infinite rotation at each turn's seam. */
|
|
animation: fs-spin 1s linear infinite;
|
|
}
|
|
|
|
/* One user (.spinning::before above) — the rail's poll glyph is deliberately STATIC
|
|
* (theme/20-shell.css). Unscoped: keyframe names resolve document-wide, a luci-app-* may ride it. */
|
|
@keyframes fs-spin { to { transform: rotate(360deg); } }
|
|
|
|
[data-darkmode="true"] .spinning::before {
|
|
background: var(--on-color, #fff);
|
|
}
|
|
|
|
[data-tab-title] {
|
|
height: 0;
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* the revealed state of both collapsibles — tab pane and file browser — in one rule */
|
|
[data-tab-active="true"],
|
|
.cbi-filebrowser.open {
|
|
opacity: 1;
|
|
height: auto;
|
|
overflow: visible;
|
|
transition: opacity var(--fs-dur-fade);
|
|
}
|
|
|
|
/* min(210px, 100%) floor: docs/css.md "base/95-luci.css: the file browser's floor, and the row
|
|
* that is not text". */
|
|
.cbi-filebrowser {
|
|
min-width: min(210px, 100%);
|
|
max-width: 100%;
|
|
border: var(--fs-hairline); /* width/style only; radius: theme/60-inputs.css — absorbed */
|
|
display: flex;
|
|
flex-direction: column;
|
|
opacity: 0;
|
|
height: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* padding: theme/60-inputs.css — absorbed. Zero side padding here let a 36px themed button
|
|
* touch this element's own clip box and lose its corners. */
|
|
.cbi-filebrowser > * {
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin: .25em .25em 0 .25em;
|
|
white-space: nowrap;
|
|
border-bottom: var(--fs-hairline);
|
|
}
|
|
|
|
.cbi-filebrowser .cbi-button-positive {
|
|
margin-inline-end: .25em;
|
|
}
|
|
|
|
/* .right is not one line of text (upload strip + Create/Cancel), so it opts out of the
|
|
* nowrap+ellipsis assumption above: docs/css.md "base/95-luci.css: the file browser's floor,
|
|
* and the row that is not text". */
|
|
.cbi-filebrowser > .right {
|
|
white-space: normal;
|
|
overflow: visible;
|
|
}
|
|
|
|
.cbi-filebrowser > div {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.cbi-filebrowser > ul > li {
|
|
display: flex;
|
|
flex-direction: row;
|
|
}
|
|
|
|
/* row hover surface + flex sizing: theme/60-inputs.css — absorbed (this file's old
|
|
* name:10/actions:3 split fitted base's ~20px buttons, not the theme's 36px ones). */
|
|
.cbi-filebrowser > ul > li > div:first-child {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.cbi-filebrowser > ul > li > div:last-child {
|
|
text-align: end;
|
|
}
|
|
|
|
.cbi-filebrowser > ul > li > div:last-child > button {
|
|
padding: .125em .25em;
|
|
}
|
|
|
|
.cbi-filebrowser .upload {
|
|
display: flex;
|
|
flex-direction: row;
|
|
flex-wrap: wrap;
|
|
margin: 0 -.125em .25em -.125em;
|
|
padding: 0 0 .125em 0;
|
|
border-bottom: var(--fs-hairline);
|
|
}
|
|
|
|
.cbi-filebrowser .upload > * {
|
|
margin: .125em;
|
|
flex: 1;
|
|
}
|
|
|
|
.cbi-filebrowser .upload > .btn {
|
|
flex-basis: 60px;
|
|
}
|
|
|
|
.cbi-filebrowser .upload > div {
|
|
flex: 10;
|
|
min-width: 150px;
|
|
}
|
|
|
|
/* the upload input's width: theme/60-inputs.css — absorbed with the generic box */
|
|
|
|
@keyframes fade-in {
|
|
0% { opacity: 0; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
@keyframes fade-out {
|
|
0% { opacity: 1; }
|
|
100% { opacity: 0; }
|
|
}
|
|
|
|
/* `fade-in` stays although nothing here rides it: an upstream animation NAME a luci-app-* may
|
|
* ride (coverage contract, docs/conventions.md). The theme's own `.fade-in` CLASS rule is gone
|
|
* on purpose — luci-mod-status re-marks it on every poll tick, replaying anything hung there.
|
|
* `fs-fade` is one-shot instead, played once by the rail flyout and the search overlay. */
|
|
@keyframes fs-fade {
|
|
from { opacity: 0; transform: translateY(4px); }
|
|
to { opacity: 1; transform: none; }
|
|
}
|
|
|
|
.fade-out {
|
|
animation: fade-out var(--fs-dur-fade);
|
|
}
|
|
|
|
.assoclist .ifacebadge {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
white-space: normal;
|
|
text-align: center;
|
|
}
|
|
|
|
.assoclist .ifacebadge > img {
|
|
margin: var(--fs-space-1);
|
|
}
|
|
|
|
.assoclist .td:nth-of-type(3),
|
|
.assoclist .td:nth-of-type(5) {
|
|
width: 25%;
|
|
}
|
|
|
|
.assoclist .td:nth-of-type(6) button {
|
|
word-break: normal;
|
|
}
|
|
|
|
/* PNGs on a white ground, inverted for dark mode: docs/css.md "base/95-luci.css: Statistics
|
|
* graphs and Realtime SVGs are art drawn for a white ground" — 180deg is exact for the three
|
|
* primaries, not tuned further. */
|
|
[data-darkmode="true"] [data-page="admin-statistics-graphs"] [data-plugin] img {
|
|
filter: invert(100%) hue-rotate(180deg);
|
|
}
|
|
|
|
/* Realtime SVGs carry inline style= for a black ground; only an author !important beats it:
|
|
* same doc section. */
|
|
#view div[style] > svg {
|
|
background-color: var(--fs-panel);
|
|
}
|
|
|
|
#view div[style] > svg line[style] {
|
|
stroke: var(--fs-text) !important;
|
|
}
|
|
|
|
/* child combinator, not descendant: reaches only the axis labels, not Channel Analysis's
|
|
* per-BSSID curve labels nested inside a <g> — same doc section. */
|
|
#view div[style] > svg > text[style] {
|
|
fill: var(--fs-text) !important;
|
|
text-shadow: none !important;
|
|
font-weight: var(--fs-weight-bold);
|
|
}
|
|
|
|
/* Overview Hide/Show toggle pills: theme/35-alerts.css (.cbi-title .label) +
|
|
* pages/20-overview.css (chevron variant) — absorbed. */
|
|
}
|