mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 01:33:56 +08:00
🐶 Sync 2026-10-20 22:40:23
This commit is contained in:
1 parent
e813f5cc54
commit
64cea2243a
216 files changed
+11913
-2968
No files matched your search
@@ -1 +1,35 @@
|
||||
{% include('themes/footstrap/partials/footer') %}
|
||||
{#
|
||||
Footer body. One layout template and one client menu renderer: sidebar and top bar are the same
|
||||
markup, morphed by :root[data-layout].
|
||||
Licensed to the public under the Apache License 2.0.
|
||||
-#}
|
||||
{% if (!blank_page): %}
|
||||
</div>{# /.fs-content #}
|
||||
|
||||
{#
|
||||
role="contentinfo" is explicit because a <footer> gets it implicitly only when
|
||||
its nearest ancestor is <body>, and this one must stay inside <main>: both
|
||||
layouts lay .fs-main out as a flex column and order the footer within it.
|
||||
The version strings are entityencode()d — /etc/openwrt_release is root-owned,
|
||||
but an unescaped `&` in a distname would still break the markup.
|
||||
#}
|
||||
<footer class="fs-footer" role="contentinfo">
|
||||
<span>
|
||||
Powered by
|
||||
<a href="https://github.com/openwrt/luci" target="_blank" rel="noreferrer">
|
||||
{{ entityencode(version.luciname, true) }} ({{ entityencode(version.luciversion, true) }})</a>
|
||||
/
|
||||
<a href="{{ entityencode(version.disturl ?? '#', true) }}" target="_blank" rel="noreferrer">
|
||||
{{ entityencode(version.distname, true) }} {{ entityencode(version.distversion, true) }} ({{ entityencode(version.distrevision, true) }})</a>
|
||||
{% if (lua_active): %}
|
||||
/ {{ _('Lua compatibility mode active') }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</footer>
|
||||
</main>{# /.fs-main #}
|
||||
</div>{# /.fs-shell #}
|
||||
<script>L.require('menu-footstrap')</script>
|
||||
<script>L.require('fs-select')</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,7 +8,7 @@
|
||||
i18n, and the rule the other templates point at. A msgid is a GLOBAL name: LuCI serves one merged
|
||||
catalogue — load_catalog() loads every *.<lang>.lmo and a lookup returns the first archive holding
|
||||
the hash — so readdir order decides who owns a string, and the Appearance layout toggle rendered
|
||||
"Максимум" on a Russian router because another catalogue translates `Top` as "maximum" (issue #6).
|
||||
"Максимум" on a Russian router because another catalogue translates `Top` as "maximum".
|
||||
Anything left bare is a name anyone may take, and contexting cannot be selective.
|
||||
|
||||
So every Appearance label carries the `footstrap` msgctxt, while the chrome's own strings — Skip
|
||||
@@ -35,7 +35,7 @@
|
||||
/* Every option saveAsDefault() writes, listed once and read back in a loop. A hand-written
|
||||
* literal with one line per axis drops an axis silently: Save to router reports success, the
|
||||
* file on disk is correct, and "Reset to router" lands on the BUILT-IN look because the value
|
||||
* the server hands the client was never there. tools/axes.mjs checks this list against
|
||||
* the server hands the client was never there. This list is checked against
|
||||
* snapshotAxes().
|
||||
*
|
||||
* '' rather than a raw undefined when unset, so head.ut's `!= ''` guards fire: int(undefined)
|
||||
@@ -69,22 +69,10 @@
|
||||
it, so the CI gate cannot see the difference. */
|
||||
fs_defaults.layout = fsd.layout || config.main?.footstrap_layout || '';
|
||||
|
||||
/* Optional companion packages announce themselves in the theme's own config —
|
||||
`uci add_list footstrap.settings.plugin=<module>` from the package's uci-defaults — rather
|
||||
than being named in this tree. The chrome requires each name and knows nothing else about it,
|
||||
so the edge points inwards exactly as fs-router.onNavigate does.
|
||||
|
||||
Whitelisted to the shape of a LuCI module name before it is printed into a <script>: writing
|
||||
that file needs root, but a value that reaches an inline script is checked anyway. */
|
||||
let fs_plugins = fsd.plugin ?? [];
|
||||
if (type(fs_plugins) != 'array')
|
||||
fs_plugins = [ fs_plugins ];
|
||||
fs_plugins = filter(fs_plugins, (m) => type(m) == 'string' && match(m, /^[a-zA-Z0-9._-]{1,48}$/));
|
||||
|
||||
http.prepare_content('text/html; charset=UTF-8');
|
||||
-%}
|
||||
|
||||
{% include('themes/footstrap/partials/head', { boardinfo, fs_defaults, fs_plugins }) %}
|
||||
{% include('themes/footstrap/partials/head', { boardinfo, fs_defaults }) %}
|
||||
|
||||
{#
|
||||
data-page carries the DISPATCH path (ctx.path), not request_path: on a firstchild route
|
||||
@@ -97,6 +85,43 @@
|
||||
#}
|
||||
{% const fs_page = entityencode(join('-', length(ctx.path) ? ctx.path : ctx.request_path), true) %}
|
||||
<body class="lang_{{ dispatcher.lang }}" data-page="{{ fs_page }}">
|
||||
<script>
|
||||
/* Records every node a script parks directly on <body>: a page that does so makes the next
|
||||
* navigation a full load, because the document is spent. fs-router.js's
|
||||
* strayBodyNode()/bodyLittered() read window.__fsBodyAdds. First byte of <body>, before
|
||||
* any chrome element or the dispatched page's own <div id="view">. Own property of this
|
||||
* <body> instance, never Node.prototype. */
|
||||
(() => {
|
||||
const body = document.body;
|
||||
if (!body || body.__fsBodyHooked) return;
|
||||
body.__fsBodyHooked = true;
|
||||
const adds = window.__fsBodyAdds || (window.__fsBodyAdds = []);
|
||||
const names = [ 'appendChild', 'insertBefore', 'replaceChild', 'append', 'prepend',
|
||||
'insertAdjacentElement', 'insertAdjacentHTML' ];
|
||||
names.forEach((name) => {
|
||||
const orig = body[name];
|
||||
if (typeof orig !== 'function') return;
|
||||
try {
|
||||
Object.defineProperty(body, name, {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value() {
|
||||
const before = Array.prototype.slice.call(this.children);
|
||||
const ret = orig.apply(this, arguments);
|
||||
const after = this.children;
|
||||
for (let i = 0; i < after.length; i++) {
|
||||
if (before.indexOf(after[i]) === -1) adds.push(after[i]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
}
|
||||
/* a host that refuses to redefine this one name leaves it unwrapped; the rest
|
||||
* still are */
|
||||
catch (e) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{#
|
||||
The wallpaper pattern's paint surface: 15-wallpaper.css masks this element so an
|
||||
uploaded SVG takes the theme's ink. A mask applies to an element's whole rendering,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
{#
|
||||
Footer body. One layout template and one client menu renderer: sidebar and top bar are the same
|
||||
markup, morphed by :root[data-layout].
|
||||
Licensed to the public under the Apache License 2.0.
|
||||
-#}
|
||||
{% if (!blank_page): %}
|
||||
</div>{# /.fs-content #}
|
||||
|
||||
{#
|
||||
role="contentinfo" is explicit because a <footer> gets it implicitly only when
|
||||
its nearest ancestor is <body>, and this one must stay inside <main>: both
|
||||
layouts lay .fs-main out as a flex column and order the footer within it.
|
||||
The version strings are entityencode()d — /etc/openwrt_release is root-owned,
|
||||
but an unescaped `&` in a distname would still break the markup.
|
||||
#}
|
||||
<footer class="fs-footer" role="contentinfo">
|
||||
<span>
|
||||
Powered by
|
||||
<a href="https://github.com/openwrt/luci" target="_blank" rel="noreferrer">
|
||||
{{ entityencode(version.luciname, true) }} ({{ entityencode(version.luciversion, true) }})</a>
|
||||
/
|
||||
<a href="{{ entityencode(version.disturl ?? '#', true) }}" target="_blank" rel="noreferrer">
|
||||
{{ entityencode(version.distname, true) }} {{ entityencode(version.distversion, true) }} ({{ entityencode(version.distrevision, true) }})</a>
|
||||
{% if (lua_active): %}
|
||||
/ {{ _('Lua compatibility mode active') }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</footer>
|
||||
</main>{# /.fs-main #}
|
||||
</div>{# /.fs-shell #}
|
||||
<script>L.require('menu-footstrap')</script>
|
||||
<script>L.require('fs-select')</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,17 +21,28 @@
|
||||
let _sd_pink = index([ 'theme', 'original' ], fs_defaults.pattern_ink) >= 0 ? fs_defaults.pattern_ink : '';
|
||||
let _sd_ac = index([ 'on', 'off' ], fs_defaults.autocollapse) >= 0 ? fs_defaults.autocollapse : '';
|
||||
let _sd_dens = index([ 'compact', 'normal', 'large' ], fs_defaults.density) >= 0 ? fs_defaults.density : '';
|
||||
/* issue #44: how far the content column may grow past --fs-content-max's 1280px default.
|
||||
/* Six numeric axes share this shape: '' (unset) must map to -1, never to 0, because 0 is a real
|
||||
value for every one of them (sharp corners, a transparent scrim, no tile). */
|
||||
function _sd_range(raw, min, max) {
|
||||
let n = int(raw);
|
||||
return (raw != '' && n >= min && n <= max) ? n : -1;
|
||||
}
|
||||
/* a cache-bust TOKEN (the cgi-upload md5 for an uploaded background/pattern/font sheet), not a
|
||||
path — the served path is fixed. Whitelisted to hex so a hand-edited config cannot inject
|
||||
markup into an inline script that interpolates it. */
|
||||
function _sd_token(v) {
|
||||
return (type(v) == 'string' && match(v, /^[a-f0-9]{6,64}$/)) ? v : '';
|
||||
}
|
||||
/* how far the content column may grow past --fs-content-max's 1280px default.
|
||||
header.ut's FS_AXES carries 'content_width', so a saved router default reaches this. */
|
||||
let _cw = int(fs_defaults.content_width);
|
||||
let _sd_cwidth = (fs_defaults.content_width != '' && _cw >= 1280 && _cw <= 3840) ? _cw : -1;
|
||||
let _sd_cwidth = _sd_range(fs_defaults.content_width, 1280, 3840);
|
||||
/* The five colour axes each hold one of three things: '' or '0' for off, '1'..'360' for a hue,
|
||||
or '#rrggbb'. Both live forms reach the client as a string, and the pre-paint re-reads which
|
||||
it is the way fs-prefs.js's normColor() does, so this passes through exactly those two shapes
|
||||
and turns everything else into ''. The hex charset is whitelisted because the value is
|
||||
interpolated into the inline data block below.
|
||||
(Do not write that block's tag name in a comment: tools/lib/ut-scripts.mjs finds the inline
|
||||
scripts by matching the opening tag, and a mention in prose is one it will match.) */
|
||||
(Do not write that block's tag name in a comment: the inline scripts are found by matching
|
||||
the opening tag, so a mention in prose would match too.) */
|
||||
function _sd_color(v) {
|
||||
if (type(v) == 'string' && match(lc(v), /^#[0-9a-f]{6}$/))
|
||||
return lc(v);
|
||||
@@ -53,32 +64,14 @@
|
||||
let _sd_ctrl = _sd_hex(fs_defaults.control);
|
||||
let _sd_bar = _sd_hex(fs_defaults.bar);
|
||||
let _sd_line = _sd_hex(fs_defaults.line);
|
||||
let _rr = int(fs_defaults.rounding);
|
||||
/* rounding 0 is SHARP corners, a real choice, so '' (unset) must map to -1, not 0 */
|
||||
let _sd_round = (fs_defaults.rounding != '' && _rr >= 0 && _rr <= 20) ? _rr : -1;
|
||||
/* login_bg is the cache-bust TOKEN for the uploaded background (the cgi-upload md5), not the
|
||||
image and not a path — the served path is fixed. Whitelisted to hex so a hand-edited config
|
||||
cannot inject markup into the inline script that builds the url(). */
|
||||
let _lb = fs_defaults.login_bg;
|
||||
let _sd_lbg = (type(_lb) == 'string' && match(_lb, /^[a-f0-9]{6,64}$/)) ? _lb : '';
|
||||
/* the Tint's strength 0-200 (% of the designed chroma). Unset -> -1, like rounding, because 0
|
||||
is a real value. */
|
||||
let _ts = int(fs_defaults.tint_strength);
|
||||
let _sd_tstr = (fs_defaults.tint_strength != '' && _ts >= 0 && _ts <= 200) ? _ts : -1;
|
||||
/* the File photo's scrim opacity 0-100; unset -> -1, 0 being a real value (a transparent
|
||||
scrim) */
|
||||
let _pd = int(fs_defaults.photo_dim);
|
||||
let _sd_pdim = (fs_defaults.photo_dim != '' && _pd >= 0 && _pd <= 100) ? _pd : -1;
|
||||
/* the uploaded pattern's cache-bust token, whitelisted like login_bg and for the same reason;
|
||||
the served path is fixed */
|
||||
let _pt = fs_defaults.pattern;
|
||||
let _sd_pat = (type(_pt) == 'string' && match(_pt, /^[a-f0-9]{6,64}$/)) ? _pt : '';
|
||||
/* the pattern's tile edge in px and the layer's opacity 0-100; unset -> -1, since 0 is a real
|
||||
strength and 0 px is not a tile */
|
||||
let _ps = int(fs_defaults.pattern_size);
|
||||
let _sd_psize = (fs_defaults.pattern_size != '' && _ps >= 40 && _ps <= 1600) ? _ps : -1;
|
||||
let _pst = int(fs_defaults.pattern_strength);
|
||||
let _sd_pstr = (fs_defaults.pattern_strength != '' && _pst >= 0 && _pst <= 100) ? _pst : -1;
|
||||
let _sd_round = _sd_range(fs_defaults.rounding, 0, 20);
|
||||
let _sd_lbg = _sd_token(fs_defaults.login_bg);
|
||||
let _sd_tstr = _sd_range(fs_defaults.tint_strength, 0, 200);
|
||||
let _sd_pdim = _sd_range(fs_defaults.photo_dim, 0, 100);
|
||||
let _sd_pat = _sd_token(fs_defaults.pattern);
|
||||
/* the pattern's tile edge in px and the layer's opacity 0-100 */
|
||||
let _sd_psize = _sd_range(fs_defaults.pattern_size, 40, 1600);
|
||||
let _sd_pstr = _sd_range(fs_defaults.pattern_strength, 0, 100);
|
||||
/* ---- the fonts ----
|
||||
The theme carries no faces, so the two stacks in styles/02-tokens.css are what everyone gets
|
||||
unless the router names something else here. Two independent options, either usable alone:
|
||||
@@ -121,8 +114,7 @@
|
||||
}
|
||||
let _sd_fsans = _sd_family(fs_defaults.font_sans);
|
||||
let _sd_fmono = _sd_family(fs_defaults.font_mono);
|
||||
let _ft = fs_defaults.fonts;
|
||||
let _sd_ftok = (type(_ft) == 'string' && match(_ft, /^[a-f0-9]{6,64}$/)) ? _ft : '';
|
||||
let _sd_ftok = _sd_token(fs_defaults.fonts);
|
||||
/* ---- writing direction ----
|
||||
Nothing in LuCI emits `dir`: the dispatcher stamps `lang` and stops, so a router in Arabic,
|
||||
Persian or Hebrew renders right-to-left text inside a left-to-right document — paragraphs
|
||||
@@ -150,23 +142,23 @@
|
||||
{# The router-wide Appearance defaults, read by both the pre-paint below and fs-prefs.js's
|
||||
current*(), so the Appearance controls show the effective default when this browser has no
|
||||
localStorage. See the sanitiser above for the unset encodings. #}
|
||||
<script>window.__fsPlugins={{ sprintf('%J', fs_plugins) }};</script>
|
||||
<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 }}",content_width:{{ _sd_cwidth }}};</script>
|
||||
<script>
|
||||
/* 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.
|
||||
loader exists — so the contract (keys, attributes, properties,
|
||||
ranges, default, order) is derived from the JS and this template is held 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.
|
||||
interpolations and this one carries none. That is the split the inline-script
|
||||
lint 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. */
|
||||
(() => {
|
||||
const root = document.querySelector(':root'),
|
||||
const root = document.documentElement,
|
||||
sd = (window.__fsSD || {}).darkmode;
|
||||
let saved = null;
|
||||
try { saved = localStorage.getItem('fs-darkmode'); } catch (e) {}
|
||||
@@ -210,7 +202,7 @@
|
||||
applier's lsGet means. The outer try is the last resort for everything else. */
|
||||
const lsGet = (k) => { try { return localStorage.getItem(k); } catch (e) { return null; } };
|
||||
try {
|
||||
const root = document.querySelector(':root'),
|
||||
const root = document.documentElement,
|
||||
sd = window.__fsSD || {};
|
||||
let p = lsGet('fs-palette');
|
||||
/* A stray or retired palette name needs no migration here: it fails the list
|
||||
@@ -234,7 +226,7 @@
|
||||
/* /cgi-bin, not a /www symlink: a plain static SVG is a same-origin document
|
||||
that runs script opened directly (OpenWrt forum thread 251930); this path
|
||||
is the CGI handler that answers with CSP 'none' + sandbox instead — see
|
||||
fs-axes.js's PAT_SERVE, which this string must keep matching. */
|
||||
the serve path of fs-axes.js's PATTERN, which this string must keep matching. */
|
||||
if (pu) root.style.setProperty('--fs-pattern-url', 'url("/cgi-bin/luci-theme-footstrap-pattern?v=' + pu + '")');
|
||||
const psRaw = lsGet('fs-pattern-size');
|
||||
const ps = (psRaw !== null) ? parseInt(psRaw, 10) : sd.pattern_size;
|
||||
@@ -260,7 +252,7 @@
|
||||
if (dn === 'compact' || dn === 'large') root.setAttribute('data-density', dn);
|
||||
else root.removeAttribute('data-density');
|
||||
|
||||
/* Content width (issue #44): how far the column may grow past the 1280px
|
||||
/* Content width: how far the column may grow past the 1280px
|
||||
default, as a px length rather than a step name — the slider is continuous.
|
||||
Written only when it differs from the default, so an untouched theme paints
|
||||
from the bare :root exactly as it did before this axis existed.
|
||||
@@ -351,7 +343,7 @@
|
||||
(() => {
|
||||
try {
|
||||
if (localStorage.getItem('fs-rail') === 'true')
|
||||
document.querySelector(':root').setAttribute('data-rail', 'true');
|
||||
document.documentElement.setAttribute('data-rail', 'true');
|
||||
} catch (e) {}
|
||||
})();
|
||||
|
||||
@@ -362,7 +354,7 @@
|
||||
try {
|
||||
const saved = localStorage.getItem('fs-layout');
|
||||
if (saved === 'top' || saved === 'sidebar')
|
||||
document.querySelector(':root').setAttribute('data-layout', saved);
|
||||
document.documentElement.setAttribute('data-layout', saved);
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
@@ -374,7 +366,7 @@
|
||||
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.
|
||||
mode and every tint axis. The two literals must stay 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. #}
|
||||
@@ -423,7 +415,7 @@
|
||||
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 192 square the
|
||||
manifest also uses down to its own 180. One raster serves both — tools/build-icons.mjs. #}
|
||||
manifest also uses down to its own 180. One raster serves both. #}
|
||||
<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
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<div class="cbi-value cbi-value-last">
|
||||
<label class="cbi-value-title" for="luci_password">{{ _('Password') }}</label>
|
||||
<div class="cbi-value-field">
|
||||
<input class="cbi-input-text" type="password" name="luci_password" id="luci_password" autocomplete="current-password" />
|
||||
<input class="cbi-input-text" type="password" name="luci_password" id="luci_password" autocomplete="current-password" autofocus />
|
||||
</div>
|
||||
</div>
|
||||
{#
|
||||
@@ -177,15 +177,10 @@
|
||||
|
||||
{# The two server values the script below needs, handed over as DATA: a `{{ … }}` block is not JS
|
||||
until the server renders it, so this line carries no logic at all and eslint's .ut processor
|
||||
enforces that (tools/lib/ut-scripts.mjs). The script itself is plain JS and is linted. #}
|
||||
enforces that. The script itself is plain JS and is linted. #}
|
||||
<script>window.__fsHttps={ports:{{ https_ports }},resource:"{{ resource }}"};</script>
|
||||
<script>
|
||||
(() => {
|
||||
const input = document.getElementsByName('luci_password')[0];
|
||||
|
||||
if (input)
|
||||
input.focus();
|
||||
|
||||
/* if the router also serves HTTPS, hop over to it before the password goes over this
|
||||
* connection; the <img> probe is upstream's trick for proving the TLS port answers first
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user