mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 12:24:33 +08:00
181 lines
9.9 KiB
Plaintext
181 lines
9.9 KiB
Plaintext
{#
|
|
Copyright 2008 Steven Barth <steven@midlink.org>
|
|
Copyright 2012 David Menting <david@nut-bolt.nl>
|
|
Copyright 2008-2022 Jo-Philipp Wich <jo@mein.io>
|
|
Copyright 2026 footstrap theme
|
|
Licensed to the public under the Apache License 2.0.
|
|
|
|
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).
|
|
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
|
|
to content, Menu, Collapse menu, Search, Log out — are deliberately msgctxt-free, because a bare
|
|
msgid is how a string inherits a translation in the ~40 languages this theme ships no catalogue
|
|
for. Skip to content and Log out are luci-base msgids verbatim; the other three are ours alone and
|
|
stay bare so a future luci-base string can adopt them. Same for the login/notice sentences and for
|
|
System/Memory/Storage in fs-overview.js, which match the stock headings.
|
|
-#}
|
|
|
|
{%
|
|
import { getuid, getspnam } from 'luci.core';
|
|
|
|
const boardinfo = ubus.call('system', 'board') ?? {};
|
|
const empty_password = (getuid() == 0 && getspnam('root')?.pwdp === '');
|
|
/* The router's saved Appearance defaults (/etc/config/footstrap): head.ut stamps them before
|
|
paint and the client's localStorage overrides every one. Absent = the built-in defaults.
|
|
|
|
`uci` and `config` come from the render scope dispatcher.uc builds per request: `uci` is
|
|
already a cursor and `config.main` is already `uci.get_all('luci','main')`. Opening a second
|
|
cursor here would allocate one per page render and re-read a package the dispatcher has
|
|
already loaded. */
|
|
const fsd = uci.get_all('footstrap', 'settings') ?? {};
|
|
/* 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
|
|
* snapshotAxes().
|
|
*
|
|
* '' rather than a raw undefined when unset, so head.ut's `!= ''` guards fire: int(undefined)
|
|
* is 0, a valid value, and an axis whose 0 is a real choice would default to it. */
|
|
const FS_AXES = [
|
|
'layout', 'darkmode', 'palette', 'wallpaper', 'density',
|
|
'tint', 'accent', 'good', 'warn', 'danger',
|
|
'card', 'control', 'bar', 'line',
|
|
'rounding', 'autocollapse', 'tint_strength', 'photo_dim',
|
|
'pattern_size', 'pattern_strength', 'pattern_ink', 'content_width',
|
|
/* the two uploaded images' cache-bust tokens (head.ut whitelists both to hex); the files
|
|
themselves are served. Not axes — they have no browser layer — but they reach the client
|
|
the same way. */
|
|
'login_bg', 'pattern',
|
|
/* the fonts, same shape: no browser layer, no Appearance control, no entry in
|
|
snapshotAxes(). font_sans/font_mono are font-family stacks (head.ut whitelists the
|
|
charset); `fonts` is the cache-bust token of the @font-face sheet the admin installs
|
|
beside the faces. All three are written with `uci set`, never by Save as default. */
|
|
'font_sans', 'font_mono', 'fonts'
|
|
];
|
|
const fs_defaults = {};
|
|
for (let k in FS_AXES)
|
|
fs_defaults[k] = fsd[k] || '';
|
|
/* the one value with a fallback of its own: the legacy migration seed
|
|
luci.main.footstrap_layout=top, which an admin may set to give the router a default top-nav
|
|
theme (a shell script cannot write localStorage).
|
|
|
|
`config.main?.`, not `config.main.`: ucode raises a hard reference error on a property read
|
|
through null, and this template renders every page, so a dispatcher that stopped defaulting
|
|
that section would answer 500 everywhere. `ucode -T -c` compiles the template and never runs
|
|
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 }) %}
|
|
|
|
{#
|
|
data-page carries the DISPATCH path (ctx.path), not request_path: on a firstchild route
|
|
/admin/status renders the overview while request_path is only ['admin','status'], so every
|
|
`body[data-page='admin-status-overview']` rule would silently not apply. The SPA router
|
|
stamps the resolved leaf path, so both ways of arriving agree.
|
|
#}
|
|
<body class="lang_{{ dispatcher.lang }}" data-page="{{ entityencode(join('-', length(ctx.path) ? ctx.path : ctx.request_path), true) }}">
|
|
{#
|
|
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,
|
|
children included, so it needs a layer of its own with nothing in it.
|
|
|
|
Outside the blank_page guard on purpose: the login page renders without the shell, and a
|
|
router whose saved default is Pattern has to show it there too.
|
|
|
|
Inert unless the axis is on — .fs-pattern is display:none by default.
|
|
|
|
data-fs-chrome: zone 1, ours — see the <nav> below.
|
|
#}
|
|
<div class="fs-pattern" data-fs-chrome aria-hidden="true"></div>
|
|
{% if (!blank_page): %}
|
|
{#
|
|
Skip link: jump the menu instead of tabbing through every section on every page.
|
|
Visually hidden until focused (.fs-skip, theme/10-chrome.css).
|
|
data-fs-chrome: zone 1 — see the <nav> below.
|
|
#}
|
|
<a class="fs-skip" data-fs-chrome href="#maincontent">{{ _('Skip to content') }}</a>
|
|
<div class="fs-shell">
|
|
{#
|
|
<nav>, not <aside>: this column IS the site navigation, and an <aside> gets
|
|
role="complementary", leaving the shell with no navigation landmark at all. CSS
|
|
keys off .fs-sidebar.
|
|
|
|
data-fs-chrome marks a zone-1 ROOT: everything from here down is ours, and no
|
|
third-party rule may match it (the fence, fs-sheets.js) or reach it from html/body
|
|
by inheritance (the pin, theme/10-chrome.css). Both key off the attribute, never off
|
|
a class list: the chrome is not one element — the skip link and the search overlay
|
|
are outside this <nav> — so a fence naming `.fs-sidebar` protects the menu and
|
|
leaves them exposed.
|
|
|
|
Roots only: never nest one marker inside another. The pin states inherited
|
|
properties on the marked element, and a direct declaration beats an inherited one
|
|
even when the inherited one is ours — nesting cost .fs-label its nowrap and forced
|
|
text-align from start to left on 302 elements. `npm run chrome-fence` fails on a
|
|
nested marker.
|
|
#}
|
|
<nav class="fs-sidebar" data-fs-chrome aria-label="{{ _('Menu') }}">
|
|
<div class="fs-brandrow">
|
|
{% include('themes/footstrap/partials/brand', { boardinfo }) %}
|
|
<button type="button" class="fs-railtoggle" id="fs-rail-toggle" aria-label="{{ _('Collapse menu') }}" title="{{ _('Collapse menu') }}">
|
|
{% include('themes/footstrap/partials/icon', { body: '<path d="M15 6l-6 6 6 6"/>' }) %}
|
|
</button>
|
|
</div>
|
|
|
|
{# LuCI's poll status ("Refreshing") and other indicators land here by id. #}
|
|
<div id="indicators" class="fs-indicators"></div>
|
|
|
|
<div class="fs-navlabel">{{ _('Menu') }}</div>
|
|
<ul class="nav" id="topmenu"></ul>
|
|
<ul id="modemenu" class="fs-modemenu"></ul>
|
|
|
|
<div class="fs-spacer"></div>
|
|
|
|
{# right cluster, in visual order: the poll pill above, then Search, Log out. The
|
|
Appearance controls are their own page, System -> Footstrap, registered by the
|
|
theme's own menu.d entry (docs/architecture.md) — no tab, no fs-appearance.js
|
|
mounting of any kind reaches this header. #}
|
|
{% include('themes/footstrap/partials/search') %}
|
|
{% include('themes/footstrap/partials/logout') %}
|
|
</nav>
|
|
|
|
<main class="fs-main" id="maincontent" tabindex="-1">
|
|
{# The document's <h1>: LuCI views start at <h2>, so without it the heading outline
|
|
begins mid-way down. It must exist for a screen reader without being seen, which
|
|
`hidden` cannot do — display:none strips the element from the accessibility tree,
|
|
so the SPA router's .fs-title-main sync would update a node no assistive tech can
|
|
read. .fs-sr clips instead, and since a foreign `*` outranks that class with one
|
|
`!important` it is marked zone 1 too. Inside .fs-main, which is not marked: that
|
|
is where app pages render. #}
|
|
<div class="fs-title fs-sr" data-fs-chrome>
|
|
<h1 class="fs-title-main">{{ entityencode(striptags(dispatched?.title ? _(dispatched.title) : (boardinfo.hostname ?? 'OpenWrt')), true) }}</h1>
|
|
</div>
|
|
|
|
{# The SPA router swaps the page with no reload, which a screen reader cannot
|
|
notice: no load event, focus dies with the clicked <a>, the new <title> is not
|
|
announced. The router writes the incoming title here after every nav; empty on a
|
|
full load, which announces itself. #}
|
|
<div id="fs-nav-status" class="fs-sr" data-fs-chrome role="status" aria-live="polite"></div>
|
|
|
|
<div class="fs-content">
|
|
{% include('themes/footstrap/partials/notices', { boardinfo, empty_password }) %}
|
|
{% endif %}
|