💐 Sync 2026-09-05 14:55:36

This commit is contained in:
github-actions[bot]
2026-09-05 14:55:36 +08:00
parent e0826e34a3
commit 70e84ddf56
8 changed files with 88 additions and 60 deletions
+1 -1
View File
@@ -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
@@ -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 */
@@ -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;
@@ -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;
}
@@ -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 ----
@@ -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('<circle cx="11" cy="11" r="7"/><path d="M16.5 16.5 21 21"/>');
@@ -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();
}
+46 -43
View File
@@ -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 <option> needs the 2em leading the other controls carry line-height: normal above */
option { font-size: 100%; line-height: 2em; }
@@ -505,15 +497,8 @@
opacity: var(--fs-disabled-opacity);
pointer-events: none;
}
.cbi-select.focus {
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
}
/* the alarm must survive focus: .focus above would repaint an invalid shell accent */
.cbi-select.cbi-input-invalid.focus {
border-color: var(--fs-danger);
box-shadow: var(--fs-focus-ring-invalid);
}
/* .focus and .cbi-input-invalid.focus join the shared accent/danger ring rules further down
* this file same two bodies as every other :focus/:focus-within ring on this token. */
/* the native <select> inside the shell: invisible, filling it, clearing the plate on the
* right. The inset is a scale step; the right margin is the plate's width, so it stays
* em-relative to the glyph it clears. Every themed-select declaration above (chevron,
@@ -624,26 +609,57 @@
/* bare `input:focus` absorbed from base's generic glow onto the same ring: it is the fallback
* for the types the list does not name and for a typeless <input>; on checkbox/radio it lands
* on the invisible element box UNDER the drawn ::before ring above coincident boxes, one
* visible ring, as the old glow behaved */
* visible ring, as the old glow behaved. `select:focus` and the keyboard-removable dynlist
* chip (ui.js gives it a tabindex) join here rather than repeating the same three
* declarations under their own selector: one shared body instead of three, 149 B off
* cascade.css. */
.cbi-input-text:focus, .cbi-input-password:focus, .cbi-input-textarea:focus,
.cbi-section-create-name:focus,
input:focus,
input[type="text"]:focus, input[type="password"]:focus, input[type="number"]:focus,
input[type="email"]:focus, input[type="url"]:focus, input[type="search"]:focus,
input[type="tel"]:focus, input[type="date"]:focus, input[type="time"]:focus,
textarea:focus {
textarea:focus,
select:focus,
.cbi-dynlist > .item:focus {
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
outline: none;
}
/* The plain accent ring above, restated for four components whose selector cannot join that
* list without changing what it matches (a pseudo-element, a legacy shell class, two
* `:focus-within` containers) same two declarations, one shared body instead of four, 186 B
* off cascade.css. `:is()` around the `:has()` compound keeps the list forgiving: unwrapped, a
* `:has()` compound sharing a selector list with plain ones fails the WHOLE rule below the
* floor (Firefox < 121; docs/css.md "Two shapes fail it"), instead of just losing that one
* selector. */
input[type="checkbox"]:focus::before, input[type="radio"]:focus::before,
.cbi-select.focus,
.cbi-dynlist > .add-item:focus-within,
.control-group:is(:has(> .cbi-input-password)):focus-within {
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
}
/* Invalid state keeps the danger cue. `.cbi-value-error input` is the Lua-CBI path and belongs
* HERE, not in base: base does declare a red border-color for it, but the rule above sets the
* `border` SHORTHAND, and a shorthand in a later layer wipes the longhand out regardless of
* specificity so every third-party app on luci-compat rendered the field in --fs-border grey
* and had lost its field-error cue while the modern .cbi-input-invalid path worked. */
* and had lost its field-error cue while the modern .cbi-input-invalid path worked.
*
* `.cbi-input-invalid:focus` is not redundant with the rule it sits beside: the generic
* `input:focus` above is (0,1,1) and would repaint a focused invalid field accent, and
* `.cbi-input-invalid:focus` at (0,2,0) is what stops it the TYPED `:focus` rules above are
* (0,2,1) and still outrank this, so a typed invalid field focuses accent, exactly as before
* the absorption. The checkbox/radio `::before` ring and the legacy `.cbi-select` shell need
* the identical danger body for the same reason on their own elements one shared rule
* instead of four, 186 B off cascade.css. */
input[type="checkbox"].cbi-input-invalid:focus::before,
input[type="radio"].cbi-input-invalid:focus::before,
.cbi-select.cbi-input-invalid.focus,
.cbi-input-invalid, input.cbi-input-invalid, textarea.cbi-input-invalid,
.cbi-value-error input, .cbi-value-error textarea {
.cbi-value-error input, .cbi-value-error textarea,
.cbi-input-invalid:focus {
border-color: var(--fs-danger);
box-shadow: var(--fs-focus-ring-invalid);
}
@@ -652,13 +668,6 @@
* the input, so half of one state lived in a layer the theme cannot reach. */
.cbi-input-invalid.cbi-dropdown:not(.btn):not(.cbi-button),
.cbi-input-invalid.cbi-dropdown:not([open]) > ul > li { border-color: var(--fs-danger); }
/* the alarm must survive focus: the generic `input:focus` above is (0,1,1) and would repaint
* a focused invalid field accent; (0,2,0) outranks it. The TYPED :focus rules (0,2,1) still
* outrank this a typed invalid field focuses accent, exactly as before the absorption. */
.cbi-input-invalid:focus {
border-color: var(--fs-danger);
box-shadow: var(--fs-focus-ring-invalid);
}
/* readonly keeps its faded border moved from base with the generic box (its border-color
* longhand would lose to the box's `border` shorthand by layer). The doubled [readonly] is
@@ -695,12 +704,8 @@
margin: 0;
transition: var(--fs-field-transition);
}
/* a chip is keyboard-removable (ui.js gives it a tabindex), so it needs the ring */
.cbi-dynlist > .item:focus {
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
outline: none;
}
/* a chip is keyboard-removable (ui.js gives it a tabindex): :focus joins the shared accent
* ring in the text-input group above. */
/* The chip's × plate is a BUTTON in base's paint group (`.item::after, .btn, .cbi-button`)
* absorbed with it, bare as base had it (coverage: an .item outside a dynlist keeps its
* plate). The machinery content/position/edges/align stays in base with the widget. */
@@ -733,7 +738,7 @@
border-radius: var(--fs-radius-sm); overflow: hidden;
transition: var(--fs-field-transition);
}
.cbi-dynlist > .add-item:focus-within { border-color: var(--fs-accent); box-shadow: var(--fs-focus-ring); }
/* :focus-within joins the shared accent ring in the text-input group above. */
.cbi-dynlist > .add-item > input,
/* `width: auto` keeps the flex basis in charge: this input sits INSIDE .cbi-value-field, so the
* elastic width above would otherwise hand it 100% of the field on top of `flex: 1 1 auto`. */
@@ -759,10 +764,8 @@
background: transparent; box-shadow: none;
min-height: var(--fs-ctl-h);
}
.control-group:has(> .cbi-input-password):focus-within {
border-color: var(--fs-accent);
box-shadow: var(--fs-focus-ring);
}
/* :focus-within (wrapped in :is() for the shared list) joins the accent ring in the
* text-input group above. */
/* base pulls .cbi-section-create up 3px to hug a table border, which glues the "Add" button
* to the last row give it a real gap instead. */
.cbi-section-create, .cbi-section-create.cbi-tblsection-create { margin-top: var(--fs-space-3); }
+1 -1
View File
@@ -37,7 +37,7 @@ endif
PKG_NAME:=pgyvpn
PKG_VERSION:=3.1.0
PKG_RELEASE:=2
PKG_RELEASE:=4
PKG_SOURCE:=pgyvpnsvr
PKG_SOURCE_URL:=https://mirrors.oray.com/orayos/packages/$(PKG_NAME)/$(PKG_ARCH_PGYYPN)/$(PKG_VERSION)/bin