<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luci/themes/luci-theme-footstrap/htdocs, branch master</title>
<subtitle>Lua Configuration Interface (mirror)</subtitle>
<id>https://git.openwrt.org/project/luci/atom?h=master</id>
<link rel='self' href='https://git.openwrt.org/project/luci/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/'/>
<updated>2026-08-29T15:49:01Z</updated>
<entry>
<title>luci-theme-footstrap: drop the exports nothing in the tree calls</title>
<updated>2026-08-29T15:49:01Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-26T05:12:55Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=8e3e7618b2efed2fb1d73d25517f13621bab6650'/>
<id>urn:sha1:8e3e7618b2efed2fb1d73d25517f13621bab6650</id>
<content type='text'>
Five module exports were listed for one reason: a gate in the theme's own
repository calls them. `fs-fit.restAt()` tells that repository's scroll sweep
that the theme has taken its reference at the offset it parked at, and
`fs-router`'s contractBreaks/clearViewIntervals/sessionExpired and
`fs-chrome.columnWidth()` do the same for its unit and layout checks.

No code here calls any of them, and none of them is part of what a theme
offers a view. The functions stay where they were -- each is used inside its
own module -- and only the export line goes, so the surface a module presents
is the one this tree actually uses.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
<entry>
<title>luci-theme-footstrap: put the settings tab back after a map redraw</title>
<updated>2026-08-29T15:49:01Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-26T05:12:55Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=ac947ca2d89271dba0590b20799be2d5bb51c94d'/>
<id>urn:sha1:ac947ca2d89271dba0590b20799be2d5bb51c94d</id>
<content type='text'>
Saving on System -&gt; System redraws the map: the old tab group goes, a
new one
is built, and ui.tabs marks it ready by setting an attribute. The theme's
observer watched childList only, so when that attribute landed after the
last
change to the group's children, the mount had already returned with no
group
to attach to and nothing woke it again -- the tab was missing until the
next
navigation.

Reported on 25.12.5 from Chrome and from iOS as "sometimes it disappears
after
Save" (openwrt/luci#8903). Modelled as a deterministic race: remove the
pane,
drop data-initialized, restore it 400ms later with no other mutation. Three
failures out of three before, three passes out of three after, on 25.12 and
24.10. The observer now watches that attribute, and a miss retries on a
widening delay, since the attribute alone would still rely on ui.tabs
setting
it that way.

The sliders on that tab also drop a calcunits option they passed to
ui.RangeSlider: the widget renders that span only when a calculate function
produced a value, and no axis here passes one.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
<entry>
<title>luci-theme-footstrap: fix the login page heading and the graph labels</title>
<updated>2026-08-29T15:49:01Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-26T05:12:54Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=47471e1b4be3efa79c3dfddd213737acc78a6a00'/>
<id>urn:sha1:47471e1b4be3efa79c3dfddd213737acc78a6a00</id>
<content type='text'>
The login page renders with no chrome, so the h1 header.ut prints never
reaches it: the document went out with "Authorization Required" as an h2
and
no h1 at all. Its card heading is the page title and is marked up as
one, at
the h2's size -- an h1's 26px wraps that string onto two lines in a 400px
card, and the size was never what was wrong.

The card also prints the hostname it belongs to, from the same
`ubus system board` call and with the same escaping the chrome's
wordmark and
&lt;title&gt; already use. It discloses nothing new: that string has always
reached
an unauthenticated browser through &lt;title&gt;. Requested as openwrt/luci#8961.

Separately, every &lt;text&gt; that luci-mod-status's realtime .svg files draw
for
an axis carries an inline light grey with a one-pixel black halo, meant for
the black background those files assume rather than for the panel a theme
paints behind them: 1.16:1 on a light palette, where the numbers are
legible
only as the shadow around them. They take the theme's text colour now --
17.9:1 light, 14.7:1 dark, where the halo is what has to go instead.

The selector is a child combinator on purpose. Status -&gt; Channel Analysis
derives a colour per BSSID and paints both the curve and its SSID label
with
it, so there the inline fill is the only thing tying a label to its
curve --
and those labels are built inside a &lt;g&gt;, while the axis labels are direct
children of the &lt;svg&gt;. Verified by planting one label of each shape: the
SSID
label keeps its colour and halo, the axis label reads the theme's text
colour
without one.

The note beside the hostname is corrected in the same pass: it still called
the card's heading an h2, which made its own argument read backwards.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
<entry>
<title>luci-theme-footstrap: fix spacing and hiding on luci-base classes</title>
<updated>2026-08-29T15:49:01Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-26T05:12:53Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=4804fbe8638f12a4e64816bb3315bbea827dff6a'/>
<id>urn:sha1:4804fbe8638f12a4e64816bb3315bbea827dff6a</id>
<content type='text'>
Four things reported on one page each and fixed on the class rather than on
the page they were seen on:

  * a block a view builds itself fused with the card below it. A view may
    return a bare widget where a section is expected -- luci-app-irqbalance
    does, for its /proc/interrupts snapshot -- and nothing gave that
    block the
    gap a section carries. The rule is written against what a map contains:
    everything luci-base puts there and spaces itself is excluded, an empty
    placeholder is left alone, and any other top-level block gets a card's
    16px;
  * a meter's value sat on its own label once its column became a card
    (luci-mod-dashboard's Wireless list on a phone, 6-9px at every
    density);
  * a page-title button row touched the heading above it, and the per-
      section
    Delete button touched the tab bar below it -- every named section, so
    SQM's queues and the firewall's zones alike;
  * the hairline under a value row separates it from the NEXT row, but was
    dropped only on :last-child, so a section that ends its rows and then
    renders a table drew a line across the top of it, cutting through a
    framed
    table's rounded corner. Counted over eight stock pages, separators fall
    only where one was dangling.

And one that is not spacing: el.hidden = true is the only way code that
ships
no stylesheet can hide an element, and the UA rule behind it loses to any
display a theme sets on a class. Measured here on .tr (table-row), .td
(table-cell), ul.nav &gt; li (block), .cbi-page-actions (flex) and .ifacebox
(inline-flex), all of which painted a hidden element anyway. No stock page
trips it today, and the theme's own point fix for the same fault -- a
restated
display:none for the search overlay -- goes away with the general one.
hidden="until-found" is excluded, since display:none would break the
find-in-page reveal it exists for. Four restatements of the attribute go
with
it -- the search overlay's, the Appearance body's, the wallpaper
row/preview/button trio and the search note's -- all of them dead the
moment
the general rule landed, and all of them still reading as the reason those
elements hide.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
<entry>
<title>luci-theme-footstrap: keep the reader's place across a poll</title>
<updated>2026-08-29T15:49:01Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-26T05:12:52Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=d5f7fa1cb796e0f7b3f3224382ecfef3c4581bdc'/>
<id>urn:sha1:d5f7fa1cb796e0f7b3f3224382ecfef3c4581bdc</id>
<content type='text'>
A poll refreshes a section with dom.content(), which empties the container
before it refills it. The document is briefly shorter than the offset the
reader sits at, the engine clamps it, and what happens next is the engine's
business: Chromium lands where it started, WebKit overshoots by 60px on
every
tick. On a live Safari that is the page creeping while you read.

The theme asked CSS.supports('overflow-anchor'), which answers "does this
engine anchor at all" -- a different question every current engine says yes
to. It measures instead: two frames after the mutation the reference it was
already holding is asked where it ended up, and whatever nobody put back is
given back. A synthetic probe was tried and rejected: it calls Firefox
broken,
because a real page puts layout and a frame between the collapse and the
refill.

Three faults sat behind the same symptom, all in which element the
reference
is taken on. elementFromPoint answers with #view in a gap between sections,
and the host's own top never moves; a point above the first section answers
with .fs-content, outside the host; and a page that is one table --
Processes,
Routes, the realtime lists -- has that table as a direct child of #view, so
the climb out of it landed on the host and gave up. The search now walks
the
element stack, steps down the viewport, refuses the host, anchors on the
table
where the climb would reach it, and falls back to the surviving section
when a
tick replaces the element it measured.

Two more things it had to learn. mousedown and keydown were routed
through the
handler that answers "the page is moving", so that answered yes for 400ms
after any click and every keystroke -- and it gates every pass that reads
layout: measured while typing with the window resizing under it, 9 of 10
passes were skipped and landed in one burst, against 0 of 10 now. And the
offset is brought forward after a correction while the reference is not,
since
the write moves the page by exactly the drift measured, leaving the
reference
where it was remembered.

216 runs across three engines, two releases, two layouts, two widths and
three
densities, with the engine's own anchoring on and off: no drift, no mid-
flick
surprise.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
<entry>
<title>luci-theme-footstrap: pack the port status cards into their row</title>
<updated>2026-08-29T15:49:01Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-26T05:12:51Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=1c6d0d3131a80f386de0673c0010fcd2995f95f7'/>
<id>urn:sha1:1c6d0d3131a80f386de0673c0010fcd2995f95f7</id>
<content type='text'>
An 11-port switch drew five 200px cards per row in three rows, with
150px of
empty column beside every one. The wrapper asked for
repeat(auto-fit, minmax(126px, 200px)), and a grid takes its track count
from
the definite max, so the floor never decided anything. With
minmax(var(--fs-port-min), 1fr) the count comes from the floor and 1fr
shares
the remainder: 11 ports on one row, and a short last row as wide as the
rows
above it.

The floor is measured, not guessed. "1024.0 PiB" is the widest string
luci.js's %1024.1mB can produce and those figures are nowrap, so a narrower
card pushes its own numbers past its padding. Stepping a card 1px at a time
until that fits gives 94px at Normal, 80 at Compact and 109 at Large,
and the
floor scales with the density axis.

The card also stops repeating itself: the up/down dot is gone, since the
speed
line already reads "no link" or a rate while the coloured bar above it
means
the interface the port belongs to; the name is cut with an ellipsis and
carries its full text as a native tooltip instead of setting every card's
width; and the traffic figures always take a row of their own, so a linked
port and a dark one read as the same layout.

Their size is declared plainly in the rule and rounded in an @supports
block,
the way 02-tokens.css states the type ladder: an engine without round()
drops
the whole declaration, flag and all, and would inherit a size chosen for
something else.

Their size is a token of its own, --fs-type-2xs: one step below the
ladder's
smallest, rounded in the same @supports block that rounds the rest of
it. The
figures are the widest thing a card must fit whole, so the floor goes
106px at
--fs-type-xs to 94px here -- the difference between ten cards plus a lonely
eleventh and one row of eleven.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
<entry>
<title>luci-theme-footstrap: add the OpenWrt 2020 colourway as a palette</title>
<updated>2026-08-21T04:23:57Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-20T19:36:11Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=46ee3563cdb7aec5cbfc61be3dd212517fe4b70d'/>
<id>urn:sha1:46ee3563cdb7aec5cbfc61be3dd212517fe4b70d</id>
<content type='text'>
luci-theme-openwrt-2020 is one colour scheme and no dark mode: the CI cyan
#00B5E2 on the navy #002B49, white content, and #5CB85C / #CC8800 /
#CC1111 for the semantic three. This adds that identity to footstrap as a
palette, which is the form suggested in the discussion on #8903 for
keeping the look if the theme itself is retired.

The pair splits the work along the reason it exists. Dark IS 2020's own
scheme — the navy is the canvas and the cyan is nearly untouched. Light
keeps the hue and darkens it, because #00B5E2 measures 2.09:1 on white,
and every level of this theme's exported colour tier has to pass WCAG AA
as text on all three surfaces: apps read those names as color: about as
often as background:. The semantic three move for the same reason — on
white 2020's green is 2.28:1 and its amber 3.42:1 — while its red at
5.25:1 is kept where it was.

The palette is registered in the four places the theme requires: the
palette axis in fs-prefs.js, the server-default whitelist and the
pre-paint switch in partials/head.ut, and the label in fs-appearance.js.
It is opt-in through Appearance; nothing changes for anyone who does not
pick it.

Contrast was measured across the whole matrix rather than by eye: 56
palette x mode x tint combinations, 4352 checks, every level clear of AA
on background, panel and panel2, and every ink clear on the fill it sits
on. axe-core reports no serious or critical violations on the widget
gallery in either mode.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
<entry>
<title>luci-theme-footstrap: add new theme</title>
<updated>2026-08-20T15:59:30Z</updated>
<author>
<name>Ivan Kvashonkin</name>
</author>
<published>2026-08-03T11:25:11Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/luci/commit/?id=6f08de768f4ddc3701144680b833773f50252c51'/>
<id>urn:sha1:6f08de768f4ddc3701144680b833773f50252c51</id>
<content type='text'>
A standalone theme for OpenWrt 24.10 and newer. It ships no CSS framework
and depends on nothing but luci-base: the package is the server chrome
(ucode templates), one stylesheet and a set of theme JS modules, while
every page is still rendered by luci-base's own view JS.

One chrome morphs between a collapsible sidebar and a top bar, chosen by
the user and remembered client-side. Light and dark modes, a palette and a
density axis are applied before the first paint, so a reload never flashes
the previous colours. A menu search covers the rendered tree, and a
client-side router swaps the view without a full document load, refusing
to reuse a document an app's own stylesheet has invaded.

Third-party luci-app-* packages are the constraint it is built around: the
chrome is namespaced (fs-*, --fs-*, [data-fs-chrome]) and is defended by
not matching an app's selectors, while .cbi-* and #view stay shared
surfaces an app is entitled to win on. The theme's own settings page is
drawn with stock .cbi-value rows, ui.Select and ui.RangeSlider.

The stylesheet is generated from a layered source kept in the theme's own
repository and committed here as one file, the way the other themes carry
theirs. The theme carries no webfonts: the font stacks name Manrope and
JetBrains Mono first and the system stack after, so a machine with either
installed uses it and one without falls through silently.

A router may name fonts of its own on top of that fallback, through three
UCI options in footstrap.settings that the templates read back: font_sans
and font_mono are font-family stacks printed into an unlayered &lt;style&gt;,
and `fonts` is the md5 of an @font-face sheet the admin installs under
/etc/footstrap/fonts, which uci-defaults links under /www and keep.d
preserves across sysupgrade. The package installs no faces and fetches
nothing at run time; it only serves what is already there.

A web manifest and one app icon ship with the theme, so the interface can
be installed to a phone's home screen. Both are static files — a theme may
not register a dispatcher node — which fixes start_url at uhttpd's default
/cgi-bin/luci/ and the splash colour at the default palette's; the manifest
is served as .json because uhttpd's compiled-in MIME map has no entry for
.webmanifest.

The client router looks up every luci-base surface it calls before it wires
anything, and on a miss logs which name is gone and stays off, leaving the
plain server-dispatched interface rather than a half-working client one.

Where the browser has no scroll anchoring of its own, the theme keeps the
reader's place itself: a poll tick that changes the height of anything
above the reader moves the page under them on WebKit, which never
implemented it. The capability decides, never a browser name, so where the
engine already anchors, this stays out of the way and nothing is corrected
twice. The two page-specific modules are required on the pages that use
them rather than on every page.

Signed-off-by: Ivan Kvashonkin &lt;vizzlef@gmail.com&gt;
</content>
</entry>
</feed>
