mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-11-01 15:03:56 +08:00
update 2026-10-08 02:18:41
This commit is contained in:
1 parent
bd061545ed
commit
170f9b0957
110 files changed
+22312
-644
No files matched your search
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-9 -1 100 100">
|
||||
<!-- The OpenWrt mark, and it stays OpenWrt's on purpose: the tab icon says which DEVICE this
|
||||
is, not which theme is painting it. What it does not need is the flat cyan tile the mark
|
||||
used to be pasted on — that tile was the "outdated" part (issue #7), and it fought every
|
||||
browser's tab strip. Transparent now, so the icon sits in whatever the browser draws.
|
||||
is, not which theme is painting it. The mark carries no flat cyan tile: a tile fights every
|
||||
browser's tab strip, so the background is transparent and the icon sits in whatever the
|
||||
browser draws.
|
||||
|
||||
Colours are the mark's own: cyan arcs, dark-blue ring. On a DARK tab strip that ring is
|
||||
nearly invisible, so it lightens under prefers-color-scheme — an SVG favicon is a live
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -52,11 +52,11 @@ function probeColor(expr) {
|
||||
* element computing `color` in every engine. It has no text and no size, so it paints
|
||||
* nothing.
|
||||
*
|
||||
* Every declaration is !important (issue #19): this is an unmarked element in a document
|
||||
* Every declaration is !important: this is an unmarked element in a document
|
||||
* shared with `luci-app-*`, and an app's unlayered `span { color: … !important }` outranks
|
||||
* a layer and a plain inline style alike. A probe that loses its own colour reports the
|
||||
* app's, which then becomes the admin's saved axis on the next confirm. */
|
||||
/* the id is this probe's escape from fs-router.js's strayBodyNode() (issue #56): it is
|
||||
/* the id is this probe's escape from fs-router.js's strayBodyNode(): it is
|
||||
* appended once and never removed, so without an fs-* id it would read as body litter and
|
||||
* force every navigation away from this page into a full load. */
|
||||
_probe = E('span', { 'id': 'fs-appearance-probe', 'aria-hidden': 'true' });
|
||||
@@ -76,7 +76,7 @@ function probeColor(expr) {
|
||||
* the space it was authored in, so `oklch(0.54 0.19 300)` would parse as three numbers in the
|
||||
* wrong units and produce a colour nobody chose — measured: #010078, graded "Too faint to read",
|
||||
* in the hex field, the swatch and the contrast readout alike. Painting one pixel makes the engine
|
||||
* convert instead (tools/export-tier.mjs uses the same method). Canvas 2D is universal on every
|
||||
* convert instead (the export tier is checked the same way). Canvas 2D is universal on every
|
||||
* engine 24.10 ships against, so there is no second path. */
|
||||
let _cx = null;
|
||||
function rasterCtx() {
|
||||
@@ -170,8 +170,8 @@ function colorControl(onPick, label, opts) {
|
||||
* hairline is graded on the second (`kind: 'shape'`) and warns rather than fails — a faint
|
||||
* border is a legitimate choice.
|
||||
*
|
||||
* Class names are written out whole: tools/fs-orphans.mjs sweeps dead CSS by matching
|
||||
* fs-* tokens in the source, and a concatenated name is invisible to it. */
|
||||
* Class names are written out whole: dead CSS is found by matching fs-* tokens in the
|
||||
* source, and a concatenated name is invisible to that match. */
|
||||
const where = o.contrast.label;
|
||||
const grade = (o.contrast.kind === 'shape')
|
||||
? ((r >= 3)
|
||||
@@ -319,7 +319,7 @@ function build() {
|
||||
* one merged catalogue — load_catalog() loads every *.<lang>.lmo and a lookup returns the first
|
||||
* archive holding the hash — so a bare msgid is a global name any luci-app may take, and
|
||||
* readdir order picks the winner: the layout toggle rendered "Максимум" on a Russian router
|
||||
* because another catalogue translates "Top" as "maximum" (issue #6). Contexting cannot be
|
||||
* because another catalogue translates "Top" as "maximum". Contexting cannot be
|
||||
* selective. The chrome, the login/notice sentences and the System/Memory/Storage headings are
|
||||
* deliberately bare — inheriting luci-base's translation covers the ~40 languages this theme
|
||||
* has no catalogue for. */
|
||||
@@ -356,7 +356,7 @@ function build() {
|
||||
large: _('Large', 'footstrap')
|
||||
}, bump(prefs.applyDensity), label)),
|
||||
|
||||
/* issue #44: the content column's own cap, separate from Density (which moves type and
|
||||
/* the content column's own cap, separate from Density (which moves type and
|
||||
* air, not the column's ceiling). The slider STARTS at the 1280px the theme has always
|
||||
* shipped and only ever widens — there is no reason to offer a column narrower than the
|
||||
* one every page was designed against, and keeping 1280 as the left end also keeps
|
||||
@@ -720,7 +720,7 @@ function build() {
|
||||
* ships them, and nothing in a package manager can read `uci luci.main.lang` to fetch the right
|
||||
* one: apk learns it from `install-if` against `luci-i18n-base-<lang>` (owfeed.yml), opkg has no
|
||||
* conditional form of that at all, and a router upgraded from 0.14.3 through the feed simply
|
||||
* loses the catalogue that used to ride inside the theme (issue #41). `install.sh` covers its
|
||||
* loses the catalogue that used to ride inside the theme. `install.sh` covers its
|
||||
* own path; this covers the one nobody ran a script on.
|
||||
*
|
||||
* ASKED OF THE PAGE, not of the package list: the theme has no ubus call of its own and must not
|
||||
@@ -828,7 +828,7 @@ function build() {
|
||||
]);
|
||||
|
||||
/* The first fill, deferred one microtask so the tree above is finished. It does not wait for
|
||||
* the form to be in the document: every readout resolves inside fs-widgets against a hidden
|
||||
* the form to be in the document: every readout resolves against a hidden
|
||||
* probe attached to <body>, so a detached form still reads the live palette. */
|
||||
Promise.resolve().then(refreshColours);
|
||||
return page;
|
||||
|
||||
@@ -212,8 +212,8 @@ function _sanitizeSvg(text) {
|
||||
* (Chromium and WebKit decline to run XSLT on an image/svg+xml document). A tile has no use for
|
||||
* one, and without the PI the embedded stylesheet is never applied. */
|
||||
|
||||
/* An external-DTD DOCTYPE prologue is left unvisited on purpose: verified-safe residual finding,
|
||||
* docs/architecture.md "The SVG sanitizer's residual, verified-safe findings". */
|
||||
/* An external-DTD DOCTYPE prologue is left unvisited on purpose: no engine resolves an external DTD, and
|
||||
* the CGI answers a direct open with CSP `default-src 'none'; sandbox`. */
|
||||
for (const n of [ ...doc.childNodes ])
|
||||
if (n.nodeType === Node.PROCESSING_INSTRUCTION_NODE) { n.remove(); elements++; }
|
||||
|
||||
@@ -253,9 +253,8 @@ function _sanitizeSvg(text) {
|
||||
* one the spec gives them — a change to how SVG itself is interpreted, not something a
|
||||
* check here could see coming or catch. */
|
||||
if ((/^on[a-z]+$/).test(n)) { el.removeAttribute(a.name); refs++; continue; }
|
||||
/* A TAB-obfuscated `javascript:` (`href="javas	cript:alert(1)"`) passes this regex:
|
||||
* verified-safe residual finding, docs/architecture.md "The SVG sanitizer's residual,
|
||||
* verified-safe findings". */
|
||||
/* A TAB-obfuscated `javascript:` (`href="javas	cript:alert(1)"`) passes this regex but
|
||||
* is only ever an image source, never a navigable `href`, and a direct open is CSP-sandboxed. */
|
||||
if ((/^javascript:/i).test(v)) { el.removeAttribute(a.name); refs++; continue; }
|
||||
/* Security review finding (LOW): `xml:base` (XML Base, W3C) is not itself a reference —
|
||||
* it changes what every RELATIVE one in the subtree UNDER IT resolves against. A perfectly
|
||||
@@ -273,8 +272,8 @@ function _sanitizeSvg(text) {
|
||||
* consistent with this file avoiding a second grammar it would have to maintain. */
|
||||
if (n === 'xml:base') { el.removeAttribute(a.name); refs++; continue; }
|
||||
/* A presentation attribute's `url(` (`fill`, `stroke`, `filter`, …) is only checked below
|
||||
* for `style`, not here: verified-safe residual finding, docs/architecture.md "The SVG
|
||||
* sanitizer's residual, verified-safe findings". */
|
||||
* for `style`, not here: the bundle is only an image source, so the
|
||||
* worst reach is a second fetch with no script and no navigation. */
|
||||
if (n === 'style') {
|
||||
const cleaned = _sanitizeStyleValue(v);
|
||||
if (cleaned.removed) {
|
||||
@@ -304,7 +303,7 @@ function _sanitizeSvg(text) {
|
||||
* The other axes are per-browser with a router default; this one has no browser layer. An admin
|
||||
* uploads an image once, it becomes the router-wide background for every device and shows
|
||||
* pre-login, so it is absent from AXIS_KEYS, snapshotAxes() and matchesSavedDefault() — it must not
|
||||
* move the Save button — and needs no factory, so tools/axes.mjs never sees it.
|
||||
* move the Save button — and needs no factory, so axis-contract checks never see it.
|
||||
*
|
||||
* The image is a served file, uhttpd having no gzip to make inlining it in every <head> viable;
|
||||
* only its cache-bust token lives in uci -> window.__fsSD -> the url() head.ut stamps. The path is
|
||||
|
||||
@@ -16,14 +16,13 @@
|
||||
* What stayed behind in `fs-prefs`, and why:
|
||||
* - the localStorage wrappers and `sd()`, which everything here calls through `prefs.`
|
||||
* - dark mode, because `guardDarkStamp` defends against a third-party app on every page and
|
||||
* `tools/chrome-fence.mjs` holds `stampDark()` to that file by path
|
||||
* `stampDark()` is checked against that file by path
|
||||
* - layout, density, rail and auto-collapse, because the chrome and the menu apply them live —
|
||||
* and because `tools/scroll-anchor.mjs` and `tools/scroll-jank.mjs` stamp layout and density
|
||||
* through `L.require('fs-prefs')` to sweep their matrix
|
||||
* and because external probes stamp layout and density through `L.require('fs-prefs')`
|
||||
*
|
||||
* The pre-paint in head.ut has already stamped every axis before the first frame, so nothing here
|
||||
* is needed to PAINT a page correctly — only to change one from the form. tools/axes.mjs reads the
|
||||
* whole resources directory rather than a path, precisely so an axis may live in a second file. */
|
||||
* is needed to PAINT a page correctly — only to change one from the form. The axis list is read
|
||||
* from the whole resources directory rather than one path, so an axis may live in a second file. */
|
||||
|
||||
const FS_RADIUS_DEFAULT = 12;
|
||||
|
||||
@@ -117,7 +116,7 @@ function propAxis(key, sdKey, prop, min, max, dfl, fmt, after) {
|
||||
* it and the first touch of any other control takes it away.
|
||||
*
|
||||
* Legacy names ('rvht'/'roman'/'github') are migrated by head.ut before paint, so they never reach
|
||||
* currentPalette() on a loaded page; the stray fallthrough covers them anyway. */
|
||||
* PALETTE.current() on a loaded page; the stray fallthrough covers them anyway. */
|
||||
|
||||
const PALETTES = [ 'hicontrast', 'bootstrap', '2020', 'forum' ]; /* the non-default values; 'footstrap' = bare :root */
|
||||
const PALETTE = prefs.listAxis('fs-palette', 'data-palette', PALETTES, 'footstrap');
|
||||
@@ -204,7 +203,7 @@ const DANGER = colorAxis('fs-danger', 'data-danger', '--fs-danger-h', '--fs-dang
|
||||
/* Rounding: the propAxis instance (default const and rationale up top), --fs-radius-base in px. */
|
||||
const RADIUS = propAxis('fs-radius', 'rounding', '--fs-radius-base', 0, 20, FS_RADIUS_DEFAULT, (v) => (v + 'px'));
|
||||
|
||||
/* Content width (issue #44): how far the reader lets the column grow past --fs-content-max's own
|
||||
/* Content width: how far the reader lets the column grow past --fs-content-max's own
|
||||
* 1280px. A propAxis like Rounding, pointed at that one token — the range comment lives in
|
||||
* 02-tokens.css, next to the token it bounds, and is not restated here.
|
||||
*
|
||||
@@ -462,21 +461,9 @@ const PATTERN = tokenAsset('pattern', '/cgi-bin/luci-theme-footstrap-pattern', '
|
||||
* run. */
|
||||
const LOGIN_BG = tokenAsset('login_bg', '/luci-static/footstrap/bg', '--fs-login-bg-url');
|
||||
|
||||
/* Every axis exported as the OBJECT itself — `{ current, apply, def? }` — not a flat
|
||||
* currentX()/applyX() pair per axis: a caller wanting both already had to carry them as a pair
|
||||
* (fs-appearance.js's colourGroup() takes `{ current, apply }`, built ad hoc from two separate
|
||||
* lookups before this), and a flat name is one more thing every consumer of THIS FILE has to keep
|
||||
* spelled the same way fs-axes.js does — 18 axis names doubled to 36 lookups, now 18. `pattern`
|
||||
* and `loginBg` are not axes (no AXIS_KEYS entry, no router-default `def()`) but the same
|
||||
* `tokenAsset()` shape, so they export the same way. `tokenOk`, `matchesSavedDefault`,
|
||||
* `saveAsDefault`, `resetToSaved` and `resetToBuiltin` stay flat: none of them is a
|
||||
* `{current, apply}` pair. `snapshotAxes` stays module-private: fs-appearance.js, its only would-be
|
||||
* caller, builds the Appearance form from the per-axis exports above, not from the router-default
|
||||
* snapshot matchesSavedDefault()/saveAsDefault() read internally.
|
||||
*
|
||||
* No flat `currentX`/`applyX` fallback kept: grepped `../luci-app-footstrap-*` (the sibling
|
||||
* checkouts) and this repo's docs for either spelling — nothing outside fs-appearance.js and
|
||||
* fs-assets.js, both updated alongside this file, ever called one. */
|
||||
/* Every axis is exported as the object itself — `{ current, apply, def? }` — not a flat
|
||||
* currentX()/applyX() pair. `pattern` and `loginBg` have the same tokenAsset() shape and export the
|
||||
* same way; the router-default helpers (tokenOk, matchesSavedDefault, ...) stay flat. */
|
||||
return baseclass.extend({
|
||||
palette: PALETTE,
|
||||
wallpaper: WALLPAPER,
|
||||
|
||||
@@ -103,14 +103,14 @@ let _probe = null;
|
||||
* it carries no chrome mark and the fence deliberately does not spare it. Every declaration is
|
||||
* therefore !important, which a style-attribute wins outright: an app carrying
|
||||
* `div { min-width: 500px !important }` otherwise wins every read and the sidebar folds into a bar
|
||||
* on a 1857px desktop (issue #19) — the cut becomes 500 + 500 + 1000 = 2000 CSS px, which is why it
|
||||
* on a 1857px desktop — the cut becomes 500 + 500 + 1000 = 2000 CSS px, which is why it
|
||||
* was reported as a zoom bug: Chrome at 90% gives 2063 CSS px and passes, 100% gives 1857 and
|
||||
* fails. box-sizing is stated for the same reason — getComputedStyle().width is
|
||||
* the content box, so a foreign `border-box` plus padding would shave the reading. */
|
||||
function resolveLen(token, dflt) {
|
||||
if (!_probe) {
|
||||
_probe = document.createElement('div');
|
||||
/* the id is fs-router.js's strayBodyNode() (issue #56) reading an fs-* id as "ours", which is
|
||||
/* the id is fs-router.js's strayBodyNode() reading an fs-* id as "ours", which is
|
||||
* a different question from the chrome mark above: this stays unmarked (no data-fs-chrome,
|
||||
* so the fence still does not spare it from a hostile !important) but still needs SOME fs-*
|
||||
* name, or an element appended once and never removed would read as body litter and force
|
||||
@@ -165,7 +165,7 @@ function shellGeometry() {
|
||||
};
|
||||
/* Plausibility, at the cost of one comparison: the rail is the sidebar collapsed, so
|
||||
* 0 < railW < sidebarW holds by construction. Both known failures destroy it — a hijacked probe
|
||||
* reports one foreign width for all four (issue #19), a renamed or absent token reports 0 for
|
||||
* reports one foreign width for all four, a renamed or absent token reports 0 for
|
||||
* all four (an abs-positioned empty div shrinks to 0, which is finite, so the per-read fallback
|
||||
* never fires). Only the relation between the numbers gives either away. */
|
||||
const sane = (g.railW > 0 && g.railW < g.sidebarW && g.contentMin > 0);
|
||||
@@ -231,7 +231,7 @@ function columnWidth(g, state) {
|
||||
/* The column stops growing: `.fs-content` is `max-width: var(--fs-content-max); margin: 0
|
||||
* auto`, so past ~1500px the surplus becomes margin, and without the cap this answers ~2280 on
|
||||
* a 2560px window for a column that is 1224 wide. No caller can reach that today (both ask a
|
||||
* lower bound), but this is the exported answer to "how wide is the content column". */
|
||||
* lower bound), but this is the answer to "how wide is the content column". */
|
||||
const room = Math.min(state.outerW - cut, g.contentMax);
|
||||
return Math.max(0, room - g.contentPad);
|
||||
}
|
||||
@@ -279,11 +279,11 @@ function fitChrome() {
|
||||
* currently needs — not only shorter. A `min-height` floor alone stops the shrink but not the
|
||||
* grow: with the classes off and `fs-dense1`/`fs-dense2` stripped by `fitTabStrips()`, this pass
|
||||
* measured the bar walking 230 -> 202 -> 164 -> 144 -> 123 -> 131 -> 123px against a settled
|
||||
* 123px on owrt2512 at 767px — 107px of growth a floor never sees, on top of the shrink it does
|
||||
* 123px at 767px — 107px of growth a floor never sees, on top of the shrink it does
|
||||
* — each step landing between two of the poll's own separate section refreshes, so the browser
|
||||
* paints in between and the reader is moved by exactly as much, on Chromium and Firefox as well
|
||||
* as Safari (`tools/fit-quiet.mjs`, plus a probe that pauses the layout mid-pass). `min-height`
|
||||
* alone was measured to `accc451`'s WebKit-only diagnosis instead — WebKit's own scroll anchoring
|
||||
* as Safari (measured with a probe that pauses the layout mid-pass). `min-height`
|
||||
* alone was measured against an earlier WebKit-only diagnosis instead — WebKit's own scroll anchoring
|
||||
* looked like the whole story only because it is the one engine with no anchoring at all to hide
|
||||
* this walk behind; Chromium and Firefox absorb it the same way they absorb any other layout
|
||||
* change, which is not the same as not producing it.
|
||||
@@ -484,7 +484,7 @@ function wireRail() {
|
||||
}
|
||||
|
||||
/* An indicator pill carries its meaning as prose ("Unsaved Changes: 2") and the collapsed rail is
|
||||
* 68px wide, so the pill wraps onto three lines and hangs past the rail's edge (issue #14). CSS
|
||||
* 68px wide, so the pill wraps onto three lines and hangs past the rail's edge. CSS
|
||||
* squares the pill there and draws this attribute instead of the label — a text node cannot be
|
||||
* reached by a selector, so the badge is lifted into an attribute here.
|
||||
*
|
||||
@@ -586,8 +586,7 @@ return baseclass.extend({
|
||||
* motion sampler `scrolling()` reads). A caller landing in that window, most of all
|
||||
* fs-select's, got the width the PREVIOUS viewport had: at 568px settling to 390px, model
|
||||
* stayed 568 for up to 220ms of the 400 (measured: −178px, exactly 568−390, against a probe
|
||||
* resizing the window mid-settle, and live-audit's `geometry|fs-content` finding on
|
||||
* owrt2410, CI run 34364446910).
|
||||
* resizing the window mid-settle, and a live geometry audit on 24.10).
|
||||
*
|
||||
* So the window's own width is compared fresh on every call, not only when `_shellOuter`
|
||||
* is still zero. `clientWidth` is the one read the old "no layout read" promise here was
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require ui';
|
||||
|
||||
/* fs-fit — the theme's one "does it still fit?" engine and the one place the reader's scroll
|
||||
* position is corrected. Add fit logic here, never a second observer: no CSS query can ask what the
|
||||
@@ -15,9 +14,9 @@
|
||||
* without it left every data table invisible. docs/anchoring-log.md, "The arm belongs to the disarm". */
|
||||
function armGate() {
|
||||
if (!fittersEnabled()) return;
|
||||
/* WRITTEN AS THE LITERAL `dataset.fsFit`, never through a helper: tools/table-contract.mjs
|
||||
* reads this file for exactly that spelling to prove the gate rule is still armed, and an
|
||||
* indirection hides the write from it. The behaviour survives being factored out; the
|
||||
/* WRITTEN AS THE LITERAL `dataset.fsFit`, never through a helper: the theme's own
|
||||
* repository checks this file for exactly that spelling to prove the gate rule is still armed,
|
||||
* and an indirection hides the write from it. The behaviour survives being factored out; the
|
||||
* contract does not. */
|
||||
try { document.documentElement.dataset.fsFit = '1'; }
|
||||
catch (e) { /* no document, no flag to write */ }
|
||||
@@ -60,14 +59,13 @@ const FLOORED = '[data-fs-floor]';
|
||||
|
||||
/* One floor per container, cleared and re-measured in one batched pass: not while the reader
|
||||
* scrolls, and not on a table box — `min-height` is undefined there (CSS 2.1 §10.7) and a 313px
|
||||
* floor still collapsed to 30px on WebKit. The clear is what keeps the answer honest (issue #41). */
|
||||
* floor still collapsed to 30px on WebKit. The clear is what keeps the answer honest. */
|
||||
function holdFloor(records) {
|
||||
if (scrolling()) return;
|
||||
const host = document.getElementById('view');
|
||||
if (!host) return; /* the login page has no view */
|
||||
/* boxes stays an array — order feeds the parallel `hs` below and dirty's own .filter() — with a
|
||||
* Set alongside it purely for the O(1) "already have this one" check .indexOf() was doing in
|
||||
* O(n). */
|
||||
* Set alongside it for the O(1) "already have this one" check. */
|
||||
const boxes = [], boxSeen = new Set(), hs = [];
|
||||
host.querySelectorAll(SHRINKS).forEach((el) => {
|
||||
let box = el, cs = window.getComputedStyle(el);
|
||||
@@ -376,8 +374,9 @@ function anchorRef() {
|
||||
|
||||
/* Two independent diagnostic trails — one per correction path (the frame-coalesced anchor below,
|
||||
* the late drift correction further down) — same shape twice: the last reason recorded, and the
|
||||
* last 8 with a timestamp, for tools/scroll-anchor.mjs to read back after a probe run. One factory,
|
||||
* so the cap and the timestamp format cannot drift between the two. */
|
||||
* last 8 with a timestamp. They are read back only by the theme repository's
|
||||
* development build; the package exports none of it. One factory, so the cap and the timestamp
|
||||
* format cannot drift between the two. */
|
||||
const TRAIL_MAX = 8;
|
||||
function makeTrail() {
|
||||
let last = null;
|
||||
@@ -626,11 +625,11 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
scrolling,
|
||||
/* unmarked, for tools/scroll-anchor.mjs — see `makeTrail()` above */
|
||||
lateWhy: () => _late.last(),
|
||||
lateTrail: () => _late.entries().slice(),
|
||||
anchorWhy: () => _anchor.last(),
|
||||
anchorTrail: () => _anchor.entries().slice(),
|
||||
/* probe-only: the two trails of `makeTrail()` above */
|
||||
lateWhy: () => _late.last(), /* fs:probe */
|
||||
lateTrail: () => _late.entries().slice(), /* fs:probe */
|
||||
anchorWhy: () => _anchor.last(), /* fs:probe */
|
||||
anchorTrail: () => _anchor.entries().slice(), /* fs:probe */
|
||||
deferMeasurement,
|
||||
|
||||
/* -> the offset this file last took a reference at, so a probe does not measure the guard instead
|
||||
|
||||
@@ -45,7 +45,7 @@ function sectionTitle(sec) {
|
||||
|
||||
/* ---- 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
|
||||
* On 25.12, 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).
|
||||
@@ -89,7 +89,7 @@ function wireDisclosure(sec) {
|
||||
widgets.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 */
|
||||
* reasoning as the aria-hidden on svgIcon()'s output, fs-widgets.js */
|
||||
widgets.syncAttr(label, 'aria-hidden', 'true');
|
||||
|
||||
if (h.dataset.fsWired) return;
|
||||
@@ -148,8 +148,8 @@ function arrange() {
|
||||
/* an SPA nav can leave the observer wired while another page renders into #view: detach as soon
|
||||
* as the route stops being the overview. body[data-page] carries the DISPATCH path from both
|
||||
* the server template and the router, so /admin/status (firstchild -> overview) matches. */
|
||||
if ((document.body.getAttribute('data-page') || '') !== /* spelled out, not hoisted: tools/page-modules.mjs reads this value out of the module's
|
||||
* SOURCE to check it against the map in menu-footstrap-common.js */
|
||||
if ((document.body.getAttribute('data-page') || '') !== /* spelled out, not hoisted: this value is read out of the module's
|
||||
* SOURCE and checked against the map in menu-footstrap-common.js */
|
||||
'admin-status-overview') {
|
||||
stopWatch();
|
||||
return;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
* axis list is AXIS_KEYS, which is exactly the fields of snapshotAxes() — what Save-as-default
|
||||
* writes.
|
||||
* All client-side, instant and persisted in localStorage, with head.ut's inline script re-applying
|
||||
* them before paint so a reload never flashes the wrong one; tools/axes.mjs derives the contract
|
||||
* from this file and holds the two copies to it.
|
||||
* them before paint so a reload never flashes the wrong one; the contract is derived
|
||||
* from this file and both copies are held to it.
|
||||
*
|
||||
* ---- three layers, and the browser always wins ----
|
||||
* Every axis resolves as localStorage ?? router-default ?? built-in. The router default is
|
||||
@@ -79,14 +79,13 @@ function currentMode() {
|
||||
*
|
||||
* `data-darkmode` is the name the theme's own CSS keys off. The other two are outbound
|
||||
* compatibility, like the `--*-color-*` export tier: nothing in `styles/` may read them, and
|
||||
* tools/axes.mjs fails the build if it does. */
|
||||
* a check fails if one does. */
|
||||
/* the attribute name reused below by the writer, the guard's reader and both MutationObserver
|
||||
* filters (measured: 15 B x4 -> 28 B, 32 B saved) */
|
||||
function stampDark(root, dark) {
|
||||
/* the literal stays spelled out HERE: tools/axes.mjs reads the attribute names out of
|
||||
* this function's SOURCE, so a hoisted const reads as no attribute at all and the gate
|
||||
* reports the pre-paint and the live applier as drifted. The 45 B a const would save are
|
||||
* not worth teaching a gate to resolve them. */
|
||||
/* the literal stays spelled out HERE: the attribute names are read out of this
|
||||
* function's SOURCE, so a hoisted const reads as no attribute at all and the pre-paint and the
|
||||
* live applier look drifted. The 45 B a const would save are not worth resolving them. */
|
||||
root.setAttribute('data-darkmode', dark ? 'true' : 'false');
|
||||
root.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
|
||||
@@ -222,7 +221,7 @@ function listAxis(key, attr, values, dflt, after) {
|
||||
}
|
||||
|
||||
/* A two-value axis: `on` is stamped as the attribute's value, `off` is a bare :root. The list shape
|
||||
* with a list of one — kept as its own name because tools/axes.mjs matches the call, and because
|
||||
* with a list of one — kept as its own name because the call is matched by name, and because
|
||||
* "two-valued" is what most of these axes are. */
|
||||
function enumAxis(key, attr, on, off) {
|
||||
return listAxis(key, attr, [ on ], off);
|
||||
@@ -233,9 +232,9 @@ const DENSITY = listAxis('fs-density', 'data-density', DENSITIES, 'normal', () =
|
||||
const currentDensity = DENSITY.current, applyDensity = DENSITY.apply,
|
||||
densityDefault = DENSITY.def;
|
||||
|
||||
/* Content width lives entirely in fs-axes.js now (a propAxis like Rounding, issue #44): unlike
|
||||
/* Content width lives entirely in fs-axes.js (a propAxis like Rounding): unlike
|
||||
* Density it sets no attribute, so nothing here or in fs-chrome.js needs to read one, and
|
||||
* currentContentWidth()/applyContentWidth() have no caller outside the Appearance tab — the one
|
||||
* contentWidth.current()/apply() have no caller outside the Appearance tab — the one
|
||||
* thing that kept Density's shape in this cold-path file. */
|
||||
function currentLayout() {
|
||||
return document.documentElement.getAttribute('data-layout') === 'top' ? 'top' : 'sidebar';
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
* The path->node half lives in fs-menutree.js (the chrome needs it too); the "has a view poisoned
|
||||
* this document with its CSS?" half in fs-sheets.js.
|
||||
*
|
||||
* `docs/…`, `tools/…` and `tests/…` in these comments name the theme's own repository
|
||||
* `docs/…` in these comments names the theme's own repository
|
||||
* (https://github.com/VizzleTF/luci-theme-footstrap), not the tree this file is read in: the
|
||||
* package ships the runtime and nothing else. */
|
||||
|
||||
@@ -130,10 +130,7 @@ function clearViewIntervals() {
|
||||
_viewIntervals.forEach((spec, id) => { if (id !== keep) window.clearInterval(id); });
|
||||
}
|
||||
/* The "Refreshing"/"Paused" pill must not outlive the poll it reports on — see the `poll-stop`
|
||||
* listener below, which is the only caller in the shipped file. Exported (fs:probe) too, because
|
||||
* navigate()'s own async chain (require(), staging into a real `.fs-content`) needs more DOM than
|
||||
* tests/lib's fakes give it; this is the one check that actually matters for the ordering bug and is
|
||||
* exercised directly, without the listener's deferral, by tests/poll-status.test.mjs. */
|
||||
* listener below, which is its only caller. */
|
||||
function hidePollIndicatorIfEmpty() {
|
||||
if (!(L.Poll && L.Poll.queue && L.Poll.queue.length === 0)) return;
|
||||
try { ui.hideIndicator('poll-status'); }
|
||||
@@ -199,8 +196,7 @@ function flushUciCache() {
|
||||
* is `uci.sections('wireless', 'wifi-device')`). So dropping those packages does not make
|
||||
* network.js refetch them, it makes every consumer read an EMPTY config until the next full
|
||||
* load: Channel Analysis with no band tabs, Network -> Switch with no VLAN sections, both
|
||||
* correct again after F5. tools/spa-parity.mjs reproduces it; tools/upstream-contract.mjs
|
||||
* notices if the list of three moves.
|
||||
* correct again after F5.
|
||||
*
|
||||
* navigate() waits for the refill, because a cached module resolves within a microtask and the
|
||||
* view would read the cache we just emptied. Only when network.js is really in the document.
|
||||
@@ -338,19 +334,15 @@ function discard(el) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- a page that leaves nodes as direct children of <body> hands the next page over by full load,
|
||||
* the same way an invasive foreign sheet does (docs/third-party-apps.md, Rule 2 extended from CSS to
|
||||
* the DOM — issue #56) ----
|
||||
/* ---- a page that parks nodes on <body> makes the next navigation a full load ----
|
||||
*
|
||||
* luci-app-bandix's render() appends 7 tooltip/modal nodes to document.body on every visit,
|
||||
* unconditionally (reproduced on owrt2512 and owrt2410) — nothing here sweeps them; the reasoning is
|
||||
* docs/spa-router.md, "Foreign view DOM". The recorder is header.ut's first <body> <script>, filling
|
||||
* window.__fsBodyAdds; this file only reads it. */
|
||||
* Some apps' render() appends tooltip/modal nodes to document.body on every visit (one leaves 7)
|
||||
* and nothing here sweeps them, so the document is spent, as with an invasive foreign sheet. The
|
||||
* recorder is header.ut's first <body> <script>, filling window.__fsBodyAdds; this file only reads it. */
|
||||
|
||||
/* -> true if `el` is a node the theme must treat as stray body litter: not the theme's own chrome,
|
||||
* not something stock LuCI parks there itself, not a node type that can never paint. Exported and
|
||||
* pure (no DOM writes, no window.__fsBodyAdds read) so tests/body-litter.test.mjs can drive it
|
||||
* without a router or a recorder. */
|
||||
* not something stock LuCI parks there itself, not a node type that can never paint. Pure: no DOM
|
||||
* writes, no window.__fsBodyAdds read. */
|
||||
function strayBodyNode(el) {
|
||||
if (!el || el.nodeType !== 1) return false;
|
||||
/* never rendered on their own, wherever a script parks them */
|
||||
@@ -358,16 +350,14 @@ function strayBodyNode(el) {
|
||||
case 'SCRIPT': case 'STYLE': case 'LINK': case 'TEMPLATE': case 'NOSCRIPT': case 'META':
|
||||
return false;
|
||||
}
|
||||
/* zone 1, ours (docs/third-party-apps.md): the chrome mark, or an fs-* id/class. All of these
|
||||
/* ours: the chrome mark, or an fs-* id/class. All of these
|
||||
* parent straight to <body> and must never be read as litter: this file's own #fs-nav-progress,
|
||||
* fs-search.js's #fs-search-ov (which also carries the mark), and the geometry/colour probes
|
||||
* fs-chrome.js and fs-appearance.js each park on <body> once and never remove — an fs-* id is
|
||||
* what tells them from bandix's own unmarked nodes below. */
|
||||
/* read through `dataset`, not a literal `hasAttribute()` call: `tools/chrome-fence.mjs` counts
|
||||
* every single-quoted occurrence of the chrome mark's own name (as a JS string) across this
|
||||
* whole tree as a JS-BUILT chrome root (EXPECT_JS_ROOTS) — right for fs-search.js's palette,
|
||||
* which mounts one, and a false positive here, which only READS the mark on somebody else's
|
||||
* node and builds nothing. camelCase avoids the exact spelling the gate scans for. */
|
||||
* what tells them from a foreign app's own unmarked nodes below. */
|
||||
/* read through `dataset`, not a literal `hasAttribute()` call: the build counts every
|
||||
* single-quoted spelling of the chrome mark's name as a JS-built chrome root, a false positive
|
||||
* for code that only READS the mark on somebody else's node. camelCase avoids that spelling. */
|
||||
if (el.dataset && el.dataset.fsChrome !== undefined) return false;
|
||||
if (el.id && el.id.indexOf('fs-') === 0) return false;
|
||||
for (const c of el.classList) if (c.indexOf('fs-') === 0) return false;
|
||||
@@ -497,7 +487,7 @@ function restoreScroll(pos, gen) {
|
||||
* their own RPCs. The engine then clamps the scroller BACK to whatever height exists NOW, firing
|
||||
* an ordinary `scroll` event that looks exactly like a reader's, and it lands before this tick has
|
||||
* ever written anything (`wroteWin`/`wroteMain` still -1), so the "our own write coming back"
|
||||
* check above cannot catch it either. Measured live (owrt2512b @1440, `/admin/status/overview` <-
|
||||
* check above cannot catch it either. Measured live (1440px wide, `/admin/status/overview` <-
|
||||
* package-manager, Back): the UA restores `window.scrollY` to the parked 2684 in the same tick
|
||||
* `popstate` fires, `commitStage()` leaves the document ~900px tall for one frame, and the next
|
||||
* native `scroll` event reports `y=0` a whole 5 s before this function's own deadline — cancelling
|
||||
@@ -847,7 +837,7 @@ function commitStage(stage, contentHost) {
|
||||
* with the transition and 213 ms without it, and the swap itself landed at 3,728 ms against 206 ms.
|
||||
* The reader spends that time looking at the page they navigated away from, under the new URL —
|
||||
* which is what a report of "the section is not where I expect it, F5 fixes it" looks like from the
|
||||
* outside (issue #42), F5 being a full load and starting no transition.
|
||||
* outside (F5 being a full load and starting no transition.
|
||||
*
|
||||
* A deadline was tried and does not hold: `skipTransition()` runs the update callback at once
|
||||
* (3-5 ms on WebKit, Chromium and Firefox alike), but it has to be called from a timer, and the
|
||||
@@ -947,8 +937,8 @@ function navigate(pathname, push, kbd) {
|
||||
* only exit leaving both pages correct is a real navigation (fs-sheets.js) */
|
||||
if (sheets.documentPoisoned()) return false;
|
||||
|
||||
/* …and a page that left its own nodes as direct children of <body> is spent the same way — Rule 2
|
||||
* extended to the DOM, issue #56. See strayBodyNode()/bodyLittered() above. */
|
||||
/* …and a page that parked its own nodes on <body> is spent the same way: see
|
||||
* strayBodyNode()/bodyLittered() above. */
|
||||
if (bodyLittered()) return false;
|
||||
|
||||
/* …and a document whose session has died is spent the same way: the only page it can render
|
||||
@@ -1011,8 +1001,8 @@ function navigate(pathname, push, kbd) {
|
||||
/* the outgoing page's links are about to become a detached tree — do not hold one of them */
|
||||
_lastHovered = null;
|
||||
/* Run every registered navigation callback. The seam is inverted on purpose: a registrant calls
|
||||
* onNavigate() and the router names nobody, so an optional module that is not installed cannot
|
||||
* be a DependencyError taking out the whole chrome.
|
||||
* onNavigate() and the router names nobody, so the router keeps no static dependency
|
||||
* (a DependencyError would take out the whole chrome).
|
||||
*
|
||||
* The RESOLVED segments are passed in, because this runs before L.env is re-pointed below and a
|
||||
* callback reading L.env.dispatchpath would record the page being left.
|
||||
@@ -1061,7 +1051,7 @@ function navigate(pathname, push, kbd) {
|
||||
/* A full load starts at the top and the in-place swap must too, or navigating away from a long
|
||||
* page opens the next one mid-scroll. Both scrollers are reset, since which one scrolls depends
|
||||
* on the layout — in the sidebar layout the window does not scroll, `.fs-shell` being 100dvh
|
||||
* with `.fs-main` owning overflow-y (issue #7) — and scrollTo on the other is a no-op.
|
||||
* with `.fs-main` owning overflow-y — and scrollTo on the other is a no-op.
|
||||
*
|
||||
* The WRITE is at commitStage now, not here — see there. `_rest` is forgotten here regardless:
|
||||
* it is the reference fs-fit tells a reader-caused scroll from an engine's clamp with, and the
|
||||
@@ -1158,7 +1148,7 @@ function navigate(pathname, push, kbd) {
|
||||
* see the listener below, not a call here. A page can also empty the queue on its own, with
|
||||
* no navigation at all (`L.Poll.remove()`, e.g. `luci-mod-status`'s graphs.js on unload,
|
||||
* `luci-app-banip`'s log view), and `stop()` dispatches the same event for that call too;
|
||||
* a call placed only here would miss it, exactly the finding this once shipped without. */
|
||||
* a call placed only here would miss it. */
|
||||
L.Poll.start();
|
||||
}
|
||||
/* kill the outgoing view's plain setInterval pollers too, as a full load would; L.Poll's own
|
||||
@@ -1313,9 +1303,8 @@ function bootDocumentIsOurs() {
|
||||
* Every module here is written against parts of somebody else's code that were never an API:
|
||||
* `L.Poll` is a deprecated alias, `L.dom.content` and `ui.instantiateView` are what `view.ut`
|
||||
* happens to use, `Request.addInterceptor` is how the session probe hears a 403.
|
||||
* tools/upstream-contract.mjs asks whether they still BEHAVE as assumed, but only against the two
|
||||
* userlands this repo owns. On a router carrying a luci-base that moved, the first anyone learns of
|
||||
* it is a click that opens nothing.
|
||||
* Existence is not behaviour: a name can survive and act differently. On a router carrying a
|
||||
* luci-base that moved, the first anyone learns of it is a click that opens nothing.
|
||||
*
|
||||
* So existence is checked at boot, once, and a missing name turns the router OFF rather than
|
||||
* on-and-broken: the page is then the server-dispatched MPA the theme was before the router
|
||||
@@ -1443,7 +1432,7 @@ function wireRouter() {
|
||||
/* A fragment change is not a navigation. Chrome fires `popstate` for a same-document
|
||||
* fragment nav, so an `<a href="#">` inside a view — a common idiom for in-page controls —
|
||||
* arrives here as if Back had been pressed, and re-running navigate() re-instantiates the
|
||||
* view and wipes the state the click just set (issue #3). The view changed only if the PATH
|
||||
* view and wipes the state the click just set. The view changed only if the PATH
|
||||
* changed. */
|
||||
if (window.location.pathname === _curPath)
|
||||
return;
|
||||
@@ -1467,34 +1456,13 @@ function wireRouter() {
|
||||
|
||||
/* ---- the poll-status pill must not outlive the poll it reports on ----
|
||||
*
|
||||
* LuCI shows the pill on `poll-start`, flips it to "Paused" on `poll-stop`, and never hides it
|
||||
* again. Two different callers empty the queue and reach `L.Poll.stop()`: this router's own
|
||||
* navigate() teardown, and a VIEW emptying its own queue mid-page with no navigation at all
|
||||
* (`L.Poll.remove()` — `luci-mod-status`'s Realtime Graphs on unload, `luci-app-banip`'s log
|
||||
* template) — both dispatch the same `poll-stop`, so one listener here covers both, where a call
|
||||
* placed only in navigate()'s teardown covered the first and silently missed the second: reproduced
|
||||
* live, owrt2410b and owrt2512b, add-then-remove on Statistics — `{"active":false,"q":0,"pill":
|
||||
* {"text":"Paused","clickable":true}}`, unchanged by a click, because nothing had ever asked to hide
|
||||
* a pill for a poll that a `remove()` rather than a navigation had ended.
|
||||
*
|
||||
* The HIDE itself is deferred past the synchronous dispatch, one microtask, rather than run inline
|
||||
* in this listener: `stop()` dispatches `poll-stop` synchronously to every listener registered at
|
||||
* that moment, including luci.js's own (`showIndicator('poll-status', 'Paused', null, 'inactive')` —
|
||||
* "Paused" is always shown with `handler: null`), and which of the two ran first here used to decide
|
||||
* everything — luci.js registers its listener from `setupDOM()`, after an async chain
|
||||
* (DOMContentLoaded + ui/rpc/form + probeRPCBaseURL), this module at eval, from the inline
|
||||
* `L.require('menu-footstrap')` in `footer.ut`, so network/cache timing picked the order.
|
||||
* Run with this listener first, its hide removed the span before luci.js re-created it for "Paused"
|
||||
* with no handler, and the next `poll-start` found that span already there and only changed its
|
||||
* text — clickless for the rest of the document. A microtask runs only once the whole synchronous
|
||||
* turn that dispatched the event has unwound, i.e. after every `poll-stop` listener already fired
|
||||
* regardless of which ran first, so the check sees the state stock's listener actually left rather
|
||||
* than racing it. It still reads `L.Poll.queue.length` at that later moment, not at dispatch time, so
|
||||
* a queue the SAME navigation's incoming page has already refilled by then is left alone — the
|
||||
* incoming page's own `require()`/render() is real async work (network, RPCs), always later than a
|
||||
* microtask queued during the synchronous `stop()` call that preceded it. `tests/poll-status.test.mjs`
|
||||
* drives both paths (a navigation's queue flush and a bare `Poll.remove()` to empty) in both listener
|
||||
* orders. docs/spa-router.md, "Teardown". */
|
||||
* LuCI shows the pill on `poll-start`, flips it to "Paused" on `poll-stop` and never hides it. The
|
||||
* queue is emptied by this router's navigate() teardown and also by a view calling `L.Poll.remove()`
|
||||
* mid-page; both reach `L.Poll.stop()`, so one `poll-stop` listener covers both. The hide runs one
|
||||
* microtask later: luci.js registers its own `poll-stop` listener (which shows "Paused" with no
|
||||
* click handler) after an async chain while this one registers at eval, so the order is not
|
||||
* deterministic, and hiding inline could remove the span before luci.js re-created it. The queue
|
||||
* length is read at that later moment, so a queue the incoming page has already refilled is left alone. */
|
||||
document.addEventListener('poll-stop', () => {
|
||||
queueMicrotask(hidePollIndicatorIfEmpty);
|
||||
});
|
||||
@@ -1534,8 +1502,7 @@ function wireVisibility() {
|
||||
|
||||
/* Callbacks to run on every SPA navigation, each handed the resolved segments of the INCOMING page
|
||||
* (they run before L.env is re-pointed). The registry is inverted on purpose: a registrant calls in
|
||||
* and the router names nobody, so it cannot grow a static dependency on a module that may not be
|
||||
* installed. */
|
||||
* and the router names nobody, so it cannot grow a static dependency on one. */
|
||||
const _navCbs = [];
|
||||
function onNavigate(fn) { if (typeof fn === 'function') _navCbs.push(fn); }
|
||||
|
||||
@@ -1544,28 +1511,22 @@ return baseclass.extend({
|
||||
wire: wireRouter,
|
||||
wireVisibility,
|
||||
onNavigate,
|
||||
/* exported for tests/router-contract.test.mjs (no tests ship in the package), where it is driven
|
||||
* against a hand-broken `L`: the one way to see the off branch */
|
||||
/* test-only, driven against a hand-broken `L`: the one way to see the off branch */
|
||||
contractBreaks, /* fs:probe */
|
||||
/* likewise out-of-package: interval-pause.test.mjs drives the navigation sweep around a
|
||||
* visibilitychange and session-expiry.test.mjs reads the verdict the interceptors reached.
|
||||
* navigate() is the real caller of the first and `_expired` gates the second. */
|
||||
/* test-only: the navigation sweep around a visibilitychange, and the session verdict the
|
||||
* interceptors reached */
|
||||
clearViewIntervals, /* fs:probe */
|
||||
sessionExpired, /* fs:probe */
|
||||
/* tests/poll-status.test.mjs: the deferred `poll-stop` listener's own check, called directly so a
|
||||
* test need not also fake a microtask tick to see it fire (see the comment at its definition and
|
||||
* at the listener above) */
|
||||
/* test-only: the deferred `poll-stop` listener's own check, called directly so a test need not
|
||||
* fake a microtask tick */
|
||||
hidePollIndicatorIfEmpty, /* fs:probe */
|
||||
/* fs-search warms its recents and the arrow-key-highlighted result, neither of which the
|
||||
* pointer/focus triggers above can see. The edge points search -> router, because the router
|
||||
* must keep no dependency on the palette. */
|
||||
prefetchSegs,
|
||||
/* out-of-package, like the three probe exports above (contractBreaks, clearViewIntervals,
|
||||
* sessionExpired) — prefetchSegs just above is not one of them, it is a real in-package export
|
||||
* fs-search.js calls. tests/body-litter.test.mjs drives the classifier and the counter directly,
|
||||
* and navigate() itself to prove the early return actually reaches them rather than just sitting
|
||||
* beside it unused */
|
||||
/* test-only: the body-litter classifier and counter, and navigate() to prove the early return
|
||||
* reaches them */
|
||||
strayBodyNode, /* fs:probe */
|
||||
bodyLittered, /* fs:probe */
|
||||
navigate, /* fs:probe */
|
||||
/* fs-search warms its recents and the arrow-key-highlighted result, neither of which the
|
||||
* pointer/focus triggers above can see. The edge points search -> router, because the router
|
||||
* must keep no dependency on the palette. */
|
||||
prefetchSegs
|
||||
});
|
||||
@@ -332,8 +332,8 @@ function labelCells(t, head) {
|
||||
* 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 moment any of those rules land. A `display` rule on one of these classes with no role to
|
||||
* match is a defect.
|
||||
*
|
||||
* 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
|
||||
@@ -342,9 +342,9 @@ function labelCells(t, head) {
|
||||
* 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. */
|
||||
/* tools/table-contract.mjs reads these two names and shapes by regex, so a `display` rule on any of
|
||||
* these classes/tags is checked against a matching role — keep both names, and each entry a
|
||||
* `'.class'` or `'tag'` string literal, even though roleTables() below now reads them as one list. */
|
||||
/* These two names and shapes are read by regex, so a `display` rule on any of these classes/tags
|
||||
* is checked against a matching role — keep both names, and each entry a `'.class'` or `'tag'`
|
||||
* string literal, even though roleTables() below reads them as one list. */
|
||||
const TABLE_ROLE_CLASSES = [
|
||||
[ '.table', 'table' ],
|
||||
[ '.thead', 'rowgroup' ],
|
||||
@@ -434,7 +434,7 @@ function markBreakColumn(t, rows, col) {
|
||||
}
|
||||
|
||||
/* Rung 3. The widest column that can be shredded: never the first, which is the row's identity
|
||||
* (issue #36 is that column being starved), and never a `nowrap`/`pre` one, where `overflow-wrap`
|
||||
* (that column being starved is a known fault), and never a `nowrap`/`pre` one, where `overflow-wrap`
|
||||
* is inert so the mark would buy a layout and no width.
|
||||
*
|
||||
* With honest floors nothing is starved, so an overflowing table has every column at its own
|
||||
|
||||
@@ -312,7 +312,7 @@ function documentPoisoned() {
|
||||
* already applying. Every footstrap rule lives in a @layer and an unlayered normal declaration
|
||||
* beats a layered one at any specificity, so a third-party `* { margin: 0; padding: 0 }` owns the
|
||||
* chrome outright — on a layer-less theme the same `*` (0,0,0) loses to any class selector. The
|
||||
* layers are what hand it the win (issue #8).
|
||||
* layers are what hand it the win.
|
||||
*
|
||||
* So put the sheet back on specificity footing by re-hosting it into the EXISTING `theme` layer;
|
||||
* only same-layer arbitrates by specificity. Measured on the real cascade:
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
/* The installed version, shown on the Appearance tab with no network call; which version is
|
||||
* available is the package manager's question.
|
||||
*
|
||||
* The Makefile (Build/Prepare), dev-sync.sh and tools/stage.sh sed the literal below BY FILE
|
||||
* NAME, so it cannot move to another file without changing all three. An unstamped checkout
|
||||
* The version stamp is written into the literal below BY FILE NAME, so it must
|
||||
* stay in this file. An unstamped checkout
|
||||
* stays 'dev'. */
|
||||
const FS_VERSION = '0.0.0-dev';
|
||||
|
||||
|
||||
@@ -53,15 +53,17 @@ function wirePageModules() {
|
||||
* and the warm pass that uses it, so both live here, in the file every page already loads.
|
||||
*
|
||||
* The palette reads the list back from localStorage when it opens, so the two halves share the key
|
||||
* and nothing else — the constant itself is named once, in fs-search.js (RECENT_KEY, RECENT_MAX),
|
||||
* since that module owns the "recently visited" feature; the literal below has to match it exactly. */
|
||||
* and nothing else — fs-search.js owns the "recently visited" feature, so RECENT_KEY and RECENT_MAX
|
||||
* below have to match its constants of the same name exactly. */
|
||||
const RECENT_KEY = 'fs-recent';
|
||||
const RECENT_MAX = 8;
|
||||
const RECENT_WARM = 5;
|
||||
|
||||
/* A key is a menu path — what the router can navigate to and what warmRecent() prefetches. */
|
||||
function remember(key) {
|
||||
if (typeof key !== 'string' || !key) return;
|
||||
const recent = prefs.lsGetArr('fs-recent').filter((x) => typeof x === 'string');
|
||||
prefs.lsSet('fs-recent', JSON.stringify([ key ].concat(recent.filter((p) => p !== key)).slice(0, 8)));
|
||||
const recent = prefs.lsGetArr(RECENT_KEY).filter((x) => typeof x === 'string');
|
||||
prefs.lsSet(RECENT_KEY, JSON.stringify([ key ].concat(recent.filter((p) => p !== key)).slice(0, RECENT_MAX)));
|
||||
}
|
||||
|
||||
/* ---- warm the pages this admin actually uses ----
|
||||
@@ -79,7 +81,7 @@ function warmRecent() {
|
||||
try { if (navigator.connection && navigator.connection.saveData) return; } catch (e) {}
|
||||
const here = (L.env.dispatchpath || []).join('/');
|
||||
/* already de-duplicated: remember() drops the key from its old slot before re-adding it */
|
||||
const keys = prefs.lsGetArr('fs-recent').filter((p) => typeof p === 'string');
|
||||
const keys = prefs.lsGetArr(RECENT_KEY).filter((p) => typeof p === 'string');
|
||||
const paths = keys.filter((p) => p !== here).slice(0, RECENT_WARM);
|
||||
if (!paths.length) return;
|
||||
const go = () => paths.forEach((p) => router.prefetchSegs(p.split('/')));
|
||||
@@ -199,8 +201,7 @@ function findProgressbarLabel(pg) {
|
||||
* cannot pick a wrong number out of a title carrying several. An empty title or one with no
|
||||
* percentage in any of the three shapes 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. */
|
||||
* caller, the same way `window.progressbar` clamps its own `level`, below. */
|
||||
function parseMeterPercent(title) {
|
||||
if (title == null) return null;
|
||||
const m = (/\((-?\d+)%\)\s*$/).exec(title) || (/(-?\d+)%\s*$/).exec(title) || (/^(-?\d+)%/).exec(title);
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
'require menu-footstrap-common as common';
|
||||
|
||||
/* The theme's one menu renderer: a vertical #topmenu that the CSS also turns into the top bar and
|
||||
* the rail flyouts — same markup, no second renderer. Disclosure primitives come from fs-widgets,
|
||||
* the auto-collapse preference from fs-prefs; the rest of the chrome is bootstrapped by
|
||||
* the rail flyouts — same markup, no second renderer. The disclosure primitives live in this file,
|
||||
* the auto-collapse preference comes from fs-prefs; the rest of the chrome is bootstrapped by
|
||||
* menu-footstrap-common, which this file composes with by injecting renderMainMenu into
|
||||
* common.init — a callback, not an override, since a required LuCI module is a singleton and
|
||||
* cannot be subclassed. Spec: docs/chrome.md */
|
||||
@@ -89,7 +89,7 @@ const EDGE_GAP = 8;
|
||||
/* Is this panel a bar dropdown (anchored under its item) rather than a rail flyout (anchored
|
||||
* beside it)? Same input as the stylesheet: `data-narrow` turns the sidebar into a bar and also
|
||||
* disables the rail (its rules are scoped `:not([data-narrow])`), so a narrow window is a bar even
|
||||
* with the rail on. Gating on isTopLayout() alone leaves such a panel unclamped (issue #19). */
|
||||
* with the rail on. Gating on isTopLayout() alone leaves such a panel unclamped. */
|
||||
function barDropdown() {
|
||||
return prefs.isTopLayout() || document.documentElement.hasAttribute('data-narrow');
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user