mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 12:53:56 +08:00
💋 Sync 2026-08-21 20:37:56
This commit is contained in:
1 parent
b6a99f1461
commit
f32af3cfd3
19 files changed
+826
-34
No files matched your search
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=31
|
||||
PKG_RELEASE:=32
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# luci-theme-footstrap (the package)
|
||||
|
||||
A LuCI theme for OpenWrt **24.10 and newer** (ucode templates). Installing and using it:
|
||||
A LuCI theme for OpenWrt **23.05 and newer** (ucode templates). Installing and using it:
|
||||
[the repository README](../README.md). Developer documentation: [`../docs/`](../docs/README.md).
|
||||
|
||||
Internal name: `footstrap`. Media path: `/luci-static/footstrap`.
|
||||
|
||||
@@ -124,9 +124,51 @@ function build() {
|
||||
node.addEventListener('widget-change', () => apply(w.getValue()));
|
||||
return node;
|
||||
};
|
||||
/* THE ONE WIDGET THIS THEME CANNOT ASSUME, and the reason 23.05 is a supported release again.
|
||||
*
|
||||
* `ui.RangeSlider` arrived in 24.10. On 23.05 it is simply not there, and because this whole
|
||||
* panel is built inside one try/catch the miss took the ENTIRE Appearance tab with it — a
|
||||
* console line and an empty tab, reported from the field rather than by a gate, because no gate
|
||||
* had ever opened a 23.05 router.
|
||||
*
|
||||
* So the widget is used where it exists and reproduced where it does not: same DOM, same class
|
||||
* names (`.cbi-range-slider` and its value/units children are what theme/60-inputs.css dresses),
|
||||
* same two events, and the same base class — `ui.AbstractElement` is the name LuCI exports on
|
||||
* BOTH 23.05 and master, which is what makes `setUpdateEvents`/`setChangeEvents` available to
|
||||
* the copy. Nothing below this line knows which of the two it got.
|
||||
*
|
||||
* Kept deliberately smaller than upstream's: `calculate` is not reproduced, because no axis on
|
||||
* this page uses it. If one ever does, use the real widget's shape rather than growing this. */
|
||||
const RangeSlider = ui.RangeSlider || ui.AbstractElement.extend({
|
||||
__init__(value, options) {
|
||||
this.value = value;
|
||||
this.options = Object.assign({ min: 0, max: 100, step: 1, calcunits: null }, options);
|
||||
},
|
||||
render() {
|
||||
this.sliderEl = E('input', {
|
||||
type: 'range', min: this.options.min, max: this.options.max,
|
||||
step: this.options.step || 'any', value: this.value
|
||||
});
|
||||
this.valueEl = E('output', { class: 'cbi-range-slider-value' }, String(this.value));
|
||||
const node = E('div', { class: 'cbi-range-slider' }, [
|
||||
this.sliderEl,
|
||||
this.valueEl,
|
||||
this.options.calcunits ? E('span', { class: 'cbi-range-slider-calc-units' }, this.options.calcunits) : null
|
||||
].filter(Boolean));
|
||||
this.node = node;
|
||||
this.setUpdateEvents(this.sliderEl, 'input', 'blur');
|
||||
this.setChangeEvents(this.sliderEl, 'change');
|
||||
this.sliderEl.addEventListener('input', () => { this.valueEl.textContent = this.sliderEl.value; });
|
||||
dom.bindClassInstance(node, this);
|
||||
return node;
|
||||
},
|
||||
getValue() { return this.sliderEl.value; },
|
||||
setValue(value) { this.sliderEl.value = value; this.valueEl.textContent = value; }
|
||||
});
|
||||
|
||||
const sliderCtl = (current, min, max, apply, label, opts) => {
|
||||
const o = opts || {};
|
||||
const w = new ui.RangeSlider(String(current), {
|
||||
const w = new RangeSlider(String(current), {
|
||||
min: min, max: max, step: o.step || 1, calcunits: o.unit || null
|
||||
});
|
||||
const node = w.render();
|
||||
|
||||
@@ -724,6 +724,22 @@
|
||||
stroke: var(--fs-text) !important;
|
||||
}
|
||||
|
||||
/* …and the axis labels beside them, for the same reason and with a worse symptom. Every <text>
|
||||
* in the .svg carries an inline fill of the file's own light grey plus a one-pixel black halo,
|
||||
* drawn for the black background the file assumes rather than for the panel this theme paints
|
||||
* behind it. On a light palette that grey lands on white at 1.16:1 — the numbers are legible
|
||||
* only as the shadow around them; --fs-text measures 17.9:1 there, and 14.7:1 on the dark
|
||||
* panel, where it is the halo that has to go instead. Both come from an inline declaration, so
|
||||
* both need the flag. Reported from the forum.
|
||||
*
|
||||
* The weight is not decoration: the SVG hardcodes a 9pt label, the smallest type this theme
|
||||
* renders anywhere, and at the regular weight it is also the thinnest. */
|
||||
#view div[style] > svg text[style] {
|
||||
fill: var(--fs-text) !important;
|
||||
text-shadow: none !important;
|
||||
font-weight: var(--fs-weight-bold);
|
||||
}
|
||||
|
||||
/* Overview Hide/Show toggle pills: theme/35-alerts.css (.cbi-title .label) +
|
||||
* pages/20-overview.css (chevron variant) — absorbed. */
|
||||
}
|
||||
@@ -28,18 +28,54 @@
|
||||
* a heavy grey cloud under a white card in light mode. */
|
||||
padding: var(--fs-space-6) var(--fs-space-6) var(--fs-space-6); box-shadow: var(--fs-shadow-pop);
|
||||
}
|
||||
/* The router's own name, first line of the card (sysauth.ut) — the shape an appliance console
|
||||
* uses for this (FortiGate prints the hostname on its login page for the same reason): a
|
||||
* CENTRED identity block, set apart from the form, above a left-aligned heading and left-aligned
|
||||
* fields. Centred and separated is what makes the mixed alignment read as deliberate rather than
|
||||
* as a stray line: the 24px below is the card's own padding, so the block sits in a gutter of the
|
||||
* same size as the one around it, and the eye takes it as a header rather than as the heading's
|
||||
* first line.
|
||||
*
|
||||
* Still SMALLER than the heading it stands over (16px against the h2's 20px). It answers
|
||||
* "which box is this", which matters only until you have read it; `Authorization Required`
|
||||
* answers "what now", which is the page's actual subject. Set larger, the two swap roles and the
|
||||
* heading reads as a subtitle.
|
||||
*
|
||||
* Not a heading element, and that is on purpose — WCAG's outline is about the page's SUBJECT,
|
||||
* and a second heading above the h2 would announce the login form as two sections. It is a <p>
|
||||
* in the template.
|
||||
*
|
||||
* The eyebrow tokens are NOT reused, though this is an eyebrow's position: they carry
|
||||
* `text-transform: uppercase`, and a hostname is a name — `Rb5009-Usb` is not what the admin
|
||||
* typed, and case is the only thing telling two similar names apart on a router farm.
|
||||
*
|
||||
* `overflow-wrap: anywhere` because the card is a DEFINITE 400px (above) and a hostname is one
|
||||
* unbreakable token of up to 63 characters — nothing in it is a break opportunity, so without
|
||||
* this a long one runs out of the card instead of wrapping inside it. */
|
||||
.fs-login-host {
|
||||
margin: 0 0 var(--fs-space-6); text-align: center;
|
||||
font-size: var(--fs-type-lg); font-weight: var(--fs-weight-bold); color: var(--fs-dim);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.fs-login .cbi-map,
|
||||
.fs-login .cbi-section,
|
||||
.fs-login .cbi-section-node {
|
||||
background: none; border: 0; padding: 0; margin: 0; box-shadow: none;
|
||||
}
|
||||
/* the login card IS the panel — the generic "heading gets its own card" rule would
|
||||
* draw a second frame inside it */
|
||||
.fs-login .cbi-map > h2,
|
||||
* draw a second frame inside it. `h1` because this page has no title bar to carry the
|
||||
* document's top-level heading (sysauth.ut says why); the card rule in theme/45-misc.css is
|
||||
* written for `.cbi-map > h2` and so does not reach it, but the reset stays listed — the
|
||||
* heading's level is not what should decide whether a second frame appears inside this card. */
|
||||
.fs-login .cbi-map > h1,
|
||||
.fs-login .cbi-map-descr {
|
||||
background: none; border: 0; border-radius: 0; box-shadow: none; padding: 0;
|
||||
}
|
||||
.fs-login .cbi-map > h2 {
|
||||
/* the h2's size, on an h1: the ramp puts an h1 at 26px (theme/45-misc.css) and in a 400px card
|
||||
* that wraps `Authorization Required` onto two lines. The heading was the right SIZE all along
|
||||
* — only its level was wrong. `margin`, because the ramp gives an h1 a bottom margin and this
|
||||
* card sets its own rhythm. */
|
||||
.fs-login .cbi-map > h1 {
|
||||
font-size: var(--fs-type-xl); font-weight: var(--fs-weight-bold); letter-spacing: -.01em; margin: 0 0 var(--fs-space-1-5);
|
||||
}
|
||||
.fs-login .cbi-map-descr { color: var(--fs-dim); font-size: var(--fs-type); margin: 0 0 var(--fs-space-5); }
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
.cbi-section > h3:first-child,
|
||||
.cbi-section > legend { margin-bottom: var(--fs-space-3); }
|
||||
|
||||
/* A SECOND heading inside the same card — the one that follows a table rather than opening the
|
||||
* section — had no air at all: measured 0px above it on Status → Overview, where the stock
|
||||
* includes stack `table + h3 + table` inside one .cbi-section and the heading read as another
|
||||
* row of the table it was captioning. The rule above cannot cover it: `:first-child` is what
|
||||
* excludes the one that opens the card, and that one takes its gap from the wrapper instead.
|
||||
* Reported from the forum. The two values are the ladder's own: a heading is separated from
|
||||
* what came before by more than it is from what it introduces. */
|
||||
.cbi-section .table + h3 { margin-top: var(--fs-space-5); }
|
||||
.cbi-section h3 + .table { margin-top: var(--fs-space-3); }
|
||||
|
||||
/* ---- tables in cards ----
|
||||
* Discriminator: a key/value include (System, Memory) has NO id; a data table has one
|
||||
* (status_leases) or the JS tag .fs-dt. Key/value is not itself a card — no frame, no clip;
|
||||
|
||||
@@ -24,12 +24,45 @@
|
||||
catalogue for.
|
||||
-#}
|
||||
|
||||
{%
|
||||
/* WHICH ROUTER THIS IS — requested as openwrt/luci#8961, by an admin coming from
|
||||
luci-theme-material, which prints the hostname in the top bar and therefore also on its login
|
||||
page. This one had no chrome at all (blank_page below), so the only place the name appeared
|
||||
was the browser TAB.
|
||||
|
||||
A SECOND `ubus.call('system', 'board')`: header.ut makes its own inside its own scope and an
|
||||
include cannot hand a local back, so the choice is one extra ubus round trip on the login
|
||||
render or threading `boardinfo` out through a global. One call, on the one page that renders
|
||||
once per session, is the cheaper of the two.
|
||||
|
||||
It discloses nothing new. The same `boardinfo.hostname` already reaches an unauthenticated
|
||||
browser through <title> (partials/head.ut) — as it does in every stock theme — so this moves
|
||||
a string that was always on the wire into the place a reader looks. */
|
||||
const boardinfo = ubus.call('system', 'board') ?? {};
|
||||
-%}
|
||||
|
||||
{% include('header', { blank_page: true }) %}
|
||||
|
||||
{# The class is what 10-login.css hooks: the form used to be found by
|
||||
`form:has(> .cbi-map input[name="luci_username"])` — 49 characters written 17 times, ~0.8 KB —
|
||||
because the markup was assumed to be stock LuCI's. It is OURS: name the form instead. #}
|
||||
<form method="post" class="fs-login">
|
||||
{#
|
||||
The router's name, above everything else in the card — an admin with three of these open has no
|
||||
other way to tell the tabs apart once the form is focused. `?? 'OpenWrt'` is the fallback
|
||||
partials/head.ut and partials/brand.ut already use, so the line never renders empty; the
|
||||
striptags/entityencode pair is theirs too, and load-bearing: the hostname is admin-controlled
|
||||
text printed to an UNAUTHENTICATED page, and one containing `&` broke entity parsing here
|
||||
before it broke anything else (see the <title> note in partials/head.ut).
|
||||
|
||||
A <p> and NOT a heading: the card's h2 is `Authorization Required` and stays the page's one
|
||||
heading. An outline is about the page's SUBJECT, and a heading above that one would announce
|
||||
the login form as two sections — a screen reader walking the headings would hear the router's
|
||||
name as a section title with the form nested under it. It is printed above the h2 and centred
|
||||
(10-login.css) because that is where a reader looks for "which box is this", not because it
|
||||
outranks anything.
|
||||
-#}
|
||||
<p class="fs-login-host">{{ entityencode(striptags(boardinfo.hostname ?? 'OpenWrt'), true) }}</p>
|
||||
{#
|
||||
TWO INDEPENDENT ALERTS, because neither variable on its own says which of the two dispatcher
|
||||
branches rendered this page — and each of the two spellings in the tree picks one and gets the
|
||||
@@ -67,7 +100,19 @@
|
||||
{% endif %}
|
||||
|
||||
<div class="cbi-map">
|
||||
<h2 name="content">{{ _('Authorization Required') }}</h2>
|
||||
{#
|
||||
An h1, where every other LuCI theme leaves this an h2. The login page has no chrome
|
||||
(blank_page above), so header.ut's `.fs-title-main` h1 never renders here and the document
|
||||
went out with its top-level heading MISSING — the one sentence saying where you are, and
|
||||
the heading a screen reader's `1` shortcut jumps to. Stock's h2 is a copy of a page that
|
||||
does have a title bar above it; this one does not, so the card's heading IS the page title.
|
||||
|
||||
`name="content"` is upstream's legacy anchor and travels with the element, not with its
|
||||
level. The card's own type scale is in 10-login.css: an h1 is 26px in the ramp
|
||||
(theme/45-misc.css) and this one is set back to the h2's 20px, because the SIZE was never
|
||||
what was wrong — only the level.
|
||||
-#}
|
||||
<h1 name="content">{{ _('Authorization Required') }}</h1>
|
||||
<div class="cbi-map-descr">
|
||||
{{ _('Please enter your username and password.') }}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user