diff --git a/luci-theme-footstrap/Makefile b/luci-theme-footstrap/Makefile
index c4766a1c..790d773a 100644
--- a/luci-theme-footstrap/Makefile
+++ b/luci-theme-footstrap/Makefile
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
-PKG_RELEASE:=58
+PKG_RELEASE:=59
endif
LUCI_TITLE:=Footstrap Theme
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js
index 521666f3..8642df68 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-appearance.js
@@ -595,7 +595,13 @@ function build() {
* the scoped uci ACL). It does not change this browser — localStorage keeps overriding — so the
* saved default only shows on a fresh browser. The two Reset buttons below are the escape
* hatches, and they do not land in the same place. */
- const saveBtn = E('button', { 'class': 'btn cbi-button-action', 'type': 'button' }, [ _('Save to router', 'footstrap') ]);
+ /* the button/status label said three times below, inside this one build() call — hoisting the
+ * RESULT, not the msgid, so update-po.sh still sees the literal `_()` argument elsewhere
+ * (measured: 22 B x3 -> 27 B, 39 B saved) */
+ const SAVE_TO_ROUTER = _('Save to router', 'footstrap');
+ /* said twice below, same build() call (measured: 24 B x2 -> 30 B, 18 B saved) */
+ const RESET_TO_ROUTER = _('Reset to router', 'footstrap');
+ const saveBtn = E('button', { 'class': 'btn cbi-button-action', 'type': 'button' }, [ SAVE_TO_ROUTER ]);
/* Two resets, because two things sit underneath a browser's tweaks (fs-prefs.js): "Reset to
* router" clears them and lets every axis fall back to whatever the router holds, while "Reset
* to built-in" writes the theme's built-ins explicitly — the only way to say "as the theme
@@ -605,7 +611,7 @@ function build() {
* "Save as default" and the theme's in "Reset to default", while the router's look also
* answered to "saved" — three names for two states, asked about on the forum (topic 251930,
* post 92). The row now reads as one save and two resets, over `router` and `built-in`. */
- const resetSavedBtn = E('button', { 'class': 'btn', 'type': 'button' }, [ _('Reset to router', 'footstrap') ]);
+ const resetSavedBtn = E('button', { 'class': 'btn', 'type': 'button' }, [ RESET_TO_ROUTER ]);
/* the stock destructive class, so the button discarding every local tweak is the red one
* (theme/55-buttons.css). "Reset to router" stays neutral: it steps back to the shared state
* rather than discarding. */
@@ -627,14 +633,14 @@ function build() {
function refreshSave() {
if (prefs.storageBroken()) {
saveBtn.disabled = false;
- saveBtn.textContent = _('Save to router', 'footstrap');
+ saveBtn.textContent = SAVE_TO_ROUTER;
saveErr.textContent = _('This browser is not storing preferences (site data is blocked), so a change here lasts until you reload. Saving as default still works and applies to every browser.', 'footstrap');
saveErr.hidden = false;
return;
}
const saved = axes.matchesSavedDefault();
saveBtn.disabled = saved;
- saveBtn.textContent = saved ? _('Saved to router', 'footstrap') : _('Save to router', 'footstrap');
+ saveBtn.textContent = saved ? _('Saved to router', 'footstrap') : SAVE_TO_ROUTER;
}
saveBtn.addEventListener('click', () => {
saveBtn.disabled = true;
@@ -675,7 +681,7 @@ function build() {
location.reload();
});
}
- twoClick(resetSavedBtn, _('Reset to router', 'footstrap'), axes.resetToSaved);
+ twoClick(resetSavedBtn, RESET_TO_ROUTER, axes.resetToSaved);
twoClick(resetBtn, _('Reset to built-in', 'footstrap'), axes.resetToBuiltin);
refreshSave(); /* correct label and enabled state before the first paint */
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
index 12ad604e..e5986b11 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-chrome.js
@@ -134,6 +134,9 @@ function resolveLen(token, dflt) {
* The defaults are stated once so the fallbacks and the sanity net below cannot restate the
* stylesheet's widths in two places. Reaching for them means the measurement failed. */
const GEOM_DFLT = { contentMin: 500, sidebarW: 224, railW: 68, contentPad: 56, contentMax: 1280 };
+/* the class name toggled below by fitShell's own escalation and by fitCluster's (measured: 16 B x3
+ * -> 25 B, 23 B saved) */
+const CLASS_IND_COMPACT = 'fs-ind-compact';
let _geom = null, _geomDensity = null, _geomWarned = false;
function shellGeometry() {
@@ -283,7 +286,7 @@ function fitChrome() {
const hadMinH = bar ? bar.style.minHeight : '';
if (pinned > 0) bar.style.minHeight = pinned + 'px';
- if (bar) bar.classList.remove('fs-bar-stack', 'fs-ind-compact', 'fs-bar-actrow');
+ if (bar) bar.classList.remove('fs-bar-stack', CLASS_IND_COMPACT, 'fs-bar-actrow');
fitTabStrips();
/* ---- does the main menu fit on the brand's row? ----
* It depends on how many sections THIS router has (stock 5, a loaded box 11), not on the
@@ -300,7 +303,7 @@ function fitChrome() {
/* first step before stacking: collapse the poll pill (~90px) to an icon square and
* re-measure — often enough to keep the menu on the brand's row
* (theme/50-toplayout.css) */
- bar.classList.add('fs-ind-compact');
+ bar.classList.add(CLASS_IND_COMPACT);
fitTabStrips();
if (!stripFitsOneRow(menu)) {
bar.classList.add('fs-bar-stack');
@@ -351,7 +354,7 @@ function fitCluster(bar, menu) {
if (clusterFitsBrandRow(bar, menu))
return;
- bar.classList.add('fs-ind-compact');
+ bar.classList.add(CLASS_IND_COMPACT);
if (clusterFitsBrandRow(bar, menu))
return;
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js
index 4f6b0cf4..3f920d4a 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-overview.js
@@ -20,6 +20,8 @@
/* section title -> grid role. _() with no msgctxt on purpose: these must resolve to exactly what
* luci-mod-status resolves to, or the titles stop matching. Built once, not per poll tick. */
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) {
/* two title markups, one per release: 25.12 wraps the heading (`.cbi-title > h3`), 24.10 emits
@@ -63,7 +65,9 @@ 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') || '') !== 'admin-status-overview') {
+ 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 */
+ 'admin-status-overview') {
stopWatch();
return;
}
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js
index 4d0f4dc1..5d3070bf 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-prefs.js
@@ -80,7 +80,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. */
+/* 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. */
root.setAttribute('data-darkmode', dark ? 'true' : 'false');
root.setAttribute('data-theme', dark ? 'dark' : 'light');
root.setAttribute('data-bs-theme', dark ? 'dark' : 'light');
@@ -137,7 +143,7 @@ function guardDarkStamp() {
check();
new MutationObserver(check).observe(root, {
attributes: true,
- attributeFilter: ['data-darkmode', 'data-theme', 'data-bs-theme']
+ attributeFilter: [ 'data-darkmode', 'data-theme', 'data-bs-theme' ]
});
}
/* ---- the browser's own chrome follows the page ----
diff --git a/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js b/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js
index 01041d72..1dde51f3 100644
--- a/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js
+++ b/luci-theme-footstrap/htdocs/luci-static/resources/fs-search.js
@@ -219,12 +219,18 @@ function build() {
const btn = document.getElementById('fs-search-btn');
if (!btn) return null;
+ /* the list's id, said three times below as an id, a class and aria-controls (measured: 17 B x3
+ * -> 26 B, 25 B saved) */
+ const ID_SEARCH_LIST = 'fs-search-list';
+ /* the attribute name toggled below and stated once more as its own starting value (measured:
+ * 15 B x3 -> 27 B, 18 B saved) */
+ const ATTR_EXPANDED = 'aria-expanded';
const input = E('input', {
'type': 'text',
'class': 'fs-search-input',
'role': 'combobox',
- 'aria-controls': 'fs-search-list',
- 'aria-expanded': 'true',
+ 'aria-controls': ID_SEARCH_LIST,
+ [ATTR_EXPANDED]: 'true',
'aria-autocomplete': 'list',
'aria-label': _('Search pages', 'footstrap'),
'placeholder': _('Search pages…', 'footstrap'),
@@ -232,7 +238,7 @@ function build() {
'autocapitalize': 'off',
'spellcheck': 'false'
});
- const list = E('div', { 'id': 'fs-search-list', 'class': 'fs-search-list', 'role': 'listbox', 'aria-label': _('Pages', 'footstrap') });
+ const list = E('div', { 'id': ID_SEARCH_LIST, 'class': ID_SEARCH_LIST, 'role': 'listbox', 'aria-label': _('Pages', 'footstrap') });
const note = E('div', { 'class': 'fs-search-note' });
const ico = E('span', { 'class': 'fs-search-ico' });
ico.innerHTML = widgets.svgIcon('');
@@ -329,7 +335,7 @@ function build() {
function open() {
if (!ov.hidden) return;
ov.hidden = false;
- btn.setAttribute('aria-expanded', 'true');
+ btn.setAttribute(ATTR_EXPANDED, 'true');
input.value = '';
render('');
input.focus();
@@ -338,7 +344,7 @@ function build() {
function close(returnFocus = true) {
if (ov.hidden) return;
ov.hidden = true;
- btn.setAttribute('aria-expanded', 'false');
+ btn.setAttribute(ATTR_EXPANDED, 'false');
if (returnFocus) btn.focus();
}
diff --git a/luci-theme-footstrap/styles/theme/60-inputs.css b/luci-theme-footstrap/styles/theme/60-inputs.css
index ed452111..0767c7e8 100644
--- a/luci-theme-footstrap/styles/theme/60-inputs.css
+++ b/luci-theme-footstrap/styles/theme/60-inputs.css
@@ -313,17 +313,9 @@
}
/* keyboard/click focus rings on the DRAWN box — the theme's one ring token, replacing
* base's old generic glow; :focus (not :focus-visible) keeps the pre-absorption behaviour
- * of ringing a clicked box */
- input[type="checkbox"]:focus::before,
- input[type="radio"]:focus::before {
- border-color: var(--fs-accent);
- box-shadow: var(--fs-focus-ring);
- }
- input[type="checkbox"].cbi-input-invalid:focus::before,
- input[type="radio"].cbi-input-invalid:focus::before {
- border-color: var(--fs-danger);
- box-shadow: var(--fs-focus-ring-invalid);
- }
+ * of ringing a clicked box. Both states (plain and invalid) are declared once, further
+ * down this file, in the selector lists every other same-body ring joins — see the two
+ * `--fs-focus-ring`/`--fs-focus-ring-invalid` rules below the text-input group. */
input[type="checkbox"]::after,
input[type="radio"]::after {
/* the mark is inset from the DRAWING, and the drawing is centred in the target, so the mark
@@ -423,7 +415,7 @@
* listbox in dark mode. `height: inherit` absorbed from base with the width above — without
* it the fixed box would cap a multi-row listbox at one line. */
select[multiple] { background-color: var(--fs-panel2); color: var(--fs-text); height: inherit; }
- select:focus { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); outline: none; }
+ /* :focus joins the shared accent ring in the text-input group below. */
/* absorbed from base's reset: 100% (= inherit) keeps the option on its select's size, and
* only