From 70e84ddf56e1379b58a92027bcce129ded185b3a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 5 Sep 2026 14:55:36 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=90=20Sync=202026-09-05=2014:55:36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-theme-footstrap/Makefile | 2 +- .../luci-static/resources/fs-appearance.js | 16 ++-- .../htdocs/luci-static/resources/fs-chrome.js | 9 +- .../luci-static/resources/fs-overview.js | 6 +- .../htdocs/luci-static/resources/fs-prefs.js | 8 +- .../htdocs/luci-static/resources/fs-search.js | 16 ++-- .../styles/theme/60-inputs.css | 89 ++++++++++--------- pgyvpn/Makefile | 2 +- 8 files changed, 88 insertions(+), 60 deletions(-) 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