🍉 Sync 2026-08-30 11:16:17

This commit is contained in:
github-actions[bot] committed 2026-08-30 11:16:17 +08:00
1 parent 85339c08e1
commit 84eac136c8
127 files changed
+3726 -86808

No files matched your search

+2 -2
View File
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=43
PKG_RELEASE:=44
endif
LUCI_TITLE:=Footstrap Theme
@@ -30,7 +30,7 @@ LUCI_DEPENDS:=+luci-base
LUCI_PKGARCH:=all
# Else luci.mk defaults these to LuCI's own, and the package claims MAINTAINER "OpenWrt LuCI
# community" / URL openwrt/luci.
LUCI_MAINTAINER:=VizzleTF <vizzletf47@gmail.com>
LUCI_MAINTAINER:=Ivan Kvashonkin <vizzlef@gmail.com>
LUCI_URL:=https://github.com/VizzleTF/luci-theme-footstrap
# CSS: ship verbatim. luci.mk would run csstidy over it, which is old enough to mangle :has(),
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

@@ -9,8 +9,8 @@
"theme_color": "#f6f8fa",
"icons": [
{
"src": "/luci-static/footstrap/app-icon-512.png",
"sizes": "512x512",
"src": "/luci-static/footstrap/app-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
}
@@ -140,6 +140,44 @@ function guardDarkStamp() {
attributeFilter: ['data-darkmode', 'data-theme', 'data-bs-theme']
});
}
/* ---- the browser's own chrome follows the page ----
*
* `<meta name="theme-color">` is what colours a mobile address bar, the Android task-switcher card
* and an installed PWA's title bar. head.ut ships it at the default palette's page colour, which is
* all a static template can know; from here it tracks the live one.
*
* Read from the BODY's computed background, not from `--fs-bg`: a custom property returns its token
* stream, so a palette whose page colour is a color-mix() would put `color-mix(in srgb, …)` in the
* attribute, and 21 of the axes reach the canvas through one mix or another.
*
* One observer rather than a call in each applier: the colour is a function of :root's attributes
* and inline properties — mode, palette, tint, its strength — and every axis already writes exactly
* those. Adding an axis therefore needs nothing here. Coalesced into a frame because the tint and
* strength sliders write on every input event, and getComputedStyle forces style resolution. */
function paintThemeColor() {
const meta = document.querySelector('meta[name="theme-color"]');
if (!meta || !document.body) return;
const bg = getComputedStyle(document.body).backgroundColor;
/* a transparent body means the sheet has not applied yet; keep the server's value */
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') meta.setAttribute('content', bg);
}
function watchThemeColor() {
let queued = false;
const paint = () => {
queued = false;
paintThemeColor();
};
const schedule = () => {
if (queued) return;
queued = true;
window.requestAnimationFrame(paint);
};
paintThemeColor();
new MutationObserver(schedule).observe(document.documentElement,
{ attributes: true, attributeFilter: [ 'style', 'class', 'data-darkmode', 'data-palette', 'data-wallpaper' ] });
}
/* "Auto" means follow the OS continuously, not only at page load. Only while the effective mode is
* auto: an explicit browser choice, or an explicit router default with no browser override. */
_mqDark.addEventListener('change', () => {
@@ -316,7 +354,7 @@ return baseclass.extend({
/* the two axis shapes, so the nineteen axes in fs-axes.js can be built from them */
listAxis, enumAxis,
currentMode, applyMode, modeDefault, guardDarkStamp,
currentMode, applyMode, modeDefault, guardDarkStamp, watchThemeColor,
currentDensity, applyDensity, densityDefault,
currentLayout, applyLayout, isTopLayout,
currentAutoCollapse, applyAutoCollapse, autoCollapseDefault,
@@ -205,6 +205,7 @@ return baseclass.extend({
* below swallows a menu failure. */
sheets.watchViewSheets();
prefs.guardDarkStamp(); /* same, for a third party stamping :root */
prefs.watchThemeColor(); /* the mobile address bar, from the live page colour */
ui.menu.load().then((menu) => {
tree.setTree(menu);
@@ -0,0 +1,79 @@
@layer tokens {
/* Static twins for every token whose value is a color-mix(), for a browser that has none.
*
* Last in the tokens layer on purpose: it has to outrank 02-tokens.css AND the per-palette
* definitions in 03-palettes.css, and directory order is source order (build-css.sh).
*
* Why a whole file rather than a fallback declaration in front of each color-mix(): a custom
* property keeps whatever token stream it is given, so the failure is deferred to the point of USE
* — `background: var(--fs-good-soft)` becomes invalid at computed-value time and computes to
* `unset`, which is `initial` for a non-inherited property. IACVT does not fall back to an earlier
* declaration in the same block, so the two-declaration trick that works for `background:
* color-mix(…)` written literally does nothing for a token. 38 tokens are affected; without this
* block a browser below the floor paints borders in currentcolor and loses every soft surface.
*
* color-mix() lands in Chrome 111, Firefox 113 and Safari 16.2 (all 2022-2023). See docs/css.md
* for the theme's browser floor and which feature sets it.
*
* The rules: a tint over transparent degrades to `transparent` — an absent surface never lowers
* contrast, while a solid version of the same colour under its own text does. A hairline degrades
* to the solid colour it is a fraction of. A focus ring degrades to the -solo ring, which is
* already color-mix-free, because an invisible focus ring is an accessibility regression rather
* than a cosmetic one. */
@supports not (color: color-mix(in srgb, red 50%, transparent)) {
:root {
/* tints: absent, not solid */
--fs-accent-soft: transparent;
--fs-good-soft: transparent;
--fs-warn-soft: transparent;
--fs-danger-soft: transparent;
--fs-accent-fill: transparent;
--fs-good-fill: transparent;
--fs-warn-fill: transparent;
--fs-danger-fill: transparent;
/* hairlines: the solid colour they are a fraction of */
--fs-accent-line: var(--fs-accent);
--fs-good-line: var(--fs-good);
--fs-warn-line: var(--fs-warn);
--fs-danger-line: var(--fs-danger);
--fs-accent-line-hi: var(--fs-accent);
--fs-good-line-hi: var(--fs-good);
--fs-warn-line-hi: var(--fs-warn);
--fs-danger-line-hi: var(--fs-danger);
/* the two rings, restated: -soft and -fill are transparent above, and a ring nobody can
* see is worse than a heavy one. -solo is already color-mix-free and measured. */
--fs-focus-ring: var(--fs-focus-ring-solo);
--fs-focus-ring-invalid: 0 0 0 2px var(--fs-panel), 0 0 0 5px var(--fs-danger);
/* surfaces that are a near-opaque panel anyway */
--fs-glass: var(--fs-panel);
--fs-bar-bg: var(--fs-panel);
--background-color-medium: var(--fs-panel2);
/* depth cues: dropped rather than approximated — each is a 1px highlight whose whole
* purpose is subtlety, and an opaque one reads as a stray line. */
--fs-emboss: none;
--fs-text-emboss: none;
--fs-shadow-bar: 0 1px 0 var(--fs-border);
/* the scrim over an uploaded photo: --fs-photo-dim is a percentage this cannot apply, so
* take the fixed dialog scrim. Losing it entirely would leave login text over a photo. */
--fs-photo-scrim: var(--fs-scrim);
/* export tier — third-party apps read these, so they must resolve to something.
* -medium keeps the hue, -low degrades to the dim text colour it was mixed toward. */
--border-color-high: var(--fs-border);
--border-color-low: var(--fs-border);
--primary-color-medium: var(--fs-accent);
--primary-color-low: var(--fs-dim);
--error-color-medium: var(--fs-danger);
--error-color-low: var(--fs-dim);
--success-color-medium: var(--fs-good);
--success-color-low: var(--fs-dim);
--warn-color-medium: var(--fs-warn);
--warn-color-low: var(--fs-dim);
}
}
}
@@ -519,9 +519,14 @@
* mid-card (measured). Stretched, both cells take the flex line's height and the
* separators meet; content still sits at the top (the cells are blocks). */
/* A `<tfoot>` with no `.tr` inside it IS the row — see the footer block below for which view
* writes which shape. Here rather than in a rule of its own: `css-dup` compares bodies of three
* declarations or more, so a second copy of these two would sit unflagged until they diverged. */
.table.fs-stacked .tr,
* writes which shape.
*
* TWO rules, not one list: a selector list is not forgiving, so a browser without `:has()`
* (Firefox before 121) throws away the WHOLE list, and `.table.fs-stacked .tr` — every card on
* every stacked table — loses `display: flex` with it. Split, the `:has()` half degrades alone.
* The duplicated body is under `css-dup`'s three-declaration floor, so it will not be flagged;
* the two must change together. */
.table.fs-stacked .tr { display: flex; flex-wrap: wrap; }
.table.fs-stacked tfoot:not(:has(> .tr)),
.table.fs-stacked .tfoot:not(:has(> .tr)) { display: flex; flex-wrap: wrap; }
.table.fs-stacked .td {
@@ -148,10 +148,16 @@
localStorage. See the sanitiser above for the unset encodings. #}
<script>window.__fsSD={layout:"{{ _sd_lay }}",darkmode:"{{ _sd_dark }}",palette:"{{ _sd_pal }}",wallpaper:"{{ _sd_wall }}",tint:"{{ _sd_tint }}",accent:"{{ _sd_acc }}",good:"{{ _sd_good }}",warn:"{{ _sd_warn }}",danger:"{{ _sd_dang }}",card:"{{ _sd_card }}",control:"{{ _sd_ctrl }}",bar:"{{ _sd_bar }}",line:"{{ _sd_line }}",rounding:{{ _sd_round }},autocollapse:"{{ _sd_ac }}",login_bg:"{{ _sd_lbg }}",tint_strength:{{ _sd_tstr }},photo_dim:{{ _sd_pdim }},density:"{{ _sd_dens }}",pattern:"{{ _sd_pat }}",pattern_size:{{ _sd_psize }},pattern_strength:{{ _sd_pstr }},pattern_ink:"{{ _sd_pink }}"};</script>
<script>
/* Each block below pre-paints one Appearance axis before the first frame, duplicating a
/* Each IIFE below pre-paints one Appearance axis before the first frame, duplicating a
live applier in fs-prefs.js. They cannot share code — this runs before the module
loader exists — so tools/axes.mjs derives the contract (keys, attributes, properties,
ranges, default, order) from the JS and holds this template to it.
ONE element for all of them, and exactly two in this file: the data blob above carries
interpolations and this one carries none. That is the split tools/lib/ut-scripts.mjs
keys off — an interpolated body is not JS until the server renders it, so it is exempt
from the lint and everything mergeable into it would go unchecked with it. Two is also
what a Content-Security-Policy would need nonces for, should uhttpd ever send one.
Precedence for every axis: localStorage ?? router default (window.__fsSD) ?? built-in.
Dark mode: a saved preference, else the router default, else the OS. */
(() => {
@@ -189,8 +195,7 @@
set(m.matches);
});
})();
</script>
<script>
/* the remaining axes, in the precedence stated above. */
(() => {
/* Guarded per read, not once around the block: a browser that refuses storage throws
@@ -320,8 +325,7 @@
root.style.setProperty('--fs-photo-dim', pd + '%');
} catch (e) {}
})();
</script>
<script>
/* the icon-rail preference, so a reload does not flash the full sidebar first. Not a
router-wide default: a transient chrome collapse, not an appearance choice. */
(() => {
@@ -330,8 +334,7 @@
document.querySelector(':root').setAttribute('data-rail', 'true');
} catch (e) {}
})();
</script>
<script>
/* the client's saved layout overrides the server's stamp above, before paint. Only a
known value is honoured: a corrupt one leaves the stamp standing rather than blanking
the attribute every layout rule matches on. */
@@ -345,6 +348,17 @@
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="darkreader-lock">
{# The colour a mobile browser paints its own chrome with — address bar, task switcher, the
installed app's title bar. Without it a dark page keeps a white bar above it.
The literal is the default palette's page colour and the SAME value manifest.json carries,
for the same reason: neither file can be rendered per request. It is a starting value
only — fs-prefs.js repaints this tag from the live body background, so it follows palette,
mode and every tint axis. tests/theme-color.test.mjs holds the two literals equal.
One tag, no `media` pair: a media-qualified tag would win over this one for a viewer whose
OS is dark but who chose light HERE, and the choice is the theme's to honour. #}
<meta name="theme-color" content="#f6f8fa">
{# The sheet link below is emitted only when the TOKEN says the sheet exists — set the option
after the file, never before. A preload is not in the stylesheet, so dropping the
@font-face rules alone left the pair behind and every page went on asking the router for
@@ -388,9 +402,9 @@
that extension, so the same bytes come back `application/octet-stream` there and with a
JSON type only as `.json`. The map is compiled into uhttpd. #}
<link rel="manifest" href="{{ media }}/manifest.json?v={{ pkgs_update_time }}">
{# iOS reads this link and never the manifest's icons array, and it scales the 512 square the
manifest also uses. One raster serves both — tools/build-icons.mjs. #}
<link rel="apple-touch-icon" href="{{ media }}/app-icon-512.png">
{# iOS reads this link and never the manifest's icons array, and it scales the 192 square the
manifest also uses down to its own 180. One raster serves both — tools/build-icons.mjs. #}
<link rel="apple-touch-icon" href="{{ media }}/app-icon-192.png">
{% if (dispatched?.css): %}
{# The `css` property of the dispatched menu.d node: an app naming a stylesheet the server
should link for its page, instead of the view injecting one at module eval.