mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
251 lines
15 KiB
CSS
251 lines
15 KiB
CSS
@layer theme {
|
|
/* The realtime graphs (Status → Realtime *) size their drawing from #view — NOT from the box
|
|
* they draw into: `width = document.querySelector('#view').offsetWidth - 2`, in bandwidth.js,
|
|
* load.js, wireless.js and connections.js, and in the luci-app-*-status copies of them. That
|
|
* assumption only holds in a theme whose .cbi-section has no gutter (luci-theme-bootstrap's has
|
|
* none, which is why the page is whole there). Ours is a card: 16px padding + a 1px border, so
|
|
* the canvas came out 34px narrower than the drawing, and the views draw NEWEST-first from the
|
|
* right — the freshest samples were the ones falling off the edge. Measured on the router, on
|
|
* every realtime page and at every width: #view 1158 vs svg 1124. It reads as a phone bug only
|
|
* because the loss is absolute: 34px of a 1124px desktop plot is 3%, of a 322px phone plot 10%.
|
|
*
|
|
* So bleed the graph box back out to the card's border edge, making the canvas exactly
|
|
* #view - 2 again. The bleed is padding + border, and it is derived from --fs-card-pad rather
|
|
* than restated, or narrowing the gutter would silently re-clip every graph.
|
|
*
|
|
* Scoped by SHAPE, not by page: a graph is an inline-styled box wrapping an <svg>, one card
|
|
* deep. The luci-app-*-status graphs are third-party pages this theme has never seen, and they
|
|
* inherit the bug by copying the stock view. channel_analysis.js is deliberately NOT matched —
|
|
* it measures its OWN box, so it is already correct, and it hangs off a bare [data-tab] div
|
|
* rather than a .cbi-section. The !important fights the inline `width: 100%` on the box; nothing
|
|
* in a cascade layer can outrank that. */
|
|
#view .cbi-section > div[style]:has(> svg) {
|
|
width: auto !important;
|
|
margin-inline: calc(-1 * (var(--fs-card-pad) + 1px));
|
|
}
|
|
|
|
/* …and the same box's frame, which the same inline style paints BLACK: every realtime view
|
|
* writes `border:1px solid #000;background:#fff` on it (bandwidth.js, load.js, wireless.js,
|
|
* connections.js, channel_analysis.js, and every luci-app-*-status copy of them). Black is a
|
|
* colour no palette of this theme contains; on a dark surface the graph reads as a framed
|
|
* cut-out, and the frame lands exactly on the card's own border edge because of the bleed
|
|
* above. Recoloured to the hairline every other card contour uses (issue #32).
|
|
*
|
|
* WIDER than the bleed rule on purpose: channel_analysis.js hangs off a bare [data-tab] div and
|
|
* must not be bled — but its border is just as black, so the shape here asks only "an
|
|
* inline-styled box wrapping an <svg>", one `#view` deep or ten.
|
|
*
|
|
* Only the COLOUR moves. The 1px stays: it is the graph's own edge, and the plot draws to it.
|
|
* `background: #fff` is left alone deliberately — the <svg> is sized to the box's content edge
|
|
* (`#view.offsetWidth - 2`, the same arithmetic the bleed restores), so the white never paints;
|
|
* a flag against it would be a rule that changes nothing. Only an author !important outranks an
|
|
* inline declaration, and `border-color` alone is enough: the width and style already agree
|
|
* with what the theme would ask for. */
|
|
#view div[style]:has(> svg) {
|
|
border-color: var(--fs-border) !important;
|
|
}
|
|
|
|
/* WCAG 2.2 SC 1.4.1 (Use of Color, A): inside a block of text a link may not be identified
|
|
* by COLOUR ALONE — it needs a non-colour cue or 3:1 against the surrounding text, and
|
|
* --fs-accent on --fs-text measures 1.4:1 in dark mode. So: underline links in prose.
|
|
* Scoped to prose deliberately — nav links, buttons, pills and table cells are identified
|
|
* by position and shape, and underlining them would be noise. */
|
|
p a, dd a,
|
|
.cbi-value-description a,
|
|
.alert-message a:not(.btn):not(.cbi-button) {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* ==================================================================
|
|
* A <strong>/<b> is a LABEL, and a label is UI text — so it takes the UI face even on a
|
|
* monospaced surface. LuCI builds every status readout as `<strong>MAC:</strong>
|
|
* ac:1f:6b:…`: the strong names the datum, the text after it IS the datum. The theme puts
|
|
* mono on those surfaces (.cbi-value-field, data-table cells, ifacebox bodies), the strong
|
|
* inherited it, and <strong> also means weight 700 — so the label asked for a BOLD MONO
|
|
* face that exists for no other reason. Measured on the live router: 227 elements across
|
|
* seven pages, 20 KB of jetbrains-mono-600 fetched to draw them — 30% of the theme's whole
|
|
* font payload, spent on the word "MAC:". Manrope 700 is already downloaded: zero bytes.
|
|
*
|
|
* It must SET the face, not merely be excluded from the mono rule — an excluded element
|
|
* still INHERITS its parent's. That was the first attempt and it changed nothing.
|
|
*
|
|
* pre/code/#syslog/textarea are exempt: there a <strong> is part of the monospaced CONTENT
|
|
* (a highlighted config line, a log level), not a label for it, and its bold resolves onto
|
|
* the mono 400 face, which is still shipped.
|
|
* ================================================================== */
|
|
:is(strong, b):not(pre *, code *, #syslog *, textarea *) {
|
|
font-family: var(--fs-font-sans);
|
|
}
|
|
|
|
/* Same argument, the last two holdouts — found by measuring:
|
|
* .ifacebadge is a BADGE — chrome, not a value. It sits in a mono cell and
|
|
* .ifacebadge-active makes it bold, so the four active firewall zone badges were the last
|
|
* thing on the router asking for a bold mono face. Sans keeps the active/inactive weight cue
|
|
* (WCAG 1.4.1 — that distinction is not carried by colour) with faces the page already has.
|
|
* <code>/<pre> hold a LITERAL, which does not inherit its container's emphasis:
|
|
* `<code>/etc/ethers</code>` inside a .cbi-value-title (600) was asking for bold mono.
|
|
*
|
|
* The second selector is not redundant: the mono rule in theme/60-inputs.css is
|
|
* `.cbi-value-field *:where(…)` = (0,1,0), a TIE with a bare `.ifacebadge` — and a tie is
|
|
* settled by SOURCE ORDER, which 60-inputs would win. Win on specificity (0,2,0) instead. */
|
|
.ifacebadge,
|
|
.cbi-value-field .ifacebadge { font-family: var(--fs-font-sans); }
|
|
code, pre { font-weight: var(--fs-weight-normal); }
|
|
|
|
/* ---- THE POLL'S `fade-in` IS NOT ANIMATED AT ALL, AND THAT IS THE WHOLE RULE ----
|
|
*
|
|
* `fade-in` is upstream's class and luci-mod-status re-marks with it on EVERY poll tick — each
|
|
* section, and the `div.includes` wrapper that holds all of them. Anything this theme hangs on
|
|
* that class therefore replays once a second across the entire content column.
|
|
*
|
|
* It was a fade plus a 4px slide, which moved the page under the reader; the slide came off
|
|
* first, and the fade alone still pulsed the whole column once a second — reported from an
|
|
* iPhone as shaking that happens WITHOUT SCROLLING AT ALL, which is what finally named it: a
|
|
* poll tick is the only thing that happens on a still page. The stock themes animate nothing
|
|
* here, which is why the same router and the same poll are calm under luci-theme-bootstrap.
|
|
*
|
|
* So the theme animates nothing on it either. The class stays in the markup (it is upstream's
|
|
* and other rules may key on it); what is gone is the animation this file used to attach. The
|
|
* `fade-in` keyframes in base/95-luci.css stay for the same reason they always did — the NAME is
|
|
* upstream's and an app may use it. */
|
|
.cbi-title-ref { color: var(--fs-accent); }
|
|
.close { color: var(--fs-text); text-shadow: none; opacity: .6; }
|
|
.close:hover { opacity: 1; }
|
|
.ifacebox { background: var(--fs-panel); border-color: var(--fs-border); }
|
|
.ifacebox .ifacebox-head { background: var(--fs-panel2); color: var(--fs-text); }
|
|
/* The active-interface highlight lives in THIS layer, next to the head it overrides: the
|
|
* same rule in base was dead — a layer beats specificity, and the plain `.ifacebox-head`
|
|
* above repainted `.active` along with the rest. */
|
|
.ifacebox .ifacebox-head.active { background: var(--fs-accent); color: var(--fs-on-accent); }
|
|
.ifacebadge { background: var(--fs-panel2); border-color: var(--fs-border); }
|
|
.ifacebadge-active { border-color: var(--fs-accent); }
|
|
.zonebadge-empty { border-color: var(--fs-border); color: var(--fs-dim); }
|
|
|
|
/* ==================================================================
|
|
* The @container PLUMBING for the responsive tables — only WHICH boxes are query containers,
|
|
* under what names. The card rules are NOT here: a CBI section table cards under `@container
|
|
* fs-content (max-width: 960px)` (theme/65-dropdown.css), a data table cards by MEASUREMENT
|
|
* (.fs-stacked, fs-select.js) at any width — which is why no data-table threshold appears in
|
|
* this file.
|
|
*
|
|
* Query the CONTENT width, not the viewport: the sidebar eats width, so a viewport breakpoint
|
|
* would stack too late (or too early) depending on layout.
|
|
*
|
|
* NAMED, deliberately — an unnamed @container binds to the nearest ancestor that happens to
|
|
* have a container-type, so giving container-type to any new component would silently
|
|
* re-target every unnamed query underneath it.
|
|
* fs-view the overview's grid (pages/20-overview.css, @container 800)
|
|
* fs-content "whatever content column I am in" — carried by BOTH .cbi-map and #view, so a
|
|
* query resolves to the NEAREST of the two. That is the point of sharing it.
|
|
*
|
|
* ONLY NAMES A QUERY ASKS FOR. `.cbi-map` also carried `fs-map`, for queries that were never
|
|
* written — and a name in this list reads as an established seam, so the next rule that wants
|
|
* the form's column would have been written against a container the sheet does not really
|
|
* publish. The containment on `.cbi-map` stays: it is what makes `fs-content` resolve to the
|
|
* form's own column rather than to `#view`.
|
|
* ================================================================== */
|
|
.cbi-map { container-type: inline-size; container-name: fs-content; }
|
|
#view { container-type: inline-size; container-name: fs-view fs-content; }
|
|
|
|
/* A modal is NOT inside #view — ui.js appends #modal_overlay to <body> — so every
|
|
* @container query above resolved to nothing in there and no table stacked: on a phone the
|
|
* Edit-interface dialog rendered its form at desktop widths and cut the inputs off at the
|
|
* dialog's right edge. Carry BOTH names so a dialog stacks its .cbi-section-table
|
|
* (fs-content) and its data tables (fs-view) exactly like the page — including any
|
|
* third-party app's modal. */
|
|
.modal { container-type: inline-size; container-name: fs-view fs-content; }
|
|
|
|
/* pager (« prev · text · next ») — generic, not page-scoped: any .pager gets an inline
|
|
* centred row with compact square buttons. */
|
|
.pager.center { display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; justify-content: center; gap: var(--fs-space-2-5); padding: var(--fs-space-3-5) 0; }
|
|
.pager .text { flex: 0 0 auto; font-size: var(--fs-type); color: var(--fs-dim); }
|
|
|
|
/* The heading ramp — absorbed from base wholesale (docs/css.md, the "native elements and
|
|
* typography" category). Stock LuCI hands h2 to a page title and h3/legend to a section
|
|
* title, and the heading-card rule below + theme/30-tables.css repaint exactly those two —
|
|
* so most of the ramp is only ever rendered by a third-party luci-app-*; it is styled
|
|
* anyway, coverage is a contract. Every step is a scale token and the ramp is monotonic.
|
|
* `legend` is deliberately NOT in the group: theme/30-tables.css owns legend (and carries
|
|
* the ramp's ink), and a member here would fight it at equal specificity, i.e. on source
|
|
* order — the one thing a cascade must not rest on. */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
font-weight: var(--fs-weight-bold);
|
|
color: var(--fs-text);
|
|
/* 1.5 is prose leading; a heading is a label and looks slack at it. */
|
|
line-height: var(--fs-leading-tight);
|
|
}
|
|
h1 { margin-bottom: var(--fs-space-4); font-size: var(--fs-type-2xl); }
|
|
h2 { font-size: var(--fs-type-xl); }
|
|
h3 { font-size: var(--fs-type-lg); }
|
|
h4 { font-size: var(--fs-type); }
|
|
h5 { font-size: var(--fs-type); }
|
|
h6 { font-size: var(--fs-type-xs); color: var(--fs-dim); text-transform: uppercase; }
|
|
|
|
/* Heading card (h2 + .cbi-map-descr): everything sitting directly on the page background
|
|
* gets a .cbi-section card's frame, so a page reads as a stack of panels instead of loose
|
|
* text floating over the (possibly patterned) background. */
|
|
.cbi-map > h2,
|
|
#view > h2 {
|
|
background: var(--fs-panel);
|
|
border: var(--fs-hairline);
|
|
border-radius: var(--fs-radius-lg);
|
|
box-shadow: var(--fs-shadow);
|
|
margin: 0 0 var(--fs-card-gap);
|
|
padding: var(--fs-space-4);
|
|
font-size: var(--fs-type-xl);
|
|
font-weight: var(--fs-weight-bold);
|
|
color: var(--fs-text);
|
|
}
|
|
/* a heading followed by a description: split the card across the two siblings (no wrapper
|
|
* element exists to hold both) */
|
|
.cbi-map > h2:has(+ .cbi-map-descr),
|
|
#view > h2:has(+ .cbi-map-descr) {
|
|
border-bottom: 0;
|
|
border-radius: var(--fs-radius-lg) var(--fs-radius-lg) 0 0;
|
|
padding-bottom: var(--fs-space-1);
|
|
margin-bottom: 0;
|
|
box-shadow: none;
|
|
}
|
|
.cbi-map > h2 + .cbi-map-descr,
|
|
#view > h2 + .cbi-map-descr {
|
|
background: var(--fs-panel);
|
|
border: var(--fs-hairline);
|
|
border-top: 0;
|
|
border-radius: 0 0 var(--fs-radius-lg) var(--fs-radius-lg);
|
|
box-shadow: var(--fs-shadow);
|
|
margin: 0 0 var(--fs-card-gap);
|
|
padding: 0 var(--fs-space-4) var(--fs-space-4);
|
|
color: var(--fs-dim);
|
|
font-size: var(--fs-type);
|
|
line-height: var(--fs-leading);
|
|
}
|
|
|
|
/* An action-button row (a `.right` wrapper holding Add/Save) sitting directly under a field
|
|
* had no gap above it — the apk "Repo Public Keys -> Add key" paste box, and every other
|
|
* paste-a-value-then-submit layout, pinned its button to the textarea. Scoped to a `.right`
|
|
* immediately following a form control, so it never touches right-aligned text or cells. */
|
|
#view :is(textarea, input[type="text"], input[type="password"], .cbi-dropdown) + .right {
|
|
margin-top: var(--fs-space-2-5);
|
|
}
|
|
|
|
/* A button row belonging to the PAGE TITLE rather than to any section (`.cbi-title-section >
|
|
* .cbi-title-buttons`; Status → Channel Analysis puts "Refresh Channels" there, a
|
|
* third-party app may put anything). Nothing gave it a bottom margin, so it sat flush
|
|
* against the first .cbi-section below and read as part of that card instead of as a
|
|
* page-level action. Same 16px every .cbi-section already uses.
|
|
*
|
|
* And a TOP margin for the same reason at the other edge: the row sits directly under the page
|
|
* heading with nothing between them — measured at 0px on Status → Channel Analysis, where
|
|
* "Refresh Channels" touched the "Channel Analysis" title (reported upstream against v0.14).
|
|
* Half the bottom gap, because a heading and its own action belong together; the full one would
|
|
* read as a separate block. */
|
|
.cbi-title-buttons { margin-block: var(--fs-space-2) var(--fs-card-gap); }
|
|
|
|
/* The per-section Delete button (`.cbi-section-remove`, what luci-base renders for every NAMED
|
|
* section: SQM's queues, the firewall's zones, any app that lets you add and drop instances) sat
|
|
* flush against whatever follows it — usually that section's own tab bar. Reported upstream on
|
|
* SQM, but nothing here is about SQM: it is one class luci-base gives every such section, and
|
|
* the gap belongs to the pattern rather than to the page. */
|
|
.cbi-section-remove { margin-bottom: var(--fs-space-2); }
|
|
}
|