mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 06:55:13 +08:00
🦄 Sync 2026-09-06 03:35:37
This commit is contained in:
1 parent
dbdd70b00d
commit
8ca86b3a5e
14 files changed
+516
-44
No files matched your search
@@ -16,7 +16,7 @@ start() {
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
local listen_port root_path root_name
|
||||
config_get listen_port "config" "listen_port" "8989"
|
||||
config_get listen_port "config" "listen_port" "8787"
|
||||
config_get root_path "config" "root_path" "/"
|
||||
|
||||
# 路径为 / 时 name 设为 root,否则留空
|
||||
@@ -43,9 +43,9 @@ userDefaults:
|
||||
EOF
|
||||
else
|
||||
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
|
||||
sed -e "s/.*port:.*/ port: $listen_port/" \
|
||||
-e "s,.*- path:.*, - path: \"$root_path\"," \
|
||||
-e "s/.*name:.*/ name: \"$root_name\"/" \
|
||||
sed -e "s/ port:.*/ port: $listen_port/" \
|
||||
-e "s, - path:.*, - path: \"$root_path\"," \
|
||||
-e "s/ name: \"root\"/ name: \"$root_name\"/" \
|
||||
-i "$CONF_PATH"
|
||||
fi
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ return view.extend({
|
||||
|
||||
o = s.option(form.Value, 'listen_port', _('Listen port'));
|
||||
o.datatype = 'port';
|
||||
o.default = '8989';
|
||||
o.default = '8787';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'root_path', _('Root directory'));
|
||||
|
||||
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=60
|
||||
PKG_RELEASE:=61
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
|
||||
@@ -480,6 +480,35 @@ function wireRail() {
|
||||
* stays in the label for screen readers, and in `title` for the pointer. */
|
||||
const IND_DOT = '•';
|
||||
|
||||
/* Idempotent attribute write, so a poll tick that finds nothing changed touches no DOM — same
|
||||
* shape as `fsSyncAttr` in menu-footstrap-common.js, restated rather than imported (that file does
|
||||
* not export it). */
|
||||
function syncIndAttr(el, name, value) {
|
||||
if (value === null) {
|
||||
if (el.hasAttribute(name)) el.removeAttribute(name);
|
||||
} else if (el.getAttribute(name) !== value) {
|
||||
el.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/* A CLICKABLE `[data-indicator]` (the poll pill, "Unsaved Changes: N", …) ships as a bare
|
||||
* span: no role, name or tabindex, so Tab skips it and a screen reader
|
||||
* announces a run of text with no name, role or state (WCAG 2.1.1, 4.1.2). ui.showIndicator's own
|
||||
* click handler already lives on this exact element — this only adds the second, W3C-APG way to
|
||||
* reach it; it does not add a competing one. The name is the pill's own prose ("Refreshing"),
|
||||
* never invented. Enter/Space call el.click() because a <span>, unlike an <a>, gets neither key
|
||||
* for free (contrast fs-widgets.js's wireSpaceKey, written for an <a role="button">); calling
|
||||
* click() cannot double-fire the mouse handler, since a keydown is not a click. */
|
||||
function wireIndicatorKeyboard(el) {
|
||||
if (el.dataset.fsWired) return;
|
||||
el.dataset.fsWired = '1';
|
||||
el.addEventListener('keydown', (ev) => {
|
||||
if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') return;
|
||||
ev.preventDefault();
|
||||
el.click();
|
||||
});
|
||||
}
|
||||
|
||||
function wireIndicatorCounts() {
|
||||
const box = document.getElementById('indicators');
|
||||
if (!box) return;
|
||||
@@ -491,6 +520,18 @@ function wireIndicatorCounts() {
|
||||
el.setAttribute('data-fs-badge', m ? m[1] : IND_DOT);
|
||||
/* the rail hides the prose; the tooltip is where it stays reachable by pointer */
|
||||
el.setAttribute('title', txt);
|
||||
/* Only a CLICKABLE indicator becomes a control. `ui.showIndicator` sets
|
||||
* `data-clickable` only when it was handed a handler, and a status-only pill is
|
||||
* given none (luci-base ui.js; `battstatus.js` calls it with a null handler) — a
|
||||
* `role="button"` tabstop with nothing behind it is worse than the bare span it
|
||||
* replaces. The ring and the pointer cursor in theme/20-shell.css are scoped to
|
||||
* `[data-clickable]` for the same reason; the two must not disagree. */
|
||||
if (el.hasAttribute('data-clickable')) {
|
||||
syncIndAttr(el, 'role', 'button');
|
||||
syncIndAttr(el, 'tabindex', '0');
|
||||
syncIndAttr(el, 'aria-label', txt.trim() || null);
|
||||
wireIndicatorKeyboard(el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
'require dom';
|
||||
'require network';
|
||||
'require fs-fit as fit';
|
||||
'require menu-footstrap-common as common';
|
||||
|
||||
/* Overview layout only: renders nothing of its own, it re-arranges the STOCK System / Memory /
|
||||
* Storage sections into a grid. Content, data and styling stay luci-mod-status's — rendering a
|
||||
@@ -23,11 +24,15 @@ const ROLES = { [_('System')]: 'sys', [_('Memory')]: 'mem', [_('Storage')]: 'sto
|
||||
/* the data-page value four call sites compare against; a string literal is not mangled, so a
|
||||
* repeat is paid in full on flash every time (measured: 24 B x4 -> 37 B, 59 B saved) */
|
||||
|
||||
function sectionTitle(sec) {
|
||||
function headerEl(sec) {
|
||||
/* two title markups, one per release: 25.12 wraps the heading (`.cbi-title > h3`), 24.10 emits
|
||||
* a bare `<h3>` as the section's first child. Matching only one silently disables the grid on
|
||||
* the other. */
|
||||
const h = sec.querySelector('.cbi-title h3, :scope > h3');
|
||||
return sec.querySelector('.cbi-title h3, :scope > h3');
|
||||
}
|
||||
|
||||
function sectionTitle(sec) {
|
||||
const h = headerEl(sec);
|
||||
if (!h) return '';
|
||||
/* the first non-empty TEXT node, not `firstChild`: 25.12 appends a hide/show <span> inside the
|
||||
* same <h3>, so `firstChild` depends on upstream keeping the words first */
|
||||
@@ -39,6 +44,101 @@ function sectionTitle(sec) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/* ---- keyboard disclosure: the card header becomes the toggle, the pill becomes its glyph ----
|
||||
*
|
||||
* Live on owrt2512 (25.12.4), Status -> Overview carries 14 `[data-clickable]` elements — the
|
||||
* topbar poll pill plus one Hide/Show toggle per card — and every one is a bare <span>: no
|
||||
* tabindex, no role, no aria-expanded. Tab never reaches one and a screen reader announces a run
|
||||
* of text with no name, role or state. WCAG 2.1.1 Keyboard (A), 4.1.2 Name, Role, Value (A).
|
||||
* docs/findings.md, "A card cannot be collapsed from the keyboard".
|
||||
*
|
||||
* index.js's own pill keeps doing the actual show/hide — untouched, so mouse behaviour for anyone
|
||||
* clicking it does not change. The header becomes a second, W3C-APG way to reach the SAME handler,
|
||||
* not a competing one. */
|
||||
|
||||
/* Idempotent attribute write, so a poll tick that finds nothing changed touches no DOM and fires
|
||||
* no mutation record. Same shape as `fsSyncAttr` in menu-footstrap-common.js — restated rather than
|
||||
* imported, since that file does not export it. */
|
||||
function syncAttr(el, name, value) {
|
||||
if (value === null) {
|
||||
if (el.hasAttribute(name)) el.removeAttribute(name);
|
||||
} else if (el.getAttribute(name) !== value) {
|
||||
el.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/* index.js's own attribute: "inactive" is expanded (the pill reads "Hide"), "active" is collapsed
|
||||
* (it reads "Show") — the chevron mirror in pages/20-overview.css reads the same attribute the
|
||||
* same way. */
|
||||
function pillExpanded(label) {
|
||||
return label.getAttribute('data-style') !== 'active';
|
||||
}
|
||||
|
||||
/* The panel `data-style` shows or hides is `.cbi-title`'s next sibling in the 25.12 markup this was
|
||||
* measured on. 24.10 emits no `.cbi-title` wrapper (headerEl() above) and is left without
|
||||
* aria-controls rather than guessed at — role, tabindex, aria-expanded and the keyboard below still
|
||||
* apply there; aria-controls is the one piece this function cannot state with confidence. */
|
||||
function panelFor(h) {
|
||||
const holder = h.closest('.cbi-title');
|
||||
return holder ? holder.nextElementSibling : null;
|
||||
}
|
||||
|
||||
let _panelSeq = 0;
|
||||
|
||||
/* Promote one card's header to a disclosure control. Idempotent: a tick that finds the header
|
||||
* already wired touches only aria-expanded, and only when it actually changed; the click/keydown
|
||||
* listeners are added once (`dataset.fsWired`), never re-added. */
|
||||
function wireDisclosure(sec) {
|
||||
const h = headerEl(sec);
|
||||
const label = h && h.querySelector('.label[data-indicator="poll-status"]');
|
||||
if (!h || !label) return; /* not this card's shape — left alone rather than guessed at */
|
||||
|
||||
const panel = panelFor(h);
|
||||
if (panel && !panel.id) panel.id = 'fs-ovl-panel-' + (_panelSeq++);
|
||||
|
||||
syncAttr(h, 'role', 'button');
|
||||
syncAttr(h, 'tabindex', '0');
|
||||
syncAttr(h, 'aria-controls', panel ? panel.id : null);
|
||||
syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
|
||||
/* the header now carries the pill's name, role and state; a screen reader user tabbing past it
|
||||
* to a second, unlabelled clickable span would hear an unexplained duplicate control — same
|
||||
* reasoning as the aria-hidden on svgIcon()'s output, fs-widgets.js:12 */
|
||||
syncAttr(label, 'aria-hidden', 'true');
|
||||
|
||||
if (h.dataset.fsWired) return;
|
||||
h.dataset.fsWired = '1';
|
||||
|
||||
h.addEventListener('click', (ev) => {
|
||||
/* a click landing on the pill itself already ran index.js's own handler; forwarding here
|
||||
* too would toggle the card twice */
|
||||
if (ev.target.closest?.('[data-indicator="poll-status"]') !== label) label.click();
|
||||
syncAttr(h, 'aria-expanded', pillExpanded(label) ? 'true' : 'false');
|
||||
});
|
||||
/* the pill is an <a>-less <span>, so neither key is native here — contrast
|
||||
* fs-widgets.js's wireSpaceKey, written for an <a role="button">, which gets Enter for free
|
||||
* and needs only Space added */
|
||||
h.addEventListener('keydown', (ev) => {
|
||||
if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') return;
|
||||
ev.preventDefault();
|
||||
label.click();
|
||||
});
|
||||
}
|
||||
|
||||
/* A `.cbi-section` LuCI still renders when a stock include has nothing to show this tick: title
|
||||
* "-", the poll pill its only content, 72px tall on the live Overview — seventh there (a fixture is
|
||||
* not proof of position: docs/playground.html puts it first). Real and cosmetic, so it is
|
||||
* suppressed rather than left as a rung in the tab order with nothing behind its own name. */
|
||||
function hideEmptyCard(sec) {
|
||||
if (sectionTitle(sec) === '-') sec.classList.add('fs-ovl-empty');
|
||||
}
|
||||
|
||||
function tidyCards(view) {
|
||||
view.querySelectorAll('.cbi-section').forEach((sec) => {
|
||||
hideEmptyCard(sec);
|
||||
wireDisclosure(sec);
|
||||
});
|
||||
}
|
||||
|
||||
/* the wrapper we built, so the poll-tick fast path costs one property read */
|
||||
let _wrapEl = null;
|
||||
|
||||
@@ -75,6 +175,18 @@ function arrange() {
|
||||
if (!view) return;
|
||||
|
||||
nameTooltips(view);
|
||||
/* 20_memory.js, 25_storage.js and 30_network.js each call their OWN local progressbar(), not
|
||||
* the theme's window.progressbar (menu-footstrap-common.js), so the reading and colour those
|
||||
* bars need are stamped on here instead — on the same poll-tick callback nameTooltips() already
|
||||
* runs on, ahead of the fast-path return below: dom.content() rewrites a section's title every
|
||||
* tick even when the wrapper survives, so a bar's reading is stale on every tick this line is
|
||||
* skipped. */
|
||||
common.annotateMeters(view);
|
||||
/* Same reasoning, same placement: dom.content() rewrites a card's body every tick even when the
|
||||
* wrapper survives, so a card that is only now getting its title text needs wiring on this
|
||||
* pass, not just the first one. tidyCards() is its own idempotent write (wireDisclosure(),
|
||||
* hideEmptyCard() above), so a tick that changes nothing here touches no DOM either. */
|
||||
tidyCards(view);
|
||||
|
||||
/* Fast path: the poll lands here on every tick, forever, and the stock poll never rebuilds
|
||||
* the .cbi-section wrappers, so the grid survives. Deliberately not a disconnect() — if a
|
||||
@@ -276,7 +388,10 @@ function patchOverview() {
|
||||
|
||||
/* `progressbar`, `renderBox` and `renderBadge` are defined in menu-footstrap-common.js, not here:
|
||||
* a stock include calls them bare from its own render(), so they must exist before the view class
|
||||
* does, and this page module is required during the navigation that races it. */
|
||||
* does, and this page module is required during the navigation that races it. `annotateMeters` is
|
||||
* required from that same module (see arrange()) rather than duplicated: it is not a template
|
||||
* global, so nothing forces it to live there, but the threshold split and label lookup it shares
|
||||
* with `window.progressbar` would drift into two copies otherwise. */
|
||||
|
||||
return baseclass.extend({
|
||||
/* called once by menu-footstrap-common's init; everything route-dependent hangs off the
|
||||
|
||||
@@ -316,6 +316,66 @@ function labelCells(t, head) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- WCAG 1.3.1: a `display` override must not cost a table its own roles ----
|
||||
*
|
||||
* theme/*.css restyles `display` on `.table`/`.thead`/`.tbody`/`.tfoot`/`.tr`/`.th`/`.td` in over
|
||||
* twenty places — `.fs-stacked`, `.fs-rowstack`, the config table's own `@container`, the phone
|
||||
* `@media`, the Overview meter row, the software page's carded description column, the 560px
|
||||
* key/value card added alongside this file — scattered across selectors this module cannot see the
|
||||
* live state of (a media query, a container query, a class some other pass toggled). A `display`
|
||||
* other than `table`/`table-row`/`table-cell`/… drops the IMPLICIT `table`/`row`/`cell` role
|
||||
* HTML-AAM derives from it: measured on Overview's key/value cards, `display: block`/`grid`/`block`
|
||||
* (table/row/cell) at both 1440 and 390px, and on the 390px card stack, `block`/`none`/`block` —
|
||||
* neither carries a role, so the column a value belongs to is never announced.
|
||||
*
|
||||
* Rather than pair each of those twenty-odd rules with a role write timed to its own trigger — a
|
||||
* race with every one of them — the role is written UNCONDITIONALLY, matching what a plain
|
||||
* `<table>` already carries implicitly. That costs nothing while the table IS a table and survives
|
||||
* the moment any of those rules land. tools/table-contract.mjs holds the pairing: a future
|
||||
* `display` rule on one of these classes with no role to match is what it is written to catch.
|
||||
*
|
||||
* The whole chain, every pass — a `role="cell"` with nothing above it saying `row`, or a `row` with
|
||||
* nothing above IT saying `table`/`rowgroup`, reads worse than no roles at all. One flat query per
|
||||
* class keeps it whole without walking each table by hand: `.tr`/`.th`/`.td` are LuCI's own names,
|
||||
* always written together with `.table` (adoptMarkup() above never adds one without the others), so
|
||||
* a class query alone reaches the whole subtree. `.thead`/`.tbody`/`.tfoot` are the same for a
|
||||
* div-based table; a REAL `<thead>`/`<tbody>`/`<tfoot>` this file never classes (20_lan.js's and
|
||||
* 30_wifi.js's `<tfoot>`, theme/30-tables.css:570/634) is reached by tag name instead. */
|
||||
const TABLE_ROLE_CLASSES = [
|
||||
[ '.table', 'table' ],
|
||||
[ '.thead', 'rowgroup' ],
|
||||
[ '.tbody', 'rowgroup' ],
|
||||
[ '.tfoot', 'rowgroup' ],
|
||||
[ '.tr', 'row' ],
|
||||
[ '.th', 'columnheader' ],
|
||||
[ '.td', 'cell' ],
|
||||
];
|
||||
const TABLE_ROLE_TAGS = [
|
||||
[ 'thead', 'rowgroup' ],
|
||||
[ 'tbody', 'rowgroup' ],
|
||||
[ 'tfoot', 'rowgroup' ],
|
||||
];
|
||||
|
||||
/* idempotent, the same idiom as menu-footstrap-common.js's fsSyncAttr: a node that already carries
|
||||
* the right role is a no-op read, so a poll tick re-rendering the same table on every tick touches
|
||||
* no DOM and wakes no attribute observer. Not imported for one line — this file has none of that
|
||||
* module's other exports to justify the dependency. */
|
||||
function setRole(el, role) {
|
||||
if (el.getAttribute('role') !== role) el.setAttribute('role', role);
|
||||
}
|
||||
|
||||
/* A hidden row/cell (`.tr.cbi-section-table-descr`, `.td.hide-xs`/`.hide-sm` under `.fs-stacked`/
|
||||
* `.fs-drop-xs`) drops out of the accessibility tree on `display: none` alone — the role written
|
||||
* here changes nothing about that, since a role is inert on a box the engine never generates. */
|
||||
function roleTables() {
|
||||
TABLE_ROLE_CLASSES.forEach(([ sel, role ]) => {
|
||||
document.querySelectorAll(inRoots(sel, liveRoots())).forEach((el) => setRole(el, role));
|
||||
});
|
||||
TABLE_ROLE_TAGS.forEach(([ sel, role ]) => {
|
||||
document.querySelectorAll(inRoots(sel, liveRoots())).forEach((el) => setRole(el, role));
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- card-stack a data table that no longer fits ----
|
||||
*
|
||||
* Measuring, scheduling and the observers are fs-fit.js; this file supplies only the decision.
|
||||
@@ -890,12 +950,14 @@ return baseclass.extend({
|
||||
fit.armGate();
|
||||
|
||||
/* A table must be tagged .fs-dt before it can be fitted, and re-tagged whenever the poll
|
||||
* brings a fresh one back, so tagging leads.
|
||||
* brings a fresh one back, so tagging leads; roleTables() follows it directly so a class
|
||||
* adoptMarkup() just added carries its role in the same pass rather than waiting a tick.
|
||||
*
|
||||
* Five registrations, not one callback: fs-fit catches per registered fitter, and the first
|
||||
* Six registrations, not one callback: fs-fit catches per registered fitter, and the first
|
||||
* of these walks third-party markup. Bundled, one throw in `tagDataTables()` leaves no table
|
||||
* tagged and — with the gate above raised — a page with no tables on it at all. */
|
||||
fit.add(tagDataTables);
|
||||
fit.add(roleTables);
|
||||
fit.add(fitTables);
|
||||
fit.add(fitScrollables);
|
||||
fit.add(unpinActionColumn);
|
||||
|
||||
@@ -153,6 +153,88 @@ function loadPlugins() {
|
||||
});
|
||||
}
|
||||
|
||||
/* warn/danger split for the meter fill (theme/25-progressbar.css): a DECISION, not a measurement —
|
||||
* 20% of an 84 MiB overlay and 20% of an 8 GiB disk are different news at the same reading, and a
|
||||
* plain percentage is kept anyway so every meter on the page reads the same way. */
|
||||
const FS_METER_WARN = 80;
|
||||
const FS_METER_DANGER = 92;
|
||||
|
||||
/* Write an attribute only when the value actually changes, so a poll tick that reads the same
|
||||
* numbers back touches no DOM and fires no attribute-mutation observer. `value === null` removes
|
||||
* the attribute instead of writing the string "null". */
|
||||
function fsSyncAttr(el, name, value) {
|
||||
if (value === null) {
|
||||
if (el.hasAttribute(name)) el.removeAttribute(name);
|
||||
} else if (el.getAttribute(name) !== value) {
|
||||
el.setAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/* The meter's name, if the markup already states one — never invented. A `.cbi-value` row's own
|
||||
* label (the RSSI/RSRP gallery shape) or a key/value table row's first cell (Memory, Storage, CPU
|
||||
* load on Overview) each stand for the whole row; a bare meter with neither (Software's disk-space
|
||||
* bar, the package manager) gets no `aria-label` written at all, and Chromium's accessible-name
|
||||
* computation then falls back to `title` — which is the reading, not a name ("95 / 100 (95%)"),
|
||||
* measured with `Accessibility.getPartialAXTree`. That fallback cannot be closed from here: `title`
|
||||
* is the ONLY source `::after { content: attr(title) }` has for the visible percentage
|
||||
* (styles/theme/25-progressbar.css), so removing it would blank the bar for a sighted reader too. */
|
||||
function findProgressbarLabel(pg) {
|
||||
const row = pg.closest('.cbi-value');
|
||||
let label = row ? row.querySelector('.cbi-value-title') : null;
|
||||
if (!label) {
|
||||
const tr = pg.closest('.tr');
|
||||
if (tr) {
|
||||
const own = pg.closest('.td, td');
|
||||
const cell = tr.querySelector('.td, td');
|
||||
if (cell && cell !== own) label = cell;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
/* The percentage inside a meter's `title`, in either shape a caller writes it in: `window.progressbar`
|
||||
* below composes `'%s / %s (%d%%)'`, parenthesised and preceded by the byte/localised reading, but a
|
||||
* bare meter markup may carry just `'97%'` with nothing around it (docs/gallery.html). Both are
|
||||
* anchored at the end of the string so neither can match a stray "%" earlier in a localised reading;
|
||||
* an empty title or one with no percentage at all yields null, on purpose — nothing to annotate.
|
||||
* `%d` is the unclamped percentage, so the result can still read past 100 or under 0 and is clamped
|
||||
* by the caller, the same way `window.progressbar` clamps its own `level`, below. Exported for
|
||||
* tests/meter.test.mjs, which is the only caller that needs the parse on its own. */
|
||||
function parseMeterPercent(title) {
|
||||
if (title == null) return null;
|
||||
const m = (/\((-?\d+)%\)\s*$/).exec(title) || (/(-?\d+)%\s*$/).exec(title);
|
||||
return m ? parseInt(m[1], 10) : null;
|
||||
}
|
||||
|
||||
/* The attribute work `window.progressbar` and a bare `.cbi-progressbar` node both need — Status ->
|
||||
* Overview's own 20_memory.js, 25_storage.js and 30_network.js (luci-mod-status) declare and call
|
||||
* their OWN local `progressbar()`, a straight copy of the same upstream body, so this theme's
|
||||
* global never runs on the meters those stock includes actually draw. `annotateMeters()` below
|
||||
* walks the DOM after the fact instead of owning every writer; the threshold split and the name
|
||||
* logic stay in this one function either way, so the two callers cannot drift apart. */
|
||||
function annotateMeter(pg) {
|
||||
const title = pg.getAttribute('title');
|
||||
const pc = parseMeterPercent(title);
|
||||
if (pc == null) return;
|
||||
const level = pc < 0 ? 0 : (pc > 100 ? 100 : pc);
|
||||
fsSyncAttr(pg, 'role', 'progressbar');
|
||||
fsSyncAttr(pg, 'aria-valuemin', '0');
|
||||
fsSyncAttr(pg, 'aria-valuemax', '100');
|
||||
fsSyncAttr(pg, 'aria-valuenow', String(level));
|
||||
fsSyncAttr(pg, 'aria-valuetext', title);
|
||||
const label = findProgressbarLabel(pg);
|
||||
fsSyncAttr(pg, 'aria-label', label ? label.textContent.trim() : null);
|
||||
fsSyncAttr(pg, 'data-fs-level',
|
||||
level >= FS_METER_DANGER ? 'danger' : (level >= FS_METER_WARN ? 'warn' : null));
|
||||
}
|
||||
|
||||
/* Every `.cbi-progressbar[title]` under `root` — the markup itself, not who last drew it. Called
|
||||
* from fs-overview.js's own poll-tick observer (see there for why this file does not run a second
|
||||
* one); `fsSyncAttr` above makes a re-run over an unchanged bar a no-op read. */
|
||||
function annotateMeters(root) {
|
||||
(root || document).querySelectorAll('.cbi-progressbar[title]').forEach(annotateMeter);
|
||||
}
|
||||
|
||||
/* The three template globals Status -> Overview needs, defined where ordering is guaranteed.
|
||||
*
|
||||
* `admin_status/index.ut` defines `progressbar`, `renderBox` and `renderBadge` in an inline script
|
||||
@@ -165,29 +247,45 @@ function loadPlugins() {
|
||||
* ordering the two. This file is required by the footer on every page and evaluates before the
|
||||
* router exists.
|
||||
*
|
||||
* Bodies are verbatim from upstream except for two deltas: L.itemlist -> window.L.itemlist (the
|
||||
* two-L trap, docs/spa-router.md), and renderBox's `[title]` — dom.append parses a scalar child as
|
||||
* Bodies are verbatim from upstream except for three deltas: L.itemlist -> window.L.itemlist (the
|
||||
* two-L trap, docs/spa-router.md); renderBox's `[title]` — dom.append parses a scalar child as
|
||||
* innerHTML (luci.js:1395) and an array member as text (:1383), and this file defines the global on
|
||||
* every admin page where upstream defines it on Status -> Overview alone. Same output: nothing in
|
||||
* 24.10, 25.12 or master calls renderBox. The typeof guards make each a no-op on a full page load,
|
||||
* where the template's own copies win the race. */
|
||||
* every admin page where upstream defines it on Status -> Overview alone; and progressbar's
|
||||
* accessibility attributes (role, aria-value*, data-fs-level) — upstream's copy writes the reading
|
||||
* into `title` alone, which nothing reads back for a screen reader off a generated `::after`. Same
|
||||
* output on the other two: nothing in 24.10, 25.12 or master calls renderBox, and progressbar's
|
||||
* `title` and fill width are untouched.
|
||||
*
|
||||
* `renderBox`/`renderBadge` keep the `typeof` guard: a full page load runs the template's inline
|
||||
* `<script>` first, in document order, before this module evaluates, so upstream has already
|
||||
* declared both — the guard just avoids overwriting an identical body. `progressbar` CANNOT reuse
|
||||
* that guard: upstream's copy of the same name wins that exact race, and it is the one WITHOUT the
|
||||
* accessibility attributes below. Guarded, the theme's version only ever won when a page other than
|
||||
* Status -> Overview loaded first and the SPA router then navigated here without a reload — which
|
||||
* is a real gap, not the common case. The assignment is unconditional so the accessible copy always
|
||||
* wins, on a full load and on an SPA arrival alike. */
|
||||
function ensureOverviewHelpers() {
|
||||
/* eslint-disable no-var -- these three bodies are copies of LuCI's admin_status/index.ut so
|
||||
they can be diffed against upstream when it changes. Modernising the `var`s would break
|
||||
that property, which is what makes carrying the copies safe. */
|
||||
if (typeof window.progressbar !== 'function')
|
||||
window.progressbar = function(query, value, max, byte) {
|
||||
var pg = document.querySelector(query),
|
||||
vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn);
|
||||
if (pg) {
|
||||
pg.firstElementChild.style.width = pc + '%';
|
||||
pg.setAttribute('title', '%s / %s (%d%%)'.format(fv, fm, pc));
|
||||
}
|
||||
};
|
||||
window.progressbar = function(query, value, max, byte) {
|
||||
var pg = document.querySelector(query),
|
||||
vn = parseInt(value) || 0,
|
||||
mn = parseInt(max) || 100,
|
||||
fv = byte ? String.format('%1024.2mB', value) : value,
|
||||
fm = byte ? String.format('%1024.2mB', max) : max,
|
||||
pc = Math.floor((100 / mn) * vn),
|
||||
reading = '%s / %s (%d%%)'.format(fv, fm, pc);
|
||||
if (pg) {
|
||||
pg.firstElementChild.style.width = pc + '%';
|
||||
pg.setAttribute('title', reading);
|
||||
/* Accessible value, colour and name: annotateMeter() re-parses `reading` back out
|
||||
* of `title` rather than reusing `pc` here directly, so a bare `.cbi-progressbar`
|
||||
* this function never touched is annotated the identical way — the clamp, the
|
||||
* label lookup and the warn/danger split stay in that one function. */
|
||||
annotateMeter(pg);
|
||||
}
|
||||
};
|
||||
if (typeof window.renderBox !== 'function')
|
||||
window.renderBox = function(title, active, childs) {
|
||||
childs = childs || [];
|
||||
@@ -235,6 +333,15 @@ return baseclass.extend({
|
||||
* remember() for what a key is */
|
||||
remember,
|
||||
|
||||
/* the seam fs-overview.js calls, on its own already-coalesced poll-tick observer, to annotate
|
||||
* the meters a stock Status -> Overview include draws with its own local progressbar() */
|
||||
annotateMeters,
|
||||
|
||||
/* module-private otherwise: tests/meter.test.mjs drives the percent parse and the clamp/threshold
|
||||
* split directly rather than through a live `.cbi-progressbar[title]` walk */
|
||||
parseMeterPercent, /* fs:probe */
|
||||
annotateMeter, /* fs:probe */
|
||||
|
||||
init(renderMainMenu) {
|
||||
/* First, and outside the promise: a third-party sheet that outranks the chrome is already
|
||||
* painting (fs-sheets: openclash's `* { margin: 0; padding: 0 }`). Deferring this to
|
||||
|
||||
@@ -12,16 +12,24 @@
|
||||
* drifted). Only the overview-specific chevron toggle stays.
|
||||
* index.js emits a span with a raw "Hide"/"Show" text node + data-style (expanded =
|
||||
* inactive, collapsed = active); data-indicator='poll-status' is unique to these
|
||||
* toggles. font-size:0 hides the word, ::after borders draw the chevron. */
|
||||
* toggles. font-size:0 hides the word, ::after borders draw the chevron.
|
||||
*
|
||||
* The card HEADER is the disclosure control now (fs-overview.js wireDisclosure()); this pill is
|
||||
* marked aria-hidden there and stays only as its glyph, so the border and fill that made it read
|
||||
* as its own 26px button are dropped — 16px, matching the rail toggle's own chevron glyph
|
||||
* (theme/20-shell.css `.fs-railtoggle .fs-ico`), and --fs-faint rather than the pill's --fs-dim,
|
||||
* a step quieter since it is decoration rather than a target now. */
|
||||
.cbi-title .label[data-indicator="poll-status"] {
|
||||
font-size: 0;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--fs-radius-sm);
|
||||
background: none;
|
||||
border: 0;
|
||||
color: var(--fs-faint);
|
||||
}
|
||||
/* The glyph itself is the Appearance fold's, to the byte — one chevron for "this opens", drawn
|
||||
* in two page files because the two hosts are a stock element keyed by attribute and one of
|
||||
@@ -47,6 +55,37 @@
|
||||
/* @endmirror */
|
||||
}
|
||||
|
||||
/* The header itself (fs-overview.js wireDisclosure() stamps role="button"): a hand cursor for a
|
||||
* pointer user, a focus ring for a keyboard one — the same ring every other interactive control
|
||||
* in this theme draws (theme/35-alerts.css `.cbi-title .label:focus-visible`, the pill's own
|
||||
* former ring, now the header's).
|
||||
*
|
||||
* Two selectors, not one: fs-overview.js's headerEl() reads `.cbi-title h3, :scope > h3` because
|
||||
* the wrapper itself differs by release — 25.12 renders `.cbi-title > h3`, 24.10 a bare `<h3>`
|
||||
* straight inside `.cbi-section`, no `.cbi-title` at all. `[role="button"]` is the anchor either
|
||||
* way, so a heading wireDisclosure() left untouched — one with nothing to collapse — is never
|
||||
* matched. Measured on a 24.10 FORM: without the second arm the attributes and the keyboard
|
||||
* worked but `cursor` stayed `auto`, `box-shadow` `none`, and focus drew only the UA outline.
|
||||
* On a live 24.10 the arm does not fire at all today, and that is not a fault in it: OpenWrt
|
||||
* 24.10.8 renders no hide/show control on an Overview card, so wireDisclosure() stamps nothing
|
||||
* there and 0 of the page 14 `<h3>`s carry the role (measured on owlab owrt2410). The arm
|
||||
* guards a SHAPE of markup, not a release — it is what keeps the styling with the behaviour on
|
||||
* whichever release grows the control next. */
|
||||
:is(.cbi-title h3, .cbi-section > h3)[role="button"] {
|
||||
cursor: pointer;
|
||||
border-radius: var(--fs-radius-sm);
|
||||
}
|
||||
:is(.cbi-title h3, .cbi-section > h3)[role="button"]:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--fs-focus-ring);
|
||||
}
|
||||
|
||||
/* An empty `.cbi-section` LuCI still renders when a stock include has nothing to show: title
|
||||
* "-", the poll pill its only content, 72px tall on the live Overview (seventh there — a
|
||||
* fixture is not proof of position, docs/findings.md "An empty card with `-` as its title").
|
||||
* Real and cosmetic, so fs-overview.js's hideEmptyCard() marks it and this drops it. */
|
||||
body[data-page="admin-status-overview"] .cbi-section.fs-ovl-empty { display: none; }
|
||||
|
||||
/* Stock overview layout: fs-overview.js wraps the stock System / Memory / Storage
|
||||
* sections in .fs-ovl; this is the grid only — System left across both rows, Memory +
|
||||
* Storage right. Stock styling and data untouched. */
|
||||
|
||||
@@ -192,8 +192,13 @@
|
||||
}
|
||||
.fs-ap-verrow { display: flex; align-items: center; justify-content: space-between; gap: var(--fs-space-2-5); }
|
||||
/* links to the project repo — reads as muted text until hovered */
|
||||
.fs-ap-version { font-size: var(--fs-type-xs); color: var(--fs-dim); font-variant-numeric: tabular-nums; text-decoration: none; }
|
||||
.fs-ap-version { position: relative; font-size: var(--fs-type-xs); color: var(--fs-dim); font-variant-numeric: tabular-nums; text-decoration: none; }
|
||||
.fs-ap-version:hover { color: var(--fs-accent); text-decoration: underline; }
|
||||
/* the link's own box was 84x17 (owrt2512/390x844, Appearance tab), under the 24px floor of
|
||||
* WCAG 2.2 SC 2.5.8 on height. Same fix as `.cbi-checkbox > label[for]::before` in
|
||||
* theme/60-inputs.css: an invisible, absolutely-positioned overlay grows the hit box to
|
||||
* 88x25 without moving the text or growing `.fs-ap-verrow`, since the overlay is out of flow. */
|
||||
.fs-ap-version::before { content: ""; position: absolute; inset: -4px -2px; }
|
||||
|
||||
/* The line that names the missing catalogue package. Same weight as the version beside it —
|
||||
* it is a fact about this router, not a warning: nothing is broken, one package is absent.
|
||||
|
||||
@@ -782,4 +782,14 @@
|
||||
* so "Refreshing" showed a text caret. */
|
||||
.fs-indicators [data-indicator][data-clickable],
|
||||
#indicators [data-indicator][data-clickable] { cursor: pointer; }
|
||||
/* fs-chrome.js promotes a clickable indicator to role="button" with Enter and Space, so
|
||||
* it is a control now and takes the control ring the buttons beside it take (55-buttons.css)
|
||||
* rather than the UA outline it was left with. `:focus-visible`, not `:focus`: a pointer press
|
||||
* on the pill should not draw a ring, the same rule every other control here follows. */
|
||||
.fs-indicators [data-indicator][data-clickable]:focus-visible,
|
||||
#indicators [data-indicator][data-clickable]:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--fs-accent);
|
||||
box-shadow: var(--fs-focus-ring);
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,16 @@
|
||||
border-radius: var(--fs-radius-pill);
|
||||
transition: width var(--fs-dur-fill);
|
||||
}
|
||||
/* Colour carries the reading too, not length alone: `data-fs-level` is set from JS
|
||||
* (menu-footstrap-common.js, FS_METER_WARN/FS_METER_DANGER — 80/92%, a decision, not a
|
||||
* measurement) on every poll tick. Solid tokens, matching the accent fill above — no
|
||||
* color-mix, so no static twin is owed to 04-nocolormix.css. */
|
||||
.cbi-progressbar[data-fs-level="warn"] > div {
|
||||
background: var(--fs-warn);
|
||||
}
|
||||
.cbi-progressbar[data-fs-level="danger"] > div {
|
||||
background: var(--fs-danger);
|
||||
}
|
||||
.cbi-progressbar::after {
|
||||
content: attr(title);
|
||||
/* the gap is a scale step, not a literal 4: every other gap in the theme scales with the
|
||||
|
||||
@@ -303,6 +303,43 @@
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:not(:first-child) {
|
||||
font-family: var(--fs-font-mono); color: var(--fs-text); text-align: end;
|
||||
}
|
||||
/* Below ~560px of container width the 40%/nowrap column above starves the value: measured on
|
||||
* System -> System at 390px content width, Firmware Version wrapped onto 5 lines and Kernel
|
||||
* Version/Architecture onto 2 each, most of the 130px label column sitting empty. Stack it like
|
||||
* the other card idioms in this file — label as an eyebrow above its value — instead of holding
|
||||
* a fixed-fraction column at any width. A width win, not a height one: measured on the gallery's
|
||||
* key/value specimen (a 340px .cbi-map, docs/gallery.html), stacking cost the card +44.9% height
|
||||
* (202.6 -> 293.5px) for the value column widening x1.67 (183.6 -> 306px) and Firmware Version's
|
||||
* line count dropping from two to one. (The five lines above are the live 390px page, where the
|
||||
* value is longer than the specimen carries — the shape is the same, the count is not.)
|
||||
*
|
||||
* 560 is the second threshold named on fs-content; the config table's card (theme/65-dropdown.css)
|
||||
* is the first, at 960, for a row full of widgets that bake in a layout width. This one holds two
|
||||
* plain text cells, so it can stay two-column down to a narrower width before the label needs the
|
||||
* row to itself.
|
||||
*
|
||||
* `:first-child:first-child` / `:last-child:last-child` double the tail pseudo-class to outrank
|
||||
* the two rules above at identical specificity otherwise — the `.hidden.hidden` idiom
|
||||
* (theme/60-inputs.css) — rather than win on source order, which the next edit to either rule
|
||||
* could invert.
|
||||
*
|
||||
* The value's right-flush alignment above is a two-column device: with the label gone from the
|
||||
* row, right-flushing floats the value away from its own caption. Left-aligned instead, so the
|
||||
* card reads top-to-bottom as caption then value, matching every other stacked cell in this file. */
|
||||
@container fs-content (max-width: 560px) {
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .tr {
|
||||
display: block;
|
||||
}
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:first-child:first-child {
|
||||
display: block; width: auto; white-space: normal;
|
||||
font-size: var(--fs-type-xs); font-weight: var(--fs-eyebrow-weight);
|
||||
letter-spacing: var(--fs-eyebrow-tracking); color: var(--fs-eyebrow-color);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.cbi-section .table:not([id]):not(.cbi-section-table):not(.fs-dt):not(:has(.tr.table-titles)):not([style*="table-layout"]) .td:last-child:last-child:not(:first-child) {
|
||||
display: block; width: auto; text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- the realtime graph legend (status/realtime/*) ---
|
||||
* The one no-id `.table` LuCI renders with inline `table-layout:fixed;width:100%` and six cells
|
||||
|
||||
@@ -110,8 +110,8 @@
|
||||
* 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 nearer of the two. */
|
||||
* fs-content "whatever content column I am in", carried by .cbi-map, #view and .modal (below),
|
||||
* so a query resolves to the nearest of the three. */
|
||||
.cbi-map { container-type: inline-size; container-name: fs-content; }
|
||||
#view { container-type: inline-size; container-name: fs-view fs-content; }
|
||||
|
||||
|
||||
@@ -782,32 +782,78 @@
|
||||
* `!important` would do it too, and would invert the layer order for every future override. */
|
||||
.hidden.hidden { display: none; }
|
||||
/* RangeSlider (UIRangeSlider) — the one LuCI widget base leaves to native browser rendering.
|
||||
* Token-driven track/thumb. */
|
||||
* Token-driven track/thumb.
|
||||
* The INPUT box is the WCAG 2.2 SC 2.5.8 target (24px min): it used to be the 6px track
|
||||
* itself, measured 258x6 on owrt2512/390x844 (System > System). Box and paint split here —
|
||||
* a transparent 24px box, the 6px bar drawn by the track pseudo-element inside it.
|
||||
*
|
||||
* A flat `height: 24px` DOES move the row, and not through `.cbi-range-slider`'s own cross
|
||||
* size — that stayed 29.5px throughout, already pinned by the value chip beside it. It moves
|
||||
* through the enclosing LINE BOX: `.cbi-value-field` lays this element out as ordinary inline
|
||||
* content, and the line's height is anchored on where `.cbi-range-slider` (inline-flex, no
|
||||
* baseline-participating child) reports ITS baseline to the ancestor line — which tracks the
|
||||
* first flex item's own edge and moved with the input's box. Measured before either fix below:
|
||||
* `.cbi-value-field` +2.75px (Chromium) / +2.5px (Firefox), `.cbi-value` +1.5px / +1.0px,
|
||||
* track/thumb ~1.25px lower.
|
||||
*
|
||||
* First half of the fix is the checkbox's own trick (`--hit`/`--box` below): grow the box for
|
||||
* the hit target, then pull the extra area back out of flow with a symmetric negative margin
|
||||
* so the box overflows itself around its old centre instead of pushing something taller. 6px
|
||||
* was the box before this rule; -9px is (6px - 24px) / 2. This is what keeps a BARE range
|
||||
* input (no `.cbi-range-slider` wrapper, no taller sibling — a third-party app's own markup)
|
||||
* from growing its own row, the same way the checkbox's margin protects a row with no toggle
|
||||
* label beside it. Measured on its own, though, it left THIS row's growth above unchanged: the
|
||||
* line-box placement of `.cbi-range-slider` tracks the input's own edge, not the flow space the
|
||||
* margin trick frees up, so the second half below is still needed on top of it. */
|
||||
input[type="range"] {
|
||||
appearance: none;
|
||||
width: 100%; height: 6px; min-height: 0; padding: 0;
|
||||
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||||
width: 100%; height: 24px; min-height: 0; padding: 0;
|
||||
margin-block: -9px;
|
||||
background: transparent; border: 0; border-radius: 0;
|
||||
outline: none; box-shadow: none; cursor: pointer;
|
||||
}
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
width: 100%; height: 6px;
|
||||
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||||
}
|
||||
input[type="range"]::-moz-range-track {
|
||||
width: 100%; height: 6px;
|
||||
background: var(--fs-track); border: 0; border-radius: var(--fs-radius-pill);
|
||||
}
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
background: var(--fs-accent); border: 2px solid var(--fs-panel);
|
||||
cursor: pointer;
|
||||
/* WebKit centres the thumb on the INPUT's box, not the track: (6 - 16) / 2 = -5px pulls
|
||||
* it back onto the 6px track. Firefox centres ::-moz-range-thumb on the track itself. */
|
||||
margin-top: -5px;
|
||||
}
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 16px; height: 16px; border-radius: 50%;
|
||||
background: var(--fs-accent); border: 2px solid var(--fs-panel); cursor: pointer;
|
||||
}
|
||||
input[type="range"]::-moz-range-track { background: var(--fs-track); height: 6px; border-radius: var(--fs-radius-pill); }
|
||||
/* -solo: the track sets `border: 0` and `outline: none`, so there is no second channel here
|
||||
* either — the tinted ring was the whole indicator on a 6px bar. */
|
||||
input[type="range"]:focus { box-shadow: var(--fs-focus-ring-solo); }
|
||||
* either — the tinted ring was the whole indicator on a 6px bar. Set on the track pseudo,
|
||||
* not the input, so the ring still hugs the 6px bar rather than the 24px hit box. */
|
||||
input[type="range"]:focus::-webkit-slider-runnable-track { box-shadow: var(--fs-focus-ring-solo); }
|
||||
input[type="range"]:focus::-moz-range-track { box-shadow: var(--fs-focus-ring-solo); }
|
||||
/* THE SAME CEILING THE ELASTIC TEXT INPUTS TAKE, and the reason is the whole point of the
|
||||
* token: a `.cbi-value-field` is `flex: 1`, so `width: 100%` here meant the slider ate the
|
||||
* entire card while the select two rows above stopped at 210px and the readout chip was flung
|
||||
* to the card's right edge. Every stretching control in a form row now ends at one x. */
|
||||
.cbi-range-slider { display: inline-flex; align-items: center; gap: var(--fs-space-3); width: 100%; max-width: var(--fs-field-max); }
|
||||
* to the card's right edge. Every stretching control in a form row now ends at one x.
|
||||
*
|
||||
* `vertical-align: middle` answers the second half of the 24px-target growth explained above
|
||||
* `input[type="range"]`. Default (`baseline`) places this inline-flex box in its line by the
|
||||
* edge of its first flex item — the range input — so that edge moving 6px -> 24px still pushed
|
||||
* `.cbi-value-field`'s line taller even with the margin trick already in place. `middle` places
|
||||
* the box by its own total height instead, which the margin trick already held at its old
|
||||
* 29.5px (pinned by the value chip, unaffected by the input's growth), decoupling the two.
|
||||
* Measured on the gallery slider fixture, both engines, this rule plus the margin trick against
|
||||
* HEAD: `.cbi-value-field` +0.000px in both, `.cbi-value` -0.187px (Chromium) / -0.183px
|
||||
* (Firefox) — under a fifth of a CSS px, not a pixel a screen can render differently. Box
|
||||
* unchanged at 258x24. */
|
||||
.cbi-range-slider { display: inline-flex; align-items: center; vertical-align: middle; gap: var(--fs-space-3); width: 100%; max-width: var(--fs-field-max); }
|
||||
.cbi-range-slider-value {
|
||||
background: var(--fs-panel2); border: var(--fs-hairline); border-radius: var(--fs-radius-sm);
|
||||
padding: var(--fs-space-1) var(--fs-space-2); color: var(--fs-accent); font-family: var(--fs-font-mono);
|
||||
|
||||
Reference in new issue
Block a user