🎉 Sync 2026-09-03 10:41:43

This commit is contained in:
github-actions[bot] committed 2026-09-03 10:41:43 +08:00
1 parent 4ee3788dd4
commit ea628602ac
11 files changed
+335 -181

No files matched your search

@@ -221,7 +221,7 @@ function colorControl(current, onPick, label, opts) {
/* Build the whole form. Returns a promise for one element wire() appends to the stock page.
*
* Everything applies immediately and there is nothing to save: every axis is this browser's, in
* localStorage, and the page repaints under the control as it moves. Only "Save as default" writes
* localStorage, and the page repaints under the control as it moves. Only "Save to router" writes
* anything, pushing the current look to the ROUTER for other browsers. That distinction is the
* model (docs/design-system.md), and why this page has no Save/Reset footer of LuCI's own. */
function render() {
@@ -575,17 +575,21 @@ 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 as default', 'footstrap') ]);
const saveBtn = E('button', { 'class': 'btn cbi-button-action', 'type': 'button' }, [ _('Save to router', 'footstrap') ]);
/* Two resets, because two things sit underneath a browser's tweaks (fs-prefs.js): "Reset to
* saved" clears them and lets every axis fall back to whatever the router holds, while "Reset
* to default" writes the theme's built-ins explicitly — the only way to say "as the theme
* ships" on a router with a saved default of its own. Neither touches
* /etc/config/footstrap. */
const resetSavedBtn = E('button', { 'class': 'btn', 'type': 'button' }, [ _('Reset to saved', 'footstrap') ]);
* 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
* ships" on a router that has a look of its own. Neither touches /etc/config/footstrap.
*
* ONE word per state, and "default" is not one of them: it used to name the router's look in
* "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') ]);
/* the stock destructive class, so the button discarding every local tweak is the red one
* (theme/55-buttons.css). "Reset to saved" stays neutral: it steps back to the shared state
* (theme/55-buttons.css). "Reset to router" stays neutral: it steps back to the shared state
* rather than discarding. */
const resetBtn = E('button', { 'class': 'btn cbi-button-negative', 'type': 'button' }, [ _('Reset to default', 'footstrap') ]);
const resetBtn = E('button', { 'class': 'btn cbi-button-negative', 'type': 'button' }, [ _('Reset to built-in', 'footstrap') ]);
/* Save's only visible failure surface. The realistic failure is the rpc rejecting — an expired
* session (403), a missing ACL, ubus down. A DELETED config is not caught: rpcd stages the set
* in the session and commit then no-ops without writing the file, returning success (measured
@@ -603,14 +607,14 @@ function build() {
function refreshSave() {
if (prefs.storageBroken()) {
saveBtn.disabled = false;
saveBtn.textContent = _('Save as default', 'footstrap');
saveBtn.textContent = _('Save to router', 'footstrap');
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 as default', 'footstrap') : _('Save as default', 'footstrap');
saveBtn.textContent = saved ? _('Saved to router', 'footstrap') : _('Save to router', 'footstrap');
}
saveBtn.addEventListener('click', () => {
saveBtn.disabled = true;
@@ -651,8 +655,8 @@ function build() {
location.reload();
});
}
twoClick(resetSavedBtn, _('Reset to saved', 'footstrap'), axes.resetToSaved);
twoClick(resetBtn, _('Reset to default', 'footstrap'), axes.resetToBuiltin);
twoClick(resetSavedBtn, _('Reset to router', 'footstrap'), axes.resetToSaved);
twoClick(resetBtn, _('Reset to built-in', 'footstrap'), axes.resetToBuiltin);
refreshSave(); /* correct label and enabled state before the first paint */
const versionLink = E('a', {
@@ -664,12 +668,31 @@ function build() {
'rel': 'noreferrer'
}, [ ver.label() ]);
/* One clause per button, in LuCI's own help-sentence idiom and under the buttons themselves,
* because that is where the question is asked: with all three named "save"/"reset" and two of
* them resets, the row does not say which state each lands in (forum topic 251930, post 90 —
* "what is the difference on 'reset to saved' and 'reset to default'?"). Three lines rather
* than a paragraph: three parts want a list, and each is read on its own.
*
* Text nodes separated by <br>, NOT a child per line: `.cbi-value-field *` in
* theme/60-inputs.css hands mono to every descendant and excludes `.cbi-value-description`
* itself, so a wrapped line becomes a descendant that the exclusion does not reach — measured,
* all three lines came out monospace. Text nodes inherit the sans face from the container, and
* one container also means one `?` glyph with every line aligned under it. */
const buttonHelp = E('div', { 'class': 'cbi-value-description' }, [
_('Save to router — store this look on the router. Browsers with a look of their own keep it.', 'footstrap'),
E('br'),
_('Reset to router — drop this browser\'s changes and follow the router.', 'footstrap'),
E('br'),
_('Reset to built-in — go back to the look the theme ships with.', 'footstrap')
]);
const defaults = [
/* the one row whose control is a pair of buttons, each named by its own text, so `make`
* ignores the caption rather than re-using it as an aria-label */
group(_('Router default', 'footstrap'),
() => E('div', { 'class': 'fs-ap-actrow' }, [ saveBtn, resetSavedBtn, resetBtn ]),
{ extra: saveErr })
group(_('Saved look', 'footstrap'),
() => E('div', { 'class': 'fs-ap-actrow' }, [ saveBtn, E('span', { 'class': 'fs-ap-actgap', 'aria-hidden': 'true' }), resetSavedBtn, resetBtn ]),
{ extra: [ buttonHelp, saveErr ] })
];
@@ -768,8 +791,8 @@ function build() {
* the misreading happens before anyone scrolls to Defaults. A bare `.alert-message`:
* theme/35-alerts.css keeps the tinted variants for a STATUS and a flat panel for a note. */
const note = E('div', { 'class': 'alert-message fs-ap-note' }, [
E('p', {}, [ _('Changes apply at once and live in this browser.', 'footstrap') ]),
E('p', {}, [ _('To keep them on the router, use Save as default under Defaults — not Save & Apply at the foot of the page.', 'footstrap') ])
E('p', {}, [ _('Footstrap theme settings apply at once and are stored permanently in this browser.', 'footstrap') ]),
E('p', {}, [ _('Save & Apply at the foot of the page belongs to the System form and does not store them.', 'footstrap') ])
]);
/* Colours and Surfaces are one fold: the same job, split into two headings only because a
@@ -781,7 +804,7 @@ function build() {
colours.concat([ E('div', { 'class': 'fs-ap-head fs-ap-sub' }, [ E('h4', {}, [ _('Surfaces', 'footstrap') ]) ]) ], surfaces),
'fs-ui-colours'),
foldable(_('Background', 'footstrap'), wallpaper, 'fs-ui-background'),
section(_('Defaults', 'footstrap'), defaults)
section(_('Saving', 'footstrap'), defaults)
]);
/* The first fill, deferred one microtask so the tree above is finished. It does not wait for
@@ -378,7 +378,7 @@ function resetToBuiltin() {
* 0, which is "the palette's own".
*
* Stated as calls rather than carried in AXES: a fourth column of thunks measured 297 B against
* this list, and a wrong value here is what the Save button's "Reset to default" shows on the
* this list, and a wrong value here is what the Save button's "Reset to built-in" shows on the
* first click — the one state no static gate can see and the live check does. */
prefs.applyLayout('top');
prefs.applyMode('auto');
@@ -12,7 +12,7 @@
*
* ---- three layers, and the browser always wins ----
* Every axis resolves as localStorage ?? router-default ?? built-in. The router default is
* Appearance -> Save as default (written to /etc/config/footstrap, read back into window.__fsSD);
* Appearance -> Save to router (written to /etc/config/footstrap, read back into window.__fsSD);
* the built-in is a bare :root. A new browser inherits the router default; this browser's own
* choice overrides it in either direction.
*
@@ -24,7 +24,7 @@
/* A browser can refuse storage outright (blocked cookies, dom.storage.enabled=false, a partitioned
* WebView) and then every access throws. The helpers below swallow it, because an axis that cannot
* be remembered must still APPLY, but they record it: otherwise current*() reads null, falls back
* to the router default, and the Save button sits disabled reading "Saved as default" over a page
* to the router's look, and the Save button sits disabled reading "Saved to router" over a page
* painted in axes the router default does not carry. */
let _lsBroken = false;
function storageBroken() { return _lsBroken; }
@@ -331,7 +331,7 @@ function currentRail() {
return document.documentElement.getAttribute('data-rail') === 'true';
}
/* ---- Save as default: write the current effective axes to /etc/config/footstrap ----
/* ---- Save to router: write the current effective axes to /etc/config/footstrap ----
* The scoped rpcd ACL (config 'footstrap' only) lets the admin's session set and commit those
* options; rpcd validates the config/section/option names, so no value reaches a shell. The server
* reads them back on the next load and head.ut's sanitiser clamps each before it becomes